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

打赏

Netty自定义协议解析原理与应用的更多相关文章

  1. [转]netty对http协议解析原理

    本文主要介绍netty对http协议解析原理,着重讲解keep-alive,gzip,truncked等机制,详细描述了netty如何实现对http解析的高性能. 1 http协议 1.1 描述 标示 ...

  2. netty对http协议解析原理解析

    本文主要介绍netty对http协议解析原理,着重讲解keep-alive,gzip,truncked等机制,详细描述了netty如何实现对http解析的高性能. 1 http协议 1.1 描述 标示 ...

  3. netty对http协议解析原理解析(转载)

    本文主要介绍netty对http协议解析原理,着重讲解keep-alive,gzip,truncked等机制,详细描述了netty如何实现对http解析的高性能. 1 http协议 1.1 描述 标示 ...

  4. netty 自定义协议

    netty 自定义协议 netty 是什么呢? 相信很多人都被人问过这个问题.如果快速准确的回复这个问题呢?网络编程框架,netty可以让你快速和简单的开发出一个高性能的网络应用.netty是一个网络 ...

  5. 《精通并发与Netty》学习笔记(14 - 解决TCP粘包拆包(二)Netty自定义协议解决粘包拆包)

    一.Netty粘包和拆包解决方案 Netty提供了多个解码器,可以进行分包的操作,分别是: * LineBasedFrameDecoder (换行)   LineBasedFrameDecoder是回 ...

  6. netty自定义协议 心跳 断线重连源码

    https://github.com/aa1356889/NettyHeartbeat

  7. 物联网架构成长之路(35)-利用Netty解析物联网自定义协议

    一.前言 前面博客大部分介绍了基于EMQ中间件,通信协议使用的是MQTT,而传输的数据为纯文本数据,采用JSON格式.这种方式,大部分一看就知道是熟悉Web开发.软件开发的人喜欢用的方式.由于我也是做 ...

  8. 这一次搞懂Spring自定义标签以及注解解析原理

    前言 在上一篇文章中分析了Spring是如何解析默认标签的,并封装为BeanDefinition注册到缓存中,这一篇就来看看对于像context这种自定义标签是如何解析的.同时我们常用的注解如:@Se ...

  9. netty源码解解析(4.0)-20 ChannelHandler: 自己实现一个自定义协议的服务器和客户端

    本章不会直接分析Netty源码,而是通过使用Netty的能力实现一个自定义协议的服务器和客户端.通过这样的实践,可以更深刻地理解Netty的相关代码,同时可以了解,在设计实现自定义协议的过程中需要解决 ...

随机推荐

  1. 多重影分身——C#中多线程的使用二(争抢共享资源)

    只要服务器承受得了,我们可以开任意个线程同时工作以提高效率,然而 两个线程争抢资源可能导致数据混乱. 例如: public class MyFood { public static int Last ...

  2. 大数据技术生态圈形象比喻(Hadoop、Hive、Spark 关系)

    [摘要] 知乎上一篇很不错的科普文章,介绍大数据技术生态圈(Hadoop.Hive.Spark )的关系. 链接地址:https://www.zhihu.com/question/27974418 [ ...

  3. SignUtil

    最近接的新项目 加密比较多  我就记录下. SignUtil是jnewsdk-mer-1.0.0.jar  com.jnewsdk.util中的一个工具类.由于我没有百度到对应的信息.所以我只能看源码 ...

  4. Django和Angular.js模板标签冲突的解决方式

    参考文章:http://yanhua365.lofter.com/post/b417f_1f0361 http://stackoverflow.com/questions/8302928/angula ...

  5. RSAC 2018:人工智能成为驱动网络安全的新 “引擎”

    作为全球顶级的权威安全会议,RSA已成为快速了解世界安全趋势的风向标,更是影响安全产业转型与持续发展的重要平台.不同于往年人工智能(AI)在安全领域更多的是一种理论探讨,今年看到的是大量人工智能在安全 ...

  6. python笔记:#011#循环

    循环 目标 程序的三大流程 while 循环基本使用 break 和 continue while 循环嵌套 01. 程序的三大流程 在程序开发中,一共有三种流程方式: 顺序 -- 从上向下,顺序执行 ...

  7. 16.git命令汇总

  8. Reactor三种线程模型与Netty线程模型

    文中所讲基本都是以非阻塞IO.异步IO为基础.对于阻塞式IO,下面的编程模型几乎都不适用 Reactor三种线程模型 单线程模型 单个线程以非阻塞IO或事件IO处理所有IO事件,包括连接.读.写.异常 ...

  9. Netflix性能监控工具Vector

    简介: Vector是Netflix开源的主机级性能监控框架,向每位工程师的浏览器提供精心挑选的高分辨率系统和应用程序指标. 登录到系统并从shell运行大量命令是一种选择,但是通常涉及的复杂性可能成 ...

  10. (Lesson2)根据类名称和属性获得元素-JavaScript面向对象

    描述:在编写选择器的时候遇到的一根问题,我需要实现Jquery的选择器功能,第一个根据ID获取Element非常简单,第二个根据类(class)去获取Element集合,这个相对复杂,而根据name和 ...