参考资料:
老外写的教程,很适合入门:http://tutorials.jenkov.com/java-nio/index.html
上面教程的译文:http://ifeve.com/overview/
示例代码:
https://github.com/gordonklg/study,socket module
A. 摘要
因为有现成的教程,本文只做摘要。
NIO 有三宝,channel、buffer、selector
Channel 与 Stream 很相似,除了:
- Channel 同时支持读操作与写操作,而 Stream 是单向的
- Channel 支持异步读写
- Channel 读写操作与 Buffer 绑定,只能把数据从 Channel 读取出来放到 Buffer 中,或是把 Buffer 中的数据写到 Channel 中
Buffer 本质上是一个内存块,Buffer 包装了这个内存块,提供一系列方法简化在该内存块上的数据读写操作。
Buffer 有三个属性:
- capacity:容量
- position:当前操作位置
- limit:允许到达的界限
其中 capacity 只能在创建时指定,无法修改。其它两个属性都有对应的读取与设值方法。
Buffer 及 Channel 主要方法的手绘示意图如下:
Selector 设计目的是使单线程可以处理多个网络连接(多个 Channel)。对于存在大量连接但是每个连接占用带宽都不多的应用,例如聊天工具、滴滴收集车辆位置信息、物联网收集设备信息等,传统 Socket 编程需要为每一个连接分配一个处理线程,占用大量系统资源。我们需要一种方案,可以让一个线程负责多个连接。
Selector 允许 Channel 注册到自己身上,SelectionKey 表示 channel 与 selector 的注册关系。
Channel 能产生4种事件,分别是:
- SelectionKey.OP_CONNECT // channel 已成功连接到服务器
- SelectionKey.OP_ACCEPT // server channel 已成功接受一个连接
- SelectionKey.OP_READ // channel 中有可读数据
- SelectionKey.OP_WRITE // channel 可以发送数据
可以设置 Selector 关注 Channel 的哪些事件。Selector 的 select() 方法会阻塞,直到注册的 Channel 产生了指定类型的事件(实际意义就是 Channel 已经准备好做某事了)。接着就可以通过 Selector 获取所有已经准备好的 SelectionKey(即Channel),依次处理相应事件,例如建立连接、获取数据、业务处理、发送数据等。
显然,同一个 selector 的所有 channel 对数据的读写以及业务逻辑的实现,在默认情况下,都是在同一个线程中的。需要注意业务逻辑是否会过度占用当前线程资源,导致整个 Selector 效率低下。可以引入工作线程池解决以上问题。
SelectionKey 对象包含以下属性:
- The interest set,Selector 感兴趣的 Channel 事件类型
- The ready set,Channel 已经准备好的事件。显然,被 Selector.select() 方法选中的 SelectionKey,其 ready set 应该与 interest set 有交集
- The Channel,通过 SelectionKey 可以获取 Channel 对象
- The Selector,通过 SelectionKey 可以获取 Selector 对象
- An attached object (optional)
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();
}
}
B. 示例代码
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 还是在集合中。(此段尚未完全确认)