C++ 网络编程入门:TCP 协议下的简易计算器项目

C++ 网络编程入门:TCP 协议下的简易计算器项目

个人主页:chian-ocean

文章专栏-Linux

网络编程入门:TCP 协议下的简易计算器项目

前言:

根据前面的经验:网络套接字,序列化与反序列化,守护进程等等,写出一个小项目。
在这里插入图片描述

文件组成

.vscode/ log.hpp // 记录日志的头文件,用于定义日志功能的类和函数。 Makefile // 项目的构建文件,定义如何使用 make 编译、链接代码。 Protocol.hpp // 定义通信协议的头文件,包含数据格式、消息结构、序列化与反序列化规则等。 ServerCal.hpp // 服务器计算相关的头文件,包含服务器端任务计算、数据处理等的声明。 Socket.hpp // 定义套接字操作的头文件,包含与网络连接、数据传输等相关的类和函数。 TcpClient/// 存放与 TCP 客户端相关的文件夹 TcpClient.cc // TCP 客户端实现源文件,处理与服务器的连接、数据发送和接收。 TcpClient.hpp // TCP 客户端头文件,声明客户端与服务器进行交互的类和方法。 TcpServer/// 存放与 TCP 服务器相关的文件夹 TcpServer.cc // TCP 服务器实现源文件,处理客户端的连接、接收和处理数据。 TcpServer.hpp // TCP 服务器头文件,声明用于启动和管理服务器端的类和函数。 

TCP服务端

TcpServer.hpp

#include<functional>#include"Socket.hpp"// 引入套接字操作相关的类#include"ServerCal.hpp"// 引入服务器端计算相关的类// 定义回调函数类型,接受一个字符串引用并返回一个字符串using func_t = std::function<std::string(std::string& package)>;// 定义 TcpServer 类classTcpServer{public:// 构造函数,接受端口号和回调函数作为参数TcpServer(uint16_t port, func_t callback):port_(port),callback_(callback){}// 初始化函数,设置监听套接字voidInit(){ listensock_.Socket();// 创建套接字 listensock_.Bind(port_);// 绑定端口 listensock_.Listen();// 开始监听lg(INFO,"Server Init");// 输出日志,表示服务器已初始化}// 启动服务器函数,开始接收客户端连接voidStart(){while(true){ std::string ip;uint16_t port;// 接受客户端连接,返回客户端的文件描述符int fd = listensock_.Accept(&ip,&port);lg(INFO,"Server Accept");// 输出日志,表示有客户端连接// 创建子进程处理客户端请求if(fork()==0){lg(INFO,"fork success");// 子进程成功创建 listensock_.Close();// 子进程关闭监听套接字 std::string inbuffer_stream;// 缓存接收的数据while(true){char buffer[1024];// 从客户端读取数据int n =read(fd,&buffer,sizeof(buffer));if(n >0){lg(INFO,"read success");// 输出日志,表示读取成功 buffer[n]=0;// 添加字符串结束符 inbuffer_stream += buffer;// 将读取的内容添加到缓冲区while(true){// 调用回调函数处理接收到的包,并返回响应信息 std::string info =callback_(inbuffer_stream);if(info.empty())break;// 如果回调返回空字符串,退出循环// 将处理后的数据写回客户端write(fd, info.c_str(), info.size());}}elseif(n ==0)break;// 如果读取到 0,表示客户端断开连接elsebreak;// 读取失败,退出循环}exit(0);// 子进程结束}// 关闭客户端连接的文件描述符,在父进程中继续等待下一个连接close(fd);}}private:uint16_t port_;// 服务器端口号 Sock listensock_;// 监听套接字对象 func_t callback_;// 回调函数,用于处理客户端发送的数据};

代码说明:

  • 构造函数 TcpServer(uint16_t port, func_t callback)
    • 初始化服务器的端口和回调函数。回调函数 callback_ 将在收到客户端请求时被调用,用于处理数据。
  • Init()
    • 该方法用于初始化服务器,创建套接字、绑定端口并开始监听客户端连接。
  • Start()
    • 该方法是服务器的主循环,用于接受客户端的连接请求。
    • 每当有新的客户端连接时,服务器会通过 fork() 创建子进程来处理该客户端的请求。
    • 在子进程中,读取客户端发送的数据,并通过回调函数处理数据。处理结果将通过套接字返回给客户端。
    • 子进程处理完毕后会退出,而父进程继续等待新的客户端连接。
  • Sock listensock_
    • Sock 类的对象,用于处理底层套接字操作,包括创建、绑定、监听、接收连接等。
  • 回调函数 func_t callback_
    • 回调函数类型,负责处理客户端发来的数据包,并返回处理后的结果。

