Java NIO入门
1、概述
NIO有三大核心部分:Channel-通道,Buffer-缓冲区,Selector-选择器。
Channel是对传统IO中的流的模拟,读入或者写出的所有数据必须通过一个Channel对象。
Buffer实质上是一个容器对象,从Channel中读取的任何数据都要读到Buffer中,同样发送给一个通道的所有数据都必须先放到Buffer中(数据总是从Channel读入到Buffer中,或者从Buffer中写出到Channel中)。
Selector用于监听多个通道的事件(比如连接打开和数据到达),所以单个线程可以监听多个数据通道。
NIO和传统IO之间的一个最大区别是:IO是面向流的,NIO是面向缓冲区的。IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,数据没有被缓存在任何地方,此外,传统IO不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将数据缓存到一个缓冲区。NIO的缓冲导向方法略有不同,数据先读入到一个稍后处理的缓冲区,需要时可在缓冲区中前后移动(注意:数据移动时需要检查缓冲区中是否包含需要处理的数据,并且,需要确保当更多的数据读入缓冲区时不要覆盖缓冲区里尚未处理的数据)。
传统IO的各种流时阻塞的,当一个线程调用read()或write()方法时该线程会被阻塞,直到有一些数据被读取或者完全写入,线程在阻塞期间不能做其他操作。NIO的非阻塞读,是一个线程向某通道发送请求读取数据,但是仅能得到目前可用的数据,如果目前没有可用的数据就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可读取之前,该线程可以继续做其他的事情;非阻塞读也是如此,一个线程请求写入一些数据到某通道时,不需要等待它完全写入,该线程同时可以去执行其他操作。线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程可以管理多个输入和输出通道。
2、Channel
2.1、 什么是Channel?
Channel是一个对象,可以通过Channel读取和写入数据。Channel和传统IO中Stream流差不多,不过Stream流是单向的(如:InputStream、OutputStream),而Channel是双向的,既可以进行读操作,又可以进行写操作。
2.2 、NIO中Channel的主要实现
NIO中的Channel主要实现有:FileChannel、DatagramChannel、SocketChannel、ServerSocketChannel,分别对应文件IO、UDP、TCP(Client和Server)。
2.3 、FileChannel
案例1:使用传统IO中的FileInputStream读取文件内容
1 public static void readFileWithIO(){
2
3 InputStream in = null;
4 try{
5 in = new BufferedInputStream(new FileInputStream("src/normal_io.txt"));
6 byte[] buf = new byte[1024];
7 int bytesRead = in.read(buf);
8 while(bytesRead != -1){
9 for(int i = 0;i < bytesRead;i++){
10 System.out.print((char)buf[i]);
11 }
12 bytesRead = in.read(buf);
13 }
14 }catch(IOException ex){
15 ex.printStackTrace();
16 }finally{
17 try{
18 if(in != null){
19 in.close();
20 }
21 }catch(IOException ex){
22 ex.printStackTrace();
23 }
24 }
25 }
输出结果:this is normal_io file!!!
案例2:使用NIO读取文件内容(通过RandomAccessFile进行操作,也可以通过FileInputStream.getChannel()进行操作)
1 public static void readFileWithNIO(){
2
3 RandomAccessFile aFile = null;
4 try{
5 aFile = new RandomAccessFile("src/nio.txt","rw");
6 FileChannel fileChannel = aFile.getChannel();
7 ByteBuffer buf = ByteBuffer.allocate(1024);
8 int bytesRead = fileChannel.read(buf);
9 while(bytesRead != -1){
10 buf.flip();
11 while(buf.hasRemaining()){
12 System.out.print((char)buf.get());
13 }
14 buf.compact();
15 bytesRead = fileChannel.read(buf);
16 }
17 }catch(IOException ex){
18 ex.printStackTrace();
19 }finally{
20 try{
21 if(aFile != null){
22 aFile.close();
23 }
24 }catch(Exception ex){
25 ex.printStackTrace();
26 }
27 }
28 }
输出结果:this is nio file!!!
2.4 、SocketChannel与ServerSocketChannel
NIO比传统IO的强大功能部分来自于Channel的非阻塞特性,Socket套接字的某些操作可能会无限期地阻塞。例如:对accept()方法的调用可能会因为等待一个客户端连接而阻塞、对read()方法的调用可能会因为没有数据可读而阻塞直到连接的另一端传来新的数据。NIO的Channel抽象的一个重要特征是可以通过配置Channel的阻塞行为,以实现非阻塞的信道,例如:channel.configureBlocking(false)。
在非阻塞的信道上调用一个方法总是会立即返回,这种返回值指示了所请求的操作完成的程度。例如:在一个非阻塞的ServerSocketChannel上调用accept()方法,如果有连接请求来了则返回客户端SocketChannel,否则返回null。
案例三:TCP应用客户端,该客户端采用NIO实现,而服务端依旧使用BIO实现。
1 public static void client(){
2
3 SocketChannel socketChannel = null;
4 ByteBuffer buffer = ByteBuffer.allocate(1024);
5
6 try{
7 socketChannel = SocketChannel.open();
8 socketChannel.configureBlocking(false);
9 socketChannel.connect(new InetSocketAddress("127.0.0.1",8888));
10 if(socketChannel.finishConnect()){
11 int i = 0;
12 while(true){
13 TimeUnit.SECONDS.sleep(1);
14 String msg = "I'm " + i++ +"-th message from the client";
15 buffer.clear();
16 buffer.put(msg.getBytes());
17 buffer.flip();
18 while(buffer.hasRemaining()){
19 System.out.println(buffer);
20 socketChannel.write(buffer);
21 }
22 }
23 }
24
25 }catch(IOException | InterruptedException ex){
26 ex.printStackTrace();
27 }finally{
28 try{
29 if(socketChannel != null){
30 socketChannel.close();
31 }
32 }catch(IOException ex){
33 ex.printStackTrace();
34 }
35 }
36 }
案例四:TCP应用服务端,该服务端采用BIO实现,客户端采用NIO实现。
1 public static void server(){
2
3 ServerSocket serverSocket = null;
4 InputStream in = null;
5 try{
6 serverSocket = new ServerSocket(8888);
7 int recvMsgSize = 0;
8 byte[] recvBuf = new byte[1024];
9 while(true){
10 Socket clientSocket = serverSocket.accept();
11 SocketAddress clientAddress = clientSocket.getRemoteSocketAddress();
12 System.out.println("Handling client at " + clientAddress);
13 in = clientSocket.getInputStream();
14
15 while((recvMsgSize = in.read(recvBuf)) != -1){
16 byte[] temp = new byte[recvMsgSize];
17 System.arraycopy(recvBuf, 0, temp, 0, recvMsgSize);
18 System.out.println(new String(temp));
19 }
20 }
21 }catch(IOException ex){
22 ex.printStackTrace();
23 }finally{
24 try{
25 if(serverSocket != null){
26 serverSocket.close();
27 }
28 if(in != null){
29 in.close();
30 }
31 }catch(IOException ex){
32 ex.printStackTrace();
33 }
34 }
35 }
案例五:TCP应用服务端,该服务端采用NIO实现,客户端采用NIO实现(即案例三中的客户端)
1 public class NIOServer {
2
3 private static final int BUF_SIZE = 1024; //缓冲区大小
4 private static final int PORT = 8888; //监听端口
5 private static final int TIMEOUT = 3000; //选择器阻塞时间
6
7 public static void main(String[] args) {
8 selector();
9 }
10
11 public static void selector() {
12
13 Selector selector = null;
14 ServerSocketChannel ssc = null;
15 try{
16 //创建Selector对象
17 selector = Selector.open();
18 //打开ServerSocketChannel
19 ssc = ServerSocketChannel.open();
20 //将ServerSocketChannel设置为非阻塞模式
21 ssc.configureBlocking(false);
22 ServerSocket serverSocket = ssc.socket();
23 //绑定监听端口
24 serverSocket.bind(new InetSocketAddress(PORT));
25 //将ServerSocketChannel注册到Selector对象上,并监听对应的感兴趣的事件(如:SelectionKey.OP_CONNECT-连接就绪,SelectionKey.OP_ACCEPT-接收就绪,SelectionKey.OP_READ-读就绪,SelectionKey.OP_WRITE-写就绪)
26 //本例中为SelectionKey.OP_ACCEPT-接收就绪
27 ssc.register(selector, SelectionKey.OP_ACCEPT);
28
29 while(true){
30 //select(TIMEOUT)阻塞到TIMEOUT毫秒数
31 if(selector.select(TIMEOUT) == 0){
32 System.out.println("==");
33 continue;
34 }
35 Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
36 while(iter.hasNext()){
37 SelectionKey key = iter.next();
38 if(key.isAcceptable()){
39 //处理接收
40 handleAccept(key);
41 }
42 if(key.isReadable()){
43 //处理读取
44 handleRead(key);
45 }
46 if(key.isWritable() && key.isValid()){
47 //处理数据写出
48 handleWrite(key);
49 }
50 if(key.isConnectable()){
51 System.out.println("isConnectable = true");
52 }
53 iter.remove();
54 }
55 }
56 }catch(IOException ex){
57 ex.printStackTrace();
58 }finally{
59 try{
60 if(selector != null){
61 selector.close();
62 }
63 if(ssc != null){
64 //关闭ServerSocketChannel
65 ssc.close();
66 }
67 }catch(IOException ex){
68 ex.printStackTrace();
69 }
70 }
71 }
72
73 public static void handleAccept(SelectionKey key) throws IOException {
74
75 ServerSocketChannel ssChannel = (ServerSocketChannel)key.channel();
76 //监听新进来的端口
77 SocketChannel sc = ssChannel.accept();
78 sc.configureBlocking(false);
79 sc.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocateDirect(BUF_SIZE));
80 }
81
82 public static void handleRead(SelectionKey key) throws IOException {
83
84 SocketChannel sc = (SocketChannel)key.channel();
85 ByteBuffer buf = (ByteBuffer)key.attachment();
86 long bytesRead = sc.read(buf);
87 while(bytesRead > 0){
88 buf.flip();
89 while(buf.hasRemaining()){
90 System.out.print((char)buf.get());
91 }
92 System.out.println();
93 buf.clear();
94 bytesRead = sc.read(buf);
95 }
96 if(bytesRead == -1){
97 sc.close();
98 }
99 }
100
101 public static void handleWrite(SelectionKey key) throws IOException{
102
103 ByteBuffer buf = (ByteBuffer)key.attachment();
104 buf.flip();
105 SocketChannel sc = (SocketChannel)key.channel();
106 while(buf.hasRemaining()){
107 sc.write(buf);
108 }
109 buf.compact();
110 }
111 }
运行结果:略
3、Buffer
3.1、 什么是Buffer?
Buffer是一个对象,包含一些要写入或者读出的数据。在NIO中,所有数据都是用缓冲区处理的,在读入数据时,数据直接读入到缓冲区中;在写出数据时,数据从缓冲区中被写出。缓冲区实质上是一个数组,但是缓冲区不仅仅是一个数组,缓冲区提供了对数据的结构化访问,而且可以跟踪系统的读写进程。
可以把Buffer理解为一组基本数据类型的元素列表,它通过4个变量来保存数据的当前位置状态:capacity、position、limit、mark,含义如下:
| 索引 | 说明 |
| capacity | 缓冲区数组的总长度 |
| position | 下一个要操作的数据元素的位置 |
| limit | 缓冲区数组中下一个不可操作的元素的位置,limit <= capacity |
| mark | 用于记录当前position的前一个位置或者默认-1 |
说明:
1)position:position变量跟踪缓冲区已经写了多少数据,它指定了下一个字节要放到数组的哪一个元素中。如果从通道中读取3个字节放到缓冲区中,则缓冲区的position将会设置为3,指向数组中的第四个元素;同样,在将数据写入通道时,position值跟踪从缓冲区中获取了多少数据,position指定下一个写入到通道的字节来自数组的哪一个元素,如果从缓冲区写了5个字节到通道中,那么缓冲区的position将被设置为5,指向缓冲数组的第6个元素。
2)limit:limit变量表明在从缓冲区写入通道时还有多少数据需要取出,或者在从通道读入缓冲区时还有多少空间可以存放数据。
3)capacity:capacity变量指定了可以存储在缓冲区中的最大数据容量。
举例:以ByteBuffer为例说明各参数的作用
假设通过ByteBuffer.allocate(10)方法创建一个10字节的数组的缓冲区,总容量capacity即为10个字节,缓冲区Buffer的状态如下图:

