基于Netty的通讯架构设计与实现 |
The design and implementation of communication architecture based on Netty |
摘要
本文将以Netty框架为基础搭建一套通讯架构体系,在该通讯架构体系中集成Socket客户端和服务端、HTTP、HTTPS客户端和服务端、webservice客户端和服务端,该通讯体系将能够满足企业项目应用中对不同通讯协议的需求,同时实现了不同应用服务间的高性能通讯。
基于Netty开发的服务端通讯架构,主要由接收器、处理器、配置中心等模块组成。采用工厂模式,实现对Socket、Http、Webservice等多种服务端对象集合进行管理,通过通讯上下文初始化、通讯渠道初始化、线程池初始化、编解码处理器配置等操作,实现服务端通讯功能。
基于Netty开发的客户端通讯架构,主要由客户端工厂、发送器、处理器、配置中心等模块组成,实现对Socket、Http、Webservice等多种客户端对象集合进行管理,实现所有客户端配置服务加载和启动,通过通讯上下文初始化、线程池初始化、客户端发送器配置等操作,实现客户端通讯功能。
ABSTRACT
The server side communication architecture based on Netty is mainly composed of receiver, processor, configuration center and other modules. Using the factory mode, we can manage a variety of server end objects such as Socket, Http, Webservice and so on. Through the initialization of communication context, the initialization of communication channels, the initialization of the thread pool, the configuration of the codec processor and so on, the communication function of the server is realized.
The client communication architecture based on Netty is mainly composed of client factory, transmitter, processor, configuration center and other modules. It can manage a variety of client objects such as Socket, Http, Webservice and so on. All client configuration services are loaded and started, and the communication context initializes and thread pools are initialized. Initialization, client transmitter configuration and other operations to achieve client communication function.
目录
摘要... 3
ABSTRACT. 4
第一章 绪论... 6
1.1 研究背景... 7
1.2 研究现状... 7
1.3 研究内容... 8
1.4 论文的组织结构... 9
第二章 相关技术介绍... 10
2.1 JAVA BIO/NIO及问题... 10
2.2 RPC通讯... 11
2.3 Netty框架简介... 11
2.3.1 Reactor线程模型... 11
2.3.2 Netty线程模型... 13
2.4 WEBSERVICE服务实现... 14
2.4.1 WEBSERVICE服务调用原理... 14
2.5 Soap报文... 15
2.6 SOP报文规范... 15
第三章 通讯模块组件... 18
3.1 通讯模块总体架构... 18
3.1.1 服务端架构... 18
3.1.2 客户端架构... 19
3.2 SOCKET协议及相关实现... 20
3.2.1 SOCKET服务端... 20
3.2.2 SOCKET客户端... 27
3.2.3 SOP报文处理... 30
3.3 HTTP协议及相关实现... 33
3.3.1 HTTP服务端... 33
3.3.2 HTTP客户端... 39
3.3.3 HTTPS协议实现... 40
1.1.1 https证书制作... 44
3.4 WEBSERVICE协议及相关实现... 44
3.4.1 WEBSERVICE服务端... 45
3.4.2 WEBSERVICE客户端... 47
3.4.3 SPEED4J支持我行ESB对接开发... 48
3.4.4 SOAP报文... 52
4.5 异常情况处理... 55
4.5.1 异常信息设计... 55
4.5.2 异常情况处理... 56
第四章 性能结果分析... 56
5.1 推广情况... 57
5.2 性能测试样例... 57
4.2.1 测试背景... 57
4.2.2 性能需求指标... 57
4.2.3 生产环境拓扑图... 58
4.2.4 负载测试... 58
4.2.5 容量测试... 60
4.2.6 健壮性测试... 63
4.2.7 稳定性测试... 68
4.2.8 测试结论... 70
第五章 总结和展望... 72
6.1 总结... 72
6.2 展望... 72
参考文献... 72
致谢... 72
第一章 绪论
在网络传输方式问题上,传统的RPC框架或者基于RMI等方式的远程服务(过程)调用采用了同步阻塞IO,当客户端的并发压力或者网络时延增大之后,同步阻塞IO会由于频繁的wait导致IO线程经常性的阻塞,由于线程无法高效的工作,IO处理能力自然下降。
在序列化方式问题上,Java序列化存在如下几个典型问题:
1) Java序列化机制是Java内部的一种对象编解码技术,无法跨语言使用;例如对于异构系统之间的对接,Java序列化后的码流需要能够通过其它语言反序列化成原始对象(副本),目前很难支持;
2) 相比于其它开源的序列化框架,Java序列化后的码流太大,无论是网络传输还是持久化到磁盘,都会导致额外的资源占用;
3) 序列化性能差(CPU资源占用高)。
线程模型问题:由于采用同步阻塞IO,这会导致每个TCP连接都占用1个线程,由于线程资源是JVM虚拟机非常宝贵的资源,当IO读写阻塞导致线程无法及时释放时,会导致系统性能急剧下降,严重的甚至会导致虚拟机无法创建新的线程。
针对以上情况,我们需要研究解决传输、协议和线程问题,即,用什么样的通道将数据发送给对方,BIO、NIO或者AIO,IO模型在很大程度上决定了框架的性能;采用什么样的通信协议是HTTP或者内部私有协议,协议的选择不同,性能模型也不同;数据报如何读取,读取之后的编解码在哪个线程进行,编解码后的消息如何派发等问题,Reactor线程模型的不同,对性能的影响非常大。
RPC(Remote Procedure Call Protocol)远程过程调用协议,它是一种通过网络,从远程计算机程序上请求服务,RPC框架针对网络协议、网络I/O模型的封装是透明的,对于调用的客户端而言,它就认为自己在调用本地的一个对象。
目前典型的RPC实现框架有:Thrift(facebook开源)、Dubbo(alibaba开源)等。衡量一个RPC框架性能的好坏与否,RPC的网络I/O模型的选择及RPC框架的传输协议非常重要,目前RPC的网络I/O模型包括:支持阻塞式同步IO、非阻塞式同步IO、多路复用IO模型和异步IO模型,RPC开源实现框架的传输协议包括:TCP协议、HTTP协议及UDP协议。目前大多数RPC开源实现框架采用基于TCP和HTTP协议开发实现的。
实际应用为了提高单个节点的通信吞吐量,提高通信性能,我们一般首选的是NIO框架(No-block IO)。Java的NIO开发一个后端的TCP/HTTP服务器,附带考虑TCP粘包、网络通信异常、消息链接处理等网络通信细节,掌握起来学习成本较高,要求相当的技术功底和技术积累,因而采用业界主流的NIO框架进行服务器后端开发,是一个明智的选择。
主流的NIO框架主要有Netty、Mina,它们主要都是基于TCP通信,非阻塞的IO、灵活的IO线程池而设计的,可以应对高并发请求。
Mina(Multipurpose Infrastructure for Network Applications) 是 Apache 组织一个较新的项目,它为开发高性能和高可用性的网络应用程序提供了非常便利的框架。
Netty是一款异步的事件驱动的网络应用框架和工具,是一个NIO客户端/服务器框架,支持快速、简单地开发网络应用,如协议服务器和客户端,它极大简化了网络编程。
BIO服务端通信模型,该架构最大的问题就是不具备弹性伸缩能力,当并发访问量增加后,服务端的线程个数和并发访问数成线性正比,由于线程是JAVA虚拟机非常宝贵的系统资源,当线程数膨胀之后,系统的性能急剧下降,随着并发量的继续增加,可能会发生句柄溢出、线程堆栈溢出等问题,并导致服务器最终宕机。
与Socket类和ServerSocket类相对应,NIO也提供了SocketChannel和ServerSocketChannel两种不同的套接字通道实现。这两种新增的通道都支持阻塞和非阻塞两种模式。阻塞模式使用非常简单,但是性能和可靠性都不好,非阻塞模式正好相反。开发人员一般可以根据自己的需要来选择合适的模式,一般来说,低负载、低并发的应用程序可以选择同步阻塞IO以降低编程复杂度。但是对于高负载、高并发的网络应用,需要使用NIO的非阻塞模式进行开发。
Netty是一个高性能、异步事件驱动的NIO框架,它提供了对TCP、UDP和文件传输的支持,作为一个异步NIO框架,Netty的所有IO操作都是异步非阻塞的,通过Future-Listener机制,用户可以方便的主动获取或者通过通知机制获得IO操作结果。
作为当前最流行的NIO框架,Netty在互联网领域、大数据分布式计算领域、游戏行业、通信行业等获得了广泛的应用,一些业界著名的开源组件也基于Netty的NIO框架构建。
本文将以Netty框架为基础搭建一套通讯架构体系,在该通讯架构体系中会集成Socket客户端和服务端、HTTP及HTTPS客户端和服务端、webservice客户端和服务端,该通讯体系将能够满足企业应用中对不同通讯协议的需求,同时实现了各个应用服务间的高性能通讯。
全文共分为七章,主要从研究背景、研究内容、相关技术、系统需求分析、功能设计、系统架构设计与实现、服务部署与性能分析几个方面对论文进行组织。
第一章介绍了课题的研究背景和研究内容,同时也对和本课题相关的国内外现状进行了分析和介绍,最后对论文的整体结构进行了阐述。
第二章对通讯构件所涉及到的相关技术进行了介绍,包括BIO/NIO相关问题、Reactor线程模型、SOP报文、Soap报文等。
第三章对基于Netty的通讯架构设计与实现进行了总体介绍,首先对通讯总体架构设计进行了分析,接着对Netty框架集成SOCKET通讯协议及其相关实现进行了分析,然后对Netty框架集成HTTP和WEBSERVICE通讯协议及其相关实现进行了分析,最后通讯模块异常情况处理进行了分析。
第四章主要对Docker环境中部署的微服务架构系统性能进行测试分析,设计了一套性能测试架构,实现系统负载、容量、健壮性和稳定性测试分析。
第五章为总结和展望。在这一章中总结了基于Netty通讯架构的设计与实现,同时对高性能通讯框架发展方向给出了期望。
第二章 相关技术介绍
BIO服务端通信模型,通常由一个独立的Acceptor线程负责监听客户端的连接,接收到客户端连接之后会为客户端创建一个新的线程来处理具体的业务逻辑,处理完成之后,返回应答消息给客户端,然后销毁线程,BIO通信模型是典型的一请求一应答模型。
伪异步IO模型采用线程池和任务队列来实现。当有客户端接入时,客户端的SOCKET被封装成为一个任务(该任务实现了Runnable接口),然后将该任务投递到后端的线程池中进行处理,JDK的线程池会维护一个队列和多个线程,实现对队列中的任务进行处理。线程池可以设置消息队列的大小和最大线程数,因而资源占用是可以控制的。
NIO异步非阻塞通讯采用IO多路复用器技术,将多个IO的阻塞复用到同一个select的阻塞上,仅需要一个线程负责多路复用器Selector轮询操作,就可以接入成千上万的客户端。
与传统的多线程/多进程模型比,I/O多路复用技术的优势是系统开销小,不需要创建新的额外进程或者线程,同时也降低了系统的维护工作量,节省了系统资源。
常用的Reactor线程模型有三种,分别为:Reactor单线程模型、Reactor多线程模型、主从Reactor多线程模型。
Reactor单线程模型,指的是所有的IO操作都在同一个NIO线程上面完成。
Reactor单线程模型示意图如下所示:
正在上传…重新上传取消
图2-3 Reactor单线程模型
Reactor模式使用的是异步非阻塞IO,所有的IO操作都不会导致阻塞,一个NIO线程通过Acceptor接收客户端的TCP连接请求消息,链路建立成功之后,通过Dispatch将对应的ByteBuffer派发到指定的Handler上进行消息解码,用户Handler可以通过NIO线程将消息发送给客户端。
Rector多线程模型与单线程模型最大的区别就是有一组NIO线程处理IO操作。
正在上传…重新上传取消
图2-4 Reactor多线程模型
Reactor多线程模型的特点:
1) 有专门一个NIO线程-Acceptor线程用于监听服务端,接收客户端的TCP连接请求;
2) 网络IO操作-读、写等由一个NIO线程池负责,线程池可以采用标准的JDK线程池实现,它包含一个任务队列和N个可用的线程,由这些NIO线程负责消息的读取、解码、编码和发送;
3) 1个NIO线程可以同时处理N条链路,但是1个链路只对应1个NIO线程,防止发生并发操作问题。
主从Reactor线程模型的服务端用于接收客户端连接的是一个独立的NIO线程池。Acceptor接收到客户端TCP连接请求处理完成后,将新创建的SocketChannel注册到IO线程池(sub reactor线程池)的某个IO线程上,由它负责SocketChannel的读写和编解码工作。Acceptor线程池仅仅只用于客户端的登陆、握手和安全认证,一旦链路建立成功,便将链路注册到后端subReactor线程池的IO线程上,由IO线程负责后续的IO操作。
它的线程模型如下图所示:
图2-5 Reactor主从多线程模型
利用主从NIO线程模型,可以解决1个服务端监听线程无法有效处理所有客户端连接的性能不足问题。因此,在Netty的官方demo中,推荐使用该线程模型。
Netty的线程模型并非固定不变,通过在启动辅助类中创建不同的EventLoopGroup实例并通过适当的参数配置,就可以支持Reactor单线程模型、多线程模型、主从Reactor线程模型。
Netty架构按照Reactor模式设计和实现,Netty的IO线程NioEventLoop聚合了多路复用器Selector,可以同时并发处理成百上千个客户端Channel连接,并且读写操作都是非阻塞的,避免了频繁IO阻塞导致的线程挂起。
图2-6 Netty线程模型
为了尽可能提升性能,Netty采用了串行无锁化设计,在IO线程内部进行串行操作,避免多线程竞争导致的性能下降。通过调整NIO线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一个队列-多个工作线程模型性能更优,而且避免了多线程操作导致的锁的竞争,从性能角度看是最优的。
Netty的“零拷贝”主要体现在如下三个方面:
对于缓冲区Buffer,特别是对于堆外直接内存的分配和回收,是一件耗时的操作。为了尽量重用缓冲区,Netty提供了基于内存池的缓冲区重用机制。Netty提供了多种内存管理策略,通过在启动辅助类中配置相关参数,实现差异化的定制。
实现一个完整的Web服务包括以下步骤:
◆ Web服务提供者设计实现Web服务,并将调试正确后的Web服务通过Web服务中介者发布,并在UDDI注册中心注册; (发布)
◆ Web服务请求者向Web服务中介者请求特定的服务,中介者根据请求查询UDDI注册中心,为请求者寻找满足请求的服务; (发现)
◆ Web服务中介者向Web服务请求者返回满足条件的Web服务描述信息,该描述信息用WSDL写成,各种支持Web服务的机器都能阅读;(发现)
◆ 利用从Web服务中介者返回的描述信息生成相应的SOAP消息,发送给Web服务提供者,以实现Web服务的调用;(绑定)
◆ Web服务提供者按SOAP消息执行相应的Web服务,并将服务结果返回给Web服务请求者。(绑定)
SOAP简单对象访问协议是“Simple Object Access Protocol”的简称,。SOAP可以运行在任何其他传输协议上,在传输层之间的头是不同的,但XML有效负载保持相同。Web Service通常会使用HTTP或HTTPS与SOAP绑定。
SOAP在安全方面是通过使用XML-Security和XML-Signature两个规范组成了WS-Security来实现安全控制的。报文编码推荐采用UTF-8或GBK,编码需要在XML头的encoding中定义。
如图3-2 所示为SOP报文通讯架构,SOP报文的通讯方式采用Socket短连接的方式进行处理,报文编码是GBK。
图3-9 SOP报文通讯架构
图3-10 sop报文结构
如图3-10所示,sop报文结构具体包含内容如下:
整个通讯数据包的组成示意图如下:
公共信息 |
交易数据 |
|||||||
交易头 |
业务数据 |
|||||||
系统信息头 |
交易公共信息头 |
交易数据头 |
数据单元 |
数据单元 |
数据单元 |
表格 单元 |
对象 单元 |
。。。 |
控制命令码可以穿插在业务数据之间,如单元与单元之间,表格内部数据项之间,以及对象内数据项之间 |
||||||||
数据单元TRANFLD是COP和SOP中代表特定内容的基本数据项,一般对应一个应用数据定义,在SOP平台中采用可自解包的通讯格式。数据单元通讯格式如下:
可选 |
机构名 |
可选 |
金额 |
可选 |
266字节数据 |
。。。 |
||||||
属性 |
0X5 |
“A网点” |
属性 |
0X6 |
“123.45” |
属性 |
0xFF |
250字节 |
0X10 |
16字节 |
||
每一个数据单元在通讯格式中用两个部分表示——长度+内容。内容部分都以字符串方式传输,截掉前导和后续的空格,以减少冗余数据的传输。长度以一个字节的16进制数表示,可表示的最大长度为250(0XFA)字节,若数据单元长度超过250字节,则采用分解传送的方式,以0XFF表示数据单元超长,如上图中266字节数据单元。
表格单元FORM是指COP和SOP平台中由格式相同的多条记录组成的复合数据单元,其中每一条记录的数据又由多个数据单元组成。表格在通讯格式中以表格名+记录条数+多条记录数据组成,每一条记录又由多个数据单元(表示方法同数据单元)表示。
表格单元的通讯格式如下:
表格名 |
记录条数 |
栏位个数 |
可选项 |
记录01 |
记录02 |
|||||||||||
0X3 |
“F01” |
0X2 |
0X3 |
打印属性或其它 |
0X3 |
“ABC” |
0X2 |
借 |
0x7 |
“1234.99” |
0X4 |
“李明” |
0X2 |
贷 |
0x5 |
“20.00” |
表格名长度 |
表格名称 |
见下表 |
姓名项 |
借贷标志项 |
金额项 |
姓名项 |
借贷标志项 |
金额项 |
||||||||
属性是可选项,在一般数据单元的定义中不使用。
在本系统中,记录条数不得大于250个。如果记录条数可能大于250条,必须采用文件传输的方式解决这一问题。
在COP/SOP系统中,对象的类型可以根据需求进行扩展和定义,目前支持的对象类型包括窗口对象、打印对象等。
对象一般由对象名+数据单元(可选项)+表格单元(可选项)组成。一个包含数据单元和表格单元的窗口对象的通讯格式如下:
对象名 |
对象内容 |
||||||
0X5 |
“OBJ01” |
数据单元 |
数据单元 |
。。。 |
表格单元01 |
数据单元 |
。。。 |
窗口对象的对象内容中不包含数据单元和表格单元的可选属性,如行、列、模式等信息。
打印对象由对象名+[打印属性+数据单元](可选项)+[表格名称+记录条数+栏位数+打印属性+格数据](可选项)+打印控制命令码(可选项)组成。打印控制命令码见打印控制命令码介绍。
通讯打包数据的配置,包括对象配置文件和 G R I D 配置文件,其格式为 ;第一行指明该对象的输出方向 ,WINDOW代表窗口,PRINTER代表打印机, 从第二行起格式如下可以有任意行:
{ F L D | G R D } : n a m e l e n t y p e s c a l e a l i g n f i l l c h a r t u r n m o d e
s t r i n g e n c r y p t (对于GRID只能有FIELD,配置文件名就是GRID的名字 ,具体字段的含义由应用层定(都是由后台接口文件自动生成)。
其意义分别是 :
F L D | G R D |
字段或表格标识,当为G R D 时,后面只有name项 |
n a m e |
f i e l d字段名称或表格或G r i d 名 称 |
l e n |
字段长度,为实际的存储长度,如整型为 4 |
t y p e |
字段类型 ,n - s h o r t ,N - i n t e g e r,L - l o n g,D - d e c i m a l ,S - c h a r,Q - d a t e ,T - t i m e,H - h e x,B - C H N 。 |
s c a l e |
小数点后位数 |
a l i g n |
对齐方式,0 - 左对齐1-右对齐 2 - 中对齐 3 - 无对齐 |
f i l l c h a r |
填充字符 |
t u r n m o d e |
转换模式:1-将传回的数据转为汉字金额2-转换为列表 中 的内容3-将数据转换为日期大写4-为空不打印 5-不管为不为空都不打印 |
s t r i n g |
如果要转换成列表内容,则指明列表名,否则为空(填 NULL)。 |
e n c r y p t |
加密标志,1-加密, 0-不加密 |
Mapfile文件内容如下:
W I N D O W F L D : G U I Y D H 8 S 0 0 0 0 N U L L 0 F L D : G U I Y X M 2 2 S 0 0 0 0 N U L L 0 G R D : F 9 3 1 3 0 1 |
第三章 通讯模块组件
如图3.1是基于Netty开发的服务端通讯架构,该架构由接收器、处理器、配置中心等模块组成。下面对各个模块进行详细介绍:
图3.1服务端架构图
如图3.2是基于Netty开发的客户端通讯架构图,该架构由客户端工厂、发送器、处理器、配置中心等模块组成。下面对各个模块进行详细介绍:
图3.2客户端架构图
如图3-3所示为Netty框架实现SOCKET服务端通讯架构的设计类图,在类图中描述了各个类之间的关联、继承、实现和依赖关系。
图3-3 Socket服务端类图
类名称 |
描述 |
ServerConf |
该类是服务端配置信息父类,属性包括服务端配置ID、监听端口、交易码解析器Bean名称、自定义过滤器Bean名称列表、服务器端错误返回格式转换ID、服务器错误返回BeanID、交易列表。 |
SocketServerConf |
该类是服务端配置信息类,属性包括:连接线程池大小、工作线程池大小、数据流入处理器Bean名称列表、数据流出处理器Bean名称列表、超时时间。 |
ServerCtx |
该类是服务端运行时上下文父类,属性包括:自定义过滤器列表、交易码解析器、交易map、错误信息。 |
SocketServerCtx |
该类是socket服务端运行时上下文类,属性包括:数据流入处理器Bean名称列表、数据流出处理器Bean名称列表。 |
CommTran |
该类是交易定义类,属性包括:交易码、交易描述.。 |
ServerTran |
该类是服务端交易类,属性包括:是否选择格式转换、是否选择流程、数据流入格式转换ID、数据流出格式转换ID、调用交易引擎流程ID。 |
图3-4 Socket服务端加载流程图
如图3-4为Socket服务端加载流程图,该流程图展示了Netty集成Socket协议栈,并实现初始化配置的过程,Socket服务端加载逻辑如下所述:
首先配置Socket服务端基础信息,这些基础信息配置过程是由Eclipse插件所开发的工具实现的,具体配置信息包括监听端口、交易码解析器Bean名称、自定义过滤器Bean名称列表、连接线程池大小、工作线程池大小等。
其次调用CommServerFactory工厂类完成所有Socket服务端配置初始化加载,在加载过程中使用ResourceUtil工具类对相关配置文件进行循环遍历,逐个加载,具体加载内容包括:读取指定文件夹下所有Socket配置文件、定义server类型为Socket服务端类型配置、创建一个SocketServer对象。
然后执行SocketServer对象的InitServer()初始化方法,将配置文件信息加载到SocketServerCtx服务器上下文中,至此Socket服务端配置文件信息便全部加载到SocketServerCtx对象中。
接下来完成Netty与Socket协议栈的集成过程。
首先创建一个SocketChannelInitializer()对象,完成通讯通道初始化操作,使用SocketChannel类创建ChannelPipeline对象,在ChannelPipeline对象中加入相关流入和流出拦截器,并创建SocketServerHandler业务处理类,添加到channelPipeline中,用于执行具体的业务逻辑。
然后调用线程的startServer()方法,启动Socket服务端线程,在线程启动过程中,设置监听线程池和工作线程池,绑定监听端口,实现对客户端请求进行监听,当监听器接收到客户端的连接请求后便开始和客户端进行通信,至此socket服务端加载完成。
SocketServerHandler业务处理类继承ChannelInboundHandlerAdapter类,重新父类方法channelRead(final ChannelHandlerContext ctx, Object msg),其中对象msg就是具体的报文数据信息。
for (IDatagramInterceptor interceptor : serverCtx.datagramInterceptors) { interceptor.decodeInterceptor(inputByte); } |
介绍报文后执行拦截器处理,拦截器接口为IDatagramInterceptor接口类,
图3-5 Socket服务端工作流程图
图3-5 为Socket服务端工作流程图,Socket服务端处理逻辑如下所述:
Socket服务接收数据信息;
接口datagramInterceptors拦截器定义了解码拦截器和编码拦截器。
public interface IDatagramInterceptor { void decodeInterceptor(byte[] datagram); void encodeInterceptor(byte[] datagram, String trancode);
byte[] encodeInterceptor(byte[] datagram, String trancode, Object args); } |
Netty架构支持多种不同的编解码框架,例如google的protobuf编解码框架、facebook的thrift编解码框架等,在Socket服务通讯中按照不同的业务需求定制化了不同的编解码器,socket服务端接收报文数据过程中,netty框架以责任链模式分别调用解码器、业务处理handler和编码器,具体相关编解码器列表如下:
解码器 |
编码器 |
长度字符串解码器 new LengthFieldBasedFrameDecoder() |
长度字符串打包编码器 new LengthFieldPrependerSetter() |
定长解码器 new FixedLengthFrameDecoder() |
长度字符串切割编码器 new LengthFieldCutter() |
分隔符解码器 new DelimiterBasedFrameDecoder() |
自定义编码器 new ChannelOutboundHandlerAdapter() |
回车符解码器 new LineBasedFrameDecoder |
|
自定义解码器 new ChannelInboundHandlerAdapter() |
在工程下的src/main/java目录下新建服务端对应的交易类,为了演示方便只做简单的打印,代码如下:
package com.spdb.speed4j.tran.demo; import java.util.HashMap; import java.util.Map; import org.springframework.stereotype.Component; import com.spdb.speed4j.comm.tran.service.ITranFlowConvertType; @Component public class TranCore implements ITranFlowConvertType { @Override public Map tranFlowConvert(Map tranFlowData) { System.out.println("Process......"); Map return tranFlowData; } } |
函数中的输入参数和输出结果都为Map类型,所以我们定义了两个JAVAMAP接口,实现交易数据的解包处理以及对客户端返回数据进行打包处理。
新建Socket服务渠道,配置通讯Handler,如果通讯过程中发生粘包和拆包时,则需配置流入、流出处理Handler列表,展示页面如图3-6所示。
数据流入处理器Bean名称对应着不同的解包处理方式,包括:长度字符串解包、定长解包、分隔符解包、自定义解包及回车符解包等。数据流出处理器Bean名称对应着不同的打包处理方式,包括:长度字符串打包、长度字符串切割及自定义bean打包处理等。
图3-6通讯Handler配置图
使用SOCKET通讯方式,服务端业务逻辑处理handler获取解码后的字节数组,该字节数组转换为字符串后是一个标准XML字符串,而接口处理函数的参数是MAP集合,因此需要完成XML到MAP集合的转换操作。
目前操作xml文件的开源项目已经非常成熟,比较流行有JiBX、XStream、JDOM、dom4j等,本论文支持自定义转换和平台转换两种方式,平台转换采用XStream技术实现XML到POJO对象转换。
图3-7 Socket服务端基础配置图
初始化过程中首先创建ChannelPipeline对象,然后添加LoggingHandler执行类。按照Inbound数据流入处理器Outbound数据流出处理器顺序调用分别按顺序调用编码及解码器。
Inbound顺序
按照Inbound顺序,判断数据流入处理器bean名称:
Outbound顺序
按照Outbound顺序,判断数据流出处理器bean名称:
最后再添加SocketServerHandler业务逻辑处理handler对象。
如图3-6所示为Netty框架实现SOCKET客户端通讯架构的设计类图,在类图中描述了各个类之间的关联、继承、实现和依赖关系。
图3-6 Socket客户端类图
类名称 |
属性描述 |
ClientConf |
该类是客户端配置信息类,属性包括服务端配置ID、监听端口、交易码解析器Bean名称、自定义过滤器Bean名称列表、服务器端错误返回格式转换ID、服务器错误返回BeanID、交易列表。 |
SocketClientConf |
该类是Socket客户户端配置信息类,属性包括:数据流入处理器Bean名称列表、数据流出处理器Bean名称列表。 |
ClientCtx |
该类是客户端运行时上下文父类,属性包括:自定义过滤器列表、交易码解析器、交易map、错误信息。 |
SocketClientCtx |
该类是socket客户端运行时上下文类,属性包括:数据流入处理器Bean名称列表、数据流出处理器Bean名称列表。 |
CommTran |
该类是交易类,属性包括:交易码、交易描述.。 |
ClientTran |
该类是客户端交易类,属性包括:是否选择格式转换、数据流入格式转换ID、数据流出格式转换ID。 |
图3-7 Socket客户端加载流程图
图3-7 为Socket客户端加载流程图,Socket客户端加载逻辑如下所述:
图3-8 Socket客户端工作流程图
Socket客户端处理逻辑如下所述:
初始化过程中首先创建ChannelPipeline对象,然后添加LoggingHandler执行类。按照Inbound数据流入处理器Outbound数据流出处理器顺序调用分别按顺序调用编码及解码器。
Inbound顺序
按照Inbound顺序,判断数据流入处理器bean名称:
Outbound顺序
按照Outbound顺序,判断数据流出处理器bean名称:
最后再添加SocketClientHandler业务逻辑处理handler对象。
如图3-2 所示为SOP报文通讯架构,SOP报文的通讯方式采用Socket短连接的方式进行处理,报文编码是GBK。
图3-9 SOP报文通讯架构
图3-10 sop报文结构
如图3-10所示,sop报文结构具体包含内容如下:
通讯数据包由公共信息部分、交易数据部分;公共信息部分包括系统信息头和交易公共信息头;交易数据部分包括交易数据头(可选)、业务数据和系统控制命令;业务数据部分又包括数据单元、表格和对象;业务数据部分可以插入系统控制命令。
控制命令码可以穿插在业务数据之间,如单元与单元之间,表格内部数据项之间,以及对象内数据项之间
整个通讯数据包的组成示意图如下:
公共信息 |
交易数据 |
|||||||
交易头 |
业务数据 |
|||||||
系统信息头 |
交易公共信息头 |
交易数据头 |
数据单元 |
数据单元 |
数据单元 |
表格 单元 |
对象 单元 |
。。。 |
数据单元TRANFLD是COP和SOP中代表特定内容的基本数据项,一般对应一个应用数据定义,在SOP平台中采用可自解包的通讯格式。数据单元通讯格式如下:
可选 |
机构名 |
可选 |
金额 |
可选 |
266字节数据 |
。。。 |
||||||
属性 |
0X5 |
“A网点” |
属性 |
0X6 |
“123.45” |
属性 |
0xFF |
250字节 |
0X10 |
16字节 |
||
每一个数据单元在通讯格式中用两个部分表示——长度+内容。内容部分都以字符串方式传输,截掉前导和后续的空格,以减少冗余数据的传输。长度以一个字节的16进制数表示,可表示的最大长度为250(0XFA)字节,若数据单元长度超过250字节,则采用分解传送的方式,以0XFF表示数据单元超长,如上图中266字节数据单元。
表格单元FORM是指SOP平台中由格式相同的多条记录组成的复合数据单元,其中每一条记录的数据又由多个数据单元组成。表格在通讯格式中以表格名+记录条数+多条记录数据组成,每一条记录又由多个数据单元(表示方法同数据单元)表示。
表格单元的通讯格式如下:
表格名 |
记录条数 |
栏位个数 |
可选项 |
记录01 |
记录02 |
|||||||||||
0X3 |
“F01” |
0X2 |
0X3 |
打印属性或其它 |
0X3 |
“ABC” |
0X2 |
借 |
0x7 |
“1234.99” |
0X4 |
“李明” |
0X2 |
贷 |
0x5 |
“20.00” |
表格名长度 |
表格名称 |
见下表 |
姓名项 |
借贷标志项 |
金额项 |
姓名项 |
借贷标志项 |
金额项 |
||||||||
在本系统中,记录条数不得大于250个。如果记录条数可能大于250条,必须采用文件传输的方式解决这一问题。
在SOP系统中,对象的类型可以根据需求进行扩展和定义,目前支持的对象类型包括窗口对象、打印对象等。
对象一般由对象名+数据单元(可选项)+表格单元(可选项)组成。一个包含数据单元和表格单元的窗口对象的通讯格式如下:
对象名 |
对象内容 |
||||||
0X5 |
“OBJ01” |
数据单元 |
数据单元 |
。。。 |
表格单元01 |
数据单元 |
。。。 |
通讯打包数据的配置,包括对象配置文件和 G R I D 配置文件,其格式为 ;第一行指明该对象的输出方向 ,如:WINDOW代表窗口,PRINTER代表打印机, 从第二行起格式如下可以有任意行:
{ F L D | G R D } : n a m e l e n t y p e s c a l e a l i g n f i l l c h a r t u r n m o d e
s t r i n g e n c r y p t
其具体含义如表所示 :
F L D | G R D |
字段或表格标识,当为G R D 时,后面只有name项 |
n a m e |
f i e l d字段名称或表格或G r i d 名 称 |
l e n |
字段长度,为实际的存储长度,如整型为 4 |
t y p e |
字段类型 ,n - s h o r t ,N - i n t e g e r,L - l o n g,D - d e c i m a l ,S - c h a r,Q - d a t e ,T - t i m e,H - h e x,B - C H N 。 |
s c a l e |
小数点后位数 |
a l i g n |
对齐方式,0 - 左对齐1-右对齐 2 - 中对齐 3 - 无对齐 |
f i l l c h a r |
填充字符 |
t u r n m o d e |
转换模式:1-将传回的数据转为汉字金额2-转换为列表 中 的内容3-将数据转换为日期大写4-为空不打印 5-不管为不为空都不打印 |
s t r i n g |
如果要转换成列表内容,则指明列表名,否则为空(填 NULL)。 |
e n c r y p t |
加密标志,1-加密, 0-不加密 |
Mapfile文件内容如下:
W I N D O W F L D : G U I Y D H 8 S 0 0 0 0 N U L L 0 F L D : G U I Y X M 2 2 S 0 0 0 0 N U L L 0 G R D : F 9 3 1 3 0 1 |
类名称 |
属性描述 |
CommServerFactory |
|
HttpServerChannelInitializer |
|
HttpServer |
|
HttpServerConf |
|
SSLManagerFactoryUtil |
|
以下工厂类对象启动时会加载所有的XML文件,完成服务端启动注册的过程。XML配置文件均使用xstream技术进行解析。服务端通讯启动过程中需要先完成初始化过程,然后启动异步线程,交由netty进行管理。
/** * 加载Server配置文件 */ private void loadServerFromXML(String server){ Resource[] resources = null; LogManager.COMM_LOGGER.info("开始加载"+server+"服务端配置"); resources = ResourceUtil.getResourceList(serverloadMap.get(server)); if (resources == null) { LogManager.COMM_LOGGER.error("加载"+server+"服务端配置列表失败"); } else { for (int i = 0; i < resources.length; i++) { XStream xstream = new XStream(new DomDriver()); String uri = null; try { uri = resources[i].getURI().toString(); LogManager.COMM_LOGGER.info("开始加载"+server+"服务端配置:" + uri); ServerConf conf = (ServerConf) xstream.fromXML(resources[i].getInputStream()); checkServerIDDup(conf.id); IServer iServer = (IServer) ctx.getBean(server); iServer.setServerConf(conf); iServer.initServer(); if (iServer.isInited()) { serverList.add(iServer); iServer.startServer(); } } } catch (Exception e) { LogManager.COMM_LOGGER.error("加载"+server+"服务端配置:" + uri + "失败", e); } } } } |
以下为异步线程初始化过程的核心代码,主要包括建立通讯上下文、完成交易码解析、遍历添加交易列表及初始化通讯通道,其中初始化通讯渠道主要用于创建ChannelPipeline添加并设置各种ChannelHandler,包括各种编解码器及自定义handler服务。
@Override public void initServer() { // 建立通讯上下文 serverCtx = new HttpServerCtx(); // 交易码解析 try { serverCtx.httpTrancodeDecoder = (IHttpTrancodeDecoder) springCtx.getBean(httpServerConf.trancodeDecoderBean); } catch (Exception e) { serverCtx.errorMsg = "Http服务端:" + httpServerConf.id + " - 初始化交易码解析器失败"; logManager.COMM_LOGGER.error(serverCtx.errorMsg, e); return; } // 交易列表 Map for (ServerTran tran : httpServerConf.tranList) { map.put(tran.tranCode, tran); } serverCtx.tranMap = map; // 初始化通讯通道 try { httpServerChannelInitializer = new HttpServerChannelInitializer(httpServerConf, serverCtx, springCtx); } catch (Exception e) { serverCtx.errorMsg = "Http服务端:" + httpServerConf.id + " - 初始化通讯通道失败"; logManager.COMM_LOGGER.error(serverCtx.errorMsg, e); return; } isInited = true; logManager.COMM_LOGGER.info("Http服务端:" + httpServerConf.id + " - 初始化成功"); } |
初始化渠道通讯过程中,主要完成创建ChannelPipeline添加并设置各种ChannelHandler,包括各种编解码器及自定义handler服务。
在HTTP通讯过程中,ChannelPipeline中添加了SslHandler用于安全认证, HttpRequestDecoder解码器、HttpObjectAggregator对象聚合解码器、 HttpResponseEncoder消息应答编码器及httpServerHandler业务逻辑处理器。
@Override protected void initChannel(NioSocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); //添加日志信息 if(httpServerConf.isLogger){ pipeline.addLast(new LoggingHandler()); } //SSL加密设置配置 if(httpServerConf.isEncryption){ //SSL加密设置配置 sslHandler = sSLManagerFactoryUtil.configureSSLOnDemand(httpServerConf.sslManagerConf, true); if (sslHandler != null) { pipeline.addLast("ssl", sslHandler); } } pipeline.addLast(new ReadTimeoutHandler(httpServerConf.timeout, TimeUnit.MILLISECONDS)); pipeline.addLast(new HttpRequestDecoder()); pipeline.addLast(new HttpObjectAggregator(Integer.MAX_VALUE)); pipeline.addLast(new HttpResponseEncoder()); // 最终处理返回流程 HttpServerHandler httpServerHandler = new HttpServerHandler(httpServerConf, serverCtx, springCtx); pipeline.addLast(httpServerHandler); } |
以下为异步线程启动过程,在该方法中定义了一个当前自身的runnable异步线程,并执行start()线程启动方法,调用自身的run()方法。
@Override public void startServer() throws CommException { status = ServerStatus.STARTING; if (!isInited) { status = ServerStatus.STOPPED; throw new CommException(CommException.INIT_ERROR,serverCtx.errorMsg); } // 新建线程启动 Thread serverThread = new Thread(this); serverThread.start(); } |
以下是runnable异步线程启动时的具体逻辑代码,
主要完成netty的reactor线程池配置工作,设置监听线程池组和工作线程池组,并创建服务端启动类ServerBootStrap实例,同时设置服务端启动相关参数;
设置并绑定服务端channel,通过channel方法指定服务端的channel类型,利用反射创建NioServerSocketChannel对象;
然后使用ServerBootStrap对象绑定并监听端口,在绑定之前系统会做一系列的初始化和检测工作,完成之后会启动监听端口,并将ServerSocetChannel注册到Selector上监听客户端连接;
Rector监听线程池中的NioEventLoop线程负责调度和执行Selector轮询操作,选择那些准备就绪的Channel集合。
@Override public void run() { // 设置线程池 EventLoopGroup bossGroup = new NioEventLoopGroup(httpServerConf.bossThreadNum,new ThreadFactory() { @Override public Thread newThread(Runnable r) { Thread thread = new Thread(r); thread.setName(httpServerConf.id+"Acceptor"); return thread; } }); EventLoopGroup workerGroup = new NioEventLoopGroup(httpServerConf.workerThreadNum,new ThreadFactory(){ @Override public Thread newThread(Runnable r) { Thread thread = new Thread(r); thread.setName(httpServerConf.id+"Worker"); return thread; } }); // 主控服务 ServerBootstrap serverBootStrap = new ServerBootstrap(); serverBootStrap.option(ChannelOption.ALLOW_HALF_CLOSURE, true); //1:1排队 serverBootStrap.option(ChannelOption.SO_BACKLOG,httpServerConf.workerThreadNum); serverBootStrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).childHandler(httpServerChannelInitializer); try { // 启动监听 channel = serverBootStrap.bind(httpServerConf.port).sync().channel(); status = ServerStatus.STARTED; logManager.COMM_LOGGER.info("Http服务端:" + httpServerConf.id + " - 启动成功"); } catch (Exception e) { status = ServerStatus.STOPPED; logManager.COMM_LOGGER.error("Http服务端:" + httpServerConf.id + " - 启动失败", e); return; } try { // 监听服务停止 channel.closeFuture().sync(); status = ServerStatus.STOPPED; logManager.COMM_LOGGER.info("Http服务端:" + httpServerConf.id + " - 停止成功"); } catch (Exception e) { logManager.COMM_LOGGER.error("Http服务端:" + httpServerConf.id + " - 停止失败", e); } finally { // 释放资源 bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } |
类名称 |
属性描述 |
CommClientFactory |
|
HttpClientChannelInitializer |
|
HttpClient |
|
HttpClientCtx |
|
HttpClientConf |
|
HttpClientSender |
|
SSL证书管理过程中包含以下属性:协议类型、证书类型、证书密码、证书存放路径、信任证书类型、信任证书密码、信任证书存放路径、证书库秘钥类型、信任证书库秘钥类型、是否开启服务端验证、是否开启客户端验证、客户端是否配置自定义证书、包含选项、排除选项。这里的信任证书就是跟证书。
服务端证书相关配置项内容:
协议类型 |
SSL |
证书类型 |
pkcs12 |
证书秘钥库类型 |
sunx509 |
证书密码 |
111111 |
证书存放路径 |
D:/servercrt/server.pfx |
根证书类型 |
jks |
根证书秘钥类型 |
sunx509 |
根证书密码 |
111111 |
根证书存放路径 |
D:/certspxf/cacrt/ca-trust.jks |
客户端证书相关配置项内容:
协议类型 |
TLSv1.2 |
证书类型 |
pkcs12 |
证书秘钥库类型 |
sunx509 |
证书密码 |
111111 |
证书存放路径 |
D:/clientrcrt/client.pfx |
根证书类型 |
jks |
根证书秘钥类型 |
sunx509 |
根证书密码 |
111111 |
根证书存放路径 |
D:/certspxf/cacrt/ca-trust.jks |
X509TrustManager信任证书管理,
加载TrustManageFactory 信任证书库管理,需要ssl的配置信息。首先要创建信任证书的KeyStore对象,需要确认信任的根证书的keystore类型(有jks和pkcs12等),其次以文件流的形式读取证书文件,然后使用keystore对象load加载证书,同时输入keystore的密码,最后实例化TrustManageFactory对象,使用加载完证书的keystore对象完成初始化。
加载KeyManageFactory证书库管理,需要ssl的配置信息。首先要创建信任证书的KeyStore对象,需要确认服务端或客户端证书的keystore类型(pkcs12),其次以文件流的形式读取证书文件,然后使用keystore对象load加载证书,同时输入keystore的密码,最后实例化KeyManageFactory对象,使用加载的证书keystore对象完成初始化。
服务端证书验证时需要考虑是单项认证还是双向认证以及是否要验证客户端证书等。
对于双向认证需要创建KeyManagerFactory和TrustManageFactory类的实例对象,使用SSLContextUtils创建SSLContext对象,创建过程中需要使用根证书和服务端证书,然后使用SSLContext创建SSLEngine对象,设置该对象相关属性,例如设置使用服务端模式,设置需要验证客户端,设置需要客户端授权。使用SSLEngine对象生成SslHandler对象,SslHandler对象是netty框架中的处理对象,用于解码获取数据后执行的处理类。
对于单向认证仅仅需要创建KeyManagerFactory对象,使用SSLContextUtils创建SSLContext对象,创建SSLEngine对象,设置该对象相关属性,设置使用服务器端模式且不验证客户端。
客户端证书验证时需要考虑是单项认证还是双向认证以及是否要验证服务端证书等。
对于双向认证需要创建KeyManagerFactory和TrustManageFactory类的实例对象,使用SSLContextUtils创建SSLContext对象,创建过程中需要使用根证书和客户端证书,然后使用SSLContext创建SSLEngine对象,设置该对象相关属性,例如设置使用客户端模式,设置需要验证客户端,设置需要客户端授权。使用SSLEngine对象构造SslHandler对象,SslHandler对象是netty框架中的处理对象,用于解码获取数据后执行的处理类。
对于单向认证需要区分是否自定义证书,仅仅需要创建KeyManagerFactory对象,使用SSLContextUtils创建SSLContext对象,创建SSLEngine对象,设置该对象相关属性,设置使用服务器端模式且不验证客户端。
TLSv1.0、TLSv1.1、TLSv1.2均是SSLv3.0的升级协议,但是HTTPS的握手协议方式不变,
双向认证首先需要需要创建KeyManagerFactory和TrustManageFactory类的实例对象,其次需要创建TLSServerParemeters对象,设置安全套阶层协议、秘钥管理、信任证书管理,设置包含和不包含的协议,然后创建ClientAuthentication客户端认证对象,将tlsserverfacry对象设置客户端认证。
然后使用SSLContext创建SSLEngine对象,设置参数为tlsserverfacry对象。使用SSLEngine对象生成SslHandler对象,SslHandler对象是netty框架中的处理对象,用于解码获取数据后执行的处理类。
单向认证不需要创建TrustManageFactory类,仅仅需要创建KeyManagerFactory类完成证书认证即可。
首先sSLManagerFactoryUtil.configureSSLOnDemandWS()方法创建sslhandler对象,使用pipeline.addLast("ssl", sslHandler)将sslhandler对象添加到pipeline中,然后再添加编解码处理和执行逻辑处理。
protected void initChannel(NioSocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); //添加日志信息 if(httpServerConf.isLogger){ pipeline.addLast(new LoggingHandler()); } //SSL加密设置配置 if(httpServerConf.isEncryption){ sslHandler = sSLManagerFactoryUtil.configureSSLOnDemandWS(httpServerConf.sslManagerConf, true, tlsServerParameters); if (sslHandler != null) { pipeline.addLast("ssl", sslHandler); } } pipeline.addLast(new HttpRequestDecoder()); pipeline.addLast(new HttpObjectAggregator(httpServerConf.maxChunkContentSize)); pipeline.addLast(new HttpResponseEncoder()); pipeline.addLast("deflater", new HttpContentCompressor()); // Set up the idle handler pipeline.addLast("idle", new IdleStateHandler(httpServerConf.readIdleTime, httpServerConf.writeIdleTime, 0)); pipeline.addLast("handler", this.getServletHandler()); } |
---已上就完成了根证书的相关操作,下一步可以颁发证书了。
签发客户端身份认证证书
客户端证书完成,注意如果在web服务器上使用客户端证书,需要在web服务器上使用根证书对客户端进行验证,切记!
Web Service是一个平台独立、松耦合、自包含、可编程的web应用服务,用于支持网络间不同应用系统的互动操作。Web Service的三个核心技术是:SOAP、WSDL和UDDI。Web Service简称为“WS”。
WSDL是用机器能阅读的方式提供的一个正式的基于XML语言的描述文档,用于描述Web Service的调用协议和通讯协议。WSDL是人与机器均可读取,通常用来辅助生成服务器和客户端代码。
SOAP可以运行在任何其他传输协议上,在传输层之间的头是不同的,但XML有效负载保持相同。Web Service通常会使用HTTP或HTTPS与SOAP绑定。
Webservice服务端主要用于提供各项目组以Webservice通讯协议方式在ESB企业总线发布相关服务,供其他应用服务请求调用,该服务端具体设计类图如下。
图 3-11 Webservice服务端类图
类名称 |
描述 |
WebServiceServer |
该类是WebService的Server服务类,定义的属性包括:初始化标志、Http服务端通讯上下文、CXF 注册路径、通讯通道、服务端运行状态、WebService服务端配置SSL/TLS参数、WebService配置参数、pipeline初始化属性,重写了startServer()和stopServer()方法。 定义线程run()方法。 |
ServerConf |
该类是服务端配置信息类,定义了服务端配置ID、监听端口、交易码解析器Bean名称、自定义过滤器Bean名称列表、服务器端错误返回格式转换ID、服务器错误返回BeanID、交易列表等属性。 |
WebServiceServerConf |
该类继承ServerConf类,是WebService服务端配置信息类,属性包括:服务主机地址、连接后读取超时时间、写返回超时时间、最大报文长度、连接线程池大小、工作线程池大小、密钥管理属性、选取的方法列表、拦截器列表等。 |
WebServiceServerChannelInitializer |
该类是Channel pipeline初始化类,定义了Http服务端通讯上下文、Http服务端配置、SSL加密设置配置属性及线程启动方法。 |
ServerCtx |
该类是服务端运行时上下文,属性包括:自定义过滤器列表、交易码解析器、交易map、错误信息等属性。 |
HttpServerCtx |
该类继承ServerCtx类,同时定义Http交易码解析器属性。 |
WebServiceHttpDestination |
该类继承AbstractHTTPDestination类, 定义属性包括:WebService的Server、WebService Server工厂、ServletContext、ClassLoader类加载器、URL等。 方法:1定义doservice()方法;2检索webservice引擎;3通过端点信息获取URL方法。 |
WebServiceHttpContextHandler |
该类主要用于保存WebService Handler的Map。 属性包括:ServletContext类、WebServiceHttpHandler对象的List集合等。 方法:1 addNettyHttpHandler()方法,添加WebServiceHttpHandler对象到list集合中;2在list集合中查询WebServiceHttpHandler对象;3删除list集合中查询WebServiceHttpHandler对象;4 handle()处理方法,遍历WebServiceHttpHandler对象,执行对象的handle方法; |
WebServiceHttpHandler |
该类属性包括: urlname、WebServiceHttpDestination类对象、ServletContext类对象等。 方法:定义了handle()方法,实际调用的是WebServiceHttpDestination类对象的doservice()方法。 |
WebServiceServletHandler |
继承ChannelInboundHandlerAdapter类,定义Netty拦截器列表、WebServiceServerChannelInitializer类、WebServiceServerConf配置文件、ServTranS服务接口类等属性。 重写了channelRead()方法、channelReadComplete()方法和exceptionCaught()方法。 |
WebServiceServerFactory |
该类是WebService Server的工厂类,实现CXF的BusLifeCycleListener类。属性包括:WebServiceServer的Map集合、CXF的bus总线、总线生命周期管理器、线程参数的Map集合、TLS server的Map集合。 方法包括:1在Map集合中检索WebServiceServer引擎;2.在已经存在的端口集合列表中删除该port端口对应的WebServiceServer引擎;3.WebServiceServer引擎管理,添加WebServiceServer服务到集合列表中;4.在CXF退出时,远程关闭WebService服务。 |
Webservice客户端主要用于提供各项目组以Webservice通讯协议方法连接ESB企业总线,调用ESB所提供的相关服务,该客户端具体设计类图如下。
图3-12 Webservice客户端类图
类名称 |
属性描述 |
ServerConf |
该类是服务端配置信息父类,属性包括:服务端配置ID、监听端口、交易码解析器Bean名称、自定义过滤器Bean名称列表、服务器端错误返回格式转换ID、服务器错误返回BeanID、交易列表。 |
webserviceClientConf |
该类是Socket服务端配置信息类,属性包括:1请求URI;2 WebServiceClientTran交易列表;3进入服务的拦截器;4离开服务的拦截器。 |
ServerCtx |
该类是服务端运行时上下文父类,属性包括:自定义过滤器列表、交易码解析器、交易map、错误信息。 |
webserviceClientCtx |
该类是socket服务端运行时上下文配置类,属性包括:自定义过滤器列表、交易map(存储服务端交易配置)、请求结果Map及错误信息等属性。 |
ClientTran |
该类是服务端交易类,属性包括:是否选择格式转换、数据流入格式转换ID、数据流出格式转换ID。 |
WebServiceClientTran |
属性:1调用入口类名称;2调用方法名称;3服务名;4服务方法。 |
CommClientFactory |
该类是客户端工厂类,该类从xml文件中加载所有clients配置,包括http、socket和webservice的客户端配置信息。 |
ESB系统作为浦发银行IT架构的核心枢纽主要提供银行内部实时的调用服务。ESB功能定位于“服务集成”,企业服务总线是银行的基础设施系统,它的作用是为服务集成、服务和产品创新等应用提供基础架构的支撑,通过企业服务总线平台,实现基于SOA架构的松耦合架构体系,全面解决系统之间的异构性问题,降低技术集成的复杂度,实现各个产品系统功能的服务化封装,奠定全行服务化的应用架构基础,全面提高全行IT架构灵活度和支持业务创新的能力。
ESB支持的信息交换模式主要包括有如下几种:
简单请求响应模式:ESB系统支持简单请求响应模式,包括同步调用方式、异步调用方式和无响应调用方式。
文件传输模式:ESB系统支持联机文件传输模式。文件发送方将文件存到指定的位置,由文件接收方通过文件传输模块获取文件进行处理。文件传输模式也支持同步调用方式和异步调用方式。
订阅发布模式:订阅发布一般用于消息的广播和批量推送,推送过程采用异步推送的处理方式。
浦发银行采用SOAP报文作为银行系统间信息交互的标准报文格式。新建服务调用方系统需要通过SOAP报文调用ESB系统上的服务;新建服务提供方系统也需要通过SOAP报文来对外发布服务供ESB系统调用。
考虑到存量系统的实际情况,ESB系统会兼容银行现有存量系统的报文格式SOP,存量系统可以通过SOP报文实现服务的调用和发布。
ESB系统作为浦发银行IT架构的核心枢纽主要提供银行内部实时的调用服务。SPEED4J平台支持WEBSERVICE调用ESB等配置化开发,通过工具配置用户信息查询交易接口和用户信息查询客户端来实现对接ESB的调用。
首先以客户信息ECIF服务为例,讲解项目开发中如何与ESB对接。连接ESB,我们首先需要配置一个系统ID和系统编号,系统ID和系统CODE需要项目组自己根据实际情况向ESB处申请的具体值填写。如下图
图3-21 系统参数配置图
用户信息查询交易接口配置:
用户信息查询交易通过与ESB进行WebService通讯完成,通讯框架通过使用Apache CXF组件支持使用特定WSDL地址来生成相关的WebService接口,系统的相关接口配置存放于项目通用组件工程中,具体配置过程如下:
展开项目下的接口管理节点,右击WebService资源管理节点,选择新增WebService服务,弹出向导,输入服务中文名:客户信息查询,WSDL地址:http://10.112.20.145:8080/Publish/WSDLfilePath/S120030018.wsdl,该地址由ESB提供,自动反显服务名称,由于该交易是外部请求,接口对象方式选择客户端,点击完成。稍等片刻后将在Console中Speed4J Console子视图中看到生成加载成功消息,至此ECIF用户信息查询交易接口就创建完成了,可以在WebService资源管理节点下看到交易的相关接口对象。
图3-22 接口配置图
用户信息查询客户端配置:
为了方便系统与外部系统进行交互,该通讯开发框架提供了渠道通讯管理工具,便于项目组统一配置和管理系统的各类外部通讯。目前系统向外部请求的外部通讯管理支持Http通讯、MQ通讯、Socket通讯以及WebService通讯,系统作为服务渠道对外提供服务的服务渠管理同样也支持Http服务、MQ服务、Socket服务以及WebService通讯。
相关通讯配置存放于项目通用组件工程中,具体的WebService外部通讯配置如下:
展开项目下的渠道通讯管理节点,展开外部通讯管理节点,右击WebService外部通讯节点,选择新建WebService外部通讯,弹出配置向导。
输入基本配置页面具体配置,包括全局唯一ID,调用主机地址,调用主机端口,请求URI,线程池初始大小,线程池最大大小,线程池队列大小,线程池保持连接时间,服务超时时间,是否开启监控等。
图3-23 外部通讯配置图
自定义过滤器配置页面用于配置报文进入系统后的过滤器,针对浦发ESB交易,通讯开发框架提供了对交易头进行处理的过滤器。如果项目不想用通讯框架提供的服务组件报文头,可以不设置此项,项目自己按照ESB提供的报文头规范进行组装即可。
交易列表配置页面用于配置交易,点击添加,输入交易码:RtlBscInfoQryClntNo,点击交易类旁的选择按钮,选择前面添加的S120030018接口,选择接口方法rtlBscInfoQryClntNo,交易描述:客户签约信息查询,点击确定,完成交易添加。
图3-24 交易列表配置图
点击Finish完成外部通讯配置过程,可在WebService外部通讯节点下查看配置项。
图3-25 客户端生成配置图
“Simple Object Access Protocol”的简称,简单对象访问协议。SOAP可以运行在任何其他传输协议上,在传输层之间的头是不同的,但XML有效负载保持相同。Web Service通常会使用HTTP或HTTPS与SOAP绑定。
SOAP在安全方面是通过使用XML-Security和XML-Signature两个规范组成了WS-Security来实现安全控制的。
报文编码推荐采用UTF-8或GBK,编码需要在XML头的encoding中定义。
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:s="http://esb.spdbbiz.com/services/S100100001" xmlns:d="http://esb.spdbbiz.com/metadata"> <soap:Header> <s:ReqHeader> <d:Mac>00000000000000000000d:Mac> <d:MacOrgId>888888d:MacOrgId> <d:SourceSysId>0001d:SourceSysId> <d:MsgId>0002201307070000000000000001d:MsgId> <d:ConsumerId>0002d:ConsumerId> <d:ServiceAdr>http://esb.spdbbiz.com:7701/services/S100100001 http://esb.spdbbiz.com/services/S100100001d:ServiceAdr> <d:ServiceAction>urn:/QueryCust01d:ServiceAction> s:ReqHeader> soap:Header> <soap:Body> <s:ReqQueryCust01> <s:ReqSvcHeader> <s:BranchId>0001d:BranchId> <s:TranTellerNo>800800d:TranTellerNo> <s:TranSeqNo>0002201307070000000000000001d:TranSeqNo> <s:TranDate>20130707d:TranDate> <s:TranTime>121200000d:TranTime> <s:SourceSysId>0001d:SourceSysId> <s:ConsumerId>0002d:ConsumerId> <s:GlobalSeqNo>0001201307070000000000000001d:GlobalSeqNo> s:ReqSvcHeader> <s:SvcBody> <s:CustId>8888888888d:CustId> <s:CustType>01d:CustType> s:SvcBody> s:ReqQueryCust01> soap:Body> soap:Envelope> |
S100100001.wsdl的样例:
<wsdl:definitions xmlns:http="http://schemas.xmlsoap.org/wsdl/http/" xmlns:mime="http://schemas.xmlsoap.org/wsdl/mime/" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:tns="http://esb.spdbbiz.com/services/S100100001/wsdl" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:s="http://esb.spdbbiz.com/services/S100100001" targetNamespace="http://esb.spdbbiz.com/services/S100100001/wsdl"> <wsdl:types> <xsd:schema attributeFormDefault="qualified" elementFormDefault="qualified" targetNamespace="http://esb.spdbbiz.com/services/S100100001/wsdl"> <xsd:import namespace="http://esb.spdbbiz.com/services/S100100001" schemaLocation="S100100001.xsd"/> xsd:schema> wsdl:types> <wsdl:message name="ReqHeader"> <wsdl:part name="ReqHeader" element="s:ReqHeader"/> wsdl:message> <wsdl:message name="ReqQueryCust01"> <wsdl:part name="ReqQueryCust01" element="s:ReqQueryCust01"/> wsdl:message> <wsdl:message name="ReqQueryCust02"> <wsdl:part name="ReqQueryCust02" element="s:ReqQueryCust02"/> wsdl:message> <wsdl:message name="RspHeader"> <wsdl:part name="RspHeader" element="s:RspHeader"/> wsdl:message> <wsdl:message name="RspQueryCust01"> <wsdl:part name="RspQueryCust01" element="s:RspQueryCust01"/> wsdl:message> <wsdl:message name="RspQueryCust02"> <wsdl:part name="RspQueryCust02" element="s:RspQueryCust02"/> wsdl:message> <wsdl:portType name="ESBServerPortType"> <wsdl:operation name="QueryCust01"> <wsdl:input message="tns:ReqQueryCust01"/> <wsdl:output message="tns:RspQueryCust01"/> wsdl:operation> <wsdl:operation name="QueryCust02"> <wsdl:input message="tns:ReqQueryCust02"/> <wsdl:output message="tns:RspQueryCust02"/> wsdl:operation> wsdl:portType> <wsdl:binding name="ESBServerSoapBinding" type="tns:ESBServerPortType"> <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/> <wsdl:operation name="QueryCust01"> <soap:operation soapAction="urn:/QueryCust01/v10"/> <wsdl:input> <soap:header message="tns:ReqHeader" part="ReqHeader" use="literal"/> <soap:body use="literal"/> wsdl:input> <wsdl:output> <soap:header message="tns:RspHeader" part="RspHeader" use="literal"/> <soap:body use="literal"/> wsdl:output> wsdl:operation> <wsdl:operation name="QueryCust02"> <soap:operation soapAction="urn:/QueryCust02/v10"/> <wsdl:input> <soap:header message="tns:ReqHeader" part="ReqHeader" use="literal"/> <soap:body use="literal"/> wsdl:input> <wsdl:output> <soap:header message="tns:RspHeader" part="RspHeader" use="literal"/> <soap:body use="literal"/> wsdl:output> wsdl:operation> wsdl:binding> <wsdl:service name="S100100001"> <wsdl:port name="ESBServerSoapEndpoint" binding="tns:ESBServerSoapBinding"> <soap:address location="http://esb.spdbbiz.com:7701/services/S100100001"/> wsdl:port> wsdl:service> wsdl:definitions> |
在异常情况处理逻辑设计上,我们定义了CommException(通讯异常)类,该类的属性包括:错误编码、原始报文和流水号,其中错误编码类型包括:未分类的系统错误、超时异常、IO异常、服务端/客户端初始化失败、解码异常、流控拒绝等。
在Speed4j平台中,通讯异常控制被定义在业务逻辑的handler处理类中,以SOCKET服务端通讯为例,业务逻辑处理类SocketServerHandler 继承netty框架中ChannelInboundHandlerAdapter类,重写了exceptionCaught()方法,在该方法中,我们定义了不同异常情况下的处理方案,具体异常处理代码如下所述:
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { if (cause instanceof ReadTimeoutException) { clientCtx.isSuccess = false; clientCtx.errorMsg = "Socket客户端:" + socketClientConf.id + " - 接收数据超时,自动断开"; logManager.COMM_LOGGER.error(clientCtx.errorMsg, new Exception(cause)); } else { clientCtx.isSuccess = false; clientCtx.errorMsg = "Socket客户端:" + socketClientConf.id + " - 通讯异常"; logManager.COMM_LOGGER.error(clientCtx.errorMsg, new Exception(cause)); } ctx.channel().close(); } |
异常情况处理方案包括:冲正、补发、人工处理、对账等。
作为客户端请求外部通讯时,调用doRequest方法,如果期间发生任何通讯错误将向调用者抛出CommException异常;
作为服务端对外提供通讯服务时,所有异常在运行框架内处理,通过LogManager记录框架错误日志。
第五章 总结和展望
最后,本文通过Netty这个NIO框架,实现了一个很简单的“高性能”的RPC服务器,但是还是有一些值得改进的地方,比如:
1、对象序列化传输可以支持目前主流的序列化框架:protobuf、JBoss Marshalling、Avro等等。
2、Netty的线程模型可以根据业务需求,进行定制。因为,并不是每笔业务都需要这么强大的并发处理性能。
3、目前RPC计算只支持一个RPC服务接口映射绑定一个对应的实现,后续要支持一对多的情况。
4、业务线程池的启动参数、线程池并发阻塞容器模型等等,可以配置化管理。
5、Netty的Handler处理部分,对于复杂的业务逻辑,现在是统一分派到特定的线程池进行后台异步处理。当然你还可以考虑JMS(消息队列)方式进行解耦,统一分派给消息队列的订阅者,统一处理。目前实现JMS的开源框架也有很多,ActiveMQ、RocketMQ等等,都可以考虑。