粘包和半包有了解过吗?netty是如何解决这个问题的(代码片段)

Java鱼仔 Java鱼仔     2022-12-27     625

关键词:

本文收录于JavaStarter ,里面有我完整的Java系列文章,学习或面试都可以看看

(一)什么是粘包、半包

在实际的网络开发中或者在面试中,最开始使用TCP协议时经常会碰上粘包和半包的情况,因此我们有必要了解一下什么是粘包,什么是半包,以及如何去解决。

粘包:故名思意就是客户端和服务端之间发送的数据包粘在了一起,原本应该分多条发送的数据包粘在了一起发送。

半包:指的是一条数据包被分割成了多条发送。

粘包和半包发生的根本原因是在TCP协议中,只有流的概念,没有包的概念,消息发送到缓冲区之后是没有边界的说法的,因此就会发生粘包和半包。

(二)粘包半包效果演示

之前写了netty的入门案例,接下来就通过这个入门案例来展示粘包和半包的效果:

首先是粘包:服务端代码如下,没有其他特殊的地方:

public class FirstServer 
    public static void main(String[] args) 
        new ServerBootstrap()
                .group(new NioEventLoopGroup())
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<NioSocketChannel>() 
                    @Override
                    protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception 
                        nioSocketChannel.pipeline().addLast(new StringDecoder());
                        nioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter()
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception 
                                System.out.println(msg);
                            
                        );
                    
                )
                .bind(8080);
    

客户端代码在发送数据时通过for循环发送十次hello:

public class FirstClient 
    public static void main(String[] args) throws InterruptedException 
        Channel channel = new Bootstrap()
                .group(new NioEventLoopGroup())
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<NioSocketChannel>() 
                    @Override
                    protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception 
                        nioSocketChannel.pipeline().addLast(new StringEncoder());
                    
                )
                .connect(new InetSocketAddress("localhost", 8080))
                .sync()
                .channel();
        for (int i = 0; i < 10; i++) 
            channel.writeAndFlush("hello");
        
    

结果发生了粘包,原本应该发送十次的数据一次性全部发过来了

接着是半包,在服务器端增加一行代码:

public class FirstServer 
    public static void main(String[] args) 
        new ServerBootstrap()
                .group(new NioEventLoopGroup())
                .channel(NioServerSocketChannel.class)
                //修改缓冲区大小,设置的小一些
                .option(ChannelOption.SO_RCVBUF,4)
                .childHandler(new ChannelInitializer<NioSocketChannel>() 
                    @Override
                    protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception 
                        nioSocketChannel.pipeline().addLast(new StringDecoder());
                        nioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter()
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception 
                                System.out.println(msg);
                            
                        );
                    
                )
                .bind(8080);
    

然后再次重新运行:既发生了粘包,又发生了半包。

(三)粘包半包原因分析

看了效果之后,我们来分析一下为什么会发生粘包半包。

TCP协议中,每发送一次数据就需要进行一次ack确认,但是这样就意味着数据的发送将是串行的。于是TCP协议中引入了一个叫做滑动窗口的概念。

滑动窗口其实就是一个缓冲区,在滑动窗口内发送的数据,无需接收响应,可以继续发送。当第一个数据ack确认之后,滑动窗口就会向下移动一个单位。大致的流程图就像下面这样:

当接收方的滑动窗口设置足够大,并且接收方处理不及时的情况下,发送方发过来的数据就会在接收方的滑动窗口中缓冲多个报文,最终导致粘包。

当接收方的滑动窗口设置小于实际发送量,就只能先处理一部分数据,等ack确认后再处理后续的,就导致了半包的情况。

除了TCP层之外,Nagle算法也会造成粘包,网卡的MSS限制也会造成半包。

(四)粘包、半包解决方案

提供两种解决粘包半包的思路:

1、指定消息的长度,在发送端和接收端都指定长度

2、在数据中插入分隔符

下面是netty中的粘包解决方案:

4.1 指定消息的长度(定长解码器)

netty提供了定长解码器,可以指定消息的长度,适用于那些每次发送字符长度都一致的场景。修改服务器端,设置定长解码器的长度为5:

public class FirstServer 
    public static void main(String[] args) 
        new ServerBootstrap()
                .group(new NioEventLoopGroup())
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<NioSocketChannel>() 
                    @Override
                    protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception 
                        nioSocketChannel.pipeline().addLast(new FixedLengthFrameDecoder(5));
                        nioSocketChannel.pipeline().addLast(new StringDecoder());
                        nioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter()
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception 
                                System.out.println(msg);
                            
                        );
                    
                )
                .bind(8080);
    

客户端每次发送长度为5的数据:

public class FirstClient 
    public static void main(String[] args) throws InterruptedException 
        Channel channel = new Bootstrap()
                .group(new NioEventLoopGroup())
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<NioSocketChannel>() 
                    @Override
                    protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception 
                        //将发送的内容encode编码
                        nioSocketChannel.pipeline().addLast(new StringEncoder());
                    
                )
                .connect(new InetSocketAddress("localhost", 8080))
                .sync()
                .channel();
        for (int i = 0; i < 10; i++) 
            channel.writeAndFlush("hello");
        
    

