Java NIO

作者: 一个没有感情的程序员 | 来源:发表于2019-08-09 11:11 被阅读0次

一、NIO概述

NIO即 non-blocking IO,非阻塞IO,在java中实现主要通过Channel、Buffer、和Selector来实现。
Channel和Buffer :channel相当于一个通道,Buffer相当于输入输出

channel和buffer
Selector:选择器,实现了使用单线程操作多个channel,IO复用
Selector原理:首先向Selector注册某几个Channel。然后调用select()方法,这样,一旦有某个注册的了Channel有事件就绪,线程就可以处理这些事件。事件包括(新连接进来,数据接收等)
channel、buffer、selector这些原理与实现、使用,会在下文详细讲到

二、Channel介绍

2.1 Channel三个特点

  1. 既可以写又可以读
  2. 可以异步读写
  3. 要从一个Buffer读或者从一个Buffer写入

2.2.Channel的实现

基本的几个实现:

  1. FileChannel
  2. DatagramChannel
  3. SocketChannel
  4. ServerSocketChannel
    FileChannel 从文件中读写数据。

DatagramChannel 能通过UDP读写网络中的数据。

SocketChannel 能通过TCP读写网络中的数据。

ServerSocketChannel可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。

RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
ByteBuffer buf = ByteBuffer.allocate(48);
//从通道中读取一个缓冲区大小的数据,如果已经读完了则返回-1。
int bytesRead = inChannel.read(buf);
//判断是否读了数据
while (bytesRead != -1) {
System.out.println("Read " + bytesRead);
//反转Buffer,从写模式转换成读模式
buf.flip();
//使用get()读取Buffer
while(buf.hasRemaining()){
System.out.print((char) buf.get());
}
//清空buf的全部(全部清空了就不用flip反转模式了,因为本来就是空的,非空不反转直接写会造成数据反转了从而混乱)
buf.clear();
//再次给buf写入数据
bytesRead = inChannel.read(buf);
}
//关闭文件
aFile.close();

三、Buffer

Buffer相当于在Java NIO中与通道进行交互的缓冲区,数据从缓冲区读到通道或者从通道读到缓冲区

3.1 Buffer的相关用法

Buffer的使用步骤

  1. 写入数据到Buffer
  2. 调用flip()方法把Buffer从写模式切换为读模式(反转Buffer)
  3. 从Buffer中读取数据
  4. 读完数据调用clear()或者compact()方法清除数据,compact()用于清除已经读过的数据,而clear()方法用于清除全部的数据

代码示例:

//创建文件类型
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
//创建文件通道
FileChannel inChannel = aFile.getChannel();
//创建一个48 byte的缓冲区
ByteBuffer buf = ByteBuffer.allocate(48);
//从通道中读取一个缓冲区大小的数据,如果已经读完了则返回-1。
int bytesRead = inChannel.read(buf); 
while (bytesRead != -1) {
//反转Buffer,从写模式转换成读模式
  buf.flip();  
  //使用get()读取Buffer
  while(buf.hasRemaining()){
      System.out.print((char) buf.get()); // read 1 byte at a time
  }
  //清空buf的全部(全部清空了就不用flip反转模式了,因为本来就是空的,非空不反转直接写会造成数据反转了从而混乱)
  buf.clear(); 
  //再次给buf写入数据
  bytesRead = inChannel.read(buf);
}
//关闭文件
aFile.close();

3.2 buffer三个属性capacity、position、limit

1.capacity 容量
作为一个内存缓存块,capacity为Buffer的大小,你只能往Buffer里写入capacity个单位,一旦Buffer满了,需要将其清空才能继续写数据。(单位:byte、char、long等,取决于Buffer的类型比如ByteBuffer、CharBuffer、LongBuffer)
2.position 指针
当写数据时候,position指向当前写的位置,position初始值为0,最大可为capacity-1,相当于数组
当读取数据,Buffer从写模式切换到读模式,position置零,每读一个单位,position+1
3.limit 当前操作的限制大小
也分为写模式和读模式
当写的时候,限制是最多能写多少数据,相当于Buffer的capacity
当读模式的时候,限制是可以读到多少数据,这时候limit相当于之前写模式的position,即之前写了多少数据,你就最多可以读多少数据。

