JAVA NIO

NIO的几个基础概念

Channel(通道),Buffer(缓冲区),Selector(选择器)

1.Channel

1>通道,为某sb提供了了渠道。而下面的例子中InputStream实际上就是读取文件的通道

 1 public class Test {
 2 
 3     public static void main(String[] args) throws IOException  {
 4 
 5         File file = new File("data.txt");
 6 
 7         InputStream inputStream = new FileInputStream(file);
 8 
 9         byte[] bytes = new byte[1024];
10 
11       //read(byte[] b) :  从输入流中读取一定数量的字节,并将其存储在缓冲区数组 b 中。
12 
13      //返回实际读取的整数形式字节数。
14 
15         inputStream.read(bytes);
16 
17         inputStream.close();
18 
19     }  
20 
21 }

如此,我们将NIO中的Channel同传统IO的Stream来类比。不同的是,IO中,Stream是单向的,比如InputStream只能进行读取操作,OutputStream只能进行写操作。而NIO中的Channel是双向的,既可以进行读操作,又可以进行写操作。

2>以下是常用的几种通道:

FileChannel  读写本地文件的数据,不支持Selector控制,对应File类
SocketChannel  通过TCP读写网络中的数据,对应Socket类
ServerSocketChannel  监听新的TCP连接,并且会创建一个可读写的SocketChannel,对应ServerSocket类
DatagramChannel  通过UDP读写网络中的数据,对应DatagramSocket类

3>Channel方法

int read(ByteBuffer dst)  从Channel到中读取数据到ByteBuffer
long read(ByteBuffer[] dsts) 将Channel到中的数据“分散”到ByteBuffer[]
int write(ByteBuffer src) 将ByteBuffer到中的数据写入到Channel
long write(ByteBuffer[] srcs) 将ByteBuffer[]到中的数据“聚集”到Channel
ong position() 返回此通道的文件位置
FileChannel position(long p) 设置此通道的文件位置
long size()  返回此通道的文件的当前大小
FileChannel truncate(long s) 将此通道的文件截取为给定大小
void force(boolean metaData) 强制将所有对此通道的文件更新写入到存储设备中

4>主要获取方式

java针对支持通道的类提供了getChannel()方法

FileInputStream fis = new FileInputStream("D:\1.jpg");

FileChannel inChannel = fis.getChannel();

jdk1.7的nio2只对各个通道提供了一个静态方法open()

FileChannel inChannel = FileChannel.open(Paths.get("D:\1.jpg"),

StandardOpenOption.READ);

5>通道之间的数据传输

read&write

//将 Buffer 中数据写入 Channel

outChannel.write(buff)

//从 Channel 读取数据到 Buffer

inChannel.read(buff)

transferFrom

从源信道读取字节到这个通道的文件中。

 1     //复制图片,利用直接缓存区
 2 
 3     public void test() throws Exception{
 4 
 5         FileChannel inChannel = FileChannel.open(Paths.get("D:\1.jpg"), StandardOpenOption.READ);
 6 
 7         FileChannel outChannel = FileChannel.open(Paths.get("D:\2.jpg"), StandardOpenOption.READ,StandardOpenOption.WRITE,StandardOpenOption.CREATE);
 8 
 9         outChannel.transferFrom(inChannel,0, inChannel.size()); 
10 
11         inChannel.close();
12 
13         outChannel.close();
14 
15     }

transferTo

将字节从这个通道的文件传输到给定的可写字节通道。

 1      //复制图片,利用直接缓存区
 2 
 3     public void test2() throws Exception{
 4 
 5         FileChannel inChannel = FileChannel.open(Paths.get("D:\1.jpg"), StandardOpenOption.READ);
 6 
 7         FileChannel outChannel = FileChannel.open(Paths.get("D:\3.jpg"), StandardOpenOption.READ,StandardOpenOption.WRITE,StandardOpenOption.CREATE);
 8 
 9         inChannel.transferTo(0, inChannel.size(), outChannel);  
10 
11         inChannel.close();
12 
13         outChannel.close();
14 
15     }
16 
17     }

2.Buffer 

