C++网络编程学习:缓冲区溢出与粘包分包

您所在的位置:网站首页 udp分包发送时间差 C++网络编程学习:缓冲区溢出与粘包分包

C++网络编程学习:缓冲区溢出与粘包分包

2023-08-13 00:38| 来源: 网络整理| 查看: 265

网络编程学习记录 使用的语言为C/C++源码支持的平台为:Windows / Linux

笔记一:建立基础TCP服务端/客户端  点我跳转 笔记二:网络数据报文的收发  点我跳转 笔记三:升级为select网络模型  点我跳转 笔记四:跨平台支持Windows、Linux系统  点我跳转 笔记五:源码的封装  点我跳转 笔记六:缓冲区溢出与粘包分包  点我跳转 笔记七:服务端多线程分离业务处理高负载  点我跳转 笔记八:对socket select网络模型的优化  点我跳转 笔记九:消息接收与发送分离  点我跳转 笔记十:项目化 (加入内存池静态库 / 报文动态库)  更多笔记请点我

笔记六 网络编程学习记录一、关于缓冲区溢出1.缓冲区溢出的原因2.缓冲区溢出的处理方法 二、粘包与分包1.粘包与分包的原因2.粘包与分包的处理方法2.1客户端升级思路2.2服务端升级思路 三、升级后的源码及其详细注释1.客户端源码 TcpClient.hpp2.服务端源码 TcpServer.hpp

一、关于缓冲区溢出 1.缓冲区溢出的原因

  之前我们所编写的服务端与客户端的数据量都是很小的,且操作也不频繁,需要键入指令发送报文。

  我们可以尝试在之前客户端代码的循环里,不断发送一种数据包,且把数据包的大小加大到1000字节,会发现很快服务端和客户端就会出现问题——要么是数据接收出现问题,要么是服务端或者客户端程序直接卡掉。这里出现问题的原因就是socket内核缓冲区溢出。

  首先,send和recv函数并不是直接通过网卡操作。在使用send函数时,send函数首先把数据写入到发送缓冲区,随后通过网卡发出;在使用recv函数时,网卡首先把接收到的消息写入接收缓冲区,recv函数再从中copy数据。注意,上文中的两个缓冲区是存在于内核中的,并不是程序中自定义的缓冲区。

  我们在之前的源码中,recv的逻辑是先接收包头,随后根据包头接收包体。而当网卡接收数据太多时,我们接收一个包头的时间,网卡可能就新接收了两个完整的数据包,这就导致内核接收缓冲区里的数据量是在不断增加的,最终导致接收缓冲区溢出,造成无法正常发送以及程序阻塞的问题。

  举个例子,缓冲区就像一个浴缸,而我们是一个拿盆子舀水的人。我们之前先接收一个包头就相当于舀出一个包头那么多的水,随后再舀出包体那么多的水。舀了两次仅仅舀出一个报文那么多的水。如果浴缸放水的速度比较大的话,我们很容易就会处理不过来。最终造成浴缸溢出(缓冲区溢出)。

2.缓冲区溢出的处理方法

  接着看上文的例子,我们怎么能阻止浴缸(缓冲区)溢出呢?首先我们不大可能改变浴缸的大小,因为太过麻烦以及治标不治本,只要浴缸放水的时间够长,总会溢出。接着,舀水的速度我们也不好改变,因为一时半会是改不了的。那我们就只能改变舀水的次数和数量了。

  如何改变舀水的数量和次数?我们可以一次舀出足够多的水,随后再从舀出的水中分出想要数量的水,这样浴缸溢出的可能性就大大减少了。

  从代码层面来看上面的思路,只要我们程序内新建一个足够大的缓冲区,一次从内核缓冲区上recv足够的数据,就可以避免内核缓冲区溢出了。

