Netty 与 粘包、拆包

Netty 粘包和拆包

起因

TCP 是面向 "字节流" 的一个协议,管发送,不管分隔,管杀不管埋。他可以保证你消息的可靠性,像自来水一样,打开~关闭,滔滔不绝。但是由于他并没有包的概念,所以我们在进行网络编程的时候的,肯定要想办法把这连绵不绝的字节流分隔成我们需要的数据包,这便是江湖生盛传已久的拆包问题了(其实我感觉这么叫并不恰当,但是大家都这么叫就这么叫吧)。话不多说,来看下怎么弄。

问题描述

比方说你客户端,往服务端发发了一个 AAAA,然后再发一个 BBBB。可能会出现下面5种情况

  1. ------AAAA--------BBBB------
  2. ----------------AAAABBBB----
  3. ------AAAABB---------BB-----
  4. ------AAA---------ABBBB-----
  5. -----AA----AA----BB---BB----

恩。。。等等情况吧

原因分析

由于 TCP/IP 协议的一些优化策略吧,具体我也不是很懂,等以后弄懂了再回来补充。

问题复现

服务端代码

import io.netty.bootstrap.ServerBootstrap;
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.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.util.CharsetUtil; 
/**
 * @author Sean Wu
 */
public class Main {
    
    public static void main(String[] args) throws Exception {
        // Configure the server.
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .handler(new LoggingHandler(LogLevel.DEBUG))
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline p = ch.pipeline();
                            p.addLast(new StringDecoder(CharsetUtil.UTF_8));
                            p.addLast(new StringEncoder(CharsetUtil.UTF_8));
                            p.addLast(new ChannelInboundHandlerAdapter() {
                                @Override
                                public void channelRead(ChannelHandlerContext ctx, Object msg) {
                                    System.out.println("收到消息" + msg.toString());
                                }
                                @Override
                                public void channelReadComplete(ChannelHandlerContext ctx) {
                                    ctx.flush();
                                }
                                
                            });
                        }
                    });

            ChannelFuture f = b.bind(6688).sync();
            f.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

发送的客户端代码

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;

/**
 * @author Sean Wu
 */
public class TestClient {

    public static void main(String[] args) throws Exception {
        NioEventLoopGroup group = new NioEventLoopGroup();

        Bootstrap b = new Bootstrap();
        b.group(group)
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<SocketChannel>() {

                    protected void initChannel(SocketChannel ch) throws Exception {
                        ChannelPipeline p = ch.pipeline();
                        p.addLast(new ChannelInboundHandlerAdapter() {
                            @Override
                            public void channelActive(ChannelHandlerContext ctx) throws Exception {
                                String[] msgs = new String[]{"AAAA", "BBBB"};
                                ByteBuf message = null;
                                for (int i = 0; i < 350; i++) {
                                    message = Unpooled.buffer(4);
                                    message.writeBytes(msgs[i % 2].getBytes());
                                    ctx.writeAndFlush(message);
                                    System.out.println("send msg:" + msgs[i % 2]);
                                }
                            }
                        });

                    }
                });
 
        ChannelFuture f = b.connect("127.0.0.1", 6688).sync();
        f.channel().closeFuture().sync();
        group.shutdownGracefully();


    }
}

正常的客户端发送

image

从结果上,我们可以看到的发送端是在正经发送的,但是服务端就比较奇怪了。那么怎么办呢,继续往下看。

解决思路

  1. 使用定长的包,长度不足的填充0。
  2. 使用换行符分隔每一个包(比如FTP协议)
  3. 使用特殊字符分割每一个包。
  4. 使用消息头和消息体,消息头中包含包的长度信息。
  5. 自定义更复杂协议。

定长的包

上面代码里,我们发的包要么就是AAAA要不就是BBBB,那么我们完全可以采用定长的策略。netty 里有个叫 FixedLengthFrameDecoder 的类,很简单的就可以搞定定长这种事。只要在上面的 initChannel 里加上

p.addLast(new FixedLengthFrameDecoder(4));

就可以了。
ps. 这个一定要放在第一行。

image

定长填充0

那如果我们发的包没有规律的长度的包呢。这个时候我们就可以采用定长填充0的策略。最简单的将客户端的for循环改成下面的样子就可以了:


for (int i = 0; i < 350; i++) {
    message = Unpooled.buffer(10);
    byte[] writeBytes = msgs[i % 2].getBytes();
    message.writeBytes(writeBytes);
    message.writeBytes(new byte[10 - writeBytes.length]);
    
    ctx.writeAndFlush(message);
    System.out.println("send msg:" + msgs[i % 2]);
}

然后你可能要人工过滤一下填充的0。
image

使用特殊字符分隔

我们也可以使用特殊字符分隔我们的包,比方说换行符或者别的一些奇奇怪怪的你喜欢的字符。
特殊字符分隔

p.addLast(new DelimiterBasedFrameDecoder(10,Unpooled.copiedBuffer("~!".getBytes())));

然后我们使用telnet 来测试下。
cmd 然后输入 telnet 127.0.0.1 6688,然后输入 Ctrl + ] , 然后输入 set localecho打开本地回显,然后在敲回车继续连上,然后就有如下效果了。
image
image
如果我们的输入太长会怎么样呢,我们可以试一下。

恩。。。效果如下。
image
或者我们也可以使用换行符做为分隔符。效果上没什么差,用 new LineBasedFrameDecoder() 就好了。

自定义协议

上面两种方法都有明显的缺陷,最好么,还是定一个自己的协议靠谱。话不多说,赶紧来自定义一个协议。

+-------+--------+-------+
| 包头  |  长度  | 内容  |
+-------+--------+-------+

