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--基础概念
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。实际上就是一个数组。
-
- capacity:该Buffer容量;
- limit:结束标记下标,表示进行下一个读写操作时的(最大)结束位置;
- position:当前的下标位置,
- mark: 自定义的标记位置;
关于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 }
channel的概念,我们上面代码例子已经覆盖,就是ServerSocketChannel 和 SocketChannel,没什么好讲的。
本文重点来了 -- Selector 选择器
关于Selector选择器,这可真是NIO中博大精深的一点了。
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 }
客户端代码
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 }
建议先运行,看看效果,呈现出来的效果就是服务端和客户端互相通信,类似简单的聊天系统。 然后再读注释,相信有一点基础的同学都可以理解。