TCP粘包的解决方案

基本概念

  1. TCP本质上是数据流,从原理上看,没有包的概念,TCP包对应用程序员可以是透明的 。
  2. 粘包实际上是把底层包的实现和上层流的概念混在一起 。
  3. 粘包问题本质上是如何确定数据流的边界 。
确定边界的几种典型办法1. 固定长度法:一般在简单的私有协议中实现,可以简化流程,方便实现 。
TCP粘包的解决方案

文章插图
 
通讯之前先通过第三方规定本次发送的包长
  • 阻塞发送与接收:
发送:send(fd, wr_data_buf, wr_data_len, 0); /* wr_data_buf 数据缓存,wr_data_len预先设定的固定长度 */接收:recv(fd, wr_data_buf, wr_data_len, 0);
  •  
  • 如以上代码所示,发送和接收就直接调用socketAPI接口就可以了 。这样写简单,但是有如下问题:容易阻塞主进程,引起多余的进程调度和不可控的系统超时;可以用独立的进程或者线程来优化,但会引起复杂的同步逻辑;无法适应大规模的发送端和接收端同时工作的场景 。
  • 无阻塞的发送和接收: 这种方式编码复杂一点,但是解决了阻塞方式引起的问题,是目前的主流解决方案 。发送端的流程图是这样的:Alice 固定长度法发送端流程图说明如下:流程图中假设发送的预设固定长度是1024个字节 。如果利用EPOLL的Level方式,应该在EPOLLOUT的回调函数中调用alice_send_data,隐式实现3->4->3的循环流程 。如果利用EPOLL的Edge方式,应该在EPOLLOUT的回调函数中调用alice_send_epoll,显式实现3-4-3的循环流程 。伪代码是这样的:
static int alice_send_data(int fd, char *wr_data_buf){int n;n = send(fd, wr_data_buf + offset, 1024 - off, MSG_DONTWAIT); /* 无阻塞发送了n个字节*/if (n < 0) {if (errno == EAGAIN || errno == EINTR)return 0;else {return -1; /* error */}} else if (n == 0) {return 1; /* socket close */}offset += n; /*记住总共发了off个字节 */if (off < 1024)/*如果小于预先给定的长度,返回0,继续调用本函数发送 */return 0;return 1; /*发完了,返回1,继续下面的工作 */}static int alice_send_epoll(int fd, char *wr_data_buf) /* edge 方式 */{int offset = 0;int finish;do {finish = alice_send_data(fd, wr_data_buf)} while (!finish);}【TCP粘包的解决方案】 
  •  
  • 接收端的流程图是这样的:Bob 固定长度法接收端流程图我们可以看出,接收部分可发送部分很相似,这样本文就不重复代码了 。
2. 变长法: 在私有和公共协议中实现,比固定长度法稍微复杂一点,但比较灵活:
TCP粘包的解决方案

文章插图
 
通讯之前先通过第三方规定长度的位置,以便接收端获取
  • 发送端知道发送数据的实际长度,然后加上记录长度的4个字节,算出数据总长,按照固定长度的办法发送 。
  • 接收端则需要动态获得数据的实际长度,它的流程图是这样的:Bob 变长法接收端流程图
我们看出,变长法在接收端实际上是两步固定长度法,所以它比固定长度法复杂 。但是由于发送端可以灵活的指定数据的长度,也就是每次发送的数据可以不同,应用更加广泛 。
3. 特殊字符串法:在私有和公共协议中实现,比变长法更复杂,但是节省包头长度字段,处理更加灵活 。
TCP粘包的解决方案

文章插图
 
通讯之前通过第三方规定一个特殊的字符串,比如说'rnrn',接收端才能据此确定数据流的边界 。
  • 发送端可以按照固定长度的办法发送 。
  • 接收端则需要不断地查找接收缓冲里面的所有数据,看是否有特殊字符串的存在 。它的流程图是这样的:Bob 特殊字符串接收端流程图




    推荐阅读