Prequel
Mina通讯框架其实是基于java jdk的Selector,并作了各种封装处理。Selector可以同时监听多个事件(IO事件),并在一些事件到达后返回。
IO模式
说到这里就要啰嗦一下IO的事情了:
高性能的I/O设计中有两个比较著名的模式: Reactor和Proactor模式. Reactor模式用于同步I/O,而Proactor运用于异步I/O操作。
哪什么是同步和异步呢?同步和异步是针对应用程序和内核的交互而言的,是一种信息通讯机制。同步指的是用户进程触发IO操作并等待或者轮询的去查看IO操作是否就绪;而异步是指用户进程触发IO操作以后便开始做自己的事情,而当IO操作完成的时候会得到IO完成的通知。
说到同步和异步,就不得不说说阻塞和非组塞了。阻塞和非阻塞是针对于进程在访问数据的时候,根据IO操作的就绪状态来采取的不同方式,说白了是一种读取或者写入操作函数的实现方式。阻塞方式下读取或者写入函数将一直等待;而非阻塞方式下,读取或者写入函数会立即返回一个状态值,阻塞和非阻塞可以说影响着线程的状态,阻塞模式下线程是挂起的,不能再干活了,非阻塞模式下线程可以继续干活。
From the above discussion, it can be found that I/O模型可以分为:同步阻塞,同步非阻塞,异步阻塞,异步非阻塞IO.
- 同步阻塞IO:
用户进程在发起一个IO操作以后,必须等待IO操作的完成,只有当真正完成了IO操作以后,用户进程才能运行。JAVA传统的IO模型属于此种方式.
- 同步非阻塞IO:
用户进程发起一个IO操作以后边可返回做其它事情,但是用户进程需要时不时的询问IO操作是否就绪,这就要求用户进程不停的去询问,从而引入不必要的CPU资源浪费。其中目前JAVA的NIO就属于同步非阻塞IO。
- 异步阻塞IO:
应用发起一个IO操作以后,不等待内核IO操作的完成,等内核完成IO操作以后会通知应用程序,这其实就是同步和异步最关键的区别,同步必须等待或者主动的去询问IO是否完成,那么为什么说是阻塞的呢?因为此时是通过select系统调用来完成的,而select函数本身的实现方式是阻塞的,而采用select函数有个好处就是它可以同时监听多个文件句柄,从而提高系统的并发性!
- 异步非阻塞IO:
用户进程只需要发起一个IO操作然后立即返回,等IO操作真正的完成以后,应用程序会得到IO操作完成的通知,此时用户进程只需要对数据进行处理就好了,不需要进行实际的IO读写操作,因为真正的IO读取或者写入操作已经由内核完成了。目前Java中还没有支持此种IO模型。
示例代码
以下是使用Selector的一个例子。其中select()函数是阻塞的,会等待至少一个事件到达,但是select可以监听多个IO事件,有一个IO到达就会唤醒,尽管需要while循环轮询,但站在多个IO事件这个层面上来看,是一个同步的非阻塞IO。从下面的代码中可以感受下。
public class SelectorServer {
public void process() throws IOException {
//创建一个selector
Selector selector = Selector.open();
//创建一个socket通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
InetSocketAddress address = new InetSocketAddress("localhost", 9527);
serverSocketChannel.socket().bind(address);
serverSocketChannel.configureBlocking(false);
//注册到selector并监听
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 得到selector所捕获的事件数量
System.out.println("listen");
while (true) {
int n = selector.select();
if (n > 0) { // 当真正捕获到事件时,才执行相应操作
Set selectedKeys = selector.selectedKeys(); // 获取捕获到的事件集合
Iterator i = selectedKeys.iterator();
while (i.hasNext()) {
SelectionKey s = (SelectionKey) i.next(); // 对事件一一处理
//一个key被处理完成后,就都被从就绪关键字(ready keys)列表中除去
i.remove();
if (s.isAcceptable()) // 表示该事件为OP_ACCEPT事件
{
System.out.println("accept");
// 从channel()中取得我们刚刚注册的ServerSocketChannel。
// 为请求获取新的SocketChannel
SocketChannel sc = ((ServerSocketChannel) s.channel()).accept().socket().getChannel();
sc.configureBlocking(false); // 设置SocketChannel为非阻塞方式
sc.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
// 将新的SocketChannel注册到selector中,注册事件为OP_READ和OP_WRITE
} else {
ByteBuffer clientBuffer = ByteBuffer.allocate(4096);
if (s.isReadable()) { // 读信息
System.out.println("read");
SocketChannel channel = (SocketChannel) s.channel(); // 获取相应的SocketChannel
int count = channel.read(clientBuffer); // 将数据读入clientBuffer
if (count > 0) { // 当有数据读入时
clientBuffer.flip(); // 反转此缓冲区
// 如果需要,对缓冲区中的字符进行解码
//处理数据
System.out.println(getChars(clientBuffer.array()));
}
} else if (s.isWritable()) {
SocketChannel channel = (SocketChannel) s.channel(); // 获取相应的SocketChannel
channel.write(ByteBuffer.wrap("abc".getBytes()));
}
}
}
}
}
}
private byte[] getBytes(char[] chars) {//将字符转为字节(编码)
Charset cs = Charset.forName("UTF-8");
CharBuffer cb = CharBuffer.allocate(chars.length);
cb.put(chars);
cb.flip();
ByteBuffer bb = cs.encode(cb);
return bb.array();
}
private char[] getChars(byte[] bytes) {//将字节转为字符(解码)
Charset cs = Charset.forName("UTF-8");
ByteBuffer bb = ByteBuffer.allocate(bytes.length);
bb.put(bytes);
bb.flip();
CharBuffer cb = cs.decode(bb);
return cb.array();
}
public static void main(String args[]) throws IOException {
new SelectorServer().process();
}
}
Client代码:
public class EchoClient {
private final int port = 9527;
private final Socket socket;
public EchoClient() throws IOException {
socket = new Socket("localhost", port);
socket.setKeepAlive(true);
System.out.println("client s1111tatup锛宻end size" + socket.getSendBufferSize());
}
public String echo(String msg) {
return "echo:" + msg;
}
public void call() {
try {
System.out.println(socket.getInetAddress().getHostAddress() + "," + socket.getPort());
PrintWriter pWriter = getWriter(socket);
BufferedReader localReader = new BufferedReader(new InputStreamReader(System.in, Charset.forName("utf-8")));
String msg = null;
while ((msg = localReader.readLine()) != null) {
System.out.println(msg);
pWriter.println(msg);
if (msg.equals("bye")) {
break;
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private PrintWriter getWriter(Socket socket) throws IOException {
OutputStream socketOut = socket.getOutputStream();
return new PrintWriter(socketOut, true);
}
public static void main(String arg0[]) throws IOException {
new EchoClient().call();
}
}
Reactor&Proactor
了解了IO模式之后,我们还要了解到 Mina是一种Reactor模式。
下面是转载过来的内容:
两种IO多路复用方案:Reactor and Proactor
一般情况下,I/O 复用机制需要事件分享器(event demultiplexor [1, 3]). 事件分享器的作用,即将那些读写事件源分发给各读写事件的处理者,就像送快递的在楼下喊: 谁的什么东西送了, 快来拿吧。开发人员在开始的时候需要在分享器那里注册感兴趣的事件,并提供相应的处理者(event handlers),或者是回调函数; 事件分享器在适当的时候会将请求的事件分发给这些handler或者回调函数.
涉及到事件分享器的两种模式称为:Reactor and Proactor [1]. Reactor模式是基于同步I/O的,而Proactor模式是和异步I/O相关的. 在Reactor模式中,事件分离者等待某个事件或者可应用或个操作的状态发生(比如文件描述符可读写,或者是socket可读写),事件分离者就把这个事件传给事先注册的事件处理函数或者回调函数,由后者来做实际的读写操作。
而在Proactor模式中,事件处理者(或者代由事件分离者发起)直接发起一个异步读写操作(相当于请求),而实际的工作是由操作系统来完成的。发起时,需要提供的参数包括用于存放读到数据的缓存区,读的数据大小,或者用于存放外发数据的缓存区,以及这个请求完后的回调函数等信息。事件分离者得知了这个请求,它默默等待这个请求的完成,然后转发完成事件给相应的事件处理者或者回调。举例来说,在Windows上事件处理者投递了一个异步IO操作(称有overlapped的技术),事件分离者等IOCompletion事件完成[1]. 这种异步模式的典型实现是基于操作系统底层异步API的,所以我们可称之为“系统级别”的或者“真正意义上”的异步,因为具体的读写是由操作系统代劳的。
举另外个例子来更好地理解Reactor与Proactor两种模式的区别。这里我们只关注read操作,因为write操作也是差不多的。下面是Reactor的做法:
某个事件处理者宣称它对某个socket上的读事件很感兴趣;
事件分离者等着这个事件的发生;
当事件发生了,事件分离器被唤醒,这负责通知先前那个事件处理者;
事件处理者收到消息,于是去那个socket上读数据了. 如果需要,它再次宣称对这个socket上的读事件感兴趣,一直重复上面的步骤;
下面再来看看真正意义的异步模式Proactor是如何做的:
事件处理者直接投递发一个写操作(当然,操作系统必须支持这个异步操作). 这个时候,事件处理者根本不关心读事件,它只管发这么个请求,它魂牵梦萦的是这个写操作的完成事件。这个处理者很拽,发个命令就不管具体的事情了,只等着别人(系统)帮他搞定的时候给他回个话。
事件分离者等着这个读事件的完成(比较下与Reactor的不同);
当事件分离者默默等待完成事情到来的同时,操作系统已经在一边开始干活了,它从目标读取数据,放入用户提供的缓存区中,最后通知事件分离者,这个事情我搞完了;
事件分享者通知之前的事件处理者: 你吩咐的事情搞定了;
事件处理者这时会发现想要读的数据已经乖乖地放在他提供的缓存区中,想怎么处理都行了。如果有需要,事件处理者还像之前一样发起另外一个写操作,和上面的几个步骤一样。