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(); } } } |
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自定义协议解析原理与应用的更多相关文章
- [转]netty对http协议解析原理
本文主要介绍netty对http协议解析原理,着重讲解keep-alive,gzip,truncked等机制,详细描述了netty如何实现对http解析的高性能. 1 http协议 1.1 描述 标示 ...
- netty对http协议解析原理解析
本文主要介绍netty对http协议解析原理,着重讲解keep-alive,gzip,truncked等机制,详细描述了netty如何实现对http解析的高性能. 1 http协议 1.1 描述 标示 ...
- netty对http协议解析原理解析(转载)
本文主要介绍netty对http协议解析原理,着重讲解keep-alive,gzip,truncked等机制,详细描述了netty如何实现对http解析的高性能. 1 http协议 1.1 描述 标示 ...
- netty 自定义协议
netty 自定义协议 netty 是什么呢? 相信很多人都被人问过这个问题.如果快速准确的回复这个问题呢?网络编程框架,netty可以让你快速和简单的开发出一个高性能的网络应用.netty是一个网络 ...
- 《精通并发与Netty》学习笔记(14 - 解决TCP粘包拆包(二)Netty自定义协议解决粘包拆包)
一.Netty粘包和拆包解决方案 Netty提供了多个解码器,可以进行分包的操作,分别是: * LineBasedFrameDecoder (换行) LineBasedFrameDecoder是回 ...
- netty自定义协议 心跳 断线重连源码
https://github.com/aa1356889/NettyHeartbeat
- 物联网架构成长之路(35)-利用Netty解析物联网自定义协议
一.前言 前面博客大部分介绍了基于EMQ中间件,通信协议使用的是MQTT,而传输的数据为纯文本数据,采用JSON格式.这种方式,大部分一看就知道是熟悉Web开发.软件开发的人喜欢用的方式.由于我也是做 ...
- 这一次搞懂Spring自定义标签以及注解解析原理
前言 在上一篇文章中分析了Spring是如何解析默认标签的,并封装为BeanDefinition注册到缓存中,这一篇就来看看对于像context这种自定义标签是如何解析的.同时我们常用的注解如:@Se ...
- netty源码解解析(4.0)-20 ChannelHandler: 自己实现一个自定义协议的服务器和客户端
本章不会直接分析Netty源码,而是通过使用Netty的能力实现一个自定义协议的服务器和客户端.通过这样的实践,可以更深刻地理解Netty的相关代码,同时可以了解,在设计实现自定义协议的过程中需要解决 ...
随机推荐
- INCA二次开发-MIP
1.INCA介绍 INCA是常用的汽车ECU测试和标定的,广泛应用于动力总成等领域.INCA提供了丰富的接口,供用户自动化.定制化.本公众号通过几篇文章,介绍下一些二次开发的方法,本篇介绍MIP. 2 ...
- 视频压缩:I帧、P帧、B帧
/*************************************************************************************************** ...
- Java核心卷笔记(一)
第三章Java基程序设计结构 1.注释 三种注释方式: // 注释单行 /* 内容 */ 注释单行 /** * 内容 */ 2. java 数据类型 Java数据类型可分为两种:基本数据类型和引用数据 ...
- ubuntu 命令整合2
通配符 * 匹配任意多个字符 ?匹配一个任意字符 示例:ls *.txt rm -rf *.txt 文本编辑器 vi.vim 格式:vi 文件名 编辑 vi的三种工作模式 正常模式(启动进入的模式) ...
- 第二章之S5PV210在BL1中点亮LED灯
1,u-boot中第一个入口在./arch/arm/cpu/armv7/start.S 翻到153行:如下图 前面都是进行一些基本设置,不用管. cpu_init_cp15设置协处理器, cpu_in ...
- MySQL varchar类型数据转tinyint类型
在mysql数据库中性别字段以前存的是'男'和'女',使用varchar类型存储的,但是在我mongo库中这个字段使用的是'1'和'0'存储的,在两个库之间的数据转换就很不方便,于是想要统一存储类型, ...
- Web3与智能合约交互实战
写在前面 在最初学习以太坊的时候,很多人都是自己创建以太坊节点后,使用geth与之交互.这种使用命令行交互的方法虽然让很多程序员感到兴奋(黑客帝国的既视感?),但不可能指望普通用户通过命令行使用Dap ...
- 理解矩阵与线性代数<转>
作者:张帅链接:https://www.zhihu.com/question/21082351/answer/34361293来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出 ...
- (细节)My SQL中主键为0和主键自排约束的关系
开始不设置主键 表的设计如下: 如果id的位置有好几个0的话:设置主键并且自动排序时,0会从1开始递增: Insert 进去 id = 0的数据,数据会从实际的行数开始增加,和从0变化不一样: 现在主 ...
- 解决VMware虚拟机报错“无法连接MKS:套接字连接尝试次数太多,正在放弃”
1.错误描述 在VMware中打开虚拟机时报错: "无法连接MKS:套接字连接尝试次数太多,正在放弃" 物理机操作系统: Windows 7 虚拟机操作系统: Kali Linux ...