java进阶--初识Nio(浅显易懂)

本文将以最浅显易懂的方式来入门NIO。

一:了解NIO之前,我们先了解BIO

  先看两者区别:

    所谓IO,无非就是用来文件存取(与磁盘交互)或网络传输的。BIO是阻塞的,NIO是非阻塞的。我们先不灌输太多理论,一点点深入。

 

下面我们会围绕代码实例解读:首先我们先看BIO写的一段网络通信的代码,毕竟NIO是为了优化BIO而出现的。

1. 案例一:

服务端代码:

 1 import java.net.ServerSocket;
 2 import java.net.Socket;
 3 
 4 public class BioServer {
 5 
 6     static byte[] bytes = new byte[128];
 7 
 8     public static void main(String[] args) throws Exception{
 9 
10         ServerSocket serverSocket = new ServerSocket(8080);
11 
12         while (true){
14             System.out.println("等待连接");
16             //会发送阻塞  让当前线程放弃CPU
17             //这里阻塞 会让socket进来,只能发一条数据
18             Socket clientSocket = serverSocket.accept();
19 
21             System.out.println("连接 接收到了");
22 
23             //有可能连上了,不发送数据,这个时候就相当于服务端瘫痪了,别人也连不上
24             //read也会阻塞
25             clientSocket.getInputStream().read(bytes);
26 
27             System.out.println("接收数据 "+ new String(bytes).trim());
28         }
29     }
30     
31 }

客户端代码:

 1 import java.net.Socket;
 2 
 3 public class BioClient {
 4 
 5     public static void main(String[] args) throws Exception{
 6         Socket socket = new Socket("127.0.0.1", 8080);
 7      
 8         socket.getOutputStream().write("我是client1".getBytes());
 9 
10         socket.close();
11     }
12 }

1.1 代码解读:

  很简单的两段代码,服务端在本地起来8080端口的ServerSocket(是用来接收Socket连接请求及发送数据的), 然后开了循环来始终保持监听外部消息。在18行 accept方法就是用来获取连接的,在25行,read方法就是从获取到的socket连接里取出输入流再读到bytes数组里,简单来讲,就是开监听,获取连接读数据。

  客户端,则是建立了一个Socket,绑上服务端的ip及端口,然后再往流里写数据发送。

服务端的打印结果为:

等待连接
连接 接收到了
接收数据 我是client1
等待连接

1.2 问题在哪呢?

  问题一:accept方法和read方法都是阻塞的,什么是阻塞现象?意思就是说,当代码执行到accept方法这一行时,如果获取不到连接,那么代码将一直卡在这行,直到有socket连接进来才继续往下走,同理read方法也是要等到取到信息才行。

  问题二:在18行我们获取到了clientSocket,但是一次循环后,这个clientSocket就丢失了,我们还不明确这个socket还有没有数据要发送(不可能每发送一次数据就得建立一次连接),我们就把它丢了,肯定是不合理的。

1.3 造成的影响?

  如果两个客户端都想要和服务端连接发送数据,首先客户端A连接进来,但只是连接,没有发送数据。那么代码将执行了accept方法,但会卡在read方法这里,此时,第二个客户端将连接不进来,因为代码此时卡死在read方法,无法执行accept方法。这种情况因为read方法阻塞造成的影响。

  第二种情况,如果一个客户端连接进来想要发送两次数据,于是当第一次连接发送数据时,都很顺利的执行了accept和read方法,然后程序再次运行到了accept方法,但是这个客户端还想要发送第二次数据呢,不可能再次连接吧。一次连接,多次传输,这是最正常不过的需求了。这种情况是因为accept方法阻塞所造成的影响。

1.4 如何解决?

  开多线程是否可以解决呢?一个线程管理一个socket连接,根本不存在阻塞问题,但是想想 你得开多少线程来维护一个大型网站上万的连接请求,服务器资源早就被拖垮了。所以多线程方法并不可取。

  如果能让accept方法和read方法解阻塞呢?我们假设它有这样的方法,先命名为isBlock(false),好吧。再顺便解决下上述的问题二,我们建立一个List集合把每次获取到的socket存起来,防止丢失,防止这些socket还有发送数据的请求。

 

2. 案例一转案例二:

于是代码演变成如下:

 1 import java.net.ServerSocket;
 2 import java.net.Socket;
 3 import java.util.ArrayList;
 4 import java.util.List;
 5 
 6 public class BioServerTemp2 {
 7 
 8     static byte[] bytes = new byte[128];
 9 
10     static List<Socket> socketList = new ArrayList<>();
11 
12     public static void main(String[] args) throws Exception{
13 
14         ServerSocket serverSocket = new ServerSocket(8080);
15         
16         while (true){
17 
18             serverSocket.isBlock(false);
19             
20             //解阻塞  其次让accept也解阻塞
21             Socket clientSocket = serverSocket.accept();
22 
23             if(clientSocket != null){
24                 clientSocket.isBlock(false);
25                 socketList.add(clientSocket);
26             }
27             
28             for(Socket socket : socketList){
29                 //read解阻塞,下个连接就能连上了  如何解阻塞  不考虑多线程
30                 int read = socket.getInputStream().read(bytes);
31 
32                 if(read > 0){
33                     //表示读到数据
34                     System.out.println(new String(bytes).trim());
35                 }
36             }
37             
38         }
39     }
40 
41 }

 2.1 代码解读

  我们在第18行和第24行 调用了isBlock(false)方法,当然这个方法是我们想象出来的,希望jdk能为我们提供的,所以这里会报红,复制的同学可以先注释掉。

  来看看整体代码,在第一版的服务端代码基础上我们做了改善,给serverSocket设置成了非阻塞,所以它调用accept方法时,也是非阻塞, 我们同时给clientSocket也设置成了非阻塞,所以它调用read方法时,也是非阻塞,此外我们还将每一次获取到的clientSocket都放入到了socketList中,每一次的循环都会遍历socketList,看看里面的socket是不是还在发送数据。

  此时,我们解决了上面说的两个问题。

2.2 但是已经没问题了么?

  问题一:我们还不明确jdk是不是提供非阻塞的方法。

  问题二:如果大量的socket进来,会使得socketList变的很大,一次循环的时间会非常长,一旦很长,那么就不能马上执行到accept方法,去获取到新的socket连接,会造成连接卡顿。而且正常情况下,这里面大部分的socket都不会发送数据了,也可能已经断开了连接,但是我们还在期待它发送数据,这样必然会造成时间资源的极大浪费。

2.3 如何解决?

  我们试想 如果这个对socketList的循环,时间上可以变得非常短,并且我们只关注那些确实在发送数据的socket,对于那些不发送数据的,或者已经断开连接的,我们根本不想关注。

 

二:千呼万唤始出来,本文的重点--NIO

  我们用NIO的方式 重写上面的代码,如下:

 1 public static void main(String[] args) throws IOException {
 2 
 3         //实例化ServerSocketChannel
 4         ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
 5 
 6         SocketAddress socketAddress = new InetSocketAddress("127.0.0.1", 8080);
 7 
 8         //绑定地址
 9         serverSocketChannel.bind(socketAddress);
10 
11         //设置非阻塞
12         serverSocketChannel.configureBlocking(false);
13 
14         while (true){
15 
16             //在非阻塞式信道上调用一个方法总是会立即返回。这种调用的返回值指示了所请求的操作完成的程度。
17             // 例如,在一个非阻塞式ServerSocketChannel上调用accept()方法,如果有连接请求来了,则返回客户端SocketChannel,否则返回null。
18             SocketChannel socketChannel = serverSocketChannel.accept();
19 
20 
21             if(accept != null){
22                 System.out.println("连接 接收到了");
23                 socketChannel.configureBlocking(false);
24 
25                 socketChannelList.add(socketChannel);
26             }
27 
28         }
29     }

 1. 代码解读

  第4到第9行,就是配置服务端的连接信息,相对传统BIO会稍显麻烦一些。  NIO中的ServerSocketChannel 就和 BIO中的 ServerSocket 一样,看第12行,它在调用configureBlocking(false),这里就是解阻塞方法,是不是和上面的isBlock(false)方法一致,在第18行,我们接收到了SocketChannel,就和BIO的Socket一样,同样,在第23行,也调用了socketChannel的configureBlocking(false)方法,确保下面的read读取数据不再阻塞。

  一开始就上NIO代码,可能会看不懂,不知道这些解阻塞方法是干嘛用的,现在应该就清楚了。

 

