• NIO 学习笔记


    服务端:

    package com.zhaowb.netty.nio;
    
    public class TimeServer {
        public static void main(String[] args) {
            int port = 8080;
    
            if (args != null && args.length > 0) {
                try {
                    port = Integer.valueOf(args[0]);
                } catch (NumberFormatException e) {
    
                }
            }
    
            MultiplexerTimeServer timeServer = new MultiplexerTimeServer(port);
            new Thread(timeServer,"NIO-MultiplexerTimeServer-001").start();
        }
    }
    package com.zhaowb.netty.nio;
    
    import java.io.IOException;
    import java.net.InetSocketAddress;
    import java.nio.ByteBuffer;
    import java.nio.channels.SelectionKey;
    import java.nio.channels.Selector;
    import java.nio.channels.ServerSocketChannel;
    import java.nio.channels.SocketChannel;
    import java.util.Date;
    import java.util.Iterator;
    import java.util.Set;
    
    
    
    
    public class MultiplexerTimeServer implements Runnable {
    
        private Selector selector;
    
        private ServerSocketChannel servChannel;
    
        private volatile boolean stop;
    
        /**
         * 初始化多路复用器,绑定监听端口
         *
         * @param port
         */
        public MultiplexerTimeServer(int port) {
            try {
                selector = Selector.open();
                servChannel = ServerSocketChannel.open();
                servChannel.configureBlocking(false); // 设置为一部非阻塞模式
                servChannel.socket().bind(new InetSocketAddress(port), 1024);
                servChannel.register(selector, SelectionKey.OP_ACCEPT); // 系统资源初始化成功后,将 ServerSocketChannel 注册到 Selector ,监听 SelectionKey.OP_ACCEPT 操作位,如果资源初始化失败,则退出
                System.out.println("The time server is start in : " + port);
            } catch (IOException e) {
                e.printStackTrace();
                System.exit(1);
            }
        }
    
        public void stop() {
            this.stop = true;
        }
    
        /**
         *  selector 休眠时间为 1s 无论是否有读写等事件发生, selector 每隔1s都别唤醒一次,selector 也提供了一个无参的 select方法。当有处于就绪状态的 Channel时,
         *
         *  selector 将返回就绪状态的 Channel 的 SelectionKey 集合,通过对就绪状态的 Channel 集合进行迭代,可以进行网络的异步读写操作
         *  @see  Runnable#run()
         */
        @Override
        public void run() {
            while (!stop) {
                try {
                    selector.select(1000);
                    Set<SelectionKey> selectionKeys = selector.selectedKeys();
                    Iterator<SelectionKey> it = selectionKeys.iterator();
                    SelectionKey key = null;
                    while (it.hasNext()) {
                        key = it.next();
                        it.remove();
                        try {
                            handleInput(key);
                        } catch (Exception e) {
                            if (key != null) {
                                key.cancel();
                                if (key.channel() != null)
                                    key.channel().close();
                            }
                        }
                    }
                } catch (Throwable t) {
                    t.printStackTrace();
                }
            }
    
            // 多路复用器关闭后,所有注册在上面的 Channel 和 Pipe 等资源都会被自动去注册并关闭。所以不需要重复释放资源
            if (selector != null){
                try {
                    selector.close();
                } catch (IOException e){
                    e.printStackTrace();
                }
            }
        }
        private void handleInput(SelectionKey key) throws IOException{
    
            if(key.isValid()){
                // 处理新接入的请求消息
                /**
                 * 处理新接入的客户端请求消息,根据 SelectionKey 的操作位进行判断即可获知网络事件的类型,
                 * 通过 ServerSocketChannel 的 accept 接受客户端的连接请求并创建 SocketChannel 实例 。
                 */
                if (key.isAcceptable()){
                    // Accept the new connection
                    ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
                    SocketChannel sc = ssc.accept();
                    sc.configureBlocking(false);
                    // Add the new Connection to the selector
                    sc.register(selector,SelectionKey.OP_READ);
                }
                /**
                 *  用于读取客户端的请求消息,首先创建一个 ByteBuffer ,暂定为1 K的缓冲区。够我玩的了
                 *  然后调用 SocketChannel 的 read 方法读取请求码流。
                 *  注意 : 已经将 SocketChannel 设置为异步非阻塞模式,因此它的 read 是非阻塞的,使用返回值进行判断,
                 *  看读取到的字节数,返回值有三种可能的结果。
                 *      1)、返回值大于0:读取到字节,对字节进行编解码;
                 *      2)、返回值等于0:没有读取到字节,属于正常场景,忽略;
                 *      3)、返回值等于-1:链路已经关闭,需要关闭 SocketChannel ,释放资源
                 *   当读取到资源以后,进行解码,首先对 readBuffer 进行 flip 操作, 作用是将缓冲区当前的 limit 设置为 position
                 *   position 设置为 0,用于后续对缓冲区的读取操作。然后根据缓冲区的可读的字节个数创建字节数组,调用 ByteBuffer
                 *   的 get 操作将缓冲区可读的 字节数复制到新创建的字节数组中,最后调用字符串中的构造函数创建请求消息体并打印。
                 *
                 */
                if(key.isReadable()){
                    // Read the data
                    SocketChannel sc = (SocketChannel) key.channel();
                    ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                    int readBytes = sc.read(readBuffer);
                    if (readBytes > 0){
                        readBuffer.flip();
                        byte[] bytes = new byte[readBuffer.remaining()];
                        readBuffer.get(bytes);
                        String body = new String( bytes,"UTF-8");
                        System.out.println("The time server received order : "
                                + body);
                        String currentTime = "QUERY TIME ORDER"
                                .equalsIgnoreCase(body) ? new Date(
                                        System.currentTimeMillis()).toString()
                                : "BAD ORDER";
                        doWrite(sc,currentTime);
                    } else if (readBytes < 0) {
                        // 对端链路关闭
                        key.cancel();
                        sc.close();
                    } else {
                        ; // 读到 0 字节 ,忽略
                    }
                }
            }
        }
    
        /**
         * 将应答数据异步发送给给客户端。
         *      1)、将字符串编码成字节数组,根据字节数据的容量创建 ByteBuffer ,调用 ByteBuffer 的 put 操作将字节数组复制到
         *      缓冲区中;
         *      2)、对缓冲区进行 flip 操作,;
         *      3)、调用 SocketChannel 的 write 方法,将缓冲区的字节数组发送出去。
         *  注意:
         *      由于 SocketChannel 是异步非阻塞的,它并不保证一次能把需要发送的字节数组发送完,此时会出现 “写半包”问题
         *      需要注册写操作,不断轮训 Selector 将没有发送完的 ByteBuffer 发送完毕,可以通过 ByteBuffer 的 hasRemaining
         *      方法判断消息是否发送完成,暂不考虑。
         * @param channel
         * @param response
         * @throws IOException
         */
        private  void doWrite(SocketChannel channel ,String response) throws  IOException{
            if (response != null && response.trim().length() > 0){
                byte[] bytes = response.getBytes();
                ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
                writeBuffer.put(bytes);
                writeBuffer.flip();
                channel.write(writeBuffer);
            }
        }
    }

    客户端:

    package com.zhaowb.netty.nio;
    
    public class TimeClient {
        public static void main(String[] args) {
    
            int port = 8080;
    
            if (args != null && args.length > 0) {
                try {
                    port = Integer.valueOf(args[0]);
                } catch (NumberFormatException e) {
    
                }
            }
            new Thread(new TimeClientHandle("127.0.0.1",port)).start();
        }
    }
    package com.zhaowb.netty.nio;
    
    import java.io.IOException;
    import java.net.InetSocketAddress;
    import java.nio.ByteBuffer;
    import java.nio.channels.SelectionKey;
    import java.nio.channels.Selector;
    import java.nio.channels.SocketChannel;
    import java.util.Iterator;
    import java.util.Set;
    
    
    public class TimeClientHandle implements Runnable {
        private String host;
        private int port;
        private Selector selector;
        private SocketChannel socketChannel;
        private volatile boolean stop;
    
        /**
         * 初始化 NIO 的多路复用器和 SocketChannel 对象,并将其设置为异步非阻塞模式
         *
         * @param host
         * @param port
         */
        public TimeClientHandle(String host, int port) {
            this.host = host == null ? "127.0.0.1" : host;
            this.port = port;
            try {
                selector = Selector.open();
                socketChannel = SocketChannel.open();
                socketChannel.configureBlocking(false);
            } catch (IOException e) {
                e.printStackTrace();
                System.exit(1);
            }
        }
    
        /**
         * @see Runnable#run()
         */
        @Override
        public void run() {
    
            /**
             * 用于发送连接请求,理想连接成功,暂不考虑连接异常,需要重连的情况
             */
            try {
                doConnect();
            } catch (IOException e) {
                e.printStackTrace();
                System.exit(1);
            }
            /**
             * 在循环体中轮询多路复用器 Selector ,当有就绪的 Channel 时,执行handleInput(key)方法
             */
            while (!stop) {
                try {
                    selector.select(1000);
                    Set<SelectionKey> selectionKeys = selector.selectedKeys();
                    Iterator<SelectionKey> it = selectionKeys.iterator();
                    SelectionKey key = null;
                    while (it.hasNext()) {
                        key = it.next();
                        it.remove();
                        try {
                            handleInput(key);
                        } catch (Exception e) {
                            if (key != null) {
                                key.cancel();
                                if (key.channel() != null) {
                                    key.channel().close();
                                }
                            }
                        }
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                    System.exit(1);
                }
            }
    
            // 多路复用器关闭后,所有注册在上面的 Channel 和 Pipe 等资源都会被自动去注册并关闭。所以不需要重复释放资源
            if (selector != null) {
                try {
                    selector.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    
        /**
         *  首先判断 SelectionKey 处于什么状态,如果是处于连接状态,说明服务端已经返回 ACK 应答消息。此时对连接结果进行判断,
         *      调用 SocketChannel 的 finishConnect()方法,如果返回 true ,说明客户端连接成功;如果返回 false 或者直接抛出
         *      IOException ,说明连接失败。
         *   连接成功: 将 SocketChannel 注册到多路复用器上,注册 SelectionKey.OP_READ 操作位,监听网络读操作,然后发送请求
         *      消息给服务端。
         * @param key
         * @throws IOException
         */
        private void handleInput(SelectionKey key) throws IOException {
            if (key.isValid()) {
                // 判断是否连接成功
                SocketChannel sc = (SocketChannel) key.channel();
                if (key.isConnectable()) {
                    if (sc.finishConnect()) {
                        sc.register(selector, SelectionKey.OP_READ);
                        doWrite(sc);
                    } else {
                        System.exit(1); // 连接失败,进程结束
                    }
    
                }
                /**
                 * 如果客户端收到服务端的应答消息,则 SocketChannel 是可读的 ,分配 1k 的缓冲区用于读取应答消息,调用
                 *      SocketChannel 的 read() 方法进行异步读取操作。由于是异步操作,需要对读取结果进行判断,
                 *      看读取到的字节数,返回值有三种可能的结果。
                 *                   1)、返回值大于0:读取到字节,对字节进行编解码;
                 *                   2)、返回值等于0:没有读取到字节,属于正常场景,忽略;
                 *                   3)、返回值等于-1:链路已经关闭,需要关闭 SocketChannel ,释放资源
                 *   当读取到资源以后,进行解码,首先对 readBuffer 进行 flip 操作, 作用是将缓冲区当前的 limit 设置为 position
                 *   position 设置为 0,用于后续对缓冲区的读取操作。然后根据缓冲区的可读的字节个数创建字节数组,调用 ByteBuffer
                 *   的 get 操作将缓冲区可读的 字节数复制到新创建的字节数组中,最后调用字符串中的构造函数创建请求消息体并打印。
                 *   执行完成后将 stop 置为 true ,线程退出循环
                 */
                if (key.isReadable()) {
                    ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                    int readBytes = sc.read(readBuffer);
                    if (readBytes > 0) {
                        readBuffer.flip();
                        byte[] bytes = new byte[readBuffer.remaining()];
                        readBuffer.get(bytes);
                        String body = new String(bytes, "UTF-8");
                        System.out.println(" Now is : " + body);
                        this.stop = true;
                    } else if (readBytes < 0) {
                        // 对端链路关闭
                        key.cancel();
                        sc.close();
                    } else {
                        ;
                    }
                }
            }
        }
    
        /**
         * 先判断 SocketChannel 的 connect() 操作,如果连接成功,则将 SocketChannel 注册到多路复用器 Selector 上,
         *   注册 SelectionKey.OP_READ ,如果没有直接连接成功,则说明服务端没有返回 TCP 握手应答消息,但这不代表连接失败,
         *   需要将 SocketChannel 注册到多路复用器 Selector 上,注册 SelectionKey.OP_CONNECT ,当服务端返回 TCP syn-ack 消息后,
         *   Selector 就能轮询到这个 SocketChannel 处于连接就绪状态。
         *
         * @throws IOException
         */
        private void doConnect() throws IOException {
            if (socketChannel.connect(new InetSocketAddress(host, port))) {
                socketChannel.register(selector, SelectionKey.OP_READ);
                doWrite(socketChannel);
            } else {
                socketChannel.register(selector, SelectionKey.OP_CONNECT);
            }
        }
    
        /**
         * 构造请求消息体,然后编码,写入到发送缓冲区中,最后调用 SocketChannel 的 write 方法进行发送。由于发送是异步的,
         * 所以会存在“半包写”问题,最后通过 hasRemaining() 方法对发送结果进行判断,如果缓冲区中的消息全部发送完成,打印
         * " Send order 2 server succeed.".
         * @param sc
         * @throws IOException
         */
        private void doWrite(SocketChannel sc) throws IOException {
    
            byte[] req = "QUERY TIME ORDER".getBytes();
            ByteBuffer writeBuffer = ByteBuffer.allocate(req.length);
            writeBuffer.put(req);
            writeBuffer.flip();
            sc.write(writeBuffer);
            if (!writeBuffer.hasRemaining()) {
                System.out.println(" Send order 2 server succeed.");
            }
        }
    }

    一个简单的小例子,感觉比BIO写起来麻烦多了。有点也很多,1. 客户端发起的连接请求是异步的,可以通过在多路复用器注册到  OP_CONNECT 等待后续结果,不要像 BIO 一样一直被同步阻塞。

    2.  SocketChannel 的读写异步,没有可读写的数据不会同步等待,直接返回,I/O可以线程就可以处理其他的链路,不需要同步等待这个链路可用。

    3. 一个 Selector 线程可以同时处理多个客户端,性能不会随着客户端的增加而降低,适合做高性能,高负载的网络服务器。

    简单记录一下,方便以后翻阅查看。

  • 相关阅读:
    PostMan测试WebService接口
    org.apache.ibatis.binding.BindingException: Invalid bound statement (not found):
    百度编辑器固定高度后图片框定位不准
    h5样式
    echarts-liquidfill 水球显示小数点
    工具
    linux使用windows磁盘,挂载共享目录
    微信订阅号关注问题
    linux 文件传输 SCP
    mysql 字符串截取
  • 原文地址:https://www.cnblogs.com/zwb1234/p/9262118.html
Copyright © 2020-2023  润新知