Netty通信遇到了TCP拆包粘包问题?看这篇文章如何解决

简介: 在上一篇文章中主要是使用Springboot开发了一个Netty通信的基本案例,这篇文章是在上一篇文章的基础之上进行讲解的,主要是考虑传输数据如果遇到粘包问题该如何解决。这篇文章会按照一下步骤进行讲解,希望对你有所收获:1、什么是TCP粘包拆包2、Netty中粘包问题的问题重现3、Netty中粘包问题的解决方案OK,在你心中有这么一个基本的脉络之后就可以开始今天的文章了。本系列所有的文章都会给出完整的代码,且在电脑上真实运行了一遍,确保无误。

一、什么是TCP拆包和粘包


我们使用TCP协议在传输数据的时候,如果数据块比较大,就会考虑将其切分。把一个大的数据包进行切割成一个个小的数据包发送。这时候就会遇到拆包和粘包的问题。

比如说在这里客户端发送了两个数据包D1和D2到服务端,在传输的时候就可能会遇到下列问题:

v2-3483ab7ce6631467f3426c9bb70a8528_1440w.jpg通过上面这张图相信你基本上能够理解了。不过我们在这里还是需要稍微解释一下:


情况1:D1和D2正常发送,每次发送一个整包。


情况2:D1数据包比较大,D2比较小。第一次发送D1的一部分,第二次发送D1剩下的和D2整包。这叫拆包。


情况3:D1和D2数据包都比较小,一次发送两个整包,这就叫做粘包。


情况4:D1数据包比较小,D2比较大。第一次发送D1整包和D2一部分,第二次发送D2剩下的。这叫拆包。


情况5:D1和D2数据包都比较大,这时候分开发。


为什么会出现这样的问题呢?想要解释清楚,就必须要考虑到计算机网络的相关知识了,TCP在接受数据的时候,有一个滑动窗口来控制接受数据的大小,这个滑动窗口你就可以理解为一个缓冲区的大小。缓冲区满了就会把数据发送。数据包的大小是不固定的,有时候比缓冲区大,有时候小。这时候就会出现上面的现象。


下面我们使用代码来重现这个现象。


二、问题重现


1、前提准备


我们是基于Springboot开发的,因此还是和上一节一样,首先创建一个Springboot的web工程,添加一下依赖:

<!--添加netty依赖-->
<dependency>
      <groupId>io.netty</groupId>
      <artifactId>netty-all</artifactId>
      <version>5.0.0.Alpha1</version>
</dependency>

如果你没有使用maven,下载相关jar包,直接导入IDE中即可。


2、服务端代码开发


步骤一:创建server类


这个server类,在上一篇文章中提到,是一个模板类,直接拿来用即可。

@Component
public class NettyServer {
    EventLoopGroup boss = new NioEventLoopGroup();
    EventLoopGroup work = new NioEventLoopGroup();
    @PostConstruct
    public void start() throws InterruptedException {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(boss, work)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 1024)
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        public void initChannel(SocketChannel sc) throws Exception {
                            sc.pipeline().addLast(new ServerUAVHandler());
                        }
                     });
            ChannelFuture cf = bootstrap.bind(8881).sync();
            System.out.println("启动成功");
    }
    @PreDestroy
    private void destory() throws Exception{
        boss.shutdownGracefully();
        work.shutdownGracefully();
    }
}

在上面的这个代码中同样我们最主要的是关注ServerUAVHandler的实现。


步骤二:Handler的实现


public class ServerUAVHandler extends ChannelHandlerAdapter {
    private int counter=0;
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buf = (ByteBuf) msg;
        byte[] req = new byte[buf.readableBytes()];
        buf.readBytes(req);
        String body = new String(req,"UTF-8");
        System.out.println("客户端信息是:"+body);
        System.out.println(body+",counter:"+ ++counter);
        ByteBuf resp=Unpooled.copiedBuffer(("A"
               +System.getProperty("line.separator")).getBytes());
        ctx.writeAndFlush(resp);
    }
}

在这个类中,使用channelRead方法来读取客户端发送过来的信息。


(1)首先定义了一个counter,用于计算客户端发送了多少条消息。

(2)在channelRead内部,首先将msg转化为ByteBuf。

