netty 4.1.45 TCP拆包粘包原因及解决办法

注:更多netty相关文章请访问博主专栏: netty专栏

netty 4.1.45 第一个netty程序中,编写了一个很简单的程序,那么这个程序在实际工作环境中运行是否正常呢?我们接下来连续发送100次请求,我们的预期应该是受到100次响应。看请求响应是否符合预期。

异常情况模拟

这里对netty 4.1.45 第一个netty程序中的代码进行改造。客户端发送100次请求。服务器和客户端都添加一个计数器来统计发送和接收请求次数。

服务器改造

package com.example;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

import java.text.SimpleDateFormat;
import java.util.Date;

public class MyNettyServer1 {
    public static void main(String[] args) {
        //配置服务端NIO线程组
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup, workGroup)//配置主从线程组
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 1024)//配置一些TCP的参数
                    .childHandler(new MyChildHandler1());//添加自定义的channel
            //绑定8080端口
            ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();
            //服务端监听端口关闭
            ChannelFuture future = channelFuture.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //netty优雅停机
            bossGroup.shutdownGracefully();
            workGroup.shutdownGracefully();
        }
    }
}

class MyChildHandler1 extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel socketChannel) {
        //将自定义的channelhandler添加到channelpipeline的末尾
        socketChannel.pipeline().addLast(new TimeServerHandler1());
    }
}

/**
 * TimeServerHandler这个才是服务端真正处理请求的服务方法
 */
class TimeServerHandler1 extends ChannelInboundHandlerAdapter {

    private int count = 0;

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

        //将请求入参转为ByteBuf对象
        ByteBuf byteBuf = (ByteBuf) msg;
        byte[] bytes = new byte[byteBuf.readableBytes()];
        byteBuf.readBytes(bytes);
        //由于我们这里传的参数是string,所以直接强制转换
        String body = new String(bytes, "UTF-8");

        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String currentTimeStr = "QUERY TIME ORDER".equalsIgnoreCase(body) ?
                format.format(new Date()) + "" : "BAD ORDER";

        //受到一次请求,count加1
        System.out.println("第 " + count++ + " 次收到客户端请求:" + body + "  返回响应:" + currentTimeStr);

        ByteBuf resp = Unpooled.copiedBuffer(currentTimeStr.getBytes());
        ctx.write(resp);//将消息发送到发送缓冲器
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        ctx.flush();//将消息从发送缓冲区中写入socketchannel中
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        ctx.close();
    }
}

客户端改造

package com.example;

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;


public class MyNettyClient1 {
    public static void main(String[] args) {
        //客户端NIO线程组
        EventLoopGroup group = new NioEventLoopGroup();

        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            ch.pipeline().addLast(new TimeClientHandler1());
                        }
                    });

            //异步链接服务器
            ChannelFuture future = bootstrap.connect("127.0.0.1", 8080).sync();
            //等等客户端链接关闭
            future.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //优雅停机
            group.shutdownGracefully();
        }
    }
}

//客户端业务逻辑处理类
class TimeClientHandler1 extends ChannelInboundHandlerAdapter {
    private int count=0;

