前言学习Netty避免不了要去了解TCP粘包/拆包问题,熟悉各个编解码器是如何解决TCP粘包/拆包问题的,同时需要知道TCP粘包/拆包问题是怎么产生的 。
在此博文前,可以先学习了解前几篇博文:
- 深入学习Netty(1)——传统BIO编程
- 深入学习Netty(2)——传统NIO编程
- 深入学习Netty(3)——传统AIO编程
- 深入学习Netty(4)—-Netty编程入门
博文中所有的代码都已上传到Github,欢迎Star、Fork
一、TCP粘包/拆包1.什么是TCP粘包/拆包问题?引用《Netty权威指南》原话,可以很清楚解释什么是TCP粘包/拆包问题 。
TCP是一个“流”协议,是没有界限的一串数据,TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TCP拆成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题 。
一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是TCP粘包/拆包 。
假设服务端分别发送两个数据包P1和P2给服务端,由于服务端读取一次的字节数目是不确定的,所以可能会发生五种情况:

文章插图
- 服务端分两次读取到两个独立的数据包;
- 服务端一次接收到两个数据包,P1和P2粘合在一起,被称为TCP粘包;
- 服务端分两次读取到两个数据包,第一次读取到完整的P1包和P2包的部分内容,第二次读取到P2包的剩余内容,被称之为TCP拆包;
- 服务端分两次读取到两个数据包,第一次读取到了P1包的部分内容P1_1,第二次读取到了P1包的剩余内容P1_2和P2包的整包
- 其实还有最后一种可能,就是服务端TCP接收的滑动窗非常小,而数据包P1/P2非常大,很有可能服务端需要分多次才能将P1/P2包接收完全,期间发生多次拆包 。
(1)MSS(Maximum Segment Size)指的是连接层每次传输的数据有个最大限制MTU(Maximum Transmission Unit),超过这个量要分成多个报文段 。
(2)MTU限制了一次最多可以发送1500个字节,而TCP协议在发送DATA时,还会加上额外的TCP Header和IP Header,因此刨去这两个部分,就是TCP协议一次可以发送的实际应用数据的最大大小,即MSS长度=MTU长度-IP Header-TCP Header 。
(3)TCP为提高性能,发送端会将需要发送的数据发送到缓冲区,等待缓冲区满了之后,再将缓冲中的数据发送到接收方 。同理,接收方也有缓冲区这样的机制,来接收数据 。
由于有上述的原因,所以会造成拆包/粘包的具体原因如下:
(1)拆包发生原因
- 要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包 。
- 待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包 。
- 要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包 。
- 接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包 。
(1)消息定长,例如每个报文的大小固定长度200字节,不够空位补空格
(2)在包尾增加回车换行符进行分割,例如FTP协议
(3)将消息分为消息头和消息体,消息头中包含表示消息总长度的字段
(4)更复杂的应用层协议 。
2.TCP粘包异常问题案例(1)TimeServerHandler
public class TimeServerHandler extends ChannelInboundHandlerAdapter {private static final Logger log = Logger.getLogger(TimeClientHandler.class.getName());private int counter;@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {ByteBuf buf = (ByteBuf) msg;byte[] req = new byte[buf.readableBytes()];buf.readBytes(req);String body = new String(req, "UTF-8").substring(0, req.length - System.getProperty("line.separator").length());// 每收到一条消息计数器就加1, 理论上应该接收到100条System.out.println("The time server receive order: " + body + "; the counter is : "+ (++counter));String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ?new Date(System.currentTimeMillis()).toString():"BAD ORDER";currentTime = currentTime + System.getProperty("line.separator");ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());ctx.writeAndFlush(resp);}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {log.warning("Unexpected exception from downstream: " + cause.getMessage());ctx.close();}}(3)TimeServer
public class TimeServer {public static final Logger log = LoggerFactory.getLogger(TimeServer.class);public static void main(String[] args) throws Exception {new TimeServer().bind();}public void bind() throws Exception {// NIO 线程组NioEventLoopGroup bossGroup = new NioEventLoopGroup();NioEventLoopGroup workerGroup = new NioEventLoopGroup();try {ServerBootstrap bootstrap = new ServerBootstrap();bootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).option(ChannelOption.SO_BACKLOG, 1024).handler(new LoggingHandler(LogLevel.INFO)).childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel socketChannel) {socketChannel.pipeline().addLast(new TimeServerHandler());}});// 绑定端口,同步等待成功ChannelFuture f = bootstrap.bind(NettyConstant.REMOTE_IP, NettyConstant.REMOTE_PORT).sync();log.info("Time server[{}] start success", NettyConstant.REMOTE_IP + ": " + NettyConstant.REMOTE_PORT);// 等待所有服务端监听端口关闭f.channel().closeFuture().sync();} finally {// 优雅退出,释放线程池资源bossGroup.shutdownGracefully();workerGroup.shutdownGracefully();}}}(3)TimeClientHandler
public class TimeClientHandler extends ChannelInboundHandlerAdapter {private static final Logger log = Logger.getLogger(TimeClientHandler.class.getName());private int counter;private byte[] req;public TimeClientHandler() {req = ("QUERY TIME ORDER" + System.getProperty("line.separator")).getBytes();}@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {ByteBuf message = null;// 循环发送100条消息,每发送一条刷新一次,服务端理论上接收到100条查询时间指令的请求for (int i = 0; i < 100; i++) {message = Unpooled.buffer(req.length);message.writeBytes(req);ctx.writeAndFlush(message);}}@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {ByteBuf buf = (ByteBuf) msg;byte[] req = new byte[buf.readableBytes()];buf.readBytes(req);String body = new String(req, "UTF-8");// 客户端每接收到服务端一条应答消息之后,计数器就加1,理论上应该有100条服务端日志System.out.println("Now is: " + body + "; the current is "+ (++counter));}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {log.warning("Unexpected exception from downstream: " + cause.getMessage());ctx.close();}}(4)TimeClient
public class TimeClient {public static final Logger log = LoggerFactory.getLogger(TimeClient.class);public static void main(String[] args) throws Exception {new TimeClient().connect(NettyConstant.REMOTE_IP, NettyConstant.REMOTE_PORT);}public void connect(final String host, final int port) throws Exception {// NIO 线程组EventLoopGroup group = new NioEventLoopGroup();try {Bootstrap bootstrap = new Bootstrap();bootstrap.group(group).channel(NioSocketChannel.class).option(ChannelOption.TCP_NODELAY, true).handler(new LoggingHandler(LogLevel.INFO)).handler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel socketChannel) throws Exception {socketChannel.pipeline().addLast(new TimeClientHandler());}});// 发起异步连接操作ChannelFuture f = bootstrap.connect(host, port).sync();// 等待所有服务端监听端口关闭f.channel().closeFuture().sync();} finally {// 优雅退出,释放线程池资源group.shutdownGracefully();}}}(5)运行测试结果
运行服务端与客户端,观察服务端与客户端
服务端:
The time server receive order: QUERY TIME ORDERQUERY TIME ORDER... // 此处忽略96个QUERY TIME ORDERQUERY TIME ORDERQUERY TIME ORDER; the counter is : 1客户端:
Now is: BAD ORDER; the current is 1从结果上来看,客户端向服务端发送的100个“QUERY TIME ORDER”命令,都粘成一个包(counter=1),服务端也只返回一个命令“BAD ORDER”,可以尝试运行客户端多次,每次运行的结果都是不一样的,但是大部分都是粘包,计数器都小于了100 。
三、Netty解决TCP粘包/拆包1.按行文本解码器LineBasedFramedDecoder和StringDecoderLineBasedFramedDecoder:依次遍历ByeBuf中可读字节,判断是否有“\n”,“\r\n”,如果有,就当前位置为结束位置,从可读索引到结束位置区间的字节就组装成一行,以换行符为结束标志的解码器,同识支持最大长度 。
StringDecoder:将接收对象转换成字符串,然后继续调用后面的handler 。
LineBasedFramedDecoder和StringDecoder就是按行切换的文本解码器,被设计用来支持TCP粘包与拆包 。
(1)改造TimeServer
增加解码器LineBasedFramedDecoder和StringDecoder
public class TimeServer {public static final Logger log = LoggerFactory.getLogger(TimeServer.class);public static void main(String[] args) throws Exception {new TimeServer().bind();}public void bind() throws Exception {// NIO 线程组NioEventLoopGroup bossGroup = new NioEventLoopGroup();NioEventLoopGroup workerGroup = new NioEventLoopGroup();try {ServerBootstrap bootstrap = new ServerBootstrap();bootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).option(ChannelOption.SO_BACKLOG, 1024).handler(new LoggingHandler(LogLevel.INFO)).childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel socketChannel) {socketChannel.pipeline().addLast(new LineBasedFrameDecoder(1024));socketChannel.pipeline().addLast(new StringDecoder());socketChannel.pipeline().addLast(new TimeServerHandler());}});// 绑定端口,同步等待成功ChannelFuture f = bootstrap.bind(NettyConstant.LOCAL_IP, NettyConstant.LOCAL_PORT).sync();log.info("Time server[{}] start success", NettyConstant.LOCAL_IP + ": " + NettyConstant.LOCAL_PORT);// 等待所有服务端监听端口关闭f.channel().closeFuture().sync();} finally {// 优雅退出,释放线程池资源bossGroup.shutdownGracefully();workerGroup.shutdownGracefully();}}}(2)改造TimeServerHandler
不需要对消息进行解码,直接String读取即可
public class TimeServerHandler extends ChannelInboundHandlerAdapter {private static final Logger log = Logger.getLogger(TimeClientHandler.class.getName());private int counter;@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {// 不需要对消息进行编解码,直接String读取String body = (String) msg;// 每收到一条消息计数器就加1, 理论上应该接收到100条System.out.println("The time server receive order: " + body + "; the counter is : "+ (++counter));String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ?new Date(System.currentTimeMillis()).toString():"BAD ORDER";currentTime = currentTime + System.getProperty("line.separator");ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());ctx.writeAndFlush(resp);}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {log.warning("Unexpected exception from downstream: " + cause.getMessage());ctx.close();}}(3)改造TimeClient
同样增加解码器LineBasedFramedDecoder和StringDecoder
public class TimeClient {public static final Logger log = LoggerFactory.getLogger(TimeClient.class);public static void main(String[] args) throws Exception {new TimeClient().connect(NettyConstant.LOCAL_IP, NettyConstant.LOCAL_PORT);}public void connect(final String host, final int port) throws Exception {// NIO 线程组EventLoopGroup group = new NioEventLoopGroup();try {Bootstrap bootstrap = new Bootstrap();bootstrap.group(group).channel(NioSocketChannel.class).option(ChannelOption.TCP_NODELAY, true).handler(new LoggingHandler(LogLevel.INFO)).handler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel socketChannel) throws Exception {socketChannel.pipeline().addLast(new LineBasedFrameDecoder(1024));socketChannel.pipeline().addLast(new StringDecoder());socketChannel.pipeline().addLast(new TimeClientHandler());}});// 发起异步连接操作ChannelFuture f = bootstrap.connect(host, port).sync();// 等待所有服务端监听端口关闭f.channel().closeFuture().sync();} finally {// 优雅退出,释放线程池资源group.shutdownGracefully();}}}(4)改造TimeClientHandler
同样地,不需要编解码了,直接返回了字符串的应答消息
public class TimeClientHandler extends ChannelInboundHandlerAdapter {private static final Logger log = Logger.getLogger(TimeClientHandler.class.getName());private int counter;private byte[] req;public TimeClientHandler() {req = ("QUERY TIME ORDER" + System.getProperty("line.separator")).getBytes();}@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {ByteBuf message = null;// 循环发送100条消息,每发送一条刷新一次,服务端理论上接收到100条查询时间指令的请求for (int i = 0; i < 100; i++) {message = Unpooled.buffer(req.length);message.writeBytes(req);ctx.writeAndFlush(message);}}@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {// 不需要编解码了,直接返回了字符串的应答消息String body = (String) msg;// 客户端每接收到服务端一条应答消息之后,计数器就加1,理论上应该有100条服务端日志System.out.println("Now is: " + body + "; the current is "+ (++counter));}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {log.warning("Unexpected exception from downstream: " + cause.getMessage());ctx.close();}}(5)运行测试结果
服务端:
The time server receive order: QUERY TIME ORDER; the counter is : 1The time server receive order: QUERY TIME ORDER; the counter is : 2...The time server receive order: QUERY TIME ORDER; the counter is : 99The time server receive order: QUERY TIME ORDER; the counter is : 100客户端:
Now is: Mon Jul 26 22:18:51 CST 2021; the current is 1Now is: Mon Jul 26 22:18:51 CST 2021; the current is 2...Now is: Mon Jul 26 22:18:51 CST 2021; the current is 99Now is: Mon Jul 26 22:18:51 CST 2021; the current is 100根据结果可知,每条消息都对计数器加1,并没有发生粘包现象 。
2.按分隔符文本解码器DelimiterBasedFrameDecoderDelimiterBasedFrameDecoder是以分隔符作为码流结束标识的消息解码,改造代码,以“$_”作为分隔符
(1)改造TimeServer
增加以“$_”为分隔符的DelimiterBasedFrameDecoder解码器,DelimiterBasedFrameDecoder构造器其中第一个参数长度表示当达到该长度后仍然没有查找到分隔符,就会抛出TooLongFrameException 。这是防止异常码流缺失分隔符导致内存溢出 。
public class TimeServer {public static final Logger log = LoggerFactory.getLogger(TimeServer.class);public static void main(String[] args) throws Exception {new TimeServer().bind();}public void bind() throws Exception {// NIO 线程组NioEventLoopGroup bossGroup = new NioEventLoopGroup();NioEventLoopGroup workerGroup = new NioEventLoopGroup();try {ServerBootstrap bootstrap = new ServerBootstrap();bootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).option(ChannelOption.SO_BACKLOG, 1024).handler(new LoggingHandler(LogLevel.INFO)).childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel socketChannel) {// 以“$_”为分隔符ByteBuf delimiter = Unpooled.copiedBuffer("$_".getBytes());socketChannel.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, delimiter));socketChannel.pipeline().addLast(new StringDecoder());socketChannel.pipeline().addLast(new TimeServerHandler());}});// 绑定端口,同步等待成功ChannelFuture f = bootstrap.bind(NettyConstant.LOCAL_IP, NettyConstant.LOCAL_PORT).sync();log.info("Time server[{}] start success", NettyConstant.LOCAL_IP + ": " + NettyConstant.LOCAL_PORT);// 等待所有服务端监听端口关闭f.channel().closeFuture().sync();} finally {// 优雅退出,释放线程池资源bossGroup.shutdownGracefully();workerGroup.shutdownGracefully();}}}(2)改造TimeServerHandler
对返回客户端的消息增加分隔符“$_”
public class TimeServerHandler extends ChannelInboundHandlerAdapter {private static final Logger log = Logger.getLogger(TimeClientHandler.class.getName());private int counter;@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {// 不需要对消息进行编解码,直接String读取String body = (String) msg;// 每收到一条消息计数器就加1, 理论上应该接收到100条System.out.println("The time server receive order: " + body + "; the counter is : "+ (++counter));String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ?new Date(System.currentTimeMillis()).toString():"BAD ORDER";// 返回客户端需要追加分隔符currentTime = currentTime + "$_";ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());ctx.writeAndFlush(resp);}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {log.warning("Unexpected exception from downstream: " + cause.getMessage());ctx.close();}}(3)改造TimeClient
增加以“$_”为分隔符的DelimiterBasedFrameDecoder解码器
public class TimeClient {public static final Logger log = LoggerFactory.getLogger(TimeClient.class);public static void main(String[] args) throws Exception {new TimeClient().connect(NettyConstant.LOCAL_IP, NettyConstant.LOCAL_PORT);}public void connect(final String host, final int port) throws Exception {// NIO 线程组EventLoopGroup group = new NioEventLoopGroup();try {Bootstrap bootstrap = new Bootstrap();bootstrap.group(group).channel(NioSocketChannel.class).option(ChannelOption.TCP_NODELAY, true).handler(new LoggingHandler(LogLevel.INFO)).handler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel socketChannel) throws Exception {// 以“$_”为分隔符ByteBuf delimiter = Unpooled.copiedBuffer("$_".getBytes());socketChannel.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, delimiter));socketChannel.pipeline().addLast(new StringDecoder());socketChannel.pipeline().addLast(new TimeClientHandler());}});// 发起异步连接操作ChannelFuture f = bootstrap.connect(host, port).sync();// 等待所有服务端监听端口关闭f.channel().closeFuture().sync();} finally {// 优雅退出,释放线程池资源group.shutdownGracefully();}}}(4)改造TimeClientHandler
对发送命令增加“$_”分隔符
public class TimeClientHandler extends ChannelInboundHandlerAdapter {private static final Logger log = Logger.getLogger(TimeClientHandler.class.getName());private int counter;private byte[] req;public TimeClientHandler() {// 以$_为分隔符,发送命令req = ("QUERY TIME ORDER$_").getBytes();}@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {ByteBuf message = null;// 循环发送100条消息,每发送一条刷新一次,服务端理论上接收到100条查询时间指令的请求for (int i = 0; i < 100; i++) {message = Unpooled.buffer(req.length);message.writeBytes(req);ctx.writeAndFlush(message);}}@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {// 不需要编解码了,直接返回了字符串的应答消息String body = (String) msg;// 客户端每接收到服务端一条应答消息之后,计数器就加1,理论上应该有100条服务端日志System.out.println("Now is: " + body + "; the current is "+ (++counter));}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {log.warning("Unexpected exception from downstream: " + cause.getMessage());ctx.close();}}(5)运行测试结果
服务端:
The time server receive order: QUERY TIME ORDER; the counter is : 1The time server receive order: QUERY TIME ORDER; the counter is : 2...The time server receive order: QUERY TIME ORDER; the counter is : 99The time server receive order: QUERY TIME ORDER; the counter is : 100客户端:
Now is: Mon Jul 26 22:18:51 CST 2021; the current is 1Now is: Mon Jul 26 22:18:51 CST 2021; the current is 2...Now is: Mon Jul 26 22:18:51 CST 2021; the current is 99Now is: Mon Jul 26 22:18:51 CST 2021; the current is 100根据结果可知,每条消息都对计数器加1,并没有发生粘包现象 。
3.固定长度解码器FixedLengthFrameDecoderFixedLengthFrameDecoder是固定长度解码器,能够对固定长度的消息进行自动解码,利用FixedLengthFrameDecoder,无论多少数据,都会按照构造函数中设置的固定长度进行解码,如果是半包消息,FixedLengthFrameDecoder会缓存半包消息并等待下一个包到达后进行拼包,直到读取到一个完整的包 。
在服务端ChannelPipeline中新增FixedLengthFrameDecoder,长度为10 。然后增加EchoServerHannel处理器,输出服务端接收到的命令
(1)EchoServer
增加长度为10的FixedLengthFrameDecoder解码器,同时再增加StringDecoder解码器
public class EchoServer {public static final Logger log = LoggerFactory.getLogger(EchoServer.class);public static void main(String[] args) throws Exception {new EchoServer().bind();}public void bind() throws Exception {// NIO 线程组NioEventLoopGroup bossGroup = new NioEventLoopGroup();NioEventLoopGroup workerGroup = new NioEventLoopGroup();try {ServerBootstrap bootstrap = new ServerBootstrap();bootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).option(ChannelOption.SO_BACKLOG, 1024).handler(new LoggingHandler(LogLevel.INFO)).childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel socketChannel) {// 增加固定长度解码器socketChannel.pipeline().addLast(new FixedLengthFrameDecoder(10));// 增加字符解码器,将msg直接转为stringsocketChannel.pipeline().addLast(new StringDecoder());socketChannel.pipeline().addLast(new EchoServerHandler());}});// 绑定端口,同步等待成功ChannelFuture f = bootstrap.bind(NettyConstant.LOCAL_IP, NettyConstant.LOCAL_PORT).sync();log.info("Time server[{}] start success", NettyConstant.LOCAL_IP + ": " + NettyConstant.LOCAL_PORT);// 等待所有服务端监听端口关闭f.channel().closeFuture().sync();} finally {// 优雅退出,释放线程池资源bossGroup.shutdownGracefully();workerGroup.shutdownGracefully();}}}(2)EchoServerHandler
输出客户端发送的命令,直接输出msg即可,因为服务端已经增加了StringDecoder解码器,直接转为String
public class EchoServerHandler extends ChannelInboundHandlerAdapter {private static final Logger log = Logger.getLogger(EchoServerHandler.class.getName());@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {System.out.println("The time server receive order: " + msg);}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {log.warning("Unexpected exception from downstream: " + cause.getMessage());ctx.close();}}(3)Telnet命令测试结果
CMD窗口Telnet窗口连接 telnet 127.0.0.1 8888

文章插图
回显输入消息welcome Lijian

文章插图
查看服务端console
2021-07-26 23:25:21,921INFO [nioEventLoopGroup-2-1] - [id: 0xe4d49ee6, L:/127.0.0.1:8888] READ: [id: 0x928b38a4, L:/127.0.0.1:8888 - R:/127.0.0.1:62315]2021-07-26 23:25:21,922INFO [nioEventLoopGroup-2-1] - [id: 0xe4d49ee6, L:/127.0.0.1:8888] READ COMPLETEThe time server receive order: welcome Li根据结果可知,服务端只接收到客户端发送的“welcome Lijian”的前10个字符,及说明FixedLengthFrameDecoder是有效的
【深入学习习总书记系列讲话精神 5 深入学习Netty——Netty是如何解决TCP粘包拆包问题的?】本篇博文是Netty的基础篇,主要介绍Netty针解决TCP粘包/拆包而产生的解码器,Netty基础篇还涉及到序列化的问题,后面将会继续介绍 。
- 春季老年人吃什么养肝?土豆、米饭换着吃
- 三八妇女节节日祝福分享 三八妇女节节日语录
- 老人谨慎!选好你的“第三只脚”
- 校方进行了深刻的反思 青岛一大学生坠亡校方整改校规
- 脸皮厚的人长寿!有这特征的老人最长寿
- 长寿秘诀:记住这10大妙招 100%增寿
- 春季老年人心血管病高发 3条保命要诀
- 眼睛花不花要看四十八 老年人怎样延缓老花眼
- 香槟然能防治老年痴呆症? 一天三杯它人到90不痴呆
- 老人手抖的原因 为什么老人手会抖