TcpServer.cc

#include<iostream>#include<string>#include<functional>#include"log.hpp"// 引入日志记录功能#include"TcpServer.hpp"// 引入 TCP 服务器类#include"Protocal.hpp"// 引入协议相关类(用于数据格式、协议处理等)// 测试请求函数的声明voidResquestTest();// 测试响应函数的声明voidResponseTest();// 用法说明函数,输出程序的使用方式voidUsage(){ std::cout <<"\n\r"<<"[Usage]:prot"<<"\n"<< std::endl;}// 主程序入口函数intmain(int argc,char* argv[]){// 检查命令行参数的数量是否正确if(argc !=2){// 如果参数数量不为2,则调用Usage函数输出使用方法Usage();return-1;// 返回错误代码}// 输出服务器启动的日志lg(INFO,"Server start"); ServerCal cal;// 创建一个 ServerCal 对象,用于计算请求数据// 获取命令行参数中的端口号,并将其转换为整数 std::string port_1 = argv[1];uint16_t port = std::stoi(port_1);// 将字符串端口号转换为无符号短整型// 创建一个 TcpServer 对象,绑定回调函数// 使用 std::bind 绑定 ServerCal::Calculator 函数与 ServerCal 对象,作为回调函数 TcpServer* tsur =newTcpServer(port, std::bind(&ServerCal::Calculator,&cal, std::placeholders::_1));// 初始化服务器 tsur->Init();// 启动服务器 tsur->Start();return0;// 程序执行成功,返回 0}

代码说明:

  • 头文件引入
    • #include "log.hpp":用于记录日志,lg(INFO, "message") 可以在程序中输出日志信息。
    • #include "TcpServer.hpp":引入定义 TcpServer 类的头文件,用于创建 TCP 服务器。
    • #include "Protocal.hpp":引入协议相关的头文件,可能包含协议的定义和数据格式的处理方法。
  • Usage() 函数
    • 该函数在命令行参数不正确时,输出程序的使用方式,提示用户如何正确传递命令行参数。
  • main() 函数
    • 程序的入口点,首先检查命令行参数的数量。如果参数不正确,则调用 Usage() 输出帮助信息,并返回错误。
    • 如果参数正确,程序继续执行:
      • 创建 ServerCal 对象 cal,该对象负责计算接收到的数据。
      • 通过 argv[1] 获取并转换端口号。
      • 创建 TcpServer 对象 tsur,并将 ServerCal::Calculator 函数作为回调函数绑定到 TcpServer 中。此回调函数会处理客户端请求的数据。
      • 调用 tsur->Init() 初始化服务器,tsur->Start() 启动服务器,开始监听和处理客户端连接。

TCP客户端