上面的问题二,面对大量的循环,如何缩短时间,等下解决,我们现在可以了解NIO的基础概念了。

 

2. NIO--基础概念

  NIO 和 IO 之间的主要区别
IO
NIO
以 Stream 为导向,面向流
以 Buffer 为导向,面向缓冲区
阻塞 IO
非阻塞 IO 选择器 

 

   老生常谈的概念,NIO是以buffer为导向,面向缓冲区的,第一次看到这个,我也不清楚,直到我听到一句解释。

 

 

 

  传统BIO,客户端和服务端的数据传输是面向流的,像是河流,它是单向的。而NIO面向buffer缓冲区,图上的通道像是铁路,而缓冲区则像是火车,火车两地的运输是双向的,载着我们要传输的数据,客户端发送数据,就把数据放到车厢里,然后到站了,服务端再从中取出数据,并且如果需要的话,也可以放入数据到车厢中给客户端回复,客户端也有取数据的能力。

 

 

3. 关键词解读

  在java的NIO中,有几个重要的概念。Channel,Buffer 和 Selector 构成了 Nio API 的核心。

  Buffer:我们先看Buffer,也就是这个放数据的载体。NIO中的关键Buffer实现有:ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer;你想存放什么类型的数据,就用什么类型的buffer。实际上就是一个数组。

  Buffer包含几个基本的属性:
    •   capacity:该Buffer容量;
    •   limit:结束标记下标,表示进行下一个读写操作时的(最大)结束位置;
    •   position:当前的下标位置,
    •   mark: 自定义的标记位置;

  无论如何,这4个属性总会满足如下关系:mark <= position <= limit <= capacity。

 

 

 

 关于buffer的用法,不是本文的重点,不过我也写了demo,例举了关于buffer的几种api使用。如下,有兴趣的,可以跑一下。

 1 import java.nio.ByteBuffer;
 2 
 3 /**
 4  * 一:除了boolean类型外,其他7种基本数据类型都有对应的buffer包装类
 5  *
 6  * ByteBuffer
 7  * charBuffer
 8  * shortBuffer
 9  * IntBuffer
10  * LongBuffer
11  * FloatBuffer
12  * DoubleBuffer
13  *
14  * 上述缓冲区的管理方式几乎一致,通过allocate() 获取缓冲区
15  *
16  *
17  * 二:缓冲区有两个核心方法:
18  *
19  * put(): 存数据到缓冲区去
20  * get(): 从缓冲区中去取数据
21  *
22  *
23  * 三:缓冲区有四个属性
24  *
25  *  capacity: 缓存区的最大容量,一旦声明不能改变
26  *  limit:界限  表示缓冲区可操作数据的大小(limit后数据不能进行读写)
27  *  position: 指针位置,表示缓冲区正在操作数据的位置
28  *  mark: 标记当前position所在的位置,可以通过reset() 恢复到mark的位置
29  *
30  *  mark <= position <= limit <= capacity
31  *
32  */
33 public class TestBuffer {
34 
35 
36     public static void main(String[] args) {
37 
38         String str = "abcde";
39 
40         /** 1. 分配缓冲区  */
41         ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
42 
43         System.out.println("-------------allocate 分配缓冲区-----------");
44         System.out.println(byteBuffer.position());
45         System.out.println(byteBuffer.limit());
46         System.out.println(byteBuffer.capacity());
47 
48         /** 2. put数据  */
49         byteBuffer.put(str.getBytes());
50 
51         System.out.println("-------------put数据-----------");
52         System.out.println(byteBuffer.position());
53         System.out.println(byteBuffer.limit());
54         System.out.println(byteBuffer.capacity());
55 
56         /** 3. 切换读模式  */
57         byteBuffer.flip();
58 
59         System.out.println("-------------flip 切换读模式-----------");
60         System.out.println(byteBuffer.position());
61         System.out.println(byteBuffer.limit());
62         System.out.println(byteBuffer.capacity());
63 
64         /** 4. 读取数据 */
65         byte[] bytes = new byte[byteBuffer.limit()];
66         byteBuffer.get(bytes);
67 
68         System.out.println("-------------get 读取数据-----------");
69         System.out.println(new String(bytes, 0, bytes.length));
70         System.out.println(byteBuffer.position());
71         System.out.println(byteBuffer.limit());
72         System.out.println(byteBuffer.capacity());
73 
74 
75         /** 5. 可重复读 */
76         byteBuffer.rewind();
77 
78         System.out.println("-------------rewind 可重复读-----------");
79         System.out.println(byteBuffer.position());
80         System.out.println(byteBuffer.limit());
81         System.out.println(byteBuffer.capacity());
82 
83 
84         /** 6. 清空缓冲区 */
85         byteBuffer.clear();
86 
87         System.out.println("-------------clear 清空缓冲区-----------");
88         System.out.println(byteBuffer.position());
89         System.out.println(byteBuffer.limit());
90         System.out.println(byteBuffer.capacity());
91 
92         System.out.println(byteBuffer.get());
93 
94 
95 
96     }
97 
98 
99 }
View Code

 

