基于Netty的RPC通信系统的编解码技术研究
2017-11-20韩星刘姣周淑君
韩星+刘姣+周淑君
摘要:近年来,面向服务的体系架构(SOA)逐渐成为构建大中型分布式系统的主流方式,远程过程调用(RPC)在其中起着举足轻重的作用。Netty作为一个基于事件驱动的、异步的网络应用框架,能够快捷高效的实现分布式系统间的远程服务调用。该文对Netty编解码器进行分析和研究,并结合消息序列化,提出了一种性能和可靠性更高的编解码方法。
关键词:netty;编解码;序列化;远程过程调用;消息协议
中图分类号:TP311.5 文献标识码:A 文章编号:1009-3044(2017)26-0104-02
Abstract: In recent years, service oriented architecture (SOA) has gradually become the mainstream way to build large and medium-sized distributed systems. Remote procedure call (RPC) plays an important role in it. Netty, as an event driven and asynchronous network application framework, can quickly and efficiently realize remote service invocation among distributed systems. In this paper, the Netty codec is analyzed and studied, and a method of encoding and decoding with higher performance and reliability is proposed combining with message serialization.
Key words: netty;serialization; codec; remote procedure call; message protocol
SUN公司在2002年推出了JDK1.4,基于Java的Socket通信开始支持非阻塞I/O,系统性能和可靠性均得到了很大的提高。但早期的API和类库依然存在一些不完善的地方,特别是对文件系统的处理能力非常薄弱。直到2007年JDK1.7发布,升级后的NIO2.0提供了异步文件通道和异步套接字通道的实现,文件处理能力有了进一步的提升[1-2]。尽管NIO的吞吐量和可靠性相对于传统的BIO(同步阻塞式IO)有了质的飞跃,但其类库和API十分繁杂,使用起来非常困难[3]。再加上粘包拆包、断线重连等可靠性处理的工作量和复杂度都非常大,因此不建议使用NIO原生API进行通信系统的开发。为了简化NIO网络编程,一些开源组织发布了诸如Netty、Mina、Grizzly和xSocket等通信框架。其中,Netty的功能、性能、健壮性、可定制和可扩展行在同类框架中都是首屈一指的,并且已经得到了大量商业项目的成功验证,如阿里巴巴的分布式服务框架Dubbo,Hadoop的RPC框架Avro等[4]。本文基于Netty框架,定义了一种通用的消息结构Message,继承Netty的半包解码器LengthFieldBaseFrameDecoder解码消息以解決TCP粘包拆包问题,使用protobuf对消息体进行序列化,使通信系统的性能和可靠性均得到了极大的提高。
1 编解码方法
1.1 粘包拆包问题
由于应用层发送消息时写入的字节大小不固定以及IP分片等原因,TCP底层会根据缓冲区的实际情况将单个业务消息拆分成多个包,或者将多个小包封装成一个大包进行发送。接收方有可能一次接收不完整个业务消息或者一次收到几个消息,此时消息解码就会出现异常,不能进行接下来的业务处理和消息回应。TCP粘包拆包无法在底层进行规避,只能通过合理的上层应用协议设计进行处理[5]。常用的解决方案有三种:一是消息定长;二是使用特殊字符对消息进行分割;三是将消息分为消息头和消息体,在消息头中存储消息长度。第一种方案在消息封装上不够灵活,固定创建的缓冲区长度必须大于最长的消息长度,因此在写入较短的消息时会造成资源浪费。第二种方案中使用特殊字符分割消息,如果消息本身就包含了该字符,则不能正确进行解码,存在一定的局限性。本文采用第三种方案,使用消息头描述消息长度。接收方先读取固定长度的消息头,获取其中包含的消息长度,根据消息长度再次(或多次)读取相应长度的字节即读完整个消息,将包中余下的字节缓存起来作为下一个消息的前一部分。
1.2 消息结构定义
消息分为消息头和消息体两个部分。消息头固定长度,用来描述消息的类型、长度和优先级等信息。消息体可变长度,承载消息实体。具体定义如表1和表2。
1.3 继承半包解码器
根据上文对消息结构的定义,本文将业务整包消息定义为4个部分。如图1所示,HDR1中包含标识符和版本号,HDR2中包含会话ID、消息类型和消息优先级,Length和ActualContent分别表示数据帧长度和数据内容。定义MessageDecoder继承半包解码器LengthFieldBasedFrameDecoder实现粘包拆包处理。在其构造方法中设置lengthFieldOffset=8(长度字段偏移的字节数)、lengthFieldLength=4(数据帧长度)、lengthAdjustment=10(长度字段调整长度)和initialBytesToStrip=12(数据帧跳过字节数)。实际的长度字段偏移位置等于in.readerIndex()加上lengthFieldOffset,读取消息长度字段所占的4个字节表示的数值即为消息长度。通常情况下再次读取Length长度的字节就能获取完整的消息,通过lengthAdjustment和initialBytesToStrip对消息长度进行调整。endprint
2 消息序列化
在网络传输上,Java序列化的码流大小和性能一直以来都为人诟病,再加上无法跨语言进行服务调用,几乎很少有通信系统使用Java序列化[6]。XML和JSON因其平台无关性和较小的内存占用成为了大多数通信系统的首选协议,但其为了良好的可读性增大了空间开销[7-8]。本文采用Google的Protobuf框架进行POJO对象的序列化。Protobuf是一个平台无关、语言无关的结构化数据的序列化工具,相对于XML和JSON,其序列化与反序列化处理时间更短,系列化后的码流更小,更有利于网络传输和持久化[9,10]。使用Protobuf序列化,首先要根据持久化对象的系列属性编写数据描述文件proto,其中包含了对包名、类名和属性的描述。然后将编写的proto文件与protoc.exe文件放在同一目录下,进入dos执行编译命令,在指定目录生成相应的FileDescriptorProto类,FileDescriptorProto类中的FileDescriptor对象通过toByteArray()和parseFrom(byte[] array)方法实现与二进制数组之间的互相转换。
3 测试验证
本文在PC上对上述系统进行测试,电脑配置为:CPU主频2.10GHz,内存4.00G,硬盘容量500G、转速7200转。为了简单方便地配置和加载服务接口对象和服务接口实现对象,本文通过Spring容器进行统一的对象管理。测试场景为:客户端同时开启10000个线程,同一时刻向服务器发起并发计算请求,服务器在异步线程中进行两数加法计算并返回结果值给客户端,从控制台打印出请求消息、响应消息和处理耗时。重复进行10次测试的处理耗时结果如图3.1所示,Netty RPC 对10000起并发计算请求的处理耗时平均为11280毫秒,远低于传统RPC系统的处理耗时。使用JConsole监视服务器程序在Java虚拟机中的运行状态,其堆内存使用量最高为81.5Mb,相对于传统RPC,其资源占用率也比较低。
4 结束语
使用原生的Java NIO进行消息系统的开发十分困难,主要体现在线程的并发控制和TCP粘包拆包的处理上。本文基于Netty搭建了一个高性能RPC框架,其异步的线程模型能够胜任高并发,高吞吐量的消息处理。自描述的消息协议配合半包解码器有效解决了TCP粘包和拆包的问题,持久化对象传输采用Protobuf进行序列化使得传输码流更小,解析速度更快。十次万级并发计算的测试结果表明该系统无论是可靠性还是性能都十分出色。在实际的消息通信应用中,本文还存在一些可以改进和完善的地方,如Reactor主从线程模型的优化,另外还可以引入Zookeeper对RPC服务器集群进行统一协调管理和服务调度。
参考文献:
[1] Norman Maurer,Marvin Allen Wolfthal. Netty in Action[M]. Manning, 2015.
[2] Netty[EB/OL]. (2016-6-29)[2017-5-17]. http://netty.io/.
[3] Pugh W, Spacco J. MPJava: High-Performance Message Passing in Java Using Java.nio[J]. Lecture Notes in Computer Science, 2003, 2958: 323-339.
[4] 李林峰. Netty权威指南[M]. 北京: 电子工业出版社, 2015.
[5] 曹建, 劉琼, 王远. 基于数据流转发的实时数据交换系统设计[J]. 计算机应用, 2016, 36(3):596-600.
[6] 崔晓旻. 基于Netty 的高可服务消息中间件的研究与实现[D]. 成都: 电子科技大学, 2015.
[7] Breg F. CD Polychronopoulos[J]. Concurrency & Computation Practice & Experience, 2003, 15(35):173-180.
[8] 何成万, 余秋惠. JosML—一个用于实现Java对象序列化的XML模型[J]. 计算机工程, 2002, 28(1):283-284.
[9] Ayham Mhd Hailiam, Andrey Borisovich Nikolaev. Data Transmission over the Network Using PROTOBUF Protocol[J]. Automation and Control in Technical Systems, 2015, 2: 3-12.
[10] 查骏. 基于NIO的远程调用框架的设计与实现[D]. 上海: 复旦大学, 2012.endprint