mina是非常好的C/S架构的java服务器,这里转了一篇关于它的使用感受。

前言
MINA是Trustin Lee最新制作的Java通讯框架。通讯框架的主要作用是封装底层IO操作,提供高级的操作API。比较出名的通讯框架有C++的ACE、Python的Twisted,而Java的通讯框架还有QuickServer、Netty2、Cindy、Grizzly等。

2004年6月,Trustin Lee发布了一个通讯框架Netty2,是Java界第一个事件模型架构的通讯框架,Cindy也从中借鉴了不少思想。由于Netty2的架构不是很好,Trustin Lee在2004年底加入Apache Directory组之后,重写了整个框架,取名为MINA。MINA是一个基于Java NIO的通讯框架,Java从1.4开始引入NIO,提供了一个非阻塞、高性能的IO底层。

目前使用MINA的产品并不是很多,比较出名的就有Apache Directory、Openfire(Jive出品的一个XMPP产品)、red5(研究flash流媒体flv技术的朋友应该很清楚这个东西,adobe fms的竞争者,国内也有视频网站在使用)等等。

笔者在07年初的时候,公司新项目需要用Java实现一个Socket Server,对比了Netty2、Cindy、QuickServer和MINA。当时Netty2已经停止开发,也找不到官方网站和代码,比较了另外三个框架之后,毅然选择了当时文档比较缺乏和使用群较少的MINA,一年以来的使用经验来看,感觉还是很不错的,MINA有着清晰的架构,很方便做自定义的扩充。在1.0发布之后,官方网站充实了很多,增加了不少文档,也听到越来越多的朋友开始使用MINA。后来专门针对JDK 1.5发布了1.1的版本,使用JDK内置的concurrent代替backport-util-concurrent。目前1.0和1.1同时存在,但已经不再增加新功能,仅仅发布bug fix的版本,新功能都在2.0中实现,2.0调整了架构,性能有更大的提升,目前还在开发中。

基本特性
通过Java NIO支持TCP和UDP协议,另外还支持RS232和VM内通讯。由于MINA有清晰的架构,你也能很简单地实现一个底层网络协议。目前不支持阻塞IO,似乎还没有计划支持,当然你可以在其之上实现一个阻塞的模型,不过按照笔者的经验来说,非阻塞IO更适合Server端编程。

一个类似ServletFilter的过滤器模型。这是笔者认为MINA的精髓所在,通过引入过滤器模型,可以将一些非业务的功能独立开来,层次更清晰,很有AOP的思想,可以很方便地进行日志、协议转换、压缩等等功能,还能在运行中动态增加或去掉功能。 
可以直接使用底层的ByteBuffer,也可以使用用户定义的消息Object和编码方式。 
高度可定制的线程模型,单线程、一个线程池,或者类似SEDA的多个线程池。 
SSL支持,攻击防御和流量控制,mock测试友好,JMX支持,Spring集成,你还需要更多吗。

一个简单的例子
MINA使用非常简单,笔者以前做过一段时间传统的Java Socket开发,不过一直对Java NIO不是很理解,但是MINA很快就上手了,MINA封装了NIO繁琐的部分,使你可以更专注于业务功能实现。话不多说,让我们来看一个简单的例子,一个很常见的例子,时间服务器。

我们的实现目标是一个能响应多个客户端的请求,然后返回服务器当前的系统时间的功能。传统的Java Socket程序,我们需要每accept一个客户端连接,就创建一个新的线程来响应,这会令到系统整体负载能力有较大的限制,而且我们必须手工编写连接管理等代码。让我们来看看MINA是怎么处理的。

首先我们从官方网站下载MINA 1.1,这里我们假设JDK为1.5以上的版本,如果你使用的是JDK 1.4,请下载MINA 1.0,MINA 1.0跟1.1几乎一样,但是强烈建议使用JDK 1.5以上以获得更好的性能。

解开压缩包之后,能看见很多jar包,这里暂不介绍每个包的具体作用,可以把所有包都导入项目。值得留意的是MINA使用了一个slf4j的日志库,该日志库大有取缔common-logging之势。 这里是我们的主程序,非常简单。

首先我们需要一个IoAcceptor,这里我们选择了一个SocketAcceptor,也就是TCP协议。

然后,我们给应用加上日志过滤器和协议编码过滤器。

最后,我们把acceptor bind到本机的8123端口,并且使用TimeServerHandler来实现协议。

TimeServerHandler是我们实现具体业务功能的地方。 IoHandlerAdapter提供了7个事件方法,我们要做的事情仅仅是挑选我们需要做出响应的事件进行重载。在我这个例子了,我重载了两个方法。sessionCreated会在客户端连接的时候调用,通常我们会在这里进行一些初始化操作,我这里仅仅是打印一条信息。messageReceived就是整个Handler的中心部分,每一个从客户端发过来的消息都会转化成对该方法的调用。由于我们加入了协议编码过滤器,因此这里获得的Object msg是一个String,而不是默认的ByteBuffer(下文会详细介绍ProtocolCodecFilter)。这里我们实现了一个很简单的业务功能,如果用户输入的是quit,就断开连接,否则就输入当前时间。可以看出,IoSession封装了对当前连接的操作。

至此,我们就实现了一个时间服务器。

01.public class TimeServer {
02.  public static void main(String[] args) throws IOException {
03.    IoAcceptor acceptor = new SocketAcceptor();
04.   
05.    SocketAcceptorConfig cfg = new SocketAcceptorConfig();
06.    cfg.getFilterChain().addLast( "logger"new LoggingFilter() );
07.    cfg.getFilterChain().addLast( "codec"new ProtocolCodecFilter( new TextLineCodecFactory()));
08.   
09.    acceptor.bind( new InetSocketAddress(8123), new TimeServerHandler(), cfg);
10.    System.out.println("Time server started.");
11.  }
12.}
01.public class TimeServerHandler extends IoHandlerAdapter {
02.  public void messageReceived(IoSession session, Object msg) throws Exception {
03.    String str = (String) msg;
04.    if"quit".equalsIgnoreCase(str) ) {
05.    session.close();
06.    return;
07.  }
08.   
09.  Date date = new Date();
10.  session.write( date.toString() );
11.  System.out.println("Message written...");
12.  }
13.   
14.  public void sessionCreated(IoSession session) throws Exception {
15.  System.out.println("Session created...");
16.  }
17.}

MINA架构
这里,我借用了一张Trustin Lee在Asia 2006的ppt里面的图片来介绍MINA的架构。
Remote Peer就是客户端,而下方的框是MINA的主要结构,各个框之间的箭头代表数据流向。

大家可以对比刚刚的例子来看这个架构图,IoService就是整个MINA的入口,负责底层的IO操作,客户端发过来的消息就是由它处理。刚刚我们使用的IoAcceptor就是一个IoService,之所以抽象成IoService,是因为MINA用同样的架构来处理服务器和客户端编程,IoService的另一个子类就是IoConnector,用于客户端。不过根据笔者的使用经验,使用非阻塞的模型进行客户端编程非常的不方便,你最好寻求其他的阻塞通讯框架。

IoService把数据转化成一个一个的事件,传递给IoFilterChain。你可以加入一连串的IoFilter,进行各种功能。笔者的建议是将一些功能性的,业务不相关的代码,用IoFilter来实现,使得整个应用结构更清晰,也方便代码重用。

被IoFilter处理过的事件,发送给 IoHandler,然后我们在这里实现具体的业务逻辑。这个部分很简单,如果你有Swing的使用经验的话,你会发现它跟Swing的事件非常相像,你要做的事情,仅仅是重载你需要的方法,然后编写具体的业务功能。在这其中,最重要的一个方法就是messageReceived了。

值得留意的是一个IoSession的类,每一个IoSession实例代表这一个连接,我们需要对连接进行的任何操作都通过这个类来实现。

从IoHandler通过调用IoSession.write等方法向客户端发送的消息,会通过跟输入数据相反的次序依次传递,直至由IoService负责把数据发送给客户端。

这就已经是MINA的全部,是不是很简单。

接下来,我会详细介绍我们编写具体代码的时候主要涉及到的三个类,IoHandler、IoSession和IoFilter。
IoHandler

MINA的内部实现了一个事件模型,而IoHanlder则是所有事件最终产生响应的位置。每一个方法的名字很明确表明该事件的含义。messageReceived是接收客户端消息的事件,我们应该在这里实现业务逻辑。messageSent是服务器发送消息的事件,一般情况下我们不会使用它。sessionClosed是客户端断开连接的事件,可以在这里进行一些资源回收等操作。值得留意的是,客户端连接有两个事件,sessionCreated和sessionOpened,两者稍有不同,sessionCreated是由I/O processor线程触发的,而sessionOpened在其后,由业务线程触发的,由于MINA的I/O processor线程非常少,因此如果我们真的需要使用sessionCreated,也必须是耗时短的操作,一般情况下,我们应该把业务初始化的功能放在sessionOpened事件中。

细心的读者可能会发现,我们刚刚的例子继承的是IoHandlerAdapter,IoHandlerAdapter其实就是一个IoHanlder的空的实现,这样我们就可以不用重载不感兴趣的事件。

IoSession
IoSession是一个接口,MINA里很多的地方都使用接口,很好地体现了面向接口编程的思想。它提供了对当前连接的操作功能,还有用户定义属性的存储功能,这点非常重要。IoSession是线程安全的,也就是我们能够在多线程环境中随意操作IoSession,这点给开发带来很大的好处。我们来看看具体提供的方法,笔者列举一些比较常用和重要的方法

在这里,笔者把IoSession的方法大致分成三类

第一类,连接操作功能。
最主要的方法有两个,向客户端发送消息和断开连接。可以看的出,write接受的变量是一个Object,但是实际上应该传入什么类型呢?具体还得看你是否使用了ProtocolCodecFilter(下面会详细介绍),如果使用了ProtocolCodecFilter,那这个message将可能是一个String,或者是一个用户定义的JavaBean。默认的情况,message是一个ByteBuffer。ByteBuffer是MINA的一个类,跟java.nio.ByteBuffer类同名,MINA 2.0将会将它改成IoBuffer,以避免讨论上的误会。

另一个值得留意的是Future类,MINA是一个非阻塞的通信框架,其中一个明显的体现就是调用IoSession.write方法是不会阻塞的。用户调用了write方法之后,消息内容会发到底层等候发送,至于什么时候发出,就不得而知了。当然,实际上调用了write之后,数据几乎是立刻发出的,这得益与NIO的高性能。但是,如果我们必须确认了消息发出,然后进行某些处理,我们就需要使用Future类,以下是一个很常见的代码

通过调用future.join,程序就会阻塞,直至消息处理结束。我们还能通过future.isWritten得知消息是否成功发送。

在这里,笔者顺便说一个实际使用的发现,消息发送是会自动合并的,简单来说,如果在很短的时间里,对同一个IoSession进行了两次write操作,客户端有可能只收到一条消息,而这条消息就是服务器发出的两条消息前后接起来。这样的设计可以在高并发的时候节省网络开销,而笔者的实际使用过程中,效果也相当好。但是如果这样行为会导致客户端工作不正常,你也可以通过参数关闭它。

第二类,属性存储操作。
通常来说,我们的系统是有用户状态的,我们就需要在连接上存储用户属性,IoSession的Attribute就是这样一个功能。例如两个连接同时连入服务器,一个连接是用户A,用户ID是13,另一个连接是用户B,用户ID是14,我们就可以在用户登录成功之后,调用IoSession.setAttribute(“login_id”,13),然后在其后的操作中,通过IoSession.getAttribute(“login_id”)获得当前登录用户ID,并进行相应的操作。简单来说,就是一个类似HttpSession的功能,当然具体的实现方法不一样。

第三类,连接状态。
这里就不多说了,从方法名上我们就能知道它具体的功能。

IoFilter
过滤器是MINA的一个很重要的功能。IoFilter也是一个接口,但是相对比较复杂,这里就不列举它的方法了。简单来说IoFilter就像ServletFilter,在事件被IoHandler处理之前或之后进行一些特定的操作,但是它比ServletFilter复杂,可以处理很多种事件,除了包括IoHandler的7个事件以外,还有一些内部的事件可以进行操作。

MINA提供了一些常用的IoFilter实现,例如有LoggingFilter(日志功能)、BlacklistFilter(黑名单功能)、CompressionFilter(压缩功能)、SSLFilter(SSL支持),这些过滤器比较简单,通过阅读它们的源代码,能够更进一步理解过滤器的实现。笔者在这里要重点介绍两个过滤器,ProtocolCodecFilter和ExecutorFilter

ProtocolCodecFilter
网络传输的内容其实本质是一个二进制流,但是我们的业务功能不会,或者说不应该去直接操作二进制流。MINA默认向IoHandler传入的message是一个ByteBuffer,如果我们直接在IoHandler操作ByteBuffer,会导致大量协议分析的代码和实际的业务代码混杂在一起。最适合的做法,就是在IoFilter把ByteBuffer转换成String或者JavaBean,ProtocolCodecFilter正是这样的一个功能的过滤器。

使用ProtocolCodecFilter很简单,我们只要把ProtocolCodecFilter加入到FilterChain就可以了,但是我们需要提供一个ProtocolCodecFactory。其实ProtocolCodecFilter仅仅是实现了过滤器部分的功能,它会将最终的转换工作,交给从ProtocolCodecFactory获得的Encode和Decode。如果我们需要编写自己的ProtocolCodec,就应该从ProtocolCodecFactory入手。MINA内置了几个ProtocolCodecFactory,比较常用的就是ObjectSerializationCodecFactory和TextLineCodecFactory。

ObjectSerializationCodecFactory是Java Object序列化之后的内容直接跟ByteBuffer互相转化,比较适合两端都是Java的情况使用。TextLineCodecFactory就是String跟ByteBuffer的转化,说白了就是文本,例如你要实现一个SMTP服务器,或者POP服务器,就可以使用它。而笔者的实际使用,大多数情况都是使用

TextLineCodecFactory。
这里提及一下IoFilter的顺序问题,IoFilter是有加入顺序的,例如,先加入LoggingFilter再加入ProtocolCodecFilter,和先加入ProtocolCodecFilter再加入LoggingFilter的效果是不一样的,前者LoggingFilter写入日志的内容是ByteBuffer,而后者写入日志的是转换后具体的类,例如String。实际使用的时候,一定要处理好过滤器的顺序。

ExecutorFilter
另一个重要的过滤器就是ExecutorFilter。这里,我需要先说明一下MINA的线程工作模式,MINA默认是单线程处理所有客户端的消息,也就是说,即使你在一台8CPU的机器上面跑,可能也只用到一个CPU,另外,如果某次消息处理太耗时,就会导致其他消息等待,整体的吞吐量下降。很多朋友抱怨MINA的性能差,其实是因为他们没有加入ExecutorFilter的缘故。ExecutorFilter设计的很精巧,大家可以仔细阅读一下源代码,它会将同一个连接的消息合并起来按顺序调用,不会出现两个线程同时处理同一个连接的情况。

1.IoAcceptor acceptor = ...;
2.IoServiceConfig acceptorConfig = acceptor.getDefaultConfig();
3.acceptorConfig.setThreadModel(ThreadModel.MANUAL);
这里再次提及IoFitler的顺序问题,一般情况下,我们会将ExecutorFilter放在ProtocolCodecFilter之后,因为我们不需要多线程地执行ProtocolCodec操作,用单一线程来进行ProtocolCodec性能会比较高,而具体的业务逻辑可能还设计数据库操作,因此更适合放在不同的线程中运行。

优化指南
MINA默认配置的性能并不是很高的,部分原因是MINA目前还保留初期版本的架构,另外一个原因是因为JVM的发展。

1.IoAcceptor acceptor = new SocketAcceptor(Runtime.getRuntime().availableProcessors() + 1, Executors.newCachedThreadPool());
首先我们关闭默认的ThreadModel设置 ThreadModel是一个很简单的线程实现,用于IoService。但是它实在太弱,以至于在并发环境产生大量问题。在MINA 2.0中,ThreadModel直接被取消。你应该使用ExecutorFilter来实现线程。
1.acceptor.getDefaultConfig().getFilterChain().addLast("threadPool"new ExecutorFilter(Executors.newCachedThreadPool());

然后我们增加I/O处理线程
每一个Acceptor/Connector都使用一个线程来处理连接,然后把连接发送给I/O processor进行读写操作,我们只可以修改I/O processor使用的线程数,用以下代码设置 当然是要将ExecutorFilter加入,上文已经很详细地描述了 笔者在开发过程中,多次遇到OutOfMemoryError,经过研究之后才发现原因。MINA默认是使用direct memory实现ByteBuffer池的方案(以下简称direct buffer),通过JNI在内存开辟一段空间来使用,该方案在早期的MINA版本中是一个非常好的特性,那是因为MINA开发初期,JVM并没有现在的强大,带有池效果的direct buffer性能比较好。但是当我们使用-Xms -Xmx等指令增加JVM可使用的内存,那仅仅增加了堆的内存空间,而direct memory的空间并没有增加,导致MINA实际使用的时候经常出现OutOfMemoryError。如果你的确想使用direct memory,可以通过-XX:MaxDirectMemorySize选项来设置。不过笔者不建议这样做,因为最新的测试表明,在现代的JVM里面,direct memory比堆的表现更差。这里可能有读者会觉得奇怪,为什么不用池,而要用堆呢,而且还需要gc。那是因为现在的JVM gc能力已经很强了,而且在并发环境里面,pool的同步也是一个性能的问题。我们可以通过这样的代码进行设置 MINA 2.0已经默认把直接内存分配改成堆,为了提供最好的性能和稳定性。 

1.ByteBuffer.setUseDirectBuffers(false);
2.ByteBuffer.setAllocator(new SimpleByteBufferAllocator());
最后一条优化技巧就是,把你的应用部署在Linux上,并且打开Java NIO使用Linux epoll的功能。可能你还没听过epoll,但是你应该听过Lighttpd、Nginx、Squid等,得益于epoll,它们提供很高的网络性能,还占用非常少的系统资源。JDK6已经默认把epoll配置打开,因此笔者建议把你的应用部署在JDK6上面,也同时因为JDK6还有别的优化特性。如果你的应用必须部署在JDK5上,你也可以通过参数把epoll支持打开。

基于MINA构建简单高性能的NIO应用的更多相关文章

  1. 基于Protobuf的分布式高性能RPC框架——Navi-Pbrpc

    基于Protobuf的分布式高性能RPC框架——Navi-Pbrpc 二月 8, 2016 1 简介 Navi-pbrpc框架是一个高性能的远程调用RPC框架,使用netty4技术提供非阻塞.异步.全 ...

  2. 一个基于mysql构建的队列表

    通常大家都会使用redis作为应用的任务队列表,redis的List结构,在一段进行任务的插入,在另一端进行任务的提取. 任务的插入 $redis->lPush("key:task:l ...

  3. 基于Raft构建弹性伸缩的存储系统的一些实践

    基于Raft构建弹性伸缩的存储系统的一些实践 原创 2016-07-18 黄东旭 聊聊架构 最近几年来,越来越多的文章介绍了 Raft 或者 Paxos 这样的分布式一致性算法,但主要集中在算法细节和 ...

  4. Spring 3整合Quartz 1实现定时任务一:常规整合(基于maven构建)

    Spring配置Quartz例子(基于maven构建) 在Spring中使用Quartz有两种方式实现:第一种是任务类继承QuartzJobBean,第二种则是在配置文件里定义任务类和要执行的方法,类 ...

  5. 基于.NET MVC的高性能IOC插件化架构

    基于.NET MVC的高性能IOC插件化架构 最近闲下来,整理了下最近写的代码,先写写架构,后面再分享几个我自己写的插件 最近经过反复对比,IOC框架选择了Autofac,原因很简单,性能出众,这篇博 ...

  6. 微服务架构 - 基于Harbor构建本地镜像仓库

    之前写过<搭建docker本地镜像仓库并提供权限校验及UI界面>文章,然后有同仁评论道这样做太复杂了,如果Harbor来搭建会更简单同时功能也更强大.于是抽时间研究了基于Harbor构建本 ...

  7. 基于Kubernetes 构建.NET Core 的技术体系

    很多公司技术支持岗位的工作,如配置域名,部署环境,修改复位配置,服务重启,扩容缩容,梳理和完善监控,根据开发的需要查找日志等工作,需要和开发进行大量的沟通,如什么是外网域名,什么是内网域名.A nam ...

  8. 基于springboot构建dubbo的入门demo

    之前记录了构建dubbo入门demo所需的环境以及基于普通maven项目构建dubbo的入门案例,今天记录在这些的基础上基于springboot来构建dubbo的入门demo:众所周知,springb ...

  9. 在TensorFlow中基于lstm构建分词系统笔记

    在TensorFlow中基于lstm构建分词系统笔记(一) https://www.jianshu.com/p/ccb805b9f014 前言 我打算基于lstm构建一个分词系统,通过这个例子来学习下 ...

随机推荐

  1. 没有VisualStudio也要HelloWorld

    前言 在博客园看到Artech的通过3个Hello World应用来了解ASP.NET 5应用是如何运行的(1)这篇文章,于是想跟着教程学习一下.说来惭愧,这篇文章发布于2014年12月,我在2016 ...

  2. 随机生成字符串-php-js

    js <script language="javascript"> function randomString(len) { len = len || 32; var ...

  3. js判断访问的当前设备是手机还是电脑

    function browserRedirect() { var sUserAgent = navigator.userAgent.toLowerCase(); var bIsIpad = sUser ...

  4. WPF实现TextBox水印效果

    在日常项目中,一个TextBox需要输入用户名,我们通常的做法是先用一个TextBlock来说明,例如下面的截图: 今天将使用另外一种方式来展示,使用水印的方式.请参考下面的代码: <Windo ...

  5. CodeIgniter中驱动器的使用方法

    驱动器“Drivers”是CodeIgniter框架从2.0版本开始加入的新特性.正如中文版译者所言: 笔者看了这三篇英文参考,加上自己的一些理解,对官方文档关于驱动器的这一部分进行了一些补充. 1. ...

  6. c语言的字符串操作(比较详细)

    1)字符串操作 strcpy(p, p1) 复制字符串 strncpy(p, p1, n) 复制指定长度字符串 strcat(p, p1) 附加字符串 strncat(p, p1, n) 附加指定长度 ...

  7. Codeforces Round #369 (Div. 2) C. Coloring Trees DP

    C. Coloring Trees   ZS the Coder and Chris the Baboon has arrived at Udayland! They walked in the pa ...

  8. java基本数据类型及相互间的转换

    1.首先复习一下java的基本数据类型,见下图 2.比较他们的字节数 备注:1字节(Byte)=8位(Bit) 3.转换中的知识点 *java中整数类型默认的int类型:小数类型默认的double: ...

  9. loadrunner中变量和参数之间的转化实例

     1.变量转换成参数值的两种方法: 方法一: char *test="Agoly"; lr_save_string(test,"testPa");   lr_e ...

  10. 从零开始---控制台用c写俄罗斯方块游戏(1)

    从零开始---控制台用c写俄罗斯方块游戏(1) 很少写博文,一来自身知识有限,二来自己知道,已经有很多这样的博文了,三就是因为懒,文笔也一般,四来刚出来工作,时间也不多 之所以写这篇博文,是因为应群里 ...