16 LineBasedFrameDecoder
按行分割协议
LineBasedFrameDecoder
和LineEncoder
采用的通信协议非常简单,即按照行进行分割,遇到一个换行符,则认为是一个完整的报文。在发送方,使用LineEncoder为数据添加换行符;在接受方,使用LineBasedFrameDecoder对换行符进行解码。
1 LineBasedFrameDecoder
LineBasedFrameDecoder采用的通信协议格式非常简单:使用换行符\n或者\r\n作为依据,遇到\n或者\r\n都认为是一条完整的消息。
LineBasedFrameDecoder提供了2个构造方法,如下:
public LineBasedFrameDecoder(final int maxLength) {
this(maxLength, true, false);
}
public LineBasedFrameDecoder(final int maxLength, final boolean stripDelimiter, final boolean failFast) {
this.maxLength = maxLength;
this.failFast = failFast;
this.stripDelimiter = stripDelimiter;
}
其中:
maxLength:
表示一行最大的长度,如果超过这个长度依然没有检测到\n或者\r\n,将会抛出TooLongFrameException
failFast:
与maxLength联合使用,表示超过maxLength后,抛出TooLongFrameException的时机。如果为true,则超出maxLength后立即抛出TooLongFrameException,不继续进行解码;如果为false,则等到完整的消息被解码后,再抛出TooLongFrameException异常。
stripDelimiter:
解码后的消息是否去除\n,\r\n分隔符,例如对于以下二进制字节流:
+--------------+
| ABC\nDEF\r\n |
+--------------+
如果stripDelimiter为true,则解码后的结果为:
+-----+-----+
| ABC | DEF |
+-----+-----+
如果stripDelimiter为false,则解码后的结果为:
+-------+---------+
| ABC\n | DEF\r\n |
+-------+---------+
2 LineEncoder
按行编码,给定一个CharSequence(如String),在其之后添加换行符\n或者\r\n,并封装到ByteBuf进行输出,与LineBasedFrameDecoder相对应。LineEncoder提供了多个构造方法,最终调用的都是:
public LineEncoder(LineSeparator lineSeparator, //换行符号
Charset charset) //换行符编码,默认为CharsetUtil.UTF_8
Netty提供了LineSeparator
来指定换行符,其定义了3个常量, 一般使用DEFAULT
即可。
public final class LineSeparator {
//读取系统属性line.separator,如果读取不到,默认为\n
public static final LineSeparator DEFAULT = new LineSeparator(StringUtil.NEWLINE);
//unix操作系统换行符
public static final LineSeparator UNIX = new LineSeparator("\n”);
//windows操作系统换行度
public static final LineSeparator WINDOWS = new LineSeparator("\r\n”);
//...
}
3 LineBasedFrameDecoder / LineEncoder使用案例
server端:LineBasedFrameDecoderServer
public class LineBasedFrameDecoderServer {
public static void main(String[] args) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(); // (1)
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap(); // (2)
b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class) // (3)
.childHandler(new ChannelInitializer<SocketChannel>() { // (4)
@Override
public void initChannel(SocketChannel ch) throws Exception {
// 使用LineBasedFrameDecoder解决粘包问题,其会根据"\n"或"\r\n"对二进制数据进行拆分,封装到不同的ByteBuf实例中
ch.pipeline().addLast(new LineBasedFrameDecoder(1024, true, true));
// 自定义这个ChannelInboundHandler打印拆包后的结果
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof ByteBuf) {
ByteBuf packet = (ByteBuf) msg;
System.out.println(
new Date().toLocaleString() + ":" + packet.toString(Charset.defaultCharset()));
}
}
});
}
});
// Bind and start to accept incoming connections.
ChannelFuture f = b.bind(8080).sync(); // (7)
System.out.println("LineBasedFrameDecoderServer Started on 8080...");
f.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
}
LineBasedFrameDecoder要解决粘包问题,根据"\n"或"\r\n"对二进制数据进行解码,可能会解析出多个完整的请求报文,其会将每个有效报文封装在不同的ByteBuf实例中,然后针对每个ByteBuf实例都会调用一次其他的ChannelInboundHandler的channelRead方法。
因此LineBasedFrameDecoder接受到一次数据,其之后的ChannelInboundHandler的channelRead方法可能会被调用多次,且之后的ChannelInboundHandler的channelRead方法接受到的ByteBuf实例参数,包含的都是都是一个完整报文的二进制数据。因此无需再处理粘包问题,只需要将ByteBuf中包含的请求信息解析出来即可,然后进行相应的处理。本例中,我们仅仅是打印。
client端:LineBasedFrameDecoderClient
public class LineBasedFrameDecoderClient {
public static void main(String[] args) throws Exception {
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap(); // (1)
b.group(workerGroup); // (2)
b.channel(NioSocketChannel.class); // (3)
b.option(ChannelOption.SO_KEEPALIVE, true); // (4)
b.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
//ch.pipeline().addLast(new LineEncoder());自己添加换行符,不使用LineEncoder
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
//在于server建立连接后,即发送请求报文
public void channelActive(ChannelHandlerContext ctx) {
byte[] req1 = ("hello1" + System.getProperty("line.separator")).getBytes();
byte[] req2 = ("hello2" + System.getProperty("line.separator")).getBytes();
byte[] req3_1 = ("hello3").getBytes();
byte[] req3_2 = (System.getProperty("line.separator")).getBytes();
ByteBuf buffer = Unpooled.buffer();
buffer.writeBytes(req1);
buffer.writeBytes(req2);
buffer.writeBytes(req3_1);
ctx.writeAndFlush(buffer);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
buffer = Unpooled.buffer();
buffer.writeBytes(req3_2);
ctx.writeAndFlush(buffer);
}
});
}
});
// Start the client.
ChannelFuture f = b.connect("127.0.0.1",8080).sync(); // (5)
// Wait until the connection is closed.
f.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
}
}
}
需要注意的是,在client端,我们并没有使用LineEncoder进行编码,原因在于我们要模拟粘包、拆包
。如果使用LineEncoder,那么每次调用ctx.write或者ctx.writeAndflush,LineEncoder都会自动添加换行符,无法模拟拆包问题
。
我们通过自定义了一个ChannelInboundHandler,用于在连接建立后,发送3个请求报文req1、req2、req3。其中req1和req2都是一个完整的报文,因为二者都包含一个换行符;req3分两次发送,第一次发送req3_1,第二次发送req3_2。
首先我们将req1、req2和req3_1一起发送:
+------------------------+
| hello1\nhello2\nhello3 |
+------------------------+
而服务端经过解码后,得到两个完整的请求req1、req2、以及req3的部分数据:
+----------+----------+--------
| hello1\n | hello2\n | hello3
+----------+----------+--------
由于req1、req2都是一个完整的请求,因此可以直接处理。而req3由于只接收到了一部分(半包),需要等到2秒后
,接收到另一部分才能处理。
因此当我们先后启动server端和client之后,在server端的控制台将会有类似以下输出:
LineBasedFrameDecoderServer Started on 8080...
2018-9-8 12:49:02:hello1
2018-9-8 12:49:02:hello2
2018-9-8 12:49:04:hello3
可以看到hello1和hello2是同一时间打印出来的,而hello3是2秒之后才打印。说明LineBasedFrameDecoder成功帮我们处理了粘包和半包问题。
最后,有几点进行说明:
-
部分同学可能认为调用一个writeAndFlush方法就是发送了一个请求,这是对协议的理解不够深刻。一个完整的请求是由协议规定的,例如我们在这里使用了LineBasedFrameDecoder,潜在的含义就是:一行数据才算一个完整的报文。因此当你调用writeAndFlush方法,如果发送的数据有多个换行符,意味着相当于发送了多次有效请求;而如果发送的数据不包含换行符,意味着你的数据还不足以构成一个有效请求。
-
对于粘包问题,例如是两个有效报文粘在一起,那么服务端解码后,可以立即处理这两个报文。
-
对于拆包问题,例如一个报文是完整的,另一个只是半包,netty会对半包的数据进行缓存,等到可以构成一个完整的有效报文后,才会进行处理。这意味着么netty需要缓存每个client的半包数据,如果很多client都发送半包,缓存的数据就会占用大量内存空间。因此我们在实际开发中,不要像上面案例那样,有意将报文拆开来发送。
-
此外,如果client发送了半包,而剩余部分内容没有发送就关闭了,对于这种情况,netty服务端在销毁连接时,会自动清空之前缓存的数据,不会一直缓存。