本章不会直接分析Netty源码,而是通过使用Netty的能力实现一个自定义协议的服务器和客户端。通过这样的实践,可以更深刻地理解Netty的相关代码,同时可以了解,在设计实现自定义协议的过程中需要解决的一些关键问题。
本周章涉及到的代码可以从github上下载: https://github.com/brandonlyg/tinytransport.git。
设计协议
本章要设计的协议是基于TCP的应用层协议。在设计一个协议之前需要先回答以下几个问题:
- 使用场景是什么?
- 这个协议有哪些功能?
- 性能上有什么要求?
- 对网络带宽有什么要求?
- 安全上有哪些要求?
接下来依次回答这些问题:
使用场景
在可信任的内部网络中,不同进程之间高速交换消息。
功能
- 在客户端和服务器进行消息交换。
- 发送消息然后异步接收响应。
- 客户端和服务器之间可以保持长连接。
- 传输大量的数据。
性能
数据包的提取性能接近内存copy。
扩展性
可以通过扩展header字段,进而扩展协议的功能。
带宽
尽量少的冗余数据,占用尽量小的带宽。
安全
由于是在可信任的内网中交互消息,没有特别端安全性要求。
这些问题的答案,就是整个协议的设计要求。下面就按照这些设计要求来设计一套完整的协议,具体类容包括以下两个部分:
- 数据包的格式。
- 客户端和服务器端消息的交互规则。
数据包格式的设计
设计自己的数据包格式之前,我们先来回顾以下LengthFieldBasedFrameDecoder能够处理的数据包格式:
| header | contentLength | conent |
这个类把header的设计留给了子类,现在我们的注意力只需要集中在header字段上即可。下面是header设计:
| begin | version | cmd | contentType | compression | sequenceId | resCode |
整个数据包的格式就是:
| begin | version | cmd | contentType | compression | sequenceId | resCode | contentLength | content |
现在来看一下这个数据包能实现哪些设计要求。
begin
类型: 32位无符号整数(uint32),这字段是一个常量,用来准确第定位到数据包的开始位置,这样就能更准确地分离出数据包,进而保证了“客户端和服务器端进行消息交换”。它的设计还要平衡数据包提取性能和准确性。严格来说,数据包中只能有一个begin,形式化描述如下:
1. 设一个数据包P的长度是L,P(i)表示数据包中任意一个Byte,begin=0XADEF4BC9(这个值可以任意选择,尽量不选择有意义的数字)。
2. 设反序列化一个uint32的算法是ui=deserUint32(i), i>=0 && i < L。
3. 必须满足: deserUint32(0) == begin, 且deserUint32(i) != begin, i > 0 && i < L。
要在(1)(2)两个前提条件下满足第(3)点,需要设计一个转义符EC=0xFF, 对P中除begin以外的部分进行转义,转义算法是:
如果deserUint32(i)==begin或P(i)==EC, 在P(i)前面插入EC。
找到begin的算法是:
如果deserUint32(i)==begin且P(i-1)!=EC。
逆转义算法是:
如果P(i)==EC, P(i+1)==EC或deserUint32(i+1)==begin, 删除P(i)。
以上使用转义符的方案,虽然能够准确地找到begin,但算法复杂度是O(L),显然不能满足“接近内存copy"这个要求。但是如果不使用转义符,就可以达到这个性能要求。如果仔细计算一下begin重复的概率就会发现, 它的重复概率只有1/0x100000000,如果再结合length字段一起检查数据包的正确性,得到错误数据包的概率就会更低。不使用转义符,以极小的出错概率换取性能大幅提升是一笔合适的买卖。
总的来说,begin可以满足两个设计要求: 消息交换,数据包的提取性能接近内存copy。
version
类型:uint8。协议的版本号,这个字段用来满足“扩展性”要求。每个version对应一种不同的header结构,换言之,知道了版本号,就知道怎样解析header。
cmd
类型: uint8。这个字段用来定义不同数据包的功能。可以使用这个字段定义心跳数据包,使用心跳数据包让"服务器和客户端保持长连接"。此外业务层可使用这个字段定义自己需要的数据包。
contentType
类型: uint8。这个字段是content的类型。使用这个字段可以在content数据交给业务层之前,对他进行一下特殊的处理。用户可以定义自己的的消息类型。它可以加"消息交换"的能力。
compression
类型: uint8。 压缩算法。这个字段可以用来表示content使用的压缩算法。通过使用适当的压缩算法,压缩满足"传输大量数据"和"带宽"的要求。
sequenceId
类型: uint32。这个字段是数据包的唯一序列号。只需要保证在一个socket连接建立-断开周期内保证它的唯一性即可。使用这个ID,可以实现“发送消息然后异步

最低0.47元/天 解锁文章
2485

被折叠的 条评论
为什么被折叠?



