第十二章 私有协议栈开发
通信协议可以分为公有协议和私有协议,私有协议比较有灵活性,会在公司内部使用,按需定制。
绝大多数私有协议都基于TCP/IP,利用Netty可以非常方便的进行私有协议的定制和开发。
12.1 私有协议介绍
本质是厂商内部发展和采用的标准,除非授权不然其他厂商无权使用。具有封闭性、垄断性、排他性等特点。
12.2 Netty 协议栈功能设计
Netty协议栈用于内部各模块之间的通信,它基于TCP/IP协议栈,是一个类HTTP 协议的应用层协议栈,相比于传统的标准协议栈,它更加轻巧、灵活和实用。
12.2.1 网络拓扑图
Netty节点并没有服务端和客户端的区分,谁先发起连接谁就作为客户端。取决于业务场景
12.2.2 协议栈功能描述
Netty协议栈承载了业务内部各模块之间的消息交互和服务调用,它的主要功能如下
- 基于Netty的NIO通信框架,提供高性能的异步通信能力;
- 提供消息的编解码框架,可以实现POJO的序列化和反序列化;
- 提供基于IP地址的白名单接入认证机制;
- 链路的有效性校验机制;
- 链路的断连重连机制。
12.2.3 通信模型
具体步骤:
- Netty协议栈客户端发送握手请求消息,携带节点ID等有效身份认证信息
- Netty协议栈服务端对握手请求消息进行合法性校验,包括节点ID有效性校验节点重复登录校验和IP地址合法性校验,校验通过后,返回登录成功的握手应答消息
- 链路建立成功之后,客户端发送业务消息
- 链路成功之后,服务端发送心跳消息;
- 链路建立成功之后,客户端发送心跳消息:
- 链路建立成功之后,服务端发送业务消息
- 服务端退出时,服务端关闭连接,客户端感知到对方关闭连接后,被动关闭客户端连接
建立链接后,双方进行全双工通信。心跳采用Ping-Pong机制,链路处于空闲状态时,客户端主动发送消息Ping给服务端,服务端接收消息后回送Pong给客户端,如果连续发送N条消息都没有回应,客户端认为链路挂死或对方处于异常状态,关闭链接并间隔周期T后发起重连操作。
12.2.4 消息定义
包含两部分,消息头和消息体
12.2.5 Netty 协议支持的字段类型
12.2.6 Netty协议的编解码规范
-
Netty协议中消息(NettyMessage)的编码规范
- crcCode:java.nio.ByteBuffer.putInt(int value),如果采用其他缓冲区实现,必须与其等价;
- length: putInt(int value)
- sessionID: putLong(long value)
- type: put(byte b)
- priority: put(byte b)
- attatchment: 如果attachment长度为0, 表示没有可选附件,长度编码为0,putInt(0)。如果大于0,首先对附件个数进行编码,putInt(attachment.size()),然后对Key进行编码,先编码长度,在转换为byte数组之后编码内容。
- body编码:通过 Jboss Marshalling将其序列化为byte数组,调用ByteBuffer.put(byte[] src)写入ByteBuffer缓冲区
- 确定了整个消息的长度,更新length重新写入ByteBuffer
-
Netty 协议的解码
- crcCode:java.nio.ByteBuffer.getInt(),如果采用其他缓冲区实现,必须与其等价;
- length: getInt()
- sessionID: getLong()
- type: get()
- priority: get()
- attatchment: 首先创建attachment对象,根据getInt()获取长度,如果attachment长度为0, 表示没有可选附件,解码结束。如果大于0,根据长度通过for循环进行解码
- body解码:通过 Jboss marshaller将其解码。
12.2.7 链路的建立
需要调用节点的一方为客户端,被调用方为服务端
链路建立成功之后,客户端发送握手请求消息,消息定义:
- 消息头的type字段值为3
- 可选附件个数为0
- 消息体为空
- 握手消息的长度为22个字节
服务端收到消息后,IP校验,成功后返回握手应答消息,链路建立成功,消息定义:
- 消息头type字段值为4
- 可选附件个数为0
- 消息体为byte类型,返回0表示认证成功,-1表示认证失败
12.2.8 链路的关闭
以下情况需要关闭连接:
- 对方宕机或者重启时,会主动关闭链路,另一方读取到操作系统的通知信号,得知对方REST链路,需要关闭连接,释放自身的句柄等资源。由于采用TCP全双工通信,通信双方都需要关闭连接,释放资源
- 消息读写过程,发生I/O异常,需要主动关闭连接
- 心跳消息读写过程中发生I/O异常,需要主动关闭连接
- 心跳超时,需要主动关闭连接
- 发生编码异常等不可恢复错误,主动关闭
12.2.9 可靠性设计
Netty协议栈,为了保证极端异常场景下Netty协议栈仍能够正常工作或者自动恢复,需要对可靠性进行统一规划和设计
1. 心跳机制
网络空闲时采用心跳机制检测链路的互通性,设计思路:
- 连续周期T没有读写消息时,客户端主动发送Ping消息给服务端
- 如果下一个周期T到来时客户端没有收到服务端回送的消息Pong或者其他业务消息时,心跳失败计数器加1
- 收到Pong或者业务消息,计数器清零,否则如果连续N次没有接收到服务端的Pong,则关闭链路,间隔INTERVAL时间后发起重连操作
- 服务端网络空闲状态持续时间达到T后,心跳失败计数器加一,接受到Ping或者业务消息计数器清零
- 连续N次没有接收到客户端的Ping消息或者业务消息,则关闭链路,释放资源,等待客户端重连
2. 重连机制
- 如果链路中断,等待INTERVAL时间后,由客户端发起重连操作,如果重连失败,间隔周期INTERVAL后再次发起重连,直到重连成功。
- 为了保证服务端能够有充足的时间释放句柄资源,在首次断连时客户端需要等待INTERVAL时间之后再发起重连,而不是失败后就立即重连。
- 为了保证句柄资源能够及时释放,无论什么场景下的重连失败,客户端都必须保证自身的资源被及时释放,包括但不限于SocketChannel、Socket等。
- 重连失败后,需要打印异常堆栈信息,方便后续的问题定位。
3. 重复登录保护
正常状态下,不允许客户端重复登录,防止客户端在异常状态下反复重连导致句柄资源被耗尽。所以服务器在收到客户端连接请求时,首先进行IP校验,校验成功的情况下,在缓存的IP地址表中查看客户端是否已经存在,如果存在则返回错误码-1,同时关闭TCP链路。客户端收到失败消息后,等待INTERVAL时间后再次发起连接,直到认证成功。为了防止由服务端和客户端对链路状态理解不一致导致的客户端无法握手成功的问题,当服务端连续N次心跳超时之后需要主动关闭链路,清空该客户端的地址缓存信息,以保证后续该客户端可以重连成功,防止被重复登录保护机制拒绝掉。
4. 消息缓存重发
链路中断后,在恢复前,缓存在消息队列中的消息不能丢失,链路恢复后重新发送,可以设置消息缓存队列上限,到达上限后不应该再继续添加
12.2.10 安全性设计
为了安全,内部长连接采用基于IP地址的安全认证机制,服务端对握手请求采用IP地址合法性校验。
12.2.11 可扩展性设计
通过Netty消息头中的可选附件attachment字段,业务可以方便地进行自定义扩展
12.3 Netty协议栈开发
12.3.1 数据结构定义
NettyMessage数据结构可参考之前定义,以及添加get,set方法
12.3.2 消息编解码
NettyMessageDecoder和NettyMessageEncoder用于编解码,使用了JBoss的Marshalling的编码和解码
12.3.3 握手和安全认证
客户端
参考之前设计,握手认证的客户端ChannelHandler–LoginAuthReqHandler实现,这里的握手是指TCP三次握手连接成功之后,是在应用层协议中,涉及协议协商、认证、加密等内容。
按照协议规范,服务端需要返回握手应答消息,客户端会对应答消息作出处理,首先判断是否是握手应答消息,如果不是就透传给后面的ChannelHandler进行处理,如果是握手应答消息,则对应答结果进行判断,如果非零说明认证失败,关闭链路重新发起连接
服务端
LoginAuthRespHandler,首先根据客户端的源地址进行重复登录判断。然后通过ChannelHandlerContext拿到Channel,获取客户端的InetSocketAddress,取得发送方的原地址信息,进行白名单校验。最后通过buildResponse构造握手应答消息返回给客户端。
12.3.4 心跳检测机制
客户端
握手成功后,由客户端HeartBeatReqHandler主动发送心跳检测消息,不需要携带消息体
且接收服务端发送的心跳应答消息,HeartBeatTask的实现就是构造函数获取ChannelHandlerContext构造心跳消息并发送。
服务端
心跳应答Handler接收心跳消息之后构造心跳应答消息返回,
心跳超时使用Netty的ReadTimeoutHandler机制。
12.3.5 断线重连
当客户端感知断连事件之后,释放资源,重新发起连接,实现:
监听网络断连事件,如果Channel关闭,则执行后续的重连任务,通过Bootstrap重新发起连接,客户端挂在closeFuture上监听关闭信号,一旦关闭就创建重连定时器,直到重连成功为止
服务端感知断连事件之后,需要清空缓存的登录认证注册信息,保证后续正常重连。
12.3.6 客户端代码
集成上述功能,依次添加解码器,编码器,读超时Handler,握手请求Handler和心跳机制Handler
12.3.7 服务端代码
与客户端对应,解码器,编码器,读超时Handler,握手认证Handler和心跳回应Handler
12.4 运行协议栈
正常情况
异常场景:服务端宕机重启
检验功能:
(1)客户端是否能够正常发起重连:
(2)重连成功之后,不再重连;
(3)断连期间,心跳定时器停止工作,不再发送心跳请求消息;
(4)服务端重启成功之后,允许客户端重新登录;
5)服务端重启成功之后,客户端能够重连和握手成功;
6)重连成功之后,双方的心跳能够正常互发。
(7)性能指标:重连期间,客户端资源得到了正常回收,不会导致句柄等资源泄漏。
客户端宕机重启
服务端要能清除缓存信息,允许客户端重新登录