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解决方案是缓冲和多线程。多线程可以同时为几个不同的连接生成数据是,并且将数据存储在缓冲区中,知道网络真正准备好再发送。但是,多线程方案也有下面这些缺点:
- 内存开销:每个线程需要大概1M的内存
- 线程调度以及上下文切换开销
- 并发控制以及同步机制:复杂而且容易出错
NIO的四个核心概念
NIO的核心抽象是如下四个概念Package java.nio:
- Buffers, which are containers for data;
- Charsets and their associated decoders and encoders, which translate between bytes and Unicode characters;
- 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.
- Selectors and selection keys, which together with selectable channels define a multiplexed, non-blocking I/O facility.
1. Buffers(缓冲区)
在使用Blocking IO的时候,一般都会建议使用缓冲区。与提供足够大缓冲区相比,几乎没有那种方法能够对网络程序的性能产生更大的影响。但是,对于新的IO模型,你将没有选择,所有的IO都要被缓冲。事实上,缓冲区已经成为API的基本部分。在新的IO模型中,不再向输出流写入数据或者输入流读取数据,而是要从缓冲区中读写数据。
除了boolean之外,每种Java简单类型都有特定的Buffer子类:
- ByteBuffer
- CharBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
但是基本上网络程序只会使用ByteBuffer。
每个缓冲区都记录了信息的四个关键部分,并且提供方法让程序获取和设置这些值:
- 容量(capacity):创建之后不能改变。
- 位置(position)
- 限度(limit)
- 标记(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的方法按照 “操作游标” 和 “操作缓冲区” 这两个维度进行划分。
- 只操作游标的方法
- 只操作缓冲区的方法
- Absolute (bulk) get/put method
- 既操作缓冲区,也操作游标的方法
- Relative (bulk) get/put method
- compact
2. Channel(通道)
通道将缓冲区的数据移入或者移出到各种IO源,如文件、Socket等。
Channel与Stream之间的关键区别:
- bi-directional. You can both read and write to a Channels. Streams are typically one-way (read or write).
- non-blocking with Selectors. Channels can be read and written asynchronously.
- 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:
-
打开一个SocketChannel并连接到互联网上的某台服务器
SocketChannel socketChannel = SocketChannel.open(); socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));
-
一个新连接到达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(选择器)
一般的使用步骤
- 创建一个Selector
- 向Selector注册通道
- 通过Selector选择通道
- 处理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
- 一旦处理完selectionKey,记得将它从selectedKeySet中移除。否则下次select仍然会出现。
- 通过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模型了。