原创

Netty解决应用层解码时出现的粘包拆包问题

1.介绍

  1. 目前网上大部分都是tcp处理粘包拆包之类的文章,这么表达是不准确的
  2. 最白话的来描述,首先TCP是协议层,只负责字节流的传送,不负责编解码,根本不会有TCP粘包拆包
  3. 出现粘包和拆包的现象是在应用层,接收tcp字节流解码成字符的过程中 不能正确解析才会出现粘拆包现象

2.netty解决应用层解码出现粘拆包问题

  1. LineBasedFrameDecoder是回车换行解码器
  2. DelimiterBasedFrameDecoder是分隔符解码器
  3. FixedLengthFrameDecoder是固定长度解码器
  4. LengthFieldBasedFrameDecoder 自定义解码器跟编码器

这些都可以防止应用层解码出现粘拆包问题,下面介绍最简单好用也是主流的解决方案

3.Google ProtoBuf编解码器

GerantServerInitializer.java服务端初始化类以及protobuf编解码器

public class GerantServerInitializer extends ChannelInitializer<Channel> {

    private GerantServerHandle gerantServerHandle = new GerantServerHandle();

    @Override
    protected void initChannel(Channel channel) throws Exception {
        channel.pipeline()
                .addLast(new ProtobufVarint32FrameDecoder())
                .addLast(new ProtobufDecoder(GerantReqProtobuf.GerantReqProtocol.getDefaultInstance()))
                .addLast(new ProtobufVarint32LengthFieldPrepender())
                .addLast(new ProtobufEncoder())
                .addLast(gerantServerHandle);
    }
}

GerantServerHandle.java服务端业务处理类以及接收protobuf消息

@ChannelHandler.Sharable
public class GerantServerHandle extends SimpleChannelInboundHandler<GerantReqProtobuf.GerantReqProtocol> {

    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, GerantReqProtobuf.GerantReqProtocol msg) throws Exception {
        //接收Protobuf
        System.out.println("收到"+ msg.getReqMsg()+",Type->"+msg.getType().getNumber());
    }
    ....
}

GerantSocketclient.java客户端连接并发送protobuf消息

public class GerantSocketclient {

    private static final Integer PORT = 6288;

    public static void main(String[] args){

        // 首先,netty通过ServerBootstrap启动服务端
        Bootstrap client = new Bootstrap();

        EventLoopGroup group = new NioEventLoopGroup();
        try{
            client.group(group);
            client.channel(NioSocketChannel.class);
            client.handler(new ChannelInitializer<Channel>() {  //通道是NioSocketChannel
                @Override
                protected void initChannel(Channel ch) throws Exception {
                    //Protobuf编解码器,一定要加在SimpleClientHandler 的上面
                    ch.pipeline()
                            .addLast(new ProtobufVarint32FrameDecoder())
                            .addLast(new ProtobufDecoder(GerantReqProtobuf.GerantReqProtocol.getDefaultInstance()))
                            .addLast(new ProtobufVarint32LengthFieldPrepender())
                            .addLast(new ProtobufEncoder())
                            .addLast(new SimpleClientHandler());
                }
            });

            //连接服务器
            ChannelFuture future = client.connect("127.0.0.1", PORT).sync();
            if(future.isSuccess()){
                System.out.println("客户端链接成功");
            }
            //发送Protobuf数据
            GerantReqProtobuf.GerantReqProtocol.Builder builder = GerantReqProtobuf.GerantReqProtocol.newBuilder();
            builder.setType(GerantReqProtobuf.ChatType.CHAT_TYPE_PUBLIC);
            builder.setReqMsg("cliend,send protobuf消息");

            for(int i=0;i<10;i++) {
                ChannelFuture futures = future.channel().writeAndFlush(builder.build());
                futures.addListener((ChannelFutureListener) channelFuture ->
                        System.out.println("客户端手动发消息成功"));
            }
            //当通道关闭了,就继续往下走
            future.channel().closeFuture().sync();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //优雅的退出程序
            group.shutdownGracefully();
        }
    }
}

这样就不会出现解码时的半包粘包问题

  • ProtobufEncoder:用于对Probuf类型序列化。
  • ProtobufVarint32LengthFieldPrepender:用于在序列化的字节数组前加上一个简单的包头,只包含序列化的字节长度。
  • ProtobufVarint32FrameDecoder:用于decode前解决半包和粘包问题(利用包头中的包含数组长度来识别半包粘包)
  • ProtobufDecoder:反序列化指定的Probuf字节数组为protobuf类型。

  • 解码工具类:ProtobufDecoder+ProtobufVarint32FrameDecoder

    接收消息的时候先接收ProtobufVarint32LengthFieldPrepender消息头,获取消息体字节数组的长度,
    直到获取到等于消息字节数组长度的字节数,才使用ProtobufDecoder进行解码

  • 编码工具类:ProtobufEncoder+ProtobufVarint32LengthFieldPrepender

    通过ProtobufVarint32LengthFieldPrepender讲整个消息体长度作为消息头附加在消息体,
    然后使用ProtobufEncoder进行编码,完成编码后讲编码字节数组通过netty再传出

总结:不管是使用Netty还是使用ProToBuf提供的编解码器,接收TCP字节流后进行解码都是在应用层处理并非TCP的协议层,这里如果只是简单用Netty String或者字节类编解码就可能会粘拆包,可以说是完全是编解码的业务逻辑决定的,因此粘拆(半)包和TCP是没有联系,更不存在TCP粘拆包

正文到此结束
本文目录