channel的概念,我们上面代码例子已经覆盖,就是ServerSocketChannel 和 SocketChannel,没什么好讲的。

 

本文重点来了 -- Selector 选择器

  关于Selector选择器,这可真是NIO中博大精深的一点了。

  选择器是一个可以监视多个事件通道的对象(例如:连接打开,数据到达等)。把每个通道都注册到选择器当中去,而选择器的作用就是监控这些通道的io状况。一看概念,还是不能知道这到底是个什么,有什么用。
  
 
我们回想下,上面仍未解决的问题二,如何缩短遍历socketList的时间,如何只关注到有数据发送的socket?
 
  Selector 就是解决如上问题的关键。简单来说,Selector就是讲这个socketList遍历工作交给了操作系统,操作系统执行起来,必然是要比你的程序执行快很多的。然后遍历的结果,操作系统只会反馈有事件发生的socket,比如有发送数据请求了,操作系统才会把这个事件反馈给你的代码程序。说起来简单,但底层的实现却非常复杂。 在linux系统中 调用的是epoll方法,而windows系统中,调用的则是select方法。这里不作赘述,下面会找章节去专门讲解。
 
  选择器可以监听四类事件,用SelectionKey的四个常量来表示:
    SelectionKey.OP_CONNECT 连接事件
    SelectionKey.OP_ACCEPT 接收事件
    SelectionKey.OP_READ 读事件
    SelectionKey.OP_WRITE 写事件
 