(3)将buf的数据转化为字节byte

(4)将buf的字节数据转化为String类型,然后输出。

(5)使用ctx的writeAndFlush方法,每收到一个客户端的数据,给对方回复一个A。别忘了还有一个换行符。


在上面的这个代码中,最主要的就是服务端每收到一条客户端的信息,就给其回复一条。也就是说客户端和服务端的消息数量应该是一样的。


3、客户端代码开发


步骤一:创建client类


@Component
public class NettyClient {
    private SocketChannel socketChannel;
    EventLoopGroup group = new NioEventLoopGroup();
    @PostConstruct
    public void start() throws Exception {
        try{
            Bootstrap b = new Bootstrap();
            b.group(group).channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY, true)
                    .option(ChannelOption.SO_KEEPALIVE,true)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        public void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new ClientHandler());
                        }
                    });
            ChannelFuture cf = b.connect("127.0.0.1",8881).sync();
            System.out.println("链接成功");
            cf.channel().closeFuture().sync();
        }finally{
           group.shutdownGracefully();
        }
    }
}

同样的代码的逻辑在上一篇文章中已经说了,我们还是最关注的事件处理类Handler。


步骤二:Handler实现


public class ClientHandler extends ChannelHandlerAdapter {
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ByteBuf resp = null;
        for (int i=0;i<100;i++){
            resp=Unpooled.copiedBuffer(("我爱你"
                +System.getProperty("line.separator")).getBytes());
            ctx.writeAndFlush(resp);
        }
    }
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buf = (ByteBuf) msg;
        byte[] req = new byte[buf.readableBytes()];
        buf.readBytes(req);
        String body = new String(req,"UTF-8");
        System.out.println(body+" ");
    }
}

这个客户端的Handler看起来有点多,一共有两个方法,channelActive和channelRead。


(1)channelActive里面使用for循环给服务器发送了100条,我爱你。每次发送还有在末尾添加一个换行符。


(2)channelRead里面接受服务器返回的消息。

按道理来讲,客户端给服务端发送了100条数据,那么服务端也会返回回来100条。我们来验证一下。

v2-3790228a8d643c10e8d22a6e45616e4d_1440w.jpg

这里输出的是服务端的信息,从上面的输出结果你就会发现,其实客户端的“我爱你”都被黏在了一块。本来100条但是现在却只有17条了,这就是发生了粘包现象。

如何来解决呢?下面我们看看。


三、粘包问题解决


解决的思路很简单,也就是每次发送一个数据包的时候,添加一个标识符,读的时候一直读到这个标识符才表示一个完整的数据包。在上面我们添加的是line.separator,也就是换行符“\n”。


1、服务端server类更改

v2-3eb6d9c33c5969a4f2c23e2c4c2597db_1440w.jpg

2、服务端Handler类更改

v2-2c992006b58e6d7bb7272cdf9727c18e_1440w.jpg

3、客户端Client更改

v2-49a5a4083a77af088683b12a057ba353_1440w.jpg

4、客户端Handler更改

v2-d968fbee75836d314246704974642943_1440w.jpg

客户端和服务端改的地方都一样,不过还是贴了出来,现在我们再运行一波。

v2-d470fe320db1b9de3cd28cba6ac59f42_1440w.jpg

看到没是不是很神奇。我们来分析一下我们都修改了什么。


好像我们就只是在server和client类添加了两个类,一个是LineBasedFrameDecoder,一个是StringDecoder,其他的都是直接删除,这两个类有什么作用呢?


(1)LineBasedFrameDecoder的作用是在读取数据的时候,一直读到是否含有换行符“\n”或者是“\r\n”。如果读到了就表示该结束了。因此就拿到了这一行的数据包。


(2)StringDecoder用于对之前LineBasedFrameDecoder读取的这一行数据包进行解码。将对象转换为字符串。


OK,好像他们俩搭配,干活真不累,现在我们终于可以解决粘包的问题了,但是同时也出现了一个新的问题,那就是如果我们的标识符不是换行符“\n”或者是“\r\n”又该怎么办呢?幸好Netty同样为我们提供了几种其他的解码器,叫做DelimiterBasedFrameDecoder和FixedLengthFrameDecoder,前面这个可以自动完成以分隔符做结束标志的消息,后面这个可以自动完成对定长消息的解码。都可以解决粘包拆包问题。


