Socket粘包,封包,拆包

粘包、拆包发生缘由算法

发生TCP粘包或拆包有不少缘由,现列出常见的几点,可能不全面,欢迎补充,缓存

一、要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。安全

二、待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。服务器

三、要发送的数据小于TCP发送缓冲区的大小,TCP将屡次写入缓冲区的数据一次发送出去,将会发生粘包。(服务端出现粘包)网络

四、接收数据端的应用层没有及时读取接收缓冲区中的数据,形成一次性接收多个包,出现粘包 (接收端出现粘包)性能

 

什么是粘包

TCP有粘包现象,而UDP不会出现粘包。优化

  • TCP(Transport Control Protocol,传输控制协议)是面向链接的,面向流的。TCP的收发两端都要有成对的Socket,所以,发送端为了将更多有效的包发送出去,采用了合并优化算法(Nagle算法),将屡次、间隔时间短、数据量小的数据合并为一个大的数据块,进行封包处理。这样的包对于接收端来讲,就没办法分辨,因此须要一些特殊的拆包机制。
  • UDP(User Datagram Protocol,用户数据报协议)是无链接的,面向消息的提供高效率服务。不会使用合并优化算法。UDP支持的是一对多的模式,因此接收端的skbuff(套接字缓冲区)采用了链式结构来记录每个到达的UDP包,在每一个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来讲,就容易进行区分处理了。

举个例子
咱们连续发送三个数据包,大小分别是1k,2k ,4k,这三个数据包,都已经到达了接收端的网络堆栈中,若是使用UDP协议,无论咱们使用多大的接收缓冲区去接收数据,咱们必须有三次接收动做,才可以把全部的数据包接收完.而使用TCP协议,咱们只要把接收的缓冲区大小设置在7k以上,咱们就可以一次把全部的数据包接收下来,只须要有一次接收动做。指针

如何处理粘包

  1. 提早通知接收端要传送的包的长度
    粘包问题的根源在于,接收端不知道发送端将要传送的字节流的长度,因此解决粘包的方法就是围绕,如何让发送端在发送数据前,把本身将要发送的字节流总大小让接收端知晓,而后接收端来一个死循环接收完全部数据。

不建议使用,由于程序的运行速度远快于网络传输速度,因此在发送一段字节前,先用send去发送该字节流长度,这样会放大网络延迟带来的性能损耗    内存

 

      2.加分割标识符
{数据段01}+标识符+{数据段02}+标识符字符串

1⃣️将发送的每条消息的首尾都加上特殊标记符,前加"<"  后加">"。这里我采起的是先将要发送的全部消息,首尾加上特殊标记后,都先放在一个字符串string中,而后一次性的发送给接收方,接受以后,再根据标记符< >,将一条条消息择(zhái)出来。(这种方法只适合数据量较小的状况)

2⃣️发送端和接收端约定好一个标识符来区分不一样的数据包,若是接收到了这么一个分隔符,就表示一个完整的包接收完毕。

也不建议使用,由于要发送的数据不少,数据的内容格式也有不少,可能会出现标识符不惟一的状况

 

     3.自定义包头(建议使用)

 

在开始传输数据时,在包头拼上自定义的一些信息,好比前4个字节表示包的长度,5-8个字节表示传输的类型(Type:作一些业务区分),后面为实际的数据包。(这种方法就是所谓的自定义协议,这种方法是最经常使用的)

这样接收方就能够根据接收到的消息长度来动态定义缓冲区的大小。

 

2、Socket的封包、拆包

一、为何基于TCP的通讯程序须要封包、拆包?
答:TCP是流协议,所谓流,就是没有界限的一串数据。可是程序中却有多种不一样的数据包,那就极可能会出现如上所说的粘包问题,因此就须要在发送端封包,在接收端拆包。

 

二、那么如何封包、拆包?
答:封包就是给一段数据加上包头或者包尾。好比说咱们上面为解决粘包所使用的两种方法,其实就是封包与拆包的具体实现。


