NIO的Selector介绍和例子代码

上一篇已经说到了缓冲区,NIO编程需要用到的。但是说到NIO编程的基础和重点,还是非Selector莫属,就是多路复用器。

下面先简单介绍一下Selector,然后再放个NIO编程的例子。

多路复用器(Selector),他是NIO编程的基础,非常的重要,它提供选择已经就绪的任务的能力。简单点说,就是Selector会不断地轮询注册在其上的通道(Channel),当然包括服务端和客户端的。如果某个通道发生了读写操作操作,这个通道就处于就绪状态,然后会被Selector轮询出来,然后通过SelectionKey可以取得就绪状态的Channel集合,从而进行后续的IO操作。

一个多路复用器(Selector)可以负责成千上万的Channel通道,没有上限的那种,这也是JDK使用了epoll代替了传统的select实现,获得连接句柄没有限制。这也意味着我们只要一个线程负责Selector的轮询,就可以接入成千上万个客户端,这也是JDK库的巨大进步。

Selector线程就类型一个管理者(Master),为什么说是线程,因为上面说道只要一个线程负责Selector的轮询。它可以管理成千上万的管道,只要轮询获得Channel集合,就可以获得就绪好的管道,然后获取管道准备好的数据,然后通知CPU执行IO的读取或者写入操作。

下面简单说一下这种模式。当IO事件(管道)注册到选择器以后,selector会分配给每个管道一个key值,相当于这个管道的标签。selector选择器是以轮询的方式进行查找注册的所有IO事件(管道),当我们的IO事件(管道)准备就绪后,selector就会识别,通过key值来找到对应的管道,进行相关的数据处理操作(从管道中读或写数据,写到我们的数据缓冲区中)。

下面说一下管道注册到多路复用器的不用的事件状态有哪些。这些状态是为了方便选择器查找。

SelectionKey.OP_CONNECT  连接状态

SelectionKey.OP_ACCEPT  接收状态

SelectionKey.OP_READ  读取状态

SelectionKey.OP_WRITE  写入状态

下面的例子只是简单的一个客户端连接服务端,然后往服务端写数据,服务端读取后打印出来。

Server代码:

public class Server implements Runnable{
	private Selector selector; //多路复用器
	private ByteBuffer readBuf = ByteBuffer.allocate(1024); //缓冲区
	public Server(int port){ //构造函数
		try {
			//1、打开多路复用器
			this.selector = Selector.open();
			//2、打开服务器通道
			ServerSocketChannel ssc = ServerSocketChannel.open(); 
			//3、服务器通道记得设置为feizuse
			ssc.configureBlocking(false);
			//4、服务器通道绑定地址
			SocketAddress local = new InetSocketAddress(port);
			ssc.bind(local);
			//5、将服务器通道注册到多路复用器那,并且监听阻塞事件
			ssc.register(this.selector, SelectionKey.OP_ACCEPT);
			System.out.println("服务器已经启动,端口号为:"+port);
		} catch (IOException e) {
			e.printStackTrace();
		} 
	}
	
	@Override
	public void run() {
		while(true){ //一直循环,让多路复用器一直监听
			try {
				//1、让多路复用器开始监听(必须的)
				this.selector.select();
				//2、返回多路复用器已经选择的结果集
				Set<SelectionKey> keys= this.selector.selectedKeys();
				//3、遍历结果集,进行处理
				Iterator<SelectionKey> ite = keys.iterator();
				while(ite.hasNext()){
					SelectionKey key = ite.next();
					//获取后直接从容器中移出就可以了
					ite.remove();
					if(key.isValid()){//如果key是有效的
						if(key.isAcceptable()){ //如果key为阻塞状态(这个和注册的时候绑定的状态对应)
							this.accept(key);
						}
						if(key.isReadable()){  //如果是可读状态
							this.read(key);
						}
					}
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		
	}
	
	private void accept(SelectionKey key) {
		
		try {
			 // 先获取服务器通道
			ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
			//调用服务器端的accpet方法获取客户端通道
			SocketChannel sc = ssc.accept();
			//设置为非阻塞
			sc.configureBlocking(false);
			//将客户端通道注册到多路复用器中
			sc.register(this.selector,SelectionKey.OP_READ);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * 这个方法是读取客户端发送给服务端的数据的。因为客户端通道注册时的注册状态为read
	 * @param key
	 */
	private void read(SelectionKey key) {
		
		try {
			//1、先清空缓冲区,防止有上一次的读数据
			this.readBuf.clear();
			//2、获取客户端通道
			SocketChannel sc = (SocketChannel) key.channel();
			//3、看客户端是否有输入
			int count = sc.read(this.readBuf); 
			if(count == -1){//如果没有数据
				sc.close();
				key.cancel();
			}
			//4、如果有数据则进行读取,读取之前记得要进行缓冲区复位。
			this.readBuf.flip();
			//5、根据缓冲区的数据长度创建对应大小的byte数组,接受缓冲区的数据
			byte[] data = new byte[this.readBuf.remaining()];
			//6、将缓冲区数据弄到byte数组里面
			this.readBuf.get(data);
			//7、将byte数组转为字符串打印出来
			String result = new String(data);
			System.out.println("Server接受到client的数据:"+result);
		} catch (IOException e) {
			e.printStackTrace();
		} 
	}

	public static void main(String[] args) {
//开启一个线程,保证多路复用器一直在轮询
		new Thread(new Server(8765)).start();
	}
}

Client代码:

public class Client {
	public static void main(String[] args){
		
		try {
			//1、创建以一个客户端通道
			SocketChannel sc = SocketChannel.open();
			//这里是服务器端的IP地址和端口号
			InetSocketAddress add = new InetSocketAddress("127.0.0.1", 8765);
			//客户端通道连接服务器端通道
			sc.connect(add);
			//定义缓冲区,拿来接收用户的输入
			ByteBuffer buf = ByteBuffer.allocate(1204);
			while(true){  //死循环,用户可以无限输入
				//定义一个字节数组,然后使用系统的输入功能
				byte[] bytes = new byte[1024];
				System.in.read(bytes);
				//将数据放入缓冲区
				buf.put(bytes);
				//记得进行复位操作
				buf.flip();
				//写出数据
				sc.write(buf);
				//清空缓冲区
				buf.clear();
			}
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}

猜你喜欢

转载自blog.csdn.net/howinfun/article/details/80935954