(入门篇 NettyNIO开发指南)第五章-分隔符和定长解码器使用

TCP    以流的方式进行数据传输上层的应用协议为了对消息进行区分,往往采用如下4种方式。

(1)消息长度固定,累计读取到长度总和为定长LEN 的报文后,就认为读取到了一个完整的消息,将计数器置位,重新开始读取下一个数据报;
(2)将回车换行符作为消息结束符,例如FTP协议,这种方式在文本协议中应用比较广泛:
(3)将特殊的分隔符作为消息的结束标志,回车换行符就是一种特殊的结束分隔符:
(4)通过在消息头中定义长度字段来标识消息的总长度。

Netty对上面四种应用做了统一的抽象提供了4种解码器来解决对应的问题,使用起来非常方便。有了这些解码器,用户不需要自己对读取的报文进行人工解码,也不需要考虑TCP的粘包和拆包。


第4章我们介绍了如何利用LineBasedFrameDecoder解决TCP的粘包问题,本章我们继续学习另外两种实用的解码器一一DelimiterBasedFrameDecoderFixedLengthFrameDecoer,前者可以自动完成以分隔符做结束标志的消息的解码,后者可以自动完成对定长消息的解码,它们都能解决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    总结

本章我们学习了两个非常实用的解码器:DelimiterBasedFrameDecoderFixedLengthFrameDecoder
DelimiterBasedFrameDecoder用于对使用分隔符结尾的消息进行自动解码,FixedLengthFrameDecoder用于对固定长度的消息进行自动解,码。有了上述两种解码器,再结合其他的解码器,如字符串解码器等,可以轻松地完成对很多消息的自动解码,而且不再需要考虑TCP粘包/拆包导致的读半包问题,极大地提升了开发效率。
应用DelimiterBasedFrameDecoder和FixedLengthFrameDecoder进行开发非常简单,在绝大数情况下,只要将DelimiterBasedFrameDecoder或FixedLengthFran1eDecoder添加到对应ChanneIPipeline的起始位即可。
熟悉了Netty的NIO基础应用开发之后,从第三部分开始,我们继续学习编解码技术。在了解编解码基础知识之后,继续学习Netty内置的编解码框架的使用,例如Java序列化、二进制编解码、谷歌的protobuf和JBoss的Marshalling序列化框架。

 

posted @ 2015-10-22 11:10  crazyYong  阅读(999)  评论(0编辑  收藏  举报