How to use JAVA to implement a highly available TCP data transmission server - [based on netty4.x]

Shock! This is probably the closest I've ever gotten to a low-level programming experience

1. What can netty do

First of all, netty is a high-performance, well-encapsulated and flexible open source framework based on NIO (true non-blocking IO). It can be used for handwriting web server, TCP server, etc. It supports rich protocols, such as: commonly used HTTP/HTTPS/WEBSOCKET, and provides a large number of methods, which is very flexible and can tailor a DIV server according to your own needs.
Use netty to write TCP server/client
1. You can design your own data transmission protocol as follows:
insert image description here
2. You can customize the encoding rules and decoding rules
3. You can customize the data interaction details between the client and the server, and handle socket flow attacks, TCP sticky packet and unpacking problem

2.Quick Start

  • Create an ordinary maven project without relying on any third-party web server, just use the main method to execute it.
    insert image description here
  • Add POM dependency
<!--netty的依赖集合,都整合在一个依赖里面了-->
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.6.Final</version>
        </dependency>
        <!--这里使用jackson反序列字节码-->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.9.7</version>
        </dependency>
        <!--加入log4j 便于深入学习整合运行过程的一些细节-->
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>
  • Design a set of data transmission protocol based on TCP
public class TcpProtocol {
    
    
    private byte header=0x58;
    private int len;
    private byte [] data;
    private byte tail=0x63;

    public byte getTail() {
    
    
        return tail;
    }

    public void setTail(byte tail) {
    
    
        this.tail = tail;
    }

    public TcpProtocol(int len, byte[] data) {
    
    
        this.len = len;
        this.data = data;
    }

    public TcpProtocol() {
    
    
    }

    public byte getHeader() {
    
    
        return header;
    }

    public void setHeader(byte header) {
    
    
        this.header = header;
    }

    public int getLen() {
    
    
        return len;
    }

    public void setLen(int len) {
    
    
        this.len = len;
    }

    public byte[] getData() {
    
    
        return data;
    }

    public void setData(byte[] data) {
    
    
        this.data = data;
    }
}

Here, hexadecimal is used to indicate the start bit and end bit of the protocol, in which 0x58 represents the start, and 0x63 represents the end, both of which are represented by one byte.

  • The startup class of the TCP server
public class TcpServer {
    
    
    private  int port;
    private Logger logger = Logger.getLogger(this.getClass());
    public  void init(){
    
    
        logger.info("正在启动tcp服务器……");
        NioEventLoopGroup boss = new NioEventLoopGroup();//主线程组
        NioEventLoopGroup work = new NioEventLoopGroup();//工作线程组
        try {
    
    
            ServerBootstrap bootstrap = new ServerBootstrap();//引导对象
            bootstrap.group(boss,work);//配置工作线程组
            bootstrap.channel(NioServerSocketChannel.class);//配置为NIO的socket通道
            bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
    
    
                protected void initChannel(SocketChannel ch) throws Exception {
    
    //绑定通道参数
                    ch.pipeline().addLast("logging",new LoggingHandler("DEBUG"));//设置log监听器,并且日志级别为debug,方便观察运行流程
                    ch.pipeline().addLast("encode",new EncoderHandler());//编码器。发送消息时候用过
                    ch.pipeline().addLast("decode",new DecoderHandler());//解码器,接收消息时候用
                    ch.pipeline().addLast("handler",new BusinessHandler());//业务处理类,最终的消息会在这个handler中进行业务处理
                }
            });
            bootstrap.option(ChannelOption.SO_BACKLOG,1024);//缓冲区
            bootstrap.childOption(ChannelOption.SO_KEEPALIVE,true);//ChannelOption对象设置TCP套接字的参数,非必须步骤
            ChannelFuture future = bootstrap.bind(port).sync();//使用了Future来启动线程,并绑定了端口
            logger.info("启动tcp服务器启动成功,正在监听端口:"+port);
            future.channel().closeFuture().sync();//以异步的方式关闭端口

        }catch (InterruptedException e) {
    
    
            logger.info("启动出现异常:"+e);
        }finally {
    
    
            work.shutdownGracefully();
            boss.shutdownGracefully();//出现异常后,关闭线程组
            logger.info("tcp服务器已经关闭");
        }

    }

    public static void main(String[] args) {
    
    
        new TcpServer(8777).init();
    }
    public TcpServer(int port) {
    
    
        this.port = port;
    }
}

As long as it is a netty-based server, it will be used bootstrapand used to bind the worker thread group, channelthe Class , and various classes of the user DIV pipeline. Pay attention to the flow order of the data and the order in which the hanlder is added handlerwhen adding a custom handler. pipelineis consistent. That is to say, from top to bottom, it should be: the decoding/encoding handler of the underlying byte stream, and the business processing handler.

  • Encoder
    Encoder is called when the server returns data to the client according to the protocol format, inherited MessageToByteEncodercode:
public class EncoderHandler extends MessageToByteEncoder {
    
    
    private  Logger logger = Logger.getLogger(this.getClass());
    protected void encode(ChannelHandlerContext ctx, Object msg, ByteBuf out) throws Exception {
    
    
        if (msg instanceof TcpProtocol){
    
    
            TcpProtocol protocol = (TcpProtocol) msg;
            out.writeByte(protocol.getHeader());
            out.writeInt(protocol.getLen());
            out.writeBytes(protocol.getData());
            out.writeByte(protocol.getTail());
            logger.debug("数据编码成功:"+out);
        }else {
    
    
            logger.info("不支持的数据协议:"+msg.getClass()+"\t期待的数据协议类是:"+TcpProtocol.class);
        }
    }
}
  • Decoder The decoder
    is a relatively core part. The custom decoding protocol, sticky packet, unpacking, etc. are all implemented in it, and inherited from. In fact, the ByteToMessageDecoderinternals of ByteToMessageDecoder have already helped us deal with the problem of unpacking/sticking packets, just follow the Its design principle is to implement the decode method:
public class DecoderHandler extends ByteToMessageDecoder {
    
    
    //最小的数据长度:开头标准位1字节
    private static int MIN_DATA_LEN=6;
    //数据解码协议的开始标志
    private static byte PROTOCOL_HEADER=0x58;
    //数据解码协议的结束标志
    private static byte PROTOCOL_TAIL=0x63;
    private Logger logger = Logger.getLogger(this.getClass());
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
    
    

        if (in.readableBytes()>MIN_DATA_LEN){
    
    
            logger.debug("开始解码数据……");
            //标记读操作的指针
            in.markReaderIndex();
            byte header=in.readByte();
            if (header==PROTOCOL_HEADER){
    
    
                logger.debug("数据开头格式正确");
                //读取字节数据的长度
                int len=in.readInt();
                //数据可读长度必须要大于len,因为结尾还有一字节的解释标志位
                if (len>=in.readableBytes()){
    
    
                    logger.debug(String.format("数据长度不够,数据协议len长度为:%1$d,数据包实际可读内容为:%2$d正在等待处理拆包……",len,in.readableBytes()));
                    in.resetReaderIndex();
                    /*
                    **结束解码,这种情况说明数据没有到齐,在父类ByteToMessageDecoder的callDecode中会对out和in进行判断
                    * 如果in里面还有可读内容即in.isReadable为true,cumulation中的内容会进行保留,,直到下一次数据到来,将两帧的数据合并起来,再解码。
                    * 以此解决拆包问题
                     */
                    return;
                }
                byte [] data=new byte[len];
                in.readBytes(data);//读取核心的数据
                byte tail=in.readByte();
                if (tail==PROTOCOL_TAIL){
    
    
                    logger.debug("数据解码成功");
                    out.add(data);
                    //如果out有值,且in仍然可读,将继续调用decode方法再次解码in中的内容,以此解决粘包问题
                }else {
    
    
                    logger.debug(String.format("数据解码协议结束标志位:%1$d [错误!],期待的结束标志位是:%2$d",tail,PROTOCOL_TAIL));
                    return;
                }
            }else {
    
    
                logger.debug("开头不对,可能不是期待的客服端发送的数,将自动略过这一个字节");
            }
        }else {
    
    
            logger.debug("数据长度不符合要求,期待最小长度是:"+MIN_DATA_LEN+" 字节");
            return;
        }

    }
}

The first is the problem of sticky packets:
as shown in the figure, normal data transmission should be like data A, one packet is a complete data, but there are also abnormal situations, such as one packet of data contains multiple data. By ByteToMessageDecoderdefault, the binary bytecode will be placed in byteBuf, so we need to know that there will be such a scene when we code.
insert image description here
The problem of sticky packets does not actually need us to solve. The following is ByteToMessageDecoderthe source code, the method of calling back our handwritten decoder in callDecode decode.

protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
    
    
        try {
    
    
            while (in.isReadable()) {
    
    //buf中是否还有数据
                int outSize = out.size();//标记out的size,解析成功的数据会添加的out中
                if (outSize > 0) {
    
    
                    fireChannelRead(ctx, out, outSize);//这个是回调业务handler的channelRead方法
                    out.clear();
                    if (ctx.isRemoved()) {
    
    
                        break;
                    }
                    outSize = 0;//清空了out,将标记size清零
                }
                int oldInputLength = in.readableBytes();//这里开始准备调用decode方法,标记了解码前的可读内容
                decode(ctx, in, out);//对应DecoderHandler中的decode方法
                if (ctx.isRemoved()) {
    
    
                    break;
                }

                if (outSize == out.size()) {
    
    //相等说明,并没有解析出来新的object到out中
                    if (oldInputLength == in.readableBytes()) {
    
    //这里很重要,若相
                    等说明decode中没有读取任何内容出来,这里一般是发生拆包后,将ByteBuf
                    的指针手动重置。重置后从这个方法break出来。让ByteToMessageDecoder
                    去处理拆包问题。这里就体现了要按照netty的设计原则来写代码                       
                    break;
                    } else {
    
    
                        continue;//这里直接continue,是考虑让开发者去跳过某些字节,比如收到了socket攻击时,数据不按照协议体来的时候,就直接跳过这些字节
                    }
                }

                if (oldInputLength == in.readableBytes()) {
    
    //这种情况属于,没有按
                照netty的设计原则来。要么是decode中没有任何逻辑代码,要么是在out中添加了
                内容后,调用了byteBuf的resetReaderIndex重置的读操作的指针
                    throw new DecoderException(
                            StringUtil.simpleClassName(getClass()) +
                            ".decode() did not read anything but decoded a message.");
                }

                if (isSingleDecode()) {
    
    //默认为false,用来设置只解析一条数据
                    break;
                }
                //这里结束后,继续wile循环,因为bytebuf仍然有可读的内容,将会继续调用
                decode方法解析bytebuf中的字节码,以此解决了粘包问题
            }
        } catch (DecoderException e) {
    
    
            throw e;
        } catch (Throwable cause) {
    
    
            throw new DecoderException(cause);
        }
    }

After analyzing the source code above, we found that the decode method is in the while loop, that is, as long as the bytebuf has content, it will always call the decode method to perform decoding operations. Therefore, when solving the problem of sticky packets, you only need to follow the normal process. After parsing the beginning of the protocol, the data bytes, and the end flag, put the data into the out list. There will be data later for sticky packet testing.

  • Unpacking problem
    Sometimes, the data we receive is incomplete, and the data in one package is split into many parts and then sent out. In this case, it is possible that the data is too large and is divided into many parts and sent out. For example, the data packet B is split into two parts for sending:
    insert image description here
    the unpacking problem is also ByteToMessageDecodersolved for us, we only need to write the decode code according to the design principles of netty.

First of all, suppose we need to solve the unpacking problem by ourselves, how should we achieve it?
Let’s analyze the problem first. What is needed is data B, but only data B_1 has been received. At this time, we should wait for the arrival of the remaining data B_2. The received data B_1 should be stored in an accumulator. When B_2 arrives, we will The two packets of data are combined and then decoded.

