• Netty自定义协议解析原理与应用


    目前,大家都选择Netty做为游戏服务器框架网络通信的框架,而且目前也有很多优秀的产品是基于Netty开发的。它的稳定性,易用性和高效率性已得到广泛的认同。在游戏服务器开发中,选择netty一般就意味着我们要使用长连接来建立与客户端的通信,并且是自定义协议,在网络开发中,我们不得不处理断包,粘包的问题,因为Tcp/ip是基于数据流的传输,包与包之间没有明确的界限,而且于由网络路由的复杂性,大包有可能分成小包,小包也有可能被组装成大包进行传输。而Netty就考虑到了这一点,而且它用一个类就帮我们处理了这个问题,这个类就是:LengthFieldBasedFrameDecoder。这里是它的API说明:http://netty.io/4.1/api/index.html

    这里简单翻译一下,以供参考。

    这个解码器是用来动态分割消息包的,这些消息包都带有一个表示消息长度的值。当你需要解码一个二进制流的包时,有一个表示消息内容长度或整个包长度的包头是非常有用的。LengthFieldBasedFrameDecoder解码器提供一些参数的配置,它可以解码任何一种带包长度信息的包。这些包经常出现在client/server模式的网络通信协议中,下面是一些例子,它们可以帮助你去选择哪个配置来使用。

    这段代码是Netty服务启动时的配置

    public class ServerManager {

       private int port;

       public ServerManager(int port) {

           this.port = port;

       }

       //参考的官方例子

       public void run() 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 {

                       //这里就是添加解码器的地方,它有几种不同的构造方法。下面是带全部参数的构造方法,这些参数的作用将在下面的例子中说明,这里没有赋值。ByteOrder可以选择编码是大端还是小端(关于大端或小端的问题,不明白的请自行百度),maxFrameLength表示接收到的包的最大长度。

                       ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(ByteOrder.BIG_ENDIAN, maxFrameLength, lengthFieldOffset, lengthFieldLength, lengthAdjustment, initialBytesToStrip, failFast));

                       ch.pipeline().addLast(new ServerHandler());

               }

           }).option(ChannelOption.SO_BACKLOG, 128) // (5)

           .childOption(ChannelOption.SO_KEEPALIVE, true); // (6)

       // Bind and start to accept incoming connections.

       ChannelFuture f = b.bind(port).sync(); // (7)

       // Wait until the server socket is closed.

       // In this example, this does not happen, but you can do that to

       // gracefully

       // shut down your server.

       f.channel().closeFuture().sync();

       } finally {

           workerGroup.shutdownGracefully();

           bossGroup.shutdownGracefully();

           }

       }

    }

    12个字节的包头记录包长,0 字节偏移,解码后不跳过包头。

    这个例子中,包头表示包长度的值是12,它表示的是包的内容HELLO,WORLD的长度。默认来说,解码器会把这个包头的长度假设为包头后面所有字节的长度,因为这个包可以被下面的这个配置解码。

    lengthFieldOffset   = 0

    lengthFieldLength   = 2

    lengthAdjustment    = 0

    initialBytesToStrip = 0 (= do not strip header)

    BEFORE DECODE (14 bytes)         AFTER DECODE (14 bytes)

    +--------+----------------+      +--------+----------------+

    | Length | Actual Content |----->| Length | Actual Content |

    | 0x000C | "HELLO, WORLD" |      | 0x000C | "HELLO, WORLD" |

    +--------+----------------+      +--------+----------------+

    Before Decode表示的是解码之前接收到的完整的数据包的包结构,After Decode表示的是解码完成后,传给下一层过滤器的包结构。在上面的服务器启动代码中,Before Decode就是客户端传过来的包,而After Decode就是经过这个解码器之后,传到ServerHandlerpublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception 方法中的Object msg的结构,是一个ByteBuf类型。下面所有的例子都是如此。

    public class ServerHandler implements ChannelInboundHandler {

    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {

    // TODO Auto-generated method stub

    }

    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {

    // TODO Auto-generated method stub

    }

    public void channelRegistered(ChannelHandlerContext ctx) throws Exception {

    // TODO Auto-generated method stub

    }

    public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {

    // TODO Auto-generated method stub

    }

    public void channelActive(ChannelHandlerContext ctx) throws Exception {

    // TODO Auto-generated method stub

    }

    public void channelInactive(ChannelHandlerContext ctx) throws Exception {

    // TODO Auto-generated method stub

    }

    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

       ByteBuf byteBuf = (ByteBuf) msg;

       //读取包的包长度

       int len = byteBuf.readInt();

       //剩下的就是包内容了。

       .........

    }

    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {

    // TODO Auto-generated method stub

    }

    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {

    // TODO Auto-generated method stub

    }

    public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {

    // TODO Auto-generated method stub

    }

    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {

    // TODO Auto-generated method stub

    //cause.printStackTrace();

    }

    2) 2个字节的包头记录包长,0 字节偏移,解码后跳过包头。

    我们可以根据ByteBuf.readableBytes(), 方法来获取包的长度值,所以,有时候我们希望解码后,可以跳过表示信息长度的包头。下面这个例子就实现了它,跳过2 个字节的包头信息。

    lengthFieldOffset   = 0

    lengthFieldLength   = 2

    lengthAdjustment    = 0

    initialBytesToStrip = 2 (= the length of the Length field)

    BEFORE DECODE (14 bytes)         AFTER DECODE (12 bytes)

    +--------+----------------+      +----------------+

    | Length | Actual Content |----->| Actual Content |

    | 0x000C | "HELLO, WORLD" |      | "HELLO, WORLD" |

    +--------+----------------+      +----------------+

    这样我们在public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception中得到的msg就只是包含了包内容的信息,而不包括包头的信息了。

    3) 2个字节的包头记录包长,0 字节偏移,包头表示包长度的值代表的整个包的长度,包括包头占的字节数。

    大部分情况下,包长度代表的是包内容的长度,比如之前的例子。但是,在有些协议中,包长度代表的是整个协议传输包的长度,包括包头的长度。下面这个例子中,我们指定一个非0lengthAdjustment值,因为下面这个例子中的包长度总是比包的内容长度多2个字节,所以我们指定lengthAdjustment = -2 作为补偿。

    lengthFieldOffset   =  0

    lengthFieldLength   =  2

    lengthAdjustment    = -2 (= the length of the Length field)

    initialBytesToStrip =  0

    BEFORE DECODE (14 bytes)         AFTER DECODE (14 bytes)

    +--------+----------------+      +--------+----------------+

    | Length | Actual Content |----->| Length | Actual Content |

    | 0x000E | "HELLO, WORLD" |      | 0x000E | "HELLO, WORLD" |

    +--------+----------------+      +--------+----------------+

    解码后,收到的msg信息和1)中是一样的。

    4) 5字节的包头,3字节表示包的长度,这3个字节在包头的末尾。不跳过包头

    这个例子是1)的一个变种。2字节表示整个包的大小(不包括这2个字节数),3字节表示包内容的长度。

    lengthFieldOffset   = 2 (= the length of Header 1)

    lengthFieldLength   = 3

    lengthAdjustment    = 0

    initialBytesToStrip = 0

    BEFORE DECODE (17 bytes)                      AFTER DECODE (17 bytes)

    +----------+----------+----------------+            +----------+----------+----------------+

    | Header 1 |  Length  | Actual Content |----->    | Header 1 |  Length  | Actual Content |

    |  0xCAFE  | 0x00000C   | "HELLO, WORLD" |        |  0xCAFE  | 0x00000C | "HELLO, WORLD" |

    +----------+----------+----------------+               +----------+----------+----------------+

    Header1的值是15Length12

    5) 4 字节的包头,在包的中间有2字节长度表示包内容的长度,解码后跳过第一个包头和包长度的值

    这个例子是上面所有例子的一个综合,在包头信息中,包长度前面有一个预设的包头,包长度后面,有一个额外的包头,预设包头影响lengthFieldOffset的值,额外的包头影响lengthAdjustment的值,这里设置一个非0的值给initialBytesToStrip 表示跳过预设包头和包长度的值。

    lengthFieldOffset   = 1 (= the length of HDR1)

    lengthFieldLength   = 2

    lengthAdjustment    = 1 (= the length of HDR2)

    initialBytesToStrip = 3 (= the length of HDR1 + LEN)

    BEFORE DECODE (16 bytes)                       AFTER DECODE (13 bytes)

    +------+--------+------+----------------+      +------+----------------+

    | HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content |

    | 0xCA | 0x000C | 0xFE | "HELLO, WORLD" |      | 0xFE | "HELLO, WORLD" |

    +------+--------+------+----------------+      +------+----------------+

     

    6) 4 字节的包头,在包的中间有2字节长度表示包内容的长度,解码后跳过第一个包头和包长度的值,包长度的值代表的是整个包的长度。

    这个例子与上面的例子类似,只是这里包长度表示的整个包的长度

    lengthFieldOffset   =  1

    lengthFieldLength   =  2

    lengthAdjustment    = -3 (= the length of HDR1 + LEN, negative)

    initialBytesToStrip = 3

    BEFORE DECODE (16 bytes)                       AFTER DECODE (13 bytes)

    +------+--------+------+----------------+      +------+----------------+

    | HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content |

    | 0xCA | 0x0010 | 0xFE | "HELLO, WORLD" |      | 0xFE | "HELLO, WORLD" |

    +------+--------+------+----------------+      +------+----------------+


     

    通过以上几种例子的配置,我们可以灵活的定义我们的协议格式,通过简单的配置Netty的解码器,就可以完成消息的解码,又方便,又安全。

    转载请注明,来自游戏技术网:http://www.youxijishu.com

    打赏

  • 相关阅读:
    HttpURLConnection中使用代理(Proxy)及其验证(Authentication)
    Java获取随机数的几种方法
    史上最全的java随机数/字符串生成算法(转)
    httpClient中的三种超时设置小结
    几个性能测试工具
    硬件知识
    北风风hadoop课程体系
    IT大数据服务管理高级课程(IT服务,大数据,云计算,智能城市)
    .net framework client profile
    Resharper中注释代码的快捷键
  • 原文地址:https://www.cnblogs.com/wgslucky/p/5962865.html
Copyright © 2020-2023  润新知