通信框架功能设计
功能描述
通信框架承载了业务内部各模块之间的消息交互和服务调用,它的主要功能如下:
基于 Netty 的 NIO 通信框架,提供高性能的异步通信能力;
提供消息的编解码框架,可以实现 POJO 的序列化和反序列化;
消息内容的防篡改机制
提供基于 IP 地址的白名单接入认证机制;
链路的有效性校验机制;
链路的断连重连机制
通信模型
(1)客户端发送应用握手请求消息,携带节点 ID 等有效身份认证信息;
(2)服务端对应用握手请求消息进行合法性校验,包括节点 ID 有效性校验、节点重复登录校验和 IP 地址合法性校验,校验通过后,返回登录成功的应用握手应答消息;
(3)链路建立成功之后,客户端发送业务消息;
(4)链路成功之后,服务端发送心跳消息;
(5)链路建立成功之后,客户端发送心跳消息;
(6)链路建立成功之后,服务端发送业务消息;
(7)服务端退出时,服务端关闭连接,客户端感知对方关闭连接后,被动关闭客户端连接。
备注:需要指出的是,协议通信双方链路建立成功之后,双方可以进行全双工通信,无论客户端还是服务端,都可以主动发送请求消息给对方,通信方式可以是 TWO WAY 或者ONE WAY。双方之间的心跳采用 Ping-Pong 机制,当链路处于空闲状态时,客户端主动发送Ping 消息给服务端,服务端接收到 Ping 消息后发送应答消息 Pong 给客户端,如果客户端连续发送 N 条 Ping 消息都没有接收到服务端返回的 Pong 消息,说明链路已经挂死或者对方处于异常状态,客户端主动关闭连接,间隔周期 T 后发起重连操作,直到重连成功。
消息定义
消息定义包含两部分:消息头;消息体。
- 在消息的定义上,因为是同步处理模式,不考虑应答消息需要填入请求消息 ID,所以消息头中只有一个消息的 ID。如果要支持异步模式,则请求消息头和应答消息头最好分开设计,应答消息头中除了包括本消息的 ID 外,还应该包括请求消息 ID,以方便请求消息的发送方根据请求消息 ID 做对应的业务处理。
- 消息体则支持 Java 对象类型的消息内容。
链路的建立
客户端的说明如下:如果 A 节点需要调用 B 节点的服务,但是 A 和 B 之间还没有建立物理链路,则有调用方主动发起连接,此时,调用方为客户端,被调用方为服务端。考虑到安全,链路建立需要通过基于 Ip 地址或者号段的黑白名单安全认证机制,作为
样例,本协议使用基于 IP 地址的安全认证,如果有多个 Ip,通过逗号进行分割。在实际的商用项目中,安全认证机制会更加严格,例如通过密钥对用户名和密码进行安全认证。
客户端与服务端链路建立成功之后,由客户端发送业务握手请求的认证消息,服务端接收到客户端的握手请求消息之后,如果 IP 校验通过,返回握手成功应答消息给客户端,应用层链路建立成功。握手应答消息中消息体为 byte 类型的结果,0:认证成功;-1 认证失败;服务端关闭连接。
链路建立成功之后,客户端和服务端就可以互相发送业务消息了,在客户端和服务端的消息通信过程中,业务消息体的内容需要通过 MD5 进行摘要防篡改。
可靠性设计
心跳机制
在凌晨等业务低谷时段,如果发生网络闪断、连接被 Hang 住等问题时,由于没有业务消息,应用程序很难发现。到了白天业务高峰期时,会发生大量的网络通信失败,严重的会导致一段时间进程内无法处理业务消息。为了解决这个问题,在网络空闲时采用心跳机制来检测链路的互通性,一旦发现网络故障,立即关闭链路,主动重连。
当读或者写心跳消息发生 I/O 异常的时候,说明已经中断,此时需要立即关闭连接,如果是客户端,需要重新发起连接。如果是服务端,需要清空缓存的半包信息,等到客户端重连。
空闲的连接和超时
检测空闲连接以及超时对于及时释放资源来说是至关重要的。由于这是一项常见的任务,Netty 特地为它提供了几个 ChannelHandler 实现。IdleStateHandler 当连接空闲时间太长时,将会触发一个 IdleStateEvent 事件。然后,可以通过在 ChannelInboundHandler 中重写 userEventTriggered()方法来处理该 IdleStateEvent 事件。
ReadTimeoutHandler 如果在指定的时间间隔内没有收到任何的入站数据,则抛出一个ReadTimeoutException 并关闭对应的 Channel。可以通过重写你的 ChannelHandler 中的exceptionCaught()方法来检测该 Read-TimeoutException。
重连机制
如果链路中断,等到 INTEVAL 时间后,由客户端发起重连操作,如果重连失败,间隔周期 INTERVAL 后再次发起重连,直到重连成功。
为了保持服务端能够有充足的时间释放句柄资源,在首次断连时客户端需要等待INTERVAL 时间之后再发起重连,而不是失败后立即重连。
为了保证句柄资源能够及时释放,无论什么场景下重连失败,客户端必须保证自身的资源被及时释放,包括但不现居 SocketChannel、Socket 等。
重连失败后,可以打印异常堆栈信息,方便后续的问题定位。
重复登录保护
当客户端握手成功之后,在链路处于正常状态下,不允许客户端重复登录,以防止客户端在异常状态下反复重连导致句柄资源被耗尽。
服务端接收到客户端的握手请求消息之后,对 IP 地址进行合法性校验,如果校验成功,在缓存的地址表中查看客户端是否已经登录,如果登录,则拒绝重复登录,同时关闭 TCP链路,并在服务端的日志中打印握手失败的原因。
客户端接收到握手失败的应答消息之后,关闭客户端的 TCP 连接,等待 INTERVAL 时间之后,再次发起 TCP 连接,直到认证成功。