Netty入门(十)netty粘包拆包原理

TCP 粘包和拆包基本介绍

由于UDP有消息保护边界,不会发生粘包拆包问题,因此粘包拆包问题只发生在TCP协议中。

  • TCP是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个数据包发给接收端,更有效的发给对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据包,合并成一个大的数据块,然后进行封包。这样做虽然提高了效率,但是接收端就难于分辨出完整的数据包了,因为面向流的通信是无消息保护边界的
  • 由于TCP无消息保护边界, 需要在接收端处理消息边界问题,也就是我们所说的粘包、拆包问题, 看一张图
  • TCP粘包、拆包图解
    在这里插入图片描述
    假设客户端分别发送了两个数据包D1D2给服务端,由于服务端一次读取到字节数是不确定的,故可能存在以下四种情况:
    • 服务端分两次读取到了两个独立的数据包,分别是D1D2,没有粘包和拆包

    • 服务端一次接受到了两个数据包,D1D2粘合在一起,称之为TCP粘包

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

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

TCP 粘包和拆包现象实例

在编写Netty 程序时,如果没有做处理,就会发生粘包和拆包的问题
看一个具体的实例:

服务端

public class MyServer {
    public static void main(String[] args) throws Exception{
        //设置main方法日志级别
        LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
        List<Logger> loggerList = loggerContext.getLoggerList();
        loggerList.forEach(logger -> {
            logger.setLevel(Level.WARN);
        });
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup,workerGroup).channel(NioServerSocketChannel.class)
                    .childHandler(new MyServerInitializer()); //自定义一个初始化类
            ChannelFuture channelFuture = serverBootstrap.bind(7000).sync();
            channelFuture.channel().closeFuture().sync();
        }finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

ChannelInitializer

public class MyServerInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        pipeline.addLast(new MyServerHandler());
    }
}

自定义handler

public class MyServerHandler extends SimpleChannelInboundHandler<ByteBuf>{
    private int count;
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
    	//读取数据,直接按照字节来接受,它不知道该接受多少字节才是一个完整的数据包
    	//完全是靠tcp协议支撑,但是tcp协议是无消息保护边界的,所以可能会产生粘包拆包问题
        byte[] buffer = new byte[msg.readableBytes()];
        msg.readBytes(buffer);

        //将buffer转成字符串
        String message = new String(buffer, Charset.forName("utf-8"));

        System.out.println("服务器接收到数据 " + message);
        System.out.println("服务器接收到消息量=" + (++this.count));

        //服务器回送数据给客户端, 回送一个随机id ,
        ByteBuf responseByteBuf = Unpooled.copiedBuffer(UUID.randomUUID().toString() + " ", Charset.forName("utf-8"));
        ctx.writeAndFlush(responseByteBuf);
    }
}

客户端

public class MyClient {
    public static void main(String[] args)  throws  Exception{
        //设置main方法日志级别
        LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
        List<Logger> loggerList = loggerContext.getLoggerList();
        loggerList.forEach(logger -> {
            logger.setLevel(Level.WARN);
        });
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group).channel(NioSocketChannel.class)
                    .handler(new MyClientInitializer()); //自定义一个初始化类
            ChannelFuture channelFuture = bootstrap.connect("localhost", 7000).sync();
            channelFuture.channel().closeFuture().sync();
        }finally {
            group.shutdownGracefully();
        }
    }
}

ChannelInitializer

public class MyClientInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        pipeline.addLast(new MyClientHandler());
    }
}

自定义handler:

public class MyClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
    private int count;
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        //使用客户端发送10条数据 hello,server 编号
        for(int i= 0; i< 10; ++i) {
            ByteBuf buffer = Unpooled.copiedBuffer("hello,server " + i, Charset.forName("utf-8"));
            ctx.writeAndFlush(buffer);
        }
    }
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
        byte[] buffer = new byte[msg.readableBytes()];
        msg.readBytes(buffer);

        String message = new String(buffer, Charset.forName("utf-8"));
        System.out.println("客户端接收到消息=" + message);
        System.out.println("客户端接收消息数量=" + (++this.count));
    }
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

测试

启动服务端,起一个客户端。
服务端日志:数据一次性全发过来了
在这里插入图片描述
客户端日志:服务端发给客户端的数据,也一次性发过去了
在这里插入图片描述
再起一个客户端:
服务端日志。出现了粘包的问题
在这里插入图片描述
客户端日志:
在这里插入图片描述

TCP 粘包和拆包解决方案

  • 使用自定义协议 + 编解码器来解决
  • 关键就是要解决服务器端每次读取数据长度的问题, 这个问题解决,就不会出现服务器多读或少读数据的问题,从而避免的TCP 粘包、拆包 。