随手一写,一个完美的协议就被我们定好了。那么包头我们用一个特殊的int来表示 0xCAFEBABE(这里只是随便选了一个,不要学这个头,容易出问题).然后我们建个协议类如下

public class SimpleProtocol {

    public static int PROTOCOL_HEAD = 0XCAFEBABE;
    private byte[] content;


    public SimpleProtocol(byte[] content) {
        this.content = content;
    }

    public int getLength(){

        return content.length;
    }

    public byte[] getContent() {
        return content;
    }

    public SimpleProtocol setContent(byte[] content) {
        this.content = content;
        return this;
    }

    @Override
    public String toString() {
        return "SimpleProtocol{" +
                "content=" + new String(content) +
                '}';
    }
}

很简单的一个协议类,可以返回内容和长度。然后我们在写一个编码器

public class SimpleProtocolEncode extends MessageToByteEncoder<SimpleProtocol> {
    @Override
    protected void encode(ChannelHandlerContext ctx, SimpleProtocol msg, ByteBuf out) throws Exception {
        // 写入包头
        out.writeInt(SimpleProtocol.PROTOCOL_HEAD)
                // 写入长度
                .writeInt(msg.getLength())
                // 写入内容
                .writeBytes(msg.getContent());
    }
}

看上去似乎也很简单,然后我们在写一个解码器


public class SimpleProtocolDecode extends ByteToMessageDecoder {

    /**
     * 基本长度,4个字节的包头,和4个字节的长度字段
     */
    private static int BASE_LENGTH = 8;
    
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {

        // 如果数据不够长的话,就 return 掉再等一等
        if (in.readableBytes() <= BASE_LENGTH) {
            return;
        }
        int bufferIndex = 0;
        while (true) {
            // 记录下索引,后面会用到
            bufferIndex = in.readerIndex();
            in.markReaderIndex();
            // 如果读到包头就可以break了
            if (in.readInt() == SimpleProtocol.PROTOCOL_HEAD) {
                break;
            }
            // 读不到包头的话还原一下标记,然后丢掉一个字节
            in.resetReaderIndex();
            in.readByte();
            if (in.readableBytes() <= BASE_LENGTH) {
                return;
            }
        }

        // 刚才   read 了一个int,是包头
        // 现在再 read 一个 int ,就是长度了
        int length = in.readInt();
        // 这个时候可能包的长度还没有到齐,我们要再判断一下
        if (in.readableBytes() < length) {
            // 没到齐的话就重置一下标记,然后等到齐了再说
            in.readerIndex(bufferIndex);
            return ;
        }
        byte[] data = new byte[length];
        in.readBytes(data);
        SimpleProtocol simpleProtocol = new SimpleProtocol(data);
        out.add(simpleProtocol);
    }
}

一个很典型的协议解码器,注释详尽,大家看注释好了,我就不多说了。然后改造我们的 Client 如下

public class TestClient {

    public static void main(String[] args) throws Exception {
        NioEventLoopGroup group = new NioEventLoopGroup();

        Bootstrap b = new Bootstrap();
        b.group(group)
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<SocketChannel>() {

                    protected void initChannel(SocketChannel ch) throws Exception {
                        ChannelPipeline p = ch.pipeline();
                        // 由于只发消息,所以我们只加一个编码器
                        p.addLast(new SimpleProtocolEncode());
                        p.addLast(new ChannelInboundHandlerAdapter() {
                            @Override
                            public void channelActive(ChannelHandlerContext ctx) throws Exception {
                                String[] msgs = new String[]{"A", "BB", "CCC", "DDDD"}; 
                                for (int i = 0; i < 250; i++) { 
                                    byte[] writeBytes = msgs[i % 4].getBytes();
                                    SimpleProtocol simpleProtocol = new SimpleProtocol(writeBytes);
                                    ctx.writeAndFlush(simpleProtocol);
                                    System.out.println("send msg:" + msgs[i % 2]);
                                }
                            }
                        });
                    }
                });
        ChannelFuture f = b.connect("127.0.0.1", 6688).sync();
        f.channel().closeFuture().sync();
        group.shutdownGracefully();
    }
}

由于我们的简易Client只发送消息而不接收消息,所以我们加个编码器就可以了。然后我们发 250 次的消息(一个A两个B三个C和四个D),测试是否会出现粘包拆包的情况(没有特殊处理肯定是会的)。

然后我们改造我们的服务端如下

public class Main {

    public static void main(String[] args) throws Exception {
        // Configure the server.
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .handler(new LoggingHandler(LogLevel.DEBUG))
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline p = ch.pipeline();

                            p.addLast(new SimpleProtocolDecode());
                            p.addLast(new StringDecoder(CharsetUtil.UTF_8)); 
                            p.addLast(new ChannelInboundHandlerAdapter() {
                                @Override
                                public void channelRead(ChannelHandlerContext ctx, Object msg) {
                                    SimpleProtocol body = (SimpleProtocol) msg;
                                    System.out.println("收到消息" + body.toString());
                                }
                                @Override
                                public void channelReadComplete(ChannelHandlerContext ctx) {
                                    ctx.flush();
                                }

                            });
                        }
                    });

            ChannelFuture f = b.bind(6688).sync();
            f.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

然后运行的服务端,在运行我们的客户端。
image
可以看到,很完美。

其他

怎么说呢,除了上面这些经常被人提起的编码器,netty 还有一些有意思的编码器,比如JsonObjectDecoder,Http2FrameCodec,XmlFrameDecoder等等,都是对已知的对象的一种封装,大概开发的时候可以去看看ByteToMessageDecoder的子类,如果netty已经有提供你需要的解码器的话(基本常见的都有的),就可以偷懒不用再自己写了。

猜你喜欢

转载自www.cnblogs.com/xisha/p/netty_unpacking.html