本文总结从三个方面由浅入深的讲述Java NIO,channel,buffer,selector(网络NIO)。
这篇总结文章也有所参考,特别是NIO入门-IBM,讲的挺好的。
NIO处理数据是以数据块为单位,而传统IO流是以字节为单位。显而易见,数据块的操作效率明显更高。
Channel:Channel是一个对象,我们对数据的读写都不会直接与Channel接触,而是通过中间的缓冲区buffer。Channel在NIO中就像是Stream在传统IO中,只不过他们的区别是Channel是双向的(unix模型的底层系统通道也是双向的),可读,可写,可同时读写,而Stream是单向的,一个Stream只能是读或者写。
Buffer:Buffer就是一个缓冲区,它是用户读写操作与Channel之间的桥梁,用户读数据只能从Channel读到Buffer,然后再从Buffer通过get()读取。写数据也是一样,用户只能先把数据通过put()方法写到Buffer,然后再从Buffer写入到Channel。
Buffer实质上是一个数组,既然是一个数组那么在使用Buffer之前,就需要给Buffer分配内存空间:
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 这种方式是直接分配内存,与操作系统的耦合性更高,速度比上面的要更快,但是分配的开销更大
ByteBuffer byteBuffer1 = ByteBuffer.allocateDirect(1024);
byte[] bytes = new byte[]{12,34,56};
ByteBuffer byteBuffer2 = ByteBuffer.wrap(bytes);
Buffer的类型包括了所有的基本数据类型,一般ByteBuffer比较常用,其他数据类型的用法也都是差不多的。
ByteBuffer可以通过asXXXBuffer()进行转换成其他类型的Buffer,例如:
IntBuffer intBuffer = byteBuffer.asIntBuffer();
先来看下Channel和Buffer的基本使用方法,然后再分析Buffer的源码。
package com.pzx.test.test003;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class NioBufferDemo {
public static void main(String[] args) {
try {
testWrite();
testRead();
} catch (IOException e) {
e.printStackTrace();
}
}
private static void testWrite() throws IOException {
FileOutputStream fos = new FileOutputStream("testNIO.txt");
FileChannel channel = fos.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
for (int i=0 ;i<10; i++) {
byteBuffer.put((byte)i);
System.out.println("write : " + (byte)i);
}
byteBuffer.flip();
channel.write(byteBuffer);
fos.close();
}
private static void testRead() throws IOException {
FileInputStream fis = new FileInputStream("testNIO.txt");
FileChannel fileChannel = fis.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while (true) {
int n = fileChannel.read(byteBuffer);
System.out.println("read..."+n);
if (n == -1) {
break;
}
}
byteBuffer.flip();
while (byteBuffer.hasRemaining()) {
System.out.println("read : " + byteBuffer.get());
}
fis.close();
}
}
运行结果:
write : 0
write : 1
write : 2
write : 3
write : 4
write : 5
write : 6
write : 7
write : 8
write : 9
read...10
read...-1
read : 0
read : 1
read : 2
read : 3
read : 4
read : 5
read : 6
read : 7
read : 8
read : 9
接下来看Buffer的源码以及几个常用的方法
public abstract class Buffer {
// Invariants: mark <= position <= limit <= capacity
private int mark = -1;
// 指向下一个字节放到数组的哪一个元素中。假设从通道读取3个字节到数组,那么position=3,
// 指向的是数组的第4个元素,写也是一样
private int position = 0;
// limit在读时表示还有多少数据需要取出,写时表示还有多少空间可以写入,position<=limit
private int limit;
// 数组大小,也就是能够放入缓冲区的最大容量,limit肯定不能比capacity大
private int capacity;
// 清空buffer
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
// 把操作区间控制到0-原position
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
}
读取数据:
先读入三个元素到缓冲区
再读2个元素
调用flip()方法
下面是写入数据:
写入了4个元素到缓冲区
再写入一个元素到缓冲区
调用clear()方法
上面这些图片展示了读取,写入,flip(),clear()函数的作用
下面是一个文件copy的例子,主要还是熟悉一下buffer的方法的使用:
private static void testCopyFile() throws IOException {
FileInputStream fis = new FileInputStream("testNIO.txt");
FileOutputStream fos = new FileOutputStream("testNIOCopy.txt");
FileChannel inChannel = fis.getChannel();
FileChannel outChannel = fos.getChannel();
// 此时position=0,limit=capacity
ByteBuffer byteBuffer = ByteBuffer.allocate(3);
while (true) {
// read前,position=0,limit=3
int n = inChannel.read(byteBuffer);
// read后,position=3,limit=3
System.out.println("n = " + n);
if (n == -1) {
break;
}
// flip后,position=0,limit=3,经过3次之后,position=0,limit=1,因为这次只读1个元素
byteBuffer.flip();
outChannel.write(byteBuffer);
// clear()把position=0, limit=3;方便下一次读取使用buffer
byteBuffer.clear();
}
// 除了用上面的方式外,还有一个更快捷的方法:transferTo,transferFrom
inChannel.transferTo(0, inChannel.size(), outChannel);
// 或者是
outChannel.transferFrom(inChannel, 0, inChannel.size());
}
输出结果:
n = 3
n = 3
n = 3
n = 1
n = -1
buffer有get(),put()方法来直接控制对buffet的读写,
ByteBuffer
类中有四个 get()
方法:
byte get();
ByteBuffer get( byte dst[] );
ByteBuffer get( byte dst[], int offset, int length );
byte get( int index );//这个方法是绝对的,它不会改变position的值,从指定的index处读取数据,上面三个get()都是相对的,get()一次会改变position
ByteBuffer
类中有五个 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 );// 这个方法是绝对的,同上面的第4个get()方法
缓冲区可以分片,分片后的缓冲区和之前的缓冲区共享底层数据,分片方法是slice():
// 分片,与原来的buffer共享数据
private static void testslice() {
ByteBuffer byteBuffer = ByteBuffer.allocate(6);
for (int i = 1; i <= 6; i++) {
byteBuffer.put((byte) i);
}
byteBuffer.clear();
while (byteBuffer.hasRemaining()) {
System.out.println(byteBuffer.get());
}
System.out.println("----------");
// 取数组的第3个和第4个元素,limit指向的是最后一个元素的下一个
byteBuffer.position(2);
byteBuffer.limit(4);
ByteBuffer slice = byteBuffer.slice();
for (int i = 0; i < slice.capacity(); i++) {
// 这里的get是绝对的,不影响position的值
slice.put((byte) (slice.get(i)*10));
}
byteBuffer.clear();
while (byteBuffer.hasRemaining()) {
System.out.println(byteBuffer.get());
}
}
输出结果:
1
2
3
4
5
6
----------
1
2
30
40
5
6
缓冲区还可以设置为只读缓冲区,通过
asReadOnlyBuffer()
直接缓冲区,加快IO速度,尽可能避免了数据从缓冲区拷贝到中间缓冲区的步骤
ByteBuffer buffer = ByteBuffer.allocateDirect( 1024 );//就这里的分配方式不一样,其他操作一样。
Java文档描述:
给定一个直接字节缓冲区,Java 虚拟机将尽最大努力直接对它执行本机 I/O 操作。也就是说,它会在每一次调用底层操作系统的本机 I/O 操作之前(或之后),尝试避免将缓冲区的内容拷贝到一个中间缓冲区中(或者从一个中间缓冲区中拷贝数据)。
内存映射机制:
普通的对文件进行操作是把实际操作的文件内容读到内存中,然后再操作。而内存映射文件是程序直接操作底层的文件,修改即保存。使用方法如下:
MappedByteBuffer mbb = fc.map( FileChannel.MapMode.READ_WRITE, 0, 1024 );//把fc的0到1024的字节映射到内存
MappedByteBuffer是ByteBuffer的子类,所以照常使用MappedByteBuffer即可,映射的事情是操作系统来完成的。
有了这个映射内存的方法,就可以相当于是在内存操作大文件了。
分散/聚集IO:
操作对象是Buffer[]数组,读取数据时是读到ByteByffer[]数组中,数组的元素一个一个依次被填充;写入数据时,会把数组组成单个数据流写入。
通道可以有选择地实现两个新的接口: ScatteringByteChannel
(读)和 GatheringByteChannel(写)
分散聚集的应用方面是可以把一个由多个部分组成的数据划分到不同的Buffer中,然后使用数组一把读取或者写入。比如网络报文的head和body。
文件加锁:
FileLock fileLock = inChannel.lock(0, 2, false);
// do something...
fileLock.release();
lock可以对整个文件加锁,也可以对文件的一部分加锁。
文件锁也分为共享锁与排他锁,共享锁只能以读的方式打开文件,其他线程可以获取文件的共享锁,但是不能获取排他锁;
排他锁只能以写的方式打开文件,其他线程无法对文件操作。
另外,对文件加锁,是会让操作系统协同加锁的,所以对文件加锁后,位于另外的虚拟机的进程或者非虚拟机的进程来访问文件也要适应文件锁的限制。
对文件加锁,也可以使用tryLock()的方式,这个方法是尝试获取锁,获取到就加锁,否则立即返回,相比lock()是不阻塞的。
对文件加锁的范围,如果是lock()或者tryLock(),那么随着文件的增大锁的范围也会增大,如果是lock(0,2,false)则不会随着文件的变化而变化
下面是Socket NIO的使用示例:
package com.pzx.test.test004;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
public class SocketNIOServer {
public static void main(String[] args) throws IOException {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
ssc.socket().bind(new InetSocketAddress(9999));
Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
// 如果没有请求过来会在这里阻塞
int n = selector.select();
if (0 == n) {
continue;
}
Iterator iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = (SelectionKey) iterator.next();
// 如果处于accept状态,则可以连接了
if (selectionKey.isAcceptable()) {
// 获取Socket连接的通道
SocketChannel socketChannel = ((ServerSocketChannel) selectionKey.channel()).accept();
System.out.println("request coming...");
socketChannel.configureBlocking(false);
// 建立连接后,就可以开始读socket发过来的消息了
socketChannel.register(selector, SelectionKey.OP_READ);
}
// 如果处于可读取数据状态-接受客户端数据
else if (selectionKey.isReadable()) {
System.out.println("reading data...");
// 读数据
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
socketChannel.configureBlocking(false);
while (true) {
// 当客户端断开后才会返回-1,否则没有读到数据时返回0
int s = socketChannel.read(byteBuffer);
System.out.println("s="+s);
if (s == 0 || s == -1) {
break;
}
byteBuffer.flip();
while (byteBuffer.hasRemaining()) {
System.out.print((char) byteBuffer.get());
}
System.out.println("\n--------");
byteBuffer.clear();
}
//socketChannel.close();
// 读完就给客户端回消息
socketChannel.register(selector, SelectionKey.OP_WRITE);
}
// 如果处于可以可写入数据状态--发送数据给客户端
else if (selectionKey.isWritable()) {
System.out.println("writing data...");
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
socketChannel.configureBlocking(false);
ByteBuffer byteBuffer = ByteBuffer.wrap("I'm server, I send u some messages.".getBytes());
socketChannel.write(byteBuffer);
socketChannel.close();
}
// 删除已使用过的selectionKey
iterator.remove();
}
}
}
}
package com.pzx.test.test004;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
public class SocketNIOClient {
public static void main(String[] args) throws IOException {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("127.0.0.1", 9999));
Selector selector = Selector.open();
socketChannel.register(selector, SelectionKey.OP_CONNECT);
while (true) {
int n = selector.select();
if (0 == n) {
continue;
}
Iterator iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
// 连接服务端
if (selectionKey.isConnectable()) {
System.out.println("client connect...");
SocketChannel socketChannel1 = (SocketChannel) selectionKey.channel();
// 如果正在连接,则完成连接
if (socketChannel1.isConnectionPending()) {
socketChannel1.finishConnect();
}
socketChannel1.configureBlocking(false);
// 发送数据到服务端
ByteBuffer byteBuffer = ByteBuffer.wrap("I'm Client. I send u".getBytes());
socketChannel1.write(byteBuffer);
// 接下来是监听服务端的回应了
socketChannel1.register(selector, SelectionKey.OP_READ);
}
// 接受服务器的回应
else if (selectionKey.isReadable()) {
System.out.println("client reading...");
SocketChannel socketChannel1 = (SocketChannel) selectionKey.channel();
socketChannel1.configureBlocking(false);
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while (true) {
int s = socketChannel1.read(byteBuffer);
System.out.println("s="+s);
if (s == 0 || s == -1) {
break;
}
byteBuffer.flip();
while (byteBuffer.hasRemaining()) {
System.out.print((char) byteBuffer.get());
}
System.out.println("\n******");
byteBuffer.clear();
}
socketChannel1.close();
}
iterator.remove();
}
}
}
}
输出结果:
server端:
request coming...
reading data...
s=20
I'm Client. I send u
--------
s=0
writing data...
client端:
client connect...
client reading...
s=35
I'm server, I send u some messages.
******
s=-1