一个具体的实例

要求客户端发送 5Message 对象, 客户端每次发送一个 Message 对象
服务器端每次接收一个Message, 分5次进行解码, 每读取到 一个Message , 会回复一个Message 对象 给客户端.

自定义协议
public class MessageProtocol {//协议包
    private int len; //关键
    private byte[] content;
    public int getLen() {return len;}
    public void setLen(int len) {this.len = len; }
    public byte[] getContent() {return content;}
    public void setContent(byte[] content) { this.content = content;}
}
编解码器

使用自定义协议做泛型
编码器:

public class MyMessageEncoder extends MessageToByteEncoder<MessageProtocol> {
    @Override
    protected void encode(ChannelHandlerContext ctx, MessageProtocol msg, ByteBuf out) throws Exception {
        System.out.println("MyMessageEncoder encode 方法被调用");
        // 写进ByteBuf,解码的时候,再从ByteBuf取出来
        out.writeInt(msg.getLen());
        out.writeBytes(msg.getContent());
    }
}

解码器:

public class MyMessageDecoder extends ReplayingDecoder<Void> {
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        System.out.println("---------------------------------");
        System.out.println("MyMessageDecoder decode 被调用");
        //需要将得到二进制字节码-> MessageProtocol 数据包(对象)
        int length = in.readInt();
        byte[] content = new byte[length];
        in.readBytes(content);

        //封装成 MessageProtocol 对象,放入 out, 传递下一个handler业务处理
        MessageProtocol messageProtocol = new MessageProtocol();
        messageProtocol.setLen(length);
        messageProtocol.setContent(content);

        out.add(messageProtocol);
    }
}
服务端
public class MyServer {
    public static void main(String[] args) throws Exception{
        //设置main方法日志级别
        LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
        List<Logger> loggerList = loggerContext.getLoggerList();
        loggerList.forEach(logger -> {
            logger.setLevel(Level.WARN);
        });
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup,workerGroup).channel(NioServerSocketChannel.class)
                    .childHandler(new MyServerInitializer()); //自定义一个初始化类

            ChannelFuture channelFuture = serverBootstrap.bind(7000).sync();
            channelFuture.channel().closeFuture().sync();
        }finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

ChannelInitializer

public class MyServerInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        pipeline.addLast(new MyMessageDecoder());//解码器
        pipeline.addLast(new MyMessageEncoder());//编码器
        pipeline.addLast(new MyServerHandler());
    }
}

自定义handler:

//处理业务的handler
public class MyServerHandler extends SimpleChannelInboundHandler<MessageProtocol>{
    private int count;
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        //cause.printStackTrace();
        ctx.close();
    }
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, MessageProtocol msg) throws Exception {
        //接收到数据,并处理
        int len = msg.getLen();
        byte[] content = msg.getContent();

        System.out.println("服务器接收到信息如下");
        System.out.println("长度=" + len);
        System.out.println("内容=" + new String(content, Charset.forName("utf-8")));
        System.out.println("服务器接收到消息包数量=" + (++this.count));

        //回复消息
        String responseContent = UUID.randomUUID().toString();
        int responseLen = responseContent.getBytes("utf-8").length;
        byte[]  responseContent2 = responseContent.getBytes("utf-8");
        //构建一个协议包
        MessageProtocol messageProtocol = new MessageProtocol();
        messageProtocol.setLen(responseLen);
        messageProtocol.setContent(responseContent2);

        ctx.writeAndFlush(messageProtocol);
    }
}
客户端
public class MyClient {
    public static void main(String[] args)  throws  Exception{
        //设置main方法日志级别
        LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
        List<Logger> loggerList = loggerContext.getLoggerList();
        loggerList.forEach(logger -> {
            logger.setLevel(Level.WARN);
        });
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group).channel(NioSocketChannel.class)
                    .handler(new MyClientInitializer()); //自定义一个初始化类

            ChannelFuture channelFuture = bootstrap.connect("localhost", 7000).sync();
            channelFuture.channel().closeFuture().sync();
        }finally {
            group.shutdownGracefully();
        }
    }
}

ChannelInitializer

public class MyClientInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        pipeline.addLast(new MyMessageEncoder()); //加入编码器
        pipeline.addLast(new MyMessageDecoder()); //加入解码器
        pipeline.addLast(new MyClientHandler());
    }
}

自定义handler

