Java网络编程学习A轮_06_NIO入门

参考资料:
老外写的教程,很适合入门:http://tutorials.jenkov.com/java-nio/index.html
上面教程的译文:http://ifeve.com/overview/

示例代码:
https://github.com/gordonklg/study,socket module

A. 摘要

因为有现成的教程,本文只做摘要。

NIO 有三宝,channel、buffer、selector

Channel 与 Stream 很相似,除了:

  • Channel 同时支持读操作与写操作,而 Stream 是单向的
  • Channel 支持异步读写
  • Channel 读写操作与 Buffer 绑定,只能把数据从 Channel 读取出来放到 Buffer 中,或是把 Buffer 中的数据写到 Channel 中

Buffer 本质上是一个内存块,Buffer 包装了这个内存块,提供一系列方法简化在该内存块上的数据读写操作。

Buffer 有三个属性:

  • capacity:容量
  • position:当前操作位置
  • limit:允许到达的界限

其中 capacity 只能在创建时指定,无法修改。其它两个属性都有对应的读取与设值方法。

Buffer 及 Channel 主要方法的手绘示意图如下:
一张图片

Selector 设计目的是使单线程可以处理多个网络连接(多个 Channel)。对于存在大量连接但是每个连接占用带宽都不多的应用,例如聊天工具、滴滴收集车辆位置信息、物联网收集设备信息等,传统 Socket 编程需要为每一个连接分配一个处理线程,占用大量系统资源。我们需要一种方案,可以让一个线程负责多个连接。
一张图片

Selector 允许 Channel 注册到自己身上,SelectionKey 表示 channel 与 selector 的注册关系。

Channel 能产生4种事件,分别是:

  • SelectionKey.OP_CONNECT // channel 已成功连接到服务器
  • SelectionKey.OP_ACCEPT // server channel 已成功接受一个连接
  • SelectionKey.OP_READ // channel 中有可读数据
  • SelectionKey.OP_WRITE // channel 可以发送数据

可以设置 Selector 关注 Channel 的哪些事件。Selector 的 select() 方法会阻塞,直到注册的 Channel 产生了指定类型的事件(实际意义就是 Channel 已经准备好做某事了)。接着就可以通过 Selector 获取所有已经准备好的 SelectionKey(即Channel),依次处理相应事件,例如建立连接、获取数据、业务处理、发送数据等。

显然,同一个 selector 的所有 channel 对数据的读写以及业务逻辑的实现,在默认情况下,都是在同一个线程中的。需要注意业务逻辑是否会过度占用当前线程资源,导致整个 Selector 效率低下。可以引入工作线程池解决以上问题。

SelectionKey 对象包含以下属性:

  • The interest set,Selector 感兴趣的 Channel 事件类型
  • The ready set,Channel 已经准备好的事件。显然,被 Selector.select() 方法选中的 SelectionKey,其 ready set 应该与 interest set 有交集
  • The Channel,通过 SelectionKey 可以获取 Channel 对象
  • The Selector,通过 SelectionKey 可以获取 Selector 对象
  • An attached object (optional)

Selector 用法示意:

    Selector selector = Selector.open(); // 获取一个 Selector 实例
    channel.configureBlocking(false);  // 只有非阻塞模式的 channel 才能使用 Selector
    SelectionKey key = channel.register(selector, SelectionKey.OP_READ); // 将 channel 注册到 Selector 上,同时指定 Selector 只关注 channel 的 READ 事件
    while(true) {
        int readyChannels = selector.select(); // Selector 的 select 方法会阻塞,直到有已经准备好的(有数据可读的) channel,或是 Selector 被 wakeup,或是线程被中断
        if(readyChannels == 0) continue;
        Set<SelectionKey> selectedKeys = selector.selectedKeys();
        Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
        while(keyIterator.hasNext()) {
            SelectionKey key = keyIterator.next();
            if(key.isAcceptable()) {
                    // a connection was accepted by a ServerSocketChannel.
            } else if (key.isConnectable()) {
                    // a connection was established with a remote server.
            } else if (key.isReadable()) {
                    // a channel is ready for reading
            } else if (key.isWritable()) {
                    // a channel is ready for writing
            }
            keyIterator.remove();
        }
    }

B. 示例代码

gordon.study.socket.nio.basic.SimpleFileChannel.java

public class SimpleFileChannel {

    public static void main(String[] args) throws Exception {
        String path = SimpleFileChannel.class.getResource("/file1").getPath();
        RandomAccessFile aFile = new RandomAccessFile(path, "rw");
        FileChannel inChannel = aFile.getChannel();
        ByteBuffer buf = ByteBuffer.allocate(48);
        int bytesRead = inChannel.read(buf);
        while (bytesRead != -1) {
            System.out.print("(Read " + bytesRead + ")");
            buf.flip();
            while (buf.hasRemaining()) {
                System.out.print((char) buf.get());
            }
            buf.clear();
            bytesRead = inChannel.read(buf);
            System.out.println();
        }
        aFile.close();
    }
}

以上示例代码演示了最基本的 Channel 与 Buffer API。

gordon.study.socket.nio.basic.SimpleSelector.java

public class SimpleSelector {

