Mina框架研究

Apache MINA(Multipurpose Infrastructure for Network Applications) 是 Apache 组织一个较新的项目,它为开发高性能和高可用性的网络应用程序提供了非常便利的框架。

这个框架的优点:

– 异步 
– 无阻塞 
– 事件驱动 
– 支持TCP, UDP, APR, 串口… 
– 通过 过滤器(Filters)实现扩展性 
– 同时提供协议框架

 

总体框架

之前的一个项目用到了MINA,最近想再系统的整理一下,主要参考MINA 2.0 User Guide

基于MINA框架的应用程序架构应该是这样的:

Mina框架研究_第1张图片

底层是基于JAVA的NIO 1.0实现的;

其核心部分架构是这样的:

Mina框架研究_第2张图片

 

内部可以分为3 个层次:

I/O Service - 执行实际的I / O,可以选择现成的Services如 (*Acceptor),也可以自己写。

I/O Filter Chain - 这是一个由多个过滤器组成的过滤器链,在这个环节将字节数据转换到特定的数据结构中(Filters/Transforms bytes into desired Data Structures and vice-versa) 
I/O Handler - 实际的业务逻辑部分

 

Server端应用

对socket通信来说,使用比较广泛的是基于Server端的应用,尤其是并发规模达到一定程度后,颇具挑战性。那么我们来看一下,基于MINA框架的Server端应用:

1、IOAcceptor 监听指定的端口,处理新的网络连接;一旦一个新的连接到达后,IOAcceptor 就产生一个session,后续所有从这个IP和端口发送过来的请求就将通过这个Session被处理。

2、Session创建后,后续所有的数据包都被人到过滤器链中,通过过滤器将原始的字节码转变成高层的对象,这个环节PacketEncoder/Decoder就十分有用。

3、最后数据包或对象被传送给Handler做业务逻辑处理;

Mina框架研究_第3张图片

Main.java:

public class Main {
private static final int PORT = 9123;
/**
* @param args the command line arguments
*/
public static void main(String[] args) throws IOException {

IoAcceptor acceptor = new NioSocketAcceptor();
acceptor.getFilterChain().addLast( "logger", new LoggingFilter() );
acceptor.getFilterChain().addLast( "codec", new ProtocolCodecFilter( new TextLineCodecFactory( Charset.forName( "UTF-8" ))));

acceptor.setHandler( new TimeServerHandler() );

acceptor.getSessionConfig().setReadBufferSize( 2048 );
acceptor.getSessionConfig().setIdleTime( IdleStatus.BOTH_IDLE, 10 );
acceptor.bind(new InetSocketAddress(PORT));

}

}

1、创建IoAcceptor;

2、加入日志记录和解码的过滤器,其中日志过滤器用SL4J库记录信息,而编码过滤器则解码所有收到的信息。使用 new TextLineCodecFactory() 发送的信息迕行编码,返是MINA自带的,功能有限,只能处理文本戒者String类型。

3、设置ServerHandler,这里是一个自定义的Handler:TimeServerHandler;

4、设置Session的对应的I/O processor 读缓存区大小2048;通常这个参数不需要设置;

5、设置空闲时间,这里的BOTH_IDLEEADER_IDLE 和 WRITER_IDLE. 都为10秒;

6、绑定监听端口9123;

TimeServerHandler.java:

/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/

package minatest1;

/**
*
* @author THINKPAD
*/

import java.util.Date;

import org.apache.mina.core.session.IdleStatus;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;

public class TimeServerHandler extends IoHandlerAdapter
{
@Override
public void exceptionCaught( IoSession session, Throwable cause ) throws Exception
{
cause.printStackTrace();
}

@Override
public void messageReceived( IoSession session, Object message ) throws Exception
{
String str = message.toString();
if( str.trim().equalsIgnoreCase("quit") ) {
session.close();
return;
}

Date date = new Date();
session.write( date.toString() );
System.out.println("Message written...");
}

@Override
public void sessionIdle( IoSession session, IdleStatus status ) throws Exception
{
System.out.println( "IDLE " + session.getIdleCount( status ));
}
}

