网络编程入门: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_zero 或 mod_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. Encode 和 Decode:
Encode:将字符串的大小和内容打包为带有协议头的格式,便于传输。Decode:从带有协议头的包中提取有效数据,移除头部信息并返回有效部分。
2. Requset 类:
- 表示客户端的计算请求,包括
x_、op_、y_ 三个字段(操作数和运算符)。 Serialize 和 Deserialize 用于数据的序列化和反序列化。- 提供
Getx、Gety 和 Getop 获取请求参数的函数。
3. Response 类:
- 表示服务器的响应,包括计算结果
result_ 和错误代码 code_。 Serialize 和 Deserialize 用于数据的序列化和反序列化。- 提供
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():
- 返回套接字的文件描述符,这个文件描述符可用于读取或写入数据。
效果图