• 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 还是在集合中。(此段尚未完全确认)

  • 相关阅读:
    1135
    Sort(hdu5884)
    🍖CBV加装饰器与正反向代理简介
    🍖django之Cookie、Session、Token
    🍖Django之forms组件
    🍖forms组件源码分析
    🍖Django与Ajax
    🍖Django框架之模板层
    进程的内存空间相互隔离
    Codeforces Round #583 (Div. 1 + Div. 2, based on Olympiad of Metropolises), problem: (D) Treasure Island
  • 原文地址:https://www.cnblogs.com/gordonkong/p/7434612.html
Copyright © 2020-2023  润新知