前言

  学习Netty避免不了要去了解TCP粘包/拆包问题,熟悉各个编解码器是如何解决TCP粘包/拆包问题的,同时需要知道TCP粘包/拆包问题是怎么产生的。

  在此博文前,可以先学习了解前几篇博文:

  参考资料《Netty In Action》、《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包接收完全,期间发生多次拆包。

2.TCP粘包/拆包问题发生的原因

 TCP是以流动的方式传输数据,传输的最小单位为一个报文段(segment)。主要有如下几个指标影响或造成TCP粘包/拆包问题,分别为MSS、MTU、缓冲区,以及Nagle算法的影响。

(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在传输前将进行拆包

(2)粘包发生原因

  • 要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。
  • 接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包

二、TCP粘包/拆包问题解决策略

1.常用的解决策略

由于底层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; @Override
public 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);
} @Override
public 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>() {
@Override
protected 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();
} @Override
public 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);
}
} @Override
public 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));
} @Override
public 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>() {
@Override
protected 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 ORDER
QUERY TIME ORDER
... // 此处忽略96个QUERY TIME ORDER
QUERY TIME ORDER
QUERY 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和StringDecoder

  LineBasedFramedDecoder:依次遍历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>() {
@Override
protected 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; @Override
public 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);
} @Override
public 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>() {
@Override
protected 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();
} @Override
public 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);
}
} @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 不需要编解码了,直接返回了字符串的应答消息
String body = (String) msg;
// 客户端每接收到服务端一条应答消息之后,计数器就加1,理论上应该有100条服务端日志
System.out.println("Now is: " + body + "; the current is "+ (++counter));
} @Override
public 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 : 1
The time server receive order: QUERY TIME ORDER; the counter is : 2
...
The time server receive order: QUERY TIME ORDER; the counter is : 99
The time server receive order: QUERY TIME ORDER; the counter is : 100

客户端:

Now is: Mon Jul 26 22:18:51 CST 2021; the current is 1
Now 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 99
Now is: Mon Jul 26 22:18:51 CST 2021; the current is 100

根据结果可知,每条消息都对计数器加1,并没有发生粘包现象。

2.按分隔符文本解码器DelimiterBasedFrameDecoder

  DelimiterBasedFrameDecoder是以分隔符作为码流结束标识的消息解码,改造代码,以“$_”作为分隔符

(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>() {
@Override
protected 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; @Override
public 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);
} @Override
public 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>() {
@Override
protected 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();
} @Override
public 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);
}
} @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 不需要编解码了,直接返回了字符串的应答消息
String body = (String) msg;
// 客户端每接收到服务端一条应答消息之后,计数器就加1,理论上应该有100条服务端日志
System.out.println("Now is: " + body + "; the current is "+ (++counter));
} @Override
public 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 : 1
The time server receive order: QUERY TIME ORDER; the counter is : 2
...
The time server receive order: QUERY TIME ORDER; the counter is : 99
The time server receive order: QUERY TIME ORDER; the counter is : 100

客户端:

Now is: Mon Jul 26 22:18:51 CST 2021; the current is 1
Now 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 99
Now is: Mon Jul 26 22:18:51 CST 2021; the current is 100

根据结果可知,每条消息都对计数器加1,并没有发生粘包现象。