So the question is, how to let ByteToMessageDecoder know that the data is incomplete, DecoderHandler.decodethere is such a piece of code in:

 if (len>=in.readableBytes()){
    
    
                    logger.debug(String.format("数据长度不够,数据协议len长度为:%1$d,数据包实际可读内容为:%2$d正在等待处理拆包……",len,in.readableBytes()));
                    in.resetReaderIndex();
                    /*
                    **结束解码,这种情况说明数据没有到齐,在父类ByteToMessageDecoder的callDecode中会对out和in进行判断
                    * 如果in里面还有可读内容即in.isReadable为true,cumulation中的内容会进行保留,,直到下一次数据到来,将两帧的数据合并起来,再解码。
                    * 以此解决拆包问题
                     */
                    return;
                }

When it is read that the len in the protocol is greater than the readable content of bytebuf, it means that the data is incomplete and unpacking has occurred. Call resetReaderIndex to reset the read operation pointer and end the method. Look at a piece of code of the CallDecode method in the parent class:

if (outSize == out.size()) {
    
    //相等说明,并没有解析出来新的object到out中
                    if (oldInputLength == in.readableBytes()) {
    
    //这里很重要,若相等说明decode中没有读取任何内容出来,这里一般是发生拆包后,将ByteBuf的指针手动重置。重置后从这个方法break出来。让ByteToMessageDecoder去处理拆包问题。这里就体现了要按照netty的设计原则来写代码                       
                    break;//退出该方法
                    } else {
    
    
                        continue;//这里直接continue,是考虑让开发者去跳过某些字节,比如收到了socket攻击时,数据不按照协议体来的时候,就直接跳过这些字节
                    }
                }

After exiting callDecode, return to channelRead:

 public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    
    
        if (msg instanceof ByteBuf) {
    
    
            CodecOutputList out = CodecOutputList.newInstance();
            try {
    
    
                ByteBuf data = (ByteBuf) msg;
                first = cumulation == null;
                if (first) {
    
    
                    cumulation = data;
                } else {
    
    
                    cumulation = cumulator.cumulate(ctx.alloc(), cumulation, data);
                }
                callDecode(ctx, cumulation, out);//注意这里传入的不是data,而是cumulator,这个对象相当于一个累加器,也就是说每次调用callDecode的时候传入的byteBuf实际上是经过累加后的cumulation
            } catch (DecoderException e) {
    
    
                throw e;
            } catch (Throwable t) {
    
    
                throw new DecoderException(t);
            } finally {
    
    
                if (cumulation != null && !cumulation.isReadable()) {
    
    //这里若是数据被读取完,会清空累加器cumulation
                    numReads = 0;
                    cumulation.release();
                    cumulation = null;
                } else if (++ numReads >= discardAfterReads) {
    
    
                    // We did enough reads already try to discard some bytes so we not risk to see a OOME.
                    // See https://github.com/netty/netty/issues/4275
                    numReads = 0;
                    discardSomeReadBytes();
                }

                int size = out.size();
                decodeWasNull = !out.insertSinceRecycled();
                fireChannelRead(ctx, out, size);
                out.recycle();
            }
        } else {
    
    
            ctx.fireChannelRead(msg);
        }
    }

The channelRead method is called once after receiving a packet of data. So far, netty has solved the unpacking problem perfectly for us. We only need to follow his design principle: when len>byteBuf.readableBytes, reset the read pointer and end the decode.

  • Business processing handler class
    The data in this layer has been completely parsed and can be used directly:
public class BusinessHandler extends ChannelInboundHandlerAdapter {
    
    
    private ObjectMapper objectMapper= ByteUtils.InstanceObjectMapper();
    private Logger logger = Logger.getLogger(this.getClass());
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    
    
        if (msg instanceof byte []){
    
    
            logger.debug("解码后的字节码:"+new String((byte[]) msg,"UTF-8"));
            try {
    
    
                Object objectContainer = objectMapper.readValue((byte[]) msg, DTObject.class);
                if (objectContainer instanceof DTObject){
    
    
                    DTObject data = (DTObject) objectContainer;
                    if (data.getClassName()!=null&&data.getObject().length>0){
    
    
                        Object object = objectMapper.readValue(data.getObject(), Class.forName(data.getClassName()));
                        logger.info("收到实体对象:"+object);
                    }
                }
            }catch (Exception e){
    
    
                logger.info("对象反序列化出现问题:"+e);
            }

        }
    }
}

Since the bytecode is not deserialized into an object in decode, further deserialization is required. When transferring data, more than one type of object may be passed, so this issue should also be taken into account in deserialization. The solution is to repackage the transferred object and include the full name class information:

public class DTObject {
    
    
    private String className;
    private byte[] object;
}

In this way, when deserialization is used to Class.forName()obtain the Class, it avoids writing a lot of if loops to judge the Class of the deserialized object. The premise is that the class name and package path must match exactly!

  • Next, write a TCP client to test
    the init method of the startup class:
 public  void init() throws InterruptedException {
    
    
        NioEventLoopGroup group = new NioEventLoopGroup();
        try {
    
    
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(group);
        bootstrap.channel(NioSocketChannel.class);
        bootstrap.option(ChannelOption.SO_KEEPALIVE,true);
        bootstrap.handler(new ChannelInitializer() {
    
    
            @Override
            protected void initChannel(Channel ch) throws Exception {
    
    
                ch.pipeline().addLast("logging",new LoggingHandler("DEBUG"));
                ch.pipeline().addLast(new EncoderHandler());
                ch.pipeline().addLast(new EchoHandler());
            }
        });
        bootstrap.remoteAddress(ip,port);
        ChannelFuture future = bootstrap.connect().sync();

            future.channel().closeFuture().sync();
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }finally {
    
    
            group.shutdownGracefully().sync();
        }
    }

Client handler:

public class EchoHandler extends ChannelInboundHandlerAdapter {
    
    

