NIO简介

1、概述

Java NIO( New IO 或者 Non Blocking IO ) ,从 Java 1.4 版本开始引入的非阻塞 IO ,用于替换标准( 有些文章也称为传统,或者 Blocking IO 。下文统称为 BIO ) Java IO API 的 IO API 。

2、核心组件

Java NIO由如下三个核心组件组成

  • Channel
  • Buffer
  • Selector

3、NIO和BIO对比

NIO BIO
基于缓冲区 基于流
非阻塞IO 阻塞IO
选择器

BIO 是面向字节流或者字符流的,而在 NIO 中,它摒弃了传统的 IO 流,而是引入 Channel 和 Buffer 的概念:从 Channel 中读取数据到 Buffer 中,或者将数据从 Buffer 中写到 Channel 中。

① 那么什么是基于 Stream呢?

在一般的 Java IO 操作中,我们以流式的方式,顺序的从一个 Stream 中读取一个或者多个字节,直至读取所有字节。因为它没有缓存区,所以我们就不能随意改变读取指针的位置。

② 那么什么是基于 Buffer 呢?

基于 Buffer 就显得有点不同了。我们在从 Channel 中读取数据到 Buffer 中,这样 Buffer 中就有了数据后,我们就可以对这些数据进行操作了。并且不同于一般的 Java IO 操作那样是顺序操作,NIO 中我们可以随意的读取任意位置的数据,这样大大增加了处理过程中的灵活性。

Java NIO 引入 Selector ( 选择器 )的概念,它是 Java NIO 得以实现非阻塞 IO 操作的最最最关键。

我们可以注册多个 Channel 到一个 Selector 中。而 Selector 内部的机制,就可以自动的为我们不断的执行查询操作,判断这些注册的 Channel 是否有已就绪的 IO 事件( 例如可读,可写,网络连接已完成)。
通过这样的机制,一个线程通过使用一个 Selector ,就可以非常简单且高效的来管理多个 Channel 了。


Channel

1、概述

在 Java NIO 中,基本上所有的 IO 操作都是从 Channel 开始。数据可以从 Channel 读取到 Buffer 中,也可以从 Buffer 写到 Channel 中

2、与流的区别

1、 对于同一个 Channel ,我们可以从它读取数据,也可以向它写入数据。而对于同一个 Stream ,通常要么只能读,要么只能写

2、 Channel 可以非阻塞的读写 IO 操作,而 Stream 只能阻塞的读写 IO 操作。
3、 Channel 必须配合 Buffer 使用,总是先读取到一个 Buffer 中,又或者是向一个 Buffer 写入。也就是说,我们无法绕过 Buffer ,直接向 Channel 写入数据。

3、Channel的实现

Channel源码如下:

public interface Channel extends Closeable {

    public boolean isOpen();//判断此通道是否打开

    public void close() throws IOException;//关闭通道

Channel比较重要的实现类有以下三个:

  • SocketChannel:客户端发起tcp的Channel
  • ServerSocketChannel:服务端用来监听新的tcp连接的Channel。每个请求都会创建一个对应的SocketChannel
  • DatagramChannel

ServerSocketChannel

Java NIO中的ServerSocketChannel是一个可以监听新进来的TCP连接的通道

ServerSocketChannel

通过调用 ServerSocketChannel.open() 方法来打开ServerSocketChannel.如:

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
关闭 ServerSocketChannel

通过调用ServerSocketChannel.close() 方法来关闭ServerSocketChannel. 如:

serverSocketChannel.close();
监听新进来的连接

通过 ServerSocketChannel.accept() 方法监听新进来的连接。当 accept()方法返回的时候,它返回一个包含新进来的连接的 SocketChannel。因此, accept()方法会一直阻塞到有新连接到达。

通常不会仅仅只监听一个连接,在while循环中调用 accept()方法. 如下面的例子:

while(true){

    SocketChannel socketChannel = serverSocketChannel.accept();

    //do something with socketChannel...
}
非阻塞模式

ServerSocketChannel可以设置成非阻塞模式。在非阻塞模式下,accept() 方法会立刻返回,如果还没有新进来的连接,返回的将是null。 因此,需要检查返回的SocketChannel是否是null.如:

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

serverSocketChannel.socket().bind(new InetSocketAddress(9999));

serverSocketChannel.configureBlocking(false);

while(true){

    SocketChannel socketChannel = ServerSocketChannel.accept();

    if(socketChannel != null){
        //do something with socketChannel...
    }

}

SocketChannel

Java NIO中的SocketChannel是一个连接到TCP网络套接字的通道。可以通过以下2种方式创建SocketChannel:

1、个SocketChannel并连接到互联网上的某台服务器。
2、新连接到达ServerSocketChannel时,会创建一个SocketChannel。

打开SocketChannel
SocketChannel socketChannel = SocketChannel.open();

socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));
关闭SocketChannel
socketChannel.close();
从 SocketChannel 读取数据
ByteBuffer buf = ByteBuffer.allocate(48);