    /**
     * 客户端与服务器TCP链路链接成功后调用该方法
     *
     * @param ctx
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        byte[] req = "QUERY TIME ORDER".getBytes();
        for (int i = 0; i < 100; i++) {
            ByteBuf firstMsg = Unpooled.buffer(req.length);
            firstMsg.writeBytes(req);
            ctx.writeAndFlush(firstMsg);//写入缓冲并且发送到socketchannel
        }
    }

    /**
     * 读取到服务端相应后执行该方法
     *
     * @param ctx
     * @param msg
     * @throws Exception
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf byteBuf = (ByteBuf) msg;
        byte[] bytes = new byte[byteBuf.readableBytes()];
        byteBuf.readBytes(bytes);
        String body = new String(bytes, "UTF-8");
        System.out.println("第"+ count++ +"次受到服务端返回:" + body);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        System.out.println("Unexpected exception from downstream : " + cause.getMessage());
        ctx.close();
    }
}

运行结果

依次运行服务器和客户端,服务器输出:

第 0 次收到客户端请求:QUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDER  返回响应:BAD ORDER
第 1 次收到客户端请求:QUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDER  返回响应:BAD ORDER

客户端输出:

第0次受到服务端返回:BAD ORDERBAD ORDER

可以看到服务器端只收到两次请求,由于收到的请求是由多个QUERY TIME ORDER拼接在一起的,所以返回结果都是BAD ORDER

客户端只有一次输出,是服务器两次输出结果拼接在一起的。

拆包粘包原因分析

首先要知道的是,我们netty的应用程序之间的通信底层是走的TCP协议。
在TCP协议中,数据会组成一个一个的固定大小的数据报进行传输。TCP层并不知道上层应用层每条消息实际的大小,它也没有必要关心 ,只需要将数据传输到另一端即可。备注:TCP本身可以保证消息的可靠性。

那么问题就来了,TCP两端都是有缓冲区的,一个是发送缓冲区,一个是接收缓冲区。 TCP协议会通过滑动窗口来决定每次发送应该发送多少数据。如果窗口比较小,应用程序的消息数据比较大,那么该消息可能会被拆开发送。如果消息比窗口小,那么就可能多条消息拼接在一起发送,因为客户端发送时时先将消息发送到发送缓冲区,所以是可以将发送缓冲区中的多条消息(数据报)合并发送的。

下面这个图可以清晰的表述粘包拆包的问题:
在这里插入图片描述

注意:拆包粘包不是netty独有的 问题,该问题是TCP协议本身就有的问题。也就是说任何底层采用了TCP协议的通信组价都会有拆包粘包的问题。

要想更加深入的理解TCP拆包粘包需要更加深入的学习TCP协议才行。

拆包粘包解决办法

netty本身已经解决了对拆包粘包问题,他提供了4中解码器,我们来看一下netty的四种处理拆包粘包的解决方案。

LineBasedFrameDecoder 换行符分隔消息

很容易可以想到一种解决办法就是,客户端每次发送一行数据,服务器端每次接收一行数据。

服务端修改

  1. 服务器端需要在channelpipeline中添加LineBasedFrameDecoder解码器。这里我们与StringDecoder解码器配合使用。
  2. 返回客户端的消息需要以换行符结尾
  3. 在channelRead读取客户端请求消息时,由于上面在channelpipeline中已经使用了StringDecoder将消息转为了string,所以可以直接按照string来处理

完整代码如下:

package com.example;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * 模拟拆包粘包问题
 */
public class MyNettyServer2 {
    public static void main(String[] args) {
        //配置服务端NIO线程组
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup, workGroup)//配置主从线程组
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 1024)//配置一些TCP的参数
                    .childHandler(new MyChildHandler2());//添加自定义的channel
            //绑定8080端口
            ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();
            //服务端监听端口关闭
            ChannelFuture future = channelFuture.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //netty优雅停机
            bossGroup.shutdownGracefully();
            workGroup.shutdownGracefully();
        }
    }
}

class MyChildHandler2 extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel socketChannel) {
        //将自定义的channelhandler添加到channelpipeline的末尾
        socketChannel.pipeline()
                .addLast(new LineBasedFrameDecoder(1024))
                .addLast(new StringDecoder())
                .addLast(new TimeServerHandler2());
    }
}

/**
 * TimeServerHandler这个才是服务端真正处理请求的服务方法
 */
class TimeServerHandler2 extends ChannelInboundHandlerAdapter {

    private int count = 0;

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

//        //将请求入参转为ByteBuf对象
//        ByteBuf byteBuf = (ByteBuf) msg;
//        byte[] bytes = new byte[byteBuf.readableBytes()];
//        byteBuf.readBytes(bytes);
//        //由于我们这里传的参数是string,所以直接强制转换
//        String body = new String(bytes, "UTF-8");
        String body = (String) msg;

        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String currentTimeStr = "QUERY TIME ORDER".equalsIgnoreCase(body) ?
                format.format(new Date()) + "" : "BAD ORDER";

        //受到一次请求,count加1
        System.out.println("第 " + count++ + " 次收到客户端请求:" + body + "  返回响应:" + currentTimeStr);

        ByteBuf resp = Unpooled.copiedBuffer((currentTimeStr+System.getProperty("line.separator")).getBytes());
        ctx.write(resp);//将消息发送到发送缓冲区
//        ctx.writeAndFlush(resp);//如果这里使用writeAndFlush,则下面channelReadComplete中就不需要flush了
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        ctx.flush();//将消息从发送缓冲区中写入socketchannel中
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        ctx.close();
    }
}

客户端修改

客户端需要修改的内容与服务器端相同,完整代码如下:

package com.example;

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

/**
 * 模拟拆包粘包问题
 */
