socket编程 TCP 粘包和半包 的问题及解决办法

一般在socket处理大数据量传输的时候会产生粘包和半包问题,有的时候tcp为了提高效率会缓冲N个包后再一起发出去,这个与缓存和网络有关系。

粘包 为x.5个包

半包 为0.5个包

由于网络原因 一次可能会来 0.5/1 /2/ 2.5/ 。。。。个包

当接收到时 要先看看那这个包中有多少个完整的包。把完整的包都处理了 也就是说把x都处理了。剩下的0.5留在接收区中,等待下次接收。

这回接收到的就是0.5+1.5/0.5+1.3/0.5+0.5..... 把完整的包都处理了,有残缺的扔掉 0.8的。

一般情况 接收到正确的后都要给发送端一个应答。不给应答的算超时,发送端将重发。

有头没尾的不能扔

没头有尾的可以扔

有头有尾但缺东西可以扔

有头有尾不缺东西不能扔

 

之所以出现粘包和半包现象,是因为TCP当中,只有流的概念,没有包的概念.

可以使用UDP协议.这样可以就可以区分每个包了.但是要确保包的丢失处理.为了提到效率,可以考虑写一个滑动窗口进行收发包.

若采用TCP协议进行传输,就要将每个包区分开来.可以有三种方式.因为TCP是面向流的.流只有打开和关闭,你要用一个流传输多个包,那就要向办法区分出每个包.

一:: 可以每次发送同样大小的包,过大的包不予发送,过小的包,后面部分用固定的字符'\0'进行填充.

二:: 将流按字符处理,抽出一个字符做转义字符(通常Java用'\'来做转义字符,比如"\n"表示换行).假如就设'\'为转义字符,发送方如果流当中出现'\',就在后面在追加一个'\',如果包结束,则用'\'做包的结束符.这样,在接收方,若读取一个单独的'\'或者流结束,就标示前面的内容构成一个包,如果连续读取两个'\',就将两个'\'用一个'\'进行替换.这样,就可以保证原来包中的信息不变,同时也能区分出每个包了.

三:: 在发送方发送一个包的时候,先将这个包的长度发送给对方(一般是4个字节表示包长),然后再将包的内容发送过去.接收方先接收4个字节,看看包的长度,然后按照长度来接收包,这样就不会出错了. 以上三种方法,是网络传输中经常用到的方法.后两种很常见.最后一种,在TCP长连接传输中应用最多. 综合以上的说法,就是要在TCP协议以上再封装一层协议,用来做分包的信息交换. 

 

一般处理是: 一个BUFFER,用于保存当前连接的读缓存

有数据时,Buffer = Buffer + DataIn,不停的接收

收完成后,开始解析Buffer,

根据包的协议,不停的解析Buffer,并形成一个个包进行处理,处理后,Buffer = Buffer - Data,并继续解包。

 

 

 

TCP粘包,拆包及解决方法

粘包拆包问题是处于网络比较底层的问题,在数据链路层、网络层以及传输层都有可能发生。我们日常的网络应用开发大都在传输层进行,由于UDP有消息保护边界,不会发生粘包拆包问题,因此粘包拆包问题只发生在TCP协议中。

 

什么是粘包、拆包?

假设客户端向服务端连续发送了两个数据包,用packet1和packet2来表示,那么服务端收到的数据可以分为三种,现列举如下:

第一种情况,接收端正常收到两个数据包,即没有发生拆包和粘包的现象,此种情况不在本文的讨论范围内。

 

第二种情况,接收端只收到一个数据包,由于TCP是不会出现丢包的,所以这一个数据包中包含了发送端发送的两个数据包的信息,这种现象即为粘包。这种情况由于接收端不知道这两个数据包的界限,所以对于接收端来说很难处理。

 

第三种情况,这种情况有两种表现形式,如下图。接收端收到了两个数据包,但是这两个数据包要么是不完整的,要么就是多出来一块,这种情况即发生了拆包和粘包。这两种情况如果不加特殊处理,对于接收端同样是不好处理的。

 

 

 

为什么会发生TCP粘包、拆包?

发生TCP粘包、拆包主要是由于下面一些原因:

 

1. 应用程序写入的数据大于套接字缓冲区大小,这将会发生拆包。

 

2.应用程序写入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,这将会发生粘包。

 

3.进行MSS(最大报文长度)大小的TCP分段,当TCP报文长度-TCP头部长度>MSS的时候将发生拆包。

 

4.接收方法不及时读取套接字缓冲区数据,这将发生粘包。

 

 

 

粘包、拆包解决办法

TCP本身是面向流的,作为网络服务器,如何从这源源不断涌来的数据流中拆分出或者合并出有意义的信息呢?通常会有以下一些常用的方法:

1、发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。

2、发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。

3、可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。

 

 

如何基于Netty处理粘包、拆包问题?

涉及到相关重要组件:

  1. ByteToMessageDecoder
  2. MessageToMessageDecoder

这两个组件都实现了ChannelInboundHandler接口,这说明这两个组件都是用来解码网络上过来的数据的。而他们的顺序一般是ByteToMessageDecoder位于head channel handler的后面,MessageToMessageDecoder位于ByteToMessageDecoder的后面。Netty中,涉及到粘包、拆包的逻辑主要在ByteToMessageDecoder及其实现中。

ByteToMessageDecoder

顾名思义、ByteToMessageDecoder是用来将从网络缓冲区读取的字节转换成有意义的消息对象的,对于源码层面指的说明的一段是下面这部分:

protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
    try {
        while (in.isReadable()) {
            int outSize = out.size();
 
            if (outSize > 0) {
                fireChannelRead(ctx, out, outSize);
                out.clear();
                
                if (ctx.isRemoved()) {
                    break;
                }
                outSize = 0;
            }
 
            int oldInputLength = in.readableBytes();
            decode(ctx, inout);
 
            if (ctx.isRemoved()) {
                break;
            }
 
            if (outSize == out.size()) {
                if (oldInputLength == in.readableBytes()) {
                    break;
                } else {
                    continue;
                }
            }
 
            if (oldInputLength == in.readableBytes()) {
                throw new DecoderException(
                        StringUtil.simpleClassName(getClass()) +
                        ".decode() did not read anything but decoded a message.");
            }
 
            if (isSingleDecode()) {
                break;
            }
        }
    } catch (DecoderException e) {
        throw e;
    } catch (Throwable cause) {
        throw new DecoderException(cause);
    }
}

