学习了龙叔的文章 面试官多次问我TCP粘包,而我为何屡屡受挫?,看到后面有人询问细节,觉得有必要将龙叔的理论和具体的代码联系起来,成为一个完整的解决方案。所以看本文之前,请先读龙叔的文章作为理论基础。
TCP本质上是数据流,从原理上看,没有包的概念,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接口就可以了。这样写简单,但是有如下问题:
无阻塞的发送和接收: 这种方式编码复杂一点,但是解决了阻塞方式引起的问题,是目前的主流解决方案。
发送端的流程图是这样的:
说明如下:
伪代码是这样的:
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); } ``` 复制代码
接收端的流程图是这样的:
我们可以看出,接收部分可发送部分很相似,这样本文就不重复代码了。发送端知道发送数据的实际长度,然后加上记录长度的4个字节,算出数据总长,按照固定长度的办法发送。
接收端则需要动态获得数据的实际长度,它的流程图是这样的:
我们看出,变长法在接收端实际上是两步固定长度法,所以它比固定长度法复杂。但是由于发送端可以灵活的指定数据的长度,也就是每次发送的数据可以不同,应用更加广泛。
流程图和代码读者可以自行可以仿照变长法和特殊字符法来实现,这里就不介绍了。
本文的结论如下:
当我们设计一个系统的时候,简单性和灵活性是一对矛盾。我们的取舍应该根据系统的实际需要,而不是越灵活越好,或者越简单越好。
TCP粘包不是一个设计缺陷,而是TCP数据流的特点。其重点是接收端如何确定接收一个数据流的边界。因为发送端可以知道发送的数据长度,都可以用固定长度法来实现。