WHAT is Java NIO

虽然说Java NIO的真正含义是Non-Blocking IO。但是实际上它的字面意义是Java New IO,在Java 1.4的时候引入,用于区分原来老的IO方式。整个新的IO API是为服务器设计的,只与服务器相关。

WHY NIO

1. 网络IO往往是性能瓶颈

与CPU和内存相比,甚至与磁盘相比,网络都很慢。CPU与主存之间的传输速度大概是6GB每秒。与之相比,磁盘的传输速度要慢的多,但是也能够达到150MB每秒。但是,当今最快的局域网的理论速度是每秒120MB,但是大多数LAN只支持这个速度的十分之一到百分之一。而通过公用的Internet传输的速度一般至少要比LAN的速度低一个数量级。随着时间的的推移,CPU、内存、磁盘和网络都会随着时间而加速。但是在可预见的将来,CPU和磁盘或许仍然会比网络速度快几个数量级。在这种情况下,你绝对不希望让异常快速的CPU等待相对缓慢的网络。

2. 多进程和多线程方案

对于CPU速度远高于网络IO的情况,传统的Java解决方案是缓冲和多线程。多线程可以同时为几个不同的连接生成数据是,并且将数据存储在缓冲区中,知道网络真正准备好再发送。但是,多线程方案也有下面这些缺点:

  1. 内存开销:每个线程需要大概1M的内存
  2. 线程调度以及上下文切换开销
  3. 并发控制以及同步机制:复杂而且容易出错

NIO的四个核心概念

NIO的核心抽象是如下四个概念Package java.nio

  1. Buffers, which are containers for data;
  2. Charsets and their associated decoders and encoders, which translate between bytes and Unicode characters;
  3. Channels of various types, which represent connections to entities capable of performing I/O operations; Its job is to abstract files and sockets. Channels are analogous to “file descriptors” found in Unix-like operating systems.
  4. Selectors and selection keys, which together with selectable channels define a multiplexed, non-blocking I/O facility.

Selector

1. Buffers(缓冲区)

在使用Blocking IO的时候,一般都会建议使用缓冲区。与提供足够大缓冲区相比,几乎没有那种方法能够对网络程序的性能产生更大的影响。但是,对于新的IO模型,你将没有选择,所有的IO都要被缓冲。事实上,缓冲区已经成为API的基本部分。在新的IO模型中,不再向输出流写入数据或者输入流读取数据,而是要从缓冲区中读写数据。

除了boolean之外,每种Java简单类型都有特定的Buffer子类:

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer

但是基本上网络程序只会使用ByteBuffer。

每个缓冲区都记录了信息的四个关键部分,并且提供方法让程序获取和设置这些值:

  1. 容量(capacity):创建之后不能改变。
  2. 位置(position)
  3. 限度(limit)
  4. 标记(mark)

分配Buffer

  • ByteBuffer buf = ByteBuffer.allocate(100);
  • ByteBuffer buf = ByteBuffer.allocateDirect(100);

使用现有数组构造Buffer

  • static ByteBuffer wrap(byte[] array)
  • static ByteBuffer wrap(char[] array)
  • etc.

可以把Buffers的方法按照 “操作游标” 和 “操作缓冲区” 这两个维度进行划分。

  1. 只操作游标的方法
  2. 只操作缓冲区的方法
    • Absolute (bulk) get/put method
  3. 既操作缓冲区,也操作游标的方法
    • Relative (bulk) get/put method
    • compact

2. Channel(通道)

通道将缓冲区的数据移入或者移出到各种IO源,如文件、Socket等。

Channel与Stream之间的关键区别:

  1. bi-directional. You can both read and write to a Channels. Streams are typically one-way (read or write).
  2. non-blocking with Selectors. Channels can be read and written asynchronously.
  3. block-oriented with Buffers. A stream-oriented I/O system deals with data one byte at a time. A block-oriented I/O system deals with data in blocks. Channels always read to, or write from, a Buffer.

最重要的四种通道实现类如下:

  • FileChannel: The FileChannel reads data from and to files.
  • DatagramChannel: The DatagramChannel can read and write data over the network via UDP.
  • SocketChannel: The SocketChannel can read and write data over the network via TCP.
  • ServerSocketChannel: The ServerSocketChannel allows you to listen for incoming TCP connections, like a web server does. For each incoming connection a SocketChannel is created.

正如上面所示的,这些Channels覆盖了文件IO 和 UDP + TCP 网络IO。

TIPS Java NIO Scatter / Gather

Scatter/gather IO 提供了使用多个缓冲区(array of buffers)保存读写数据的方式。

基本上所有的Channels都提供Scatter/Gather IO。因为他们都实现了ScatteringByteChannel和 GatheringByteChannel接口:

  • long read( ByteBuffer[] dsts );
  • long read( ByteBuffer[] dsts, int offset, int length );
  • long write( ByteBuffer[] srcs );
  • long write( ByteBuffer[] srcs, int offset, int length );

1. FileChannel

Java NIO中的FileChannel是一个连接到文件的通道。可以通过文件通道读写文件。

NOTE FileChannel无法设置为非阻塞模式,它总是运行在阻塞模式下。