3.3 Buffer的类型

Java NIO 有以下Buffer类型

  1. ByteBuffer
  2. MappedByteBuffer
  3. CharBuffer
  4. DoubleBuffer
  5. FloatBuffer
  6. IntBuffer
  7. LongBuffer
  8. ShortBuffer

不同的类型相当于不同的容器,里面可以写数据的类型也不一样。

3.4 Buffer大小的分配

ByteBuffer buf = ByteBuffer.allocate(48);

3.5 Buffer的比较

  1. equeals()
    只比较 Buffer本身的一部分,不对里面的内容比较
  2. compareTo()
    比较每一个元素,如果第一个不相等的小于另一个对应的元素,则认为小于。如果全部元素都相等,则比较剩余的长度,先耗尽的为小。

四、Scatter/Gather

scatter(分散):将channel写入多个Bufffer
示例:

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body   = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = { header, body };
channel.read(bufferArray);

按照数组中的顺序写入buffer,当第一个buffer写完后,写第二个。

gather(聚集):将多个Buffer的内容读进同一个channel
示例:

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body   = ByteBuffer.allocate(1024);
//write data into buffers
ByteBuffer[] bufferArray = { header, body };
channel.write(bufferArray);

按照顺序将Buffer数组中的内容读到channel中,只会读取position到limit之间的数据

五、channel之间传输

通道中如果有一个通道是FileChannel,那么可以实现两个channel之间的数据传输

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();
//使用transferFrom传输
toChannel.transferFrom(position, count, fromChannel);

注:把transferFrom改成transferTo则按照相反的传输方向传输。

六、Selector

Selector可以理解为选择器,是Java Nio中管理多个channel的选择器。可以实现一个线程操作多个channel
示例:

//创建一个Selector
Selector selector = Selector.open();
channel.configureBlocking(false);
把通道注册到selector
SelectionKey key = channel.register(selector,Selectionkey.OP_READ);

channel与selector一起使用的时候,channel必须处于非阻塞模式下。所以selector不能与FileChannel一起使用,FileChannel不能切换到非阻塞模式,而套接字都可以。
register()方法的第二个参数是selector的监听事件,当监听事件触发的时候表示该事件已经就绪
可以监听下面四种不同的事件类型
1.Connect
2.Accept
3.Read
4.Write
监听多个事件示例:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
SelectionKey key = channel.register(selector,interesSet)

6.1 SelectionKey

通道注册到selector后会返回一个SelectionKey,这个类型里包含了一些属性:
1.interest集合
2.ready集合
3.Channel
4.Selector
5.附加的对象(可选)

interest集合

用于查看之前注册所选择的事件,获取示例

int interestSet = selectionKey.interestOps();

ready集合

ready集合是已经准备就绪的操作的集合,可以用来检测什么事件已经就绪,用以下四个方法,如果事件就绪则返回true。

selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();

获取Channel和Selector

代码如下:

Channel channel  = selectionKey.channel();
Selector selector = selectionKey.selector();

附加对象

可以将一个或者多个对象信息附在selectionKey上,方便管理或者操作,比如将Buffer附上
代码示例:

selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();

或者:

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

在注册的时候直接附上

6.2 Selector选择通道

select()方法

执行Selector的select()方法可以指导是否有感兴趣的通道就绪
select() 阻塞到至少有一个通道在你注册的事件上就绪了。
select(long timeout) 和select()一样,除了最长会阻塞timeout毫秒(参数)。
selectNow() 不会阻塞,不管什么通道就绪都立刻返回(译者注:此方法执行非阻塞的选择操作。如果自从前一次选择操作后,没有通道变成可选择的,则此方法直接返回零。)。
注意:调用select方法返回的数字是上一次调用select到这次期间新就绪的通道
一旦调用了select()方法,并且返回值表明有一个或更多个通道就绪了,然后可以通过调用selector的selectedKeys()方法,访问“已选择键集(selected key set)”中的就绪通道。如下所示:

