Chinaunix首页 | 论坛 | 博客
  • 博客访问: 757931
  • 博文数量: 144
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 1150
  • 用 户 组: 普通用户
  • 注册时间: 2014-03-17 14:32
个人简介

小公司研发总监,既当司令也当兵!

文章分类

全部博文(144)

分类: LINUX

2015-12-26 22:33:46


part1:粘包

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的情况就是大家经常说的"粘包"。

part2:粘包的原因

"粘包"可发生在发送端也可发生在接收端。
1.由Nagle算法造成的发送端的粘包:Nagle算法是一种改善网络传输效率的算法。简单的说,当我们提交一段数据给TCP发送时,TCP并不立刻发送此段数据,而是等待一小段时间,看看在等待期间是否还有要发送的数据;若有,则会一次把这两段数据发送出去。这是对Nagle算法一个简单的解释。详细的请看相关书籍。像C和D的情况就有可能是Nagle算法造成的。
2.接收端接收不及时造成的接收端粘包:TCP会把接收到的数据存在自己的缓冲区中,然后通知应用层取数据。当应用层由于某些原因不能及时的把TCP的数据取出来,就会造成TCP缓冲区中存放了几段数据,而造成粘包。

什么时候要考虑“粘包”:
1. 如果利用tcp每次发送数据,就与对方建立连接,然后双方发送完一段数据后,就关闭连接,这样就不会出现粘包问题。即在短连接中,是不需要考虑粘包问题的。
2:如果发送数据无结构,如文件传输,这样发送方只管发送,接收方只管接收存储就ok,也不用考虑粘包。
3:如果双方建立连接,需要在连接后一段时间内发送不同结构数据,如连接后,有多种命令型字符串(以简易ftp服务,cd表示切换目录,ls显示当前目录等等)。

part3:封包和拆包

为了避免粘包现象,可采取的一些措施:
    (1)对于发送方引起的粘包现象,用户可通过编程设置来避免,TCP提供了强制数据立即传送的操作指令push,TCP软件收到该操作指令后,就立即将本段数据发送出去,而不必等待发送缓冲区满。种编程设置方法虽然可以避免发送方引起的粘包,但它关闭了优化算法,降低了网络发送效率,影响应用程序的性能,一般不建议使
    (2)两次发送间隔采用sleep一段时间,减缓粘包发生。这种方式实现很简单,但效率低,而且只是减缓,而不能完全避免。
    (3)
对于接收方引起的粘包,则可通过优化程序设计、精简接收进程工作量、提高接收进程优先级等措施,使其及时接收数据,从而尽量避免出现粘包现象。这种方式也是只能减缓,不能避免。
    (4)设计为应答模式,每次发送后,都必须等到对方应答后再进行下一次发送。这种方式大多数情况是正确的,但不能解决B类似的情况;而且程序实时性差,并且会增加网络流量。

  
由于以上一些常见方法都有较为明显的缺点,后来学者提出一种添加额外识别信息的方式来应对“粘包”问题。同样,以简易FTP服务器为例:我们在实现前规定,所有命令发送采用“(命令标示)+(命令长度)+(命令内容)”的方式进行发送。那么前面举例的“切换到childdir并显示目录”的操作发送内容:
“CMD+11+cd childdir”
"CMD+2+ls"
按照这种规定,服务器即使收到发生了“粘包”的数据,也能够正确解析。

封包封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部分内容了(以后讲过滤非法包时封包会加入"包尾"内容)。包头其实上是个大小固定的结构体,其中有个结构体成员变量表示包体的长度,这是个很重要的变量,其他的结构体成员可根据需要自己定义。根据包头长度固定以及包头中含有包体长度的变量就能正确的拆分出一个完整的数据包。
对于拆包目前我最常用的是以下两种方式:
    (1)动态缓冲区暂存方式
    之所以说缓冲区是动态的,是因为当需要缓冲的数据长度超出缓冲区的长度时会增大缓冲区长度。大概过程描述如下:
    A,为每一个连接动态分配一个缓冲区,同时把此缓冲区和SOCKET关联,常用的是通过结构体关联;
    B,当接收到数据时首先把此段数据存放在缓冲区中;
    C,判断缓存区中的数据长度是否够一个包头的长度;如不够,则不进行拆包操作,等待数据;
    D,根据包头数据解析出里面代表包体长度的变量;
    E,判断缓存区中除包头外的数据长度是否够一个包体的长度,如不够,则不进行拆包操作,等待数据;
    F,取出整个数据包。这里的"取"的意思是不光从缓冲区中拷贝出数据包,而且要把此数据包从缓存区中删除掉。删除的办法就是把此包后面的数据移动到缓冲区的起始地址。


    这种方法有两个缺点:其一,为每个连接动态分配一个缓冲区增大了内存的使用;其二,有三个地方需要拷贝数据,一个地方是把数据存放在缓冲区,一个地方是把完整的数据包从缓冲区取出来,一个地方是把数据包从缓冲区中删除。

    (2)利用底层的缓冲区来进行拆包

    前面提到过这种方法的缺点,通过使用环形缓冲区能够解决第三个地方的拷贝,但不能解决第一个和第二个。
    由于TCP也维护了一个缓冲区,所以我们完全可以利用TCP的缓冲区来缓存我们的数据。这样一来就不需要为每一个连接分配一个缓冲区了,另一方面我们知道recv或者wsarecv都有一个参数,用来表示我们要接收多长长度的数据。利用这两个条件我们就可以对第一种方法进行优化。
     对于阻塞SOCKET来说,我们可以利用一个循环来接收包头长度的数据,然后解析出代表包体长度的那个变量,再用一个循环来接收包体长度的数据。对于非阻塞的SOCKET,比如完成端口,我们可以提交接收包头长度的数据的请求。当请求返回时,我们判断接收的数据长度是否等于包头长度:若等于,则提交接收包体长度的数据的请求;若不等于,则提交接收剩余数据的请求。当接收包体时,采用类似的方法。



阅读(2558) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~