大并发量 socket 通信的解决方案
笔者之前的工作主要是做java的web端开发,后因工作原因参与了一个国家级的大项目,主要负责其中底层通讯的前置机模块。几经波折,将该系统完成后,结果在第一轮的测试中就惨败退回。其根本原因就在于原设计文档的要求单“通信机”与“终端”(注一)之间的并发量要达到2W以上的连接通信,而实际运行并发量只能达到2600个相差了近十倍左右。经过代码调优、扩展JVM内存等等手段,但因基础数据相差过大,所取得的优化效果十分有限。后考虑在根本着手,只有更改整个系统的通信接口,才有可能达到设计文档上的要求。某天在某个技术QQ群里一次讨论中,有网友向我推荐了一个框架,这就是本文要介绍的主角-MINA。
注一:前置机分成了三个部分,其设计的结构图如下所示:
What is MINA
Apache MINA(Multipurpose Infrastructure for Network Applications) 是 Apache 组织一个较新的项目,它为开发高性能和高可用性的网络应用程序提供了非常便利的框架。当前发行的 MINA 版本支持基于 Java NIO 技术的 TCP/UDP 应用程序开发、串口通讯程序(只在最新的预览版中提供),MINA 所支持的功能也在进一步的扩展中
使用案例:
目前正在使用 MINA 的软件包括有:Apache Directory Project、AMQP(Advanced Message Queuing Protocol)、RED5 Server(Macromedia Flash Media RTMP)、ObjectRADIUS、Openfire 等等。
Apache直属MINA的子项目:
FTPServer,AsyncWeb,SSHD
其实在有人推荐了MINA之后,本人就上网google了一把,搜索一番下来之后才发现自己的眼界过于狭隘了,原来有相同功能的开源框架还真不少,看来以后得继续多泡论坛和QQ了(有正当理由上班泡坛子,聊QQ了,偷笑一个^_^。。。。。。)
同类框架:
Netty2:具有很好的构架,并且该框架非常有名,使用该框架的项目不少,这意味着开发团队持续更新的动力非常大。同时Netty2的文档非常齐全,并且支持JMX。Netty2的缺点就是实现代码的质量不是非常好,最大的缺点就是只支持普通的TCP。
Cindy:起源于Netty2之后,借鉴了Netty2中MessageRecognizer类的设计,在当前的版本中已经全面支持普通TCP/Secure TCP/UDP单播/UDP多播/Pipe,可以使用同一个模型进行同步/异步IO处理。Cindy目前缺点是文档相对较少以及应用的项目比较少。
Grizzl:的设计与一般的nio框架相比是比较不同的,主要不同点在于读和写都是采用blocking方式,并且使用临时selector;线程模型高度可配置。性能据说比MINA还高,但是学习曲线很高。QuickServer: http://www.quickserver.org/
Xscocket:是一个轻量级的解决方案,核心思想是屏蔽,简化nio方式的的开发,并不需要过多的学习。
对于这些框架的基本使用和基础架构,本人都经过了一番研究,发现这些框架要么重者过重,要么轻者过轻。鉴于项目的规模及时间的紧迫,再三比较之下,本人还是选择了学习曲线低,性能优异的MINA. 至于这些优点是如何体现出来的,本文以下内容将继续为您解读。
MINA快速入门
闲话少说,借用大师级的写书经验,咱们先来一个hello world。当然在这前我们还是得先做一些准备的。
预备知识:
JAVA NIO
多线程
Socket
以上知识对本文的阅读理解有一定帮助,但并非一定必需。
资源下载:
MINA2.0:暂时分为1.x和2.x两个主版本,本文只涉及2.X的版本,至于为什么只讲2.X而不讲1.X,比较冠冕堂皇的回答是----因为有一位伟人曾经说过:要以发展的眼光看世界。。。。。。而真实原因嘛。。。。。。咳咳。。。。大家都知道的。。我就不便多言了。下载地址:http://mina.apache.org/downloads.html。项目中使用的是2.03,截止本文发稿为止,最新版本为:2.04
log4j:因为其中缺少log4j的包,所以做试验的朋友还需要去下一个log4j的包。
开发工具:eclipse
Jdk:1.6x
监视测试工具:Oracle JRockit Mission Control 4.0.1 强烈推荐,简称JRMC,开发过程中,用它解决了很多性能瓶颈的问题,具体使用方法,因为篇幅所限在此不做详述,请自行查询相关文档。
Hello world的关键关键代码:
Server端的Main函数:
IoAcceptor acceptor = new NioSocketAcceptor();// 建立监控器
//acceptor.getSessionConfig().setReadBufferSize(2048);
//acceptor.getSessionConfig().setIdleTime(IdleStatus.BOTH_IDLE, 10);
acceptor.getFilterChain().addLast("codec",
New ProtocolCodecFilter(new TextLineCodecFactory(Charset.forName("UTF-8"),
LineDelimiter.WINDOWS.getValue(), LineDelimiter.WINDOWS.getValue())));//加载解/编码工厂
acceptor.setHandler(newStandBaowenHandler()); //设置处理类,其主要内容见下。
acceptor.bind(new InetSocketAddress(9998));//绑定的监听端口,可多次绑定,也可同时绑定多个。
StandBaowenHandler的关键代码
public void messageReceived(IoSession session, Object message) throws Exception { session.getService().getManagedSessions();
String baowenSrc = message.toString();// 原始报文
System.out.println(baowenSrc);
}
鉴于篇幅关系,类没有写全,更具体的内容,请大家参考mina压缩包里自带的demo。但实际上服务端需要手写的部分确实只有以上二块内容,服务端就算是写好了,由次可以看出mina的入门之快。现在我们来测试。打开cmd(这个对于同仁们来说,就不用详述了吧。。。- -!!),输入telnet 127.0.0.1 9998(这里的9998要与上面代码绑定的端口一致)随便输入一些字符,敲下回车后就会在控制台显示您所输入的信息了。
做得这一步的童鞋可以说对MINA的使用可以说已经初步入门了,是不是觉得太简单了?这也太假了吧,这就算入门了?呵呵。。。个人认为这就是MINA的强悍之一,简单的几行代码就搞定了一个服务端,要入门简直是太简单了。但任何事物都有其二面性,真要把一个东西学好,用好,还要不出错,我们需要了解的东西就太多太多了。下面我们就从MINA框架的底层说起,因为在实际应用当中,要解决一些碰到的难点及问题,对框架的整个运作体系必须要有个全面深入的了解。只有这样才能在碰到问题时,有目的,有针对性的去找到症结所在。
MINA深度了解
Mina的应用层
图1
一个设计成熟的开源框架,总是会仅可能的减少侵入性,并在整个项目中找到合适的位置,而不应对整个项目的构架设计产生过多的影响,图1就是MINA的应用层示意图。从图中和上节的DEMO中我们可以看到,MINA很好的把业务代码和底层的通信隔离了开来,我们要做的仅仅是建立好监听,然后写上我们需要实现的业务逻辑就OK了。
MINA的内部流程
图2
(1) IoService:这个接口在一个线程上负责套接字的建立,拥有自己的Selector,监听是否有连接被建立。
(2) IoProcessor:这个接口在另一个线程上负责检查是否有数据在通道上读写,也就是说它也拥有自己的Selector,这是与我们使用JAVA NIO 编码时的一个不同之处,
通常在JAVA NIO 编码中,我们都是使用一个Selector,也就是不区分IoService与IoProcessor 两个功能接口。另外,IoProcessor也是MINA框架的核心组件之一.在MINA框架启动时,会用一个线程池来专门生成线程,来负责调用注册在IoService 上的过滤器,并在过滤器链之后调用IoHandler。在默认情况IoProcessor 会用N+1个线程来轮流询问监视的端口是否有数据传送,其中n为cpu的内核个数。按一般的多线程设计概念来说,IoProcessor的线程数是越多越好,但实际上并非如此,因为大家都知道,IO的操作是非常占用资源的,所以项目中的IoProcessor的线程数应该根据实际需要来定,而这个数字可以在生成IoAcceptor对象时进行设定。Eg IoAcceptor acceptor = newNioSocketAcceptor(N);
(3.) IoFilter:这个接口定义一组拦截器,这些拦截器可以包括日志输出、黑名单过滤(参见之前代码示例的注释部份),甚至是在过滤器链中利用AOP写上权限控制(笔者负责的部分没有涉及到这权限部分,但根据笔者对框架的理解要实现这一点应该问题不大,有需要的可以自行试验)。数据的编码(write 方向)与解码(read 方向)等功能,其中数据的encode 与decode是最为重要的、也是您在使用Mina 时最主要关注的地方(笔者曾经为了decode的解码编写,重写了不下十几种实现方式才找到准确无误适合项目的解码方法)。
(4.) IoHandler:这个接口负责编写业务逻辑,也就是接收、发送数据的地方。只本文的代码实例中,可以看到真正的业务代码只有一句:System.out.println(str);真实项目中当然不可能如此简单,但如果大家把业务处理写好,并写好业务接口,真正要用时,呆需要在此处替换即可,再次见证了MINA分层的彻底。
说了这么多,以上内容也只能让大家对MINA有个基础的了解,对于MINA框架优势的认识可能还不是很多,那么下面的内容,就此展开对比讨论,以便大家对MINA的适用场景及优点有个更全面的了解。
选择MINA的理由
传统socket编程
在传统I/O中,最简单实现高并发服务器的编程方式就是对每一个客户开启一个线程。但是这种方式有如下几个弊端:
客户端上限很大的情况下不能及时响应
服务器硬件资源受限,性能也会急剧下降
受制于操作系统的限制
但这种设计方式优点还是有的:
编码简单,实现容易
一定数量的连接性能比较好。
笔者的项目开发中,最开始就是采用的这种方式,写起来方便,控制起来也方便,但遗憾的是JVM最多只能开到2K多的线程,就会报- can not create new thread的错误。
(实现结构图见下:)
图3(一对一的结构图。)
改进的Socket编程
为了解决每一个线程一个连接的模型,笔者最开始想到用多个线程处理N个用户,这样既可以保证处理多个用户的同时,线程开销降到系统的临界点。
这样的方式与前一个模型优势在于同样的多线程,但线程数固定,充分运用系统的优势性能,又不存在多余的开销。但是缺点也是显而易见的:
轮询的消耗不可避免。
一但产生io阻塞,其阻塞的时间纯属浪费。
客户数量固定的时候没有前一模型响应快
编码更加复杂。
图4(一对多)
使用MINA框架的编程
为了解决上述的矛盾,最终的解决方案只能是异步的NIO,而随着笔者对JAVA NIO的研究发现,要实现异步的NIO,并应用到实际项目中,必须对NIO有着比较深刻的了解和把握,笔者曾尝试着利用JAVA 原生 NIO接口写了一个DEMO(具体的使用方法,感兴趣的童鞋可以GOOGLE一把,你会发现用原生NIO写程序与使用MINA写程序对比起来是多么的痛苦。。。。 -_-!!),但由于笔者在这方面的底子过薄,试验结果不如人意,但要对NIO进行更为深入的学习,时间上面也不允许。直到MINA框架的映入眼帘,以上难题不再是问题。。。。以下是利用MINA的实现方式的一个简图。
图5
其中IoService接口会专门起一个线程来轮询是否有新的连接产生,一旦有连接产生则通知IoProcessor,而IoProcessor则起n+1个线程来检查连接是否有数据在上面读写(注二)。一旦有连接产生,并有数据读写,则通知decode或ENCODE,进行报文的解码或编码,将处理后的报文再交给业务类进行业务处理。其中IoProcessor是处理请求的分配,包括选择Selector,超时验证,状态记录等。总之这个类和IoService一起配合工作,封装了NIO底层的实现以及MINA框架内部的功能的支持.由于过于复杂,篇幅所限所以不作详细讲解.
结合实例,并根据以上的图文讲解,我们可以很轻易的总结出利用MINA编程的几个大致步骤:
创建一个实现了IoService接口的类
设置一个实现了IoFilter接口的过滤器(如果有需要的情况下)
设置一个IoHandler 接口实现的处理类,用于处理事件(必须)
对IoService绑定一个端口开始工作
关于MINA的大致运行流程及使用步骤,我们就暂时分析到这,具体更细节的关于一些核心类的使用方法及自定义编码器的方法,大家可以直接参考mina中所带的几个案例,写得非常详细,足够解决大家在项目中碰到的大部分问题,接下来要与大家交流的是使用MINA时非常有可能遇到的一些扩展知识。
注二:这一点请特别注意,因IoProcessor也是相当于轮询机制,这导致在报文过长时,或其它原因导致报文不能一次传输完毕的情况下,必须保存同一连接(在MINA中是以IoSession类生成的对象)下的上一次状态,这样才能截取到一个完成的报文,而这也是Decode(编码器)需要做的核心工作,新手往往就在这上面要跌跟斗。
扩展知识
这部分的内容要说起来跟MINA的使用关联不大,但实际情况是用上了MINA框架的项目基本上多多少少都会涉及到这一块,那就是多线程的编程。多线程的编程历来是JAVA编程中的重难点,很多新手碰到此类编程问题,往往都找不出原因所在,甚至一些有多年编程经验的程序员也会在这上面偶而犯下错,在这里笔者也没有能力通过很短的篇来解说多线程,那么就向大家介绍几个类及一些小常识吧,希望能给大家带来帮助。