Set selectedKeys = selector.selectedKeys();

当像Selector注册Channel时,Channel.register()方法会返回一个SelectionKey 对象。这个对象代表了注册到该Selector的通道。可以通过SelectionKey的selectedKeySet()方法访问这些对象。
可以遍历这个已选择的键集合来访问就绪的通道。如下:

//返回通道集合
Set selectedKeys = selector.selectedKeys();
遍历 Set
Iterator keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
    //从keyset中拿到SelectionKey
    SelectionKey key = keyIterator.next();
    if(key.isAcceptable()) {
        //访问就绪逻辑
    } else if (key.isConnectable()) {
        //连接就绪逻辑
    } else if (key.isReadable()) {
        //读就绪逻辑
    } else if (key.isWritable()) {
        //写就绪逻辑
    }
    keyIterator.remove();
}

七、SocketChannel

SocketChannel是一个用于处理TCP网络套接字连接的Channel,可以通过两种方式创建SoketChannel
1.主动连接:通过打开一个SocketChannel类型的Channel主动连接到互联网某台服务器
2.接受连接:通过ServerSocketChannel接受一个连接,会生成一个SocketChannel。
这里主要讲主动连接:

//打开一个SocketChannel
SocketChannel socketChannel = SocketChannel.open();
//把SocketChannel连接到ip端口
socketChannel.connect(new InetSocketAddress("http://jianshu.com", 80));
//创建一个字节型的缓冲区
ByteBuffer buf = ByteBuffer.allocate(48);
//把通道内容读进缓冲区,read表示读了多少字节进Buffer里,如果返回-1则表示已经读到了末尾
int bytesRead = socketChannel.read(buf);
while(byteRead!=-1){
    bytesRead=soketChannel.read(buf)
    //doSomeThing
}
String returnDate="ok";
ByteBuffer outBuffer= ByteBuffer.allocate(48);
//先清空
outBuffer.clear();
//把数据放入Buffer
outBuffer.put(returnDate.getBytes());
//切换到读模式
outBuffer.flip();
//循环写入通道
while(outBuffer.hasRemaining()) {
    socketChannel.write(buf);
}
//关闭通道
socketChannel.close();

非阻塞模式:SocketChannel的非阻塞模式主要体现在connect(),read(),write(),通过调用SocketChannel的configureBlocking(false|truee)方法来设置是否为非阻塞模式。
下面是connect的非阻塞示例,其他的与此类似。

socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("http://jianshu.com", 80));
while(! socketChannel.finishConnect() ){
    //wait, or do something else...
}

八、ServerSocketChannel

在javaNio中,ServerSocketChannel可以监听某个端口新连接的tcp,就像标准IO的ServerSocket一样。
同时可以使用ServerSocketChannel的accept()方法来获取SocketChannel
阻塞模式:

//打开ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//监听9999端口
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
//通过while(true)来持续性监听
while(true){
//accept()方法会一致阻塞直到新的连接到达,新连接到达会返回一个SocketChannel
 SocketChannel socketChannel =serverSocketChannel.accept();

    //do something with socketChannel...
}
关闭ServerSockketChannel
serverSocketChannel.close();

非阻塞模式

//打开ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//监听9999端口
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
//设置为非阻塞模式,accept立即返回,如果没有连接返回的对象是null
serverSocketChannel.configureBlocking(false);
//通过while(true)来持续性监听
while(true){
//accept()方法会一致阻塞直到新的连接到达,新连接到达会返回一个SocketChannel
 SocketChannel socketChannel =serverSocketChannel.accept();
if(socketChannel != null){

    //do something with socketChannel...
    }
}
//关闭ServerSockketChannel
serverSocketChannel.close();

相信看完这些,读者们已经知道如何使用JavaNIO了吧,推荐刚学的读者手动写个使用selector+异步channel的demo,有助于串联起上面说的有些零散的知识,如果需要示例代码请私聊回复。
点击关注不迷路

相关文章

网友评论

      本文标题:Java NIO

      本文链接:https://www.haomeiwen.com/subject/moycjctx.html