NIO详细介绍(Selector,Channel)
1.Selector(多路复用)
原先的bio中,一个客户端连接,就为它分配一个线程。这样的问题,当用户激增时候,线程会增加很多,增加服务器开销。
所以后来使用了线程池进行管理线程,但是有个弊端,如果线程池有100个线程,这个时候第101个就会等待。传统的bio(Server/Client)如下图:
有这个弊端,Nio就用selector解决。
NIO中非阻塞I/O 采用了基于Reactor模式的工作方式,I/O 调用不会被阻塞,相反是注册感兴趣的特定I/O 事件,如可读数据到
达,新的套接字连接等等,在发生特定事件时,系统再通知我们。NIO中实现非阻塞I/O的核心对象就是Selector,Selector 就是
注册各种I/O 事件地方,而且当那些事件发生时,就是这个对象告诉我们所发生的事件,如下图所示:
从图中可以看出,当有读或写等任何注册的事件发生时,可以从Selector 中获得相应的SelectionKey,同时从 SelectionKey中可
以找到发生的事件和该事件所发生的具体的SelectableChannel,以获得客户端发送过来的数据。
使用NIO中非阻塞I/O 编写服务器处理程序,大体上可以分为下面三个步骤:
1. 向Selector 对象注册感兴趣的事件。
2. 从Selector 中获取感兴趣的事件。
3. 根据不同的事件进行相应的处理。
/* * 注册事件 */
private Selector getSelector() throws IOException {
// 创建 Selector 对象
Selector sel = Selector.open();
// 创建可选择通道,并配置为非阻塞模式
ServerSocketChannel server = ServerSocketChannel.open();
server.configureBlocking(false);
// 绑定通道到指定端口
ServerSocket socket = server.socket();
InetSocketAddress address = new InetSocketAddress(port);
socket.bind(address);
// 向 Selector 中注册感兴趣的事件
server.register(sel, SelectionKey.OP_ACCEPT); return sel;
}
创建了ServerSocketChannel对象,并调用 configureBlocking()方法,配置为非阻塞模式,接下来的三行代码把该通道绑定到指定端口,最后向Selector 中注册事件,此处指定的是参数是OP_ACCEPT,即指定我们想要监听accept 事件,也就是新的连接发 生时所产生的事件,对于ServerSocketChannel 通道来说,我们唯一可以指定的参数就是OP_ACCEPT。
当Selector 中获取感兴趣的事件,即开始监听,进入内部循环:
public void listen(){ System.out.println("listen on " + this.port + "."); try { //轮询主线程 while (true){ //大堂经理再叫号 selector.select(); //每次都拿到所有的号子 Set<SelectionKey> keys = selector.selectedKeys(); Iterator<SelectionKey> iter = keys.iterator(); //不断地迭代,就叫轮询 //同步体现在这里,因为每次只能拿一个key,每次只能处理一种状态 while (iter.hasNext()){ SelectionKey key = iter.next(); iter.remove(); //每一个key代表一种状态 //没一个号对应一个业务 //数据就绪、数据可读、数据可写 等等等等 process(key); } } } catch (IOException e) { e.printStackTrace(); } }
在非阻塞I/O 中,内部循环模式基本都是遵循这种方式。首先调用select()方法,该方法会阻塞,直到至少有一个事件发生,然后
再使用selectedKeys()方法获取发生事件的SelectionKey,再使用迭代器进行循环。
最后根据不同事件进行不同处理:
private void process(SelectionKey key) throws IOException { //针对于每一种状态给一个反应 if(key.isAcceptable()){ ServerSocketChannel server = (ServerSocketChannel)key.channel(); //这个方法体现非阻塞,不管你数据有没有准备好 //你给我一个状态和反馈 SocketChannel channel = server.accept(); //一定一定要记得设置为非阻塞 channel.configureBlocking(false); //当数据准备就绪的时候,将状态改为可读 key = channel.register(selector,SelectionKey.OP_READ); } else if(key.isReadable()){ //key.channel 从多路复用器中拿到客户端的引用 SocketChannel channel = (SocketChannel)key.channel(); int len = channel.read(buffer); if(len > 0){ buffer.flip(); String content = new String(buffer.array(),0,len); key = channel.register(selector,SelectionKey.OP_WRITE); //在key上携带一个附件,一会再写出去 key.attach(content); System.out.println("读取内容:" + content); } } else if(key.isWritable()){ SocketChannel channel = (SocketChannel)key.channel(); String content = (String)key.attachment(); channel.write(ByteBuffer.wrap(("输出:" + content).getBytes())); channel.close(); } }
2.Channel
通道是一个对象。我们用来读取和输出对象。里面的数据我们不是用bio中的字节流处理,而是用buffer缓冲区。是将数据从通
道读入缓冲区,再从缓冲区获取这个字节。
在NIO 中,提供了多种通道对象,而所有的通道对象都实现了 Channel 接口。它们之间的继承关系如下图所示: