网络编程中的粘包问题及自定义协议设计

网络编程中的粘包问题及自定义协议设计 文章目录1、Socket编程缺陷1.1、粘包问题1.2、解决方法2、jsoncpp简介2.1、数据-json字符串2.2、json字符串-数据2.3、其他操作3、网络计算器实现3.1、服务端主要方法3.2、自定义协议实现3.3、服务层实现4、总结1、Socket编程缺陷1.1、粘包问题直接调用系统提供的read/write/recv/send只能能将数据写入发送缓冲区或者从接收缓冲区中提取数据如下图。一台主机中既有接收缓冲区又有发送缓冲区所以能够同时发送、接收数据即全双工。再看用户将协议栈作为一个黑盒模型一台主机向协议栈中写入数据另一台主机从协议栈中读出数据。这就是一个生产者-消费者模型。TCP协议是基于数据流的协议全称Transmission Control Protocol(传输控制协议)。只保证数据发送、发送多少数据、什么时候发送、出错了都由TCP协议保证。例如在一个remote-ssh工具中A主机给B主机发送ls -a -l然后又发送一个pwd。A先将ls -a -l写入缓冲区然后写入pwd发送给了BB接收到的可能是ls -a -lpwd。这就是数据粘包问题。下面linux2.6.18的缓冲区是TCP缓冲区1.2、解决方法序列化将数据变成特定的数据结构。反序列化将数据从特定的数据结构变成可用的数据。一般序列化不选取结构体或者类这是因为不同语言间结构体或者类内存布局不同。很多序列化操作Protocol Buffers不是都是采用字符串拼接因为是编码的规定可以实现跨平台向、跨语言性。序列化和反序列化是为了自定义协议做准备。自定义一个协议格式为{len}\r\n{data}\r\n在接收端中检查是否数据内容长度小于len如果小于就不做处理说明没有读取完整。UDP是基于数据报的协议不存在数据粘包问题但也需要业务层协议。2、jsoncpp简介常用的序列化方式有jsonjson可读性高。用jsoncpp就可以实现操作也简单。sudoaptaptinstalllibjsoncpp-dev# Ubuntusudoyuminstalljsoncpp-devel# CentOS2.1、数据-json字符串#includeiostream#includejsoncpp/json/value.h#includejsoncpp/json/writer.h#includesstream#includestring#includejsoncpp/json/json.hstructPeople{std::string _name;int_age;std::string _sex;};intmain(){People shang3{ZhangSan,21,男};Json::Value root;root[name]shang3._name;root[age]shang3._age;root[sex]shang3._sex;// version 1Json::FastWriter fwriter;std::string info1fwriter.write(root);// version 2Json::StyledWriter swriter;std::string info2swriter.write(root);// version 3Json::StreamWriterBuilder builder;std::unique_ptrJson::StreamWriterwriter(builder.newStreamWriter());std::stringstream ss;writer-write(root,ss);std::coutinfo1std::endl;std::coutinfo2std::endl;std::coutss.str()std::endl;return0;}Json::Value是一个json对象后续操作基本都是围绕json对象进行的。jsoncpp库之所以简单就是因为操作有点像map是通过运算符重载实现的key-value操作。对于Json::FastWriter和Json::StyledWriter两者基本一样但是后者会添加换行符有利于人的阅读而前者读写操作更快。对于version 3先创建了一个Json::StreamWriterBuilder工厂类对象这个工厂类用于生成Json::StreamWriter对象该对象能直接写入Ostrem中。编译# 需要指定链接jsoncpp库g test.cpp-ljsoncpp-otest结果{age:21,name:ZhangSan,sex:\u7537} { age : 21, name : ZhangSan, sex : \u7537 } { age : 21, name : ZhangSan, sex : \u7537 }2.2、json字符串-数据#includeiostream#includejsoncpp/json/reader.h#includestring#includejsoncpp/json/json.hstructPeople{std::string _name;int_age;std::string _sex;};intmain(){std::stringjson_string({\age\:21,\name\:\ZhangSan\,\sex\:\\u7537\});Json::Reader reader;Json::Value root;boolsuccesreader.parse(json_string,root);if(!succes)return1;People zhang3;zhang3._nameroot[name].asString();zhang3._ageroot[age].asInt();zhang3._sexroot[sex].asString();std::coutname:zhang3._namestd::endl;std::coutage:zhang3._agestd::endl;std::coutsex:zhang3._sexstd::endl;return0;}Json:Reader是一个用于从json字符串中读取数据的对象。reader.parse(json_string, root)可以从json_string字符串中读取数据写入root对象。返回值为bool类型。Json::Value是一个万能容器数据类型通过枚举和联合体实现的弱类型化。需要通过asString等类似的函数强转。结果name:ZhangSan age:21 sex:男2.3、其他操作创建子对象Json::Value root;root[user][name]Tom;root[user][age]18;创建数组Json::Value root;root[nums].append(1);root[nums].append(2);root[nums].append(3);root[nums].append(4);数组中放对象Json::Value arr;Json::Value obj1;obj1[id]1;Json::Value obj2;obj2[id]2;arr.append(obj1);arr.append(obj2);3、网络计算器实现本节源码已上传至【giteehttps://gitee.com/muyi-2580/learning-linux/tree/main/7_6】。3.1、服务端主要方法voidLoop()const{// 信号捕捉子进程退出signal(SIGCHLD,SIG_IGN);while(true){InetAddr clientaddr;autosockfd_listensockfd-Acceptor(clientaddr);if(sockfdnullptr)continue;LOG(LogLevel::DEBUG)accept from : clientaddr.ToString();if(fork()0){// childsockfd-Close(_listensockfd);Service(sockfd,clientaddr);exit(SUCCESS);}// parent_listensockfd-Close(sockfd);}}采用多进程信号回收方式处理服务。voidService(conststd::shared_ptrSocketsockfd,constInetAddrclientaddr)const{std::string inbuf;std::string outbuf;while(true){outbuf.clear();intnsockfd-Recv(inbuf);if(n0)break;if(_handler)outbuf_handler(inbuf);if(outbuf.empty())continue;nsockfd-Send(outbuf);if(n0)break;}}_handle其类型为std::functionstd::string(std::string)为回调函数或者lambda表达式。将从网络端收到的数据传到协议层处理。返回一个序列后、封包完整的数据用于应答客户端。outbuf _handler(inbuf)这里使用是为了解耦_handler只负责一部分服务。3.2、自定义协议实现// 请求classRequest{public:Request();Request(constdoublex,constdoubley,constcharoper);// 序列化boolSerialize(std::string*out);// 反序列化boolDeSerialize(conststd::stringin);public:// 写成public不推荐double_data_x;double_data_y;char_oper;};// 应答classResponse{public:Response();// 序列化boolSerialize(std::string*out);// 反序列化boolDeSerialize(conststd::stringin);// private:public:double_result;int_code;};Request类是客户端向服务端发起的请求Response类是服务端回复客户端的应答。conststd::string gsep\r\n;usinghandler_req_tstd::functionResponse(Request);usinghandler_res_tstd::functionvoid(Response);自定义协议格式为{len}\r\n{json_string}\r\n这里定义了分隔符、以及处理请求、处理应答的类型。classProtocol{public:Protocol(handler_req_t handler_req,conststd::stringversion0.0.1);Protocol(handler_res_t handler_res,conststd::stringversion0.0.1);std::stringPacket(conststd::stringjson_string)const;/* * -1 : error * 0 : no error, but unpacket is empty * 1 : succes */intUnPacket(std::stringpacket,std::string*json_string);std::stringPhaseRequest(std::stringinbuf){std::string result;while(true){// 1. 解包intnUnPacket(inbuf,result);if(n0){LOG(LogLevel::DEBUG)no way...;return;}elseif(n0){LOG(LogLevel::INFO)\nresult;LOG(LogLevel::INFO)Phase Request done!;returnresult;}// 2. 反序列化Request request;if(!request.DeSerialize(result)){LOG(LogLevel::DEBUG)Request DeSerialize error...;return;}Response response;// 3. 交给上层服务if(_handler_req)response_handler_req(request);// 4. 应答序列化if(!response.Serialize(result)){LOG(LogLevel::DEBUG)Response Serialize error...;return;}// 5. 封包resultPacket(result);}}std::stringPhaseResponse(std::string inbuf){while(true){std::string json_string;// 1. 解包intnUnPacket(inbuf,json_string);if(n0){LOG(LogLevel::DEBUG)no way...;return;}if(n0){LOG(LogLevel::INFO)Phase Response done!;return;}// 2. 反序列化Response response;if(!response.DeSerialize(json_string))return;if(_handler_res)_handler_res(response);}}private:std::string _version;handler_req_t _handler_req;handler_res_t _handler_res;};PhaseRequest解包-反序列化-_handler_req处理-序列化-封包。是给TcpServer的回调方法。其中_handler_req是上层服务传递的方法作用是接收一个请求类返回一个应答类。PhaseResponse解包-反序列化-_handler_res处理。是给客户端处理结果的方法。3.3、服务层实现classCalculator{public:Calculator(){}ResponseExecute(Request request)const{doublexrequest._data_x;doubleyrequest._data_y;Response response;switch(request._oper){case:response._resultxy;break;case-:response._resultx-y;break;case*:response._resultx*y;break;case/:{if(y0)response._code1;elseresponse._resultx/y;}break;default:response._code-1;LOG(LogLevel::WARNING)Obtained illegal symbols!;break;}returnresponse;}};Execute接收一个请求并返回一个应答结果支持加减乘除运算。用于回调传递给PhaseRequest。4、总结OSI 7层网络模型设计得非常好本文设计的网络计算器就设计出了应用层、表示层、会话层。一个好的项目要想有较低的耦合度就应该按照7层模型实现。本文简要介绍了一下数据粘包问题jsoncpp的使用方法以及序列化、反序列化、自定义协议。