《netty权威指南》学习笔记
TCP粘包/拆包
定义
TCP是个“流”协议,所谓流,就是没有界限的一串数据。TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分。所以,一个完成的包可能会被TCP拆分为多个包,也有可能把多个小包封装成一个大包,这就是所谓的TCP粘包河拆包问题。
解决策略
- 消息定长:例如每个报文大小固定为200字节,如果不够,空位补空格
- 在包尾增加回车换行符进行分割,如FTP协议
- 将消息分为消息头和消息体,消息头中包含消息总长度(或者消息体总长度);–通常的设计思路是在消息头中使用int32来表示消息的总长度;
- 更复杂的应用协议
TCP粘包/拆包-异常案例
我们来复现异常比较经典的TCP粘包/拆包现场;
客户端循环发送100次请求消息,每发送一条消息就刷新一次,保证每条消息都会被写入Channel中,按照我们的设计,服务端应该会接收到100条查询请求;
客户端核心代码ChannelHandle
@ChannelHandler.Sharable
class TimeClientHandler extends ChannelHandlerAdapter {
//用来记录反馈的次数
private int repeat ;
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//每次发送消息,结尾增加一个换行符
byte[] req = ("QUERY TIME ORDER"+System.getProperty("line.separator")).getBytes();
ByteBuf firstMsg = null;
//循环发送一百次 请求消息
for (int i = 0; i < 100; i++) {
firstMsg = Unpooled.copiedBuffer(req);
ctx.channel().writeAndFlush(firstMsg);
}
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
byte[] resp = new byte[buf.readableBytes()];
buf.readBytes(resp);
String body = new String(resp, "UTF-8").substring(0,resp.length - System.getProperty("line.separator").length());
System.out.println("Now is:" + body+"; repeat is:" + ++repeat);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.channel().close();
}
}
服务端核心代码Handler
@ChannelHandler.Sharable
class TimeServerHandler extends ChannelHandlerAdapter {
//用来记录请求的次数
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());
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 += System.getProperty("line.separator");
ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
ctx.writeAndFlush(resp);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
执行的结果如下图:
虽然客户端发送了100次请求,但是服务端最终只接收了两次请求,说明客户端发送消息时发生了粘包,由于服务端只接收了两次请求,因此服务端也只会对客户端发送两条应答;
但是从客户端的打印消息来看,客户端只接收了一条应答消息,说明服务端应答消息也发生了应答。
Netty解决TCP粘包/拆包之道
1. LineBasedFrameDecoder
直接在客户端和服务端的handler中增加LineBasedFrameDecoder解码器即可。
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
LineBasedFrameDecoder的工作原理:它依次遍历ByteBuf中可读字节,判断是否包含有”\n”或者”\r\n”,如果有,就以此位置为结束位置,从可读索引到结束位置区间的字节组成一行。它是以换行符为结束标志的解码器,支持携带结束符和不携带结束符两种解码方式,同时支持配置单行的最大长度。如果连续读取到最大长度仍然没有发现换行符,就会抛出异常(TooLongFrameException),同时忽略掉之前读取到的异常码流。
LineBasedFrameDecoder构造函数-分析
public LineBasedFrameDecoder(final int maxLength) {
this(maxLength, true, false);
}
/**
* maxLength 最大长度
* stripDelimiter 解码帧是否应该去掉分隔符
* failFast
* ture:无论maxLength部分有没有被read,直接抛出异常;
* false:after被读取后抛出异常
*
*/
public LineBasedFrameDecoder(final int maxLength, final boolean stripDelimiter, final boolean failFast) { }
2.DelimiterBasedFrameDecoder
DelimiterBasedFrameDecoder是以指定的分隔符做结束标识的消息解码器。
ByteBuf delimiter =Unpooled.copiedBuffer("$$_".getBytes());
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024,delimiter));
上述以”$$_”作为分隔符。
DelimiterBasedFrameDecoder构造函数-分析
public DelimiterBasedFrameDecoder(int maxFrameLength, ByteBuf delimiter) {
this(maxFrameLength, true, delimiter);
}
public DelimiterBasedFrameDecoder(int maxFrameLength, boolean stripDelimiter, ByteBuf delimiter);
//支持多个分隔符
public DelimiterBasedFrameDecoder(int maxFrameLength, ByteBuf... delimiters)
3.FixedLengthFrameDecoder
FixedLengthFrameDecoder是以固定长度解码器,它能够按照指定的长度对消息进行解码。无论一次接收多少数据报,他都会按照构造函数中指定的长度进行解码,如果是半包消息,则会缓存半包消息,等待下个包到达之后进行拼包,知道读取到一个完整的包。
ch.pipeline().addLast(new FixedLengthFrameDecoder(3));