Netty权威指南之私有协议栈开发
相关知识点:
通信协议从广义区分,可以分为公有制协议和私有协议。由于私有协议的灵活性,它往往会在某个公司或组织内部使用,按需定制,也因为如此,升级起来会非常方便,灵活性好。
绝大多数的私有协议传输层都基于TCP/IP,所以利用Netty的NIO TCP协议栈可以非常方便的进行私有协议的定制和开发。
本章学习目标:
1、私有协议介绍
2、基于Netty的私有栈设计
3、私有协议栈开发
第一节:私有协议介绍
私有协议本质上是厂商内部发展和采用的标准,除法授权,其他厂商一般无权使用该协议。私有协议也称非标准协议,就是未经国际或国家标准化组织采纳或批准,由某个企业自己定制,协议实现细节不愿公开,只是企业自己生产的设备之间使用的协议。私有协议具有封闭性、垄断性、排他性等特点。如果网上大量存在私有标准,现行网络和用户一旦使用了它,后进入的厂家设备就必须跟着这种非标准协议,才能够互联互通,否则根本不可能进入现行网络。这样,使用非标准协议的厂家实现了垄断市场的愿望。
尽管私有协议具有垄断的特性,但并非所有的私有协议设计者的初衷是为了垄断。由于现代软件系统复杂性,一个大型软件系统往往会被认为的拆分为多个模块,另外随着移动互联网的兴起,网站的规模越来越大,业务功能越来越多,为了能够支持业务发展,往往需要集群和分布式部署,这样,各个模块之间就用跨节点通信。
在传统的Java 应用中,通常使用以下四种方式进行跨节点通信。
1、通过RMI进行远程服务调用
2、通过Java的Socket+java 序列化方式进行跨节点通信
3、利用以下开源的RPC框架进行远程服务调用,比如Facebook 的thrift ,apache 的avro等。
3、利用标准的公有协议进行跨节点服务调用,比如HTTP+XML,Restful+json或webservice等。
跨节点的远程服务调用,除了链路层的物理连接外,还需要对请求和响应的消息进行编解码。在请求和应答消息本身以外,也需要携带一些其他控制和管理指令,比如:链路建立的握手请求和响应消息、链路检测的心跳消息等。当这些功能组合在一起之后,就回形成私有协议。
事实上,私有协议并没有标准的定义,只是能够用于跨进程、跨主机数据交换的非标准协议,都可以称为私有协议。通常情况下,正规的私有协议都有具体的协议规范文档,类似于《******协议****规范》,但是在实际的项目中,内部使用的私有协议往往是口头约定的规范,由于并不需要对外展现或者外部调用,所以一般不会单独写相关的内部私有协议规范文档。
第二节:基于Netty的私有栈设计
Netty私有栈功能设计
Netty协议栈用于内部各模块之间的通信,它基于TCP/IP协议栈,是一个类似HTTP协议的应用层协议栈,相比与传统的标准协议栈,它更加轻巧、灵活和实用。
网络拓扑图:
如图所示14-1,在分布式组网环境下,每个Netty节点(Netty进程)之间建立长连接,使用Netty协议进行通信。Netty节点并没有服务端和客户端区分,谁首先发起连接,谁就作为客户端,另一个自然就是服务端。一个Netty节点即可以作为服务端连接另外的Netty节点,也可以作为Netty服务端被其他Netty节点连接,这完全取决于使用者业务场景和具体业务组网。
协议栈功能描述
Netty协议栈承载了业务内部各模块之间的消息交互和服务调用,它的主要功能如下。
1、基于Netty的NIO通信框架,提供高性能的异步通信能力;
2、提供消息的编解码框架,可以实现POJO序列化和反序列化
3、提供基于IP地址的白名单接入认证机制;
4、链路的有效性校验机制
5、链路的断连重连机制
通信模式
1、Netty协议栈客户端发送握手请求消息,携带节点ID等有效的身份认证信息;
2、Netty协议栈服务端对握手请求消息进行合法校验,包括节点ID有效性校验、节点重复登入校验和IP地址合法性校验,校验通过后,返回登入成功的握手应答消息;
3、链路建立成功之后,客户端发送业务信息;
4、链路建立成功之后。服务端发送心跳消息;
5、链路建立成功之后,客户端发送心跳消息;
6、链路建立成功之后,服务端发送业务消息;
7、服务端退出时,服务端关闭连接,客户端感知对方关闭连接后,被动关闭客户端连接。
备注:需要指出的是,Netty协议通信双方链路建立成功之后,双方可以进行全双工通信,无论客户端还是服务端。都可以主动发送请求消息给对方,通信方式可以使TWOWAY或者ONE WAY。双方之间的心跳采用Ping-Pong机制,当链路处于空闲状态时,客户端主动发送Ping消息给服务端,服务端接受到Ping消息后发送应答消息Pong给客户端,如果客户端连线发送N条Ping 消息都没有接受到服务端返回的Pong消息,说明链路已经挂死或者对方处于异常状态,客户端主动关闭连接,间隔周期T后发起重新连接,直到连接成功。
消息定义
Netty协议栈消息定义包含两部分:
1、消息头
2、消息体
Netty协议的编解码规范
1、Netty协议的编码
Netty协议NettyMessage的编码规范如下.
1、crcCode:java.nio.ByteBuffer.putInt(int value),如果采用其他缓存区实现,必须与其等价;
2、length:java.nio.ByteBuffer.putInt(int value),如果采用其他缓存区实现,必须与其等价;
3、sessionID:java.nio.ByteBuffer.putLong(long value),如果采用其他缓存区实现,必须与其等价;
4、type:java.nio.ByteBuffer(byte b):如果采用缓存区实现,必须与其等价;
5、priority:java.nio.ByteBuffer.put(byte b):如果采用其他缓存区实现,必须与其等价;
6、attachment:它的编码规则为-------如果attachment长度为0,表示没有可选附件,则长度编码设为0,java.nio.ByteBuffer.putInt(0);如果大于0,说明有附件需要编码,具体的编码规则如下-----------------------
1、首先对附件个数进行编码,java.nio.ByteBuffer.putInt(attachment.size())
2、然后对Key进行编码,先编码长度,再将它转化为byte数组之后编码内容,具体代码ru
想
需要说明的是,将String字符串写入ByteBuffer和通过JBoss Marshalling将Object序列化为字节数组,此处没有详细张开介绍,后续代码开发章节会具体给出实现。
7、body的编码:通过JBoss Marshalling将其序列化为byte数组,然后调用java.nio.ByteBuffer.put(byte[] src)将其写入ByteBuffer缓存区中。
由于整个消息的长度必须等全部字段都编码完成之后才能确认,所以最后需要更新消息头中的length字段,将其重新写入ByteBuffer中。
Netty协议的解码
相对于NettyMeaasge的编码,仍旧以java.nio.ByteBuffer为例,给出Netty协议的解码规范。
1、crcCode:通过java.nio.ByteBuffer.getInt()获取校验码字段,其他缓存区需要与其等价;
2、length:通过java.nio.ByteBUffer.getInt()获取Netty消息的长度,其他缓冲区需要与其等价;
3、sessionID:通过java.nio.ByteBUffer.getLong()获取会话ID,其他缓冲区需要与其等价;
4、type:通过java.nio.ByteBuffer.get()获取消息类型,其他缓冲区需要与其等价;
5、priority:通过java.nio.ByteBuffer.get()获取消息优先级,其他缓冲区需要与其等价;
6、attachment:它的解码规则为------------首先创建一个新的attachment对象,调用java.nio.ByteBUffer.getInt()获取附件长度,如果为0,说明附件为空,解码结束,继续解压消息体;如果非空,则根据长度通过for循环进行解码
后续的代码开发章节中会给出附件解码的具体实现,此处不再详细展开,仅仅给出解码的规则。
7、body:通过JBOss的marshaller 对其进行解码
链路的建立
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协议栈还可以正常工作或自动恢复,需要对它的可靠性进行统一规范和设计。
1、心跳机制
在凌晨等业务低谷时期,如果发生网络闪断、连接被Hang住等网络问题,由于没有业务消息,应用进程很难发现。到了白天业务高潮期,会发生大量的网络通信失败,严重的会导致一段时间进程无法处理业务消息。为 了解决这个问题,在网络空闲时采用网络心跳机制来检测链路的互通性,一旦发现网络故障,立即关闭链路,主动重连。
具体思路设计如下:
1、当网络处于空闲状态持续时间到达T(连线周期T没有读写消息)时,客户端发送Ping 心跳消息给服务端。
2、如果在下一个周期T到来时客户端没有收到对方发送的Pong心跳应答消息或者读取到服务端发送的其他业务消息,则 心跳失败计数器加1;
3、每当客户端接收到服务的业务消息或者Pong应答消息,则心跳失败计数器清0;当连续N次没有接受到服务端的Pong消息或者业务消息,则关闭链路,间隔INTERVAL 时间后发起重连操作;
4、服务端网络空闲状态持续时间达到T后,服务端将心跳计数器加1;只要接受到客户端发送的Ping消息或者其他业务消息,计数器清零。
5、服务端连线N次没有接受到客户端的ping消息或者业务消息,则关闭链路,释放资源,等待客户端重新连接。
通过Ping-Pong双向心跳连接,可以保证无论通信哪一方出现网络故障,都能被及时的检测出来。为了防止由于对方短时间内繁忙而没有及时返回应答造成的误判,只有连线N次心跳检测都失败才认定链路已经损坏,需要关闭链路并重建链路。
当读或写心跳发送I/O异常的时候,说明链路已经中断,此时需要立即关闭链路,如果是客户端,需要重新发起连接。如果是服务端,需要清空缓存的半包信息,等待客户端重连。
重连机制
如果链路中断,等待INTERVAL时间后,由客户端发起重连操作,如果重连失败,间隔周期INTERVAL后再次发起重连,直到重连成功。
为了保证服务端能够有充足的时间是否句柄资源,在首次断链时客户端需要等待INTERVAL时间之后再发起重连,而不是失败之后就立即重连。
为了保证句柄资源能够及时释放,无论什么场景下的重连失败,客户端都必须保证自身的资源被及时释放,包括但不限于SocketChannel、Socket等。
重连失败后,需要打印异常堆栈信息,方便后续的问题定位。
重复登入保护
当客户端握手成功之后,在链路处于正常的情况下,不允许客户端重复登入,以防止客户端在异常状态下反复重连导致句柄资源耗尽。
服务端接受到客户端的握手请求消息之后,首先对IP地址进行合法性检验,如果校验成功,在缓存的地址表中查看客户端是否已经登入,如果已经登入,则拒绝重复登入,返回错误码-1,同时关闭TCP链路,并在服务端的日志打印中打印握手失败的原因。
客户端接收到握手失败的应答消息之后,关闭客户端的TCP连接,等待INTERVAL时间之后,再次发起TCP连接,直到认证成功。
为了防止服务端和客户端对链路状态状态理解不一致导致客户端无法握手成功的问题,当服务器连线N次心跳超时之后需要主动关闭链路,清空该客户端的地址缓存信息,以保证后续客户端可以重连成,防止被重复登入保护机制拒绝掉。
消息缓存重发
无论是客户端还是服务端,当发生链路中断之后,在链路恢复之前,缓存在消息队列中待发送的消息不能丢,等链路恢复之后,重新发生这些消息 ,保证链路中断期间消息不丢失。
考虑到内存溢出的风险,建立消息缓存队列设置上限,当达到上限之后,应该拒绝向该队列添加新的消息。
安全性设计
为了保证整个集群环境的安全,内部长连接采用基于IP地址的安全认证机制,服务端对握手请求消息的IP地址进行合法性校验;如果在白名单之内,则校验通过;否则,拒绝对方连接。
如果将Netty协议栈放到公网中使用,需要采用更加严格的安全认证机制,比如基于秘钥和AES加密的用户名和密码认证机制,也可以采用SSL/TSL安全传输。
作为Demo程序,Netty采用最简单的基于IP地址的白名单安全认证机制。
可拓展性设计
Netty协议需要具备一定的拓展能力,业务可以在消息头中定义域字段,比如:消息流水号、业务自定义消息头等。通过Netty消息头中的可选附件attachment字段,业务可以方便的进行自定义拓展。
Netty协议栈架构需要具备一定的拓展能力,比如统一的消息拦截、接口日志、安全、加密等可以被方便的添加和删除,不需要修改之前的逻辑代码,类似Servlet的Filter 、AOP,但考虑到性能因素,不推荐通过AOP实现功能的拓展。
1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,请转载时务必注明文章作者和来源,不尊重原创的行为我们将追究责任;3.作者投稿可能会经我们编辑修改或补充。