#include<iostream>// 引入标准输入输出流,用于控制台输出#include<unistd.h>// 引入UNIX标准库,提供sleep函数和系统调用等功能#include<time.h>// 引入时间相关的库,用于生成随机数种子#include<string>// 引入字符串类#include"log.hpp"// 引入日志记录功能#include"Socket.hpp"// 引入套接字操作相关的类#include"Protocal.hpp"// 引入协议相关类(用于数据序列化、反序列化、编码、解码等)// 用法说明函数,输出程序的使用方法voidUsage(){ std::cout <<"\n\r"<<"[Usage]:port"<<"\n"<< std::endl;}// 主程序入口intmain(int argc,char* argv[]){// 检查命令行参数的数量是否正确if(argc !=3){Usage();// 如果参数数量不为3,调用Usage函数输出使用方法return-1;// 返回错误代码}srand(time(nullptr));// 使用当前时间作为随机数生成的种子 Sock sock;// 创建一个套接字对象 sock.Socket();// 初始化套接字// 获取命令行参数中的IP地址和端口号 std::string ip = argv[1]; std::string port_1 = argv[2]; std::cout <<"port: "<< port_1 << std::endl;// 输出连接的端口号// 将端口号从字符串转换为无符号短整型uint16_t port = std::stoi(port_1);// 连接到服务器 sock.Connect(ip, port);lg(INFO,"connect successful");// 输出连接成功的日志信息 std::string op ="+-*/%";// 运算符集合int cnt =1;// 计算请求的次数// 持续进行请求while(true){ cnt++;// 创建一个请求对象,生成随机数据 Requset req; req.x_ =rand()%100+1;// 随机生成1到100之间的整数作为x req.y_ =rand()%100;// 随机生成0到99之间的整数作为y req.op_ = op[rand()% op.size()];// 随机从运算符集合中选择一个运算符 std::string con;// 序列化后的字符串 std::string out_stream;// 编码后的数据流// 序列化请求对象bool r = req.Serialize(&con);if(!r)lg(WARNING,"Serialize failure");// 如果序列化失败,输出警告日志 out_stream =Encode(con);// 对序列化后的数据进行编码// 输出请求的详细信息 std::cout <<"============"<<" The "<< cnt <<" Request "<<"============"<< std::endl; std::cout <<"x: "<< req.x_ << std::endl; std::cout <<"op: "<< req.op_ << std::endl; std::cout <<"y: "<< req.y_ << std::endl;// 发送请求数据给服务器write(sock.GetFd(), out_stream.c_str(), out_stream.size());// 从服务器接收响应数据 std::string in_stream;char inbuffer[1024];int n =read(sock.GetFd(),&inbuffer,sizeof(inbuffer));if(n >0){ inbuffer[n]=0;// 添加字符串结束符} in_stream += inbuffer;// 将接收到的数据追加到输入流中 std::string bon;// 解码后的数据 Response resp;// 响应对象Decode(in_stream,&bon);// 解码接收到的数据 resp.Deserialize(bon);// 反序列化响应数据 resp.Print();// 打印响应结果sleep(1);// 每次请求之间暂停1秒钟 std::cout <<"========================================"<< std::endl;// 输出分隔线}return0;// 程序结束}

代码说明:

  • Usage()
    • 用于输出程序的使用说明,提示用户需要传递端口号作为参数。
  • main()
    • 主程序入口,首先检查命令行参数是否正确。如果参数不正确,调用 Usage() 输出帮助信息。
    • 使用 srand(time(nullptr)) 设置随机数种子,确保每次运行时生成不同的随机数。
    • 创建一个 Sock 对象,初始化并连接到指定的 IP 地址和端口号。
    • while 循环中,程序持续进行请求:
      • 生成随机的请求数据,包括 x_y_ 和运算符 op_
      • 使用 Serialize() 方法将请求数据序列化,调用 Encode() 方法进行编码。
      • 发送请求数据到服务器。
      • 接收来自服务器的响应数据,使用 Decode() 进行解码,并使用 Deserialize() 方法将响应数据反序列化。
      • 最后,输出计算结果。
  • 日志记录
    • 使用 lg(INFO, "message") 输出程序的日志信息,帮助调试和跟踪程序的状态。

计算器

#pragmaonce#include<iostream>#include"log.hpp"// 引入日志功能,用于输出日志#include"Protocal.hpp"// 引入协议相关的类和功能// 定义错误符号的枚举类型enumerr_symbol{ div_zero =1,// 除法为零错误 mod_zero // 取余为零错误};// 服务器计算类classServerCal{public:// 构造函数,初始化 ServerCal 对象ServerCal(){}// 计算函数,根据传入的请求计算结果 Response Calculatate(Requset &req){ Response resp(0,0);// 创建一个默认响应对象,初始值为 0// 根据请求的运算符执行相应的数学运算switch(req.op_){case'+':// 加法 resp.result_ = req.Getx()+ req.Gety();break;case'-':// 减法 resp.result_ = req.Getx()- req.Gety();break;case'*':// 乘法 resp.result_ = req.Getx()* req.Gety();break;case'/':// 除法if(req.Gety()==0)// 检查除数是否为零{ resp.code_ = div_zero;// 如果除数为零,设置错误代码为 `div_zero`break;} resp.result_ = req.Getx()/ req.Gety();// 执行除法运算break;case'%':// 取余if(req.Gety()==0)// 检查除数是否为零{ resp.code_ = mod_zero;// 如果除数为零,设置错误代码为 `mod_zero`break;} resp.result_ = req.Getx()% req.Gety();// 执行取余运算break;default:break;// 如果运算符不匹配,直接退出}return resp;// 返回计算结果}// 计算器函数,处理客户端发送的请求数据包 std::string Calculator(std::string &package){// 移除报头部分,解码数据包 std::string context;bool rDecode =Decode(package,&context);// 解码包头if(!rDecode)return"";// 解码失败时返回空字符串// 反序列化请求数据 Requset req; req.Deserialize(context);// 根据请求数据进行计算 Response resp; resp =Calculatate(req);sleep(3);// 模拟计算延时(可能用于测试)// 序列化计算结果并返回 std::string in;bool r = resp.Serialize(&in);if(!r)return"";// 如果序列化失败,返回空字符串// 编码处理后的结果 context =""; context =Encode(in);// 编码计算结果并准备返回return context;// 返回最终的编码结果}// 析构函数,销毁 ServerCal 对象~ServerCal(){}};