是时候上完整代码了,已经在注释中尽量解释了。
服务端代码
  1 import java.io.IOException;
  2 import java.net.InetSocketAddress;
  3 import java.nio.ByteBuffer;
  4 import java.nio.channels.ClosedChannelException;
  5 import java.nio.channels.SelectionKey;
  6 import java.nio.channels.Selector;
  7 import java.nio.channels.ServerSocketChannel;
  8 import java.nio.channels.SocketChannel;
  9 import java.util.*;
 10 
 11 public class Server {
 12 
 13     private Selector selector;
 14     private ByteBuffer readBuffer = ByteBuffer.allocate(1024);//调整缓存的大小可以看到打印输出的变化
 15     private ByteBuffer sendBuffer = ByteBuffer.allocate(1024);//调整缓存的大小可以看到打印输出的变化
 16 
 17     String str;
 18 
 19     Scanner scanner = new Scanner(System.in);
 20 
 21     public static void main(String[] args) throws IOException {
 22         System.out.println("server started...");
 23         new Server().start();
 24     }
 25 
 26 
 27     public void start() throws IOException {
 28 
 29         ServerSocketChannel ssc = ServerSocketChannel.open();
 30         //设置为非阻塞
 31         ssc.configureBlocking(false);
 32 
 33         ssc.bind(new InetSocketAddress("localhost", 8080));
 34 
 35         // 通过open()方法建立Selector
 36         selector = Selector.open();
 37 
 38         //注册接收事件
 39         ssc.register(selector, SelectionKey.OP_ACCEPT);
 40         
 41         while (true) {
 42 
 43             //查找一遍  相当于上面的循环了
 44             //这里不会空循环, 阻塞到至少有一个通道在你注册的事件上就绪了。🌟🌟🌟
 45 
 46             /** 通过Selector的select()方法可以选择已经准备就绪的通道*/
 47             //系统底层会依次询问每个通道是否已经就绪
 48             //记住这里面相当于有多个通道,一个客户端维护一个通道 🌟🌟🌟
 49 
 50             //之前在select()调用时进入就绪的通道不会在本次调用中被记入,而在前一次select()调用进入就绪但现在已经不在处于就绪的通道也不会被记入。
 51             //哪怕第一次的通道事件没有被消费,在第二次的select调用中也不会返回第一次的事件  🌟🌟🌟
 52 
 53             //linux中调用的是epoll方法,而在windows中调用的是select方法,返回的是监听到的事件数量
 54             int readyNum = selector.select();
 55 
 56             System.out.println("当前selector监听到的事件数量为" + readyNum);
 57 
 58             if(readyNum == 0){
 59                 continue;
 60             }
 61             //取出有事件的key
 62             Set<SelectionKey> keys = selector.selectedKeys();
 63 
 64             Iterator<SelectionKey> keyIterator = keys.iterator();
 65             
 66             //相当于这里只关心那些产生事件的socket
 67             while (keyIterator.hasNext()) {
 68 
 69                 SelectionKey key = keyIterator.next();
 70 
 71                 //判断是不是已被cancel取消了
 72                 //可以通过cancel方法取消键,取消的键不会立即从selector中移除,而是添加到cancelledKeys中,
 73                 // 在下一次select操作时移除它.所以在调用某个key时,需要使用isValid进行校验.
 74                 if (!key.isValid()) {
 75                     continue;
 76                 }
 77                 
 78                 if (key.isAcceptable()) {
 79                     //检测此键是否为接收事件
 80                     accept(key);
 81 
 82                 } else if (key.isReadable()) {
 83                     //检测此键是否为读事件
 84                     read(key);
 85 
 86                 } else if (key.isWritable()) {
 87                     //检测此键是否为写事件
 88                     write(key);
 89                 }
 90 
 91                 //该事件已经处理,可以丢弃
 92                 keyIterator.remove(); 
 93             }
 94         }
 95     }
 96 
 97     private void accept(SelectionKey key) throws IOException {
 98         ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
 99 
100         SocketChannel clientChannel = ssc.accept();
101         //设置为非阻塞
102         clientChannel.configureBlocking(false);
103         
104         //注册的操作 就相当于往list里面加元素
105         clientChannel.register(selector, SelectionKey.OP_READ);
106 
107         System.out.println("一个新客户端连接进来了:  "+clientChannel.getRemoteAddress());
108     }
109 
110     private void read(SelectionKey key) throws IOException {
111         //读事件 这里就是SocketChannel了
112         SocketChannel socketChannel = (SocketChannel) key.channel();
113 
114         this.readBuffer.clear();
115 
116         int numRead;
117         try {
118             numRead = socketChannel.read(this.readBuffer);
119 
120         } catch (IOException e) {
121 
122             key.cancel();
123             socketChannel.close();
124 
125             return;
126         }
127 
128         readBuffer.flip();
129 
130         byte[] bytes = new byte[readBuffer.limit()];
131 
132         readBuffer.get(bytes);
133 
134         System.out.println("服务端收到信息:  " + new String(bytes, 0, readBuffer.limit()));
135 
136         socketChannel.register(selector, SelectionKey.OP_WRITE);
137 
138     }
139 
140 
141     private void write(SelectionKey key) throws IOException, ClosedChannelException {
142         SocketChannel channel = (SocketChannel) key.channel();
143 
144         System.out.print("服务端回话:   ");
145 
146         String message = scanner.nextLine();
147 
148         sendBuffer.clear();
149         sendBuffer.put(message.getBytes());
150 
151         //切换写模式
152         sendBuffer.flip();
153 
154         //往通道中写数据  向客户端写数据
155         channel.write(sendBuffer);
156 
157         channel.register(selector, SelectionKey.OP_READ);
158 
159     }
160 
161 
162 }
View Code

客户端代码

 1 import java.io.IOException;
 2 import java.net.InetSocketAddress;
 3 import java.nio.ByteBuffer;
 4 import java.nio.channels.SelectionKey;
 5 import java.nio.channels.Selector;
 6 import java.nio.channels.SocketChannel;
 7 import java.util.Iterator;
 8 import java.util.Scanner;
 9 import java.util.Set;
10 
11 public class Client {
12 
13     ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
14     ByteBuffer readBuffer = ByteBuffer.allocate(1024);
15 
16     public static void main(String[] args) throws IOException {
17         new Client().start();
18     }
19 
20     public void start() throws IOException {
21 
22         SocketChannel sc = SocketChannel.open();
23 
24         sc.configureBlocking(false);
25 
26         sc.connect(new InetSocketAddress("localhost", 8080));
27 
28         Selector selector = Selector.open();
29 
30         sc.register(selector, SelectionKey.OP_CONNECT);
31 
32         Scanner scanner = new Scanner(System.in);
33 
34         while (true) {
35 
36             selector.select();
37 
38             Set<SelectionKey> keys = selector.selectedKeys();
39 
40             Iterator<SelectionKey> keyIterator = keys.iterator();
41 
42             while (keyIterator.hasNext()) {
43 
44                 SelectionKey key = keyIterator.next();
45 
46                 // 判断此通道上是否正在进行连接操作。
47                 if (key.isConnectable()) {
48 
49                     sc.finishConnect();
50                     sc.register(selector, SelectionKey.OP_WRITE);
51                     System.out.println("server connected...");
52                     break;
53 
54                 } else if (key.isReadable()){     //读取数据
55 
56                     System.out.print("收到服务端回话:   ");
57                     SocketChannel client = (SocketChannel) key.channel();
58                     //将缓冲区清空以备下次读取
59                     readBuffer.clear();
60                     int num = client.read(readBuffer);
61                     System.out.println(new String(readBuffer.array(),0, num));
62                     //注册读操作,下一次读取
63                     sc.register(selector, SelectionKey.OP_WRITE);
64 
65 
66                 } else if (key.isWritable()) {     //写数据
67 
68                     System.out.println();
69                     System.out.print("客户端请输入对话:   ");
70                     String message = scanner.nextLine();
71                     System.out.println();
72 
73                     writeBuffer.clear();
74                     writeBuffer.put(message.getBytes());
75                     //将缓冲区各标志复位,因为向里面put了数据标志被改变要想从中读取数据发向服务器,就要复位
76                     writeBuffer.flip();
77                     sc.write(writeBuffer);
78 
79                     sc.register(selector, SelectionKey.OP_READ);
80 
81                 }
82 
83                 keyIterator.remove();
84             }
85         }
86     }
87 
88 
89 }
View Code

 

 

建议先运行,看看效果,呈现出来的效果就是服务端和客户端互相通信,类似简单的聊天系统。 然后再读注释,相信有一点基础的同学都可以理解。

 

posted on 2020-10-14 20:18  半城枫叶半城雨丶  阅读(410)  评论(0编辑  收藏  举报