int bytesRead = socketChannel.read(buf);

首先,分配一个Buffer。从SocketChannel读取到的数据将会放到这个Buffer中。

然后,调用SocketChannel.read()。该方法将数据从SocketChannel 读到Buffer中。read()方法返回的int值表示读了多少字节进Buffer里。如果返回的是-1,表示已经读到了流的末尾(连接关闭了)

写入 SocketChannel
String newData = "New String to write to file..." + System.currentTimeMillis();

ByteBuffer buf = ByteBuffer.allocate(48);

buf.clear();

buf.put(newData.getBytes());

buf.flip();

while(buf.hasRemaining()) {

    channel.write(buf);

}

注意SocketChannel.write()方法的调用是在一个while循环中的。Write()方法无法保证能写多少字节到SocketChannel。所以,我们重复调用write()直到Buffer没有要写的字节为止。

非阻塞模式

可以设置 SocketChannel 为非阻塞模式(non-blocking mode).设置之后,就可以在异步模式下调用connect(), read() 和write()了。

如果SocketChannel在非阻塞模式下,此时调用connect(),该方法可能在连接建立之前就返回了。为了确定连接是否建立,可以调用finishConnect()的方法。

socketChannel.configureBlocking(false);

socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));

while(! socketChannel.finishConnect() ){

    //wait, or do something else...

}

DatagramChannel

Java NIO中的DatagramChannel是一个能收发UDP包的通道。因为UDP是无连接的网络协议,所以不能像其它通道那样读取和写入。它发送和接收的是数据包。

打开 DatagramChannel
DatagramChannel channel = DatagramChannel.open();

channel.socket().bind(new InetSocketAddress(9999));
接收数据

通过receive()方法从DatagramChannel接收数据

ByteBuffer buf = ByteBuffer.allocate(48);

buf.clear();

channel.receive(buf);

receive()方法会将接收到的数据包内容复制到指定的Buffer. 如果Buffer容不下收到的数据,多出的数据将被丢弃。

发送数据

通过send()方法从DatagramChannel发送数据

String newData = "New String to write to file..." + System.currentTimeMillis();

ByteBuffer buf = ByteBuffer.allocate(48);

buf.clear();

buf.put(newData.getBytes());

buf.flip();

int bytesSent = channel.send(buf, new InetSocketAddress("jenkov.com", 80));

这个例子发送一串字符到”jenkov.com”服务器的UDP端口80。 因为服务端并没有监控这个端口,所以什么也不会发生。也不会通知你发出的数据包是否已收到,因为UDP在数据传送方面没有任何保证。

连接到特定的地址

可以将DatagramChannel“连接”到网络中的特定地址的。由于UDP是无连接的,连接到特定地址并不会像TCP通道那样创建一个真正的连接。而是锁住DatagramChannel ,让其只能从特定地址收发数据。

channel.connect(new InetSocketAddress("jenkov.com", 80));

当连接后,也可以使用read()和write()方法,就像在用传统的通道一样。只是在数据传送方面没有任何保证。

int bytesRead = channel.read(buf);

int bytesWritten = channel.write(but);