为了节省篇幅,我把注释删除掉了,当上面一个channel handler传入的ByteBuf有数据的时候,这里我们可以把in参数看成网络流,这里有不断的数据流入,而我们要做的就是从这个byte流中分离出message,然后把message添加给out。分开将一下代码逻辑:

  1. 当out中有Message的时候,直接将out中的内容交给后面的channel handler去处理。
  2. 当用户逻辑把当前channel handler移除的时候,立即停止对网络数据的处理。
  3. 记录当前in中可读字节数。
  4. decode是抽象方法,交给子类具体实现。
  5. 同样判断当前channel handler移除的时候,立即停止对网络数据的处理。
  6. 如果子类实现没有分理出任何message的时候,且子类实现也没有动bytebuf中的数据的时候,这里直接跳出,等待后续有数据来了再进行处理。
  7. 如果子类实现没有分理出任何message的时候,且子类实现动了bytebuf中的数据,则继续循环,直到解析出message或者不在对bytebuf中数据进行处理为止。
  8. 如果子类实现解析出了message但是又没有动bytebuf中的数据,那么是有问题的,抛出异常。
  9. 如果标志位只解码一次,则退出。

可以知道,如果要实现具有处理粘包、拆包功能的子类,及decode实现,必须要遵守上面的规则,我们以实现处理第一部分的第二种粘包情况和第三种情况拆包情况的服务器逻辑来举例:

对于粘包情况的decode需要实现的逻辑对应于将客户端发送的两条消息都解析出来分为两个message加入out,这样的话callDecode只需要调用一次decode即可。

对于拆包情况的decode需要实现的逻辑主要对应于处理第一个数据包的时候第一次调用decode的时候out的size不变,从continue跳出并且由于不满足继续可读而退出循环,处理第二个数据包的时候,对于decode的调用将会产生两个message放入out,其中两次进入callDecode上下文中的数据流将会合并为一个bytebuf和当前channel handler实例关联,两次处理完毕即清空这个bytebuf。

当然,尽管介绍了ByteToMessageDecoder,用户自己去实现处理粘包、拆包的逻辑还是有一定难度的,Netty已经提供了一些基于不同处理粘包、拆包规则的实现:如DelimiterBasedFrameDecoder、FixedLengthFrameDecoder、LengthFieldBasedFrameDecoder和LineBasedFrameDecoder等等。其中:

DelimiterBasedFrameDecoder是基于消息边界方式进行粘包拆包处理的。

FixedLengthFrameDecoder是基于固定长度消息进行粘包拆包处理的。

LengthFieldBasedFrameDecoder是基于消息头指定消息长度进行粘包拆包处理的。

LineBasedFrameDecoder是基于行来进行消息粘包拆包处理的。

用户可以自行选择规则然后使用Netty提供的对应的Decoder来进行具有粘包、拆包处理功能的网络应用开发。

 

最后

在通常的高性能网络应用中,客户端通常以长连接的方式和服务端相连,因为每次建立网络连接是一个很耗时的操作。比如在RPC调用中,如果一个客户端远程调用的过程中,连续发起了多次调用,而如果这些调用对应于同一个连接的时候,那么就会出现服务器需要对于这些多次调用消息的粘包拆包问题的处理。如果是你,你会选择哪种策略呢?

 

posted @ 2018-10-06 19:43  panchanggui  阅读(9148)  评论(0编辑  收藏  举报