1>缓冲区,是NIO中非常重要的东西,NIO中所有数据的读和写都离不开Buffer。类似于,上述代码中,读取数据时放在byte数组中,而NIO中,读取的数据只能放在Buffer中,同样的写入数据也是写入到Buffer中。

2>下图是客户端向服务端发送数据,然后服务端接收数据的过程。

3>Buffer的属性:

capacity  容量  Buffer所能够存放的最大容量
position  位置  下一个被读或写的位置
limit  上界 可供读写的最大位置,用于限制position position < limit
mark   标记 标记位置,用于记录某次读写的位置,可以通过reset()方法回到这里

 

4>Buffer类型

除了boolean类型意外每个基元类型都会有缓冲区。主要讲解的是ByteBuffer.这里大概说一下,Buffer是一个抽象类,包括一个Buffer的最基本属性,比如,容量,位置,上界,标记.子类通过调用父类构造方法来实例化这几个参数,子类也都有各自的容量实现.比如ByteBuffer类用字节数组当缓冲区.旗下又有两个自己的实现类.

类型  缓冲区
byte  ByteBuffer
char  CharBuffer
double  DoubleBuffer
float  FloatBuffer
int  IntBuffer
long  LongBuffer
short  ShortBuffer

5>Buffer的方法

方法 说明
position  移动偏移量指针
limit  移动限制大小指针
mark  打标记,寄了当前偏移量的位置。可使用reset恢复到标记位置
reset  恢复到标记位置
clear  初始化指针,清理所有数据,转换为写模式(实际只是偏移指针,数据还在)
flip   转换为读取模式 byteBuffer.flip();
rewind  重置偏移量指针到初始状态,可以重新写入或重新读取
remaining  可读或可写容量
hasRemaining  是否可读或可写
hasArray 是否有数组缓存,若为堆缓冲区,则会有数据缓存,若为直接缓冲区,则没有
offset  当前数组偏移量,当把当前数组切片时,无需复制内存,直接指向偏移量。

为了更清晰的说明缓冲区的功能,接下来以ByteBuffer举例(各类型的功能上基本大同小异)

方法  说明
allocate  申请堆缓冲区 ByteBuffer byteBuffer = ByteBuffer.allocate(8);
allocateDirect  申请直接缓冲区 ByteBuffer byteBuffer = ByteBuffer.allocateDirect(8);
wrap  将字节数组包在缓冲区中,可以理解为将字节数组转换为字节堆缓冲区
slice  缓冲区切片,当前偏移量到当前限制大小的内存生成一个缓冲区,无需复制内存,直接指向偏移量
duplicate  共享一份缓冲区,缓冲区内容修改会互相影响,读取不影响
asReadOnlyBuffer  拷贝一份只读的缓冲区
ix 根据实际的offset偏移,对于外部来说是透明的,比如缓冲区切片之后,生成新的缓冲区实际是同一片内存的缓冲区存在offset偏移量,对切片后的缓冲区读写都会做便宜操作。
compact  初始化指针,清理已读取数据,转换为写模式(实际只是偏移指针position,数据还在)此时将data初始化,会将未读取的2个字节复制到数组头部,同时转换为写模式。
getXXX  读取数据  byte[] data1 = new byte[3];  byteBuffer.get(data1);
putXXX  写入数据 byte[] data = new byte[] {'H','E','L','L','O'}; byteBuffer.put(data);
asXXXBuffer  转换为指定类型的缓冲区,字节缓冲区可以转换为其他基元类型的缓冲区,其他基元类型缓冲区不能反过来转换

总结:NIO通过引入缓冲区的概念使得对字节操作比传统字节操作方便一些,但是读写模式需要来回转换会让人有点头晕。

3.Selector

1>选择器,多路复用器。NIO的核心之一。Selector的作用就是用来轮询每个Channel的状态是否处于可读、可写。一旦发现Channel有注册的事情发生,便获取事件然后进行处理。如此可以实现单线程管理多个channels,也就是可以管理多个网络连接。

2>Selector的创建过程

// 1.创建Selector

Selector selector = Selector.open();

// 2.将Channel注册到选择器中

// ....... new channel的过程 ....

//Notes:channel要注册到Selector上就必须是非阻塞的,所以FileChannel是不可以使用Selector的,因为FileChannel是阻塞的

