阻塞与非阻塞
阻塞/非阻塞关注的是程序在调用结果时的状态
- 阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
- 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。
我们所学的传统IO是阻塞式的,NIO中提供了非阻塞式的用法,是通过选择器(Selector)监听通道实现的。
下面我们会以NIO编写一个阻塞式和非阻塞式的网络通信
阻塞式网络通信
废话不多说,直接上码
服务端
1 @Test
2 public void server() throws Exception{
3 // 获取通道
4 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
5 FileChannel outChannel = FileChannel.open(Paths.get("src/test/6.png"), StandardOpenOption.CREATE, StandardOpenOption.WRITE);
6
7 // 绑定连接
8 serverSocketChannel.bind(new InetSocketAddress(9898));
9
10 // 获取客户端连接通道
11 SocketChannel socketChannel = serverSocketChannel.accept();
12
13 // 分配指定大小的缓冲区
14 ByteBuffer buf = ByteBuffer.allocate(1024);
15
16 // 接受客户端的数据, 并保存到本地
17 while (socketChannel.read(buf) != -1) {
18 buf.flip();
19 outChannel.write(buf);
20 buf.clear();
21 }
22
23
24 // 发送给客户端回馈
25 buf.put("服务端接收数据成功".getBytes());
26 buf.flip();
27 socketChannel.write(buf);
28
29 // 关闭通道
30 serverSocketChannel.close();
31 outChannel.close();
32
33 }
客户端
1 @Test
2 public void client() throws Exception{
3 // 获取通道
4 SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
5
6 FileChannel channel = FileChannel.open(Paths.get("src/test/4.png"), StandardOpenOption.READ);
7
8 // 分配指定大小的缓冲区
9 ByteBuffer buffer = ByteBuffer.allocate(1024);
10
11 // 读取本地文件,并发送到服务器
12 while (channel.read(buffer) != -1) {
13 buffer.flip();
14 socketChannel.write(buffer);
15 buffer.clear();
16 }
17
18 // socketChannel.shutdownOutput();
19
20 // 接受服务端的反馈
21 int len = 0;
22 while ((len = socketChannel.read(buffer)) != -1) {
23 buffer.flip();
24 System.out.println(new String(buffer.array(), 0,len));
25 buffer.clear();
26 }
27
28
29 // 关闭通道
30 socketChannel.close();
31 channel.close();
32
33 }
先启动服务端再启动客户端,但是我们会发现程序无法结束,就是因为服务端不知道客户端的数据是否发送完成,所以服务端一直在等待客户端数据发送完成再接收,因此程序一直处于阻塞状态。
解决的方法有以下几种:
- 直接关闭客户端(暴力法)
- 在客户端显式说明程序运行到哪里运行数据发送完成
使用Selector选择器(推荐)
非阻塞式通信
在贴代码前我们先来看看选择器(Selector)
选择器的应用
- 当调用register(Selector sel, ing ops)将通道注册选择器时, 选择器对通道的监听事件,需要通过第二个参数ops指定。
- 可以监听的事件类型(可使用SelectionKey)的四个常量表示:
常量 | 解释 |
---|---|
SelectionKey.OP_READ | 监听读 |
SelectionKey.OP_WRITE | 监听写 |
SelectionKey.OP_CONNECT | 监听连接 |
SelectionKey.OP_ACCEPT | 监听接收 |
- 若注册时不止监听一个事件,则可以使用“位或”操作符连接
1// 注册监听事件
2int interestSet = SelectionKey.OP_READ|SelectionKey.OP_WRITE;
下面我们看看代码:
服务端
1 @Test
2 public void noBlockServer() throws Exception {
3 // 获取通道
4 ServerSocketChannel ssChannel = ServerSocketChannel.open();
5
6 // 切换非阻塞模式
7 ssChannel.configureBlocking(false);
8
9 // 绑定连接
10 ssChannel.bind(new InetSocketAddress(9898));
11
12 // 获取选择器
13 Selector selector = Selector.open();
14
15 // 将通道注册到选择器上, 并且指定“监听接收事件”
16 ssChannel.register(selector, SelectionKey.OP_ACCEPT);
17
18 // 轮询式的获取选择器上已经“准备就绪”的事件, 如果selector.select()大于0,说明至少有一个准备就绪
19 while (selector.select() > 0) {
20 // 获取当前选择器中所有注册的“选择键(已就绪的监听事件—)"
21 Iterator<SelectionKey> it = selector.selectedKeys().iterator();
22
23 //迭代获取准备就绪的事件
24 while (it.hasNext()) {
25 // 获取准备就绪的事件
26 SelectionKey sk = it.next();
27 // 判断具体是什么事件准备就绪
28 if (sk.isAcceptable()) {
29 // 若”接收就绪“,获取客户端连接
30 SocketChannel sChannel = ssChannel.accept();
31 // 切换非阻塞模式
32 sChannel.configureBlocking(false);
33 // 将通道注册到选择器上
34 sChannel.register(selector, SelectionKey.OP_READ);
35 } else if (sk.isReadable()) {
36 // 获取当前选择器上”读就绪“状态的通道
37 SocketChannel sChannel = (SocketChannel)sk.channel();
38 //读取数据
39 ByteBuffer buf = ByteBuffer.allocate(1024);
40
41 int len = 0;
42 while ((len = sChannel.read(buf)) != -1) {
43 buf.flip();
44 System.out.println(new String(buf.array(), 0, len));
45 buf.clear();
46 }
47 }
48
49 // 取消选择键
50 it.remove();
51 }
52
53
54 }
55 }
客户端
1 @Test
2 public void noBlockClient() throws Exception{
3 // 获取通道
4 SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
5
6 // 切换为非阻塞模式
7 sChannel.configureBlocking(false);
8
9 // 分配指定大小的缓冲区
10 ByteBuffer buf = ByteBuffer.allocate(1024);
11
12 // 发送数据给服务器端
13 buf.put(new Date().toString().toString().getBytes());
14 buf.flip();
15 sChannel.write(buf);
16 buf.clear();
17
18
19 // 关闭通道
20 sChannel.close();
21
22 }
需要注意的一点就是在单元测试中不能够使用Scanner()函数,即使使用了也不会生效。
上面使用的SocketChannel和ServerSocketChannel使用的都是TCP协议,NIO中也可以通过DatagramChannel
实现通信功能,使用的是UDP协议
接收端
1 @Test
2 public void receive() throws Exception {
3 DatagramChannel dc = DatagramChannel.open();
4
5 dc.configureBlocking(false);
6
7 dc.bind(new InetSocketAddress(9898));
8
9 Selector selector = Selector.open();
10
11 dc.register(selector, SelectionKey.OP_READ);
12
13 while (selector.select() > 0) {
14 Iterator<SelectionKey> it = selector.selectedKeys().iterator();
15
16 while (it.hasNext()) {
17 SelectionKey sk = it.next();
18 if (sk.isReadable()) {
19 ByteBuffer buf = ByteBuffer.allocate(1024);
20 dc.receive(buf);
21 buf.flip();
22 System.out.println(new String(buf.array(), 0, buf.limit()));
23 buf.clear();
24 }
25 }
26 it.remove();
27 }
28
29 }
发送端
1 @Test
2 public void send() throws Exception{
3 // 获取通道
4 DatagramChannel dc = DatagramChannel.open();
5 // 切换非阻塞模式
6 dc.configureBlocking(false);
7 // 分配固定大小的缓冲区
8 ByteBuffer buf = ByteBuffer.allocate(1024);
9 // 发送数据给服务端
10 buf.put(new Date().toString().toString().getBytes());
11 buf.flip();
12 dc.send(buf, new InetSocketAddress("127.0.0.1", 9898));
13 buf.clear();
14 // 关闭通道
15 dc.close();
16 }
NIO的选择器
的介绍在此告一段落, 代码中注释都写得很清楚了, 一定要结合注释一起理解哦~