这里主要有一下几个主要的方法:

messageReceived(…),对接收到的消息(已经解码)迕行下一步处理,这里对收到的字符串进行判断,如果是”quit”则断开连接;否则输出当前时间的字符串格式;

exceptionCaught(…),自定义异常处理, 要不然异常会被“吃掉”;

sessionIdle,当Session处于IDLE状态的时候,输出空闲状态次数;

 

 

测试,输入:telnet 127.0.0.1 9123,随便输入一串字符串,显示当前的时间:

Mina框架研究_第4张图片

 

IO Service



IoService是一个接口,有两种实现:IoAcceptor和IoConnector;其中IoAcceptor是针对Server端的实现,IoConnector是针对Client端的实现;IoService的职责包括:

1、监听器管理

2、IoHandler

3、IoSession管理

4、FilterChain管理

5、Statistics管理

Mina框架研究_第5张图片

 

IoAcceptor

主要用于创建新的连接。MINA提供了多种实现,所以几乎不需要我们自己再去实现:

NioSocketAcceptor:无阻塞的Socket 传输Acceptor,针对TCP

NioDatagramAcceptor : 无阻塞的Socket 传输Acceptor,针对UDP

AprSocketAcceptor : 阻塞的Socket 传输Acceptor,基于 APR

VmPipeSocketAcceptor : the in-VM Acceptor

 

IoConnector

针对Client端的Socket连接,有多种实现:

NioSocketConnector : 无阻塞的Socket 传输Connector,针对TCP 
NioDatagramConnector : 无阻塞的Socket 传输Connector,针对UDP 
AprSocketConnector : 阻塞的Socket 传输Connector,基于 APR 

ProxyConnector : 一个支持代理服务的 Connector ,通过截取连接的请求,并将终端指向代理设置的地址。

SerialConnector : 针对串口传输的Connector

VmPipeConnector : the in-VM * Connector*

 

Session

任何时候只要有新的连接到来,都会生成一个Session对象,并且一致保存在内存中,只到连接断开;

Session有一系列状态,如下:

Connected : session被创建,并有效 
Idle : session至少在一个空闲周期(见配置)内没有处理过任何请求 
Idle for read : 在一个空闲周期内没有做实际的读操作

Idle for write : 在一个空闲周期内没有做实际的写操作

Idle for both : 在一个空闲周期内没有做实际的读和写操作 
Closing :session正在被关闭

Closed : session已经被关闭

Mina框架研究_第6张图片

 

IoBuffer

IoBuffer是MINA内部使用的一个byte buffer,MINA并没有直接使用NIO 的ByteBuffer。不过IoBuffer 是对 ByteBuffer 的一个封装。IoBuffer 中的很多方法都是对 ByteBuffer 的直接继承。只是对 ByteBuffer 添加了一些扩展了更加实用的方法。

基本用法

由于IoBuffer是对Nio的ByteBuffer 的封装,所以基本概念还是相同的,下面简单介绍一下:

1、capacity:该属性描述这个缓冲区最多能缓冲多少个元素,也是Buffer最大存储元素数,这个值是在创建Buffer的时候指定的,且不能修改。

2、Limit:在从Buffer中向Channel中写数据时,limit变量指示了还剩多少数据可以读取,在从Channel中读取数据到Buffer中时,limit变量指示了还剩多少空间可供存放数据。position正常情况下小于或者等于limit。

3、Position:Buffer实际上也就是个array。当你从Channel中读数据时,你把从Channel中读出来的数据放进底层array,position变量用来跟踪截止目前为止已经写了多少数据。更精确的讲,它指示如果下次写Buffer时数据应该进入array的哪个位置。因此如果已经从Channel中读出了3个字节,Buffer的position会被置为3,指向array中第四个位置。