封包:
封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部份内容了.包头其实上是个大小固定的结构体,其中有个结构体成员变量表示包体的长度,这是个很重要的变量,其余的结构体成员可根据须要本身定义(如传输的类型).根据包头长度固定以及包头中含有包体长度的变量就能正确的拆分出一个完整的数据包.

 

 

 对于拆包目前我最经常使用的是如下两种方式.
    第1种拆包方式:动态缓冲区暂存方式.之因此说缓冲区是动态的是由于当须要缓冲的数据长度超出缓冲区的长度时会增大缓冲区长度.
    大概过程描述以下:
    A,为每个链接动态分配一个缓冲区,同时把此缓冲区和SOCKET关联,经常使用的是经过结构体关联.
    B,当接收到数据时首先把此段数据存放在缓冲区中.
    C,判断缓存区中的数据长度是否够一个包头的长度,如不够,则不进行拆包操做.
    D,根据包头数据解析出里面表明包体长度的变量.
    E,判断缓存区中除包头外的数据长度是否够一个包体的长度,如不够,则不进行拆包操做.
    F,取出整个数据包.这里的"取"的意思是不光从缓冲区中拷贝出数据包,并且要把此数据包从缓存区中删除掉.删除的办法就是把此包后面的数据移动到缓冲区的起始地址.

    这种方法有两个缺点.1.为每一个链接动态分配一个缓冲区增大了内存的使用.2.有三个地方须要拷贝数据,一个地方是把数据存放在缓冲区,一个地方是把完整的数据包从缓冲区取出来,一个地方是把数据包从缓冲区中删除.这种拆包的改进方法会解决和完善部分缺点.

 

采用环形缓冲.可是这种改进方法仍是不能解决第一个缺点以及第一个数据拷贝,只能解决第三个地方的数据拷贝(这个地方是拷贝数据最多的地方).

环形缓冲实现方案是定义两个指针,分别指向有效数据的头和尾.在存放数据和删除数据时只是进行头尾指针的移动

第2种拆包方式会解决这两个问题.

第2种拆包方式:利用底层的缓冲区来进行拆包
   因为TCP也维护了一个缓冲区,因此咱们彻底能够利用TCP的缓冲区来缓存咱们的数据,这样一来就不须要为每个链接分配一个缓冲区了.另外一方面咱们知道recv或者wsarecv都有一个参数,用来表示咱们要接收多长长度的数据.利用这两个条件咱们就能够对第一种方法进行优化了.
   对于阻塞SOCKET来讲,咱们能够利用一个循环来接收包头长度的数据,而后解析出表明包体长度的那个变量,再用一个循环来接收包体长度的数据.

对于非阻塞的SOCKET,好比完成端口,咱们能够提交接收包头长度的数据的请求,当GetQueuedCompletionStatus返回时,咱们判断接收的数据长度是否等于包头长度,若等于,则提交接收包体长度的数据的请求,若不等于则提交接收剩余数据的请求.当接收包体时,采用相似的方法.

 

 

 

三:如何判断包的合法性. 判断包的合法性能够结合下面两种方式来判断.可是想100%的断定出非法包,只能经过信息安全中的知识来断定了,对这种方法这里不作阐述. 1.经过包头的结构来判断包的合法性. 最初的时候我是根据包头来判断包的合法性,好比判断Command是否超出命令范围,nDataLen是否大于最大包的长度.可是这种方法没法过滤掉非法包,当出现非法包时咱们惟一能作的就是断开链接,或许这也是最好的处理办法. 咱们能够给一个完整的包加上开始和结束标志,标志能够是个整数,也能够是一串字符串.以第一种拆包方式为例来讲明.当要拆一个完整包时咱们先从缓冲区有效数据头指针地址搜索包的开始标志,搜索到后而且当前数据够一个包头数据,则判断开始标志和包头是否合法,若合法则根据表明数据长度的变量的值定位到包尾,判断包尾标志是否与咱们定义的一致,若一致则这个包是合法的包.如有一项不一致则继续寻找下个包的开始标志,并把下个合法包的前面的数据所有舍弃. 2.经过逻辑层来判断包的合法性. 当取出一个合法的包时,咱们还要根据当前数据处理的逻辑来判断包的合法性.好比说在登录成功后的某段时间服务器又收到了同一个客户端的登录包,那咱们就能够判断这个包是非法的,简单处理就是断开链接.