观察最后的结果:

这种方法的缺点在于,对于长度不确定的数据无法做到拆分。

4.2 指定分隔符(分隔符解码器)

netty中提供了分隔符解码器,可以获取到回车的分隔符“\\n”或“\\r\\n”,直接对上面的代码进行修改:

服务器端
//nioSocketChannel.pipeline().addLast(new FixedLengthFrameDecoder(5));
nioSocketChannel.pipeline().addLast(new LineBasedFrameDecoder(1024));

客户端
//channel.writeAndFlush("hello");
channel.writeAndFlush("hello"+"\\n");

4.3 LTC解码器

最后介绍一个最实用的解码器:LengthFieldBasedFrameDecoder,LTC编码器弥补了定长编码器只能限定长度的缺点,实用更加灵活。

LTC解码器中有四个重要的参数:

1、lengthFieldOffset:长度字段偏移量

2、lengthFieldLength:长度字段长度

3、lengthAdjustment:长度字段后,多少个字节之后是内容

4、initialBytesToStrip:跳过多少个字节

我们可以通过源码中的这个例子来理解四个参数:

1、首先是长度偏移量,可以看到解码前的字节中长度之前还有一些字节HDR1,长度是1,因此lengthFieldOffset就等于1

2、长度字段的长度是2,因此lengthFieldLength等于2

3、长度字段后,经过1个字节之后是内容,因此lengthAdjustment等于1

4、initialBytesToStrip设置为3,因此解码之后输出的就是HDR2+ActualContent

下面给出在代码中的实现方式:首先是服务端的代码,增加了LengthFieldBasedFrameDecoder解码器

public class FirstServer 
    public static void main(String[] args) 
        new ServerBootstrap()
                .group(new NioEventLoopGroup())
                .channel(NioServerSocketChannel.class)
                .handler(new LoggingHandler(LogLevel.INFO))
                .childHandler(new ChannelInitializer<NioSocketChannel>() 
                    @Override
                    protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception 
                        //nioSocketChannel.pipeline().addLast(new FixedLengthFrameDecoder(5));
                        nioSocketChannel.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024,0,4,0,4));
                        nioSocketChannel.pipeline().addLast(new StringDecoder());
                        nioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter()
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception 
                                System.out.println(msg.toString());
                            
                        );
                    
                )
                .bind(8080);
    

客户端的代码,在客户端写数据时增加数据的长度:

public class FirstClient 
    public static void main(String[] args) throws InterruptedException 
        Channel channel = new Bootstrap()
                .group(new NioEventLoopGroup())
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<NioSocketChannel>() 
                    @Override
                    protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception 
                        nioSocketChannel.pipeline().addLast(new LengthFieldPrepender(4,false));
                    
                )
                .connect(new InetSocketAddress("localhost", 8080))
                .sync()
                .channel();
        String[] contentList=new String[]"hello","helloWorld","hello, world";
        for (int i = 0; i <3;i++) 
            String content=contentList[i];
            byte[] data = content.getBytes();
            ByteBuf buf= Unpooled.buffer();
            buf.writeBytes(data,0,data.length);
            channel.writeAndFlush(buf);
        
    

(五)总结

到这里关于粘包和半包以及netty解决粘包问题到这里就告一段落了,网络编程想要学好这条路才刚开始,我是鱼仔,我们下期再见!

socket编程粘包和半包问题的及处理

一般在socket处理大数据量传输的时候会产生粘包和半包问题,有的时候tcp为了提高效率会缓冲N个包后再一起发出去,这个与缓存和网络有关系。粘包为x.5个包半包为0.5个包由于网络原因一次可能会来0.5/1/2/2.5/。。。。个包当接... 查看详情

详解啥是tcp粘包和拆包现象并演示netty是如何解决的

参考技术A本文介绍什么是TCP粘包和拆包现象,并通过Netty编写详细的案例来重现TCP粘包问题,最后再通过一个Netty的demo来解决这个问题。具体内容如下TCP编程底层都有粘包和拆包机制,因为我们在C/S这种传输模型下,以TCP协议传... 查看详情

netty进阶——粘包与半包(代码示例)(代码片段)

目录一、消息粘包和消息半包的概述1.1、消息粘包1.2、消息半包二、粘包现象代码示例2.1、粘包现象服务端示例代码2.2、粘包现象客户端示例代码2.3、分别启动服务端,客户端,查看服务端结果输出三、半包现象代码示... 查看详情

netty进阶——粘包与半包(短链接方式解决粘包问题)(代码片段)

目录一、短链接方式解决粘包问题(代码示例)1.1、短链接方式解决粘包问题的服务端代码示例1.2、短链接方式解决粘包问题的客户端代码示例1.3、分别启动服务端,客户端,查看服务端结果输出一、短链接方式... 查看详情

