NIO揭秘-5

Networking and asynchronous I/O

        网络是学习AIO的一个绝佳的平台,同时也是java语言的基础知识,网络中操作NIO和平常操作NIO几乎没什么差别,同样也是依赖与channel和buffer,获得channel也是同样通过InputStream和outputStream。

    在这一节中我们将学习AIO的一些基本知识----什么是AIO什么不是,同样也会以例子说明。

    Asynchronous I/O

    异步IO是一种非阻塞试的读写方法。通常地,当你调用read()方法时,代码将阻塞直到数据的到来。同样的,write()方法将阻塞至数据被写入。

    异步IO的调用是通过对指定的IO事件的注册实现的---比如可读数据的到来、新套接字的连接等等,并且系统会在这些事情到来时通知你。

    异步IO的一个优点就是能在同时处理很多IO。同步程序经常需要借助于池或者创建许多线程来应对许多连接。但在AIO中,你可以在任意channel上监听IO事件,而不需要池和其余的线程。

    我们借助一个MultiPortEcho.java这个程序来讲解AIO。这个程序就像一个传统的回显服务端,它将网络上传来的数据发送给他能发送到的连接上。但是,它增加了一个特性,他能同时监听多个端口,并且处理他们的请求,所有的事情都在一个线程中完成。

    Selectors

    AIO中最主要的部件就是Selector,Selector是你注册各种各样IO事件的地方,并且也是他在事件发生的时候通知你。

    好,我们先来看看Selector的创建。

    Selector selector = Selector.open();

    然后,我们在不同的channel上调用register()方法,目的是在这些channel上注册我们感兴趣的IO事件。register()的第一个参数往往是selector。

    Opening a ServerSocketChannel

    为了接受连接请求,我们需要一个ServerSocketChannel。实际上,我们在每一个我们监听的端口上都需要一个。对于每个端口,我们像下面这样开一个ServerSocketChannel。

ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking( false );
ServerSocket ss = ssc.socket();
IntetSocketAddress address = new InetSocketAddress( ports[i] );
ss.bind( address );

   第一行我们创建了一个ServerSocketChannel,后三行我们将它绑定在了一个指定的端口上。第二行我们设置了ServerSocketChannel为非阻塞式的,我们必须在每一个socket channel上调用这个方法,否则AIO将不工作。

    Selection Keys

    我们下一步将在新创建的ServerSocketChannel上注册selector,我们使用register(),就像下面这样:

SelectionKey key = ssc.register( selector , SelectionKey.OP_ACCEPT );

    register()方法的第一个参数一般是Selector,第二个参数是OP_ACCEPOT,他指定了我们希望监听连接事件,换句话说,当一个连接被创建的时候,就是这个事件触发的时候。当然,这只是ServerSocketChannel众多事件的一种。

    注意我们调用register()方法的返回值,SelectionKey代表将selector注册到channel上。

    The inner loop

    现在我们已经注册了我们感兴趣的IO事件,即将进入主循环,每个使用selectors的程序的循环都像下面这样:

int num = selector.select();

Set selectedKeys = selector.selectedKeys();
Iterator it = selectedKeys.iterator();

while( it.hasNext() ){
    SelectionKey key = (SelectionKey)it.next();
    //deal with i/o event
}

    首先,我们为selector调用select,这个方法将阻塞知道有注册的事件到来,当一个或多个事件发生时,select()方法返回发生事件的数目。

    然后我们调用Selector的selectedKeys方法,他将返回所有发生了的事件的selectionKey的集合。

    然后我们通过迭代selectionKeys来处理每个事件。对于每个selectionkey,你必须知道是哪一个IO对象所触发的。

    Listening for new connections

    当程序执行到这里时,我们仅仅只注册了serversocketchannel,并且只是注册了accept事件。为了验证这个,我们调用readyOps()方法来查看哪种事件发生了。

if ( ( key.readyOps() & SelectionKey.OP_ACCEPT ) == SelectionKey.OP_ACCEPT ){
    
    // accept the new connection

}

    可以确信的是,readyOps()方法告诉了我们是一个新连接的事件。

    Accepting a new connection

    因为我们知道在server socket上有一个新连接的事件,我们可以安全的接受它。换句话说,不用担心accpet()操作会阻塞。

    ServerSocketChannel ss = ( ServerSocketChannel )key.channel();

    SocketChannel sc = ssc.accept();

    我们下一步要配置新连接到socketChannel是非阻塞的,并且接受连接的目的是为了从socket中读取数据,所以我们必须在socketChannel上注册selector,就像下面这样:

sc.configureBlocking( false );
SelectionKey newKey = sc.register( selector,SelectionKey.OP_READ );

    注意我们使用OP_READ参数在socketChannel上注册了读事件。

    Remvoing the processed SelectionKey

    在处理selectionKey后,我们要准备从主循环中退出了,但首先我们需要从seleced keys的集合中移除selectionKey。如果我们不易出这个key,它将在主循环中仍然处于集合状态,也就是说还会在处理一遍。我们调用迭代器的remove方法来移除处理过后的selectionKey.

    it.remove();

    现在我们退出主循环以便接受新来的数据。

    Incoming I/O

    当数据从socket中到达时,它将触发一个IO事件。这将调用主循环中的Selector.select( )。此时,selectionKey将被标记为OP_READ事件,就像下面这样:

   

else if ( (key.readyOps() & SelectionKey.OP_READ) == SelectionKey.OP_READ ){
    
    //read the data
    SocketChannel sc = ( SocketChannel )key.channel(); 
    //....

}

    和上面一样,我们得到触发IO事件的channel,因为仅仅是做回显程序,所以我们直接读入数据然后转发。

    Back to the main loop

    每次我们调用selector的select方法时,我们将得到selectionKeys的集合。每一个key代表这一个IO事件,我们处理这个事件,然后移除处理后的key,然后返回到主循环的顶部。

    这个程序是个过分简单的,因为他旨在演示AIO的技术。在真实的运用中,我们需要处理关闭了的channel。并且你希望使用不止一个线程,这个程序之所以可以使用单线程是因为他是一个demo,但在真是的世界中,还是建议大家使用线程池来做。

   

package com.nio.example;

import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class MultiPortEcho {
	
	private int[] port;
	
	public MultiPortEcho(int[] port){
		try{
			this.port = port;
			echo();
		}catch(Exception e){
			e.printStackTrace();
			throw new ExceptionInInitializerError( "多端口回显程序初始化失败!" );
		}
	}
	
	public void echo() throws Exception{
		
		/**首先创建一个Selector*/
		Selector selector = Selector.open();
		
		/**然后对每个port都进行监听,注册事件*/
		for( int i = 0 ; i < port.length ; ++i ){
			
			/**根据serversocketchannel得到一个serversocket*/
			ServerSocketChannel ssc = ServerSocketChannel.open();
			/** 设置为不阻塞 */
			ssc.configureBlocking(false);
			
			ServerSocket ss = ssc.socket();
			
			/** 绑定端口 */
			InetSocketAddress address = new InetSocketAddress(port[i]);
			ss.bind(address);
			
			/** 注册连接事件 */
			ssc.register(selector, SelectionKey.OP_ACCEPT);
		}
		
		/** 注册完事件后,就需要对新连接进行处理了 */
		while( true ){
			
			/** 文档上说当注册的事件到达时,会调用selector的select方法,应该是
			 * 阻塞 */
			int num = selector.select();
			
			/** 对每个Key进行轮询 */
			Set<SelectionKey> sks = selector.selectedKeys();
			Iterator<SelectionKey> it = sks.iterator();
			
			while( it.hasNext() ){
				
				SelectionKey sk = it.next();
				
				/** 如果是新连接事件,则接受连接 */
				if((sk.readyOps() & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT ){
					
					/** 取得新连接上来的channel */
					ServerSocketChannel s = (ServerSocketChannel)sk.channel();
					SocketChannel sc = s.accept();
					
					/** 给此channel注册读取事件 */
					sc.configureBlocking(false);
					sc.register(selector, SelectionKey.OP_READ);
					
					/** 移除此key  !!!!! very important*/
					it.remove();
					
					System.out.println( "a new connection : " + sc );
					
				}else if( (sk.readyOps() & SelectionKey.OP_READ) == SelectionKey.OP_READ ){
					
					/** 如果是读事件,则取得数据 */
					SocketChannel s = (SocketChannel)sk.channel();
					
					ByteBuffer buffer = ByteBuffer.allocate(1024);
					
					while( -1 != s.read(buffer)){
						
						buffer.flip();
						s.write(buffer);
						buffer.clear();
					}
					
					System.out.println( " data arrive : " + s );
					
					it.remove();
					
				}
				
			}
		}
		
	}
	
	public static void main(String[] args) {
		
		int []port = new int[]{8001,8002};
		new MultiPortEcho(port);
		
	}
	
	
	
}

 说明:此代码部分参考了网上的代码,并非原创。

转载于:https://my.oschina.net/zhangya/blog/30582

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值