代码说明:

1. ServerCal
  • ServerCal 类包含了处理数学计算的逻辑,通过接收客户端发送的请求并计算结果,然后返回计算结果给客户端。
2. Calculatate(Requset &req) 函数
  • 该函数接收一个 Requset 对象,表示客户端发送的请求。
  • 根据请求的操作符(op_),执行相应的数学运算(加、减、乘、除、取余)。如果请求中存在除数为零的情况(除法和取余),则返回相应的错误代码(div_zeromod_zero)。
  • 最终返回一个 Response 对象,包含计算结果。
3. Calculator(std::string &package) 函数
  • 该函数接收客户端发送的包含请求数据的字符串 package
  • 首先解码数据包头部,去除冗余信息并获取有效数据。
  • 然后将解码后的数据反序列化为 Requset 对象。
  • 使用 Calculatate() 方法进行实际的计算。
  • 计算结果通过 Serialize() 序列化,之后进行编码。
  • 返回最终的结果给客户端。

请求和响应服务

#pragmaonce#include<iostream>#include<string>#include<cstring>#include<jsoncpp/json/json.h>// 引入JSON库,用于数据的序列化和反序列化#include"log.hpp"// 引入日志功能// 定义协议分隔符常量const std::string space_sep =" ";// 空格分隔符const std::string protocal_sep ="\n";// 协议分隔符(换行符)// 编码函数:将数据进行编码,格式为 "len/n x + y/n" std::string Encode(std::string &context){ std::string s = std::to_string(context.size());// 获取数据长度并转换为字符串 s += protocal_sep;// 添加协议分隔符 s += context;// 添加实际数据内容 s += protocal_sep;// 添加协议分隔符return s;// 返回编码后的数据}// 解码函数:将协议数据解码,移除头部长度并提取有效数据boolDecode(std::string &package, std::string *context){auto pos = package.find(protocal_sep);// 查找协议分隔符位置if(pos == std::string::npos)returnfalse;// 如果没有找到协议分隔符,返回解码失败 std::string len_str = package.substr(0, pos);// 获取数据长度部分 std::size_t len = std::stoi(len_str);// 将长度字符串转换为整数int total_len = len + len_str.size()+2;// 总长度 = 数据长度 + 协议头的长度(包括分隔符)if(package.size()< total_len)// 检查数据包是否完整returnfalse;*context = package.substr(pos +1, len);// 提取有效数据// 移除解码数据,剩余部分为下一个包的内容 package.erase(0, total_len);returntrue;// 解码成功}// 请求类(Client Request)classRequset{public:// 构造函数,初始化请求数据Requset(int data1,char op,int data2):x_(data1),op_(op),y_(data2){}Requset()// 默认构造函数{}~Requset()// 析构函数{}voidPrint()// 打印请求内容{ std::cout << x_ << op_ << y_ << std::endl;}// 序列化函数:将请求数据(x, op, y)序列化为字符串boolSerialize(std::string *out){#ifdefMyself// 在Myself模式下:直接构建字符串格式 "x op y" std::string s = std::to_string(x_); s += space_sep; s += op_; s += space_sep; s += std::to_string(y_);*out = s;// 返回序列化后的数据returntrue;#else// 否则使用JSON格式序列化 Json::Value root; root["x"]= x_; root["y"]= y_; root["op"]= op_; Json::FastWriter w;*out = w.write(root);// 返回序列化后的JSON字符串returntrue;#endif}// 反序列化函数:从字符串中提取数据(x, op, y)boolDeserialize(const std::string &in){#ifdefMyself// 在Myself模式下:解析有效载荷auto pos1 = in.find(space_sep);if(pos1 == std::string::npos)returnfalse;// 如果没有找到空格分隔符,返回失败 std::string part_x = in.substr(0, pos1);// 提取x部分auto pos2 = in.rfind(space_sep); std::string oper = in.substr(pos1 +1, pos2);// 提取操作符部分 std::string part_y = in.substr(pos2 +1);// 提取y部分if(pos2 != pos1 +2)// 如果格式不正确,返回失败returnfalse; op_ = in[pos1 +1];// 设置操作符 x_ = std::stoi(part_x);// 转换x为整数 y_ = std::stoi(part_y);// 转换y为整数returntrue;#else// 否则使用JSON格式反序列化 Json::Value root; Json::Reader r; r.parse(in, root);// 解析JSON字符串 x_ = root["x"].asInt();// 提取x y_ = root["y"].asInt();// 提取y op_ = root["op"].asString()[0];// 提取操作符(假设只有一个字符)returntrue;#endif}intGetx(){return x_;}// 获取x值intGety(){return y_;}// 获取y值charGetop(){return op_;}// 获取操作符private:int x_;// 操作数xchar op_;// 运算符int y_;// 操作数y};// 响应类(Server Response)classResponse{public:Response(int ret,int code):result_(ret),code_(code){}Response(){}~Response(){}// 序列化函数:将响应数据(result_, code_)序列化为字符串boolSerialize(std::string *out){#ifdefMyself// 在Myself模式下:构建字符串格式 "result code" std::string s = std::to_string(result_); s += space_sep; s += std::to_string(code_);*out = s;returntrue;#else// 否则使用JSON格式序列化 Json::Value root; root["result"]= result_; root["code"]= code_; Json::FastWriter w;*out = w.write(root);// 返回序列化后的JSON字符串returntrue;#endif}// 反序列化函数:从字符串中提取响应数据(result_, code_)boolDeserialize(const std::string &in){#ifdefMyselfauto pos = in.find(space_sep); std::string res = in.substr(0, pos); std::string code = in.substr(pos +1);if(pos != in.rfind(space_sep))// 如果没有找到正确的分隔符,返回失败returnfalse; result_ = std::stoi(res);// 转换result_为整数 code_ = std::stoi(code);// 转换code_为整数returntrue;#else// 否则使用JSON格式反序列化 Json::Value root; Json::Reader r; r.parse(in, root);// 解析JSON字符串 result_ = root["result"].asInt();// 提取result_ code_ = root["code"].asInt();// 提取code_returntrue;#endif}voidPrint()// 打印响应内容{ std::cout <<"result_: "<< result_ <<" code_: "<< code_ << std::endl;}private:int result_;// 计算结果int code_;// 错误代码(例如除数为零等错误)};

代码总结:

1. EncodeDecode
  • Encode:将字符串的大小和内容打包为带有协议头的格式,便于传输。
  • Decode:从带有协议头的包中提取有效数据,移除头部信息并返回有效部分。
2. Requset
  • 表示客户端的计算请求,包括 x_op_y_ 三个字段(操作数和运算符)。
  • SerializeDeserialize 用于数据的序列化和反序列化。
  • 提供 GetxGetyGetop 获取请求参数的函数。
3. Response
  • 表示服务器的响应,包括计算结果 result_ 和错误代码 code_
  • SerializeDeserialize 用于数据的序列化和反序列化。
  • 提供 Print 方法输出响应内容。

网络组件

#pragmaonce#include<iostream>#include<string>#include<cstring>#include<strings.h>// 用于处理字节操作#include<sys/types.h>// 引入系统数据类型#include<sys/socket.h>// 引入套接字API#include<arpa/inet.h>// 引入IP地址相关函数#include<netinet/in.h>// 引入IPv4协议结构和常量#include"log.hpp"// 引入日志记录功能int backlog =10;// 定义监听队列的最大连接数// 定义错误枚举,表示不同的错误类型enumerr{ Socketerr =1,// 套接字创建失败 Bindeterr,// 套接字绑定失败 Listeneterr,// 监听失败 Accepteterr,// 接受连接失败};// 套接字类,封装了套接字的创建、绑定、监听、连接和关闭等操作classSock{public:// 默认构造函数Sock(){}// 析构函数~Sock(){}// 创建套接字voidSocket(){ sockfd_ =socket(AF_INET, SOCK_STREAM,0);// 创建一个IPv4 TCP套接字if(sockfd_ <0)// 检查套接字是否创建成功{lg(FATAL,"Socket error: %d,%s", errno,strerror(errno));// 输出日志并退出exit(Socketerr);// 错误退出}}// 绑定套接字到指定端口voidBind(uint16_t port){structsockaddr_in peer; socklen_t len =sizeof(peer);bzero(&peer, len);// 清空结构体 peer.sin_port =htons(port);// 设置端口(使用网络字节顺序) peer.sin_family = AF_INET;// 设置地址族为IPv4 peer.sin_addr.s_addr = INADDR_ANY;// 绑定到所有本地接口// 执行绑定操作if(bind(sockfd_,(structsockaddr*)&(peer), len)<0){lg(FATAL,"Bind error: %d,%s", errno,strerror(errno));// 输出日志并退出exit(Bindeterr);// 错误退出}}// 开始监听连接请求voidListen(){if(listen(sockfd_, backlog)<0)// 监听套接字,最大连接数为 `backlog`{lg(FATAL,"Listen error: %d,%s", errno,strerror(errno));// 输出日志并退出exit(Listeneterr);// 错误退出}}// 接受客户端连接intAccept(std::string *clientip,uint16_t*clientport){structsockaddr_in peer; socklen_t len =sizeof(peer);bzero(&peer, len);// 清空结构体// 等待并接受一个客户端的连接请求int newfd =accept(sockfd_,(structsockaddr*)&(peer),&len);if(newfd <0){lg(FATAL,"Accept error: %d,%s", errno,strerror(errno));// 输出日志并退出exit(Accepteterr);// 错误退出}// 获取客户端的IP地址char ip[64];inet_ntop(AF_INET,&peer.sin_addr.s_addr, ip,sizeof(ip));*clientip = ip;// 返回客户端IP*clientport =ntohs(peer.sin_port);// 返回客户端端口号(使用主机字节序)return newfd;// 返回新的文件描述符}// 连接到服务器boolConnect(const std::string &ip,constuint16_t&port){structsockaddr_in peer; socklen_t len =sizeof(peer);bzero(&peer, len);// 清空结构体 peer.sin_addr.s_addr =inet_addr(ip.c_str());// 设置目标IP peer.sin_port =htons(port);// 设置目标端口(使用网络字节顺序) peer.sin_family = AF_INET;// 设置地址族为IPv4int n =connect(sockfd_,(structsockaddr*)&(peer), len);// 连接到服务器if(n <0){lg(WARNING,"Connect error: %d,%s", errno,strerror(errno));// 输出日志并返回失败returnfalse;}returntrue;// 连接成功}// 关闭套接字voidClose(){close(sockfd_);}// 获取套接字的文件描述符intGetFd(){return sockfd_;// 返回套接字的文件描述符}private:int sockfd_;// 套接字的文件描述符};

代码说明:

1. Socket()
  • 创建一个 IPv4 TCP 套接字,并检查是否成功创建。如果创建失败,输出日志并退出程序。
2. Bind(uint16_t port)
  • 将套接字绑定到指定的端口,并设置为监听来自所有网络接口的请求。如果绑定失败,输出日志并退出程序。
3. Listen()
  • 开始监听客户端连接,backlog 变量定义了最大等待连接队列。如果监听失败,输出日志并退出程序。
4. Accept(std::string \*clientip, uint16_t \*clientport)
  • 接受一个客户端的连接请求,并返回一个新的文件描述符,用于与客户端进行通信。还会返回客户端的 IP 地址和端口号。
5. Connect(const std::string &ip, const uint16_t &port)
  • 连接到指定的服务器 IP 和端口。如果连接失败,输出日志并返回 false;否则返回 true
6. Close()
  • 关闭当前套接字,释放资源。
7. GetFd()
  • 返回套接字的文件描述符,这个文件描述符可用于读取或写入数据。

效果图

image-20250805205412995

源码地址

Read more

【Kafka高级篇】避开Kafka原生重试坑,Java业务端自建DLQ体系,让消息不丢失、不积压

【Kafka高级篇】避开Kafka原生重试坑,Java业务端自建DLQ体系,让消息不丢失、不积压

