MINA是Trustin Lee最新制作的Java通讯框架。通讯框架的主要作用是封装底层IO操作,提供高级的操作API。比较出名的通讯框架有C++的ACE、Python的Twisted,而Java的通讯框架还有QuickServer、Netty2、Cindy、Grizzly等。
通过Java NIO支持TCP和UDP协议,另外还支持RS232和VM内通讯。由于MINA有清晰的架构,你也能很简单地实现一个底层网络协议。目前不支持阻塞IO,似乎还没有计划支持,当然你可以在其之上实现一个阻塞的模型,不过按照笔者的经验来说,非阻塞IO更适合Server端编程。
可以直接使用底层的ByteBuffer,也可以使用用户定义的消息Object和编码方式。
高度可定制的线程模型,单线程、一个线程池,或者类似SEDA的多个线程池。
SSL支持,攻击防御和流量控制,mock测试友好,JMX支持,Spring集成,你还需要更多吗。
MINA使用非常简单,笔者以前做过一段时间传统的Java Socket开发,不过一直对Java NIO不是很理解,但是MINA很快就上手了,MINA封装了NIO繁琐的部分,使你可以更专注于业务功能实现。话不多说,让我们来看一个简单的例子,一个很常见的例子,时间服务器。
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的主要结构,各个框之间的箭头代表数据流向。
IoHandler
IoSession是一个接口,MINA里很多的地方都使用接口,很好地体现了面向接口编程的思想。它提供了对当前连接的操作功能,还有用户定义属性的存储功能,这点非常重要。IoSession是线程安全的,也就是我们能够在多线程环境中随意操作IoSession,这点给开发带来很大的好处。我们来看看具体提供的方法,笔者列举一些比较常用和重要的方法
最主要的方法有两个,向客户端发送消息和断开连接。可以看的出,write接受的变量是一个Object,但是实际上应该传入什么类型呢?具体还得看你是否使用了ProtocolCodecFilter(下面会详细介绍),如果使用了ProtocolCodecFilter,那这个message将可能是一个String,或者是一个用户定义的JavaBean。默认的情况,message是一个ByteBuffer。ByteBuffer是MINA的一个类,跟java.nio.ByteBuffer类同名,MINA 2.0将会将它改成IoBuffer,以避免讨论上的误会。
通常来说,我们的系统是有用户状态的,我们就需要在连接上存储用户属性,IoSession的Attribute就是这样一个功能。例如两个连接同时连入服务器,一个连接是用户A,用户ID是13,另一个连接是用户B,用户ID是14,我们就可以在用户登录成功之后,调用IoSession.setAttribute(“login_id”,13),然后在其后的操作中,通过IoSession.getAttribute(“login_id”)获得当前登录用户ID,并进行相应的操作。简单来说,就是一个类似HttpSession的功能,当然具体的实现方法不一样。
这里就不多说了,从方法名上我们就能知道它具体的功能。
过滤器是MINA的一个很重要的功能。IoFilter也是一个接口,但是相对比较复杂,这里就不列举它的方法了。简单来说IoFilter就像ServletFilter,在事件被IoHandler处理之前或之后进行一些特定的操作,但是它比ServletFilter复杂,可以处理很多种事件,除了包括IoHandler的7个事件以外,还有一些内部的事件可以进行操作。
网络传输的内容其实本质是一个二进制流,但是我们的业务功能不会,或者说不应该去直接操作二进制流。MINA默认向IoHandler传入的message是一个ByteBuffer,如果我们直接在IoHandler操作ByteBuffer,会导致大量协议分析的代码和实际的业务代码混杂在一起。最适合的做法,就是在IoFilter把ByteBuffer转换成String或者JavaBean,ProtocolCodecFilter正是这样的一个功能的过滤器。
这里提及一下IoFilter的顺序问题,IoFilter是有加入顺序的,例如,先加入LoggingFilter再加入ProtocolCodecFilter,和先加入ProtocolCodecFilter再加入LoggingFilter的效果是不一样的,前者LoggingFilter写入日志的内容是ByteBuffer,而后者写入日志的是转换后具体的类,例如String。实际使用的时候,一定要处理好过滤器的顺序。
另一个重要的过滤器就是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的发展。
首先我们关闭默认的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支持打开。