    //连接成功后发送消息测试
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
    
    
        User user = new User();
        user.setBirthday(new Date());
        user.setUID(UUID.randomUUID().toString());
        user.setName("冉鹏峰");
        user.setAge(22);
        DTObject dtObject = new DTObject();
        dtObject.setClassName(user.getClass().getName());
        dtObject.setObject(ByteUtils.InstanceObjectMapper().writeValueAsBytes(user));
        TcpProtocol tcpProtocol = new TcpProtocol();
        byte [] objectBytes=ByteUtils.InstanceObjectMapper().writeValueAsBytes(dtObject);
        tcpProtocol.setLen(objectBytes.length);
        tcpProtocol.setData(objectBytes);
        ctx.write(tcpProtocol);
        ctx.flush();
    }
}

This handler is to simulate sending a packet of data to the server for testing after the TCP connection is established, and to writesend the data through the channel. As long as TcpClientthe encoder is configured in the startup class EncoderHandler, the object can be directly tcpProtocolpassed in, and it will In EncoderHandlerthe encodemethod, it is automatically converted into bytecode and put into bytebuf.

  • Normal data transmission test:

result:

2019-01-14 16:30:34 DEBUG [org.wisdom.server.decoder.DecoderHandler] 开始解码数据……
2019-01-14 16:30:34 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据开头格式正确
2019-01-14 16:30:34 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据解码成功
2019-01-14 16:30:34 DEBUG [org.wisdom.server.business.BusinessHandler] 解码后的字节码:{
    
    "className":"pojo.User","object":"eyJuYW1lIjoi5YaJ6bmP5bOwIiwiYWdlIjoyNCwiYmlydGhkYXkiOiIyMDE5LzAxLzE0IDA0OjMwOjE0IiwidWlkIjoiOGY0OTM0OGEtMWNmMy00ZTEyLWEzZTAtY2M1ZTJjZTkzMDdlIn0="}
2019-01-14 16:30:34 INFO [org.wisdom.server.business.BusinessHandler] 收到实体对象:User{
    
    name='冉鹏峰', age=24, UID='8f49348a-1cf3-4e12-a3e0-cc5e2ce9307e', birthday=Mon Jan 14 04:30:00 CST 2019}

