美文网首页
Netty自定义协议解析原理与应用

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

作者: 王广帅 | 来源:发表于2018-12-27 15:06 被阅读24次

    目前,大家都选择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();
    
           }
    
       }
    
    }
    

    1)2个字节的包头记录包长,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就是经过这个解码器之后,传到ServerHandler的public 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();
    
    }
    
    1. 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就只是包含了包内容的信息,而不包括包头的信息了。

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

    大部分情况下,包长度代表的是包内容的长度,比如之前的例子。但是,在有些协议中,包长度代表的是整个协议传输包的长度,包括包头的长度。下面这个例子中,我们指定一个非0的lengthAdjustment值,因为下面这个例子中的包长度总是比包的内容长度多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)中是一样的。

    1. 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的值是15,Length是12

    1. 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" |
    
    +------+--------+------+----------------+      +------+----------------+
    
    1. 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

    雪奈尔蓝牙键盘,点击这里查看购买

    相关文章

      网友评论

          本文标题:Netty自定义协议解析原理与应用

          本文链接:https://www.haomeiwen.com/subject/guirlqtx.html