(入门篇 NettyNIO开发指南)第五章-分隔符和定长解码器使用
TCP 以流的方式进行数据传输上层的应用协议为了对消息进行区分,往往采用如下4种方式。
(1)消息长度固定,累计读取到长度总和为定长LEN 的报文后,就认为读取到了一个完整的消息,将计数器置位,重新开始读取下一个数据报;
(2)将回车换行符作为消息结束符,例如FTP协议,这种方式在文本协议中应用比较广泛:
(3)将特殊的分隔符作为消息的结束标志,回车换行符就是一种特殊的结束分隔符:
(4)通过在消息头中定义长度字段来标识消息的总长度。
Netty对上面四种应用做了统一的抽象提供了4种解码器来解决对应的问题,使用起来非常方便。有了这些解码器,用户不需要自己对读取的报文进行人工解码,也不需要考虑TCP的粘包和拆包。
第4章我们介绍了如何利用LineBasedFrameDecoder解决TCP的粘包问题,本章我们继续学习另外两种实用的解码器一一DelimiterBasedFrameDecoder和FixedLengthFrameDecoer,前者可以自动完成以分隔符做结束标志的消息的解码,后者可以自动完成对定长消息的解码,它们都能解决TCP粘包/拆包导致的读半包问题。
本章主要内容包括:
1.DelimiterBasedFrameDecoder服务端开发
2.DelimiterBasedFrameDecoder客户端开发
3.运行DelimiterBasedFrameDecoder服务端和客户端
4.FixedLengthFrameDecoer服务端开发
5.通过telnet命令行调试FixedLengtllFrameDecoder服务端
5.1 DelimiterBasedFrameDecoder应用开发
通过对DelimiterBasedFrameDecoder的使用,我们可以自动完成以分隔符作为码流结束标识的消息的解码,下面通过一个演示程序来学习下如何DelimiterBasedFrameDecoder进行开发。
演示程序以经典的Echo服务为例。EchoServer接收到EchoClient的请求消息后,将其打印出来,然后将原始消息返回给客户端,消息以“$”作为分隔符。
5.1.1 DelimiterBasedFrameDecoder 服务端开发
下面我们直接看EchoServer的源代码:
EchoServer服务端EchoServer
package lqy4_delimiter_101; import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; 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.DelimiterBasedFrameDecoder; import io.netty.handler.codec.string.StringDecoder; import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; /** * @author lilinfeng * @date 2014年2月14日 * @version 1.0 */ public class EchoServer { 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, 100) .handler(new LoggingHandler(LogLevel.INFO)) .childHandler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { ByteBuf delimiter = Unpooled.copiedBuffer("$_" .getBytes()); ch.pipeline().addLast( new DelimiterBasedFrameDecoder(1024, delimiter)); ch.pipeline().addLast(new StringDecoder()); ch.pipeline().addLast(new EchoServerHandler()); } }); // 绑定端口,同步等待成功 ChannelFuture f = b.bind(port).sync(); // 等待服务端监听端口关闭 f.channel().closeFuture().sync(); } finally { // 优雅退出,释放线程池资源 bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } public static void main(String[] args) throws Exception { int port = 8080; if (args != null && args.length > 0) { try { port = Integer.valueOf(args[0]); } catch (NumberFormatException e) { // 采用默认值 } } new EchoServer().bind(port); } }
我们重点看3741行,首先创建分隔符缓冲对象ByteBuf,本例程中使用$作为分隔符。第40行,创建DelimiterBasedFrameDecoder对象,将其加入到ChannelPipeline中。DelimiterBasedFrameDecoder有多个构造方法,这里我们传递两个参数,第一个1024表示单条消息的最大长度,当达到该长度后仍然没有查找到分隔符,就抛出TooLongFrameException异常,防止由于异常码流缺失分隔符导致的内存溢出,这是Netty解码器的可靠
性保护:第二个参数就是分隔符缓冲对象。
下面继续看EcboServerHandler的实现。
package lqy4_delimiter_101; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandler.Sharable; import io.netty.channel.ChannelHandlerAdapter; import io.netty.channel.ChannelHandlerContext; /** * @author lilinfeng * @date 2014年2月14日 * @version 1.0 */ @Sharable public class EchoServerHandler extends ChannelHandlerAdapter { int counter = 0; @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { String body = (String) msg; System.out.println("This is " + ++counter + " times receive client : [" + body + "]"); body += "$_"; ByteBuf echo = Unpooled.copiedBuffer(body.getBytes()); ctx.writeAndFlush(echo); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close();// 发生异常,关闭链路 } }
第21~23行直接将接收的消息打印出来,由于DelimiterBasedFrameDecoder自动对请求消息进行了解码,后续的ChannelHandler接收到的msg对象就是个完整的消息包;第二个ChannelHandler是StringDecoder,它将ByteBuf解码成字符串对象:第三个EchoServerHandler接收到的msg消息就是解码后的字符串对象。
由于我们设置DelimiterBasedFrameDecoder过滤掉了分隔符,所以,返回给客户端时需要在请求消息尾部拼接分隔符“$_”,最后创建ByteBuf,将原始消息重新返回给客户端。
5.1.2 DelimiterBasedFrameDecoder客户端开发
首先看下EchoClient的实现。
EchoClient客户端EchoClient
1 package lqy4_delimiter_101; 2 3 import io.netty.bootstrap.Bootstrap; 4 import io.netty.buffer.ByteBuf; 5 import io.netty.buffer.Unpooled; 6 import io.netty.channel.ChannelFuture; 7 import io.netty.channel.ChannelInitializer; 8 import io.netty.channel.ChannelOption; 9 import io.netty.channel.EventLoopGroup; 10 import io.netty.channel.nio.NioEventLoopGroup; 11 import io.netty.channel.socket.SocketChannel; 12 import io.netty.channel.socket.nio.NioSocketChannel; 13 import io.netty.handler.codec.DelimiterBasedFrameDecoder; 14 import io.netty.handler.codec.string.StringDecoder; 15 16 /** 17 * @author lilinfeng 18 * @date 2014年2月14日 19 * @version 1.0 20 */ 21 public class EchoClient { 22 23 public void connect(int port, String host) throws Exception { 24 // 配置客户端NIO线程组 25 EventLoopGroup group = new NioEventLoopGroup(); 26 try { 27 Bootstrap b = new Bootstrap(); 28 b.group(group).channel(NioSocketChannel.class) 29 .option(ChannelOption.TCP_NODELAY, true) 30 .handler(new ChannelInitializer<SocketChannel>() { 31 @Override 32 public void initChannel(SocketChannel ch) 33 throws Exception { 34 ByteBuf delimiter = Unpooled.copiedBuffer("$_" 35 .getBytes()); 36 ch.pipeline().addLast( 37 new DelimiterBasedFrameDecoder(1024, 38 delimiter)); 39 ch.pipeline().addLast(new StringDecoder()); 40 ch.pipeline().addLast(new EchoClientHandler()); 41 } 42 }); 43 44 // 发起异步连接操作 45 ChannelFuture f = b.connect(host, port).sync(); 46 47 // 当代客户端链路关闭 48 f.channel().closeFuture().sync(); 49 } finally { 50 // 优雅退出,释放NIO线程组 51 group.shutdownGracefully(); 52 } 53 } 54 55 /** 56 * @param args 57 * @throws Exception 58 */ 59 public static void main(String[] args) throws Exception { 60 int port = 8080; 61 if (args != null && args.length > 0) { 62 try { 63 port = Integer.valueOf(args[0]); 64 } catch (NumberFormatException e) { 65 // 采用默认值 66 } 67 } 68 new EchoClient().connect(port, "127.0.0.1"); 69 } 70 }
与服务端类似,分别将DelimiterBasedFrameDecoder和StringDecoder添加到客户端ChannelPipeline中,最后添加客户端1/0事件处理类EchoClientHandler,下面继续看EchoClientHandler的实现。
EchoClient客尸端 EchoClientHandler
package lqy4_delimiter_101; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerAdapter; import io.netty.channel.ChannelHandlerContext; /** * @author lilinfeng * @date 2014年2月14日 * @version 1.0 */ public class EchoClientHandler extends ChannelHandlerAdapter { private int counter; static final String ECHO_REQ = "Hi, Lilinfeng. Welcome to Netty.$_"; /** * Creates a client-side handler. */ public EchoClientHandler() { } @Override public void channelActive(ChannelHandlerContext ctx) { // ByteBuf buf = UnpooledByteBufAllocator.DEFAULT.buffer(ECHO_REQ // .getBytes().length); // buf.writeBytes(ECHO_REQ.getBytes()); for (int i = 0; i < 10; i++) { ctx.writeAndFlush(Unpooled.copiedBuffer(ECHO_REQ.getBytes())); } } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { System.out.println("This is " + ++counter + " times receive server : [" + msg + "]"); } @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { ctx.flush(); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } }
第25~26行在TCP链路建立成功之后循环发送请求消息给服务端,第32~33行打印接收到的服务端应答消息同时进行计数。
5.1.3 运行DelimiterBasedFrameDecoder服务端和客户端
服务端运行结果如下。
1 This is 1 times receive client : [Hi, Lilinfeng. Welcome to Netty.] 2 This is 2 times receive client : [Hi, Lilinfeng. Welcome to Netty.] 3 This is 3 times receive client : [Hi, Lilinfeng. Welcome to Netty.] 4 This is 4 times receive client : [Hi, Lilinfeng. Welcome to Netty.] 5 This is 5 times receive client : [Hi, Lilinfeng. Welcome to Netty.] 6 This is 6 times receive client : [Hi, Lilinfeng. Welcome to Netty.] 7 This is 7 times receive client : [Hi, Lilinfeng. Welcome to Netty.] 8 This is 8 times receive client : [Hi, Lilinfeng. Welcome to Netty.] 9 This is 9 times receive client : [Hi, Lilinfeng. Welcome to Netty.] 10 This is 10 times receive client : [Hi, Lilinfeng. Welcome to Netty.]
客户端运行结果如下。
1 This is 1 times receive server : [Hi, Lilinfeng. Welcome to Netty.] 2 This is 2 times receive server : [Hi, Lilinfeng. Welcome to Netty.] 3 This is 3 times receive server : [Hi, Lilinfeng. Welcome to Netty.] 4 This is 4 times receive server : [Hi, Lilinfeng. Welcome to Netty.] 5 This is 5 times receive server : [Hi, Lilinfeng. Welcome to Netty.] 6 This is 6 times receive server : [Hi, Lilinfeng. Welcome to Netty.] 7 This is 7 times receive server : [Hi, Lilinfeng. Welcome to Netty.] 8 This is 8 times receive server : [Hi, Lilinfeng. Welcome to Netty.] 9 This is 9 times receive server : [Hi, Lilinfeng. Welcome to Netty.] 10 This is 10 times receive server : [Hi, Lilinfeng. Welcome to Netty.]
服务端成功接收到了客户端发送的10条“Hi,Lilinfeng.WelcometoNetty.”
请求消息,客户端成功接收到了服务端返回的10条“Hi,Lilinfe口g.WelcometoNetty.”应答消息。测试结果表明使用Delin1iterBasedFrameDecoder可以自动对采用分隔符做码流结束标识的消息进行解码。
本例程运行10次的原因是模拟TCP粘包/拆包,在笔者的机器上,连续发送10条Echo请求消息会发生粘包,如果没有DelimiterBasedFrameDecoder解码器的处理,服务端和客户端程序都将运行失败。
下面我们将服务端的DelimiterBasedFrameDecoder注释掉,最终代码如下
服务端结果
This is 1 times receive client : [Hi, Lilinfeng. Welcome to Netty.$_Hi, Lilinfeng. Welcome to Netty.$_Hi, Lilinfeng. Welcome to Netty.$_Hi, Lilinfeng. Welcome to Netty.$_Hi, Lilinfeng. Welcome to Netty.$_Hi, Lilinfeng. Welcome to Netty.$_Hi, Lilinfeng. Welcome to Netty.$_Hi, Lilinfeng. Welcome to Netty.$_Hi, Lilinfeng. Welcome to Netty.$_Hi, Lilinfeng. Welcome to Netty.$_]
由于没有分隔符解码器,导致服务端一次读取了客户端发送的所有消息,这就是典型的没有考虑TCP粘包导致的问题。
5.2 FixedLengthFrameDecoder应用开发
FixedLengtl1FrameDecoder是固定长度解码器,它能够按照指定的长度对消息进行自动解码,开发者不需要考虑TCP的粘包/拆包问题,非常实用。下面我们通过一个应用实例对其用法进行讲解。
5.2.1 FixedlengthFrameDecoder服务端开发
在服务端的ChannelPipeline中新增FixedLengthFrameDecoder,长度设置为20,然后再依次增加字符串解码器和EchoServerHandler,代码如下。
EcboServer服务端 EchoServer
package lqy5_fixlengthframe_108; 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.FixedLengthFrameDecoder; import io.netty.handler.codec.string.StringDecoder; import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; /** * @author lilinfeng * @date 2014年2月14日 * @version 1.0 */ public class EchoServer { 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, 100) .handler(new LoggingHandler(LogLevel.INFO)) .childHandler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast( new FixedLengthFrameDecoder(20)); ch.pipeline().addLast(new StringDecoder()); ch.pipeline().addLast(new EchoServerHandler()); } }); // 绑定端口,同步等待成功 ChannelFuture f = b.bind(port).sync(); // 等待服务端监听端口关闭 f.channel().closeFuture().sync(); } finally { // 优雅退出,释放线程池资源 bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } public static void main(String[] args) throws Exception { int port = 8080; if (args != null && args.length > 0) { try { port = Integer.valueOf(args[0]); } catch (NumberFormatException e) { // 采用默认值 } } new EchoServer().bind(port); } }
EchoServerHandler 的功能 比较简单 ,直接将 读取到的消息打印 出来 ,代码如下 。
EchoServer 服务端 EchoServerHandler
1 package lqy5_fixlengthframe_108; 2 3 import io.netty.channel.ChannelHandler.Sharable; 4 import io.netty.channel.ChannelHandlerAdapter; 5 import io.netty.channel.ChannelHandlerContext; 6 7 /** 8 * @author lilinfeng 9 * @date 2014年2月14日 10 * @version 1.0 11 */ 12 @Sharable 13 public class EchoServerHandler extends ChannelHandlerAdapter { 14 15 @Override 16 public void channelRead(ChannelHandlerContext ctx, Object msg) 17 throws Exception { 18 System.out.println("Receive client : [" + msg + "]"); 19 } 20 21 @Override 22 public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { 23 cause.printStackTrace(); 24 ctx.close();// 发生异常,关闭链路 25 } 26 }
利用FixedLengthFrameDecoder解码器,无论一次接收到多少数据报,它都会按照构造函数中设置的固定长度进行解码,如果是半包消息,FixedLengthFrameDecoder会缓存半包消息并等待下个包到达后进行拼包,直到读取到一个完整的包。
下面的章节我们通过telnet命令行来测试EchoServer服务端,看它能否按照预期进行工作。
5.2.1 利 用 telnet 命令行测试 EchoServer 服务端
5.3 总结
本章我们学习了两个非常实用的解码器:DelimiterBasedFrameDecoder和FixedLengthFrameDecoder。
DelimiterBasedFrameDecoder用于对使用分隔符结尾的消息进行自动解码,FixedLengthFrameDecoder用于对固定长度的消息进行自动解,码。有了上述两种解码器,再结合其他的解码器,如字符串解码器等,可以轻松地完成对很多消息的自动解码,而且不再需要考虑TCP粘包/拆包导致的读半包问题,极大地提升了开发效率。
应用DelimiterBasedFrameDecoder和FixedLengthFrameDecoder进行开发非常简单,在绝大数情况下,只要将DelimiterBasedFrameDecoder或FixedLengthFran1eDecoder添加到对应ChanneIPipeline的起始位即可。
熟悉了Netty的NIO基础应用开发之后,从第三部分开始,我们继续学习编解码技术。在了解编解码基础知识之后,继续学习Netty内置的编解码框架的使用,例如Java序列化、二进制编解码、谷歌的protobuf和JBoss的Marshalling序列化框架。