channel.configureBlocking(false);

// 第二个参数指定了我们对 Channel 的什么类型的事件感兴趣

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

// 也可以使用或运算|来组合多个事件,例如

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

// 不过值得注意的是,一个 Channel 仅仅可以被注册到一个 Selector 一次, 如果将 Channel 注册到 Selector 多次, 那么其实就是相当于更新 SelectionKey 的 interest set.

3>一个Channel在Selector注册其代表的是一个SelectionKey事件,SelectionKey类型包括:

OP_READ 可读事件;值为1<<0
OP_WRITE 可写事件;值为1<<2
OP_CONNECT 客户端连接服务端的事件(tcp连接),一般为创建SocketChannel客户端channel;;值为1<<3
OP_ACCEPT 服务端连接客户端的事件,一般为创建ServerSocketChannel客户端channel;;值为1<<4
  OP_READ OP_WRITE OP_CONNECT ACCEPT
服务器ServerSocketChannel       Y
服务器SocketChannel  Y Y    
客户端SocketChannel Y Y Y  

 4>Selector的10个方法

open()  创建一个Selector对象
isOpen() 是否是open状态,如果调用了close()方法则会返回false
provider() 获取当前Selector的Provider
keys() 获取当前channel注册在Selector上所有的key
selectedKeys()  获取当前channel就绪的事件列表
selectNow() 获取当前是否有事件就绪,该方法立即返回结果,不会阻塞;如果返回值>0,则代表存在一个或多个
select(long timeout)   selectNow()的阻塞超时方法,超时时间内,有事件就绪时才会返回;否则超过时间也会返回
select()  selectNow()的阻塞方法,直到有事件就绪时才会返回
wakeup() 调用该方法会时,阻塞在select()处的线程会立马返回;(ps:下面一句划重点)即使当前不存在线程阻塞在select()处,那么下一个执行select()方法的线程也会立即返回结果,相当于执行了一次selectNow()方法
close()  用完Selector后调用其close()方法会关闭该Selector,且使注册到该Selector上的所有SelectionKey实例无效。channel本身并不会关

5>SelectionKey

选择键,谈到Selector就不得不提SelectionKey,两者是紧密关联,配合使用的;每个Channel向Selector注册时,都将会返回一个SelectionKey对象。选择键将Channel与Selector建立了关系,并维护了channel事件。

可以通过cancel方式取消键,取消的键不会立即从selector中移除,而是添加到cancelledKeys中,在下一次select操作时移除它。所以在调用某个key时,需要使用isValid进行校验。

6>SelectionKey的方法

channel()  返回此选择键所关联的通道.即使此key已经被取消,仍然会返回.
selector() 返回此选择键所关联的选择器,即使此键已经被取消,仍然会返回.
isValid() 检测此key是否有效.当key被取消,或者通道被关闭,或者selector被关闭,都将导致此key无效.在AbstractSelector.removeKey(key)中,会导致selectionKey被置为无效.
cancel() 请求将此键取消注册.一旦返回成功,那么该键就是无效的,被添加到selector的cancelledKeys中.cancel操作将key的valid属性置为false,并执行selector.cancel(key)(即将key加入cancelledkey集合)
interesOps() 获得此键的interes集合.
interestOps(int ops) 将此键的interst设置为指定值.此操作会对ops和channel.validOps进行校验.如果此ops不会当前channel支持,将抛出异常.
readyOps()  获取此键上ready操作集合.即在当前通道上已经就绪的事件.
isReadable() 检测此键是否为"read"事件.等效于:k.,readyOps() & OP_READ != 0
isWritable() 测试此键的通道是否已准备好进行写入
isAcceptable() 测试此键的通道是否已准备好接受新的套接字连接
isConnectable() 测试此键的通道是否已完成其套接字连接操作
attach(Object ob) 将给定的对象作为附件添加到此key上.在key有效期间,附件可以在多个ops事件中传递.
attachment() 获取附件.一个channel的附件,可以再当前Channel(或者说是SelectionKey)生命周期中共享,但是attachment数据不会作为socket数据在网络中传输.
原文地址:https://www.cnblogs.com/chensisi/p/13163590.html