public class MyClientHandler extends SimpleChannelInboundHandler<MessageProtocol> {
    private int count;
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        //使用客户端发送10条数据 "今天天气冷,吃火锅" 编号
        for(int i = 0; i< 5; i++) {
            String mes = "今天天气冷,吃火锅";
            byte[] content = mes.getBytes(Charset.forName("utf-8"));
            int length = mes.getBytes(Charset.forName("utf-8")).length;

            //创建协议包对象
            MessageProtocol messageProtocol = new MessageProtocol();
            messageProtocol.setLen(length);
            messageProtocol.setContent(content);
            ctx.writeAndFlush(messageProtocol);
        }
    }
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, MessageProtocol msg) throws Exception {
        int len = msg.getLen();
        byte[] content = msg.getContent();

        System.out.println("客户端接收到消息如下");
        System.out.println("长度=" + len);
        System.out.println("内容=" + new String(content, Charset.forName("utf-8")));
        System.out.println("客户端接收消息数量=" + (++this.count));
    }
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        System.out.println("异常消息=" + cause.getMessage());
        ctx.close();
    }
}
测试

启动服务端和一个客户端:
服务端:

---------------------------------
MyMessageDecoder decode 被调用
服务器接收到信息如下
长度=27
内容=今天天气冷,吃火锅
服务器接收到消息包数量=1
MyMessageEncoder encode 方法被调用
---------------------------------
MyMessageDecoder decode 被调用
服务器接收到信息如下
长度=27
内容=今天天气冷,吃火锅
服务器接收到消息包数量=2
MyMessageEncoder encode 方法被调用
---------------------------------
MyMessageDecoder decode 被调用
服务器接收到信息如下
长度=27
内容=今天天气冷,吃火锅
服务器接收到消息包数量=3
MyMessageEncoder encode 方法被调用
---------------------------------
MyMessageDecoder decode 被调用
服务器接收到信息如下
长度=27
内容=今天天气冷,吃火锅
服务器接收到消息包数量=4
MyMessageEncoder encode 方法被调用
---------------------------------
MyMessageDecoder decode 被调用
服务器接收到信息如下
长度=27
内容=今天天气冷,吃火锅
服务器接收到消息包数量=5
MyMessageEncoder encode 方法被调用

客户端:

MyMessageEncoder encode 方法被调用
MyMessageEncoder encode 方法被调用
MyMessageEncoder encode 方法被调用
MyMessageEncoder encode 方法被调用
MyMessageEncoder encode 方法被调用
---------------------------------
MyMessageDecoder decode 被调用
客户端接收到消息如下
长度=36
内容=f0f15163-0af9-4470-a9ec-e44dac36569a
客户端接收消息数量=1
---------------------------------
MyMessageDecoder decode 被调用
客户端接收到消息如下
长度=36
内容=c4dd32a4-cd7a-43e2-8c2f-d2dceb1ae0ac
客户端接收消息数量=2
---------------------------------
MyMessageDecoder decode 被调用
客户端接收到消息如下
长度=36
内容=190f6b2c-e337-4717-a0d9-5f32401e4796
客户端接收消息数量=3
---------------------------------
MyMessageDecoder decode 被调用
客户端接收到消息如下
长度=36
内容=023359ea-22f5-487f-83bd-24cf01e55001
客户端接收消息数量=4
---------------------------------
MyMessageDecoder decode 被调用
客户端接收到消息如下
长度=36
内容=88ad5923-c4ca-4bf3-9f42-2ba2d97d6912
客户端接收消息数量=5

再次启动一个客户端,还是没有出现粘包拆包的问题。

练习

解决TCP 粘包和拆包现象实例中的问题。

自定义协议
public class MyMessageProtocol {
    private int len; //关键
    private byte[] content;
    public int getLen() { return len; }
    public void setLen(int len) {this.len = len;}
    public byte[] getContent() {   return content;   }
    public void setContent(byte[] content) {    this.content = content;   }
}
编解码器

使用自定义协议做泛型

编码器:注意泛型修改为自定义协议

public class MyMessageEncoder extends MessageToByteEncoder<MyMessageProtocol> {
    @Override
    protected void encode(ChannelHandlerContext ctx, MyMessageProtocol msg, ByteBuf out) throws Exception {
        System.out.println("MyMessageEncoder encode 方法被调用");
        out.writeInt(msg.getLen());
        out.writeBytes(msg.getContent());
    }
}

解码器:

public class MyMessageDecoder extends ReplayingDecoder<Void> {
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        System.out.println("---------------------------------");
        System.out.println("MyMessageDecoder decode 被调用");
        //需要将得到二进制字节码-> MessageProtocol 数据包(对象)
        int length = in.readInt();
        byte[] content = new byte[length];
        in.readBytes(content);

        //封装成 MessageProtocol 对象,放入 out, 传递下一个handler业务处理
        MyMessageProtocol messageProtocol = new MyMessageProtocol();
        messageProtocol.setLen(length);
        messageProtocol.setContent(content);
        out.add(messageProtocol);
    }
}
服务端修改

ChannelInitializer修改:

pipeline.addLast(new MyMessageEncoder());//出站
pipeline.addLast(new MyMessageDecoder());//入站

自定义handler修改:

public class MyClientHandler extends SimpleChannelInboundHandler<MyMessageProtocol> {
    private int count;
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        //使用客户端发送10条数据 hello,server 编号
        for(int i= 0; i< 10; ++i) {
            String content="hello,server" + i;
            MyMessageProtocol messageProtocol=new MyMessageProtocol();
            messageProtocol.setLen(content.getBytes(Charset.forName("utf-8")).length);
            messageProtocol.setContent(content.getBytes(Charset.forName("utf-8")));
            ctx.writeAndFlush(messageProtocol);
        }
    }
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, MyMessageProtocol msg) throws Exception {
        int len = msg.getLen();
        byte[] content = msg.getContent();
        System.out.println("客户器接收到信息如下");
        System.out.println("长度=" + len);
        System.out.println("内容=" + new String(content, Charset.forName("utf-8")));
        System.out.println("客户端接收到消息包数量=" + (++this.count));
    }
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}
客户端修改

ChannelInitializer修改:

pipeline.addLast(new MyMessageEncoder());//出站
pipeline.addLast(new MyMessageDecoder());//入站

自定义handler修改:泛型也得修改

public class MyClientHandler extends SimpleChannelInboundHandler<MyMessageProtocol> {
    private int count;
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        //使用客户端发送10条数据 hello,server 编号
        for(int i= 0; i< 5; ++i) {
            String content="hello,server" + i;
            MyMessageProtocol messageProtocol=new MyMessageProtocol();
            messageProtocol.setLen(content.getBytes(Charset.forName("utf-8")).length);
            messageProtocol.setContent(content.getBytes(Charset.forName("utf-8")));
            ctx.writeAndFlush(messageProtocol);
        }
    }
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, MyMessageProtocol msg) throws Exception {
        int len = msg.getLen();
        byte[] content = msg.getContent();
        System.out.println("客户器接收到信息如下");
        System.out.println("长度=" + len);
        System.out.println("内容=" + new String(content, Charset.forName("utf-8")));
        System.out.println("客户端接收到消息包数量=" + (++this.count));
    }
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}
测试

服务端:可以看到服务端接受数据都是分开的,并没有出现某一次数据传输出现粘包拆包现象

---------------------------------
MyMessageDecoder decode 被调用
服务器接收到信息如下
长度=13
内容=hello,server0
服务器接收到消息包数量=1
MyMessageEncoder encode 方法被调用
---------------------------------
MyMessageDecoder decode 被调用
服务器接收到信息如下
长度=13
内容=hello,server1
服务器接收到消息包数量=2
MyMessageEncoder encode 方法被调用
---------------------------------
MyMessageDecoder decode 被调用
服务器接收到信息如下
长度=13
内容=hello,server2
服务器接收到消息包数量=3
MyMessageEncoder encode 方法被调用
---------------------------------
MyMessageDecoder decode 被调用
服务器接收到信息如下
长度=13
内容=hello,server3
服务器接收到消息包数量=4
MyMessageEncoder encode 方法被调用
---------------------------------
MyMessageDecoder decode 被调用
服务器接收到信息如下
长度=13
内容=hello,server4
服务器接收到消息包数量=5
MyMessageEncoder encode 方法被调用

客户端;

MyMessageEncoder encode 方法被调用
MyMessageEncoder encode 方法被调用
MyMessageEncoder encode 方法被调用
MyMessageEncoder encode 方法被调用
MyMessageEncoder encode 方法被调用
---------------------------------
MyMessageDecoder decode 被调用
客户器接收到信息如下
长度=36
内容=a76ab882-3a13-42d7-b03b-9fda18414049
客户端接收到消息包数量=1
---------------------------------
MyMessageDecoder decode 被调用
客户器接收到信息如下
长度=36
内容=d5d51c34-8c9c-4299-a70e-3b20beabf5b2
客户端接收到消息包数量=2
---------------------------------
MyMessageDecoder decode 被调用
客户器接收到信息如下
长度=36
内容=d5fdba9b-915f-42b0-a5ff-9a30fe7c067c
客户端接收到消息包数量=3
---------------------------------
MyMessageDecoder decode 被调用
客户器接收到信息如下
长度=36
内容=b668d81e-4988-4fa0-bf92-0df0c7156e5e
客户端接收到消息包数量=4
---------------------------------
MyMessageDecoder decode 被调用
客户器接收到信息如下
长度=36
内容=d7f4e984-26cb-453e-9bcc-79d047a0e305
客户端接收到消息包数量=5
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值