• 解码分隔符和基于长度的协议
    • 分隔符协议
    • 基于长度的协议

    解码分隔符和基于长度的协议

    使用 Netty 时会遇到需要解码以分隔符和长度为基础的协议,本节讲解Netty 如何解码这些协议。

    分隔符协议

    经常需要处理分隔符协议或创建基于它们的协议,例如SMTP、POP3、IMAP、Telnet等等。Netty 附带的解码器可以很容易的提取一些序列分隔:

    Table 8.5 Decoders for handling delimited and length-based protocols

    名称 描述
    DelimiterBasedFrameDecoder 接收ByteBuf由一个或多个分隔符拆分,如NUL或换行符
    LineBasedFrameDecoder 接收ByteBuf以分割线结束,如”\n”和”\r\n”

    下图显示了使用”\r\n”分隔符的处理:

    解码分隔符和基于长度的协议 - 图1

    1. 字节流
    2. 第一帧
    3. 第二帧

    Figure 8.5 Handling delimited frames

    下面展示了如何用 LineBasedFrameDecoder 处理

    Listing 8.8 Handling line-delimited frames

    1. public class LineBasedHandlerInitializer extends ChannelInitializer<Channel> {
    2. @Override
    3. protected void initChannel(Channel ch) throws Exception {
    4. ChannelPipeline pipeline = ch.pipeline();
    5. pipeline.addLast(new LineBasedFrameDecoder(65 * 1024)); //1
    6. pipeline.addLast(new FrameHandler()); //2
    7. }
    8. public static final class FrameHandler extends SimpleChannelInboundHandler<ByteBuf> {
    9. @Override
    10. public void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception { //3
    11. // Do something with the frame
    12. }
    13. }
    14. }
    1. 添加一个 LineBasedFrameDecoder 用于提取帧并把数据包转发到下一个管道中的处理程序,在这种情况下就是 FrameHandler
    2. 添加 FrameHandler 用于接收帧
    3. 每次调用都需要传递一个单帧的内容

    使用 DelimiterBasedFrameDecoder 可以方便处理特定分隔符作为数据结构体的这类情况。如下:

    • 传入的数据流是一系列的帧,每个由换行(“\n”)分隔
    • 每帧包括一系列项目,每个由单个空格字符分隔
    • 一帧的内容代表一个“命令”:一个名字后跟一些变量参数

    清单8.9中显示了的实现的方式。定义以下类:

    • 类 Cmd - 存储帧的内容,其中一个 ByteBuf 用于存名字,另外一个存参数
    • 类 CmdDecoder - 从重写方法 decode() 中检索一行,并从其内容中构建一个 Cmd 的实例
    • 类 CmdHandler - 从 CmdDecoder 接收解码 Cmd 对象和对它的一些处理。

    所以关键的解码器是扩展了 LineBasedFrameDecoder

    Listing 8.9 Decoder for the command and the handler

    1. public class CmdHandlerInitializer extends ChannelInitializer<Channel> {
    2. @Override
    3. protected void initChannel(Channel ch) throws Exception {
    4. ChannelPipeline pipeline = ch.pipeline();
    5. pipeline.addLast(new CmdDecoder(65 * 1024));//1
    6. pipeline.addLast(new CmdHandler()); //2
    7. }
    8. public static final class Cmd { //3
    9. private final ByteBuf name;
    10. private final ByteBuf args;
    11. public Cmd(ByteBuf name, ByteBuf args) {
    12. this.name = name;
    13. this.args = args;
    14. }
    15. public ByteBuf name() {
    16. return name;
    17. }
    18. public ByteBuf args() {
    19. return args;
    20. }
    21. }
    22. public static final class CmdDecoder extends LineBasedFrameDecoder {
    23. public CmdDecoder(int maxLength) {
    24. super(maxLength);
    25. }
    26. @Override
    27. protected Object decode(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {
    28. ByteBuf frame = (ByteBuf) super.decode(ctx, buffer); //4
    29. if (frame == null) {
    30. return null; //5
    31. }
    32. int index = frame.indexOf(frame.readerIndex(), frame.writerIndex(), (byte) ' '); //6
    33. return new Cmd(frame.slice(frame.readerIndex(), index), frame.slice(index +1, frame.writerIndex())); //7
    34. }
    35. }
    36. public static final class CmdHandler extends SimpleChannelInboundHandler<Cmd> {
    37. @Override
    38. public void channelRead0(ChannelHandlerContext ctx, Cmd msg) throws Exception {
    39. // Do something with the command //8
    40. }
    41. }
    42. }
    1. 添加一个 CmdDecoder 到管道;将提取 Cmd 对象和转发到在管道中的下一个处理器
    2. 添加 CmdHandler 将接收和处理 Cmd 对象
    3. 命令也是 POJO
    4. super.decode() 通过结束分隔从 ByteBuf 提取帧
    5. frame 是空时,则返回 null
    6. 找到第一个空字符的索引。首先是它的命令名;接下来是参数的顺序
    7. 从帧先于索引以及它之后的片段中实例化一个新的 Cmd 对象
    8. 处理通过管道的 Cmd 对象

    基于长度的协议

    基于长度的协议协议在帧头文件里定义了一个帧编码的长度,而不是结束位置用一个特殊的分隔符来标记。表8.6列出了 Netty 提供的两个解码器,用于处理这种类型的协议。

    Table 8.6 Decoders for length-based protocols

    名称 描述
    FixedLengthFrameDecoder 提取固定长度
    LengthFieldBasedFrameDecoder 读取头部长度并提取帧的长度

    如下图所示,FixedLengthFrameDecoder 的操作是提取固定长度每帧8字节

    解码分隔符和基于长度的协议 - 图2

    1. 字节流 stream
    2. 4个帧,每个帧8个字节

    大部分时候帧的大小被编码在头部,这种情况可以使用LengthFieldBasedFrameDecoder,它会读取头部长度并提取帧的长度。下图显示了它是如何工作的:

    解码分隔符和基于长度的协议 - 图3

    1. 长度 “0x000C” (12) 被编码在帧的前两个字节
    2. 后面的12个字节就是内容
    3. 提取没有头文件的帧内容

    Figure 8.7 Message that has frame size encoded in the header

    LengthFieldBasedFrameDecoder 提供了几个构造函数覆盖各种各样的头长字段配置情况。清单8.10显示了使用三个参数的构造函数是maxFrameLength,lengthFieldOffset lengthFieldLength。在这
    情况下,帧的长度被编码在帧的前8个字节。

    Listing 8.10 Decoder for the command and the handler

    1. public class LengthBasedInitializer extends ChannelInitializer<Channel> {
    2. @Override
    3. protected void initChannel(Channel ch) throws Exception {
    4. ChannelPipeline pipeline = ch.pipeline();
    5. pipeline.addLast(
    6. new LengthFieldBasedFrameDecoder(65 * 1024, 0, 8)); //1
    7. pipeline.addLast(new FrameHandler()); //2
    8. }
    9. public static final class FrameHandler
    10. extends SimpleChannelInboundHandler<ByteBuf> {
    11. @Override
    12. public void channelRead0(ChannelHandlerContext ctx,
    13. ByteBuf msg) throws Exception {
    14. // Do something with the frame //3
    15. }
    16. }
    17. }
    1. 添加一个 LengthFieldBasedFrameDecoder ,用于提取基于帧编码长度8个字节的帧。
    2. 添加一个 FrameHandler 用来处理每帧
    3. 处理帧数据

    总而言之,本部分探讨了 Netty 提供的编解码器支持协议,包括定义特定的分隔符的字节流的结构或协议帧的长度。这些编解码器非常有用。