NIO 介绍
1. java在JDK1.4版本之后推出了新的IO系统,也就是NIO(new IO),也可以理解为非阻塞IO(Non-Blocking IO)。《java NIO》中介绍了nio出现的原因:
操作系统与 Java 基于流的 I/O模型有些不匹配。操作系统要移动的是大块数据(缓冲区),这往往是在硬件直接存储器存取( DMA)的协助下完成的。而 JVM 的 I/O 类喜欢操作小块数据——单个字节、几行文本。结果,操作系统送来整缓冲区的数据, java.io 的流数据类再花大量时间把它们拆成小块,往往拷贝一个小块就要往返于几层对象。操作系统喜欢整卡车地运来数据, java.io 类则喜欢一铲子一铲子地加工数据。有了 NIO,就可以轻松地把一卡车数据备份到您能直接使用的地方( ByteBuffer 对象)。但是Java里的RandomAccessFile类是比较接近操作系统的方式。
NIO和传统IO的区别就在于,IO是面向流的,NIO是面向缓冲区的。这两者的区别就在于:
IO:每次从流中读取一个或者多个字节,知道读取所有字节,数据没有被缓存在任何地方。此外,不能前后移动流中的数据,如果需要前后移动从流中读取的数据,需要先将它缓存在一个缓冲区。在IO中各种流是阻塞的,这就意味着当一个线程在调用read()或者write()时,会阻塞直到读到数据或者数据被写完。
NIO: 数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变得可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道。
2. java Nio主要依赖三大组件:缓冲区Buffer、通道Channel和选择器Selector
Nio是基于Channel和Buffer进行操作,数据总是从Channel读取到Buffer中,或者从Buffer写入到Channel中。Selector用于监听多个通道的事件(比如,链接打开,数据到达等),因此单个线程可以监听到多个通道的数据。
2.1 Buffer
缓冲区包括很多种类型,主要有下面几种,我们在开发中用的最多的是ByteBuffer。缓冲区可以理解为一个容器,一个连续的数组。Channel提供从文件或者网络读取数据的通道,但是读写的数据都必须经过Buffer。
缓冲区属性介绍:
容量(capacity):缓冲区的最大大小
上界(limit):缓冲区当前的大小
位置(position):下一个要读写的位置,由get()和put()更新
标记(mark):备忘位置,由mark()来指定mark = position,由reset()来指定position=mark
2.2 通道(Channel)
缓冲区为我们卸载了数据,但是数据的写入和读取不能直接进行read()和write()这样的系统调用,JVM为我们提供了一层对系统调用的封装。而Channel可以用最小的开销来访问操作系统本身的IO服务,这就是为什么要用Channel的原因。
NIO中的Channel的主要实现有:
-
FileChannel
-
DatagramChannel
-
SocketChannel
-
ServerSocketChannel
上面四种Channel分别可以对应文件IO、UDP和TCP(Server和Client)。
这里使用SocketChannel来探讨NIO。NIO的强大之处就在于Channel的非阻塞性。我们都知道套接字的很多操作都会阻塞当前的线程。accept()方法会阻塞住直到有客户端连接进来,read()方法的调用也会因为没有数据可读而阻塞,直到连接的另外一端传过来数据。总的来说,创建、接受、读取、写入数据等I/O调用都会阻塞直到有对应事件发生。NIO的channel抽象的一个重要特征就是可以通过配置它的阻塞行为,以实现非阻塞式的信道。
channel.configureBlocking(false)
在非阻塞 信道上调用方法会立即返回,这种调用的返回值指示了所请求操作的完成程度。例如,在一个非阻塞ServerSocketChannel上调用accept()方法,如果有连接请求来了,则返回客户端SocketChannel,否则返回null。
2.3 选择器Selector
选择器是计算机底层多路复用机制在java层面的抽象,在后面的文章中 会进一步介绍多路复用的概念。Selector主要用于检查一个或者多个Nio Channel的状态。这样就可以实现单线程管理多个Channels。
使用Selector的好处在于:使用更少的线程来管理多个通道,相比多线程,避免了线程上下文切换带来的开销。
Selector的使用方法:
a. Selector的创建:
Selector selector = Selector.open();
b. 注册Channel到Selector上:
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, Selectionkey.OP_READ);
Channel必须是非阻塞的。所以FileChannel不适合用selector,因为FileChannel不能切换为非阻塞模式,更准确的说是因为FIleChannel没有继承SelectableChannel。Socket可以正常注册到Selector上。
register() 方法的第二个参数。这是一个“ interest集合 ”,意思是在通过Selector监听Channel时对什么事件感兴趣。可以监听四种不同类型的事件:
-
Connect
-
Accept
-
Read
-
Write
这四种事件用SelectionKey的四个常量来表示:
SelectionKey.OP_CONNECT
SelectionKey.OP_ACCEPT
SelectionKey.OP_READ
SelectionKey.OP_WRITE
如果对不止一个事件感兴趣,可以使用运算符即可:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
c. 选择器选择过程
选择器实际上是对select(),poll()等本地系统调用的一个封装。每一个选择器会维护三个键集合:已注册的键集合、已选择的键集合和已取消的键集合。通过执行Selector.select()、Selector.select(int timeout)或Selector.selectNow(),选择过程被调用,这时会执行以下步骤:
- 首先会检查被取消的键的集合。因为在任何时候选择键(通道和选择器的绑定关系)都可能被取消,所以在正式选择之前需要先检查一下被取消的键。如果这个集合非空,则其中的键会从另外两个键集合中去除。
- 每个通道有一个感兴趣操作集合,底层的系统调用可以去检查这些操作是否就绪,如果就绪就会更新该通道绑定的选择键里的相关值。所以你只需要去检查选择键里的相关值就可以知道该操作是不是准备好了。
- 完成步骤2可能很耗时。完成后还需要再进行步骤1,因为这个过程中某些选择键也可能被取消,这样做是为了提高程序的健壮性(robust)。
- 最后select()操作会返回此次选择过程中ready()集合被改变的键的数量,而不是所有的ready()集合中的键的数量。这非常合理,因为你可以知道这次选择过程到底有几个通道准备就绪。通过判断select()返回值是否大于0,就可以知道要不要去操作了。
3. 代码演示Java Nio
通常的做法如下:在选择器上调用一次select操作(这会更新已选择键的集合),然后遍历selectedKeys返回的键的集合。接着键将从已选择的键的集合中被移除(通过Iterator.remove()方法),然后检测下一个键。完成后,继续下一次select操作。
服务端代码:
public class SelectorTest {
public static void main(String[] args) throws IOException {
new SelectorTest().select();
}
public void select() throws IOException {
//创建选择器
Selector selector = Selector.open();
//创建serverChannel
ServerSocketChannel ssc = ServerSocketChannel.open();
//设置为非阻塞模式
ssc.configureBlocking(false);
//绑定监听的地址
ssc.socket().bind(new InetSocketAddress(20000), 1024);
//将serverChannel注册到选择器上,监听accept事件,返回选择键
ssc.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
//此次选择过程准备就绪的通道数量
int num = selector.select();
if (num == 0) {
//若没有准备好的就继续循环
continue;
}
//返回已就绪的键集合
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
handle(selector, key);
//因为已经处理了该键,所以把当前的key从已选择的集合中去除
it.remove();
}
}
}
public void handle(Selector selector, SelectionKey key) throws IOException {
if (key.isValid()) {
//当一个ServerChannel为accept状态时,注册这个ServerChannel的SocketChannel为可读取状态
if (key.isAcceptable()) {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
SocketChannel channel = serverChannel.accept();
//把通道注册到选择器之前要设置为非阻塞,否则会报异常
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ);
}
//如果channel是可读取状态,则读取其中的数据
if (key.isReadable()) {
//只有SocketChannel才能读写数据,所以如果是可读取状态,只能是SocketChannel
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer in = ByteBuffer.allocate(1024);
//将socketChannel中的数据读入到buffer中,返回当前字节的位置
int readBytes = sc.read(in);
if (readBytes > 0) {
//把buffer的position指针指向buffer的开头
in.flip();
byte[] bytes = new byte[in.remaining()];
in.get(bytes);
String body = new String(bytes, "UTF-8");
System.out.println("The server receive : " + body);
//把response输出到socket中
doWrite(sc, "Hello client");
} else if (readBytes < 0) {
key.cancel();
sc.close();
}
}
}
}
private void doWrite(SocketChannel sc, String response) throws IOException {
//把服务器端返回的数据写到socketChannel中
if (response == null && response.trim().length() > 0) {
byte[] bytes = response.getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
writeBuffer.put(bytes);
writeBuffer.flip();
sc.write(writeBuffer);
}
}
}
客户端代码:
public class Client {
public static final int PORT = 20000;
public static final String HOST = "127.0.0.1";
private volatile boolean stop = false;
public static void main(String[] args) throws IOException {
new Client().select();
}
public void select() throws IOException {
// 创建选择器
Selector selector = Selector.open();
// 创建SocketChannel
SocketChannel sc = SocketChannel.open();
// 设置为非阻塞模式
sc.configureBlocking(false);
try {
doConnect(selector, sc);
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
while (!stop) {
int num = selector.select();
if (num == 0) {
continue;
}
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
try {
handleKeys(selector, key);
} catch (Exception e) {
e.printStackTrace();
if (key != null) {
key.cancel();
if (key.channel() != null) {
key.channel().close();
}
}
}
// 因为已经处理了该键,所以把当前的key从已选择的集合中去除
it.remove();
}
}
if (selector != null) {
selector.close();
}
}
private void doWrite(SocketChannel sc, String response) throws IOException {
if (response != null && response.trim().length() > 0) {
byte[] bytes = response.getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
writeBuffer.put(bytes);
writeBuffer.flip();
sc.write(writeBuffer);
if (!writeBuffer.hasRemaining()) {
System.out.println("Send msg successfully");
}
}
}
private void handleKeys(Selector selector, SelectionKey key) throws IOException {
if (key.isValid()) {
SocketChannel sc = (SocketChannel) key.channel();
// 判断是否连接成功
if (key.isConnectable()) {
if (sc.finishConnect()) {
sc.register(selector, SelectionKey.OP_READ);
doWrite(sc, "Hello Server");
} else {
System.exit(1);
}
}
if (key.isReadable()) {
ByteBuffer in = ByteBuffer.allocate(1024);
// 将socketChannel中的数据读入到buffer中,返回当前字节的位置
int readBytes = sc.read(in);
if (readBytes > 0) {
// 把buffer的position指针指向buffer的开头
in.flip();
byte[] bytes = new byte[in.remaining()];
in.get(bytes);
String body = new String(bytes, "UTF-8");
System.out.println("The Client receive : " + body);
this.stop = true;
} else if (readBytes < 0) {
// 对端链路关闭
key.cancel();
sc.close();
} else {
// 读到0字节,忽略
}
}
}
}
private void doConnect(Selector selector, SocketChannel sc) throws IOException {
if (sc.connect(new InetSocketAddress(HOST, PORT))) {
System.out.println("Client connect successfully...");
// 如果直接连接成功,则注册读操作
sc.register(selector, SelectionKey.OP_READ);
doWrite(sc, "Hello server!");
} else {
// 如果没有连接成功,则注册连接操作
sc.register(selector, SelectionKey.OP_CONNECT);
}
}
}
4. 总结
文章的内容源于书本,博客,论坛的一些知识的总结整理。后面文章会从操作系统层面分析多路复用器的原理。