3.固定长度解码器FixedLengthFrameDecoder

  FixedLengthFrameDecoder是固定长度解码器,能够对固定长度的消息进行自动解码,利用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>() {
@Override
protected void initChannel(SocketChannel socketChannel) {
// 增加固定长度解码器
socketChannel.pipeline().addLast(new FixedLengthFrameDecoder(10));
// 增加字符解码器,将msg直接转为string
socketChannel.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());

    @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("The time server receive order: " + msg);
} @Override
public 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,921  INFO [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,922 INFO [nioEventLoopGroup-2-1] - [id: 0xe4d49ee6, L:/127.0.0.1:8888] READ COMPLETE
The time server receive order: welcome Li

根据结果可知,服务端只接收到客户端发送的“welcome Lijian”的前10个字符,及说明FixedLengthFrameDecoder是有效的

本篇博文是Netty的基础篇,主要介绍Netty针解决TCP粘包/拆包而产生的解码器,Netty基础篇还涉及到序列化的问题,后面将会继续介绍。

深入学习Netty(5)——Netty是如何解决TCP粘包/拆包问题的?的更多相关文章

  1. 《精通并发与Netty》学习笔记(13 - 解决TCP粘包拆包(一)概念及实例演示)

    一.粘包/拆包概念 TCP是一个“流”协议,所谓流,就是没有界限的一长串二进制数据.TCP作为传输层协议并不不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行数据包的划分,所以在业务上认 ...

  2. Netty使用LineBasedFrameDecoder解决TCP粘包/拆包

    TCP粘包/拆包 TCP是个”流”协议,所谓流,就是没有界限的一串数据.TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TC ...

  3. 《精通并发与Netty》学习笔记(14 - 解决TCP粘包拆包(二)Netty自定义协议解决粘包拆包)

    一.Netty粘包和拆包解决方案 Netty提供了多个解码器,可以进行分包的操作,分别是: * LineBasedFrameDecoder (换行)   LineBasedFrameDecoder是回 ...

  4. 1. Netty解决Tcp粘包拆包

    一. TCP粘包问题 实际发送的消息, 可能会被TCP拆分成很多数据包发送, 也可能把很多消息组合成一个数据包发送 粘包拆包发生的原因 (1) 应用程序一次写的字节大小超过socket发送缓冲区大小 ...

  5. 【转】Netty之解决TCP粘包拆包(自定义协议)

    1.什么是粘包/拆包 一般所谓的TCP粘包是在一次接收数据不能完全地体现一个完整的消息数据.TCP通讯为何存在粘包呢?主要原因是TCP是以流的方式来处理数据,再加上网络上MTU的往往小于在应用处理的消 ...

  6. Netty之解决TCP粘包拆包(自定义协议)

    1.什么是粘包/拆包 一般所谓的TCP粘包是在一次接收数据不能完全地体现一个完整的消息数据.TCP通讯为何存在粘包呢?主要原因是TCP是以流的方式来处理数据,再加上网络上MTU的往往小于在应用处理的消 ...

  7. Netty解决TCP粘包/拆包问题 - 按行分隔字符串解码器

    服务端 package org.zln.netty.five.timer; import io.netty.bootstrap.ServerBootstrap; import io.netty.cha ...

  8. 【Netty】使用解码器Decoder解决TCP粘包和拆包问题

    解码器Decoder和ChannelHandler的关系 netty的解码器通常是继承自ByteToMessageDecoder,而它又是继承自ChannelInboundHandlerAdapter ...

  9. Netty TCP粘包/拆包问题《一》

    1.使用LineBasedFrameDecoder,StringDecoder解析器进行解决TCP粘包/拆包问题 2.代码搞起: TimeClient:客户端 /* * Copyright 2013- ...

随机推荐

  1. adb安装 mac和Windows

    一.mac安装 参考地址https://blog.csdn.net/VSRfind/article/details/79593098 1.首先安装一个软件 在用Mac进行Android开发之前,我们一 ...

  2. JUC 并发编程--04 常用的辅助类CountDownLatch , CyclicBarrier , Semaphore , 读写锁 , 阻塞队列,CompletableFuture(异步回调)

    CountDownLatch 相当于一个减法计数器, 构造方法指定一个数字,比如6, 一个线程执行一次,这个数字减1, 当变为0 的时候, await()方法,才开始往下执行,, 看这个例子 Cycl ...

  3. 学习响应式编程 Reactor (4) - reactor 转换类操作符(1)

    Reactor 操作符 数据在响应式流中的处理,就像流过一条装配流水线.Reactor 既是传送带,又是一个个的装配工或机器人.原材料从源头(最初的 Publisher )流出,经过一个个的装配线中装 ...

  4. 想自己写框架?不了解Java注解机制可不行

    无论是在JDK还是框架中,注解都是很重要的一部分,我们使用过很多注解,但是你有真正去了解过他的实现原理么?你有去自己写过注解么? 概念 注解(Annotation),也叫元数据.一种代码级别的说明.它 ...

  5. 合宙Luat直播间即将开启,你揭开行业奥秘,让你快人一步。

    嗨~刚陪你们过儿童节 和你们一起成长的合宙Luat 又有新计划 -- 合宙Luat官方直播即将开启 - 敬请关注 - - 官方直播什么内容 - 可能是合宙研发动态 可能是新品发布资讯 可能是行业大咖分 ...

  6. 【题解】hdu 3586 Information Disturbing 二分 树形dp

    题目描述 Information DisturbingTime Limit: 6000/3000 MS (Java/Others) Memory Limit: 131072/65536 K (Java ...

  7. PL/SQL连不上,报 ORA-12170:TNS 连接超时

    排错步骤: 1.查看网络是否通畅 打开cmd, ping 数据库IP 2. 查看端口是否通畅 打开cmd,tnsping 数据库IP 如果piing不通,可能是防火墙问题 3.检查防火墙状态 #ser ...

  8. 写DockerFile的一些技巧

    Docker镜像由只读层组成,每个层都代表一个Dockerfile指令.这些层是堆叠的,每一层都是前一层变化的增量.示例Dockerfile: ​ FROM ubuntu:15.04 COPY . / ...

  9. 在vs中调试关闭之后不关闭页面

    在vs中调试api时会自动打开一个新的浏览器窗口,在关闭这个浏览器窗口时,会关闭调试.关闭调试时也会关闭浏览器窗口. 设置成调试时在已有的浏览器中打开调试页面,关闭调试也不会关掉浏览器窗口,反之亦然 ...

  10. 我用段子讲.NET之依赖注入其一

    <我用段子讲.NET之依赖注入其一> 1) 西城的某个人工湖畔,湖水清澈见底,湖畔柳树成荫.人工湖往北,坐落着两幢写字楼,水晶大厦靠近地铁站,由于为了与湖面天际线保持一致,楼层只有26层高 ...