limit不能大于capacity,此例中两个值都被设置为10,通过将limit和capacity指向数组的尾部之后来说明这一点,如下图:

将position设置为0,当读取数据进入缓冲区时,下一个读取的数据就进入slot 0;当从缓冲区写出数据时,从缓冲区读取的下一个字节就来自slot 0。position的设置如下图(由于capacity不会改变,在接下来的讨论中可以忽略):

第一次从输入通道中读取3个字节到Buffer中,从position位置开始这3个字节将被依次放入到0、1、2的位置,此时position就增加到3,limit没有改变,如下图:

第二次从输入通道中读取2个字节到Buffer中,从position位置开始这3个字节将被依次放入到3、4的位置,此时position就增加到5,limit依然没有改变,如下图:

当需要将Buffer中的数据写出到通道中时,必须调用flip()方法,flip()方法将limit设置为当前position,将position设置为0,如下图:

然后将数据从Buffer中写入到通道中,将从position位置开始依次将数据写入到通道中。此时position设置为0,limit为原来的position,这意味着要写入到通道的数据包含之前读到的所有字节,并且一个字节也不多。
假设,第一次将4个字节数据从Buffer中写入到输出通道,则position增加到4,limit不变,如下图:

第二次写入时Buffer中只剩些1字节可写。limit在调用flip()方法时被设置为5,并且position不能超过limit,所以最后一次写入操作将从缓冲区取出最后1字节并将它写入到输出通道,position增加到5,并且limit不变,如下图:

最后,调用缓冲区的clear()方法或者compact()方法重设缓冲区以便接收更多的字节数据。
1)clear()方法将limit设置为与capacity相同,将position设置为0,如下图:

2)compact()方法将limit设置为与capacity,将position设置为0,如下图:

此时,缓冲区可以循环往复继续接收新的数据。
注意:
调用clear()方法时,position将被设置为0,limit设置为capacity,但是Buffer中的数据并未被清除。这些标记只是告诉我们下次可以从哪里开始往Buffer中写入数据,如果Buffer中有一些未读的数据,调用clear()方法后,数据将“被遗忘”,意味着不再有任何标记去表示哪些数据被读过哪些数据没有被读过。
调用compact()时,如果Buffer中仍有未读的数据,compact()方法会将Buffer中所有未读的数据拷贝到Buffer起始处,然后将position设置到最后一个未读元素的下一个位置,limit属性依然像clear()方法一样被设置为capacity,并不会覆盖未读的数据。
3.2 、NIO中Buffer类型
NIO中的关键Buffer实现有:ByteBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer、CharBuffer,分别对应基本数据类型:byte、short、int、long、float、double、char。另外,NIO中还有MappedByteBuffer、HeapByteBuffer、DirectByteBuffer等类型。
3.3 、Buffer的使用
通过以上案例可总结出使用Buffer缓冲区的一般步骤:
1)分配缓冲空间,如:ByteBuffer buf = ByteBuffer.allocate(1024);
2)写入数据到Buffer缓冲区,如:int bytesRead = fileChannel.read(buf);
3)调用flip方法,如:buf.flip();
4)从Buffer缓冲区中读取数据,如:System.out.println((char)buf.get())
5)调用clear()方法或者compact()方法
3.4、 Buffer数据访问
3.4.1、get()方法
ByteBuffer类中有4个get()方法:
1)byte get()
2)ByteBuffer get(byte[] dst)
3)ByteBuffer get(byte[] dst, int offset, int length)
4)byte get(int index)
方法1)获取单个字节;方法2)和方法3)将一组字节读入到一个数组中;方法4)从缓冲区中的特定位置获取字节;返回类型为ByteBuffer的方法返回的是调用该方法的缓冲区的this值。
另外,方法1)、2)、3)是相对的,方法4)是绝对的。相对意味着get操作服从position和limit的值,即数据从当前position读取,在get操作之后position的值会增加;绝对意味着get操作会忽略position和limit的值,并不会Buffer缓冲区的参数产生影响。
3.4.2、put()方法
ByteBuffer中有5个put()方法
1)ByteBuffer put(byte b)
2)ByteBuffer put(byte[] src)
3)ByteBuffer put(byte[] src, int offset, int length)
4)ByteBuffer put(ByteBuffer src)
5)ByteBuffer put(int index, byte b)
方法1)写入(put)单个字节;方法2)、3)写入来自源数组的一组字节;方法4)将数据从数据从一个给定的源ByteBuffer写入当前ByteBuffer;方法5)将字节写入Buffer缓冲区中特定的位置;返回类型为ByteBuffer的方法返回的是调用该方法的缓冲区的this值。
另外,与get()方法一样,把put()方法划分为相对的和绝对的。方法1)、2)、3)、4)是相对的,方法5)是绝对的。
3.4.3、类型化的get()和put()方法
除了前文描述的get()和put()方法,ByteBuffer还有用于读写不同类型值得其他方法,如:
| get()方法 | put()方法 |
| getByte() | putByte() |
| getShort() | putShort() |
| getInt() | putInt() |
| getLong() | putLong() |
| getFloat() | putFloat() |
| getDouble() | putDouble() |
| getChar() | putChar() |
4、IO与NIO的区别
4.1、IO与NIO的主要区别
Java中IO与NIO的主要区别如下表:
| IO | NIO |
| 面向Stream流 | 面向Buffer缓冲区 |
| 阻塞IO | 非阻塞IO |
| 无选择器 | 有选择器 |
1)面向Stream流与面向Buffer缓冲区
Java中IO与NIO之间第一个最大区别就是:IO是面向流的,NIO是面向缓冲区的。IO面向流意味着每次从流中读取一个或多个字节,直至读取多个字节,读取的数据没有被缓存在任何地方,另外IO流不能前后移动流中的数据,如果需要前后移动从流中读取的数据,则需要先将数据缓存到一个缓冲区;NIO的缓冲导向方法略有不同,数据会被读取到一个稍后处理的缓冲区,需要时可以在缓冲区中前后移动,增加了处理过程中的灵活性,注意:使用NIO时需要检查缓冲区中是否包含所需要处理的数据,并且需确保当更多的数据读入缓冲区时不要覆盖缓冲区里尚未处理的数据。
2)阻塞IO与非阻塞IO
Java IO的各种流都是阻塞的,这就意味着当一个线程调用read()或write()时,该线程将被阻塞,直到有一些数据被读取或数据完全写入,该线程在读写期间不能再做其他任何事情。
Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据时仅能得到目前可用的数据,如果目前没有可用数据时,就什么都不会获取到。一个线程请求写入一些数据到某通道,不需要等待完全写入,这个线程同时可以去做别的事情。线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个线程可以管理多个输入和输出通道。
3)选择器Selectors
Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。
4.2、IO和NIO如何影响应用程序的设计
无论选择IO还是NIO工具箱,可能会影响应用程序设计的以下方面:
1)对IO和NIO类的AIP调用不同
使用NIO的API调用与使用IO是有所不同的,这并不意外,使用IO是逐字节读取,而使用NIO时数据必须先读入缓冲区再处理
2)数据处理
使用纯粹的NIO设计相比较IO设计,数据处理也会受到影响。
在IO设计中,通过InputStream或Reader逐字节读取数据。假设正在处理一基于行的文本数据流,内容如下:
Name: Anna
Age: 25
Email: anna@mailserver.com
Phone: 1234567890
该文本行的流可以这样处理:
InputStream input = ... ; // get the InputStream from the client socket
BufferedReader reader = new BufferedReader(new InputStreamReader(input));
String nameLine = reader.readLine();
String ageLine = reader.readLine();
String emailLine = reader.readLine();
String phoneLine = reader.readLine();
请注意处理状态是由程序执行多久决定。换句话说,一旦reader.readLine()方法返回,就知道文本行已读完,readLine()方法阻塞直到整行读完,这就是原因。同时会知道此行包含名称;同样,第二行readLine()调用返回的时候,也会知道此行包含年龄等。该处理程序仅在有新数据读入时运行,并且知道每步的数据是什么,一旦正在运行的线程已处理过读入的某些数据,该线程不会再回退数据(大多如此)。下图说明了这条原则:

而一个NIO的实现会有所不同,下面是一个简单的例子:
ByteBuffer buffer = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buffer);
注意第二行,从通道读取字节到ByteBuffer。当这个方法调用返回时,并不知道所需的所有数据是否都在缓冲区内,只知道该缓冲区包含一些字节,这就使得处理有些困难。假设第一次read(buffer)调用后,读入缓冲区的数据只有半行,例如:"Name: An",显然我们不能处理这样的数据,需要等待,直到整行数据读入缓存,在此之前对数据的任何处理都毫无意义
3)用来处理数据的线程数
NIO可以只使用一个(或几个)线程管理多个通道(网络连接或文件),但付出的代价是解析数据可能会比从一个阻塞流中读取数据更复杂。
如果需要管理同时打开的成千上万个连接,这些连接每次只是发送少量的数据,例如聊天服务器,实现NIO的服务器可能是一个优势。同样,如果你需要维持许多打开的连接到其他计算机上,如P2P网络中,使用一个单独的线程来管理你所有出站连接,可能是一个优势。一个线程多个连接的设计方案如下图所示:

Java NIO:单线程管理多个连接
如果有少量的连接使用非常高的带宽,一次发送大量的数据,也许典型的IO服务器实现可能非常契合。下图说明了一个典型的IO服务器设计:

Java IO:一个典型的IO服务器设计-一个连接通过一个线程处理
浙公网安备 33010602011771号