小公司研发总监,既当司令也当兵!
分类: LINUX
2015-12-26 22:33:46
TCP是个"流"协议,所谓流,就是没有界限的一串数据。大家可以想想河里的流水,是连成一片的,其间是没有分界线的。但一般通讯程序开发是需要定义一个个相互独立的数据包的,比如在简易ftp服务器实现中,使用字符串命令执行操作:“cd”,切换目录;“ls”显示当前路径等等,每次发生一个命令,认定为一个独立的数据包。由于TCP"流"的特性以及网络状况,进行数据传输时会出现以下几种情况.
假设我们连续调用两次send分别发送两段data数据(还是以tcp简易服务器为例,切换到chillddir,并显示目录):“cd childdir”和“ls”。在接收端有以下几种接收情况(当然不止这几种情况,这里只列出了有代表性的情况)
A.先接收到“cd childdir”,然后接收到“ls”。
B.先接收到data1的部分数据:“cd”;然后接收到data1余下的部分以及data2的全部:“ childdirls”。
C.先接收到了data1的全部数据和data2的部分数据:“cd childdirl”;然后接收到了data2的余下的数据:“s”。
D.一次性接收到了data1和data2的全部数据:“cd childdirls”。
对于A这种情况正是我们需要的,不再做讨论。对于B,C,D的情况就是大家经常说的"粘包"。
为了避免粘包现象,可采取的一些措施:
(1)对于发送方引起的粘包现象,用户可通过编程设置来避免,TCP提供了强制数据立即传送的操作指令push,TCP软件收到该操作指令后,就立即将本段数据发送出去,而不必等待发送缓冲区满。这种编程设置方法虽然可以避免发送方引起的粘包,但它关闭了优化算法,降低了网络发送效率,影响应用程序的性能,一般不建议使用。
(2)两次发送间隔采用sleep一段时间,减缓粘包发生。这种方式实现很简单,但效率低,而且只是减缓,而不能完全避免。
(3)对于接收方引起的粘包,则可通过优化程序设计、精简接收进程工作量、提高接收进程优先级等措施,使其及时接收数据,从而尽量避免出现粘包现象。这种方式也是只能减缓,不能避免。
(4)设计为应答模式,每次发送后,都必须等到对方应答后再进行下一次发送。这种方式大多数情况是正确的,但不能解决B类似的情况;而且程序实时性差,并且会增加网络流量。
这种方法有两个缺点:其一,为每个连接动态分配一个缓冲区增大了内存的使用;其二,有三个地方需要拷贝数据,一个地方是把数据存放在缓冲区,一个地方是把完整的数据包从缓冲区取出来,一个地方是把数据包从缓冲区中删除。
(2)利用底层的缓冲区来进行拆包
前面提到过这种方法的缺点,通过使用环形缓冲区能够解决第三个地方的拷贝,但不能解决第一个和第二个。
由于TCP也维护了一个缓冲区,所以我们完全可以利用TCP的缓冲区来缓存我们的数据。这样一来就不需要为每一个连接分配一个缓冲区了,另一方面我们知道recv或者wsarecv都有一个参数,用来表示我们要接收多长长度的数据。利用这两个条件我们就可以对第一种方法进行优化。
对于阻塞SOCKET来说,我们可以利用一个循环来接收包头长度的数据,然后解析出代表包体长度的那个变量,再用一个循环来接收包体长度的数据。对于非阻塞的SOCKET,比如完成端口,我们可以提交接收包头长度的数据的请求。当请求返回时,我们判断接收的数据长度是否等于包头长度:若等于,则提交接收包体长度的数据的请求;若不等于,则提交接收剩余数据的请求。当接收包体时,采用类似的方法。