4、Mark:一个可以记忆的Position位置的值,在调用reset()方法时会将缓冲区的Position重置为该索引,并非总是需要定义Mark,但是在定义Mark时,不能将其定义为负数,并且不能让它大于Position,如果定义了Mark,则在该Position或Limit调整为小于该Mark值时,该Mark将被丢弃。

下面通过一个例子来说明:

i、初始状态下:

此时position为0,limit和capacity都被设为9;

Mina框架研究_第7张图片

 

ii、从Channel中读入4个字节数据到Buffer,这时position指向4(第5个):

Mina框架研究_第8张图片

 

iii、在做写操作之前,我们必须调用一次flip()方法,这个方法做了两件重要的事情: 
1. 将limit设置到当前的position处。 
2. 设置position为0。

Mina框架研究_第9张图片

 

iiii、执行写操作后;

Mina框架研究_第10张图片

iv、执行clear后,position设为0,limit设为capition,mark则丢弃;

Mina框架研究_第11张图片

 

 

因为IoBuffer是一个抽象类,不能直接实例化,所有使用的时候需要调用allocate方法来进行内存分配;

allocate有两种定义:

   1: // Allocates a new buffer with a specific size, defining its type (direct or heap)
   2: public static IoBuffer allocate(int capacity, boolean direct)
   3:  
   4: // Allocates a new buffer with a specific size
   5: public static IoBuffer allocate(int capacity)

这里:

capacity:buffer的大小;

direct:如果为true,则得到direct buffer,如果为false,则得到heap buffer

direct buffer和heap buffer的区别分析:

Direct Buffer不是分配在堆上的,它不被GC直接管理(但Direct Buffer的JAVA对象是归GC管理的,只要GC回收了它的JAVA对象,操作系统才会释放Direct Buffer所申请的空间),它似乎给人感觉是“内核缓冲区(buffer in kernel)”。Heap Buffer则是分配在堆上的,或者我们可以简单理解为Heap Buffer就是byte[]数组的一种封装形式。当我们把一个Heap Buffer写入Channel的时候,实际上底层实现会先构建一个临时的Direct Buffer,然后把Heap Buffer的内容复制到这个临时的Direct Buffer上,再把这个Direct Buffer写出去。因此把一个Direct Buffer写入一个Channel的速度要比把一个Heap Buffer写入一个Channel的速度要快。但是Direct Buffer创建和销毁的代价很高,所以要用在尽可能重用的地方。

public static IoBuffer allocate(int capacity)的用法:
   1: // 设置Allocates分配的默认类型,这里设为heap buffer.
   2:  IoBuffer.setUseDirectBuffer(false);
   3:  // 返回一个新的heap buffer.
   4:  IoBuffer buf = IoBuffer.allocate(1024);

 

IoBuffer允许生成一个自动扩展的buffer(这也是没有选择使用NIO的ByteBuffer的原因之一);通过设置AutoExpand属性即可:

   1: IoBuffer buffer = IoBuffer.allocate(8);
   2: buffer.setAutoExpand(true);
   3:  
   4: buffer.putString("12345678", encoder);
   5:        
   6: // Add more to this buffer
   7: buffer.put((byte)10);


Filters

IoFilter 是MINA框架中一个核心的部分,如下图,对间于IoService和IoHandle之间,用于过滤所有的I/O事件和请求,其扮演的角色就像J2EE中的Servlet。

Mina框架研究_第12张图片

MINA提供不少现成的过滤器:

1、LoggingFilter :日志过滤器,用于记录所有的事件和请求日志.

2、ProtocolCodecFilter:规约解析过滤器,用来将所有收到的ByteBuffer内容转换为POJO消息(对象),实现往来报文的编码和解码;

3、CompressionFilter:压缩过滤器;

4、SSLFilter

 

ProtocolCodecFilter

相对比较常用的是ProtocolCodecFilter,下面主要介绍一下ProtocolCodecFilter的使用:

还是以官方帮助文档中例子来说明(http://mina.apache.org/chapter-11-codec-filter.html)

这是一个图片服务器程序,请求图片的下行报文格式:

image

 

width: 请求图片的宽度 
height: 请求图片的高度 
numchars: 生成的字节个数

 

服务端返回两张图片:

image

length1: 图片1的大小 
image1: 图片1(PNG格式) 
length2: 图片2的大小

image2: 图片2(PNG格式)

 

现在如果想通过MINA框架来实现基于这一简单规约的编解码通信功能,应该如何做呢?

   1: public class ImageServer {
   2:     public static final int PORT = 33789;
   3:  
   4:     public static void main(String[] args) throws IOException {
   5:         ImageServerIoHandler handler = new ImageServerIoHandler();
   6:         NioSocketAcceptor acceptor = new NioSocketAcceptor();
   7:         acceptor.getFilterChain().addLast("protocol", new ProtocolCodecFilter(new ImageCodecFactory(false)));
   8:         acceptor.setLocalAddress(new InetSocketAddress(PORT));
   9:         acceptor.setHandler(handler);
  10:         acceptor.bind();
  11:         System.out.println("server is listenig at port " + PORT);
  12:     }
  13: }

这里先来看和Filter有关的部分:

acceptor.getFilterChain().addLast("protocol", new ProtocolCodecFilter(new ImageCodecFactory(false)));
在acceptor的Filter链中加入我们自己的过滤器,一个ProtocolCodecFilter的实现,这里的ImageCodecFactory又是什么呢?
   1: public class ImageCodecFactory implements ProtocolCodecFactory {
   2:     private ProtocolEncoder encoder;
   3:     private ProtocolDecoder decoder;
   4:  
   5:     public ImageCodecFactory(boolean client) {
   6:         if (client) {
   7:             encoder = new ImageRequestEncoder();
   8:             decoder = new ImageResponseDecoder();
   9:         } else {
  10:             encoder = new ImageResponseEncoder();
  11:             decoder = new ImageRequestDecoder();
  12:         }
  13:     }
  14:  
  15:     public ProtocolEncoder getEncoder(IoSession ioSession) throws Exception {
  16:         return encoder;
  17:     }
  18:  
  19:     public ProtocolDecoder getDecoder(IoSession ioSession) throws Exception {
  20:         return decoder;
  21:     }
  22: }
ImageCodecFactory是对接口ProtocolCodecFactory 的实现,是用来构建Filter的。ProtocolCodecFactory 接口只有两个方法:
   1: public interface ProtocolCodecFactory {
   2:  
   3:     public ProtocolEncoder getEncoder(IoSession is) throws Exception;
   4:  
   5:     public ProtocolDecoder getDecoder(IoSession is) throws Exception;
   6: }
   7:  

没错,这个规约编解码工厂需要装配一个编码器(Encoder)和一个解码器(Decoder):

编码器:

   1: public class ImageRequestEncoder implements ProtocolEncoder {
   2:  
   3:     public void encode(IoSession session, Object message, ProtocolEncoderOutput out) throws Exception {
   4:         ImageRequest request = (ImageRequest) message;
   5:         IoBuffer buffer = IoBuffer.allocate(12, false);
   6:         buffer.putInt(request.getWidth());
   7:         buffer.putInt(request.getHeight());
   8:         buffer.putInt(request.getNumberOfCharacters());
   9:         buffer.flip();
  10:         out.write(buffer);
  11:     }
  12:  
  13:     public void dispose(IoSession session) throws Exception {
  14:         // nothing to dispose
  15:     }
  16: }

解码器:

   1: public class ImageRequestDecoder extends CumulativeProtocolDecoder {
   2:  
   3:     protected boolean doDecode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception {
   4:         if (in.remaining() >= 12) {
   5:             int width = in.getInt();
   6:             int height = in.getInt();
   7:             int numberOfCharachters = in.getInt();
   8:             ImageRequest request = new ImageRequest(width, height, numberOfCharachters);
   9:             out.write(request);
  10:             return true;
  11:         } else {
  12:             return false;
  13:         }
  14:     }
  15: }

这里解码器不是直接从ProtocolDecoder继承的,为什么呢?因为实际中接受的一个数据包并不能保证包含完整的一帧报文,就是所谓的断帧的情况,如果用CumulativeProtocolDecoder,就不需要自己处理这种情况了,MINA框架会将未解码的数据保留,等下一包数据到来后继续尝试解码;

这里编码器的encode和解码器的doDecode方法就是实际用来进行应用层规约编解码的;其中ImageResuest是下行请求规约对应POJO.

整个类的协助关系如下图所示:

 

Mina框架研究_第13张图片

 

Filter过滤器处理完后,就交给Handler,做进一步业务处理;

Response部分的就不多罗嗦了,可以看这里

至此对MINA框架用过的一些东西,从使用指南的角度做了一些总结,还有很多方面还没有用过,先留一个坑,等来日填补吧。MINA框架最吸引我的是这种设计思想,将通信、规约解析、业务处理分离得恰到好处,既不影响性能又方便扩展和替换。下一步希望从源代码的角度,学习一下其内部的实现思想。

解码器进一步介绍

在Mina 中编写解码器,可以实现ProtocolDecoder 接口,其中有decode()、finishDecode()、dispose()三个方法。这里的finishDecode()方法可以用于处理在IoSession 关闭时剩余的未读取数据,一般这个方法并不会被使用到,除非协议中未定义任何标识数据什么时候截止的约定,譬如:Http 响应的Content-Length 未设定,那么在你认为读取完数据后,关闭TCP连接(IoSession 的关闭)后,就可以调用这个方法处理剩余的数据,当然你也可以忽略调剩余的数据。同样的,一般情况下,我们只需要继承适配器ProtocolDecoderAdapter,关注decode()方法即可。但前面说过解码器相对编码器来说,最麻烦的是数据发送过来的规模,以聊天室为例,一个TCP 连接建立之后,那么隔一段时间就会有聊天内容发送过来,也就是decode()方法会被往复调用,这样处理起来就会非常麻烦。那么Mina 中幸好提供了CumulativeProtocolDecoder类,从名字上可以看出累积性的协议解码器,也就是说只要有数据发送过来,这个类就会去读取数据,然后累积到内部的IoBuffer 缓冲区,但是具体的拆包(把累积到缓冲区的数据解码为JAVA 对象)交由子类的doDecode()方法完成,实际上CumulativeProtocolDecoder就是在decode()反复的调用暴漏给子类实现的doDecode()方法。

具体执行过程如下所示:

A. 你的doDecode()方法返回true 时,CumulativeProtocolDecoder 的decode()方法会首先判断你是否在doDecode()方法中从内部的IoBuffer 缓冲区读取了数据,如果没有,则会抛出非法的状态异常,也就是你的doDecode()方法返回true 就表示你已经消费了本次数据(相当于聊天室中一个完整的消息已经读取完毕),进一步说,也就是此时你必须已经消费过内部的IoBuffer 缓冲区的数据(哪怕是消费了一个字节的数据)。如果验证过通过,那么CumulativeProtocolDecoder 会检查缓冲区内是否还有数据未读取,如果有就继续调用doDecode()方法,没有就停止对doDecode()方法的调用,直到有新的数据被缓冲。


B. 当你的doDecode()方法返回false 时,CumulativeProtocolDecoder 会停止对doDecode()方法的调用,但此时如果本次数据还有未读取完的,就将含有剩余数据的IoBuffer 缓冲区保存到IoSession 中,以便下一次数据到来时可以从IoSession 中提取合并。如果发现本次数据全都读取完毕,则清空IoBuffer 缓冲区。简而言之,当你认为读取到的数据已经够解码了,那么就返回true,否则就返回false。这个CumulativeProtocolDecoder 其实最重要的工作就是帮你完成了数据的累积,因为这个工作是很烦琐的。




你可能感兴趣的:(netty-mina)