public class MyNettyClient2 {
    public static void main(String[] args) {
        //客户端NIO线程组
        EventLoopGroup group = new NioEventLoopGroup();

        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            ch.pipeline()
                                    .addLast(new LineBasedFrameDecoder(1024))
                                    .addLast(new StringDecoder())
                                    .addLast(new TimeClientHandler2());
                        }
                    });

            //异步链接服务器
            ChannelFuture future = bootstrap.connect("127.0.0.1", 8080).sync();
            //等等客户端链接关闭
            future.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //优雅停机
            group.shutdownGracefully();
        }
    }
}

//客户端业务逻辑处理类
class TimeClientHandler2 extends ChannelInboundHandlerAdapter {
    private int count = 0;

    /**
     * 客户端与服务器TCP链路链接成功后调用该方法
     *
     * @param ctx
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        byte[] req = ("QUERY TIME ORDER" + System.getProperty("line.separator")).getBytes();
        for (int i = 0; i < 100; i++) {
            ByteBuf firstMsg = Unpooled.buffer(req.length);
            firstMsg.writeBytes(req);
            ctx.writeAndFlush(firstMsg);//写入缓冲并且发送到socketchannel
        }
    }

    /**
     * 读取到服务端相应后执行该方法
     *
     * @param ctx
     * @param msg
     * @throws Exception
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//        ByteBuf byteBuf = (ByteBuf) msg;
//        byte[] bytes = new byte[byteBuf.readableBytes()];
//        byteBuf.readBytes(bytes);
//        String body = new String(bytes, "UTF-8");

        String body = (String) msg;
        System.out.println("第 " + count++ + " 次受到服务端返回:" + body);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        System.out.println("Unexpected exception from downstream : " + cause.getMessage());
        ctx.close();
    }
}

运行结果

服务器端:在这里插入图片描述
客户端:
在这里插入图片描述
由于序号是从0开始的,所以到99结束,服务器端和客户端都是100条记录,拆包粘包问题解决。

原理分析

原理很简单,客户端发送消息时,以换行符结尾。
服务器接收消息时,以换行符分隔消息。如果服务器接收到的消息不是一条完整消息,则等待下一个数据报的到来,再进行消息的拼接组装。
如此一来就可以就可以解决消息的拆包粘包问题了。

同时LineBasedFrameDecoder支持配置单行最大长度,上面代码中我们配置的是1024.如果超过该长度则抛出异常。

DelimiterBasedFrameDecoder 固定分隔符解码器

顾名思义,就是采用指定的分隔符来进行编解码。
服务端和消费端只需要将上面的LineBasedFrameDecoder替换为DelimiterBasedFrameDecoder并且指定分隔符即可。然后在发送消息时以指定的分隔符结尾。
这里就不贴完整代码了。

服务端修改

class MyChildHandler3 extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel socketChannel) {
        ByteBuf delimiter = Unpooled.copiedBuffer("#".getBytes());//这里我们以 # 号分隔
        socketChannel.pipeline()
                .addLast(new DelimiterBasedFrameDecoder(1024, delimiter))
                .addLast(new StringDecoder())
                .addLast(new TimeServerHandler3());
    }
}

返回消息时指定分隔符:

ByteBuf resp = Unpooled.copiedBuffer((currentTimeStr + "#").getBytes());
        ctx.write(resp);//将消息发送到发送缓冲区

客户端修改

ByteBuf delimiter = Unpooled.copiedBuffer("#".getBytes());//这里我们以 # 号分隔
ch.pipeline()
   .addLast(new DelimiterBasedFrameDecoder(1024, delimiter))//1024是配置的单行数据最大长度
   .addLast(new StringDecoder())
   .addLast(new TimeClientHandler3());

然后发送消息时指定分隔符

public void channelActive(ChannelHandlerContext ctx) {
    byte[] req = ("QUERY TIME ORDER" + "#").getBytes();
    for (int i = 0; i < 100; i++) {
        ByteBuf firstMsg = Unpooled.buffer(req.length);
        firstMsg.writeBytes(req);
        ctx.writeAndFlush(firstMsg);//写入缓冲并且发送到socketchannel
    }
}

FixedLengthFrameDecoder 固定长度的分隔符

固定长度分隔符使用场景比较小,这里就不再描述了,使用起来跟上面类似。

自定义解码器

请点击链接:netty 4.1.45自定义编解码器

注:更多netty相关文章请访问博主专栏: netty专栏

发布了233 篇原创文章 · 获赞 211 · 访问量 90万+

猜你喜欢

转载自blog.csdn.net/fgyibupi/article/details/104224209