大概思路如下: char _Recv_buf[4096]; int DataRecv { //接收客户端发送的数据 int recv_len = recv(socket, _Recv_buf, 4096, 0); if(recv_len 处理_Recv_buf内的数据 } return 0; }

  但是这样会出现新的问题,即粘包与分包问题,请看下文。

二、粘包与分包 1.粘包与分包的原因

  上文中处理缓冲区溢出的思路是没有问题的,但是上文中的源码写法会存在问题。

  我们一次接收那么多数据,其中数据的界限是没有限定的,比如上文中是想要一次接收4096个字节。假如缓冲区内有5个1000字节大小的数据包,我们这次接收4096字节,等于说接收的数据中有4.096个数据包,其中就包含了新的问题。

  首先是粘包问题。即一次接收中含有多个数据包,这就导致数据包界限不清,粘在了一起。像上文中的4.096个包,接收端是不清楚的,接收端只知道有4096字节的数据,但是它不知道一个包是多大。所以我们可以通过包头来获取一个数据包的大小,由此来处理相应大小的数据以解决粘包问题。

  接着是分包问题。即一次接收中含有不完整的包。例如上文中的4096个字节,其中包含了4个完整的包,和一个包的前96个字节。对此,我们只能处理前4个完整的数据包。那么问题来了,对于上文中的缓冲区,由于recv函数每次都会覆盖这个缓冲区,这就导致缓冲区内无法存放未处理的消息。对于这个问题,我们可以新建一个缓冲区,来存放未处理的消息,实现双缓冲,即可处理分包问题。

TCP是面向数据流的协议,所以会出现粘包分包问题;UDP是面向消息的协议,每一个数据段都是一条消息,所以不会出现粘包分包问题。 2.粘包与分包的处理方法 2.1客户端升级思路

  首先是新建两个缓冲区,一个用来存放recv到的数据,一个用来存放所有待处理数据。首先第一个缓冲区recv到数据,随后把第一个缓冲区内的数据copy到第二个缓冲区内,即可实现数据的存放。随后处理数据之类的还是先获取包头,随后根据包头处理包体数据。

大致思路如下: //接收数据 char 接收缓冲区[4096] char 消息缓冲区[40960]; int RecvData(SOCKET temp_socket)//处理数据 { //接收客户端发送的数据 int recv_len = recv(temp_socket, 接收缓冲区, 4096, 0); if(recv_len 3.选出包头数据 //解决粘包问题 if(4.判断消息缓冲区内数据长度是否大于等于报文长度) //解决少包问题 { 5.响应数据 6.将处理过的消息移出消息缓冲区 } } return 0; } 2.2服务端升级思路

  与客户端整头思路相似,但是需要注意,服务端有多个连接,如果多个连接共用一个缓冲区会存在错误,所以每一个客户端连接都需要有自己的缓冲区。对此,我们可以新建一个客户端连接类,来存放每一个客户端的socket以及它的缓冲区。

大致思路如下: class 客户端连接 { public: 1.获取socket() 2.获取缓冲区() 3.获取缓冲区长度() 4.设置缓冲区长度() private: 1.socket 2.缓冲区 }; std::vector _clients;//储存客户端socket char 接收缓冲区[4096]; 0.此时前面OnRun函数里的判断过程也需要改变 //遍历所有socket 查看是否有待处理事件 for(int n=0; n if(-1 == RecvData(_clients[n]))//处理请求 客户端退出的话 { std::vector::iterator iter = _clients.begin()+n;//找到退出客户端的地址 if(iter != _clients.end())//如果是合理值 { delete _clients[n]; _clients.erase(iter);//移除 } } } } int RecvData(客户端连接* client)//处理数据 { //接收客户端发送的数据 int recv_len = recv(client->获取socket(), 接收缓冲区, 4096, 0); if(recv_len 3.选出包头数据 //解决粘包问题 if(4.判断消息缓冲区内数据长度是否大于等于报文长度) //解决少包问题 { 5.响应数据 6.将处理过的消息移出消息缓冲区 client->设置缓冲区长度(); } } return 0; } 三、升级后的源码及其详细注释 1.客户端源码 TcpClient.hpp #ifndef _TcpClient_hpp_ #define _TcpClient_hpp_ #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN #include #include #pragma comment(lib,"ws2_32.lib") #else #include #include #include #define SOCKET int #define INVALID_SOCKET (SOCKET)(~0) #define SOCKET_ERROR (-1) #endif //枚举类型记录命令 enum cmd { CMD_LOGIN,//登录 CMD_LOGINRESULT,//登录结果 CMD_LOGOUT,//登出 CMD_LOGOUTRESULT,//登出结果 CMD_NEW_USER_JOIN,//新用户登入 CMD_ERROR//错误 }; //定义数据包头 struct DataHeader { short cmd;//命令 short date_length;//数据的长短 }; //包1 登录 传输账号与密码 struct Login : public DataHeader { Login()//初始化包头 { this->cmd = CMD_LOGIN; this->date_length = sizeof(Login); } char UserName[32];//用户名 char PassWord[32];//密码 char data[932]; }; //包2 登录结果 传输结果 struct LoginResult : public DataHeader { LoginResult()//初始化包头 { this->cmd = CMD_LOGINRESULT; this->date_length = sizeof(LoginResult); } int Result; char Data[992];//无意义数据 }; //包3 登出 传输用户名 struct Logout : public DataHeader { Logout()//初始化包头 { this->cmd = CMD_LOGOUT; this->date_length = sizeof(Logout); } char UserName[32];//用户名 char data[964]; }; //包4 登出结果 传输结果 struct LogoutResult : public DataHeader { LogoutResult()//初始化包头 { this->cmd = CMD_LOGOUTRESULT; this->date_length = sizeof(LogoutResult); } int Result; char data[992]; }; //包5 新用户登入 传输通告 struct NewUserJoin : public DataHeader { NewUserJoin()//初始化包头 { this->cmd = CMD_NEW_USER_JOIN; this->date_length = sizeof(NewUserJoin); } char UserName[32];//用户名 }; #include #define RECV_BUFFER_SIZE 4096 class TcpClient { public: //构造 TcpClient() { _sock = INVALID_SOCKET; //缓冲区相关 _Recv_buf = new char[RECV_BUFFER_SIZE]; _Msg_buf = new char[RECV_BUFFER_SIZE*10]; _Len_buf = 0; } //析构 virtual ~TcpClient() { delete[] _Recv_buf; delete[] _Msg_buf; //关闭socket CloseSocket(); } //初始化socket 返回1为正常 int InitSocket() { #ifdef _WIN32 //启动windows socket 2,x环境 WORD ver = MAKEWORD(2,2); WSADATA dat; if(0 != WSAStartup(ver,&dat)) { return -1;//-1为环境错误 } #endif //创建socket if(INVALID_SOCKET != _sock) { printf("关闭连接\n",_sock); CloseSocket();//如果之前有连接 就关闭连接 } _sock = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); if(INVALID_SOCKET == _sock) { return 0;//0为socket创建错误 } return 1; } //连接服务器 返回1为成功 int Connect(const char *ip,unsigned short port) { //如果为无效套接字 则初始化 if(INVALID_SOCKET == _sock) { InitSocket(); } //连接服务器 sockaddr_in _sin = {}; _sin.sin_family = AF_INET;//IPV4 _sin.sin_port = htons(port);//端口号 #ifdef _WIN32 _sin.sin_addr.S_un.S_addr = inet_addr(ip);//IP #else _sin.sin_addr.s_addr = inet_addr(ip);//IP #endif if(SOCKET_ERROR == connect(_sock,(sockaddr*)&_sin,sizeof(sockaddr_in))) { return 0;//连接失败 } else { return 1;//连接成功 } } //关闭socket void CloseSocket() { if(INVALID_SOCKET != _sock) { #ifdef _WIN32 //关闭socket closesocket(_sock); //清除windows socket 环境 WSACleanup(); #else //关闭socket/LINUX close(_sock); #endif _sock = INVALID_SOCKET; } } //查询是否有待处理消息 bool OnRun() { if(IsRun())//如果有连接则监听事件 { fd_set _fdRead;//建立集合 FD_ZERO(&_fdRead);//清空集合 FD_SET(_sock,&_fdRead);//放入集合 timeval _t = {1,0};//select最大响应时间 //新建seclect int _ret = select(_sock+1,&_fdRead,NULL,NULL,&_t); if(_ret FD_CLR(_sock,&_fdRead);//清理计数器 if(-1 == RecvData(_sock)) { CloseSocket(); return false; } } return true; } return false; } //判断是否工作中 bool IsRun() { return _sock != INVALID_SOCKET; } //发送数据 int SendData(DataHeader *_head) { if(IsRun() && _head) { send(_sock,(const char*)_head,_head->date_length,0); return 1; } return 0; } //接收数据 int RecvData(SOCKET temp_socket)//处理数据 { //接收客户端发送的数据 int recv_len = recv(temp_socket, _Recv_buf, RECV_BUFFER_SIZE, 0); if(recv_len //选出包头数据 DataHeader* header = (DataHeader*)_Msg_buf; //判断消息缓冲区内数据长度是否大于等于报文长度 避免少包问题 if(_Len_buf >= header->date_length) { //计算出消息缓冲区内剩余未处理数据的长度 int size = _Len_buf - header->date_length; //响应数据 NetMsg(header); //将消息缓冲区剩余未处理的数据前移 memcpy(_Msg_buf, _Msg_buf + header->date_length, size); //消息缓冲区的数据末尾前移 _Len_buf = size; } else { //消息缓冲区数据不足 break; } } return 0; } //响应数据 virtual void NetMsg(DataHeader *_head) { printf("接收到包头,命令:%d,数据长度:%d\n",_head->cmd,_head->date_length); switch(_head->cmd) { case CMD_LOGINRESULT://登录结果 接收登录包体 { LoginResult *_result = (LoginResult*)_head; printf("登录结果:%d\n",_result->Result); } break; case CMD_LOGOUTRESULT://登出结果 接收登出包体 { LogoutResult *_result = (LogoutResult*)_head; printf("登录结果:%d\n",_result->Result); } break; case CMD_NEW_USER_JOIN://新用户登录通知 { NewUserJoin *_result = (NewUserJoin*)_head; printf("用户:%s已登录\n",_result->UserName); } break; case CMD_ERROR://错误 { printf("错误数据\n"); getchar(); } break; default: { printf("未知数据\n"); getchar(); } break; } } private: SOCKET _sock; //缓冲区相关 char *_Recv_buf;//接收缓冲区 char *_Msg_buf;//消息缓冲区 int _Len_buf;//缓冲区数据尾部变量 }; #endif 2.服务端源码 TcpServer.hpp #ifndef _TcpServer_hpp_ #define _TcpServer_hpp_ #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN #include #include #pragma comment(lib,"ws2_32.lib")//链接此动态链接库 windows特有 #else #include//selcet #include//uni std #include #define SOCKET int #define INVALID_SOCKET (SOCKET)(~0) #define SOCKET_ERROR (-1) #endif //枚举类型记录命令 enum cmd { CMD_LOGIN,//登录 CMD_LOGINRESULT,//登录结果 CMD_LOGOUT,//登出 CMD_LOGOUTRESULT,//登出结果 CMD_NEW_USER_JOIN,//新用户登入 CMD_ERROR//错误 }; //定义数据包头 struct DataHeader { short cmd;//命令 short date_length;//数据的长短 }; //包1 登录 传输账号与密码 struct Login : public DataHeader { Login()//初始化包头 { this->cmd = CMD_LOGIN; this->date_length = sizeof(Login); } char UserName[32];//用户名 char PassWord[32];//密码 char data[932]; }; //包2 登录结果 传输结果 struct LoginResult : public DataHeader { LoginResult()//初始化包头 { this->cmd = CMD_LOGINRESULT; this->date_length = sizeof(LoginResult); } int Result; char Data[992];//无意义数据 }; //包3 登出 传输用户名 struct Logout : public DataHeader { Logout()//初始化包头 { this->cmd = CMD_LOGOUT; this->date_length = sizeof(Logout); } char UserName[32];//用户名 char data[964]; }; //包4 登出结果 传输结果 struct LogoutResult : public DataHeader { LogoutResult()//初始化包头 { this->cmd = CMD_LOGOUTRESULT; this->date_length = sizeof(LogoutResult); } int Result; char data[992]; }; //包5 新用户登入 传输通告 struct NewUserJoin : public DataHeader { NewUserJoin()//初始化包头 { this->cmd = CMD_NEW_USER_JOIN; this->date_length = sizeof(NewUserJoin); } char UserName[32];//用户名 }; #include #define RECV_BUFFER_SIZE 4096 class ClientSocket { public: //构造 ClientSocket(SOCKET sockfd = INVALID_SOCKET) { _sockfd = sockfd; //缓冲区相关 _Msg_buf = new char[RECV_BUFFER_SIZE*10]; _Len_buf = 0; } //析构 virtual ~ClientSocket() { delete[] _Msg_buf; } //获取socket SOCKET GetSockfd() { return _sockfd; } //获取缓冲区 char* MsgBuf() { return _Msg_buf; } //获取缓冲区尾部变量 int GetLen() { return _Len_buf; } //设置缓冲区尾巴变量 void SetLen(int len) { _Len_buf = len; } private: SOCKET _sockfd; //缓冲区相关 char *_Msg_buf;//消息缓冲区 int _Len_buf;//缓冲区数据尾部变量 }; class TcpServer { public: //构造 TcpServer() { _sock = INVALID_SOCKET; //缓冲区相关 _Recv_buf = new char[RECV_BUFFER_SIZE]; } //析构 virtual ~TcpServer() { delete[] _Recv_buf; //关闭socket CloseSocket(); } //初始化socket 返回1为正常 int InitSocket() { #ifdef _WIN32 //启动windows socket 2,x环境 WORD ver = MAKEWORD(2,2); WSADATA dat; if(0 != WSAStartup(ver,&dat)) { return -1;//-1为环境错误 } #endif //创建socket if(INVALID_SOCKET != _sock) { printf("关闭连接\n",_sock); CloseSocket();//如果之前有连接 就关闭连接 } _sock = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); if(INVALID_SOCKET == _sock) { return 0;//0为socket创建错误 } return 1; } //绑定IP/端口 int Bind(const char* ip,unsigned short port) { //如果为无效套接字 则初始化 if(INVALID_SOCKET == _sock) { InitSocket(); } //绑定网络端口和IP地址 sockaddr_in _myaddr = {}; _myaddr.sin_family = AF_INET;//IPV4 _myaddr.sin_port = htons(port);//端口 #ifdef _WIN32 if(ip)//ip为空则监听所有网卡 { _myaddr.sin_addr.S_un.S_addr = inet_addr(ip);//IP } else { _myaddr.sin_addr.S_un.S_addr = INADDR_ANY;//IP } #else if(ip)//ip为空则监听所有网卡 { _myaddr.sin_addr.s_addr = inet_addr(ip);//IP } else { _myaddr.sin_addr.s_addr = INADDR_ANY;//IP } #endif if(SOCKET_ERROR == bind(_sock,(sockaddr*)&_myaddr,sizeof(sockaddr_in)))//socket (强制转换)sockaddr结构体 结构体大小 { printf("绑定失败\n"); return 0; } else { printf("绑定成功\n绑定端口为%d\n",port); return 1; } } //监听端口 int Listen(int n) { //如果为无效套接字 则提示 if(INVALID_SOCKET == _sock) { printf("请先初始化套接字并绑定IP端口\n"); return 0; } //监听网络端口 if(SOCKET_ERROR == listen(_sock,n))//最大连接队列 { printf("监听失败\n"); return 0; } else { printf("监听成功\n"); return 1; } } //接受连接 int Accept() { //等待接收客户端连接 sockaddr_in clientAddr = {};//新建sockadd结构体接收客户端数据 int addr_len = sizeof(sockaddr_in);//获取sockadd结构体长度 SOCKET temp_socket = INVALID_SOCKET;//声明客户端套接字 #ifdef _WIN32 temp_socket = accept(_sock,(sockaddr*)&clientAddr,&addr_len);//自身套接字 客户端结构体 结构体大小 #else temp_socket = accept(_sock,(sockaddr*)&clientAddr,(socklen_t*)&addr_len);//自身套接字 客户端结构体 结构体大小 #endif if(INVALID_SOCKET == temp_socket)//接收失败 { printf("错误,接受到无效客户端SOCKET\n",temp_socket); return 0; } else { printf("新客户端加入\nIP地址为:%s \n", inet_ntoa(clientAddr.sin_addr)); //群发所有客户端 通知新用户登录 NewUserJoin *user_join = new NewUserJoin(); strcpy(user_join->UserName,inet_ntoa(clientAddr.sin_addr)); SendDataToAll(user_join); //将新的客户端加入动态数组 _clients.push_back(new ClientSocket(temp_socket)); return 1; } } //关闭socket void CloseSocket() { if(INVALID_SOCKET != _sock) { #ifdef _WIN32 //关闭客户端socket for(int n=0; n close(_clients[n]->GetSockfd()); delete _clients[n]; } //关闭socket/LINUX close(_sock); #endif _sock = INVALID_SOCKET; _clients.clear(); } } //查询是否有待处理消息 bool OnRun() { if(IsRun()) { fd_set fdRead;//建立集合 fd_set fdWrite; fd_set fdExcept; FD_ZERO(&fdRead);//清空集合 FD_ZERO(&fdWrite); FD_ZERO(&fdExcept); FD_SET(_sock,&fdRead);//放入集合 FD_SET(_sock,&fdWrite); FD_SET(_sock,&fdExcept); timeval s_t = {2,0};//select最大响应时间 SOCKET maxSock = _sock;//最大socket //把连接的客户端 放入read集合 for(int n=_clients.size()-1; n>=0; --n) { FD_SET(_clients[n]->GetSockfd(),&fdRead); if(maxSock GetSockfd()) { maxSock = _clients[n]->GetSockfd(); } } //select函数筛选select int ret = select(maxSock+1,&fdRead,&fdWrite,&fdExcept,&s_t); if(ret FD_CLR(_sock,&fdRead);//清理 Accept();//连接 } //遍历所有socket 查看是否有待处理事件 for(int n=0; n if(-1 == RecvData(_clients[n]))//处理请求 客户端退出的话 { std::vector::iterator iter = _clients.begin()+n;//找到退出客户端的地址 if(iter != _clients.end())//如果是合理值 { delete _clients[n]; _clients.erase(iter);//移除 } } } } //printf("空闲时间处理其他业务\n"); return true; } return false; } //判断是否工作中 bool IsRun() { return _sock != INVALID_SOCKET; } //发送数据 int SendData(DataHeader *head,SOCKET temp_socket) { if(IsRun() && head) { send(temp_socket,(const char*)head,head->date_length,0); return 1; } return 0; } //向所有人发送数据 void SendDataToAll(DataHeader *head) { for(int n=0;n //接收客户端发送的数据 int buf_len = recv(t_client->GetSockfd(), _Recv_buf, RECV_BUFFER_SIZE, 0); if(buf_len //选出包头数据 DataHeader* header = (DataHeader*)t_client->MsgBuf(); //判断消息缓冲区内数据长度是否大于等于报文长度 避免少包问题 if(t_client->GetLen() >= header->date_length) { //计算出消息缓冲区内剩余未处理数据的长度 int size = t_client->GetLen() - header->date_length; //响应数据 NetMsg(header,t_client->GetSockfd()); //将消息缓冲区剩余未处理的数据前移 memcpy(t_client->MsgBuf(), t_client->MsgBuf() + header->date_length, size); //消息缓冲区的数据末尾前移 t_client->SetLen(size); } else { //消息缓冲区数据不足 break; } } return 0; } //响应数据 void NetMsg(DataHeader *head,SOCKET temp_socket) { printf("接收到包头,命令:%d,数据长度:%d\n",head->cmd,head->date_length); switch(head->cmd) { case CMD_LOGIN://登录 接收登录包体 { Login *login = (Login*)head; /* 进行判断操作 */ //printf("%s已登录\n密码:%s\n",login->UserName,login->PassWord); LoginResult *result = new LoginResult; result->Result = 1; SendData(result,temp_socket); } break; case CMD_LOGOUT://登出 接收登出包体 { Logout *logout = (Logout*)head; /* 进行判断操作 */ //printf("%s已登出\n",logout->UserName); LogoutResult *result = new LogoutResult(); result->Result = 1; SendData(result,temp_socket); } break; default://错误 { head->cmd = CMD_ERROR; head->date_length = 0; SendData(head,temp_socket); } break; } } private: SOCKET _sock; std::vector _clients;//储存客户端 //缓冲区相关 char *_Recv_buf;//接收缓冲区 }; #endif


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3