Netty中解决粘包/半包

目录

什么是TCP粘包半包?

TCP 粘包/半包发生的原因

解决粘包半包

channelRead和channelReadComplete区别


什么是TCP粘包半包?

                Netty中解决粘包/半包_第1张图片

       假设客户端分别发送了两个数据包 D1 和 D2 给服务端,由于服务端一次读取到的字节数是不确定的,故可能存在以下 4 种情况。

1. 服务端分两次读取到了两个独立的数据包,分别是 D1 和 D2,没有粘包和拆包。

2. 服务端一次接收到了两个数据包,D1 和 D2 粘合在一起,被称为 TCP 粘包。

3. 服务端分两次读取到了两个数据包,第一次读取到了完整的 D1 包和 D2 包的部分内容,第二次读取到了 D2 包的剩余内容,这被称为 TCP 拆包。

4. 服务端分两次读取到了两个数据包,第一次读取到了 D1 包的部分内容 D1_1,第二次读取到了 D1 包的剩余内容 D1_2 和 D2 包的整包。

       如果此时服务端 TCP 接收滑窗非常小,而数据包 D1 和 D2 比较大,很有可能会发生第五种可能,即服务端分多次才能将 D1 和 D2 包接收完全,期间发生多次拆包。


TCP 粘包/半包发生的原因

       由于 TCP 协议本身的机制(面向连接的可靠地协议-三次握手机制)客户端与服务器会维持一个连接(Channel),数据在连接不断开的情况下,可以持续不断地将多个数据包发往服务器,但是如果发送的网络数据包太小,那么他本身会启用 Nagle 算法(可配置是否启用)对较小的数据包进行合并(基于此,TCP 的网络延迟要 UDP 的高些)然后再发送(超时或者包大小足够)。那么这样的话,服务器在接收到消息(数据流)的时候就无法区分哪些数据包是客户端自己分开发送的,这样产生了粘包。服务器在接收到数据库后,放到缓冲区中,如果消息没有被及时从缓存区取走,下次在取数据的时候可能就会出现一次取出多个数据包的情况,造成粘包现象。

       UDP:本身作为无连接的不可靠的传输协议(适合频繁发送较小的数据包),他不会对数据包进行合并发送(也就没有 Nagle 算法之说了),他直接是一端发送什么数据,直接就发出去了,既然他不会对数据合并,每一个数据包都是完整的(数据+UDP头+IP头等等发一次数据封装一次)也就没有粘包一说了。

分包产生的原因简单来说:

        应用程序写入数据的字节大小大于套接字发送缓冲区的大小,就是一个数据包被分成了多次接收。


解决粘包半包

解决方案:

1. 增加分割符。

客户端传送数据时带上分隔符,服务端接收数据时,通过分隔符判断数据是否发送完整。

核心代码如下:

 public static final String DELIMITER_SYMBOL = "@~";

 private static class ChannelInitializerImp extends ChannelInitializer {

        @Override
        protected void initChannel(Channel ch) throws Exception {
            ByteBuf delimiter = Unpooled.copiedBuffer(DELIMITER_SYMBOL.getBytes());
            ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024,
                    delimiter));
            ch.pipeline().addLast(new DelimiterServerHandler());
        }
    }

2. 消息定长,例如每个报文的大小为固定长度 200 字节,如果不够,空位补空格。

   //客户端按照request的长度发送数据,服务端以request的长度接收数据
   public final static String REQUEST = "hello,netty!";

   private static class ChannelInitializerImp extends ChannelInitializer {

        @Override
        protected void initChannel(Channel ch) throws Exception {
            ch.pipeline().addLast(
                    new FixedLengthFrameDecoder(
                            FixedLengthEchoClient.REQUEST.length()));
            ch.pipeline().addLast(new FixedLengthServerHandler());
       }
   }

   //服务端按照response的长度发送数据,服务端以response的长度接收数据
   public static final String RESPONSE = "Welcome to Netty!";
  
   private static class ChannelInitializerImp extends ChannelInitializer {
        @Override
        protected void initChannel(Channel ch) throws Exception {
            ch.pipeline().addLast(
                    new FixedLengthFrameDecoder(
                            FixedLengthEchoServer.RESPONSE.length()));
            ch.pipeline().addLast(new FixedLengthClientHandler());
        }
    }

3. 将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段,通常设计思路为消息头的第一个字段使用int32来表示消息的总长度,使用LengthFieldBasedFrameDecoder。

   //客户端跟服务端handle都需要添加如下代码
   ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 0, 4, 0, 4));

   //maxFrameLength: 指定消息的最大长度,超过这个长度的消息会被丢弃。
   //lengthFieldOffset: 指定长度字段在消息中的偏移量。
   //lengthFieldLength: 指定长度字段的长度。
   //lengthAdjustment: 在计算消息长度时,需要添加的偏移量,通常为长度字段本身的长度。
   //initialBytesToStrip: 解析出一条完整消息后,需要跳过的字节数,通常设置为长度字段的长度。

channelRead和channelReadComplete区别

两者的区别:
       Netty 是在读到完整的业务请求报文后才调用一次业务 ChannelHandler 的 channelRead
方法,无论这条报文底层经过了几次 SocketChannel 的 read 调用。
       但是 channelReadComplete 方法并不是在业务语义上的读取消息完成后被触发的,而是
在每次从 SocketChannel 成功读到消息后,由系统触发,也就是说如果一个业务消息被 TCP
协议栈发送了 N 次,则服务端的 channelReadComplete 方法就会被调用 N 次。

       简单理解就是,消息会被发送到接收缓冲区,如果一条消息很大需要经过两次发送到接收缓冲区才能发完,当收到一个完整的应用层消息报文,channelRead会被触发一次。而ReadComplete方法每次读取完接收缓冲区的报文,channelReadComplete才会被触发一次。

你可能感兴趣的:(分布式中间件,网络,tcp/ip,网络协议,服务器,后端)