【正文】netty死磕1.4: html
Java NIO Selector 一文全解 java
Java NIO的核心组件包括:编程
(1)Channel(通道)服务器
(2)Buffer(缓冲区)网络
(3)Selector(选择器)异步
其中Channel和Buffer比较好理解 ,联系也比较密切,他们的关系简单来讲就是:数据老是从通道中读到buffer缓冲区内,或者从buffer写入到通道中。socket
选择器和他们的关系又是什么?ide
选择器(Selector) 是 Channel(通道)的多路复用器,Selector 能够同时监控多个 通道的 IO(输入输出) 情况。性能
Selector的做用是什么?学习
选择器提供选择执行已经就绪的任务的能力。从底层来看,Selector提供了询问通道是否已经准备好执行每一个I/O操做的能力。Selector 容许单线程处理多个Channel。仅用单个线程来处理多个Channels的好处是,只须要更少的线程来处理通道。事实上,能够只用一个线程处理全部的通道,这样会大量的减小线程之间上下文切换的开销。
并非全部的Channel,都是能够被Selector 复用的。比方说,FileChannel就不能被选择器复用。为何呢?
判断一个Channel 能被Selector 复用,有一个前提:判断他是否继承了一个抽象类SelectableChannel。若是继承了SelectableChannel,则能够被复用,不然不能。
SelectableChannel类是何方神圣?
SelectableChannel类提供了实现通道的可选择性所须要的公共方法。它是全部支持就绪检查的通道类的父类。全部socket通道,都继承了SelectableChannel类都是可选择的,包括从管道(Pipe)对象的中得到的通道。而FileChannel类,没有继承SelectableChannel,所以是否是可选通道。
通道和选择器注册以后,他们是绑定的关系吗?
答案是否是。不是一对一的关系。一个通道能够被注册到多个选择器上,但对每一个选择器而言只能被注册一次。
通道和选择器之间的关系,使用注册的方式完成。SelectableChannel能够被注册到Selector对象上,在注册的时候,须要指定通道的哪些操做,是Selector感兴趣的。
使用Channel.register(Selector sel,int ops)方法,将一个通道注册到一个选择器时。第一个参数,指定通道要注册的选择器是谁。第二个参数指定选择器须要查询的通道操做。
能够供选择器查询的通道操做,从类型来分,包括如下四种:
(1)可读 : SelectionKey.OP_READ
(2)可写 : SelectionKey.OP_WRITE
(3)链接 : SelectionKey.OP_CONNECT
(4)接收 : SelectionKey.OP_ACCEPT
若是Selector对通道的多操做类型感兴趣,能够用“位或”操做符来实现:int key = SelectionKey.OP_READ | SelectionKey.OP_WRITE ;
注意,操做一词,是一个是使用很是泛滥,也是一个容易混淆的词。特别提醒的是,选择器查询的不是通道的操做,而是通道的某个操做的一种就绪状态。
什么是操做的就绪状态?
一旦通道具有完成某个操做的条件,表示该通道的某个操做已经就绪,就能够被Selector查询到,程序能够对通道进行对应的操做。比方说,某个SocketChannel通道能够链接到一个服务器,则处于“链接就绪”(OP_CONNECT)。再比方说,一个ServerSocketChannel服务器通道准备好接收新进入的链接,则处于“接收就绪”(OP_ACCEPT)状态。还比方说,一个有数据可读的通道,能够说是“读就绪”(OP_READ)。一个等待写数据的通道能够说是“写就绪”(OP_WRITE)。
Channel和Selector的关系肯定好后,而且一旦通道处于某种就绪的状态,就能够被选择器查询到。这个工做,使用选择器Selector的select()方法完成。select方法的做用,对感兴趣的通道操做,进行就绪状态的查询。
Selector能够不断的查询Channel中发生的操做的就绪状态。而且挑选感兴趣的操做就绪状态。一旦通道有操做的就绪状态达成,而且是Selector感兴趣的操做,就会被Selector选中,放入选择键集合中。
一个选择键,首先是包含了注册在Selector的通道操做的类型,比方说SelectionKey.OP_READ。也包含了特定的通道与特定的选择器之间的注册关系。
开发应用程序是,选择键是编程的关键。NIO的编程,就是根据对应的选择键,进行不一样的业务逻辑处理。
选择键的概念,有点儿像事件的概念。
选择键和事件的关系是什么?
一个选择键有点儿像监听器模式里边的一个事件,可是又不是。因为Selector不是事件触发的模式,而是主动去查询的模式,因此不叫事件Event,而是叫SelectionKey选择键。
Selector对象是经过调用静态工厂方法open()来实例化的,以下:
// 一、获取Selector选择器 Selector selector = Selector.open();
Selector的类方法open()内部是向SPI发出请求,经过默认的SelectorProvider对象获取一个新的实例。
要实现Selector管理Channel,须要将channel注册到相应的Selector上,以下:
// 二、获取通道 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // 3.设置为非阻塞 serverSocketChannel.configureBlocking(false); // 四、绑定链接 serverSocketChannel.bind(new InetSocketAddress(SystemConfig.SOCKET_SERVER_PORT)); // 五、将通道注册到选择器上,并制定监听事件为:“接收”事件 serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);
上面经过调用通道的register()方法会将它注册到一个选择器上。
首先须要注意的是:
与Selector一块儿使用时,Channel必须处于非阻塞模式下,不然将抛出异常IllegalBlockingModeException。这意味着,FileChannel不能与Selector一块儿使用,由于FileChannel不能切换到非阻塞模式,而套接字相关的全部的通道均可以。
另外,还须要注意的是:
一个通道,并无必定要支持全部的四种操做。好比服务器通道ServerSocketChannel支持Accept 接受操做,而SocketChannel客户端通道则不支持。能够经过通道上的validOps()方法,来获取特定通道下全部支持的操做集合。
万事俱备,能够开干。下一步是查询就绪的操做。
经过Selector的select()方法,能够查询出已经就绪的通道操做,这些就绪的状态集合,包存在一个元素是SelectionKey对象的Set集合中。
下面是Selector几个重载的查询select()方法:
(1)select():阻塞到至少有一个通道在你注册的事件上就绪了。
(2)select(long timeout):和select()同样,但最长阻塞事件为timeout毫秒。
(3)selectNow():非阻塞,只要有通道就绪就马上返回。
select()方法返回的int值,表示有多少通道已经就绪,更准确的说,是自前一次select方法以来到这一次select方法之间的时间段上,有多少通道变成就绪状态。
一旦调用select()方法,而且返回值不为0时,下一步工干啥?
经过调用Selector的selectedKeys()方法来访问已选择键集合,而后迭代集合的每个选择键元素,根据就绪操做的类型,完成对应的操做:
Set selectedKeys = selector.selectedKeys(); Iterator keyIterator = selectedKeys.iterator(); while(keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); if(key.isAcceptable()) { // a connection was accepted by a ServerSocketChannel. } else if (key.isConnectable()) { // a connection was established with a remote server. } else if (key.isReadable()) { // a channel is ready for reading } else if (key.isWritable()) { // a channel is ready for writing } keyIterator.remove(); }
处理完成后,直接将选择键,从这个集合中移除,防止下一次循环的时候,被重复的处理。键能够但不能添加。试图向已选择的键的集合中添加元素将抛出java.lang.UnsupportedOperationException。
package com.crazymakercircle.iodemo.base; import com.crazymakercircle.config.SystemConfig; import java.io.IOException; import java.net.InetSocketAddress; 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; public class SelectorDemo { static class Client { /** * 客户端 */ public static void testClient() throws IOException { InetSocketAddress address= new InetSocketAddress(SystemConfig.SOCKET_SERVER_IP, SystemConfig.SOCKET_SERVER_PORT); // 一、获取通道(channel) SocketChannel socketChannel = SocketChannel.open(address); // 二、切换成非阻塞模式 socketChannel.configureBlocking(false); // 三、分配指定大小的缓冲区 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); byteBuffer.put("hello world ".getBytes()); byteBuffer.flip(); socketChannel.write(byteBuffer); socketChannel.close(); } public static void main(String[] args) throws IOException { testClient(); } } static class Server { public static void testServer() throws IOException { // 一、获取Selector选择器 Selector selector = Selector.open(); // 二、获取通道 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // 3.设置为非阻塞 serverSocketChannel.configureBlocking(false); // 四、绑定链接 serverSocketChannel.bind(new InetSocketAddress(SystemConfig.SOCKET_SERVER_PORT)); // 五、将通道注册到选择器上,并注册的操做为:“接收”操做 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); // 六、采用轮询的方式,查询获取“准备就绪”的注册过的操做 while (selector.select() > 0) { // 七、获取当前选择器中全部注册的选择键(“已经准备就绪的操做”) Iterator<SelectionKey> selectedKeys = selector.selectedKeys().iterator(); while (selectedKeys.hasNext()) { // 八、获取“准备就绪”的时间 SelectionKey selectedKey = selectedKeys.next(); // 九、判断key是具体的什么事件 if (selectedKey.isAcceptable()) { // 十、若接受的事件是“接收就绪” 操做,就获取客户端链接 SocketChannel socketChannel = serverSocketChannel.accept(); // 十一、切换为非阻塞模式 socketChannel.configureBlocking(false); // 十二、将该通道注册到selector选择器上 socketChannel.register(selector, SelectionKey.OP_READ); } else if (selectedKey.isReadable()) { // 1三、获取该选择器上的“读就绪”状态的通道 SocketChannel socketChannel = (SocketChannel) selectedKey.channel(); // 1四、读取数据 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); int length = 0; while ((length = socketChannel.read(byteBuffer)) != -1) { byteBuffer.flip(); System.out.println(new String(byteBuffer.array(), 0, length)); byteBuffer.clear(); } socketChannel.close(); } // 1五、移除选择键 selectedKeys.remove(); } } // 七、关闭链接 serverSocketChannel.close(); } public static void main(String[] args) throws IOException { testServer(); } } }
NIO编程的难度比同步阻塞BIO大不少。
请注意以上的代码中并无考虑“半包读”和“半包写”,若是加上这些,代码将会更加复杂。
(1)客户端发起的链接操做是异步的,能够经过在多路复用器注册OP_CONNECT等待后续结果,不须要像以前的客户端那样被同步阻塞。
(2)SocketChannel的读写操做都是异步的,若是没有可读写的数据它不会同步等待,直接返回,这样I/O通讯线程就能够处理其余的链路,不须要同步等待这个链路可用。
(3)线程模型的优化:因为JDK的Selector在Linux等主流操做系统上经过epoll实现,它没有链接句柄数的限制(只受限于操做系统的最大句柄数或者对单个进程的句柄限制),这意味着一个Selector线程能够同时处理成千上万个客户端链接,并且性能不会随着客户端的增长而线性降低。所以,它很是适合作高性能、高负载的网络服务器。
代码工程: JavaNioDemo.zip
下载地址:在疯狂创客圈QQ群文件共享。
疯狂创客圈 Netty 死磕系列 10多篇深度文章: 【博客园 总入口】 QQ群:104131248