前言

  学习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. 激光雷达Lidar与毫米波雷达Radar:自动驾驶的利弊

    激光雷达Lidar与毫米波雷达Radar:自动驾驶的利弊 Lidar vs Radar: pros and cons for autonomous driving 新型无人驾驶汽车的数量在缓慢增加,各 ...

  2. 如果攻击者操控了 redirect_uri,会怎样?

    读者在看这篇文章之前,请先了解 Oauth2.0 的 Authorization Code 授权流程,可以看 Authorization Code 授权原理和实现方法 在 Token Enpoint ...

  3. springboot——重定向解决刷新浏览器造成表单重复提交的问题(超详细)

    原因:造成表单重复提交的原因是当我们刷新浏览器的时候,浏览器会发送上一次提交的请求.由于上一次提交的请求方式为post,刷新浏览器就会重新发送这个post请求,造成表单重复提交. 解决办法: 将请求当 ...

  4. 重新整理 .net core 实践篇—————中间件[十九]

    前言 简单介绍一下.net core的中间件. 正文 官方文档已经给出了中间件的概念图: 和其密切相关的是下面这两个东西: IApplicationBuilder 和 RequestDelegate( ...

  5. Java字符串比较(3种方法)以及对比 C++ 时的注意项

    字符串比较是常见的操作,包括比较相等.比较大小.比较前缀和后缀串等.在 Java 中,比较字符串的常用方法有 3 个:equals() 方法.equalsIgnoreCase() 方法. compar ...

  6. 深度解读MRS IoTDB时序数据库的整体架构设计与实现

    [本期推荐]华为云社区6月刊来了,新鲜出炉的Top10技术干货.重磅技术专题分享:还有毕业季闯关大挑战,华为云专家带你做好职业规划. 摘要:本文将会系统地为大家介绍MRS IoTDB的来龙去脉和功能特 ...

  7. excel VBA根据单元格内的逗号把内容拆分行

    Sub test1()    Dim h    Dim j As Integer    j = 0  '用于辅助循环的进行,可以在拆分行获取下一个需要拆分单元格的行号    'Application. ...

  8. Linux 中的 AutoHotKey 键映射替代方案

    1. Windows 之 AutoHotKey 初次了解AutoHotKey,是在Win 下最爱效率神器:AutoHotKey | 晚晴幽草轩这篇博客中,博主有对AutoHotKey作详细介绍,这里不 ...

  9. Redis的Pipeline、事务和lua

    1. Pipeline 1.1 Pipeline概念 Redis客户端执行一条命令分别为如下4个过程: 1) 发送命令 2) 命令排队 3) 命令执行 4) 返回结果 其中1)+4)称为Round T ...

  10. explicit 关键字 禁止隐式转换

    explicit可以抑制内置类型隐式转换,所以在类的构造函数中,使用explicit关键字,防止不必要的隐式转换