在使用FileChannel之前,必须先打开它。但是,我们无法直接打开一个FileChannel,需要通过InputStream、OutputStream或RandomAccessFile来获取一个FileChannel实例:

RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();

2. SocketChannel

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

  1. 打开一个SocketChannel并连接到互联网上的某台服务器

     SocketChannel socketChannel = SocketChannel.open();
     socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));
    
  2. 一个新连接到达ServerSocketChannel时,会创建一个SocketChannel

     SocketChannel socketChannel = serverSocketChannel.accept();
    

TIPS Non-blocking Mode

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

socketChannel.configureBlocking(false);

3. ServerSocketChannel

Java NIO中的 ServerSocketChannel 是一个可以监听新进来的TCP连接的通道, 就像标准IO中的ServerSocket一样。

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

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

例子:

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

while(true){
    SocketChannel socketChannel = serverSocketChannel.accept();
    //do something with socketChannel...
}

TIPS Non-blocking Mode

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...
	}
}

4. DatagramChannel

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

DatagramChannel channel = DatagramChannel.open();
channel.socket().bind(new InetSocketAddress(9999));

TIPS

Channel的区别一般在于打开和获取的方式不同,一旦获得一个Channel,那么接下来的读写过程基本都是一样的(要注意区分Non-blocking Mode)。

3. Selector(选择器)

一般的使用步骤

  1. 创建一个Selector
  2. 向Selector注册通道
  3. 通过Selector选择通道
  4. 处理Ready Channels相应的事件

1. Creating a Selector

Selector selector = Selector.open();

2. Registering Channels with the Selector

channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

说明

1、与Selector一起使用时,Channel必须处于非阻塞模式下。这意味着不能将FileChannel与Selector一起使用,因为FileChannel不能切换到非阻塞模式。而套接字通道都可以。

2、注意register()方法的第二个参数。这是一个“interest集合”,意思是在通过Selector监听Channel时对什么事件感兴趣。可以监听四种不同类型的事件:

  • Connect
  • Accept
  • Read
  • Write

对应SelectionKey的四个常量:

  • SelectionKey.OP_CONNECT
  • SelectionKey.OP_ACCEPT
  • SelectionKey.OP_READ
  • SelectionKey.OP_WRITE

如果对多个事件感兴趣,可以使用Or操作符:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;    

SelectionKey

将一个通道注册到Selector之后,返回一个SelectionKey对象。这个SelectionKey对象包含如下几个重要的信息:

  • The interest set: int interestSet = selectionKey.interestOps();
  • The ready set: int readySet = selectionKey.readyOps();
  • The Channel: selectionKey.channel();
  • The Selector: selectionKey.selector();
  • An attached object (可选,类似于上下文的概念): selectionKey.attachment();

3. Selecting Channels via a Selector

将Channel注册到Selector之后,可以调用select方法对这些通道进行监听。

  • int select() - blocks
  • int select(long timeout) - blocks with timeout
  • int selectNow() - non-block. return 0 immediately if none is ready.

int返回值表示有多少个channel是ready的。

你可以通过Selector的selectedKeys得到ready的channels:

Set<SelectionKey> selectedKeys = selector.selectedKeys();    

然后你可以遍历这个Selected key集合得到准备好的Channels:

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.
        // ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
		// SocketChannel sc = ssc.accept();
    } else if (key.isConnectable()) {
        // a connection was established with a remote server.
        // ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
		// SocketChannel sc = ssc.accept();
    } else if (key.isReadable()) {
        // a channel is ready for reading
        // SocketChannel sc = (SocketChannel)key.channel();
    } else if (key.isWritable()) {
        // a channel is ready for writing
        // SocketChannel sc = (SocketChannel)key.channel();
    }

    keyIterator.remove();
}

NOTE

  1. 一旦处理完selectionKey,记得将它从selectedKeySet中移除。否则下次select仍然会出现。
  2. 通过SelectionKey.channel()返回的Ready Channel应该根据情况,转换为相应的类型。比如ServerSocketChannel或者SocketChannel,等等。

TIP wakeUp()

某个线程调用select()方法后阻塞了,即使没有通道已经就绪,也有办法让其从select()方法返回。只要让其它线程在第一个线程调用select()方法的那个对象上调用Selector.wakeup()方法即可。阻塞在select()方法上的线程会立马返回。

如果有其它线程调用了wakeup()方法,但当前没有线程阻塞在select()方法上,下个调用select()方法的线程会立即“醒来(wake up)”。

Memory mapping

Example: map a FileChannel (all or a portion of it) into memory.

For this we use the FileChannel.map() method. The following line of code maps the first 1024 bytes of a file into memory:

MappedByteBuffer mbb = fc.map( FileChannel.MapMode.READ_WRITE, 0, 1024 );

The map() method returns a MappedByteBuffer, which is a subclass of ByteBuffer. Thus, you can use the newly-mapped buffer as you would any other ByteBuffer, and the operating system will take care of doing the mapping for you, on demand.

Java NIO.2

Java 1.7有引入了NIO 2,主要是引入了AIO。至此,Java总算是覆盖了底层操作系统所有的IO模型了。

参考文章

  1. The Rox Java NIO Tutorial
  2. Java NIO vs. IO
  3. Java NIO Tutorial
  4. Getting started with NIO