1 Mina基本开发知识
1.1 非阻塞模式
Java NIO非堵塞应用通常适用用在I/O读写等方面,我们知道,系统运行的性能瓶颈通常在I/O读写,包括对端口和文件的操作上,过去,在打开一个I/O通道后,read()将一直等待在端口一边读取字节内容,如果没有内容进来,read()也是傻傻的等,这会影响我们程序继续做其他事情,那么改进做法就是开设线程,让线程去等待,但是这样做也是相当耗费资源(传统socket通讯服务器设计模式)的。
Java NIO非堵塞技术实际是采取Reactor模式,或者说是Observer模式为我们监察I/O端口,如果有内容进来,会自动通知我们,这样,我们就不必开启多个线程死等,从外界看,实现了流畅的I/O读写,不堵塞了。
Java NIO出现不只是一个技术性能的提高,你会发现网络上到处在介绍它,因为它具有里程碑意义,从JDK1.4开始,Java开始提高性能相关的功能,从而使得Java在底层或者并行分布式计算等操作上已经可以和C或Perl等语言并驾齐驱。
如果你至今还是在怀疑Java的性能,说明你的思想和观念已经完全落伍了,Java一两年就应该用新的名词来定义。从JDK1.5开始又要提供关于线程、并发等新性能的支持,Java应用在游戏等适时领域方面的机会已经成熟,Java在稳定自己中间件地位后,开始蚕食传统C的领域。
NIO 有一个主要的类Selector,这个类似一个观察者,只要我们把需要探知的socketchannel告诉Selector,我们接着做别的事情,当有事件发生时,他会通知我们,传回一组 SelectionKey,我们读取这些Key,就会获得我们刚刚注册过的socketchannel,然后,我们从这个Channel中读取数据,放心,包准能够读到,接着我们可以处理这些数据。 Selector内部原理实际是在做一个对所注册的channel的轮询访问,不断的轮询(目前就这一个算法),一旦轮询到一个channel有所注册的事情发生,比如数据来了,他就会站起来报告,交出一把钥匙,让我们通过这把钥匙(SelectionKey表示 SelectableChannel 在 Selector 中的注册的标记。 )来读取这个channel的内容.
1.2 什么是mian
现在已经是World Wide Web的时代,无数的web应用框架被创造出来从而大大的提高了web开发的速度。抛开WWW的这个优势,我们知道还有很多协议是HTTP协议所无法替代的。有时,我们仍然需要构造c/s应用来实现适当的协议。
你有没有曾经使用java或者其他语言实现过某个协议栈?就像你所经历过的那样,编写网络应用即使对于有经验的开发者也不是容易的事情。这归咎于以下几个方面:
* 没有为开发者设计的合适的网络应用框架.
* 使你无法在有限的时间内创建你的应用.
* 网络I/O编码,消息的编/解码,业务逻辑常常纠缠在一起.
* 使程序失去可维护性和可复用性
* 网络应用难于进行单元测试
* 你失去了敏捷性
MINA是一个网络应用框架,在不牺牲性能和可扩展性的前提下用于解决上面的所有问题。
1.3 几个接口
IoAcceptor执行所有底层IO,将他们翻译成抽象的IO事件,并把翻译过的事件和关联的IoSession发送给IoHandler。
1.3.1 IoSession
一个代表了IoSession程序同一个远程实体的IO连接。通过IoSession,你可以写出message到远程实体,访问session的配置,并且更改session的属性。
1.3.2 IoHandler
* sessionCreated: 当一个IO连接建立时被调用,这个方法在任何IO操作之前被调用,以便socket参数或session属性能够最先被设置。
* sessionOpened: 在sessionCreated调用之后被调用。
* sessionClosed: 当IO连接被关闭时被调用。
* sessionIdle: 当在远程实体和用户程序之间没有数据传输的时候被调用。
* exceptionCaught: 当IoAcceptor 或者你的IoHandler.中出现异常时被调用。
* messageReceived: 当接收到新的协议消息时被调用。可以在这里实现你的控制流程。
* messageSent: 当用户请求的消息通过 IoSession#write(Object) 确实发送后被调用。
1.4 编制一个ECHO服务器
1.4.1 业务逻辑
package org.apache.mina.examples.echoserver;
import org.apache.mina.common.ByteBuffer;
import org.apache.mina.common.IoHandlerAdapter;
import org.apache.mina.common.IoSession;
import org.apache.mina.common.TransportType;
import org.apache.mina.transport.socket.nio.SocketSessionConfig;
public class EchoProtocolHandler extends IoHandlerAdapter
{
public void sessionCreated( IoSession session )
{
if (session.getTransportType() == TransportType.SOCKET) {
((SocketSessionConfig)session.getConfig()).setReceiveBufferSize(2048);
}
public void exceptionCaught( IoSession session, Throwable cause )
{
session.close();
}
public void messageReceived( IoSession session, Object message )
{
if (!(message instanceof ByteBuffer))
return;
ByteBuffer rb = (ByteBuffer)message;
// Write the received data back to remote peer
ByteBuffer wb = ByteBuffer.allocate( rb.remaining() );
wb.put( rb );
wb.flip();
session.write( wb );
}
}
1.4.2 控制逻辑
1.4.2.1 主程序
刚刚我们使用MINA实现echo协议,现在我们将handler绑定到一个server端口上。
import java.net.InetSocketAddress;
import org.apache.mina.transport.socket.nio.SocketAcceptor;
import org.apache.mina.transport.socket.nio.SocketAcceptorConfig;
public class Main
{
/** Choose your favorite port number. */
private static final int PORT = 8080;
public static void main( String[] args ) throws Exception
{
SocketAcceptor acceptor = new SocketAcceptor();
SocketAcceptorConfig defaultConfig = new SocketAcceptorConfig();
defaultConfig.setReuseAddress(true);
// Bind
acceptor.bind(new InetSocketAddress(PORT), new EchoProtocolHandler(), defaultConfig);
System.out.println( "Listening on port " + PORT );
}
}
1.4.2.2 扩展逻辑
IoFilter提供了更加有力的方式来扩展MINA。它拦截所有的IO事件进行事件的预处理和后处理。你可以把它想象成Servlet的filters。IoFilter能够实现以下几种目的:
* 事件日志
* 性能检测
* 数据转换(e.g. SSL support)
* 防火墙…等等
1.4.2.2.1 日志filter
我们的echo协议handler不对任何IO事件进行日志。我们可以通过添加一个filter来增加日志能力。MINA提供了IoLoggingFilter来进行日志。我们只要添加日志filter到ServiceRegistry即可。 .
DefaultIoFilterChainBuilder chain = config.getFilterChain();
addLogger(chain);
private static void addLogger( DefaultIoFilterChainBuilder chain ) throws Exception
{
chain.addLast( "logger", new LoggingFilter() );
}
1.4.2.2.2 SSL的filter
想使用SSL?MINA也提供了一个SSL的filter,但它需要JDK1.5。 .
DefaultIoFilterChainBuilder chain = config.getFilterChain();
addLogger(chain);
private static void addSSLSupport( DefaultIoFilterChainBuilder chain )
throws Exception
{
SSLFilter sslFilter =
new SSLFilter( BogusSSLContextFactory.getInstance( true ) );
chain.addLast( "sslFilter", sslFilter );
System.out.println( "SSL ON" );
}
1.5 实现反转Echo协议服务器
在上面我们通过简单的echo server的例子学习了如何使用IO层,但是如果想实现复杂的如LDAP这样的协议怎么办呢?它似乎是一个恶梦,因为IO层没有帮助你分离‘message解析’和‘实际的业务逻辑(比如访问一个目录数据库)’。MINA提供了一个协议层来解决这个问题。协议层将ByteBuffer事件转换成高层的POJO事件:
使用协议层必须实现5个接口:ProtocolHandler, ProtocolProvider, ProtocolCodecFactory,ProtocolEncoder, 和 ProtocolDecoder。可能看上去有点麻烦,但是请注意ProtocolCodecFactory, ProtocolEncoder, 和ProtocolDecoder是可以完全复用的;Apache的ASN1项目为MINA提供了ASN.1解码器,更通用的解码器如:XML、java对象序列化和简单的文本将在MINA的下一个版本中提供。一旦你实现了一个灵活的解码器,你可以在未来的应用中复用它,即使你不打算复用你的解码器,MINA也提供了一个很简单的方法来实现复杂的协议。(请参考高级主题)在这一章中,我们添加一个‘反转’server,它用于反转它接到的所有文本,我们通过它来示范如何编写一个协议层。
ProtocolSession同IO层的IoSession同样继承自Session。就像前面提到的,你只需撰写面向POJO的message而不是ByteBuffer的。ProtocolEncoder将message对象解释成ByteBuffers以便IO层能够将他们输出到socket。ProtocolHandler类似于IO层的IoHandler.dataRead和dataWritten方法被替换成messageReceived和messageSent。这是因为ProtocolDecoder已经将IO层接收到的 ByteBuffers转换成了message对象。
1.5.1 业务逻辑
package org.apache.mina.examples.reverser;
import org.apache.mina.common.IoHandler;
import org.apache.mina.common.IoHandlerAdapter;
import org.apache.mina.common.IoSession;
public class ReverseProtocolHandler extends IoHandlerAdapter
{
public void exceptionCaught( IoSession session, Throwable cause )
{
// Close connection when unexpected exception is caught.
session.close();
}
public void messageReceived( IoSession session, Object message )
{
// Reverse received string
String str = message.toString();
StringBuffer buf = new StringBuffer( str.length() );
for( int i = str.length() 1; i >= 0; i )
{
buf.append( str.charAt( i ) );
}
// and write it back.
session.write( buf.toString() );
}
}
1.5.2 对象转换方式
ProtocolEncoder 和ProtocolDecoder只有一个方法。ProtocolEncoder将message对象转换成一个ByteBuffer,而ProtocolDecoder将一个ByteBuffer转换成message对象。下面我们将学习如何实现这些接口。要实现反转协议要实作的唯一接口就是ProtocolProvider。它非常简单:(注:Provider用于在一个统一的类中提供该协议相关的Handler、Decoder和Encoder。)
import org.apache.mina.protocol.*;
/**
* {@link ProtocolProvider} implementation for reverser server protocol.
*/
public class ReverseProtocolProvider implements ProtocolProvider
{
// Protocol handler is usually a singleton.
private static ProtocolHandler HANDLER =
new ReverseProtocolHandler();
// Codec factory is also usually a singleton.
private static ProtocolCodecFactory CODEC_FACTORY =
new ProtocolCodecFactory()
{
public ProtocolEncoder newEncoder()
{
// Create a new encoder.
return new TextLineEncoder();
}
public ProtocolDecoder newDecoder()
{
// Create a new decoder.
return new TextLineDecoder();
}
};
public ProtocolCodecFactory getCodecFactory()
{
return CODEC_FACTORY;
}
public ProtocolHandler getHandler()
{
return HANDLER;
}
}
1.5.3 控制逻辑
1.5.3.1 主程序
package org.apache.mina.examples.reverser;
import org.apache.mina.common.*;
import org.apache.mina.protocol.*;
import org.apache.mina.registry.*;
/**
* (<b>Entry point</b>) Reverser server which reverses all text lines from
* clients.
*
* @author Trustin Lee (trustin@apache.org)
* @version $Rev: 165594 $, $Date: 20050502 16:21:22 +0900 $,
*/
public class Main
{
private static final int PORT = 8080;
public static void main( String[] args ) throws Exception
{
ServiceRegistry registry = new SimpleServiceRegistry();
// Bind
Service service = new Service( "reverse", TransportType.SOCKET, PORT );
registry.bind( service, new ReverseProtocolProvider() );
System.out.println( "Listening on port " + PORT );
}
}
1.5.3.2 扩展逻辑
ProtocolFilter 同IO层的IoFilter类似:添加IoLoggingFilter来记录底层IO事件是为了debug。我们可以用ProtocolLoggingFilter代替它来记录高层事件:
private static void addLogger( ServiceRegistry registry )
{
ProtocolAcceptor acceptor = registry.getProtocolAcceptor( TransportType.SOCKET );
acceptor.getFilterChain().addLast( "logger", new ProtocolLoggingFilter() );
System.out.println( "Logging ON" );
}
1.6 其它相关知识
1.6.1 NIO ByteBuffer的功能。
MINA没有直接使用使用java NIO的ByteBuffer类。它使用一个自制的ByteBuffer来扩展java以下是它们的一些区别:
* MINA ByteBuffer是一个抽象类,用户可以自由的扩展它
* MINA 管理 MINA ByteBuffers 并对其提供对象池.
* MINA ByteBuffer提供很多便利的方法,如:无符号数值的getter和基于String的getter和putter
如果你使用MINA,你将不需要直接使用NIO buffers,因为仅使用MINA buffers就可以完成大多数buffer操作。
1.6.2 ByteBuffer 池
MINA有一个全局的ByteBuffer池,它被在同一个虚拟机下的所有MINA应用共享。任何分配的buffers将在IO操作或者事件处理方法被执行之后被释放。所以你可以调用ByteBuffer.allocate()来从池中得到一个ByteBuffer而不需要将它返回到池中。请查阅ByteBufferJavaDocs获得更多信息。
1.6.3 线程模式
MINA通过它灵活的filter机制来提供多种线程模型。没有线程池过滤器被使用时MINA运行在一个单线程模式。如果添加了一个IoThreadPoolFilter
到IoAcceptor,你将得到一个leaderfollower模式的线程池。如果再添加一个ProtocolThreadPoolFilter,你的server将有两个线程池;一个(IoThreadPoolFilter)被用于对message对象进行转换,另外一个(ProtocolThreadPoolFilter)被用于处理业务逻辑。
SimpleServiceRegistry加上IoThreadPoolFilter和ProtocolThreadPoolFilter的缺省实现即可适用于需要高伸缩性的应用。如果你想使用自己的线程模型,请查看SimpleServiceRegistry的源代码,并且自己初始化Acceptor。显然,这是个繁琐的工作。
IoThreadPoolFilter threadPool = new IoThreadPoolFilter();
threadPool.start();
IoAcceptor acceptor = new SocketAcceptor();
acceptor.getFilterChain().addLast( "threadPool", threadPool );
ProtocolThreadPoolFilter threadPool2 = new ProtocolThreadPoolFilter();
threadPool2.start();
ProtocolAcceptor acceptor2 = new IoProtocolAcceptor( acceptor );
acceptor2.getFilterChain().addLast( "threadPool", threadPool2 );
threadPool2.stop();
threadPool.stop();
1.6.4 更复杂的协议支持
‘Reverser’示例相对于其他复杂的协议来说仍然过于简单。要想让一个server工作,仍然有许多message类型和它们的转换的工作需要作。MINA提供了一下工具类来提供帮助:
* DemuxingProtocolHandler
* DemuxingProtocolCodecFactory
1.6.5 VM 内部管道通讯
你一定已经知道协议层是建立在IO层之上的,但是有时也不一定。虽然我们通常使用协议层来包装IO层,但仍有一种特殊的协议层实现,称作:’inVM pipe communication’让我们假设你需要使用MINA实现一个SMTP server和一个Spam Filter server。SMTPserver可能需要同Spam Filter server通讯以便发现spam message或者RBL中列出的客户端。如果这两个server是在同一个java虚拟机中,一个IO层是多余的,你可以绕过message对象的编解码的过程。InVMpipe communication可以使你使用同样的代码而不管spam filter server是否在同一个虚拟机中。
2 Mina快速入门(TCP协议)
2.1 介绍
Apache MINA 是一个网络应用框架,有助于用户非常方便地开发高性能、高伸缩性的网络应用。它通过Java NIO提供了一个抽象的、事件驱动的、异步的位于各种传输协议(如TCP/IP和UDP/IP)之上的API,
Apache MINA 通常可被称之为:
NIO 框架库;
客户端/服务器框架库;
或者一个网络socket库。
然而,它所提供的功能远不止这些。
(以上内容大致翻译自Apache MINA网站)
如期官方文档的介绍,Apache MINA 是一个网络应用程序框架,它对Java中的socket和NIO进行了有效和清晰的封装,方便开发人员开发TCP/UDP程序,从而抛开在使用原始的socket时需要考虑的各种繁杂而又烦人问题(线程、性能、会话等),把更多精力专著在应用中的业务逻辑的开发上。
Apache MINA 有两个主要版本:2.0 和 1.1,2.0与1.1有较大的区别,其采用java NIO进行开发,使得性能得到有效的提升,在接口方面也有不小的变化,具体信息可以参见其网站说明。
下面的介绍以 Apache MINA 2.0 为例。
2.2 软件准备
要编写和运行一个基于Apache MINA 2.0的程序,需要JDK 5.0以上版本,还需要以下软件:
(1)MINA 2.x Core
下载地址:http://mina.apache.org/downloads.html
(2)SLF4J 1.3.0 or greater
下载地址:http://www.slf4j.org/download.html
这里需要用到两个 SLF4J 的jar包:slf4j-api.jar 和 slf4j-jdk14.jar。
分别解压下载的MINA 2.x Core 和SLF4J文件,找出下列jar包文件:
mina-core-2.0.0-M1.jar
slf4j-api.jar
slf4j-jdk14.jar
2.3 编写一个Apache MINA 时间服务器
该程序的功能非常简单,就是当客户端连接到服务器的9123端口后,程序将服务器当前的时间信息以字符串的形式发送给客户端。
我们可以用Eclipse来创建编写这个程序。
(1)在Eclipse中创建一个Java项目,例如 TimeServerProject,然后将mina-core-2.0.0-M1.jar、slf4j-api.jar 和 slf4j-jdk14.jar这三个文件添加到项目的Libraries中。
2.3.1 服务器端通讯编写NioSocketAcceptor
创建一个 MinaTimeServer 类,其内容为:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.charset.Charset;
import org.apache.mina.common.IdleStatus;
import org.apache.mina.common.IoAcceptor;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.filter.codec.textline.TextLineCodecFactory;
import org.apache.mina.filter.logging.LoggingFilter;
import org.apache.mina.transport.socket.nio.NioSocketAcceptor;
public class MinaTimeServer {
private static final int PORT = 9123;
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));
}
}
2.3.2 服务器端应用编写IoHandlerAdapter
(3)创建一个 TimeServerHandler 类,其内容为:
import java.util.Date;
import org.apache.mina.common.IdleStatus;
import org.apache.mina.common.IoHandlerAdapter;
import org.apache.mina.common.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();
System.out.println("Message read:");
System.out.println(str);
Date date = new Date();
session.write(date.toString());
System.out.println("Message written...");
session.close();
}
@Override
public void sessionIdle(IoSession session, IdleStatus status)
throws Exception {
System.out.println("IDLE " + session.getIdleCount(status));
}
}
(4)编译并运行MinaTimeServer类
2.3.3 客户端的应用
2.3.3.1 浏览器方式
打开您的浏览器,在地址栏中输入 http://127.0.0.1:9123/,然后回车,你就可以看到服务器的时间显示在您的浏览器内了。同时您也可以在运行MinaTimeServer的控制台窗口中看到类似下面的信息:
Message read:2008-4-11 12:22:44 org.apache.mina.filter.logging.LogLevel$4 log
信息: CREATED
2008-4-11 12:22:44 org.apache.mina.filter.logging.LogLevel$4 log
信息: OPENED
2008-4-11 12:22:44 org.apache.mina.filter.logging.LogLevel$4 log
信息: RECEIVED: HeapBuffer[pos=0 lim=261 cap=2048: 47 45 54 20 2F 20 48 54 54 50 2F 31 2E 31 0D 0A...]
GET / HTTP/1.1
Message written...
Message read:
Accept: */*
Message written...
Message read:
Accept-Language: zh-cn,en-US;q=0.5
Message written...
Message read:
UA-CPU: x86
Message written...
Message read:
Accept-Encoding: gzip, deflate
Message written...
Message read:
User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.2; .NET CLR 1.1.4322; .NET CLR 2.0.50727)
Message written...
Message read:
Host: 127.0.0.1:9123
Message written...
Message read:
Connection: Keep-Alive
Message written...
Message read:
Message written...
2008-4-11 12:22:44 org.apache.mina.filter.logging.LogLevel$4 log
信息: SENT: HeapBuffer[pos=0 lim=29 cap=30: 46 72 69 20 41 70 72 20 31 31 20 31 32 3A 32 32...]
2008-4-11 12:22:44 org.apache.mina.filter.logging.LogLevel$4 log
信息: SENT: HeapBuffer[pos=0 lim=0 cap=0: empty]
2008-4-11 12:22:44 org.apache.mina.filter.logging.LogLevel$4 log
信息: CLOSED
这是程序的日志信息以及,浏览器发送过来请求的HTTP头信息。
2.3.3.2 客户端模式
或者也可以通过telnet连接服务器的9123端口,连敲两次回车,就可以看到服务器发送过来的时间信息了。
3 Mian传递对象(TCP协议)
接触java的Mina框架已经有很多时间了,在网上也读过了很多的相关文章,发现Mina框架的确是一个值得拿来好好研究的东西,前些日子写了一个山寨QQ项目,其中的通信部分用到了java中自带的InputStream,OutputStream,Writer,Reader等等,感觉其中的很大的一个问题就是难以将事务处理的逻辑层与解析层分离开来,造成整个项目看起来比较臃肿,繁琐,不够模块化,接触Mina后发现mina在这方面做的很是恰到好处。
1、Mina框架传递对象是怎么回事
2、Mina传递对象可以用来做什么
3、Mina传递对象是怎么进行的
4、Mina传递对象过程中会遇到什么问题呢
在用原来的java的InputStream,OutputStream,Writer,Reader等进行通信的时候我们会将信息编码转化成字节流等进行信息传递,InputStream,OutputStream是基于字节流的,而Writer,Reader是基于字符的,我们都知道进行通信的服务器和客户端是事先必须定好通信协议,如果我们将<msg>你好吗?</msg>定义为是一条消息,<request>视频</request>定义为一条视频请求,如果客户端将这条消息和请求发送给了服务器,服务器要想得到消息和请求的真正内容(在这里分别是“你好吗?”和“视频”)并进行处理和应答就必须进行信息的解析,就要一条一条的进行判断:1、如果是信息是<msg>……</msg>格式的就将其看做是一条消息;2、如果是<request>……</request>格式的就将其看作是一条请求;3、如果是其他形式就将其视为无效信息,不予处理。当然这不失为一种办法可以进行信息的提取,但是我们会发现在这个过程中信息的发送、接受、解析、处理、应答等都是一条一条的,很是零散,比较难以统一,没有实现消息定义和解析处理过程的分离,这样写好了一个程序,如果日后想要进行改正其中的一条信息格式,就要在整个项目中Ctrl+F了,比较繁琐,还容易出错。
这是我们会自然的想到要用一种东西将各个格式的信息进行分类统一起来并方便进行一些必要的信息处理,为符合这些特点,我们会想到类这个东东恰恰满足了这些性质,我们可以将信息的格式中的内容定义为类的属性,而对这些属性的处理就可以用类中的方法来予以解决,这样就对信息进行了很好的包装。
这种思想有了,那就是在通信的时候直接进行形式上的对象传递(实际上在通信的时候都是最终以字节流的方式进行传递的),那么我们就要找一种工具进行这种形式的信息传递,对了,这种工具就是Mina框架,我们只看他其中的一个方法
public void messageReceived(IoSession session, Object message),这是进行消息接收是能够被触发的一个方法,参数session代表当前的会话对象,参数message代表接收的到的信息,这时您会发现message的类型是Object型,而类 Object 是类层次结构的根类,当然可以用对象型的作为message啦!前面提到通信的时候都是最终以字节流的方式进行传递的,这样就要进行:对象(客户端)->字节流(客户端)->发送->接收->字节流(服务器)->对象(服务器)的过程,呵呵不用担心,这些繁琐的过程,Mina都提供了很好的底层默认实现所以你只需稍稍敲点代码就行了。 光说不练还是不行,先上一个程序实例:
3.1 服务器端程序
3.1.1 主程序NioSocketAcceptor
package Mina.server;
import java.io.IOException;
import java.net.InetSocketAddress;
import org.apache.mina.core.filterchain.DefaultIoFilterChainBuilder;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.filter.codec.serialization.ObjectSerializationCodecFactory;
import org.apache.mina.transport.socket.SocketAcceptor;
import org.apache.mina.transport.socket.nio.NioSocketAcceptor;
public class MainServer
{
private static MainServer mainServer = null;
private SocketAcceptor acceptor = new NioSocketAcceptor();
private DefaultIoFilterChainBuilder chain = acceptor.getFilterChain();
private int bindPort = 8888;
public static MainServer getInstances()
{
if (null == mainServer) {
mainServer = new MainServer();
}
return mainServer;
}
private MainServer()
{
chain.addLast("myChin", new ProtocolCodecFilter(
new ObjectSerializationCodecFactory()));
acceptor.setHandler(ServerHandler.getInstances());
try {
acceptor.bind(new InetSocketAddress(bindPort));
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws Exception {
MainServer.getInstances();
}
}
3.1.2 响应类IoHandlerAdapter
package Mina.server;
import org.apache.mina.core.filterchain.IoFilterAdapter;
import org.apache.mina.core.service.IoHandler;
import org.apache.mina.core.session.IdleStatus;
import org.apache.mina.core.session.IoSession;
import Mina.Object.UserInfo;
public class ServerHandler extends IoFilterAdapter implements IoHandler
{
private static ServerHandler samplMinaServerHandler = null;
public static ServerHandler getInstances()
{
if (null == samplMinaServerHandler) {
samplMinaServerHandler = new ServerHandler();
}
return samplMinaServerHandler;
}
private ServerHandler() {
}
// 当连接后打开时触发此方法,一般此方法与 sessionCreated 会被同时触发
public void sessionOpened(IoSession session) throws Exception {
}
public void sessionClosed(IoSession session) {
}
public void messageReceived(IoSession session, Object message)
throws Exception {
if (message instanceof UserInfo) {
UserInfo text = (UserInfo) message;
System.out.println("服务器接收到从客户端的姓名:"+text.getName());
System.out.println("服务器接收到从客户端的QQ:"+text.getQQNum());
}
}
public void exceptionCaught(IoSession arg0, Throwable arg1)
throws Exception {
}
// 当消息传送到客户端后触发
public void messageSent(IoSession arg0, Object arg1) throws Exception {
}
// 当一个新客户端连接后触发此方法.
public void sessionCreated(IoSession arg0) throws Exception {
}
// 当连接空闲时触发此方法.
public void sessionIdle(IoSession arg0, IdleStatus arg1) throws Exception {
}
}
3.2 客户端程序
3.2.1 主程序NioSocketConnector
package Mina.client;
import java.net.InetSocketAddress;
import org.apache.mina.core.filterchain.DefaultIoFilterChainBuilder;
import org.apache.mina.core.future.ConnectFuture;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.filter.codec.serialization.ObjectSerializationCodecFactory;
import org.apache.mina.transport.socket.nio.NioSocketConnector;
public class MainClient {
private static MainClient mainClient = null;
NioSocketConnector connector = new NioSocketConnector();
DefaultIoFilterChainBuilder chain = connector.getFilterChain();
public static MainClient getInstances() {
if (null == mainClient) {
mainClient = new MainClient();
}
return mainClient;
}
private MainClient() {
chain.addLast("myChin", new ProtocolCodecFilter(
new ObjectSerializationCodecFactory()));
connector.setHandler(ClientHandler.getInstances());
connector.setConnectTimeout(30);
ConnectFuture cf = connector.connect(new InetSocketAddress("localhost",
8888));
}
public static void main(String args[]) {
MainClient.getInstances();
}
}
3.2.2 功能端IoHandlerAdapter
package Mina.client;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;
import Mina.Object.UserInfo;
public class ClientHandler extends IoHandlerAdapter
{
private static ClientHandler samplMinaClientHandler = null;
public static ClientHandler getInstances()
{
if (null == samplMinaClientHandler)
{
samplMinaClientHandler = new ClientHandler();
}
return samplMinaClientHandler;
}
private ClientHandler() {
}
public void sessionOpened(IoSession session) throws Exception {
session.write("客户端与服务器的会话打开了……");
UserInfo text=new UserInfo();
text.setName("梅竹寒香");
text.setQQNum("972341215");
session.write(text);
}
public void sessionClosed(IoSession session)
{
}
public void messageReceived(IoSession session, Object message)
throws Exception
{
}
public void messageSent(IoSession arg0, Object arg1) throws Exception
{
System.out.println("客户端已经向服务器发送了:"+(String)arg1);
}
}
3.3 公共类:
package Mina.Object;
public class UserInfo implements java.io.Serializable{
private String name;
private String QQNum;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getQQNum() {
return QQNum;
}
public void setQQNum(String qQNum) {
QQNum = qQNum;
}
}
如下建包即可:以上就是对象的收发的简单示例,如果报错,或许会是一下原因:1、包的引进是否妥当 2、是否引入了mina的第三方包(网上有了很多的相关文章,在此就不在赘述了)
3.4 几点注意
通过程序您会看到对象已经成功传递并进行了相关属性的输出,对于这个简单的程序我稍做些相关说明:
(1) 进行传递的对象所实例化的类要实现java.io.Serializable序列化接口
(2) 2、您会发现实例中的类尤其是相关的IoHandlerAdapter继承类都采用了单实例模式,为什么这样做呢,原因很简单,那就是要在整个通信过程中做到对象session的等实例的单一防止发生“所托非人”的现象
(3) 3、服务器接收到message在进行类判断时用了instanceof关键字如果你看到上面的实例就觉得对象传递的学习已经成功了,那就错了,细心的博友看到这个包结构:是不是有点问题呢。
例如客户端传了一个userinfo对象到服务器,在服务器端判断如果是userinfo对象后就打印出相关信息,我看源码文档其中有这样的建包方式其中服务器和客户端共用了中间的Mina.Object包,这样在收到对象后就能通过instanceof关键字判断是不是useinfo对象,我看了一下,这个方法是可行的,现在的问题是,我们如果编写通讯软件的时候,肯定是服务器和客户端是要分开的,所以那个Mina.Object包是不能共享的,所以问题来了
(1)、如果将userinfo放到客户端中,那么该怎么用instanceof进行判断是不是userinfo呢(这时你已经不能再引入服务器中的userinfo了)
(2)、如果在客户端和服务器中都编写一个类定义一样的userinfo,可是他们这两个类是分属不同的包,所以是两个不同的类了,这样在用instanceof进行判断的时候也是行不通的;那么我们该用什么方法来进行判断接收到的类是不是userinfo对象呢?
这个问题把我纠结了很久,在网上面搜了好久也没有解决,最后想了想那个(2)或许可以改动改动就可以解决,问题的关键在于两个UserInfo分属于两个不同的包,如果可以将包名一致就好了,但是在一个工程里面不能同时建立两个命名一样的包,这样你就会发现何不建立两个工程呢一个是服务器,一个是客户端,这样都可以分别建立名字都是Object的包,这样可不可行呢,经过试验果然可以,这样就就解决了上面的问题工程图如下
好啦,有了这个工具,您会有什么想法呢?对象传递还可以做什么?那就是可以用它来进行图片,文件的传递啦,这个只是个小小的提示具体怎么实现,就要看各位博友怎么发挥啦!呵呵
4 Mina快速入门(UDP协议)
4.1 服务器端
4.1.1 应用逻辑IoHandlerAdapter
package com.taobao.mina.myudp;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.util.Date;
import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IdleStatus;
import org.apache.mina.core.session.IoSession;
/**
* Class the extends IoHandlerAdapter in order to properly handle
* connections and the data the connections send
*
* @author <a href="http://mina.apache.org" mce_href="http://mina.apache.org">Apache MINA Project</a>
*/
public class MemoryMonitorHandler extends IoHandlerAdapter {
private MemoryMonitor server;
public MemoryMonitorHandler(MemoryMonitor server)
{
this.server = server;
}
/**
* 异常来关闭session
*/
@Override
public void exceptionCaught(IoSession session, Throwable cause)
throws Exception {
cause.printStackTrace();
session.close(true);
}
/**
* 服务器端收到一个消息
*/
@Override
public void messageReceived(IoSession session, Object message)
throws Exception {
if (message instanceof IoBuffer) {
IoBuffer buffer = (IoBuffer) message;
buffer.setAutoExpand(true);
System.out.println("服务器端获得udp信息:" + buffer.getLong());
Charset c = Charset.forName("UTF-8");
CharsetEncoder ce = c.newEncoder();
// 给client返回信息 IoBuffer.wrap
IoBuffer clientBuffer = IoBuffer.wrap((new Date().toLocaleString() + "服务器已收到。").getBytes(c));
clientBuffer.setAutoExpand(true);
session.setAttribute("clientbuffer", clientBuffer);
session.write(clientBuffer);
}
}
@Override
public void sessionClosed(IoSession session) throws Exception {
System.out.println("服务器端关闭session...");
}
@Override
public void sessionCreated(IoSession session) throws Exception {
System.out.println("服务器端成功创建一个session...");
}
@Override
public void sessionIdle(IoSession session, IdleStatus status)
throws Exception {
// System.out.println("Session idle...");
}
@Override
public void sessionOpened(IoSession session) throws Exception {
System.out.println("服务器端成功开启一个session...");
}
}
4.1.2 控制逻辑NioDatagramAcceptor
import org.apache.mina.core.filterchain.DefaultIoFilterChainBuilder;
import org.apache.mina.filter.logging.LoggingFilter;
import org.apache.mina.transport.socket.DatagramSessionConfig;
import org.apache.mina.transport.socket.nio.NioDatagramAcceptor;
/**
* The class that will accept and process clients in order to properly
* track the memory usage.
*
* @author <a href="http://mina.apache.org" mce_href="http://mina.apache.org">Apache MINA Project</a>
*/
public class MemoryMonitor {
private static final long serialVersionUID = 1L;
public static final int PORT = 18567;
public MemoryMonitor() throws IOException {
// 创建UDP数据包NIO
NioDatagramAcceptor acceptor = new NioDatagramAcceptor();
// NIO设置底层IOHandler 把服务器的本身传入
acceptor.setHandler(new MemoryMonitorHandler(this));
// 设置filter
DefaultIoFilterChainBuilder chain = acceptor.getFilterChain();
chain.addLast("logger", new LoggingFilter());
// 设置是否重用地址?也就是每个发过来的udp信息都是一个地址?
DatagramSessionConfig dcfg = acceptor.getSessionConfig();
dcfg.setReuseAddress(true);
// 绑定端口地址
acceptor.bind(new InetSocketAddress(PORT));
System.out.println("UDPServer listening on port " + PORT);
}
public static void main(String[] args) {
try {
new MemoryMonitor();
} catch (IOException e) {
e.printStackTrace();
}
}
}
4.2 客户端
4.2.1 应用逻辑IoHandlerAdapter
import java.net.InetSocketAddress;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.future.ConnectFuture;
import org.apache.mina.core.future.IoFutureListener;
import org.apache.mina.core.service.IoConnector;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IdleStatus;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.example.udp.MemoryMonitor;
import org.apache.mina.transport.socket.nio.NioDatagramConnector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Sends its memory usage to the MemoryMonitor server.
* 这样的写法 将本来可以单独的client写到IoHandlerAdapter 更紧凑
*
* @author <a href="http://mina.apache.org" mce_href="http://mina.apache.org">Apache MINA Project</a>
*/
public class MemMonClient extends IoHandlerAdapter
{
/**
* Default constructor.
*/
public MemMonClient()
{
}
private void sendData() throws InterruptedException
{
for (int i = 0; i < 10; i++) {
long free = Runtime.getRuntime().freeMemory(); // 得到当前空闲内存大小
IoBuffer buffer = IoBuffer.allocate(8);
buffer.putLong(free); // 只把剩余内存大小放入buffer,扔给server
buffer.flip();
session.write(buffer); // 写入
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
throw new InterruptedException(e.getMessage());
}
}
}
@Override
public void exceptionCaught(IoSession session, Throwable cause)
throws Exception {
cause.printStackTrace();
}
@Override
public void messageReceived(IoSession session, Object message)
throws Exception {
Charset c = Charset.forName("UTF-8");
CharsetDecoder cd = c.newDecoder();
IoBuffer buffer = (IoBuffer)message;
System.out.println("客户端收到来自服务器的消息String:" + (buffer.getString(cd)));
}
@Override
public void messageSent(IoSession session, Object message) throws Exception
{
System.out.println("客户端向服务器发送信息:" + ((IoBuffer)message).getLong());
}
@Override
public void sessionClosed(IoSession session) throws Exception
{
System.out.println("客户端关闭了当前会话");
}
@Override
public void sessionCreated(IoSession session) throws Exception
{
System.out.println("客户端成功创建session");
}
@Override
public void sessionIdle(IoSession session, IdleStatus status)
throws Exception {
}
@Override
public void sessionOpened(IoSession session) throws Exception {
System.out.println("客户端成功开启一个session id:"+session.getId());
}
}
4.2.2 控制逻辑NioDatagramConnector
import java.net.InetSocketAddress;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.future.ConnectFuture;
import org.apache.mina.core.future.IoFutureListener;
import org.apache.mina.core.service.IoConnector;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IdleStatus;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.example.udp.MemoryMonitor;
import org.apache.mina.transport.socket.nio.NioDatagramConnector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
*/
public class MemoryClientconector
{
private final static Logger LOGGER = LoggerFactory.getLogger(MemMonClient.class);
private IoSession session;
private IoConnector connector;
public memoryclientconector()
{
connector = new NioDatagramConnector();
connector.setHandler(this);
ConnectFuture connFuture = connector.connect(new InetSocketAddress(
"localhost", MemoryMonitor.PORT)); // 这样不太好吧
connFuture.awaitUninterruptibly();
// 给conn添加一个监听器
connFuture.addListener(new IoFutureListener<ConnectFuture>() {
public void operationComplete(ConnectFuture future) {
if (future.isConnected()) {
session = future.getSession();
try {
sendData();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
try {
throw new Exception(" 连接错误。 ");
} catch (Exception e) {
e.printStackTrace();
}
}
}
});
}
public static void main(String[] args)
{
new MemoryClientconector ();
}
}
5 Mina几个关键类
5.1 IoSession
双方专用的会话通道!
[img]http://mina.apache.org/class-diagrams.data/IoSession.png" alt="[/img]
5.2 IoConnector
双方建立连接的接口,即open、close
[img]http://mina.apache.org/class-diagrams.data/IoService-connector.png" alt="[/img]
5.3 IoAcceptor
接收客户端的一个受理,然后生成一个会话类!
[img]http://mina.apache.org/class-diagrams.data/IoService-acceptor.png" alt="[/img]
5.4 IoProcessor
当有一个请求到达后,调用相应处理模块处理用户请求!
[img]http://mina.apache.org/class-diagrams.data/IoProcessor.png" alt="[/img]
5.5 IoBuffer
[img]http://mina.apache.org/class-diagrams.data/IoBuffer.png" alt="[/img]
5.6 IoFuture
[img]http://mina.apache.org/class-diagrams.data/IoFuture.png" alt="[/img]
5.7 ProtocolDecoder
协议解密
[img]http://mina.apache.org/class-diagrams.data/ProtocolDecoder.png" alt="[/img]
5.8 ProtocolEncoder
协议加密
[img]http://mina.apache.org/class-diagrams.data/ProtocolEncoder.png" alt="[/img]
6 Mian的状态机制
如果你使用Mina开发一个复杂的网络应用时,你可能在某些地方会遇到那个古老而又好用的状态模式,来使用这个模式解决你的复杂应用。然而,在你做这个决定之前,你或许想检出Mina的状态机的代码,它会根据当前对象的状态来返回对接收到的简短的数据的处理信息。注意:现在正式发布Mina的状态机。因此你要自己在Mina的SVN服务器上检出该代码,并自己编译,请参考开发指南,来获取更多的关于检出和编译Mina源码的信息。Mina的状态机可以和所有已经发布的版本Mina配合使用(1.0.x, 1.1.x 和当前发布的版本)。
6.1 一个简单的例子
让我们使用一个简单的例子来展示一下Mina的状态机是如何工作的。下面的图片展示了一个录音机的状态机。其中的椭圆是状态,箭头表示事务。每个事务都有一个事件的名字来标记该事务。
初始化时,录音机的状态是空的。当磁带放如录音机的时候,加载的事件被触发,录音机进入到加载 状态。在加载的状态下,退出的事件会使录音机进入到空的状态,播放的事件会使加载的状态进入到 播放状态。等等......我想你可以推断后后面的结果:)
现在让我们写一些代码。外部(录音机中使用该代码的地方)只能看到
6.1.1 录音机的接口TapeDeck
行为方法,是时。
public interface TapeDeck
{
void load(String nameOfTape);
void eject();
void start();
void pause();
void stop();
}
下面我们开始编写真正执行的代码,这些代码在一个事务被触发时,会在状态机中执行。首先我们定义一个状态。这些状态都使用字符串常量来定义,并且使用@state标记来声明。
状态类,是间,即方法运行完后的状态。
public class TapeDeckHandler
{
@State public static final String EMPTY = "Empty";
@State public static final String LOADED = "Loaded";
@State public static final String PLAYING = "Playing";
@State public static final String PAUSED = "Paused";
}
现在我们已经定义了录音机中的所有状态,我们可以根据每个事务来创建相应的代码。每个事务都和一个TapeDeckHandler的方法对应。每个事务的方法都使用@Transtration标签来声明,这个标签定义了事件的ID,该ID会触发事务的执行。事务开始时的状态使用start,事务结束使用next,事务正在运行使用on。
6.1.2 状态类实现TapeDeckHandler
public class TapeDeckHandler
{
@State public static final String EMPTY = "Empty";
@State public static final String LOADED = "Loaded";
@State public static final String PLAYING = "Playing";
@State public static final String PAUSED = "Paused";
@Transition(on = "load", in = EMPTY, next = LOADED)
public void loadTape(String nameOfTape)
{
System.out.println("Tape '" + nameOfTape + "' loaded");
}
@Transitions({
@Transition(on = "play", in = LOADED, next = PLAYING),
@Transition(on = "play", in = PAUSED, next = PLAYING)
})
public void playTape()
{
System.out.println("Playing tape");
}
@Transition(on = "pause", in = PLAYING, next = PAUSED)
public void pauseTape()
{
System.out.println("Tape paused");
}
@Transition(on = "stop", in = PLAYING, next = LOADED)
public void stopTape()
{
System.out.println("Tape stopped");
}
@Transition(on = "eject", in = LOADED, next = EMPTY)
public void ejectTape()
{
System.out.println("Tape ejected");
}
}
请注意,TapeDeckHandler 类没有实现TapeDeck ,呵呵,这是故意的。现在让我们亲密接触一下这个代码。在loadTape方法上的@Transition标签:
@Transition(on = "load", in = EMPTY, next = LOADED)
public void loadTape(String nameOfTape) {}
指定了这个状态后,当录音机处于空状态时,磁带装载事件启动后会触发loadTape方法,并且录音机状态将会变换到Loaded状态。@Transition标签中关于pauseTape,stopTape,ejectTape的方法就不需要在多介绍了。关于playTape的标签和其他的标签看起来不太一样。从上面的图中我们可以知道,当录音机的状态在Loaded或者Paused时,play事件都会播放磁带。当多个事务同时条用同一个方法时,@Transition标签需要按下面的方法使用:
@Transitions({
@Transition(on = "play", in = LOADED, next = PLAYING),
@Transition(on = "play", in = PAUSED, next = PLAYING)
})
public void playTape(){}
@Transition标签清晰的列出了声明的方法被多个事务调用的情况。
###############################################################
要点:更多关于@Transition 标签的参数
(1)如果你省略了on参数,系统会将该值默认为“*”,这样任何事件都可以触发该方法。
(2)如果你省略了next参数,系统会将默认值改为“_self_”,这个是和当前的状态相关的,如果你要实现一个循环的事务,你所需要做的就是省略状态机中的next参数。
(3)weight参数用于定义事务的查询顺序,一般的状态的事务是根据weight的值按升序排列的,weight默认的是0.
现在最后一步就是使用声明类创建一个状态机的对象,并且使用这个状态机的实例创建一个代理对象,该代理对象实现了TapeDeck接口:
6.1.3 程序的调用程序
public static void main(String[] args)
{
// 创建录音机事件的句柄
TapeDeckHandler handler = new TapeDeckHandler();
// 创建录音机的状态机
StateMachine sm = StateMachineFactory.getInstance(Transition.class).create(TapeDeckHandler.EMPTY, handler);
// 使用上面的状态机,通过一个代理创建一个TapeDeck的实例
TapeDeck deck = new StateMachineProxyBuilder().create(TapeDeck.class, sm);
// 加载磁带
deck.load("The Knife - Silent Shout");
// 播放
deck.play();
// 暂停
deck.pause();
// 播放
deck.play();
// 停止
deck.stop();
// 退出
deck.eject();
}
TapeDeckHandler handler = new TapeDeckHandler();
StateMachine sm = StateMachineFactory.getInstance(Transition.class).create(TapeDeckHandler.EMPTY, handler);
使用TapeDeckHandler创建一个状态机的实例。StateMachineFactory.getInstance(...) 调用的方法中使用的Transition.class 是通知工厂我们使用@Transition 标签创建一个状态机。我们指定了状态机开始时状态是空的。一个状态机是一个基本的指示图。状态对象和图中的节点对应,事务对象和箭头指向的方向对应。我们在TapeDeckHandler中使用的每一个@Transition 标签都和一个事务的实例对应。
要点:那么, @Transition 和 Transition 有什么不同吗?
@Transition 是你用来标记当事务在状态之间变化时应该使用那个方法。在后台处理中,
Mina的状态机会为MethodTransition 中每一个事务标签创建一个事务的实例。MethodTransition 实现了Transition 接口。作为一个Mina状态机的使用者,你不用直接使用Transition 或者MethodTransition 类型的对象。
录音机TapeDeck 的实例是通过调用StateMachineProxyBuilder来创建的:
TapeDeck deck = new StateMachineProxyBuilder().create(TapeDeck.class, sm);
StateMachineProxyBuilder.create()使用的接口需要由代理的对象来实现,状态机的实例将接收由代理产生的事件所触发的方法。当代码执行时,输出的结果如下:
Tape 'The Knife - Silent Shout' loaded
Playing tape
Tape paused
Playing tape
Tape stopped
Tape ejected
要点:这和Mina有什么关系?或许你已经注意到,在这个例子中没有对Mina进行任何配置。但是不要着急。稍后我们将会看到如何为Mina的IoHandler接口创建一个状态机。
6.2 Mnia的工作机制
6.2.1 它是怎样工作的?
让我们走马观花的看看当代理调用一个方法的时候发生了什么。查看一个StateContext(状态的上下文)对象状态上下文之所以重要是因为它保存了当前的状态。代理调用一个方法时,状态上下文会通知StateContextLookup 实例去方法的参数中获取一个状态的上下文。一般情况下,StateContextLookup 的实现将会循环方法中的参数,并查找一个指定类型的对象,并且使用这个对象反转出一个上下文对象。如果没有声明一个状态上下文,StateContextLookup 将会创一个,并将其存放到对象中。
当代理Mina的IoHandler接口时,我们将使用IoSessoinStateContextLookup 实例,该实例用来查询一个IoSession中的方法参数。它将会使用 IoSession的属性值为每一个Mina的session来存放一个独立的状态上下文的实例。这中方式下,同样的状态机可以让所有的Mina的会话使用,而不会使每个会话彼此产生影响。
要点:在上面的例子中,当我们使用StateMachineProxyBuilder创建一个代理时,我们一直没有我们一直没有配置StateContextLookup 使用哪种实现。如果没有配置,系统会使用SingletonStateContextLookup 。
SingletonStateContextLookup 总是不理会方法中传递给它的参数,它一直返回一个相同的状态上下文。很明显,这中方式在多个客户端并发的情况下使用同一个同一个状态机是没有意义的。这种情况下的配置会在后面的关于IoHandler 的代理配置时进行说明。
将方法请求反转成一个事件对象所有在代理对象上的方法请求都会有代理对象转换成事件对象。一个事件有一个ID或者0个或多个参数。事件的ID和方法的名字相当,事件的参数和方法的参数相当。调用方法deck.load("The Knife - Silent Shout") 相当于事件{id = "load", arguments = ["The Knife - Silent Shout"]}.事件对象中包含一个状态上下文的引用,该状态上下文是当前查找到的。
6.2.2 触发状态机
一旦事件对象被创建,代理会调用StateMachine.handle(Event).方法。StateMachine.handle(Event)遍历事务对象中当前的状态,来查找能够接收当前事件的事务的实例。这个过程会在事务的实例找到后停止。这个查询的顺序是由事务的重量值来决定的(重量值一般在@Transition 标签中指定)。
6.2.3 执行事务
最后一部就是在Transition 中调用匹配事件对象的Transition.execute(Event)方法。当事件已经执行,这个状态机将更新当前的状态,更新后的值是你在事务中定义的后面的状态。
要点:事务是一个接口。每次你使用@Transition 标签时,MethodTransition对象将会被创建。
MethodTransition(方法事务)
MethodTransition非常重要,它还需要一些补充说明。如果事件ID和@Transition标签中的on参数匹配,事件的参数和@Transition中的参数匹配,那么MethodTransition和这个事件匹配。所以如果事件看起来像{id = "foo", arguments = [a, b, c]},那么下面的方法:
@Transition(on = "foo")
public void someMethod(One one, Two two, Three three) { ... }
只和这个事件匹配((a instanceof One && b instanceof Two && c instanceof Three) == true).。当匹配时,这个方法将会被与其匹配的事件使用绑定的参数调用。
要点: Integer, Double, Float, 等也和他们的基本类型int, double, float, 等匹配。
因此,上面的状态是一个子集,需要和下面的方法匹配:
@Transition(on = "foo")
public void someMethod(Two two) { ... }
上面的方法和((a instanceof Two || b instanceof Two || c instanceof Two) == true)是等价的。在这种情况下,第一个被匹配的事件的参数将会和该方法绑定,在它被调用的时候。一个方法如果没有参数,在其事件的ID匹配时,仍然会被调用:
@Transition(on = "foo")
public void someMethod() { ... }
这样做让事件的处理变得有点复杂,开始的两个方法的参数和事件的类及状态的上下文接口相匹配。这意味着:
@Transition(on = "foo")
public void someMethod(Event event, StateContext context, One one, Two two, Three three) { ... }
@Transition(on = "foo")
public void someMethod(Event event, One one, Two two, Three three) { ... }
@Transition(on = "foo")
public void someMethod(StateContext context, One one, Two two, Three three) { ... } 上面的方法和事件{id = "foo", arguments = [a, b, c]} if ((a instanceof One && b instanceof Two&& c instanceof Three) == true) 是匹配的。当前的事件对象和事件的方法绑定,当前的状态上下文和该方法被调用时的上下文绑定。在此之前一个事件的参数的集合将会被使用。当然,一个指定的状态上下文的实现将会被指定,以用来替代通用的上下文接口。
@Transition(on = "foo")
public void someMethod(MyStateContext context, Two two) { ... }
要点:方法中参数的顺序很重要。若方法需要访问当前的事件,它必须被配置为第一个方法参数。当事件为第一个参数的时候,状态上下问不能配置为第二个参数,它也不能配置为第一个方法的参数。事件的参数也要按正确的顺序进行匹配。方法的事务不会在查找匹配事件方法的时候重新排序。
到现在如果你已经掌握了上面的内容,恭喜你!我知道上面的内容会有点难以消化。希望下面的例子能让你对上面的内容有更清晰的了解。注意这个事件Event {id = "messageReceived", arguments = [ArrayList a = [...], Integer b = 1024]}。下面的方法将和这个事件是等价的:
// All method arguments matches all event arguments directly
@Transition(on = "messageReceived")
public void messageReceived(ArrayList l, Integer i) { ... }
// Matches since ((a instanceof List && b instanceof Number) == true)
@Transition(on = "messageReceived")
public void messageReceived(List l, Number n) { ... }
// Matches since ((b instanceof Number) == true)
@Transition(on = "messageReceived")
public void messageReceived(Number n) { ... }
// Methods with no arguments always matches
@Transition(on = "messageReceived")
public void messageReceived() { ... }
// Methods only interested in the current Event or StateContext always matches
@Transition(on = "messageReceived")
public void messageReceived(StateContext context) { ... }
// Matches since ((a instanceof Collection) == true)
@Transition(on = "messageReceived")
public void messageReceived(Event event, Collection c) { ... }
但是下面的方法不会和这个事件相匹配:
// Incorrect ordering
@Transition(on = "messageReceived")
public void messageReceived(Integer i, List l) { ... }
// ((a instanceof LinkedList) == false)
@Transition(on = "messageReceived")
public void messageReceived(LinkedList l, Number n) { ... }
// Event must be first argument
@Transition(on = "messageReceived")
public void messageReceived(ArrayList l, Event event) { ... }
// StateContext must be second argument if Event is used
@Transition(on = "messageReceived")
public void messageReceived(Event event, ArrayList l, StateContext context) { ... }
// Event must come before StateContext
@Transition(on = "messageReceived")
public void messageReceived(StateContext context, Event event) { ... }
6.2.4 状态继承
状态的实例将会有一个父类的状态。如果StateMachine.handle(Event)的方法不能找到一个事务和当前的事件在当前的状态中匹配,它将会寻找父类中的装。如果仍然没有找到,那么事务将会自动寻找父类的父类,知道找到为止。这个特性很有用,当你想为所有的状态添加一些通用的代码时,不需要为每一个状态的方法来声明事务。这里你可以创建一个类的继承体系,使用下面的方法即可:
@State public static final String A = "A";
@State(A) public static final String B = "A->B";
@State(A) public static final String C = "A->C";
@State(B) public static final String D = "A->B->D";
@State(C) public static final String E = "A->C->E";
使用状态继承来处理错误信息让我们回到录音机的例子。如果录音机里没有磁带,当你调用deck.play()方法时将会怎样?让我们试试:
示例代码:
public static void main(String[] args) {
...
deck.load("The Knife - Silent Shout");
deck.play();
deck.pause();
deck.play();
deck.stop();
deck.eject();
deck.play();
}
运行结果:
Tape stopped
Tape ejected
Exception in thread "main" o.a.m.sm.event.UnhandledEventException:
Unhandled event: org.apache.mina.statemachine.event.Event@15eb0a9[id=play,...]
at org.apache.mina.statemachine.StateMachine.handle(StateMachine.java:285)
at org.apache.mina.statemachine.StateMachine.processEvents(StateMachine.java:142)
...
哦,我们得到了一个无法处理的异常UnhandledEventException,这是因为在录音机的空状态时,没有事务来处理播放的状态。我们将添加一个指定的事务来处理所有不能匹配的事件。
@Transitions({
@Transition(on = "*", in = EMPTY, weight = 100),
@Transition(on = "*", in = LOADED, weight = 100),
@Transition(on = "*", in = PLAYING, weight = 100),
@Transition(on = "*", in = PAUSED, weight = 100)
})
public void error(Event event) {
System.out.println("Cannot '" + event.getId() + "' at this time");
}
现在当你运行上面的main()方法时,你将不会再得到一个异常,输出如下:
...
Tape stopped
Tape ejected
Cannot 'play' at this time.
现在这些看起来运行的都很好,是吗?但是如果们有30个状态而不是4个,那该怎么办?那么我们需要在上面的错误方法处理中配置30个事务的声明。这样不好。让我们用状态继承来解决:
public static class TapeDeckHandler
{
@State public static final String ROOT = "Root";
@State(ROOT) public static final String EMPTY = "Empty";
@State(ROOT) public static final String LOADED = "Loaded";
@State(ROOT) public static final String PLAYING = "Playing";
@State(ROOT) public static final String PAUSED = "Paused";
...
@Transition(on = "*", in = ROOT)
public void error(Event event) {
System.out.println("Cannot '" + event.getId() + "' at this time");
}
}
这个运行的结果和上面的是一样的,但是它比要每个方法都配置声明要简单的多。
6.2.5 Mina的状态机和IoHandler配合使用
现在我们将上面的录音机程序改造成一个TCP服务器,并扩展一些方法。服务器将接收一些命令类似于:
load <tape>, play, stop等等。服务器响应的信息将会是+ <message> 或者是- <message>。协议是基于
Mina自身提供的一个文本协议,所有的命令和响应编码都是基于UTF-8。这里有一个简单的会话示例:
telnet localhost 12345
S: + Greetings from your tape deck!
C: list
S: + (1: "The Knife - Silent Shout", 2: "Kings of convenience - Riot on an empty street")
C: load 1
S: + "The Knife - Silent Shout" loaded
C: play
S: + Playing "The Knife - Silent Shout"
C: pause
S: + "The Knife - Silent Shout" paused
C: play
S: + Playing "The Knife - Silent Shout"
C: info
S: + Tape deck is playing. Current tape: "The Knife - Silent Shout"
C: eject
S: - Cannot eject while playing
C: stop
S: + "The Knife - Silent Shout" stopped
C: eject
S: + "The Knife - Silent Shout" ejected
C: quit
S: + Bye! Please come back!
该程序完整的代码在org.apache.mina.example.tapedeck 包中,这个可以通过检出Mina源码的SVN库中的mina-example 来得到。代码使用。
MinaProtocolCodecFilter来编解码传输的二进数据对象。这里只是为每个状态对服务器的请求实现了一个简单的编解码器。在此不在对Mina中编解码的实现做过多的讲解。现在我们看一下这个服务器是如何工作的。这里面一个重要的类就是实现了录音机程序的TapeDeckServer 类。这里我们要做的第一件事情就是去定义这些状态:
@State public static final String ROOT = "Root";
@State(ROOT) public static final String EMPTY = "Empty";
@State(ROOT) public static final String LOADED = "Loaded";
@State(ROOT) public static final String PLAYING = "Playing";
@State(ROOT) public static final String PAUSED = "Paused";
在这里没有什么新增的内容。然而,但是处理这些事件的方法看起来将会不一样。让我们看看playTape的方法。
@IoHandlerTransitions({
@IoHandlerTransition(on = MESSAGE_RECEIVED, in = LOADED, next = PLAYING),
@IoHandlerTransition(on = MESSAGE_RECEIVED, in = PAUSED, next = PLAYING)
})
public void playTape(TapeDeckContext context, IoSession session, PlayCommand cmd) {
session.write("+ Playing \"" + context.tapeName + "\"");
}
这里没有使用通用的@Transition和@Transitions的事务声明,而是使用了Mina指定的 @IoHandlerTransition和@IoHandlerTransitions声明。当为Mina的IoHandler创建一个状态机时,它会选择让你使用Java enum (枚举)类型来替代我们上面使用的字符串类型。这个在Mina的IoFilter中也是一样的。我们现在使用MESSAGE_RECEIVED来替代"play"来作为事件的名字(on是@IoHandlerTransition的一个属性)。这个常量是在org.apache.mina.statemachine.event.IoHandlerEvents中定义的,它的值是"messageReceived",这个和Mina的IoHandler中的messageReceived()方法是一致的。谢谢Java 5中的静态导入,我们在使用该变量的时候就不用再通过类的名字来调用该常量,我们只需要按下面的方法导入该类:
import static org.apache.mina.statemachine.event.IoHandlerEvents.*;
这样状态内容就被导入了。另外一个要改变的内容是我们自定了一个StateContext 状态上下文的实现--TapeDeckContext。这个类主要是用于返回当前录音机的状态的名字。
static class TapeDeckContext extends AbstractStateContext
{
public String tapeName;
}
要点:为什么不把状态的名字保存到IoSession中?我们可以将录音机状态的名字保存到IoSession中,但是使用一个自定义的StateContext来保存这个状态将会使这个类型更加安全。
最后需要注意的事情是playTape()方法使用了PlayCommand命令来作为它的最后的一个参数。最后一个参数和IoHandler's messageReceived(IoSession session, Object message)方法匹配。这意味着只有在客户端发送的信息被编码成layCommand命令时,该方法才会被调用。在录音机开始进行播放前,它要做的事情就是要装载磁带。当装载的命令从客户端发送过来时,服务器提供的磁带的数字代号将会从磁带列表中将可用的磁带的名字取出:
@IoHandlerTransition(on = MESSAGE_RECEIVED, in = EMPTY, next = LOADED)
public void loadTape(TapeDeckContext context, IoSession session, LoadCommand cmd) {
if (cmd.getTapeNumber() < 1 || cmd.getTapeNumber() > tapes.length) {
session.write("- Unknown tape number: " + cmd.getTapeNumber());
StateControl.breakAndGotoNext(EMPTY);
} else {
context.tapeName = tapes[cmd.getTapeNumber() - 1];
session.write("+ \"" + context.tapeName + "\" loaded");
}
}
这段代码使用了StateControl状态控制器来重写了下一个状态。如果用户指定了一个非法的数字,我们将不会将加载状态删除,而是使用一个空状态来代替。代码如下所示:StateControl.breakAndGotoNext(EMPTY);
状态控制器将会在后面的章节中详细的讲述。connect()方法将会在Mina开启一个会话并调用sessionOpened()方法时触发。@IoHandlerTransition(on = SESSION_OPENED, in = EMPTY)
public void connect(IoSession session) {
session.write("+ Greetings from your tape deck!");
}
它所做的工作就是向客户端发送欢迎的信息。状态机将会保持空的状态。pauseTape(), stopTape() 和 ejectTape() 方法和 playTape()很相似。这里不再进行过多的讲述。listTapes(), info() 和 quit() 方法也很容易理,也不再进行过多的讲解。请注意后面的三个方法是在根状态下使用的。这意味着listTapes(), info() 和 quit() 可以在任何状态中使用。
现在让我们看一下错误处理。error()将会在客户端发送一个非法的操作时触发:
@IoHandlerTransition(on = MESSAGE_RECEIVED, in = ROOT, weight = 10)
public void error(Event event, StateContext context, IoSession session, Command cmd) {
session.write("- Cannot " + cmd.getName() + " while "
+ context.getCurrentState().getId().toLowerCase());
}error()已经被指定了一个高于listTapes(), info() 和 quit() 的重量值来阻止客户端调用上面的方法。注意error()方法是怎样使用状态上下文来保存当前状态的ID的。字符串常量值由@State annotation (Empty, Loaded etc) 声明。这个将会由Mina的状态机当成状态的ID来使用。
commandSyntaxError()方法将会在ProtocolDecoder抛CommandSyntaxException 异常时被调用。它将会简单的输出客户端发送的信息不能解码为一个状态命令。
exceptionCaught() 方法将会在任何异常发生时调用,除CommandSyntaxException 异常(这个异常有一个较高的重量值)。它将会立刻关闭会话。最后一个@IoHandlerTransition的方法是unhandledEvent() ,它将会在@IoHandlerTransition中的方法没有事件匹配时调用。我们需要这个方法是因为我们没有@IoHandlerTransition的方法来处理所有可能的事件 (例如:我们没有处理messageSent(Event)方法)。没有这个方法,Mina的状态机将会在执行一个事件的时候抛出一个异常。最后一点我们要看的是那个类创建了IoHandler的代理,main()方法也在其中:
private static IoHandler createIoHandler() {
StateMachine sm = StateMachineFactory.getInstance(IoHandlerTransition.class).create(EMPTY, new TapeDeckServer());
return new StateMachineProxyBuilder().setStateContextLookup(
new IoSessionStateContextLookup(new StateContextFactory() {
public StateContext create() {
return new TapeDeckContext();
}
})).create(IoHandler.class, sm);
}
// This code will work with MINA 1.0/1.1:
public static void main(String[] args) throws Exception {
SocketAcceptor acceptor = new SocketAcceptor();
SocketAcceptorConfig config = new SocketAcceptorConfig();
config.setReuseAddress(true);
ProtocolCodecFilter pcf = new ProtocolCodecFilter(
new TextLineEncoder(), new CommandDecoder());
config.getFilterChain().addLast("codec", pcf);
acceptor.bind(new InetSocketAddress(12345), createIoHandler(), config);
}
// This code will work with MINA trunk:
public static void main(String[] args) throws Exception {
SocketAcceptor acceptor = new NioSocketAcceptor();
acceptor.setReuseAddress(true);
ProtocolCodecFilter pcf = new ProtocolCodecFilter(
new TextLineEncoder(), new CommandDecoder());
acceptor.getFilterChain().addLast("codec", pcf);
acceptor.setHandler(createIoHandler());
acceptor.setLocalAddress(new InetSocketAddress(PORT));
acceptor.bind();
}
createIoHandler() 方法创建了一个状态机,这个和我们之前所做的相似。除了我们一个IoHandlerTransition.class类来代替Transition.class 在StateMachineFactory.getInstance(...)方法中。这是我们在使用 @IoHandlerTransition 声明的时候必须要做的。当然这时我们使用了一个IoSessionStateContextLookup和一个自定义的StateContextFactory类,这个在我们创建一个IoHandler 代理时被使用到了。如果我们没有使用IoSessionStateContextLookup ,那么所有的客户端将会使用同一个状态机,这是我们不希望看到的。
main()方法创建了SocketAcceptor实例,并且绑定了一个ProtocolCodecFilter ,它用于编解码命令对象。最后它绑定了12345端口和IoHandler的实例。这个oHandler实例是由createIoHandler()方法创建的。
7 Mian的异步和同步
7.1 分析
在解释Half Sync/Half Async模式之前,先介绍一个亲身经历的项目。曾经使用一个通讯支撑模块EMF,该模块完成了底层的socket通讯功能,和外部应用建立长连接,同时为上层应用提供一个回调接口如下:
public interface Hook
{
void callback(Message msg);
}
上层应用可以根据自己业务逻辑的需要,实现该接口。
class MyHook implements Hook{
public void callback(Message msg)
{
//应用的业务逻辑
}
}
然后应用可以将该实现注册到通讯支撑模块中。
//注册应用的回调实现
EmfHook hook = new MyHook();
EmfService.regist(hook);
在EMF的实现中负责将受到的socket数据拼装成应用需要的消息结构,然后在一定的匹配规则,找到一个应用的Hook,回调之(其实是多个hook,有可能多个应用对同一种消息感兴趣,简化之)。
//从socket上收到数据,Byte[] buf=.....;
//将数据解析成应用的消息结构Message msg=parse(buf);
//根据消息内容查找相应的EmfHookEmfHook hook = findHook(msg);
//调用应用的实现hook.callback(msg);
一开始,这个模块运行的很好,性能不错,应用扩展也很方便,大家都很是满意,看来这个月的kpi一定不错,呵呵。然有一天,在做压力测试的时候发现,性能很差了,好久才能处理一条消息,数据吞吐量非常小,cpu却也不忙。这是怎么回事哪?
后来发现问题在于EMF的socket数据读取并解析工作和上层应用的hook操作是在一个线程中的,而某些应用的Hook实现有一些比较耗时操作,而所以导致在执行上层应用操作的时候,EMF并没有去进行I/O操作,读取数据,整个系统停在哪儿了。
最后稍作改动,将hook.callback(data)放在另外一个线程池中,这样EMF的线程和应用逻辑的线程就不再相互干扰,性能大大的提供。
从这个项目中得到的一个教训是:[size=small;]I/O密集操作的线程应该和业务逻辑的线程尽量分开[/size]。
7.2 引用
半同步半异步(Half Sync/Half Async)体系结构模式将并发系统中的异步和同步服务分开,简化了编程,同时又没有降低性能。
通俗一点说,Half Sync/Half Async模式其实就是将异步请求排队到一个同步队列中,然后再从队列中取出请求处理,其实就是一个扩大的生产者/消费者问题。在socket编程的关键在于,将业务逻辑操作和底层的io操作分散到不同的线程中,避免业务逻辑操作可能导致整个线程的堵塞。
在mina中也存在Half Sync/Half Async模式,在默认情况下,mina的业务逻辑处理接口IoHandler的实现类就是在NioProcessor 的worker现在中执行的(NioProcessor的执行机制请参考)。如果业务逻辑接口堵塞或者耗时都将导致NioProcessor线程无法充分利用。
ExecutorFilter
IoAcceptor acceptor = new NioSocketAcceptor();
acceptor.getFilterChain().addLast( "logger", new LoggingFilter() );
acceptor.getFilterChain().addLast( "codec", new ProtocolCodecFilter( new TextLineCodecFactory( Charset.forName( "UTF-8" ))));
acceptor.getFilterChain().addLast("executor", new ExecutorFilter());
acceptor.setHandler( new TimeServerHandler() );
acceptor.getSessionConfig().setReadBufferSize( 2048 );
acceptor.getSessionConfig().setIdleTime( IdleStatus.BOTH_IDLE, 10 );
acceptor.bind( new InetSocketAddress(PORT) );
如上述代码,通过在FilterChain中增加一个ExecutorFilter,mina将NioProcessor的IO操作线程和TimeServerHandler的业务处理线程分开了。在上述例子中,当NioProcessor收到socket上的数据,filter和handler的执行顺序为:logger->codec->executor->TimeServerHandler(有关IoFilter内容具体请参考http://uniseraph.javaeye.com/blog/228194)。其中logger->codec就在NioProcessor的工作者线程worker中,而executor和TimeServerHandler则是在executorFilter的线程池中。具体参考代码如下:
private ExecutorFilter(Executor executor, boolean createdExecutor, IoEventType... eventTypes) {
if (executor == null) {
throw new NullPointerException("executor");
}/*这里将evenType初始化为所有事件类型的集合*/
if (eventTypes == null || eventTypes.length == 0) {
eventTypes = new IoEventType[] { IoEventType.EXCEPTION_CAUGHT,
IoEventType.MESSAGE_RECEIVED, IoEventType.MESSAGE_SENT,
IoEventType.SESSION_CLOSED, IoEventType.SESSION_IDLE,
IoEventType.SESSION_OPENED, };
}
//下面省略
}
public final void messageReceived(NextFilter nextFilter, IoSession session,
Object message) {
/*executorFilter对该事件有效*/ if (eventTypes.contains(IoEventType.MESSAGE_RECEIVED)) {
fireEvent(new IoFilterEvent(nextFilter,
IoEventType.MESSAGE_RECEIVED, session, message));
} else {
nextFilter.messageReceived(session, message);
}
}
protected void fireEvent(IoFilterEvent event) {
getExecutor().execute(event);
}
由上可知,如果executorFilter对某个事件有效,那么将在线程池中执行该事件,如果无效则在原有的NioProcessor.Worker线程中执行下一个IoFilter或者IoHandler。当然也可以不用ExecutorFilter,而是在IoHandler的实现类中放一个线程池,但是多数情况下是没有这样的必要。
8 Mina的reactor机制
8.1 Reactor概念
从上面来两个图可以看出:与传统的单个Reactor模式实现不同,mina中采用了Multiple Reactor的方式。NioSocketAcceptor和NioProcessor使用不同selector,能够更加充分的榨取服务器的性能。
8.1.1 acctptor主要负责
(1) 绑定一个/多个端口,开始监听(五查询)
(2) 处理客户端的建链请求 (建场所)connect
(3) 关闭一个/多个监听端口 (断链接)close
8.1.2 processor主要负责
(1) 接受客户端发送的数据,并转发给业务逻辑成处理
(2) 发送数据到客户端
8.1.3 实例说明
首先看一个最简单的mina服务端程序,
SocketAcceptor acceptor = new NioSocketAcceptor();// 设定一个事件处理器
acceptor.setHandler(new EchoProtocolHandler()); // 绑定一个监听端口
acceptor.bind(new InetSocketAddress(PORT));
8.2 NioSocketAcceptor的机制 (招生处)
8.2.1 构造
在NioSocketAcceptor的构造函数中,把NioProcessor也构造出来了
protected AbstractPollingIoAcceptor(IoSessionConfig sessionConfig,
Class<? extends IoProcessor<T>> processorClass)
{
this(sessionConfig, null, new SimpleIoProcessorPool<T>(processorClass), true);
}
8.2.2 建立池(装备处)
为用户来的信息创建空间;即信号量大小,可以有多少处理线程。
SimpleIoProcessorPool是一个NioProcessor池,默认大小是cpu个数+1,这样能够充分利用多核的cpu 。private static final int DEFAULT_SIZE = Runtime.getRuntime().availableProcessors() + 1;
public SimpleIoProcessorPool(Class<? extends IoProcessor<T>> processorType)
{
this(processorType, null, DEFAULT_SIZE);
}
8.2.3 初始化
创建一个部门;调用init方法,准备selector
protected void init() throws Exception
{
selector = Selector.open();
}
到此,生产线已经装配好,就等按下开关,就可以正常运行了。
8.2.4 绑定端口
给部门一个业务。在调用NioSocketAcceptor.bind()函数的时候最终调用,AbstractPollingIoAcceptor.bind0,这是NioSocketAcceptor的关键所在
protected final Set<SocketAddress> bind0(
List<? extends SocketAddress> localAddresses) throws Exception
{
/*2.1.1registerQueue是一个待监听动作列表,每次要新绑定一个端口,增加一个register动作,在工作者线程中处理*/
AcceptorOperationFuture request = new AcceptorOperationFuture( localAddresses);
registerQueue.add(request);
/*2.1.2 创建并启动工作者线程*/
startupWorker();
wakeup();
//堵塞直至监听成功
request.awaitUninterruptibly();
//以下省略
}
通过实现生产者/消费者问题来处理绑定端口开始监听动作,其中生产者是bind动作,消费者在Work.run方法中,registQueue是消息队列
8.2.5 等待请求
堵塞直至绑定监听端口成功。招生处至到有学生来。
public class DefaultIoFuture implements IoFuture
{
public IoFuture awaitUninterruptibly()
{
synchronized (lock)
{
while (!ready)
{//当ready为true时候,跳出循环
waiters++;
try
{
lock.wait(DEAD_LOCK_CHECK_INTERVAL);
} catch (InterruptedException ie)
{
// Do nothing : this catch is just mandatory by contract
} finally
{
waiters--;
if (!ready)
{
checkDeadLock();
}
}
}
}
return this;
}
}
真正的绑定端口开始监听动作是在Woker线程中执行的 ,取消绑定操作与绑定操作类似,暂时先不描述。 AbstractPollingIoAcceptor的工作者线程是NioSocketAcceptor的核心所在,完成了以下三个主要功能:
1、处理绑定监听端口请求
2、处理取消监听端口绑定请求
3、处理socket连接请求
1,2主要是在系统初始化或者系统关闭的时候在registerQuerue/cancelQueue中增加一个消息,3是系统运行时NioSocketAcceptor处理socket建链请求的关键。1,2,3的主要内容在工作者线程Work.run方法中。 NioSocketAcceptor继承自父类AbstractPollingIoAcceptor的Worker.run 。
private class Worker implements Runnable
{
public void run()
{
int nHandles = 0;
while (selectable)
{
try {
boolean selected = select();
// this actually sets the selector to OP_ACCEPT,
// and binds to the port in which this class will
// listen on
nHandles += registerHandles();
if (selected)
{
processHandles(selectedHandles());
}
// check to see if any cancellation request has been made.
nHandles -= unregisterHandles();
if (nHandles == 0) {
synchronized (lock) {
if (registerQueue.isEmpty()
&& cancelQueue.isEmpty()) {
worker = null;
break;
}
}
}
} catch (Throwable e) {
ExceptionMonitor.getInstance().exceptionCaught(e);
try {
Thread.sleep(1000);
} catch (InterruptedException e1) {
ExceptionMonitor.getInstance().exceptionCaught(e1);
}
}
}
if (selectable && isDisposing())
{
selectable = false;
try {
if (createdProcessor) {
processor.dispose();
}
} finally {
try {
synchronized (disposalLock) {
if (isDisposing()) {
destroy();
}
}
} catch (Exception e) {
ExceptionMonitor.getInstance().exceptionCaught(e);
} finally {
disposalFuture.setDone();
}
}
}
}
8.2.5.1 监听连接请求循环
3.1 首先系统在一个无限循环中不停的允许,处理socket建链请求/端口监听/取消端口监听请求,selectable是一个标志位,初始化的时候置为true,要关闭时候置为false,则系统退出循环。
3.2 使用selector监听是否有连接请求操作。
public void run()
{
int nHandles = 0;
while (selectable)
{
try {
// Detect if we have some keys ready to be processed
boolean selected = select();
//以下省略
}
}
}
注意,虽然 boolean selected = select();是一个堵塞操作,但是run方法不会陷入死循环。因为即使没有新的连接请求到达,但是每次bind/unbind都会调用NioSocketAccepto.wakeup唤醒处于select状态的selctor。
protected void wakeup()
{
selector.wakeup();
}
而系统退出是,关闭的acceptor的dispose方法最终会调用unbind,所以退出时不会有问题。 3.2 从registerQueue中获取绑定请求消息,开始绑定某个端口,并开始监听,准备相关上下文信息
public void run()
{
int nHandles = 0;
while (selectable)
{
try {
// Detect if we have some keys ready to be processed
boolean selected = select();
// this actually sets the selector to OP_ACCEPT,
// and binds to the port in which this class will
// listen on
nHandles += registerHandles();
//以下省略
}
对于registerHandleres真正完成了对端口的监听
private int registerHandles()
{
for (;;)
{
AcceptorOperationFuture future = registerQueue.poll();
if (future == null)
{
return 0;
}
Map<SocketAddress, H> newHandles = new HashMap<SocketAddress, H>();
List<SocketAddress> localAddresses = future.getLocalAddresses();
try {
for (SocketAddress a : localAddresses)
{
H handle = open(a);
newHandles.put(localAddress(handle), handle);
}
boundHandles.putAll(newHandles);
// and notify.
future.setDone();
return newHandles.size();
} catch (Exception e)
{
future.setException(e);
} finally
{
// Roll back if failed to bind all addresses.
if (future.getException() != null) {
for (H handle : newHandles.values()) {
try {
close(handle);
} catch (Exception e) {
ExceptionMonitor.getInstance().exceptionCaught(e);
}
}
wakeup();
}
}
}
}
3.2.1 registerHandlers将registerQueue中的所有绑定监听消息取出来,在循环中处理,注意一个细节是,一个绑定监听消息AcceptorOperationFuture可能要绑定监听多个ip的同一个端口。3.2.2 registerHandlers的一个关键动作是 H handle = open(a);参看NioSocketAcceptor.open方法的实现,发现这就是关键
protected ServerSocketChannel open(SocketAddress localAddress)
throws Exception
{
ServerSocketChannel c = ServerSocketChannel.open();
boolean success = false;
try {
c.configureBlocking(false);
// Configure the server socket,
c.socket().setReuseAddress(isReuseAddress());
// XXX: Do we need to provide this property? (I think we need to remove it.)
c.socket().setReceiveBufferSize(
getSessionConfig().getReceiveBufferSize());
// and bind.
c.socket().bind(localAddress, getBacklog());
c.register(selector, SelectionKey.OP_ACCEPT);
success = true;
} finally {
if (!success) {
close(c);
}
}
return c;
}
8.2.5.2 开始监听一个端口
在这个方法中真正开始监听一个端口,并设置相关细节
1.采用非堵塞方式
2.可从用端口
3.socket读取的缓冲区大小
4.listen队列的长度
并且在selector中注册了对SelectionKey.OP_ACCEPT的关注。 3.2.3 通过 future.setDone();唤醒bind线程,告知绑定操作已经成功,否则bind线程还将继续堵塞。 3.3 如果发现有连接请求过来,则处理之
private class Worker implements Runnable
{
public void run()
{
int nHandles = 0;
while (selectable)
{
try {
// Detect if we have some keys ready to be processed
boolean selected = select();
// this actually sets the selector to OP_ACCEPT,
// and binds to the port in which this class will
// listen on
nHandles += registerHandles();
if (selected)
{
processHandles(selectedHandles());
}
8.2.5.3 完成新建一个连接
processHandles是负责完成新建一个连接,并将这个连接交给NioProcessor监控
private void processHandles(Iterator<H> handles) throws Exception
{
while (handles.hasNext())
{
H handle = handles.next();
handles.remove();
T session = accept(processor, handle);
if (session == null)
{
break;
}
finishSessionInitialization(session, null, null);
// add the session to the SocketIoProcessor
session.getProcessor().add(session);
}
}
3.3.1 accept方法创建了一个NioSocketSession, 3.3.2 finishSessionInitialization对这个session进行初始化,3.3.3 将session交给NioProcessor管理,这里有两个地方需要注意。3.3.3.1 processor是一个SimpleIoProcessorPool,里面有个多个NioProcessor,SimpleIoProcessor将轮询取一个processor负责管理新建的NioSocketSession 。3.3.3.2 NioProcessor.add方法只是将NioSocketSession放到一个newSessions的队列中,并启动NioProcessor的工作者线程。不会马上生效,要等NioProcessor的worker线程执行addNew的时候,才会真正开始管理新增的session,这个与Acceptor的bind类似。
public final void add(T session)
{
if (isDisposing())
{
throw new IllegalStateException("Already disposed.");
}
newSessions.add(session);
startupWorker();
}
至此NioSocketAcceptor的基本实现已经描述完毕,相信读者对也有一个初步的认识。(思考:1. 描述一下NioSocketAcceptor处理一个新连接请求的全过程?
2. 下面代码中,mina做了什么,是怎么关闭监听端口的? NioSocketAcceptor acceptor = new NioSocketAcceptor(); accetpro.dispose();)
8.3 NioProcessor 的机制(财务处、装备处)
8.3.1 基本功能
NioProcessor是mina中的另一个核心部分,与NioSocketAcceptor类似,NioProcessor三个主要功能是:
1、接受一个NioSession
2、出来NioSession上的read、write等事件
3、关闭一个NioSession
与NioSocketAcceptor类似,NioProcessor的实现采用了template模式,以上功能整体流程在NioProcessor的父类AbstractPollingIoProcessor中基本完成了,NioSocketAcceptor只是针对Nio的情况完成实现。如上图,NioSocketAcceptor创建了SimpleIoProcessorPool,SimpleIoProcessorPool中默认存在cpu数+1个NioProcessor,并且这些NioProcessor的工作者线程共享一个线程池。(开通了N条跑道,可以同时有N架飞机起落。)(1)接受一个NioSession :与NioSocketAcceptor新增一个端口绑定类似,NioProcessor.addSession只是将NioSocketAcceptor新建的NioSession放入一个消息队列中,由工作者线程负责初始化该NioSession,在selector为该session注册OP_READ事件。(2)关闭一个NioSession :与接受一个NioSession类似,不再描述。(3)NioSession的数据处理:为NioProcessor继承自AbstractPollingIoProcessor的工作者线程中完成主要功能。
8.3.2 Selector事件管理(财务处)
注意在NioProcessor中的select超时时间为1秒,这意味着最多一秒钟的时候,NioProcessor.Worker线程唤醒一次。而在NioSocketAcceptor.Work.run中select是没有超时时间的。下面Worker线程两次唤醒之间简称为一个周期,易知一个周期的长度小于等于一秒。
public void run()
{
int nSessions = 0;
lastIdleCheckTime = System.currentTimeMillis();
for (;;)
{
try {
boolean selected = select(1000);
nSessions += add();
updateTrafficMask();
if (selected)
{
process();
}
long currentTime = System.currentTimeMillis();
flush(currentTime);
nSessions -= remove();
notifyIdleSessions(currentTime);
if (nSessions == 0)
{
synchronized (lock)
{
if (newSessions.isEmpty() && isSelectorEmpty()) {
worker = null;
break;
}
}
}
// Disconnect all sessions immediately if disposal has been
// requested so that we exit this loop eventually.
if (isDisposing())
{
for (Iterator<T> i = allSessions(); i.hasNext(); )
{
scheduleRemove(i.next());
}
wakeup();
}
} catch (Throwable t)
{
ExceptionMonitor.getInstance().exceptionCaught(t);
try
{
Thread.sleep(1000);
} catch (InterruptedException e1)
{
ExceptionMonitor.getInstance().exceptionCaught(e1);
}
}
}
try {
synchronized (disposalLock)
{
if (isDisposing())
{
dispose0();
}
}
} catch (Throwable t)
{
ExceptionMonitor.getInstance().exceptionCaught(t);
} finally
{
disposalFuture.setValue(true);
}
}
8.3.2.1 获取一个请求队列
在addSession中从新增session队列newSeesion获取一个新增NioSession,并开始监控之。
private int add()
{
int addedSessions = 0;
for (;;)
{
T session = newSessions.poll();
if (session == null)
{
break;
}
if (addNow(session))
{
addedSessions ++;
}
}
return addedSessions;
}
2.1 在addNow(T session)中完成了单个NioSession的初始化
private boolean addNow(T session)
{
boolean registered = false;
boolean notified = false;
try
{
init(session);
registered = true;
// Build the filter chain of this session.
session.getService().getFilterChainBuilder().buildFilterChain(
session.getFilterChain());
// DefaultIoFilterChain.CONNECT_FUTURE is cleared inside here
// in AbstractIoFilterChain.fireSessionOpened().
((AbstractIoService) session.getService()).getListeners().fireSessionCreated(session);
notified = true;
} catch (Throwable e)
{
if (notified)
{
scheduleRemove(session);
session.getFilterChain().fireExceptionCaught(e);
wakeup();
} else
{
ExceptionMonitor.getInstance().exceptionCaught(e);
try
{
destroy(session);
} catch (Exception e1)
{
ExceptionMonitor.getInstance().exceptionCaught(e1);
} finally
{
registered = false;
}
}
}
return registered;
}
8.3.2.2 触发一个请求事件
2.1.1 初始化session,这里是Template Method的又一个体现,因为不同的类型的session初始化实现不同,在NioProcessor中包括:设置非堵塞模式.为该session注册OP_READ事件。
@Override
protected void init(NioSession session) throws Exception
{
SelectableChannel ch = (SelectableChannel) session.getChannel();
ch.configureBlocking(false);
session.setSelectionKey(ch.register(selector, SelectionKey.OP_READ, session));
}
2.1.2 构建IoFilterChain,具体请参考http://uniseraph.javaeye.com/blog/228194
2.1.3 触发NioSession上的相关事件,依次为sessionCreated->sessionOpened ->
IoServiceListener的sessionCreated 。从trafficControllingSessions获取需要修改session的traffic参数,具体与新增NioSession类似,不再详细描述。
8.3.3 Process响应处理(装备处)
关键内容来了,如果有NioSession的channel处理于OP_READ状态,则处理之
if (selected)
{
process();
}
process方法对于所有发生了OP_READ或OP_WRITE的NioSession依次进行处理,注意虽然在NioSession初始化的时候只注册了OP_READ事件,但是在上一周期调用session.write方法的时候,上一周期的flush方法将会注册OP_WRITE方法。本周期发送的数据都是上一周期确定的。
8.3.3.1 状态查询
private void process(T session)
{
if (isReadable(session) && session.getTrafficMask().isReadable())
{
read(session);
}
if (isWritable(session) && session.getTrafficMask().isWritable())
{
scheduleFlush(session);
}
}
在read方法中读取socket上的数据,调用发IoFilter,逐层传递到IoHander(具体参考http://uniseraph.javaeye.com/blog/228194);如果读到-1,则增加一个关闭连接消息到队列中;如果发生异常,则异常调用IoFilter和IoHandler的fireExceptionCaught方法
8.3.3.2 接收响应
private void read(T session)
{
IoSessionConfig config = session.getConfig();
IoBuffer buf = IoBuffer.allocate(config.getReadBufferSize());
final boolean hasFragmentation =
session.getTransportMetadata().hasFragmentation();
try
{
int readBytes = 0;
int ret;
try {
if (hasFragmentation)
{
while ((ret = read(session, buf)) > 0)
{
readBytes += ret;
if (!buf.hasRemaining())
{
break;
}
}
} else
{
ret = read(session, buf);
if (ret > 0)
{
readBytes = ret;
}
}
} finally
{
buf.flip();
}
if (readBytes > 0)
{
session.getFilterChain().fireMessageReceived(buf);
buf = null;
if (hasFragmentation)
{
if (readBytes << 1 < config.getReadBufferSize())
{
session.decreaseReadBufferSize();
} else if (readBytes == config.getReadBufferSize())
{
session.increaseReadBufferSize();
}
}
}
if (ret < 0)
{
scheduleRemove(session);
}
} catch (Throwable e)
{
if (e instanceof IOException)
{
scheduleRemove(session);
}
session.getFilterChain().fireExceptionCaught(e);
}
}
8.3.3.3 关闭连接
如果没有消息积累,也没有新创建的连接,则关闭线程池。