It can be seen that the final entity object User is successfully parsed out.
In debug mode, you will also see such a table output on the console:

         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 58 00 00 00 b5 7b 22 63 6c 61 73 73 4e 61 6d 65 |X....{
    
    "className|
|00000010| 22 3a 22 70 6f 6a 6f 2e 55 73 65 72 22 2c 22 6f |":"pojo.User","o|
|00000020| 62 6a 65 63 74 22 3a 22 65 79 4a 75 59 57 31 6c |bject":"eyJuYW1l|
|00000030| 49 6a 6f 69 35 59 61 4a 36 62 6d 50 35 62 4f 77 |Ijoi5YaJ6bmP5bOw|
|00000040| 49 69 77 69 59 57 64 6c 49 6a 6f 79 4e 43 77 69 |IiwiYWdlIjoyNCwi|
|00000050| 59 6d 6c 79 64 47 68 6b 59 58 6b 69 4f 69 49 79 |YmlydGhkYXkiOiIy|
|00000060| 4d 44 45 35 4c 7a 41 78 4c 7a 45 30 49 44 41 30 |MDE5LzAxLzE0IDA0|
|00000070| 4f 6a 4d 77 4f 6a 45 30 49 69 77 69 64 57 6c 6b |OjMwOjE0IiwidWlk|
|00000080| 49 6a 6f 69 4f 47 59 30 4f 54 4d 30 4f 47 45 74 |IjoiOGY0OTM0OGEt|
|00000090| 4d 57 4e 6d 4d 79 30 30 5a 54 45 79 4c 57 45 7a |MWNmMy00ZTEyLWEz|
|000000a0| 5a 54 41 74 59 32 4d 31 5a 54 4a 6a 5a 54 6b 7a |ZTAtY2M1ZTJjZTkz|
|000000b0| 4d 44 64 6c 49 6e 30 3d 22 7d 63                |MDdlIn0="}c     |
+--------+-------------------------------------------------+----------------+

This is equivalent to a real data capture display. After the data is converted into bytecode, it is sent in the TCP buffer in binary form. But binary is too long, so it is usually converted into hexadecimal display, a table displays a byte of data, and the data is arranged from position to high position, from left to right, and from top to bottom. Among them 0x58is TcpProtocolthe start flag set in, 00 00 00 b5and it is the length of the data. Because it is an int type, it occupies four bytes. 7b--7dThe content is the data content to be transmitted, and the end is 0x63the TcpProtocolset end flag bit.

  • Sticky package test
    In order to simulate sticky package, first comment out TcpClientthe encoder configured in the startup class EncoderHandler:
bootstrap.handler(new ChannelInitializer() {
    
    
            @Override
            protected void initChannel(Channel ch) throws Exception {
    
    
                ch.pipeline().addLast("logging",new LoggingHandler("DEBUG"));
                //ch.pipeline().addLast(new EncoderHandler()); 因为需要在byteBuf中手动模拟粘包的场景
                ch.pipeline().addLast(new EchoHandler());
            }
        });

Then send the three frames of data intentionally in one packet and send them in EchoHanlder as follows:

 public void channelActive(ChannelHandlerContext ctx) throws Exception {
    
    
        User user = new User();
        user.setBirthday(new Date());
        user.setUID(UUID.randomUUID().toString());
        user.setName("冉鹏峰");
        user.setAge(24);
        DTObject dtObject = new DTObject();
        dtObject.setClassName(user.getClass().getName());
        dtObject.setObject(ByteUtils.InstanceObjectMapper().writeValueAsBytes(user));
        TcpProtocol tcpProtocol = new TcpProtocol();
        byte [] objectBytes=ByteUtils.InstanceObjectMapper().writeValueAsBytes(dtObject);
        tcpProtocol.setLen(objectBytes.length);
        tcpProtocol.setData(objectBytes);
        ByteBuf buffer = ctx.alloc().buffer();
        buffer.writeByte(tcpProtocol.getHeader());
        buffer.writeInt(tcpProtocol.getLen());
        buffer.writeBytes(tcpProtocol.getData());
        buffer.writeByte(tcpProtocol.getTail());
        //模拟粘包的第二帧数据
        buffer.writeByte(tcpProtocol.getHeader());
        buffer.writeInt(tcpProtocol.getLen());
        buffer.writeBytes(tcpProtocol.getData());
        buffer.writeByte(tcpProtocol.getTail());
        //模拟粘包的第三帧数据
        buffer.writeByte(tcpProtocol.getHeader());
        buffer.writeInt(tcpProtocol.getLen());
        buffer.writeBytes(tcpProtocol.getData());
        buffer.writeByte(tcpProtocol.getTail());
        ctx.write(buffer);
        ctx.flush();
    }

operation result:

2019-01-14 16:44:51 DEBUG [org.wisdom.server.decoder.DecoderHandler] 开始解码数据……
2019-01-14 16:44:51 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据开头格式正确
2019-01-14 16:44:51 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据解码成功
2019-01-14 16:44:51 DEBUG [org.wisdom.server.business.BusinessHandler] 解码后的字节码:{
    
    "className":"pojo.User","object":"eyJuYW1lIjoi5YaJ6bmP5bOwIiwiYWdlIjoyNCwiYmlydGhkYXkiOiIyMDE5LzAxLzE0IDA0OjQ0OjE0IiwidWlkIjoiODFkZTU5YWUtMzQ4Mi00ZDFhLWJjNDMtN2NjMTJmOTI1ZTUxIn0="}
2019-01-14 16:44:51 INFO [org.wisdom.server.business.BusinessHandler] 收到实体对象:User{
    
    name='冉鹏峰', age=24, UID='81de59ae-3482-4d1a-bc43-7cc12f925e51', birthday=Mon Jan 14 04:44:00 CST 2019}
2019-01-14 16:44:51 DEBUG [org.wisdom.server.decoder.DecoderHandler] 开始解码数据……
2019-01-14 16:44:51 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据开头格式正确
2019-01-14 16:44:51 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据解码成功
2019-01-14 16:44:51 DEBUG [org.wisdom.server.business.BusinessHandler] 解码后的字节码:{
    
    "className":"pojo.User","object":"eyJuYW1lIjoi5YaJ6bmP5bOwIiwiYWdlIjoyNCwiYmlydGhkYXkiOiIyMDE5LzAxLzE0IDA0OjQ0OjE0IiwidWlkIjoiODFkZTU5YWUtMzQ4Mi00ZDFhLWJjNDMtN2NjMTJmOTI1ZTUxIn0="}
2019-01-14 16:44:51 INFO [org.wisdom.server.business.BusinessHandler] 收到实体对象:User{
    
    name='冉鹏峰', age=24, UID='81de59ae-3482-4d1a-bc43-7cc12f925e51', birthday=Mon Jan 14 04:44:00 CST 2019}
2019-01-14 16:44:51 DEBUG [org.wisdom.server.decoder.DecoderHandler] 开始解码数据……
2019-01-14 16:44:51 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据开头格式正确
2019-01-14 16:44:51 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据解码成功
2019-01-14 16:44:51 DEBUG [org.wisdom.server.business.BusinessHandler] 解码后的字节码:{
    
    "className":"pojo.User","object":"eyJuYW1lIjoi5YaJ6bmP5bOwIiwiYWdlIjoyNCwiYmlydGhkYXkiOiIyMDE5LzAxLzE0IDA0OjQ0OjE0IiwidWlkIjoiODFkZTU5YWUtMzQ4Mi00ZDFhLWJjNDMtN2NjMTJmOTI1ZTUxIn0="}
2019-01-14 16:44:51 INFO [org.wisdom.server.business.BusinessHandler] 收到实体对象:User{
    
    name='冉鹏峰', age=24, UID='81de59ae-3482-4d1a-bc43-7cc12f925e51', birthday=Mon Jan 14 04:44:00 CST 2019}

The server successfully parsed three frames of data, BusinessHandlerand channelReadthe method was called three times.
And the captured data packets are indeed simulated three frames of data stuck in one packet:

         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 58 00 00 00 b5 7b 22 63 6c 61 73 73 4e 61 6d 65 |X....{
    
    "className|
|00000010| 22 3a 22 70 6f 6a 6f 2e 55 73 65 72 22 2c 22 6f |":"pojo.User","o|
|00000020| 62 6a 65 63 74 22 3a 22 65 79 4a 75 59 57 31 6c |bject":"eyJuYW1l|
|00000030| 49 6a 6f 69 35 59 61 4a 36 62 6d 50 35 62 4f 77 |Ijoi5YaJ6bmP5bOw|
|00000040| 49 69 77 69 59 57 64 6c 49 6a 6f 79 4e 43 77 69 |IiwiYWdlIjoyNCwi|
|00000050| 59 6d 6c 79 64 47 68 6b 59 58 6b 69 4f 69 49 79 |YmlydGhkYXkiOiIy|
|00000060| 4d 44 45 35 4c 7a 41 78 4c 7a 45 30 49 44 41 30 |MDE5LzAxLzE0IDA0|
|00000070| 4f 6a 51 30 4f 6a 45 30 49 69 77 69 64 57 6c 6b |OjQ0OjE0IiwidWlk|
|00000080| 49 6a 6f 69 4f 44 46 6b 5a 54 55 35 59 57 55 74 |IjoiODFkZTU5YWUt|
|00000090| 4d 7a 51 34 4d 69 30 30 5a 44 46 68 4c 57 4a 6a |MzQ4Mi00ZDFhLWJj|
|000000a0| 4e 44 4d 74 4e 32 4e 6a 4d 54 4a 6d 4f 54 49 31 |NDMtN2NjMTJmOTI1|
|000000b0| 5a 54 55 78 49 6e 30 3d 22 7d6358 00 00 00 b5 |ZTUxIn0="}cX....|
|000000c0| 7b 22 63 6c 61 73 73 4e 61 6d 65 22 3a 22 70 6f |{
    
    "className":"po|
|000000d0| 6a 6f 2e 55 73 65 72 22 2c 22 6f 62 6a 65 63 74 |jo.User","object|
|000000e0| 22 3a 22 65 79 4a 75 59 57 31 6c 49 6a 6f 69 35 |":"eyJuYW1lIjoi5|
|000000f0| 59 61 4a 36 62 6d 50 35 62 4f 77 49 69 77 69 59 |YaJ6bmP5bOwIiwiY|
|00000100| 57 64 6c 49 6a 6f 79 4e 43 77 69 59 6d 6c 79 64 |WdlIjoyNCwiYmlyd|
|00000110| 47 68 6b 59 58 6b 69 4f 69 49 79 4d 44 45 35 4c |GhkYXkiOiIyMDE5L|
|00000120| 7a 41 78 4c 7a 45 30 49 44 41 30 4f 6a 51 30 4f |zAxLzE0IDA0OjQ0O|
|00000130| 6a 45 30 49 69 77 69 64 57 6c 6b 49 6a 6f 69 4f |jE0IiwidWlkIjoiO|
|00000140| 44 46 6b 5a 54 55 35 59 57 55 74 4d 7a 51 34 4d |DFkZTU5YWUtMzQ4M|
|00000150| 69 30 30 5a 44 46 68 4c 57 4a 6a 4e 44 4d 74 4e |i00ZDFhLWJjNDMtN|
|00000160| 32 4e 6a 4d 54 4a 6d 4f 54 49 31 5a 54 55 78 49 |2NjMTJmOTI1ZTUxI|
|00000170| 6e 30 3d 22 7d6358 00 00 00 b5 7b 22 63 6c 61 |n0="}cX....{"cla|
|00000180| 73 73 4e 61 6d 65 22 3a 22 70 6f 6a 6f 2e 55 73 |ssName":"pojo.Us|
|00000190| 65 72 22 2c 22 6f 62 6a 65 63 74 22 3a 22 65 79 |er","object":"ey|
|000001a0| 4a 75 59 57 31 6c 49 6a 6f 69 35 59 61 4a 36 62 |JuYW1lIjoi5YaJ6b|
|000001b0| 6d 50 35 62 4f 77 49 69 77 69 59 57 64 6c 49 6a |mP5bOwIiwiYWdlIj|
|000001c0| 6f 79 4e 43 77 69 59 6d 6c 79 64 47 68 6b 59 58 |oyNCwiYmlydGhkYX|
|000001d0| 6b 69 4f 69 49 79 4d 44 45 35 4c 7a 41 78 4c 7a |kiOiIyMDE5LzAxLz|
|000001e0| 45 30 49 44 41 30 4f 6a 51 30 4f 6a 45 30 49 69 |E0IDA0OjQ0OjE0Ii|
|000001f0| 77 69 64 57 6c 6b 49 6a 6f 69 4f 44 46 6b 5a 54 |widWlkIjoiODFkZT|
|00000200| 55 35 59 57 55 74 4d 7a 51 34 4d 69 30 30 5a 44 |U5YWUtMzQ4Mi00ZD|
|00000210| 46 68 4c 57 4a 6a 4e 44 4d 74 4e 32 4e 6a 4d 54 |FhLWJjNDMtN2NjMT|
|00000220| 4a 6d 4f 54 49 31 5a 54 55 78 49 6e 30 3d 22 7d |JmOTI1ZTUxIn0="}|
|00000230|63|c               |
+--------+-------------------------------------------------+----------------+

It can be seen that there are indeed three tails [63]

In the netty4.x version, the sticky packet problem is indeed handled by the CallDecode method in netty's ByteToMessageDecoder.

  • Unpacking problem Comment out the encoder
    in this time , and then simulate the data unpacking problem in channelActive of EchoHandler:TcpClientEncoderHandler
public void channelActive(ChannelHandlerContext ctx) throws Exception {
    
    
        User user = new User();
        user.setBirthday(new Date());
        user.setUID(UUID.randomUUID().toString());
        user.setName("冉鹏峰");
        user.setAge(24);
        DTObject dtObject = new DTObject();
        dtObject.setClassName(user.getClass().getName());
        dtObject.setObject(ByteUtils.InstanceObjectMapper().writeValueAsBytes(user));
        TcpProtocol tcpProtocol = new TcpProtocol();
        byte [] objectBytes=ByteUtils.InstanceObjectMapper().writeValueAsBytes(dtObject);
        tcpProtocol.setLen(objectBytes.length);
        tcpProtocol.setData(objectBytes);
        ByteBuf buffer = ctx.alloc().buffer();
        buffer.writeByte(tcpProtocol.getHeader());
        buffer.writeInt(tcpProtocol.getLen());
        buffer.writeBytes(Arrays.copyOfRange(tcpProtocol.getData(),0,tcpProtocol.getLen()/2));//只发送二分之一的数据包
        //模拟拆包
        ctx.write(buffer);
        ctx.flush();
        Thread.sleep(3000);//模拟网络延时
        buffer = ctx.alloc().buffer();        
        buffer.writeBytes(Arrays.copyOfRange(tcpProtocol.getData(),tcpProtocol.getLen()/2,tcpProtocol.getLen()));//将剩下的二分之和尾巴发送过去
        buffer.writeByte(tcpProtocol.getTail());
        ctx.write(buffer);
        ctx.flush();

    }

Running results:
First, on the client side:

2019-01-14 17:08:33 DEBUG [DEBUG] [id: 0x3b8cbbbb, L:/127.0.0.1:51138 - R:/127.0.0.1:8777] WRITE: 95B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 58 00 00 00 b5 7b 22 63 6c 61 73 73 4e 61 6d 65 |X....{
    
    "className|
|00000010| 22 3a 22 70 6f 6a 6f 2e 55 73 65 72 22 2c 22 6f |":"pojo.User","o|
|00000020| 62 6a 65 63 74 22 3a 22 65 79 4a 75 59 57 31 6c |bject":"eyJuYW1l|
|00000030| 49 6a 6f 69 35 59 61 4a 36 62 6d 50 35 62 4f 77 |Ijoi5YaJ6bmP5bOw|
|00000040| 49 69 77 69 59 57 64 6c 49 6a 6f 79 4e 43 77 69 |IiwiYWdlIjoyNCwi|
|00000050| 59 6d 6c 79 64 47 68 6b 59 58 6b 69 4f 69 49    |YmlydGhkYXkiOiI |
+--------+-------------------------------------------------+----------------+
2019-01-14 17:08:33 DEBUG [DEBUG] [id: 0x3b8cbbbb, L:/127.0.0.1:51138 - R:/127.0.0.1:8777] FLUSH
2019-01-14 17:08:36 DEBUG [DEBUG] [id: 0x3b8cbbbb, L:/127.0.0.1:51138 - R:/127.0.0.1:8777] WRITE: 92B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 79 4d 44 45 35 4c 7a 41 78 4c 7a 45 30 49 44 41 |yMDE5LzAxLzE0IDA|
|00000010| 31 4f 6a 41 34 4f 6a 45 30 49 69 77 69 64 57 6c |1OjA4OjE0IiwidWl|
|00000020| 6b 49 6a 6f 69 4f 57 45 79 5a 6a 49 35 4d 6d 4d |kIjoiOWEyZjI5MmM|
|00000030| 74 4d 6a 4d 35 4f 43 30 30 5a 6a 6b 77 4c 57 46 |tMjM5OC00ZjkwLWF|
|00000040| 6b 5a 57 59 74 5a 6d 46 6c 4e 44 45 7a 5a 6a 55 |kZWYtZmFlNDEzZjU|
|00000050| 35 4e 32 45 33 49 6e 30 3d 22 7d 63             |5N2E3In0="}c    |
+--------+-------------------------------------------------+----------------+
2019-01-14 17:08:36 DEBUG [DEBUG] [id: 0x3b8cbbbb, L:/127.0.0.1:51138 - R:/127.0.0.1:8777] FLUSH

It is true that the data is divided into two packets and sent out.

Look at the output log of the server:

2019-01-14 17:08:33 DEBUG [DEBUG] [id: 0x8e5811b3, L:/127.0.0.1:8777 - R:/127.0.0.1:51138] RECEIVED: 95B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 58 00 00 00 b5 7b 22 63 6c 61 73 73 4e 61 6d 65 |X....{
    
    "className|
|00000010| 22 3a 22 70 6f 6a 6f 2e 55 73 65 72 22 2c 22 6f |":"pojo.User","o|
|00000020| 62 6a 65 63 74 22 3a 22 65 79 4a 75 59 57 31 6c |bject":"eyJuYW1l|
|00000030| 49 6a 6f 69 35 59 61 4a 36 62 6d 50 35 62 4f 77 |Ijoi5YaJ6bmP5bOw|
|00000040| 49 69 77 69 59 57 64 6c 49 6a 6f 79 4e 43 77 69 |IiwiYWdlIjoyNCwi|
|00000050| 59 6d 6c 79 64 47 68 6b 59 58 6b 69 4f 69 49    |YmlydGhkYXkiOiI |
+--------+-------------------------------------------------+----------------+
2019-01-14 17:08:33 DEBUG [org.wisdom.server.decoder.DecoderHandler] 开始解码数据……
2019-01-14 17:08:33 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据开头格式正确
2019-01-14 17:08:33 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据长度不够,数据协议len长度为:181,数据包实际可读内容为:90正在等待处理拆包……
2019-01-14 17:08:36 DEBUG [DEBUG] [id: 0x8e5811b3, L:/127.0.0.1:8777 - R:/127.0.0.1:51138] RECEIVED: 92B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 79 4d 44 45 35 4c 7a 41 78 4c 7a 45 30 49 44 41 |yMDE5LzAxLzE0IDA|
|00000010| 31 4f 6a 41 34 4f 6a 45 30 49 69 77 69 64 57 6c |1OjA4OjE0IiwidWl|
|00000020| 6b 49 6a 6f 69 4f 57 45 79 5a 6a 49 35 4d 6d 4d |kIjoiOWEyZjI5MmM|
|00000030| 74 4d 6a 4d 35 4f 43 30 30 5a 6a 6b 77 4c 57 46 |tMjM5OC00ZjkwLWF|
|00000040| 6b 5a 57 59 74 5a 6d 46 6c 4e 44 45 7a 5a 6a 55 |kZWYtZmFlNDEzZjU|
|00000050| 35 4e 32 45 33 49 6e 30 3d 22 7d 63             |5N2E3In0="}c    |
+--------+-------------------------------------------------+----------------+
2019-01-14 17:08:36 DEBUG [org.wisdom.server.decoder.DecoderHandler] 开始解码数据……
2019-01-14 17:08:36 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据开头格式正确
2019-01-14 17:08:36 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据解码成功
2019-01-14 17:08:36 DEBUG [org.wisdom.server.business.BusinessHandler] 解码后的字节码:{
    
    "className":"pojo.User","object":"eyJuYW1lIjoi5YaJ6bmP5bOwIiwiYWdlIjoyNCwiYmlydGhkYXkiOiIyMDE5LzAxLzE0IDA1OjA4OjE0IiwidWlkIjoiOWEyZjI5MmMtMjM5OC00ZjkwLWFkZWYtZmFlNDEzZjU5N2E3In0="}
2019-01-14 17:08:36 INFO [org.wisdom.server.business.BusinessHandler] 收到实体对象:User{
    
    name='冉鹏峰', age=24, UID='9a2f292c-2398-4f90-adef-fae413f597a7', birthday=Mon Jan 14 05:08:00 CST 2019}

In the first packet of data, when it is judged that the readable content in the bytebuf is not enough, the decoding is terminated, and the while loop in the callDecode of the parent class is broken, and when the next packet of data arrives in the channelRead of the parent class, the two The packet data is combined and decoded again.

  • Finally, test the scene of unpacking and sticking at the same time
    , or comment out TcpClientthe encoder in EncoderHandler, and then in the ChannelActive method of EchoHandler:
 public void channelActive(ChannelHandlerContext ctx) throws Exception {
    
    
        User user = new User();
        user.setBirthday(new Date());
        user.setUID(UUID.randomUUID().toString());
        user.setName("冉鹏峰");
        user.setAge(24);
        DTObject dtObject = new DTObject();
        dtObject.setClassName(user.getClass().getName());
        dtObject.setObject(ByteUtils.InstanceObjectMapper().writeValueAsBytes(user));
        TcpProtocol tcpProtocol = new TcpProtocol();
        byte [] objectBytes=ByteUtils.InstanceObjectMapper().writeValueAsBytes(dtObject);
        tcpProtocol.setLen(objectBytes.length);
        tcpProtocol.setData(objectBytes);
        ByteBuf buffer = ctx.alloc().buffer();
        buffer.writeByte(tcpProtocol.getHeader());
        buffer.writeInt(tcpProtocol.getLen());
        buffer.writeBytes(Arrays.copyOfRange(tcpProtocol.getData(),0,tcpProtocol.getLen()/2));//拆包,只发送一半的数据

        ctx.write(buffer);
        ctx.flush();
        Thread.sleep(3000);
        buffer = ctx.alloc().buffer();
        buffer.writeBytes(Arrays.copyOfRange(tcpProtocol.getData(),tcpProtocol.getLen()/2,tcpProtocol.getLen())); //拆包发送剩余的一半数据   
        buffer.writeByte(tcpProtocol.getTail());
        //模拟粘包的第二帧数据
        buffer.writeByte(tcpProtocol.getHeader());
        buffer.writeInt(tcpProtocol.getLen());
        buffer.writeBytes(tcpProtocol.getData());
        buffer.writeByte(tcpProtocol.getTail());
        //模拟粘包的第三帧数据
        buffer.writeByte(tcpProtocol.getHeader());
        buffer.writeInt(tcpProtocol.getLen());
        buffer.writeBytes(tcpProtocol.getData());
        buffer.writeByte(tcpProtocol.getTail());
        ctx.write(buffer);
        ctx.flush();

    }

Finally, directly view the output of the server:

2019-01-14 17:19:25 DEBUG [org.wisdom.server.decoder.DecoderHandler] 开始解码数据……
2019-01-14 17:19:25 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据开头格式正确
2019-01-14 17:19:25 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据长度不够,数据协议len长度为:181,数据包实际可读内容为:90正在等待处理拆包……
2019-01-14 17:19:28 DEBUG [DEBUG] [id: 0xc46234aa, L:/127.0.0.1:8777 - R:/127.0.0.1:51466] RECEIVED: 466B
2019-01-14 17:19:28 DEBUG [org.wisdom.server.decoder.DecoderHandler] 开始解码数据……
2019-01-14 17:19:28 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据开头格式正确
2019-01-14 17:19:28 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据解码成功
2019-01-14 17:19:28 DEBUG [org.wisdom.server.business.BusinessHandler] 解码后的字节码:{
    
    "className":"pojo.User","object":"eyJuYW1lIjoi5YaJ6bmP5bOwIiwiYWdlIjoyNCwiYmlydGhkYXkiOiIyMDE5LzAxLzE0IDA1OjE5OjE0IiwidWlkIjoiODE2Zjg2ZDItNDBhMS00MDRkLTgwMWItZmY1NzgxMTJhNjFmIn0="}
2019-01-14 17:19:28 INFO [org.wisdom.server.business.BusinessHandler] 收到实体对象:User{
    
    name='冉鹏峰', age=24, UID='816f86d2-40a1-404d-801b-ff578112a61f', birthday=Mon Jan 14 05:19:00 CST 2019}
2019-01-14 17:19:28 DEBUG [org.wisdom.server.decoder.DecoderHandler] 开始解码数据……
2019-01-14 17:19:28 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据开头格式正确
2019-01-14 17:19:28 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据解码成功
2019-01-14 17:19:28 DEBUG [org.wisdom.server.business.BusinessHandler] 解码后的字节码:{
    
    "className":"pojo.User","object":"eyJuYW1lIjoi5YaJ6bmP5bOwIiwiYWdlIjoyNCwiYmlydGhkYXkiOiIyMDE5LzAxLzE0IDA1OjE5OjE0IiwidWlkIjoiODE2Zjg2ZDItNDBhMS00MDRkLTgwMWItZmY1NzgxMTJhNjFmIn0="}
2019-01-14 17:19:28 INFO [org.wisdom.server.business.BusinessHandler] 收到实体对象:User{
    
    name='冉鹏峰', age=24, UID='816f86d2-40a1-404d-801b-ff578112a61f', birthday=Mon Jan 14 05:19:00 CST 2019}
2019-01-14 17:19:28 DEBUG [org.wisdom.server.decoder.DecoderHandler] 开始解码数据……
2019-01-14 17:19:28 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据开头格式正确
2019-01-14 17:19:28 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据解码成功
2019-01-14 17:19:28 DEBUG [org.wisdom.server.business.BusinessHandler] 解码后的字节码:{
    
    "className":"pojo.User","object":"eyJuYW1lIjoi5YaJ6bmP5bOwIiwiYWdlIjoyNCwiYmlydGhkYXkiOiIyMDE5LzAxLzE0IDA1OjE5OjE0IiwidWlkIjoiODE2Zjg2ZDItNDBhMS00MDRkLTgwMWItZmY1NzgxMTJhNjFmIn0="}
2019-01-14 17:19:28 INFO [org.wisdom.server.business.BusinessHandler] 收到实体对象:User{
    
    name='冉鹏峰', age=24, UID='816f86d2-40a1-404d-801b-ff578112a61f', birthday=Mon Jan 14 05:19:00 CST 2019}

Summarize

For unpacking and sticking, as long as the code is implemented in accordance with the design principles of netty, it can be solved happily and easily. Although this example DTObjectpacks the data, it avoids the embarrassment of adding an if judgment every time an object type is added during decoding. However, it still cannot handle the scene when transferring List and Map. The next article will introduce how to deal with List, Map, and common object scenarios.

Guess you like

Origin blog.csdn.net/qq_43842093/article/details/132008740