    public static void main(String[] args) throws Exception {
        new Thread(new Runnable() {

            @Override
            public void run() {
                try {
                    Selector selector = Selector.open();
                    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
                    serverSocketChannel.bind(new InetSocketAddress(8888));
                    serverSocketChannel.configureBlocking(false);
                    System.out.println("##valid ops for server socket channel: " + serverSocketChannel.validOps());
                    SelectionKey sk = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
                    System.out.println("##Selection key ready ops before Selector.select(): " + sk.readyOps());

                    while (true) {
                        int readyChannels = selector.select();
                        System.out.println("readyChannels by Selector.select(): " + readyChannels);
                        if (readyChannels == 0) {
                            continue;
                        }
                        Set<SelectionKey> selectedKeys = selector.selectedKeys();
                        System.out.println("selected keys by Selector.select(): " + selectedKeys.size());
                        Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
                        while (keyIterator.hasNext()) {
                            SelectionKey key = keyIterator.next();
                            System.out.println("##Selection key ready ops after Selector.select(): " + key.readyOps());
                            SocketChannel channel = serverSocketChannel.accept();
                            if (channel != null) {
                                // create a new thread to handle this client
                            }
                            keyIterator.remove();
                        }
                        Thread.sleep(1000);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();

        for (int i = 0; i < 3; i++) {
            Thread.sleep(400);
            new Thread(new Client()).start();
        }
    }

    private static class Client implements Runnable {

        @Override
        public void run() {
            try (Socket socket = new Socket()) {
                socket.connect(new InetSocketAddress(8888));
                System.out.println("    Connected to server!");
                while (true) {
                    Thread.sleep(1000);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

以上示例代码使用 Selector 处理服务端连接建立过程。

代码第14行将 ServerSocketChannel 注册到 Selector 上,同时表明关注 ServerSocketChannel 的 Accept 事件(ServerSocketChannel 只支持这一种事件),显然,这时候 ServerSocketChannel 尚未准备好 Accept 事件,所以第15行代码打印出的 ready ops 为 0。

片刻后(400ms),第一个客户端成功连接到服务端,此时 ServerSocketChannel 产生 Accept 事件,Selector.select() 方法返回,由于 Selector 只注册了一个 Channel,返回值显然是1。然后遍历被选中的 SelectionKey 列表,创建 SocketChannel 处理本次连接。

代码第35行通过 sleep 的方法模拟复杂环境下创建 SocketChannel 耗时较长的情况。这产生了一个有趣的现象:客户端很早就完成了连接(socket.isConnected() == true),但是服务端要等待 sleep 时间耗尽后才能建立一个 SocketChannel,也就是说,虽然服务端还没有通过 ServerSocketChannel.accept() 方法创建出一个 SocketChannel,但是实际上 TCP 连接已经建立完成??(不甚理解)

大概推测,ServerSocketChannel 内部有地方保存已建立好的 TCP 连接(操作系统层面的已建立),accept() 方法被调用时,会将一个底层 TCP 连接包装为 SocketChannel。推断的理由一是客户端 socket 状态是已连接(也就是三次握手已经完成),另一点是,如果注释掉代码第29行的 accept() 方法调用,会发现 Selector.select() 方法在第一个客户端连接过来后,几乎就不会被阻塞了(注掉第35行的 sleep 更加明显),也就是说,ServerSocketChannel 的 Accept 事件是按照有没有待处理的客户端连接来确定的。

代码执行输出如下:

##valid ops for server socket channel: 16
##Selection key ready ops before Selector.select(): 0
    Connected to server!
readyChannels by Selector.select(): 1
selected keys by Selector.select(): 1
##Selection key ready ops after Selector.select(): 16
    Connected to server!
    Connected to server!
readyChannels by Selector.select(): 1
selected keys by Selector.select(): 1
##Selection key ready ops after Selector.select(): 16
readyChannels by Selector.select(): 1
selected keys by Selector.select(): 1
##Selection key ready ops after Selector.select(): 16

观察输出,显然,每个 ServerSocketChannel 在一次 Selector.select 大轮询中,只建立了一个 Socket 连接,哪怕实际上当时有多个连接可以建立。如果我们把建立连接的 ServerSocketChannel 与处理数据读写的 SocketChannel 注册到同一个 Selector 上,可能导致连接请求来不及处理。
如果将代码第29行优化为以下逻辑:

                            SocketChannel channel = serverSocketChannel.accept();
                            while (channel != null) {
                                channel = serverSocketChannel.accept();
                            }

这样改后,如果短时间有大量连接,会导致业务处理收到冲击,可能长时间得不到响应(线程资源都花在建立连接上了)。所以,更合理的方法是将负责建立连接的 ServerSocketChannel 与处理数据读写的 SocketChannel 注册到不同的 Selector 上。

最后一个细节是代码第33行的移除 selection key。Java NIO 的 Selector 会将已准备好并且用户关注的 SelectionKey 加入 selectedKeys 集合,但是不会主动删除。因此,当我们确定本次事件已经处理完毕时,要主动移除掉该 selection key,否则下次获取 selectedKeys 集合时,该 selection key 还是在集合中。(此段尚未完全确认)

posted @ 2017-08-26 09:55  首夜盲毒预言家  阅读(195)  评论(0编辑  收藏  举报