##NIO
本章翻译和总结自DevelopWorks的《Getting started with NIO》,介绍了Java NIO的主要原理。如有谬误,请发送pull request。
Index:
-1.为什么要使用NIO
-2.NIO和传统IO的区别?
-3.Channels和Buffers
-4.如何分配Buffer,生成文件的管道,通过管道做文件IO?
-5.Buffer的内部实现
-6.分发和收集式NIO
-7.文件锁
-8.网络和异步I/O
-9.字符集
-1.为什么要使用NIO(Back to Index)
NIO把最耗费时间的IO行为转移到了操作系统因此提高了IO速度。
-2.NIO和传统IO的区别?(Back to Index)
两者最重要的区别是如何封装数据和传输数据。
* 传统IO:面向流,IO系统每次处理一个字节
* NIO:面向块,IO系统每次处理一块数据
但现在的传统IO也有使用NIO的优良特性来优化自身的性能;使用NIO也可以很容的实现传统IO,——只要每次读写一个字节即可。
-3.Channels和Buffers(Back to Index)
Channel和Buffer是NIO的核心类。传统IO使用两个Stream对象做对接,不经过中间介质;而NIO使用Buffer作为缓冲区,Channel的读写行为都发生在Buffer上,而不是另一个Channel上。
Buffer主要为Channel提供数据缓冲,但也提供直接对其内容操作的方法。Buffer追踪系统的读写进度。对于每个Java原语类型都有其对应的Buffer派生类,如ByteBuffer、CharBuffer和ShortBuffer等
Channel是为用户提供读写操作的对象,作用类似传统IO中的Stream对象,但Channel是双向的(可以从Buffer读或写)而Stream是单向的(需要把一个输入流和输出流关联起来)
-4.如何分配Buffer,生成文件的管道,通过管道做文件IO?(Back to Index)
ByteBuffer buffer = ByteBuffer.allocate(1024);//生成Buffer
FileInputStream fin = new FileInputStream("input.txt");//生成输入流对象
FileChannel fcin = fin.getChannel();//从输入流获取管道对象
FileOutputStream fout = new FileOutputStream("output.txt");//生成输出流对象
FileChannel fcout = fout.getChannel();//从输出流获取管道对象 典型的buffer读写代码片段如下所示:
while(true){
buffer.clear();//将position置为0,将limit置为buffer.capacity
int r = fcin.read(buffer);//写buffer
if(r = -1){
break;
}
buffer.flip();//将position置为0,将limit置为之前position的值
fcout.write(buffer);//读buffer
}-5.Buffer的内部实现(Back to Index)
* 状态变量
* position:指向有效数据区的尾部
* limit:总是小于等于capacity,在读的情况下指向有效数据区的尾部,在写的情况下指向缓冲区的尾部
* capacity:缓冲区的最大容量
* clear()和flip()方法:
* clear()使得position回到0,limit指向缓冲区尾部,为read(写)做准备
* flip()使得position回到0,limit指向有效数据区的尾部,为write(读)做准备
* get()和put()方法
* 无论get还是put方法都分为绝对方法(指定了绝对位置)和相对方法,返回的ByteBuffer对象均为发生本次调用的对象自身
* get()方法有四种形式
* byte get();
* ByteBuffer get(byte dst[]);
* ByteBuffer get(byte dst[], int offset, int length);
* byte get(int index);
* put()方法有五种形式
* ByteBuffer put(byte b);
* ByteBuffer put(byte src[]);
* ByteBuffer put(byte src[], int offset, int length);
* ByteBuffer put(ByteBuffer src);
* ByteBuffer put(int index, byte b);
* get和put方法针对每种原语类型也有不同的实现
* Direct Buffer,该缓冲区并不存在与堆上,使用native方法提高IO速度
* Buffer对象生成方法
* allocate(int size);//分配指定大小的Buffer
* wrap(byte array[]);//将字节数组转换为Buffer
* slice();//以当前的position为头,limit为尾切分出子Buffer,注意,子Buffer与原Buffer共享底层数据
* asReadOnlyBuffer();//生成一个只读Buffer,只读Buffer不能被转换为可写Buffer
* Memory-mapped file I/O,可以生成一个内存映射文件Buffer对象,避免对内存的读写而直接进行IO操作,提高了效率,但可能会有不安全因素
* MappedByteBuffer mbb = fc. map(FileChannel.MapMode.READ_WRITE,0,1024);
-6.分发和收集式NIO(Back to Index)
分发和收集式NIO利用两个Channel和一组Buffer实现数据的传输,具体实现方式是使用Channel的read(Buffer[])和write(Buffer[]),在read(写)时会顺序选择第一个可用的(剩余容量大于要写入块大小)的Buffer进行。应用场景如使用两个Buffer,一个Buffer为消息的header大小,另一个Buffer为消息的body大小,这样就能很自然的将两者分开读取。
-7.文件锁(Back to Index)
文件锁的并不是用来阻止其他进程/线程对文件的访问,而是通过文件锁机制来实现系统不同部分的协调运转。通常可以为整个或部分文件加锁,文件锁分为互斥锁(exclusive lock)和共享锁(shared lock),一个获取文件锁的操作示例如下:
RandomAccessFile raf = new RandomAccessFile("usefilelocks.txt","rw");//互斥锁要求必须打开w选项(因为写互斥是OS支持的)
FileChannel fc = raf.getChannel();
FileLock lock = fc.lock(start,end,false);
// ...
lock.release();//释放锁文件锁需要谨慎使用,使用建议:
* 只使用互斥锁
* 将锁看做是建议性的
-8.网络和异步I/O(Back to Index)
异步I/O实现了数据的无阻塞读写操作,其核心类包括Selector,ServerSocketChannel,SocketChannel和SelectionKey以及ByteBuffer。
下图展示了NIO典型工作过程

如下github链接是一个典型的NIO实现
https://github.com/yangwm/JavaLearn/blob/master/src/jnio/MultiPortEcho.java
可用的注册事件名和对应值
* 服务端接收客户端连接事件 SelectionKey.OP_ACCEPT(16)
* 客户端连接服务端事件 SelectionKey.OP_CONNECT(8)
* 读事件 SelectionKey.OP_READ(1)
* 写事件 SelectionKey.OP_WRITE(4)
在实际应用中,可能需要将Channel从Selector的注册表中移出,同时使用多线程来处理收到的数
NIO比BIO的在某些情况下还更差一些,因为这里需要使用两个system call (select 和 recvfrom),而BIO只调用了一个system call (recvfrom)。但是,用select的优势在于它可以同时处理多个连接。所以如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。
-9.字符集(Back to Index)
使用CharsetDecoder和CharsetEncoder可以在CharBuffer和ByteBuffer之间做转换,如:
Charset latin1 = Charset.forName("ISO-8859-1");
CharsetDecoder decoder = latin1.newDecoder();
CharsetEncoder encoder = latin1.newEncoder();
ByteBuffer inputData;
CharBuffer cb = decoder.decode(inputData);
ByteBuffer outputData = encoder.encode(cb);