Netty粘包与拆包解决方案(一)

在RPC框架中,粘包与拆包问题是必须解决的一个问题,因为RPC框架中,各个微服务相互之间都是维护一个TCP长连接,比如dubbo就是一个全双工的长连接。由于微服务往对方发送消息的时候,所有的请求都是使用的同一个连接,这样就会产生粘包和拆包的问题,就会出现丢包的情况,Netty提供了更好的解决方案。

1、粘包和拆包

产生粘包和拆包的主要原因是:操作系统在发送TCP数据包的时候,底层会有一个缓冲区,例如1024字节大小,如果一次请求发送的数据量比较小,没达到缓存区大小,TCP则会将多个请求合并为同一个请求进行发送,这就形成了粘包问题;如果一次请求发送的数据量比较大,超过了缓冲区大小,TCP就会将其拆分为多次发送,这就是拆包问题,也就是将一个大的包拆分为多个小包进行发送。

2、常见解决方案

(1)客户端在发送数据包的时候,每个包都固定长度,比如1024字节大小,如果客户端发送的数据长度不足1024个字节,则通过补充空格的方式补全到指定长度进行发送;

(2)客户端在每个包的末尾使用固定的分隔符,例如\n\r,如果一个包被拆分了,则等待下一个包发送过来之后找到其中的\r\n,然后对其拆分后的头部部分与前一个包的剩余部分进行合并,这样就得到了一个完整的包;

(3)将消息分为头部和消息体,在头部中保存有当前整个消息的长度,只有在读取到足够长度的消息之后才算是读到了一个完整的消息;

(4)通过自定义协议进行粘包和拆包的处理;

3、Netty提供的粘包和拆包解决方案

3.1 FixedLengthFrameDecoder

对于使用固定长度的粘包和拆包场景,可以使用FixedLengthFrameDecoder,该解码器会每次读取固定长度的消息,如果当前读取的消息不足指定的长度,那么就会等待下一个消息到达后进行补全操作。其使用也比较简单,只需要在构造函数中指定每个消息的长度即可。这里需要注意的是:FixedLengthFrameDecoder只是一个解码器,Netty也只提供了一个解码器,这是因为对于解码器是需要等待下一个包的进行补全的,代码相对比较复杂,而对于编码器,用户可以自定义,因为编码器只需要将长度不足的部分进行补全即可。

//server端
public void bind(int port) throws InterruptedException {
    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)
                .handler(new LoggingHandler(LogLevel.INFO))
                .childHandler(new ChannelInitializer() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ChannelPipeline pipeline = ch.pipeline();
                        //这里将FixedLengthFrameDecoder添加到pipeline中,指定长度为20
                        pipeline.addLast(new FixedLengthFrameDecoder(20));
                        //将前一步解码得到的数据转成字符串
                        pipeline.addLast(new StringDecoder());
                        //这里FixedLengthFrameEncoder自定义,用户将长度不够指定长度的进行补全操作
                        pipeline.addLast(new FixedLengthFrameEncoder(20));
                        //数据处理
                        pipeline.addLast(new EchoServerHandler());
                    }
                });
        ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
        channelFuture.channel().closeFuture().sync();
    } finally {
        bossGroup.shutdownGracefully();
        workGroup.shutdownGracefully();
    }
}

上面的pipeline中,对于入栈的数据,这里主要添加了FixedLengthFrameDecoder和StringDecoder,前面一个用于处理固定长度消息的粘包和拆包问题,第二个则是将处理之后的消息转换成字符串。最后有EchoServerHandler处理最终的数据,处理完毕后,将处理得到的数据交由FixedLengthFrameEncoder处理,自定义编码器,主要作用是将长度不足20的消息进行空格补齐操作。这里FixedLengthFrameEncoder继承了MessageToByteEncoder类,重写了ecoder()方法,在该方法中,主要将消息长度不足20的消息进行空格补全操作;

//自定义FixedLengthFrameEncoder编码器
public class FixedLengthFrameEncoder extends MessageToByteEncoder {
    private int length;
    public FixedLengthFrameEncoder(int length) {
        this.length = length;
    }
    @Override
    protected void encode(ChannelHandlerContext ctx, String msg, ByteBuf out)
            throws Exception {
        //对于超过指定长度的消息,这里需要抛出异常信息
        if (msg.length() > length) {
            throw new UnsupportedOperationException("message length is too large, it is limited, length is: " + length);
        }
        //如果长度不够,需要进行补全操作
        if (msg.length() < length) {
            msg = addSpace(msg);
        }
        //将msg写出
        ctx.writeAndFlush(Unpooled.wrappedBuffer(msg.getBytes()));
    }
    /**
     * 补全操作
     * @param message
     * @return
     */
    private String addSpace(String message) {
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < length - message.length(); i++) {
            builder.append(" ");
        }
        return builder.toString();
    }
}

 ServerHandler最终处理数据的响应,然后发送响应数据给客户端:

public class EchoServerHandler extends SimpleChannelInboundHandler {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg)
            throws Exception {
        System.out.println("server receives message:" + msg.trim());
        ctx.writeAndFlush("hello client");
    }
}
//client客户端
public void connect(String host, int port) throws InterruptedException {
    EventLoopGroup group = new NioEventLoopGroup();
    try {
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(group)
                .channel(NioSocketChannel.class)
                .option(ChannelOption.TCP_NODELAY, true)
                .handler(new ChannelInitializer() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        /**
                         * 对服务器发送的消息进行粘包和拆包处理,由于服务器发送的消息已经进行空格补全操作
                         */
                        ChannelPipeline pipeline = ch.pipeline();
                        //并且长度为20,因而这里指定的长度为20
                        pipeline.addLast(new FixedLengthFrameDecoder(20));
                        //将粘包和拆包处理得到的消息转换成字符串
                        pipeline.addLast(new StringDecoder());
                        //对客户端发送的数据进行补全操作,保证长度为20
                        pipeline.addLast(new FixedLengthFrameEncoder(20));
                        //客户端发送数据给服务器,并处理服务器响应的消息
                        pipeline.addLast(new EchoClientHandler());
                    }
                });
        ChannelFuture channelFuture = bootstrap.connect(host, port).sync();
        channelFuture.channel().closeFuture().sync();
    } finally {
        group.shutdownGracefully();
    }
}

对于客户端而言,其消息的处理流程其实与服务器相似,对于入栈消息,需要进行粘包和拆包处理,然后将其转成字符串,对于出栈消息,则需要将长度不足20的消息进行空格补全操作。客户端与服务器端处理的主要区别在于handler不一样。

EchoClientHandler继承了SimpleChannelInboundHandler,重写了channelRead0()和channelActive()两个方法,这两个方法的主要作用在于:channelActive()会在客户端连接上服务器时执行,也就是说,其连上服务器之后就会往服务器发送消息。而channelRead0()主要是在服务器发送响应给客户端时执行,这里主要输出服务器响应消息,对于服务器而言,EchoServerHandler只重写了channelRead0()方法,这是因为服务器只需要等待客户端发送消息过来,然后在该方法中进行处理,处理完成后直接将响应发送给客户端。

public class EchoClientHandler extends SimpleChannelInboundHandler {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg)
            throws Exception {
        System.out.println("client receives message: " + msg.trim());
    }
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ctx.writeAndFlush("hello server");
    }
}

你可能感兴趣的:(Netty,rpc,网络,java)