参考资料:
老外写的教程,很适合入门:http://tutorials.jenkov.com/java-nio/index.html
上面教程的译文:http://ifeve.com/overview/html
示例代码:
https://github.com/gordonklg/study,socket modulejava
由于有现成的教程,本文只作摘要。git
NIO 有三宝,channel、buffer、selectorgithub
Channel 与 Stream 很类似,除了:编程
Buffer 本质上是一个内存块,Buffer 包装了这个内存块,提供一系列方法简化在该内存块上的数据读写操做。服务器
Buffer 有三个属性:网络
其中 capacity 只能在建立时指定,没法修改。其它两个属性都有对应的读取与设值方法。dom
Buffer 及 Channel 主要方法的手绘示意图以下:
异步
Selector 设计目的是使单线程能够处理多个网络链接(多个 Channel)。对于存在大量链接可是每一个链接占用带宽都很少的应用,例如聊天工具、滴滴收集车辆位置信息、物联网收集设备信息等,传统 Socket 编程须要为每个链接分配一个处理线程,占用大量系统资源。咱们须要一种方案,可让一个线程负责多个链接。
socket
Selector 容许 Channel 注册到本身身上,SelectionKey 表示 channel 与 selector 的注册关系。
Channel 能产生4种事件,分别是:
能够设置 Selector 关注 Channel 的哪些事件。Selector 的 select() 方法会阻塞,直到注册的 Channel 产生了指定类型的事件(实际意义就是 Channel 已经准备好作某事了)。接着就能够经过 Selector 获取全部已经准备好的 SelectionKey(即Channel),依次处理相应事件,例如创建链接、获取数据、业务处理、发送数据等。
显然,同一个 selector 的全部 channel 对数据的读写以及业务逻辑的实现,在默认状况下,都是在同一个线程中的。须要注意业务逻辑是否会过分占用当前线程资源,致使整个 Selector 效率低下。能够引入工做线程池解决以上问题。
SelectionKey 对象包含如下属性:
Selector 用法示意:
Selector selector = Selector.open(); // 获取一个 Selector 实例 channel.configureBlocking(false); // 只有非阻塞模式的 channel 才能使用 Selector SelectionKey key = channel.register(selector, SelectionKey.OP_READ); // 将 channel 注册到 Selector 上,同时指定 Selector 只关注 channel 的 READ 事件 while(true) { int readyChannels = selector.select(); // Selector 的 select 方法会阻塞,直到有已经准备好的(有数据可读的) channel,或是 Selector 被 wakeup,或是线程被中断 if(readyChannels == 0) continue; Set<SelectionKey> selectedKeys = selector.selectedKeys(); Iterator<SelectionKey> 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(); } }
gordon.study.socket.nio.basic.SimpleFileChannel.java
public class SimpleFileChannel { public static void main(String[] args) throws Exception { String path = SimpleFileChannel.class.getResource("/file1").getPath(); RandomAccessFile aFile = new RandomAccessFile(path, "rw"); FileChannel inChannel = aFile.getChannel(); ByteBuffer buf = ByteBuffer.allocate(48); int bytesRead = inChannel.read(buf); while (bytesRead != -1) { System.out.print("(Read " + bytesRead + ")"); buf.flip(); while (buf.hasRemaining()) { System.out.print((char) buf.get()); } buf.clear(); bytesRead = inChannel.read(buf); System.out.println(); } aFile.close(); } }
以上示例代码演示了最基本的 Channel 与 Buffer API。
gordon.study.socket.nio.basic.SimpleSelector.java
public class SimpleSelector { public static void main(String[] args) throws Exception { new Thread(new Runnable() { @Override public void run() { try { Selector selector = Selector.open(); ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.bind(new InetSocketAddress(8888)); serverSocketChannel.configureBlocking(false); System.out.println("##valid ops for server socket channel: " + serverSocketChannel.validOps()); SelectionKey sk = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); System.out.println("##Selection key ready ops before Selector.select(): " + sk.readyOps()); while (true) { int readyChannels = selector.select(); System.out.println("readyChannels by Selector.select(): " + readyChannels); if (readyChannels == 0) { continue; } Set<SelectionKey> selectedKeys = selector.selectedKeys(); System.out.println("selected keys by Selector.select(): " + selectedKeys.size()); Iterator<SelectionKey> keyIterator = selectedKeys.iterator(); while (keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); System.out.println("##Selection key ready ops after Selector.select(): " + key.readyOps()); SocketChannel channel = serverSocketChannel.accept(); if (channel != null) { // create a new thread to handle this client } keyIterator.remove(); } Thread.sleep(1000); } } catch (Exception e) { e.printStackTrace(); } } }).start(); for (int i = 0; i < 3; i++) { Thread.sleep(400); new Thread(new Client()).start(); } } private static class Client implements Runnable { @Override public void run() { try (Socket socket = new Socket()) { socket.connect(new InetSocketAddress(8888)); System.out.println(" Connected to server!"); while (true) { Thread.sleep(1000); } } catch (Exception e) { e.printStackTrace(); } } } }
以上示例代码使用 Selector 处理服务端链接创建过程。
代码第14行将 ServerSocketChannel 注册到 Selector 上,同时代表关注 ServerSocketChannel 的 Accept 事件(ServerSocketChannel 只支持这一种事件),显然,这时候 ServerSocketChannel 还没有准备好 Accept 事件,因此第15行代码打印出的 ready ops 为 0。
片刻后(400ms),第一个客户端成功链接到服务端,此时 ServerSocketChannel 产生 Accept 事件,Selector.select() 方法返回,因为 Selector 只注册了一个 Channel,返回值显然是1。而后遍历被选中的 SelectionKey 列表,建立 SocketChannel 处理本次链接。
代码第35行经过 sleep 的方法模拟复杂环境下建立 SocketChannel 耗时较长的状况。这产生了一个有趣的现象:客户端很早就完成了链接(socket.isConnected() == true),可是服务端要等待 sleep 时间耗尽后才能创建一个 SocketChannel,也就是说,虽然服务端尚未经过 ServerSocketChannel.accept() 方法建立出一个 SocketChannel,可是实际上 TCP 链接已经创建完成??(不甚理解)
大概推测,ServerSocketChannel 内部有地方保存已创建好的 TCP 链接(操做系统层面的已创建),accept() 方法被调用时,会将一个底层 TCP 链接包装为 SocketChannel。推断的理由一是客户端 socket 状态是已链接(也就是三次握手已经完成),另外一点是,若是注释掉代码第29行的 accept() 方法调用,会发现 Selector.select() 方法在第一个客户端链接过来后,几乎就不会被阻塞了(注掉第35行的 sleep 更加明显),也就是说,ServerSocketChannel 的 Accept 事件是按照有没有待处理的客户端链接来肯定的。
代码执行输出以下:
##valid ops for server socket channel: 16 ##Selection key ready ops before Selector.select(): 0 Connected to server! readyChannels by Selector.select(): 1 selected keys by Selector.select(): 1 ##Selection key ready ops after Selector.select(): 16 Connected to server! Connected to server! readyChannels by Selector.select(): 1 selected keys by Selector.select(): 1 ##Selection key ready ops after Selector.select(): 16 readyChannels by Selector.select(): 1 selected keys by Selector.select(): 1 ##Selection key ready ops after Selector.select(): 16
观察输出,显然,每一个 ServerSocketChannel 在一次 Selector.select 大轮询中,只创建了一个 Socket 链接,哪怕实际上当时有多个链接能够创建。若是咱们把创建链接的 ServerSocketChannel 与处理数据读写的 SocketChannel 注册到同一个 Selector 上,可能致使链接请求来不及处理。
若是将代码第29行优化为如下逻辑:
SocketChannel channel = serverSocketChannel.accept(); while (channel != null) { channel = serverSocketChannel.accept(); }
这样改后,若是短期有大量链接,会致使业务处理收到冲击,可能长时间得不到响应(线程资源都花在创建链接上了)。因此,更合理的方法是将负责创建链接的 ServerSocketChannel 与处理数据读写的 SocketChannel 注册到不一样的 Selector 上。
最后一个细节是代码第33行的移除 selection key。Java NIO 的 Selector 会将已准备好而且用户关注的 SelectionKey 加入 selectedKeys 集合,可是不会主动删除。所以,当咱们肯定本次事件已经处理完毕时,要主动移除掉该 selection key,不然下次获取 selectedKeys 集合时,该 selection key 仍是在集合中。(此段还没有彻底确认)