目录
利用LineBasedFrameDecoder 解决TCP 粘包问题
LineBasedFrameDecoder 和 StringDecoder 的原理分析
概念
TCP 是个“流”协议,所谓流,就是没有界限的一串数据。TCP底层并不了解上层业务数据的具体含义,它会根据 TCP 缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被 TCP 拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的 TCP 粘包和拆包问题。
1. TCP 粘包/拆包问题说明
假设客户端分别发送了两个数据包 D1 和 D2 给服务端,由于服务端一次读取到的字节数是不确定的,可能存在以上图中的情况。D1 和 D2 被粘包和拆包的发送情况。
2、TCP 粘包/拆包发生的原因
1.应用程序 write 写入的字节大小大于套接口发送缓冲区大小;
2.进行 MSS 大小的 TCP 分段;
3.以太网帧的 payload 大于 MTU 进行 IP 分片。
3、粘包问题的解决策略
1.消息定长,例如每个报文的大小为固定长度 200字节,如果不够,空位补空格;
2.在包尾增加回车换行符进行分割,例如 FTP 协议;
3.将消息分为消息头和消息体,消息头中包含表示消息总长度的字段,通常设计思路为消息头的第一个字段使用 int32 来表示消息的总长度。
4.更复杂的应用层协议。
未考虑 TCP 粘包导致功能异常案例
服务端代码改造:
package com.netty.netty;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import java.util.Date;
public class NettyTimeServerHandler extends ChannelHandlerAdapter {
private int counter;
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
@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());
System.out.println("server recv body: " + body + " ; the counter is : " + ++counter);
String now = "QUERY TIME ORDER".equals(body) ? new Date(System.currentTimeMillis()).toString()
: "BAD ORDER";
now += System.getProperty("line.separator");
ByteBuf resp = Unpooled.copiedBuffer(now.getBytes());
ctx.writeAndFlush(resp);
}
}
客户端代码改造:
package com.netty.netty;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
public class NettyTimeClientHandler extends ChannelHandlerAdapter {
private int counter;
private byte[] req;
public NettyTimeClientHandler() {
req = ("QUERY TIME ORDER" + System.getProperty("line.separator")).getBytes();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ByteBuf message = null;
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");
System.out.println("Now is : " + body + " ; the counter is : " + ++counter);
}
}
运行结果如下
服务端:
客户端:
服务端只接收到两条消息,这说明发生了 TCP 粘包。
客户端也只接收到了两条消息,这说明应答消息也发生了粘包。
利用LineBasedFrameDecoder 解决TCP 粘包问题
为了解决 TCP 粘包/拆包导致的半包读写问题,Netty默认提供了多种编解码器用于处理半包。
服务端代码修改:
package com.netty.netty;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
public class NettyTimeServer {
public void bind(int port) throws Exception {
// 配置服务端的 NIO 线程组
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.childHandler(new ChildChannelHandler());
// 绑定端口,同步等待成功
ChannelFuture f = b.bind(port).sync();
// 等待服务端监听端口关闭
f.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel channel) throws Exception {
// 新增两个解码器
channel.pipeline().addLast(new LineBasedFrameDecoder(1024));
channel.pipeline().addLast(new StringDecoder());
channel.pipeline().addLast(new NettyTimeServerHandler());
}
}
public static void main(String[] args) throws Exception {
int port = 8080;
new NettyTimeServer().bind(port);
}
}
package com.netty.netty;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import java.util.Date;
public class NettyTimeServerHandler extends ChannelHandlerAdapter {
private int counter;
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
@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());
String body = (String) msg;
System.out.println("server recv body: " + body + " ; the counter is : " + ++counter);
String now = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new Date(System.currentTimeMillis()).toString()
: "BAD ORDER";
now += System.getProperty("line.separator");
ByteBuf resp = Unpooled.copiedBuffer(now.getBytes());
ctx.writeAndFlush(resp);
}
}
客户端代码修改:
package com.netty.netty;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
public class NettyTimeClient {
public void connect(int port, String host) throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
try{
Bootstrap b = new Bootstrap();
b.group(group).channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.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 NettyTimeClientHandler());
}
});
// 发起异步连接操作
ChannelFuture f = b.connect(host, port).sync();
// 等待客户端链路关闭
f.channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
int port = 8080;
new NettyTimeClient().connect(port, "127.0.0.1");
}
}
package com.netty.netty;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
public class NettyTimeClientHandler extends ChannelHandlerAdapter {
private int counter;
private byte[] req;
public NettyTimeClientHandler() {
req = ("QUERY TIME ORDER" + System.getProperty("line.separator")).getBytes();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ByteBuf message = null;
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");
String body = (String) msg;
System.out.println("Now is : " + body + " ; the counter is : " + ++counter);
}
}
运行结果:
结果完全符合预期,说明通过使用 LineBasedFrameDecoder 和 StringDecoder 成功解决了 TCP 粘包导致的读半包问题。
LineBasedFrameDecoder 和 StringDecoder 的原理分析
LineBasedFrameDecoder 的工作原理是它依次遍历 ByteBuf 中的可读字节,判断看是否有 “\n” 或者 “\r\n”,如果有,就以此位置为结束为止,从可读索引到结束为止区间的字节就组成了一行。它是以换行符为结束标志的解码器,支持携带结束符或者不携带结束符两种解码方式,同时支持配置单行的最大长度。如果连续读取到最大长度后仍然没有发现换行符,就会抛出异常,同时忽略掉之前读到的异常码流。
StringDecoder 的功能非常简单,就是将接收到的对象转换成字符串,然后继续调用后面的 Handler。LineBasedFrameDecoder + StringDecoder 组合就是按行切换的文本解码器,它被设计用来支持TCP 的粘包和拆包。
摘自:Netty权威指南