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

打賞


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM