Netty实现通信框架

通信框架功能设计

功能描述:

通信框架承载了业务内部各模块之间的消息交互和服务调用,他的主要功能如下:

基于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对象类型的消息内容。

Netty消息定义表

名称

类型

长度

描述

header

Header

变长

消息头定义

body

Object

变长

消息的内容

消息头定义(Header)

名称

类型

长度

描述

md5

String

变长

消息体摘要,缺省MD5摘要

msgID

Long

64

消息的ID

Type

Byte

8

0:业务请求消息 1:业务响应消息 2:业务one way消息 3:握手请求消息 4:握手应答消息 5:心跳请求消息 6:心跳应答消息

Priority

Byte

8

消息优先级:0~255

Attachment

Map

变长

可选字段,用于扩展消息头

链路的建立:

客户端的说明如下:如果A节点需要调用B节点的服务,但是A和B之间还没有建立物理链路,则有调用方主动发起连接,此时,调用方为客户端,被调用方为服务端。

考虑到安全,链路建立需要通过基于Ip地址或者号段的黑白名单安全认证机制,作为样例,本协议使用基于IP地址的安全认证,如果有多个Ip,通过逗号进行分割。在实际的商用项目中,安全认证机制会更加严格,例如通过密钥对用户名和密码进行安全认证。

客户端与服务端链路建立成功之后,由客户端发送业务握手请求的认证消息,服务端接收到客户端的握手请求消息之后,如果IP校验通过,返回握手成功应答消息给客户端,应用层链路建立成功。握手应答消息中消息体为byte类型的结果,0:认证成功;-1认证失败;服务端关闭连接。

链路建立成功之后,客户端和服务端就可以互相发送业务消息了,在客户端和服务端的消息通信过程中,业务消息体的内容需要通过MD5进行摘要防篡改。

可靠性设计:

1、心跳机制

在凌晨等业务低谷时段,如果发生网络闪断、连接被Hang住等问题时,由于没有业务消息,应用程序很难发现。到了白天业务高峰期时,会发生大量的网络通信失败,严重的会导致一段时间进程内无法处理业务消息。为了解决这个问题,在网络空闲时采用心跳机制来检测链路的互通性,一旦发现网络故障,立即关闭链路,主动重连。

当读或者写心跳消息发生I/O异常的时候,说明已经中断,此时需要立即关闭连接,如果是客户端,需要重新发起连接。如果是服务端,需要清空缓存的半包信息,等到客户端重连。

空闲的连接和超时

检测空闲连接以及超时对于及时释放资源来说是至关重要的。由于这是一项常见的任务,Netty 特地为它提供了几个ChannelHandler 实现。

IdleStateHandler 当连接空闲时间太长时,将会触发一个IdleStateEvent 事件。然后,可以通过在ChannelInboundHandler 中重写userEventTriggered()方法来处理该IdleStateEvent 事件。

ReadTimeoutHandler 如果在指定的时间间隔内没有收到任何的入站数据,则抛出一个ReadTimeoutException 并关闭对应的Channel。可以通过重写你的ChannelHandler 中的exceptionCaught()方法来检测该Read-TimeoutException。

2、重连机制

如果链路中断,等到INTEVAL时间后,由客户端发起重连操作,如果重连失败,间隔周期INTERVAL后再次发起重连,直到重连成功。

为了保持服务端能够有充足的时间释放句柄资源,在首次断连时客户端需要等待INTERVAL时间之后再发起重连,而不是失败后立即重连。

为了保证句柄资源能够及时释放,无论什么场景下重连失败,客户端必须保证自身的资源被及时释放,包括但不现居SocketChannel、Socket等。

重连失败后,可以打印异常堆栈信息,方便后续的问题定位。

3、重复登录保护

当客户端握手成功之后,在链路处于正常状态下,不允许客户端重复登录,以防止客户端在异常状态下反复重连导致句柄资源被耗尽。

服务端接收到客户端的握手请求消息之后,对IP地址进行合法性校验,如果校验成功,在缓存的地址表中查看客户端是否已经登录,如果登录,则拒绝重复登录,同时关闭TCP链路,并在服务端的日志中打印握手失败的原因。

客户端接收到握手失败的应答消息之后,关闭客户端的TCP连接,等待INTERVAL时间之后,再次发起TCP连接,直到认证成功。

实现:

 其中认证申请和认证检查可以在完成后移除。

前期准备

cn.tuling.nettyadv.vo中定义了消息有关的实体类,为了防篡改,消息体需要进行摘要,vo包下提供了EncryptUtils类,可以对消息体进行摘要,目前支持MD5、SHA-1和SHA-256 这三种,缺省为MD5,其中MD5额外提供了加盐摘要。

同时在cn.tuling.nettyadv.kryocodec中定义了有关序列化和反序列化的工具类和Handler,本项目中序列化使用了Kryo序列化框架。

服务端

服务端中NettyServe类是服务端的主入口,内部使用了ServerInit类进行Handler的安装。

最先安装的当然是解决粘包和半包问题的Handler,很自然,这里应该用LengthFieldBasedFrameDecoder进行解码,为了实现方便,我们也没有在消息报文中附带消息的长度,由Netty帮我们在消息报文的最开始增加长度,所以编码器选择了LengthFieldPrepender。

接下来,自然就是序列化和反序列化,直接使用我们在kryocodec下已经准备好的KryoDecoder和KryoEncoder即可。

服务端需要进行登录检查、心跳应答、业务处理,对应着三个handler,于是我们分别安装了LoginAuthRespHandler、HeartBeatRespHandler、ServerBusiHandler。

为了节约网络和服务器资源,如果客户端长久没有发送业务和心跳报文,我们认为客户端出现了问题,需要关闭这个连接,我们引入Netty的ReadTimeoutHandler,当一定周期内(默认值50s,我们设定为15s)没有读取到对方任何消息时,会触发一个ReadTimeouttException,这时我们检测到这个异常,需要主动关闭链路,并清除客户端登录缓存信息,等待客户端重连。

客户端

客户端的主类是NettyClient,并对外提供一个方法send,供业务使用内部使用了ClientInit类进行Handler的安装。

最先安装的当然是解决粘包和半包问题的Handler,同样这里应该用LengthFieldBasedFrameDecoder进行解码,编码器选择了LengthFieldPrepender。

接下来,自然就是序列化和反序列化,依然使用KryoDecoder和KryoEncoder即可。

客户端需要主动发出认证请求和心跳请求。

在TCP三次握手,链路建立后,客户端需要进行应用层的握手认证,才能使用服务,这个功能由LoginAuthReqHandler负责,而这个Handler在认证通过后,其实就没用了,所以在认证通过后,可以将这个LoginAuthReqHandler移除(其实服务端的认证应答LoginAuthRespHandler同样也可以移除)。

对于发出心跳请求有两种实现方式,一是定时发出,本框架的第一个版本就是这种实现方式,但是这种方式其实有浪费的情况,因为如果客户端和服务器正在正常业务通信,其实是没有必要发送心跳的;所以第二种方式就是,当链路写空闲时,为了维持通道,避免服务器关闭链接,发出心跳请求。为了实现这一点,我们首先在整个pipeline的最前面安装一个CheckWriteIdleHandler进行写空闲检测,空闲时间定位8S,取服务器读空闲时间15S的一半,然后再安装一个HearBeatReqHandler,因为写空闲会触发一个FIRST_WRITER_IDLE_STATE_EVENT入站事件,我们在HearBeatReqHandler的userEventTriggered方法中捕捉这个事件,并发出心跳请求报文。

考虑到在我们的实现中并没有双向心跳(即是客户端向服务器发送心跳请求,是服务器也向客户端发送心跳请求),客户端这边同样需要检测服务器是否存活,所以我们客户端这边安装了一个ReadTimeoutHandler,捕捉ReadTimeoutException后提示调用者,并关闭通信链路,触发重连机制。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

30岁老阿姨

支持一下哦!!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值