🍃 予枫:个人主页 📚 个人专栏: 《Java 从入门到起飞》《读研码农的干货日常》 💻 Debug 这个世界,Return 更好的自己! 引言 做Java消息中间件开发的同学,大概率都踩过Kafka重试的坑——相较于RabbitMQ丰富的原生重试机制,Kafka的重试支持显得十分简陋,一旦消息消费失败,要么反复重试导致系统雪崩,要么直接丢弃造成数据丢失。今天就手把手教大家,在Java业务端通过自建“重试Topic”和“死信Topic”,打造一套闭环的消息异常容错体系,彻底解决Kafka消息消费的兜底难题。 文章目录 * 引言 * 一、KAFKA原生重试机制的痛点剖析 * 二、Java业务端异常兜底核心方案:自建重试Topic+死信Topic * 核心流程拆解(图文结合理解,建议收藏) * 三、方案落地实现(Java代码实战,直接复制可用) * 3.1 环境准备(依赖配置) * 3.2 核心实体设计(封装消息,记录重试次数) * 3.

By Ne0inhk
如何解决IDEA/Datagrip无法连接数据库的问题:解决方法为添加参数-Djava.net.preferIPv4Stack=true

如何解决IDEA/Datagrip无法连接数据库的问题:解决方法为添加参数-Djava.net.preferIPv4Stack=true

如何解决IDEA/Datagrip无法连接数据库的问题:解决方法为添加参数-Djava.net.preferIPv4Stack=true 引言 在开发过程中,我们常常使用集成开发环境(IDE)如 IntelliJ IDEA 或 JetBrains DataGrip 来与数据库进行交互。然而,有时可能会遇到无法连接数据库的情况,尤其是当使用新版的 IDEA 或 DataGrip 时。这种问题通常是由于网络配置或者 IDE 与数据库之间的兼容性问题引起的。 一种常见的解决办法是添加 JVM 参数 -Djava.net.preferIPv4Stack=true,以优先使用 IPv4 协议栈。这种方式能够有效解决因 IPv6 配置问题导致的数据库连接失败问题。本文将详细介绍如何通过修改 IDEA 或 DataGrip 的启动参数来解决这个问题。 文章目录 * 如何解决IDEA/Datagrip无法连接数据库的问题:解决方法为添加参数-Djava.net.

By Ne0inhk
Java Web 开发:JSON 基础 + @Test 测试 + Cookie/Session/ 请求处理

Java Web 开发:JSON 基础 + @Test 测试 + Cookie/Session/ 请求处理

个人主页:♡喜欢做梦 欢迎  👍点赞  ➕关注  ❤️收藏  💬评论 目录 编辑 🍍JSON的概念  🍐概念  🍐@Test注解 🍑什么是@Test? 🍑与JSON关联 🍑@Test标记的方法与main方法的区别  🍍JSON语法  🍐核心数据类型  🍐常见使用 🍑对象 🍑数组  🍑JSON字符串和Java对象的互转 🍑传递JSON 🍑获取URL中的参数 🍑上传文件:@RequestPart  🍍Cookie和Seeion  🍐Cookie 🍑什么是Cookie? 🍑Cookie的获取  🍐Session 🍑什么是Session?  🍐Cookie和Session之间的关系 🍑Session的存储 🍑Session的获取 🍍获取header 🍍JSON的概念  🍐概念 JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,易于人阅读和编写,同时也易于机器解析和生成。他基于JavaScript的一个子集,但采用了独立语言的文

By Ne0inhk
Java 部署:跨环境部署(开发 / 测试 / 生产配置隔离)

Java 部署:跨环境部署(开发 / 测试 / 生产配置隔离)

👋 大家好,欢迎来到我的技术博客! 📚 在这里,我会分享学习笔记、实战经验与技术思考,力求用简单的方式讲清楚复杂的问题。 🎯 本文将围绕Java部署这个话题展开,希望能为你带来一些启发或实用的参考。 🌱 无论你是刚入门的新手,还是正在进阶的开发者,希望你都能有所收获! 文章目录 * Java 部署:跨环境部署(开发 / 测试 / 生产配置隔离) 🚀 * 为什么需要跨环境配置隔离?🤔 * Spring Boot 中的 Profile 机制:最常用的隔离方案 🌈 * 1. 创建 Profile 特定的配置文件 * 示例:通用配置(application.yml) * 示例:开发环境配置(application-dev.yml) * 示例:生产环境配置(application-prod.yml) * 2. 激活 Profile * 方式一:启动参数 * 方式二:环境变量 * 方式三:application.

By Ne0inhk