netty进阶——粘包与半包(短链接方式解决粘包问题)(代码片段)

目录一、短链接方式解决粘包问题(代码示例)1.1、短链接方式解决粘包问题的服务端代码示例1.2、短链接方式解决粘包问题的客户端代码示例1.3、分别启动服务端,客户端,查看服务端结果输出一、短链接方式... 查看详情

netty进阶——粘包与半包(固定长度方式解决粘包问题)(代码片段)

目录一、固定长度方式解决粘包问题(代码示例)1.1、固定长度方式解决粘包问题的服务端代码示例1.2、固定长度方式解决粘包问题的客户端代码示例1.3、分别启动服务端,客户端,查看服务端结果输出一、固定... 查看详情

netty进阶——粘包与半包(固定长度方式解决粘包问题)(代码片段)

目录一、固定长度方式解决粘包问题(代码示例)1.1、固定长度方式解决粘包问题的服务端代码示例1.2、固定长度方式解决粘包问题的客户端代码示例1.3、分别启动服务端,客户端,查看服务端结果输出一、固定... 查看详情

netty进阶——粘包与半包(预设长度方式解决粘包问题)(代码片段)

目录一、预设长度方式解决粘包问题(代码示例)1.1、预设长度方式解决粘包问题的服务端代码示例1.2、预设长度方式解决粘包问题的客户端代码示例1.3、分别启动服务端,客户端,查看服务端结果输出一、预设... 查看详情

netty进阶——粘包与半包(预设长度方式解决粘包问题)(代码片段)

目录一、预设长度方式解决粘包问题(代码示例)1.1、预设长度方式解决粘包问题的服务端代码示例1.2、预设长度方式解决粘包问题的客户端代码示例1.3、分别启动服务端,客户端,查看服务端结果输出一、预设... 查看详情

netty进阶——粘包与半包(现象分析)

目录一、粘包1.1、粘包现象1.2、粘包原因二、半包2.1、半包现象2.2、半包原因三、粘包与半包现象的本质原因四、MSS限制的了解五、Nagle算法的了解一、粘包1.1、粘包现象发送abcdef,接收abcdef1.2、粘包原因应用层:接收方... 查看详情

netty进阶——粘包与半包(现象分析)

目录一、粘包1.1、粘包现象1.2、粘包原因二、半包2.1、半包现象2.2、半包原因三、粘包与半包现象的本质原因四、MSS限制的了解五、Nagle算法的了解一、粘包1.1、粘包现象发送abcdef,接收abcdef1.2、粘包原因应用层:接收方... 查看详情

netty进阶——粘包与半包(固定分隔符方式解决粘包问题)(代码片段)

目录一、固定分隔符方式解决粘包问题(代码示例)1.1、固定分隔符解决粘包问题的服务端代码示例1.2、固定分隔符方式解决粘包问题的客户端代码示例1.3、分别启动服务端,客户端,查看服务端结果输出一、固... 查看详情

netty进阶——粘包与半包(固定分隔符方式解决粘包问题)(代码片段)

目录一、固定分隔符方式解决粘包问题(代码示例)1.1、固定分隔符解决粘包问题的服务端代码示例1.2、固定分隔符方式解决粘包问题的客户端代码示例1.3、分别启动服务端,客户端,查看服务端结果输出一、固... 查看详情

解决粘包和拆包问题(代码片段)

解决粘包和拆包问题 上一篇我们介绍了如果使用Netty来开发一个简单的服务端和客户端,接下来我们来讨论如何使用解码器来解决TCP的粘包和拆包问题我们知道,TCP是以一种流的方式来进行网络转播的,当tcp三次握手简历通... 查看详情

[编织消息框架][设计协议]解决粘包半包(下)

接下来介绍netty如何切割分包学习目的,了解处理业务,方便以后脱离依赖读者如果不感兴趣或看不懂可以先忽略,难度比较大LengthFieldBasedFrameDecoder.classpublicLengthFieldBasedFrameDecoder(ByteOrderbyteOrder,//大小端模式默认大端ByteOrderBIG_END... 查看详情

netty解决粘包和拆包问题的四种方案(代码片段)

 在RPC框架中,粘包和拆包问题是必须解决一个问题,因为RPC框架中,各个微服务相互之间都是维系了一个TCP长连接,比如dubbo就是一个全双工的长连接。由于微服务往对方发送信息的时候,所有的请求都是使... 查看详情

netty进阶——粘包与半包(滑动窗口)

TCP以一个段(segment)为单位,每发送一个段就需要进行一次确认应答(ack)处理,但如果这么做,缺点是包的往返时间越长性能就越差。为了解决此问题,引入了窗口概念,窗口大小即决定了... 查看详情

netty进阶——粘包与半包(滑动窗口)

TCP以一个段(segment)为单位,每发送一个段就需要进行一次确认应答(ack)处理,但如果这么做,缺点是包的往返时间越长性能就越差。为了解决此问题,引入了窗口概念,窗口大小即决定了... 查看详情