现在我们解决了一个大问题,但是同时也出现了一个新的大问题,那就是我们的数据都是通过网络进行传输的,因此就需要编码和解码的操作。netty又为我们提供了什么编码解码技术呢?下篇文章更精彩。感谢大家。


相关文章
|
2月前
|
网络协议 前端开发
netty的TCP服务端和客户端实现
本文介绍了使用Netty框架实现TCP服务端和客户端的步骤,包括添加Netty依赖、编写服务端和客户端的代码,涉及NioEventLoopGroup、ServerBootstrap、Bootstrap、ChannelInitializer等核心组件,以及如何启动服务端监听和客户端连接。
214 4
|
4月前
|
编解码 网络协议 开发者
Netty运行原理问题之NettyTCP的粘包和拆包的问题如何解决
Netty运行原理问题之NettyTCP的粘包和拆包的问题如何解决
|
4月前
|
网络协议 安全 网络安全
网络编程:基于socket的TCP/IP通信。
网络编程:基于socket的TCP/IP通信。
279 0
|
6月前
|
网络协议 安全 Java
Java网络编程入门涉及TCP/IP协议理解与Socket通信。
【6月更文挑战第21天】Java网络编程入门涉及TCP/IP协议理解与Socket通信。TCP/IP协议包括应用层、传输层、网络层和数据链路层。使用Java的`ServerSocket`和`Socket`类,服务器监听端口,接受客户端连接,而客户端连接指定服务器并交换数据。基础示例展示如何创建服务器和发送消息。进阶可涉及多线程、NIO和安全传输。学习这些基础知识能助你构建网络应用。
53 1
|
4月前
|
前端开发 网络协议
Netty实战巅峰:从零构建高性能IM即时通讯系统,解锁并发通信新境界
【8月更文挑战第3天】Netty是一款高性能、异步事件驱动的网络框架,适用于开发高并发网络应用,如即时通讯(IM)系统。本文将指导你利用Netty从零构建高性能IM程序,介绍Netty基础及服务器/客户端设计。服务器端使用`ServerBootstrap`启动,客户端通过`Bootstrap`连接服务器。示例展示了简单的服务器启动过程。通过深入学习,可进一步实现用户认证等功能,打造出更完善的IM系统。
189 1
|
4月前
|
移动开发 网络协议 算法
(十)Netty进阶篇:漫谈网络粘包、半包问题、解码器与长连接、心跳机制实战
在前面关于《Netty入门篇》的文章中,咱们已经初步对Netty这个著名的网络框架有了认知,本章的目的则是承接上文,再对Netty中的一些进阶知识进行阐述,毕竟前面的内容中,仅阐述了一些Netty的核心组件,想要真正掌握Netty框架,对于它我们应该具备更为全面的认知。
243 2
|
6月前
|
消息中间件 存储 网络协议
拼多多面试:Netty如何解决粘包问题?
粘包和拆包问题也叫做粘包和半包问题,**它是指在数据传输时,接收方未能正常读取到一条完整数据的情况(只读取了部分数据,或多读取到了另一条数据的情况)就叫做粘包或拆包问题。** 从严格意义上来说,粘包问题和拆包问题属于两个不同的问题,接下来我们分别来看。 ## 1.粘包问题 粘包问题是指在网络通信中,发送方连续发送的多个小数据包被接收方一次性接收的现象。这可能是因为底层传输层协议(如 TCP)会将多个小数据包合并成一个大的数据块进行传输,导致接收方在接收数据时一次性接收了多个数据包,造成粘连。 例如以下案例,正常情况下客户端发送了两条消息,分别为“ABC”和“DEF”,那么接收端也应该收到两
47 0
拼多多面试:Netty如何解决粘包问题?
|
6月前
|
网络协议
netty粘包问题分析
netty粘包问题分析
49 0
|
6月前
|
Java
Netty传输object并解决粘包拆包问题
Netty传输object并解决粘包拆包问题
57 0
|
6月前
|
Java
Netty中粘包拆包问题解决探讨
Netty中粘包拆包问题解决探讨
44 0