目前,大家都選擇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(); } |
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 字節偏移,包頭表示包長度的值代表的整個包的長度,包括包頭占的字節數。
大部分情況下,包長度代表的是包內容的長度,比如之前的例子。但是,在有些協議中,包長度代表的是整個協議傳輸包的長度,包括包頭的長度。下面這個例子中,我們指定一個非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)中是一樣的。
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的值是15,Length是12
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
打賞