最近用两周左右的时间阅读了一下apache mina的源码,有一些体会,现总结如下:
一、什么是MINA
MINA是Multipurpose Infrastructure Networked Applications的首字符缩写,直译过来是“多目的基础设施网络应用程序”,它是一个Apache的顶级开源项目,目的是为了帮助开发人员简化网络程序的开发,把注意力集中在业务逻辑上。它的整体架构如下:
从架构图上,我们可以看到各个组件的功能如下:
IoService: 负责处理和其它节点的通信,并且把一个socket抽象成一个IoSession.
IoFilterChain: 一组过滤器组成的过滤器链,比如codec filter ,负责数据的编码和解码,编码对应写操作,解码对应读操作。
IoHandler:负责业务逻辑处理。
我们一般只要关心过滤器的编写和IoHandler的编写。
二、MINA在设计上有哪些特点:
1、IO操作的异步化:
所谓IO操作的异步化,其实底层是通过java的wait/notifyAll机制来实现的。示意图如下:
源代码可以参考DefaultIoFuture及其子类DefaultReadFuture和DefaultWriteFuture.
2、基于事件的处理模型:
主要是使用了java NIO 的 selector来进行就绪socket的轮询,包括OP_ACCEPT
OP_READ和OP_WRITE三种事件的监听,OP_ACCEPT监听的是新的socket 连接请求
OP_READ和OP_WRITE监听的是socket的读写事件,后面第三节会有详细的描述。
3、组件模块化:
从前面的mina架构来看,mina对整个网络处理模型做了很好的抽象,方便用户根据实际需要进行灵活的扩展。比如服务器组件IoAcceptor就有SocketAcceptor和DatagramAcceptor两个子接口,分别对应tcp和udp协议。 IoFilter接口方便用户方便的实现应用层协议的编解码器的实现。 IoHandler方便应用扩展业务处理逻辑。
三、基于JavaNIO进行读写的详细分析
下面是一个时间服务器的demo,启动服务器以后, telnet到本地服务器的9123端口,随意输入一行,telnet会回显当前的系统时间。
// Create the acceptor
IoAcceptor acceptor = new NioSocketAcceptor();
// Add two filters : a logger and a codec
acceptor.getFilterChain().addLast( "logger", new LoggingFilter() );
acceptor.getFilterChain().addLast( "codec", new ProtocolCodecFilter( new TextLineCodecFactory( Charset.forName( "UTF-8" ))));
// Attach the business logic to the server
acceptor.setHandler( new TimeServerHandler() );
// Configurate the buffer size and the iddle time
acceptor.getSessionConfig().setReadBufferSize( 2048 );
acceptor.getSessionConfig().setIdleTime( IdleStatus.BOTH_IDLE, 10 );
// And bind ;
acceptor.bind( new InetSocketAddress(9123) );
下面我们用一组图来说明上面这个Time server的运作机制。
1、建立服务器套接字,等待客户端的连接请求并响应客户端的连接请求:
对照上面的示意图,我们对建立服务器套接字并且处理客户端连接请求的过程进行简单描述:
1)创建一个异步的AcceptorOperationFuture,写入一个队列。
2)Acceptor线程完成两件事情:套接字绑定和响应客户端连接请求,这两件事情是在一轮循环中处理的 。
先说套接字绑定的处理:
step1: 创建一组ServerSocketChannel,一个ServerSocketChannel对应一个服务器套接字 。
step2: 把ServerSocketChannel注册到Selector,并且关心OP_ACCEPT事件(也就是关心客户端的连接请求) 。
响应客户端连接请求:
step1: selector进行select操作,拿到一组OP_ACCEPT事件就绪的SelectionKey,通过SelectionKey获取到ServerSocketChannel;
step2: 每一个ServerSocketChannel 接收一个SocketChannel ,把SocketChannel, IoHandler,IoFilterChain,IoProcessor包装在一起成为一个NioSession.
step3: 把NioSession交给一个IoProcessor的池,进行处理。IoProcessor池会分配一个IoProcessor(分配算法:ioSession的id % IoProcessor池的大小 = IoProcessor的数组下标) 处理这个NioSession ,这里IoProcessor池是设计模式中复合模式(composition pattern)的使用。所谓复合模式,就是实现接口的同时包含了一组接口。
从IoProcessor池这种处理方式我们可以看到,一个IoProcessor处理了若干个套接字连接,这些套接字连接对应不同的服务器端口。
2、接下来是NioProcessor对NioSocketSession的处理, 整个处理过程如下:
1) NioProcessor把NioSocketSession加入一个自己的一个队列NewSessionsQueue。
2)NioProcessor启动的一个Processor线程完成两件事情: 为新到来的NioSocketSession准备读; 处理socketchannel的读写。这两件事情是在一轮循环中处理的 。
先说为NioSocketSession准备读,步骤如下:
step1:先从队列NewSessionsQueue取NioSocketSession;
step2: 把NioSocketSession的socketChannel注册到NioProcessor的selector上,并且关心的是OP_READ,注册完以后,selectionKey被写入NioSocketSession属性,然后为NioSocketSession准备一个完成的过滤器链,这里需要注意的是在前面连接请求被Accept的时候就已经创建了一个过滤器链,包含了HeadFilter和TailFilter,这里再把业务自己定义的Filter加到HeadFilter和TailFilter中间。
下面是示意图:
socketChannel准备读写以后,接下来就是读写操作了,示意图如下:
step1: 利用selector对OP_READ和OP_WRITE也就是读写就绪的Niosocketsession进行轮询,得到一组就绪的SelectionKey,利用这组SelectionKey得到NioSession的迭代器 。
step2: 对NioSession的迭代器进行遍历,写就绪的NioSession进入一个队列flushingSessions,一次性批量处理,提高写的效率。
读就绪的NioSession则立即处理。
读的处理 :把IoBuffer(是对java.nio.ByteBuffer的一个封装,支持内存容量的自动扩充和减少)交给一个过滤器链进行过滤,最后由一个TailFilter把经过过滤器过滤以后得到message交给IoHandler进行业务处理。
写的处理: 把一个WriteRequest(里面包含了一个异步的写请求WriteFuture) ,交给过滤器链进行反向过滤,最后由HeadFilter把WriteRequest放到NioSession的WriteRequestQueue ,然后把NioSession加入到一个flushing sessions队列,由processor thread批量处理这个flushing sessions队列。每次取一个niosession,对里面的WriteRequest队列进行遍历,每个WriteRequest对应一次ByteBuffer的写, 一直写到WriteRequest队列为空或者超过niosession的最大写字节数。值得注意的是,这里每个niosession之所以有最大写字节数,是为了保证所谓的读写公平,即不要在单个niosession的写操作上耗费过多的时间,而影响了其它niosession的写和读(作者根据经验,在兼顾效率和公平性的基础上,把这个单个niosession的最大写字节数设为了最大读缓冲区的1.5倍)。
四、应用层协议的编码器的设计:
下面是Mina自带的一个sumup的例子,这个例子完成的是客户端发送一组整型变量给服务器,服务器求和以后,返回给客户端,客户端最后输出这些整型变量的和。 示意图如下:
client先后发送1,2,3给服务器,服务器返回1,3,6 三个求和结果给client.
在这个例子中,消息的格式如下:
从上面的示意图可以看到,这个应用的消息是固定字节数的,而一般来说,消息的包头中存放了消息体的字节长度等描述信息, 解码器需要根据消息包头的描述信息进行解码。(后面结合HSF进行进一步的说明)。那么客户端和服务器是如何进行编码和解码的呢?
消息的编码比较简单,根据前面介绍的消息的格式,把消息写入一个iobuffer(它是对java.nio.ByteBuffer的一个增强,支持内存的自动扩展和减少) 即可。
消息的解码相对复杂,示意图如下。 因为TCP是流式传输协议,没有信息边界,所以解码器的一个工作机制可以简要描述如下:
1、读消息头。
2、根据消息头里面对消息体的定义,一个一个读消息体的元素。
3、在读消息体元素的时候,如果nio bytebuffer中的字节数不够,则需要等待bytebuffer的新一轮读取(粘包)
4、而bytebuffer一次读到的字节数组,可能包含了两个消息的内容,那么第一个消息体只需要读到下一个消息的头(不包括)就可以截止(半包) 。
接下来要写的东西:
3、对javaNIO的使用做一个梳理。
4、事件处理框架:epoll, kequeue等。
5、从mina源码中学习到了什么。