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

  本章不会直接分析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,可以实现“发送消息然后异步

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值