ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

TCP数据粘包的处理

TCP数据粘包的处理 1. 背锅侠TCP在前面介绍套接字通信的时候说到了TCP是传输层协议它是一个面向连接的、安全的、流式传输协议。因为数据的传输是基于流的所以发送端和接收端每次处理的数据的量处理数据的频率可以不是对等的可以按照自身需求来进行决策。TCP协议是优势非常明显但是有时也会给我们造成困扰正所谓成也萧何败萧何。假设我们有如下需求客户端和服务器之间要进行基于TCP的套接字通信通信过程中客户端会每次会不定期给服务器发送一个不定长度的有特定含义的字符串。通信的服务器端每次都需要接收到客户端这个不定长度的字符串并对其进行解析根据上面的描述服务器在接收数据的时候有如下几种情况一次接收到了客户端发送过来的一个完整的数据包一次接收到了客户端发送过来的N个数据包由于每个包的长度不定无法将各个数据包拆开一次接收到了一个或者N个数据包 下一个数据包的一部分还是很悲剧无法将数据包拆开一次收到了半个数据包下一次接收数据的时候收到了剩下的一部分下个数据包的一部分更悲剧头大了另外还有一些不可抗拒的因素比如客户端和服务器端的网速不一样发送和接收的数据量也会不一致对于以上描述的现象很多时候我们将其称之为TCP的粘包问题但是这种叫法不太对的本身TCP就是面向连接的流式传输协议特性如此我们却说是TCP这个协议出了问题这只能说是使用者的无知。多个数据包粘连到一起无法拆分是我们的需求过于复杂造成的是程序猿的问题而不是协议的问题TCP协议表示这锅它不想背。现在问题来了服务器端如果想保证每次都能接收到客户端发送过来的这个不定长度的数据包程序猿应该如何解决这个问题呢下面给大家提供几种解决方案使用标准的应用层协议比如http、https来封装要传输的不定长的数据包在每条数据的尾部添加特殊字符, 如果遇到特殊字符, 代表当条数据接收完毕了有缺陷: 效率低, 需要一个字节一个字节接收, 接收一个字节判断一次, 判断是不是那个特殊字符串在发送数据块之前, 在数据块最前边添加一个固定大小的数据头, 这时候数据由两部分组成数据头数据块数据头存储当前数据包的总字节数接收端先接收数据头然后在根据数据头接收对应大小的字节数据块当前数据包的内容2. 解决方案如果使用TCP进行套接字通信如果发送的数据包粘连到一起导致接收端无法解析我们通常使用添加包头的方式轻松地解决掉这个问题。关于数据包的包头大小可以根据自己的实际需求进行设定这里没有啥特殊需求因此规定包头的固定大小为4个字节用于存储当前数据块的总字节数。2.1 发送端对于发送端来说数据的发送分为4步根据待发送的数据长度N动态申请一块固定大小的内存N44是包头占用的字节数将待发送数据的总长度写入申请的内存的前四个字节中此处需要将其转换为网络字节序大端将待发送的数据拷贝到包头后边的地址空间中将完整的数据包发送出去字符串没有字节序问题释放申请的堆内存。由于发送端每次都需要将这个数据包完整的发送出去因此可以设计一个发送函数如果当前数据包中的数据没有发送完就让它一直发送处理代码如下/* 函数描述: 发送指定的字节数 函数参数: - fd: 通信的文件描述符(套接字) - msg: 待发送的原始数据 - size: 待发送的原始数据的总字节数 函数返回值: 函数调用成功返回发送的字节数, 发送失败返回-1 */ int writen(int fd, const char* msg, int size) { const char* buf msg; int count size; while (count 0) { int len send(fd, buf, count, 0); if (len -1) { close(fd); return -1; } else if (len 0) { continue; } buf len; count - len; } return size; }有了这个功能函数之后就可以发送带有包头的数据块了具体处理动作如下/* 函数描述: 发送带有数据头的数据包 函数参数: - cfd: 通信的文件描述符(套接字) - msg: 待发送的原始数据 - len: 待发送的原始数据的总字节数 函数返回值: 函数调用成功返回发送的字节数, 发送失败返回-1 */ int sendMsg(int cfd, char* msg, int len) { if(msg NULL || len 0 || cfd 0) { return -1; } // 申请内存空间: 数据长度 包头4字节(存储数据长度) char* data (char*)malloc(len4); int bigLen htonl(len); memcpy(data, bigLen, 4); memcpy(data4, msg, len); // 发送数据 int ret writen(cfd, data, len4); // 释放内存 free(data); return ret; }关于数据的发送最后再次强调字符串没有字节序问题但是数据头不是字符串是整形因此需要从主机字节序转换为网络字节序再发送。2.2 接收端了解了套接字的发送端如何发送数据接收端的处理步骤也就清晰了具体过程如下1.首先接收4字节数据并将其从网络字节序转换为主机字节序这样就得到了即将要接收的数据的总长度2.根据得到的长度申请固定大小的堆内存用于存储待接收的数据3.根据得到的数据块长度接收固定数目的数据保存到申请的堆内存中4.处理接收的数据5.释放存储数据的堆内存从数据包头解析出要接收的数据长度之后还需要将这个数据块完整的接收到本地才能进行后续的数据处理因此需要编写一个接收数据的功能函数保证能够得到一个完整的数据包数据处理函数实现如下/* 函数描述: 接收指定的字节数 函数参数: - fd: 通信的文件描述符(套接字) - buf: 存储待接收数据的内存的起始地址 - size: 指定要接收的字节数 函数返回值: 函数调用成功返回发送的字节数, 发送失败返回-1 */ int readn(int fd, char* buf, int size) { char* pt buf; int count size; while (count 0) { int len recv(fd, pt, count, 0); if (len -1) { return -1; } else if (len 0) { return size - count; } pt len; count - len; } return size; }这个函数搞定之后就可以轻松地接收带包头的数据块了接收函数实现如下/* 函数描述: 接收带数据头的数据包 函数参数: - cfd: 通信的文件描述符(套接字) - msg: 一级指针的地址函数内部会给这个指针分配内存用于存储待接收的数据这块内存需要使用者释放 函数返回值: 函数调用成功返回接收的字节数, 发送失败返回-1 */ int recvMsg(int cfd, char** msg) { // 接收数据 // 1. 读数据头 int len 0; readn(cfd, (char*)len, 4); len ntohl(len); printf(数据块大小: %d\n, len); // 根据读出的长度分配内存1 - 这个字节存储\0 char *buf (char*)malloc(len1); int ret readn(cfd, buf, len); if(ret ! len) { close(cfd); free(buf); return -1; } buf[len] \0; *msg buf; return ret; }这样在进行套接字通信的时候通过调用封装的sendMsg()和recvMsg()就可以发送和接收带数据头的数据包了而且完美地解决了粘包的问题。
返回列表