NIO学习笔记

仅供学习交流,如有错误请指出,如要转载请加上出处,谢谢

Channel和Buffer

概述与实现

所有的IO在NIO中都是从channel开始,channel像是流,buffer像是缓冲区,流可以读到缓冲区中,缓冲区可以写到流中,如下图所示

channel主要实现:FileChannel(文件流),DatagramChannel(数据报流-UDP),SocketChannel(socket流-TCP),ServerSocketChannel(服务端socket流-TCP)

Buffer主要实现(主要是七个IO的基本类型):ByteBuffer,CharBuffer,DoubleBuffer,FloatBuffer,IntBuffer,LongBuffer,ShortBuffer

简单的FileChannel读取数据到buffer
buffer内存模型的三个标记位:
1.capacity(容量):buffer初始化时分配的容量
2.position(位置):buffer内读取或写入的位置(读取或写入时position从0开始,如果在get(position)方法中有赋值,则从该位置开始)
3.limit(最大容量):读取或写入的最大位置(读取时,置为position的位置,写入时,置为catacity的位置) 总结:不管是读取还是写入,都是从position标志的位置开始,到limit的位置结束

将channel读入buffer的示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public void simpleChannelToBuffer(String fromPath){
ByteBuffer buffer=null;
RandomAccessFile file=null;
FileChannel fileChannel=null;
try {
file = new RandomAccessFile(fromPath,"rw");
fileChannel = file.getChannel();//建立到目的文件的通道
buffer = ByteBuffer.allocate(48);//分配48bytes的缓存区
int bytesRead =0 ;
while((bytesRead=fileChannel.read(buffer))!=-1){//将通道内的文件数据读取到缓存区中
System.out.print(bytesRead);
buffer.flip();//切换读模式,position置为0,limit置为写入时的position的位置
while(buffer.hasRemaining()){//缓存区中是否有数据
System.out.print((char)buffer.get());
}
buffer.clear();//清空所有的数据,切换成写模式
//buffer.compact();//清空已经读取的数据,切换成写模式
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
fileChannel.close();
file.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

Scatter(分散)与Gather(聚集)

Scatter(分散):从channel读取数据写入多个buffer中

代码如下:

1
2
3
4
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = { header, body };
channel.read(bufferArray); //依次写入到buffer中

Gather(聚集):将多个buffer的数据写入同一个channel中

代码如下

1
2
3
4
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = { header, body };
channel.write(bufferArray); //依次写入到channel中

通道之间的数据传输

1
2
3
4
5
6
7
8
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel toChannel = toFile.getChannel();
long position = 0; //读取的位置
long count = fromChannel.size(); //读取的大小
toChannel.transferFrom(position, count, fromChannel); //数据从fromChannel到toChannel
//另一种写法:fromChannel.transferTo(position, count, toChannel);

注意:在SocketChannel中,传输的只会是准备好的数据,可能不足count大小,但是一有数据就会传输,只到buffer被填满

Selector

如果你想单线程异步处理多个流,或者是你的应用打开了多个链接(通道),但是每个链接的的流量又很低,这就是Selector的工作

Selector首先要注册Channel,调用select()方法,一直阻塞到有某个注册的通道(Channel)有事件(数据或者新接连)就绪,等到这个方法返回,线程就开始处理这个通道中的事件

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

selector可以监听channel以下四种不同类型的事件:

  1. OP_CONNECT(连接就绪) :比如:socketChannel
  2. OP_ACCEPT(接收就绪):比如:ServerSocketChannel
  3. OP_READ(读就绪)
  4. OP_WRITE(写就绪)

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Selector selector = Selector.open();//创建一个selector
channel.configureBlocking(false);//设置非阻塞模式
//selector注册channel,事件可以多选,比如SelectionKey.OP_READ | SelectionKey.OP_WRITE
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
while(true) {
int readyChannels = selector.select(); //返回读就绪的channel
if(readyChannels == 0) continue; //如果还没有就继续监听
Set selectedKeys = selector.selectedKeys();//获取已就绪的channel的集合
Iterator keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
//如下是判断哪种事件的channel的处理
if(key.isAcceptable()) {
// a connection was accepted by a ServerSocketChannel.
} else if (key.isConnectable()) {
// a connection was established with a remote server.
} else if (key.isReadable()) {
// a channel is ready for reading
} else if (key.isWritable()) {
// a channel is ready for writing
}
keyIterator.remove();//该事件处理完就移除
}
}

pipe

Java NIO 管道是2个线程之间的单向数据连接。Pipe有一个source通道和一个sink通道。数据会被写到sink通道,从source通道读取

首先创建Pipe:Pipe pipe = Pipe.open();
ThreadA向管道写数据,访问sink通道,代码如下:

1
2
3
4
5
6
7
8
9
Pipe.SinkChannel sinkChannel = pipe.sink();
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()) {
sinkChannel.write(buf);
}

ThreadB向管道读取数据,访问source通道,代码如下:

1
2
3
Pipe.SourceChannel sourceChannel = pipe.source();
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = sourceChannel.read(buf);

NIO和IO

NIO与IO的差异如下:

IO NIO
面向流 面向缓冲
阻塞IO 非阻塞IO
选择器

面向流和面向缓冲

IO是面向流的,是一个不可控制处理方式,必须从头到尾直到读取所有的字节,没有缓冲的余地,而NIO是面向缓冲区的,它先将流中的数据读取到缓冲区中,然后再对数据进行处理,这样增加了数据处理的灵活性

阻塞和非阻塞

IO是阻塞IO,当一个线程在读取数据的时候是阻塞的,再次期间不能做其他的任何事情,直到读写数据结束
NIO是非阻塞IO,当一个线程在读取数据时,在该数据变成可读性之前,也就是空闲时间,可以做其他事情

选择器

Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道

参考

http://ifeve.com/java-nio-all/