NIO之选择器

阻塞与非阻塞

阻塞/非阻塞关注的是程序在调用结果时的状态

  • 阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
  • 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

我们所学的传统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的选择器的介绍在此告一段落, 代码中注释都写得很清楚了, 一定要结合注释一起理解哦~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值