定义
通信协议从广义上区分,可以分为公有协议和私有协议。由于私有协议的灵活性,它往往会在某个公司或者组织内部使用,按需定制,也因为如此,升级起来会非常方便,灵活性好。绝大多数的私有协议传输层都基于TCP/IP,所以利用Netty的NIO TCP协议栈可以非常方便地进行私有协议的定制和开发。
私有协议本质上是厂商内部发展和采用的标准,除非授权,其他厂商一般无权使用该协议。私有协议也称非标准协议,就是未经国际或国家标准化组织采纳或批准,由某个企业自己制订,协议实现细节不愿公开,只在企业自己生产的设备之间使用的协议。私有协议具有封闭性、垄断性、排他性等特点。
跨节点通信
在传统的Java应用中,通常使用以下4种方式进行跨节点通信。
(1)通过RMI进行远程服务调用;
(2)通过Java的Socket+Java序列化的方式进行跨节点调用;
(3)利用一些开源的RPC框架进行远程服务调用,例如Facebook的Thrift,Apache的Avro等;
(4)利用标准的公有协议进行跨节点服务调用,例如HTTP+XML、RESTful+JSON或者WebService。
跨节点的远程服务调用,除了链路层的物理连接外,还需要对请求和响应消息进行编解码。在请求和应答消息本身以外,也需要携带一些其他控制和管理类指令,例如链路建立的握手请求和响应消息、链路检测的心跳消息等。当这些功能组合到一起之后,就会形成私有协议。
协议栈功能设计
协议栈功能描述
Netty协议栈承载了业务内部各模块之间的消息交互和服务调用,它的主要功能如下。
(1)基于Netty的NIO通信框架,提供高性能的异步通信能力;
(2)提供消息的编解码框架,可以实现POJO的序列化和反序列化;
(3)提供基于IP地址的白名单接入认证机制;
(4)链路的有效性校验机制;
(5)链路的断连重连机制。
通信模型
(1)Netty协议栈客户端发送握手请求消息,携带节点ID等有效身份认证信息;
(2)Netty协议栈服务端对握手请求消息进行合法性校验,包括节点ID有效性校验、节点重复登录校验和IP地址合法性校验,校验通过后,返回登录成功的握手应答消息;
(3)链路建立成功之后,客户端发送业务消息;
(4)链路成功之后,服务端发送心跳消息;
(5)链路建立成功之后,客户端发送心跳消息;
(6)链路建立成功之后,服务端发送业务消息;
(7)服务端退出时,服务端关闭连接,客户端感知对方关闭连接后,被动关闭客户端连接。
备注:需要指出的是,Netty协议通信双方链路建立成功之后,双方可以进行全双工通信,无论客户端还是服务端,都可以主动发送请求消息给对方,通信方式可以是TWO WAY或者ONE WAY。双方之间的心跳采用Ping-Pong机制,当链路处于空闲状态时,客户端主动发送Ping消息给服务端,服务端接收到Ping消息后发送应答消息Pong给客户端,如果客户端连续发送N条Ping消息都没有接收到服务端返回的Pong消息,说明链路已经挂死或者对方处于异常状态,客户端主动关闭连接,间隔周期T后发起重连操作,直到重连成功。
消息定义
Netty协议栈消息定义包含两部分:
消息头;消息体。
Netty消息定义表
名称 |
类型 |
长度 |
描述 |
Header |
Header |
变长 |
消息头定义 |
Body |
Object |
变长 |
对于请求消息,它只是方法的参数,对于响应消息,它是返回值 |
Netty协议消息头定义(Header)
名称 |
类型 |
长度 |
描述 |
crcCode |
Int |
32 |
Netty消息校验码 |
Length |
Int |
32 |
整个消息长度 |
sessionID |
Long |
64 |
会话ID |
Type |
Byte |
8 |
0:业务请求消息 1:业务响应消息 2:业务one way消息 3握手请求消息 4握手应答消息 5:心跳请求消息 6:心跳应答消息 |
Priority |
Byte |
8 |
消息优先级:0~255 |
Attachment |
Map<String,Object> |
变长 |
可选字段,由于推展消息头 |
链路的建立
Netty协议栈支持服务端和客服端,对于使用Netty协议栈的应用程序而言,不需要刻意区分到底是客户端还是服务器端,在分布式组网环境中,一个节点可能既是客户端也是服务器端,这个依据具体的用户场景而定。
Netty协议栈对客户端的说明如下:如果A节点需要调用B节点的服务,但是A和B之间还没有建立物理链路,则有调用方主动发起连接,此时,调用方为客户端,被调用方为服务端。
考虑到安全,链路建立需要通过基于Ip地址或者号段的黑白名单安全认证机制,作为样例,本协议使用基于IP地址的安全认知,如果有多个Ip,通过逗号进行分割。在实际的商用项目中,安全认证机制会更加严格,例如通过密钥对用户名和密码进行安全认证。
客户端与服务端链路建立成功之后,由客户端发送握手请求消息,握手请求消息的定义如下
(1) 消息头的type字段值为3;
(2) 可选附件数为0;
(3) 消息头为空
(4) 握手消息的长度为22个字节
服务端接收到客户端的握手请求消息之后,如果IP校验通过,返回握手成功应答消息给客户端,应用层链路建立成功。握手应答消息定义如下:
(1)消息头的type字段值为4
(2)可选附件个数为0;
(3)消息体为byte类型的结果,0:认证成功;-1认证失败;
链路建立成功之后,客户端和服务端就可以互相发送业务消息了。
链路的关闭
由于采用长连接通信,在正常的业务运行期间,双方通过心跳和业务消息维持链路,任何一方都不需要主动关闭连接。
但是,在以下情况下,客户端和服务端需要关闭连接:
(1)当对方宕机或者重启时,会主动关闭链路,另一方读取到操作系统的通知信号得知对方REST链路,需要关闭连接,释放自身的句柄等资源。由于采用TCP全双工通信,通信双方都需要关闭连接,释放资源;
(2)消息读写过程中,发生了I/O异常,需要主动关闭连接;
(3)心跳消息读写过程发生了I/O异常,需要主动关闭连接;
(4)心跳超时,需要主动关闭连接;
(5)发生编码异常等不可恢复错误时,需要主动关闭连接。
可靠性设计
Netty协议栈可能会运行在非常恶劣的网络环境中,网络超时、闪断、对方进程僵死或者处理缓慢等情况都有可能发生。为了保证在这些极端异常场景下Netty协议栈仍能够正常工作或者自动恢复,需要对他的可靠性进行统一规划和设计。
心跳机制
在凌晨等业务低谷时段,如果发生网络闪断、连接被Hang住等问题时,由于没有业务消息,应用程序很难发现。到了白天业务高峰期时,会发生大量的网络通信失败,严重的会导致一段时间进程内无法处理业务消息。为了解决这个问题,在网络空闲时采用心跳机制来检测链路的互通性,一旦发现网络故障,立即关闭链路,主动重连。
当读或者写心跳消息发生I/O异常的时候,说明已经中断,此时需要立即关闭连接,如果是客户端,需要重新发起连接。如果是服务端,需要清空缓存的半包信息,等到客户端重连。
重连机制
如果链路中断,等到INTEVAL时间后,由客户端发起重连操作,如果重连失败,间隔周期INTERVAL后再次发起重连,直到重连成功。
为了保持服务端能够有充足的时间释放句柄资源,在首次断连时客户端需要等待INTERVAL时间之后再发起重连,而不是失败后立即重连。
为了保证句柄资源能够及时释放,无论什么场景下重连失败,客户端必须保证自身的资源被及时释放,包括但不现居SocketChannel、Socket等。
重连失败后,需要打印异常堆栈信息,方便后续的问题定位。
重复登录保护
当客户端握手成功之后,在链路处于正常状态下,不允许客户端重复登录,以防止客户端在异常状态下反复重连导致句柄资源被耗尽。
服务端接收到客户端的握手请求消息之后,首先对IP地址进行合法性校验,如果校验成功,在缓存的地址表中查看客户端是否已经登录,如果登录,则拒绝重复登录,返回错误码-1,同时关闭TCP链路,并在服务端的日志中打印握手失败的原因。
客户端接收到握手失败的应答消息之后,关闭客户端的TCP连接,等待INTERVAL时间之后,再次发起TCP连接,知道认证成功。
为了防止由服务端和客户端对链路状态理解不一致导致的客户端无法握手成功问题,当服务端连续N次心跳超时之后需要主动关闭链路,清空改客户端的地址缓存信息,以保证后续改客户端可以重连成功,防止被重复登录保护机制拒绝掉。
测试
1、 正常情况
2、 客户端宕机,服务器应能清除客户端的缓存信息,允许客户端重新登录
3、 服务器宕机,客户端应能发起重连
4、在LoginAuthRespHandler中进行注释,可以模拟当服务器不处理客户端的请求时,客户端在超时后重新进行登录。