需求描述
1、 可自定义协议,协议可扩展、紧凑、高效
2、 可自动管理重连,重连由客户端发起
3、 需进行心跳检测,及时发现连接失效
4、 请求应答模型应当支持同步和异步
5、 连接的分组管理,并且在重连情况下能正确处理连接的分组
6、 请求的发送应当支持四种模型:
(1) 向单个连接发起请求
(2) 向分组内的某个连接发起请求,这个选择策略可定义
(3) 向分组内的所有连接发起请求
(4) 向多个分组发起请求,每个分组的请求遵循
7、 编程模型上尽量做到简单、易用、透明,向上层代码屏蔽网络处理的复杂细节。
8、 高度可配置,包括网络参数、服务层参数等
9、 高度可靠,正确处理网络异常,对内存泄露等隐患有预防措施
10、 可扩展
详细设计
Nio框架概述
Notify 1.7采用了我过去实现的一个nio开源框架,在此基础上做了部分修改和扩展,
这个框架放在了google code,全称为yanf4j,也就是Yet another nio framework for java。
整体的设计与大多数开源nio框架类似,都是Reactor + Handler的模式,
一个大体的框架图如下
一些设计要点:
1、 Yanf4j采用多个Reactor来承载大量连接,以降低单个Reactor的负载,并且如果可能,
会分离出单独一个Reactor专门用于Accept接受连接,以便提高服务器的连接接入速度。
2、 线程模型,主要分为三部分:
(1)实际的IO读取操作
(2)实际的IO写操作
(3)解码出来的消息的派发处理这三个阶段都可以配置。
3、 采用阻塞读,借鉴grizzly的做法,在高负载情况下,read很可能多次返回0,
在此情况下会启用temp selector强制读取一次,这在高速局域网环境内能减少线程切换的开销,提高读取效率。
4、 写入socket缓冲区的操作尽量允许并发,用户线程可直接写入channel,
而不一定只有 IO线程可写从而提高写入效率,这是通过精巧的锁操作来实现的,具体参见源码。
5、 在Reactor实现中注意规避一些JDKnio的bug
6、 可扩展,将整个框架换分为core和nio,以方便以后添加aio支持等。
服务层设计详解
整体架构:
首先给两张服务层的整体架构图:
这张图展示了所有的请求和响应命令以及它们之间的关系。
请求的应答在正常情况下返回的应该是绿线所关联的命令,
在异常情况下(例如超时、线程池繁忙)可能都是返回BooleanAck应答命令。
具体请看协议文档。
服务层的主要模块的静态结构图:
主要模块的角色介绍如下:
RemotingController :
作为通讯层的一个基础接口提供给上层应用,提供包括通讯组件的
启动、停止、同步以及异步发送消息、各种请求调用模型等接口方法。
它有两个子接口分别是对应客户端的RemotingClient和对应服务端的RemotingServer。
RemotingClient:
在c/s结构中提供给客户端使用的通讯组件,具有建立连接、心跳管理、重连管理等功能。
RemotingServer:
提供给需要实现服务器的通讯组件,服务器启动后将返回一个URI 给用户,客户端使用此URI连接服务器。
ReconnectManager:
重连管理器,重连都是由客户端发起。
RemotingContext:
通讯组件的全局上下文,用于保存全局性的状态,包括回调线程池、分组管理器等。
GroupManager:
分组管理器,提供分组到连接的映射关系管理。
RequestProcessor:
请求处理器,RemotingController允许注册RequestCommand对应的处理器,
当请求到来时将自动调用这些处理器处理并应答。
Connection:
连接的抽象,提供发送、关闭、请求调用API等功能。
从服务层的实现角度来讲,Nio框架和服务层的映射关系可见下图:
请求的同步和异步
服务层为了划分清晰,将以invoke为前缀的方法名同一规定为同步调用,
以send为前缀的方法名同一规定为异步调用。
异步调用的回调监听器接口名称统一以CallBackListenner作为后缀。
首先考察下异步调用的实现,各种send方法有多种重载版本,
如果不传入回调监听器,那么就是不关心请求结果,服务层将忽略应答,
如果传入监听器,那么将在应答返回的时候,由服务层负责调用监听器,
监听器的调用是放在监听器接口返回的线程池里面。
看下3个主要的监听器接口:
一次异步回调调用的过程如下:
1、 将请求命令和回调监听器包装成一个CallBack并在即将发送的连接上注册
2、 将请求命令通过连接发送出去
3、 向RemotingContext提交一个CallBack的检测任务CallBackRunner,
阻塞在 CountDownLatch上等待结果是否完全返回或者超时。
4、 对端接收到请求后,返回应答
5、 收到应答,NotifyHandler通过opaque查找到请求的CallBack
6、 NotifyHandler将应答传递给CallBack
7、 Callback将CountDownLatch减一。
8、 CountDownLatch降低为0后,CallBackRunner解除阻塞,将结果传递给回调监听器。
一次同步调用的过程与之类似,只不过在发送的时候将阻塞在调用线程上等待,
不需要CallBackRunner去检测应答是否返回或者超时:
1、 将请求命令和回调监听器包装成一个CallBack并在即将发送的连接上注册
2、 将请求命令通过连接发送出去
3、 阻塞在CallBack的CountDownLatch上等待应答或者超时
4、 对端接收到请求,并返回应答
5、 NotifyHandler接收到nio框架层返回的应答,通过opaque查找到请求的CallBack
6、 Notifyhandler将应答传递给CallBack
7、 CallBack的CountDownLatch减一。
8、 当CountDownLatch降为0之后,请求线程解除阻塞,返回结果给应用层。
异步请求调用的流程图:
同步请求的调用流程:
总结:
1、 同步调用阻塞在请求线程上,这都是通过CountDownLatch实现
2、 阻塞的解除由NotifyHandler调用CallBack来解除
3、 异步调用的阻塞放在了CallBackRunner中,同样通过CountDownLatch来同步结果。
4、 CallBackRunner检测是否超时并查看结果是否完整(对于多分组请求来说),
并负责调用 CallBackListener
5、 CallBackListener的调用是在CallBackListener自带的线程池里面。
重连管理的实现
重连对于客户端来说是必须实现的功能,在连接断开的情况能自动修复连接,
这对于系统的可靠性是至关重要的。
重连功能主要由重连管理器负责实现,重连管理器内部维护一个重连任务的队列,
并且有N个(可配置,默认为1)线程去从这个队列取出重连任务并执行连接请求,
这是一个典型的生产者——消费者模型。
重连唯一需要的注意地方是要及时移除已经被关闭的分组的重连任务,也就是取消重连任务。
重连管理器内部维护了一个集合,保存了被取消重连的分组名称,
在每次启动连接任务前会检查重连任务所代表的分组是否在这个集合内,
如果在的话,将放弃这次重连任务。
分组管理
分组的概念在客户端和服务器上是不同:
1、 对于客户端来说,一个URL代表一个分组,分组内的连接是同构的,指向同一个服务器, 类似连接池的概念。
2、 对于服务器来说,分组代表一个订阅分组,订阅者在不同的机器上,但是属于同一个订阅分组
分组管理由分组管理器负责,默认每个连接都属于一个全局分组,
连接的分组归属管理在客户端和服务器上也是不同的:
1、 客户端发起连接请求,并传入预期的连接个数,这些连接在建立后将加入URI代表的分组。
2、 服务器的连接建立后,Notify客户端负责推送元信息,根据元信息将连接加入对应的分组。
分组管理器(GroupManager) 的实现很简单,恕不赘述。
心跳检测
为了及时发现连接是否可用,通讯层需要做心跳检测,由客户端在连接空闲时发出心跳命令,
服务器在规定时间内返回应答,如果应答没有及时返回,那么就认为该连接失效,
客户端关闭连接并发起重连任务。
对于客户端来说,在NotifyHandler的onSessionIdle方法中发起心跳命令,并提交一个监测线程等待应答。
对于服务器来说,注册一个HeartBeatCommandProcessor用于处理心跳命令,简单地返回应答。
当前的默认策略:
认为连接的空闲超时为10秒(可配置),也就是说连接在10秒内没有进行任何IO读写操作即认为空闲;
心跳应答的等待时间默认为5秒(无法配置),如果5秒没有返回应答即关闭连接启动重连。
可配置性
配置类都在remoting的config包下,BaseConfig 作为基础配置类,
ServerConfig和ClientConfig分别提供给服务器和客户端作为配置类,具体配置信息请查看这些类。