linux【网络编程】之协议定制、序列化与反序列化
- 一、序列化与反序列化
- 二、应用层协议如何定制
- 三、网络通信中数据流动的本质
- 四、网络版计算器编写
- 4.1 业务流程
- 4.2 核心代码
一、序列化与反序列化
由于socket api的接口,在读写数据的时候是以字符串的方式发送接收的,如果需要传输结构化的数据,就需要制定一个协议
结构化数据在发送到网络中之前需要完成序列化
接收方收到的是序列字节流,需要完成反序列化才能使用(如ChatInfo._name)
二、应用层协议如何定制
当我们进行网络通信的的时候,一端发送时构造的数据, 在另一端能够正确的进行解析(完整的读到一条报文), 就是可行的的. 这种约定, 就是 应用层协议
如何保证读到的消息是一个完整的请求
TCP是面向字节流的,无法直接读取,需要明确报文和报文的边界,常见的方法有.定长:固定报文长度、特殊符号:在报文前面加上一个字段、自描述。
三、网络通信中数据流动的本质
我们调用的所有的发送函数(read),不是把数据发送到网络中,发送函数的本质是拷贝函数(将数据从应用层缓冲区拷贝到发送缓冲区)
- Client->Server:tcp发送的本质,其实就是将数据从Client的发送缓冲区拷贝到Server的接收缓冲区
- 反过来Server->CLient:其实就是将数据从Server的发送缓冲区拷贝到
Client的接收缓冲区 - 这也说明了网络编程(套接字编程)是全双工的
四、网络版计算器编写
有了前面的知识,下面实现一个服务器版的计算器. 我们需要客户端把要计算的两个数发过去, 然后由服务器进行计算, 最后再把结果返回给客户端
4.1 业务流程
在完成服务器与客户端正常通信的基础上完成一次请求与响应的流程
客户端:
- 从键盘读取数据并调用ParseLine()函数将输入的数据转换成类似
“123+123"
的格式 - 序列化字符串,将结构化的数据转化成用于网络通信的一个大字符串,调用请求类的序列化函数enLength()
- 添加报头:通过协议定制的规则,将”x op y”—->“content_len”\r\n”x op y”\r\n
- 向服务端发送已经构建好的报文
- 阻塞读取服务端处理后的响应数据
等待服务器发送响应报文 - 读取到一个完整的报文
- 调用协议方法去掉报头并将结果输出到text里面
- 对收到的响应正文序列化填充到响应类对象的成员变量里
- 通过正常调用,访问处理后的结果
- 发送新的请求
服务端:
- 创建子进程去执行任务
- 死循环式的读取来自客户端的报文,调用recvRequest,读取一个完整的请求放入输出型参数text里面
- 根据协议定制,调用deLength()去掉报文的报头,得到有效数据req_str
- 将来自网络的字符串通过调用请求类的反序列化转化成结构化的数据,请求类对象成员完成赋值
- 通过回调函数(一个输入型参数(请求类对象),一个输出型参数(响应类对象))传递两个类对象,将计算结果赋值给响应类的成员变量
- 开始将处理结果返回给客户端,调用响应类中的序列化方法将结构化数据转换成一个大字符串
- 对大字符串添加报头构建成一个完整的报文,发送给客户端
- 服务端等待新的请求…
4.2 核心代码
Protocol.hpp包含了协议定制函数、请求响应序列化与反序列化函数、完整报文获取函数
#pragma once#include #include #include #include using namespace std;#define SEP " " // 分隔符#define SEP_LEN strlen(SEP) // 分隔符长度,不能用sizeof#define LINE_SEP "\r\n"#define LINE_SEP_LINE strlen(LINE_SEP)enum{OK = 0,DIV_ZERO,MOD_ZERO,OP_ERROR};// 协议定制:给报文段加一个特殊字段:有效载荷的长度//报头有效载荷// //"exitcode result"-->"content_len"\r\n"exitcode result"\r\n----std::string enlength(const std::string &text){// text就是"x op y"string send_string = std::to_string(text.size()); // content_lensend_string += LINE_SEP;send_string += text;send_string += LINE_SEP;return send_string;}// 去掉报头,提取有效载荷//"content_len"\r\n"exitcode result"\r\n-->exitcode resultbool delength(const std::string &package, std::string *text){auto pos = package.find(LINE_SEP);if (pos == string::npos)return false;// 提取报头字符串string text_len_string = package.substr(0, pos);// 将报头信息转化成字符串int text_len = std::stoi(text_len_string);// 提取有效载荷*text = package.substr(pos + LINE_SEP_LINE, text_len);return true;}// 请求class Request{public:Request(): x_(0), y_(0), op_(0){}Request(int x, int y, char op): x_(x), y_(y), op_(op){}// 序列化bool serialize(std::string *out){#ifdef MYSELF// 将结构化数据转化成-->"x op y"out->clear();std::string x_string = std::to_string(x_);std::string y_string = std::to_string(y_);*out = x_string;*out += SEP;*out += op_;*out += SEP;*out += y_string;#elseJson::Value root;//定义一个万能对象root["first"]=x_;root["second"]=y_;root["oper"]=op_;Json::FastWriter writer;*out=writer.write(root);#endifreturn true;}// 反序列化bool deserialize(const std::string &in){ #ifdef MYSELF //"x op yyy";auto left = in.find(SEP);auto right = in.rfind(SEP);if (left == std::string::npos || right == std::string::npos)return false;if (left == right)return false;// 截取子串if (right - (left + SEP_LEN) != 1)return false;std::string x_string = in.substr(0, left); // 定位到xstd::string y_string = in.substr(right + SEP_LEN); // 定位到yyyyif (x_string.empty())return false;if (y_string.empty())return false;x_ = std::stoi(x_string);y_ = std::stoi(y_string);op_ = in[left + SEP_LEN]; // 截取op#elseJson::Value root;Json::Reader reader;reader.parse(in,root);//将解析出来的值放进root里面x_=root["first"].asInt();//将val转化成整数y_=root["second"].asInt();op_=root["oper"].asInt();#endifreturn true;}public:int x_;int y_;char op_;};// 响应class Response{public:Response(): exitcode_(0), result_(0){}Response(int exitcode, int result): exitcode_(exitcode), result_(result){}// 序列化bool serialize(std::string *out){#ifdef MYSELF// 清空字符串out->clear();// 将退出码和结果转换成字符串string ec_string = std::to_string(exitcode_);string res_string = std::to_string(result_);// 合并字符*out = ec_string;*out += SEP;*out += res_string;#elseJson::Value root;root["exitcode"]=exitcode_;root["result"]=result_;Json::FastWriter writer;*out=writer.write(root);#endifreturn true;}// 反序列化bool deserialize(const std::string &in){#ifdef MYSELF//"exitcode result"auto mid = in.find(SEP);if (mid == std::string::npos)return false;// 截取字符串string ec_string = in.substr(0, mid);string res_string = in.substr(mid + SEP_LEN);if (ec_string.empty() || res_string.empty())return false;// 写入退出码和结果exitcode_ = std::stoi(ec_string);result_ = std::stoi(res_string);#elseJson::Reader reader;Json::Value root;reader.parse(in,root);exitcode_=root["exitcode"].asInt();result_=root["result"].asInt();#endifreturn true;}public:int exitcode_; // 0成功,!0错误int result_; // 计算结果};// 读取一个完整的请求放入text里面//"content_len"\r\n"x op y"\r\n"content_len"\r\n"x op y"\r\nbool recvRequest(int sock, std::string &inbuffer, string *text){char buffer[1024];while (true){ssize_t n = recv(sock, buffer, sizeof(buffer) - 1, 0);if (n > 0){buffer[n] = 0;inbuffer += buffer;// 边读边处理auto pos = inbuffer.find(LINE_SEP);// 如果没有读到\r\n,接着去读if (pos == string::npos)continue;// 走到这已经读到了content_len,知道了有效载荷长度string text_len_string = inbuffer.substr(0, pos); // 报头int text_len = std::stoi(text_len_string);// 正文长度int total_len = text_len_string.size() + 2 * LINE_SEP_LINE + text_len;std::cout << "处理前#inbuffer: \n"<< inbuffer << endl;if (inbuffer.size() < total_len){std::cout << "你输入的消息,没有严格遵守我们的协议,正在等待后续的内容, continue" << std::endl;continue; // 没有读到一个完整的报文}// 至少有一个报文*text = inbuffer.substr(0, total_len);inbuffer.erase(0, total_len);std::cout << "处理后#inbuffer: \n"<< inbuffer << endl;break;}elsereturn false;}return true;}
服务端响应流程
void handlerEnter(int sock, func_t func){string inuffer;//将所有信息写入到inbufferwhile(true){// 1.读取:"content_len"\r\n"x op y"\r\n// 1.1 保证读到的消息是【一个完整】的请求std::string req_text; // 输出型参数,整个报文if (!recvRequest(sock, inuffer,&req_text))return;std::cout<<"带报头的请求:\n"<<req_text<<endl;// 1.2 去报头,只要正文std::string req_str; // 正文部分if (!delength(req_text, &req_str))return;std::cout<<"去掉报头后的正文:\n"<<req_str<<endl; // 2.反序列化// 2.1 得到一个结构化对象,对象中的成员已经被填充Request req;if (!req.deserialize(req_str))return;// 3.处理数据---------业务逻辑// 3.1 得到一个结构化的响应,resp成员已被填充Response resp;func(req, resp); // 回调// 4.对响应Response,序列化// 4.1 得到一个字符串std::string resp_str;resp.serialize(&resp_str); // 输出型参数,将序列化结果写入resp_strstd::cout<<"计算完成,序列化响应: "<<resp_str<<endl;// 5.然后发送响应// 5.1添加协议报头,构建成一个完整的报文std::string send_string = enlength(resp_str);std::cout<<"构建带报头的响应正文: \n"<<send_string<<endl;// 发送send(sock, send_string.c_str(), send_string.size(), 0); // 有问题std::cout<<"发送响应报文成功: \n"<<endl;}}
客户端请求流程
void run(){struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(clientport_);server.sin_addr.s_addr = inet_addr(clientip_.c_str());// 发起链接if (connect(sockfd_, (struct sockaddr *)&server, sizeof(server)) != 0){std::cerr << "connect create error" << endl;}else{string msg;string inbuffer;while (true){cout << "mycal>>> ";std::getline(std::cin, msg);Request req = ParseLine(msg); // 从键盘提取字符串string content;// 序列化结构数据req.serialize(&content);// 添加报头string send_string = enlength(content);// 发送数据send(sockfd_, send_string.c_str(), send_string.size(), 0);// 接收响应报文string package, text;if (!recvRequest(sockfd_,inbuffer,&package))continue;//去掉报头,获取正文放在text里面 if(!delength(package,&text)) continue;//将收到的响应正文反序列化Response resp;resp.deserialize(text);std::cout << "exitCode: " << resp.exitcode_ << std::endl;std::cout << "result: " << resp.result_ << std::endl; }}}
正常输入输出显示如下图
以上只是提供了几个核心的代码块,完整版代码可以去我的Gitee,代码注释详细,希望对你有所帮助