• NIO编程


      本文参考《Netty权威指南》

      与Socket类和ServerSocketl类相对应,NIO也提供SocketChannel和ServerSocketChannel两种不同的套接字通道实现。两种新增的通道都支持阻塞和非阻塞两种模式。

      基本概念

      1. 缓冲区Buffer

      Buffer是一个对象,包含一些要写入或者要读出的数据。缓冲区实质上是一个数组,但是一个缓冲区不仅仅是一个数组,它提供了对数据的结构化访问以及维护读写位置(Limit)等信息。

          

      2. 通道Channel

      Channel是一个通道,网络数据通过Channel读取和写入。通道与流不同在于通道是双向的,流只是在一个方向上移动(一个流必须是InputStream或者OutputStream的子类),而通道可以用于读、写或者二者同时进行。

      3. 多路复用器 Selector

      Selector通过不断轮询注册在其上的Channel,如果某个Channel上面发生读或者写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作。基于epoll操作(区别select的三点)。

      NIO服务端序列图

      

      服务端代码:

      

      
     1 public class NIOTimeServer {
     2     public static void main(String[] args){
     3         int port = 8082;
     4         if(args != null && args.length > 0){
     5             try{
     6                 port = Integer.valueOf(args[0]);
     7             } catch (Exception e){
     8                 //采用默认值
     9             }
    10         }
    11         MultiplexerTimeServer timeServer = new MultiplexerTimeServer(port);
    12         new Thread(timeServer,"NIO-MultiplexerTimeServer-001").start();
    13     }
    14 }
    TimeServer Code
      
    public class MultiplexerTimeServer implements Runnable{
        private Selector selector;
        private ServerSocketChannel servChannel;
        private volatile boolean stop;
        private int port;
        /**
         * 初始化多路复用器,绑定监听端口
         */
        public MultiplexerTimeServer(int port){
            this.port = port;
            try {
                //1. 打开ServerSocketChannel,用于监听客户端的连接,他是所有客户端连接的副管道
                servChannel = ServerSocketChannel.open();
                //2. 绑定监听端口,设置连接为非阻塞模式
                servChannel.configureBlocking(false);
                servChannel.socket().bind(new InetSocketAddress(port),1024);
                //3. 创建多路复用器
                selector = Selector.open();
                //4. 将ServerSocketChannel注册到多路复用器Selector上,监听Accept事件
                servChannel.register(selector, SelectionKey.OP_ACCEPT);
                System.out.println("The time server is start in port: " + port);
            }catch (IOException e){
                e.printStackTrace();
                System.exit(1);
            }
        }
        public void setStop(){
            this.stop = true;
        }
    
        public void run(){
            //多路复用器在线程run方法的无限循环体内轮询准备就绪的key
            while(!stop){
                try{
                    /**
                     * Selects a set of keys whose corresponding channels are ready for I/O operations.
                     * <p> This method performs a blocking <a href="#selop">selection
                     * operation</a>.  It returns only after at least one channel is selected,
                     * */
                    selector.select(1000);
                    Set<SelectionKey> selectionKeys = selector.selectedKeys();
                    Iterator<SelectionKey> it = selectionKeys.iterator();
                    SelectionKey selectionKey = null;
                    while(it.hasNext()){
                        selectionKey = it.next();
                        it.remove();
                        try{
                            //处理就绪的任务
                            handleInput(selectionKey);
                        } catch (Exception e){
                            if (selectionKey != null){
                                selectionKey.cancel();
                                if(selectionKey.channel() != null){
                                    selectionKey.channel().close();
                                }
                            }
                        }
                    }
                }catch (Throwable t){
                    t.printStackTrace();
                }
            }
        }
    
        private void handleInput(SelectionKey key) throws IOException{
            //处理新接入的请求消息
            if(key.isValid()){
                if(key.isAcceptable()){
                    //Accept the new connect
                    ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
                    //6. 多路复用器监听到有新的客户端接入,处理新的接入请求,完成TCP三次握手,建立物理链路
                    SocketChannel sc = ssc.accept();
                    //7. 设置客户端链路为非阻塞模式
                    sc.configureBlocking(false);
                    //8. 将新接入的客户端连接注册到多路复用器上,监听读操作,读取客户端发送的网络消息
                    sc.register(selector,SelectionKey.OP_READ);
                }
                if (key.isReadable()){
                    //Read the data
                    SocketChannel sc = (SocketChannel) key.channel();
                    ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                    //9. 异步读取客户端请求消息到缓冲区
                    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 receive order: " + body);
                        String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body)? new java.util.Date(
                                System.currentTimeMillis()).toString():"Bad order";
                        System.out.println("currentTime: " + currentTime);
                        doWrite(sc,currentTime);
                    } else if(readBytes <0){
                        //对端链路关闭
                        key.cancel();
                        sc.close();
                    } else ; //读到0字节,忽略
                }
            }
        }
        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);
            }
        }
    }
    MultiplexerTimeServer

      NIO客户端序列图

      

      客户端代码:

      

      
     1 public class NIOTimeClient {
     2     public static void main(String[] args){
     3         int port = 8082;
     4         if(args != null && args.length >0){
     5             try{
     6                 port = Integer.valueOf(args[0]);
     7             } catch (NumberFormatException e){
     8                 //采用默认值
     9             }
    10         }
    11         new Thread(new NIOTimeClientHandle("192.168.1.107",port),"TimeClient - 001").start();
    12     }
    13 }
    TimeClient
      
      1 public class NIOTimeClientHandle implements Runnable{
      2     private String host;
      3     private int port;
      4     private Selector selector;
      5     private SocketChannel socketChannel;
      6     private volatile boolean stop;
      7 
      8     public NIOTimeClientHandle(String host,int port){
      9         this.host = host;
     10         this.port = port;
     11         try {
     12             //初始化NIO的多路复用器和SocketChannel,
     13             //1. 打开SocketChannel,绑定客户端本地地址(可选默认系统会随机分配一个可用的本地地址)
     14             selector = Selector.open();
     15             socketChannel = SocketChannel.open();
     16             //需要注意:SocketChannel创建之后,需要将其设置为异步非阻塞模式
     17             socketChannel.configureBlocking(false);
     18         }catch (IOException e){
     19             e.printStackTrace();
     20             System.exit(1);
     21         }
     22     }
     23 
     24     public void run(){
     25         //发送连接请求,因为是示例,没有做重连操作
     26         try {
     27             //4 5 步
     28             doConnect();
     29         } catch (IOException e){
     30             e.printStackTrace();
     31             System.exit(1);
     32         }
     33         /**
     34          * 循环体中轮询多路复用器Selector。当有就绪的channel时,执行 handleInput(key)方法
     35          */
     36         while(!stop){
     37             try{
     38                 selector.select(1000);
     39                 Set<SelectionKey> selectionKeys = selector.selectedKeys();
     40                 Iterator<SelectionKey> it = selectionKeys.iterator();
     41                 SelectionKey key = null;
     42                 while(it.hasNext()){
     43                     key = it.next();
     44                     it.remove();
     45                     try{
     46                         handleInput(key);
     47                     } catch (Exception e){
     48                         if(key != null){
     49                             key.cancel();
     50                         }
     51                     }
     52                 }
     53             }catch (Exception e){
     54                 e.printStackTrace();
     55                 System.exit(1);
     56             }
     57         }
     58     }
     59     private void handleInput(SelectionKey key) throws IOException{
     60         /**
     61          public abstract boolean isValid()
     62          Tells whether or not this key is valid.
     63          A key is valid upon creation and remains so until it is cancelled,
     64          its channel is closed, or its selector is closed.
     65          */
     66         if(key.isValid()){
     67             //判断是否连接成功
     68             SocketChannel sc = (SocketChannel) key.channel();
     69             /**
     70              public final boolean isConnectable()
     71              Tests whether this key's channel has either finished, or failed to finish,
     72              its socket-connection operation.
     73              */
     74             /**
     75              * 首先对Selection进行判断,看它处于什么状态,如果是处于连接状态,说明服务器已经返回ACK应答消息。
     76              * 这是我们需要对连接结果进行判断,调用SocketChannel的finishConnect()方法,如果返回值为true,说明
     77              * 客户端连接成功,否则是失败。将SocketChannel注册到多路复用器上,注册SelectionKey.OP_READ操作位,
     78              * 监听网络读操作
     79              */
     80             if(key.isConnectable()){
     81                 /**
     82                  public abstract boolean finishConnect() throws IOException
     83                  Finishes the process of connecting a socket channel.
     84                  */
     85                 if(sc.finishConnect()){
     86                     sc.register(selector,SelectionKey.OP_READ);
     87                     doWrite(sc);
     88                 } else{
     89                     System.exit(1); //连接失败,进程退出
     90                 }
     91             }
     92             //读取消息预先分配1MB的接收缓冲区用于读取应答消息
     93             if(key.isReadable()){
     94                 ByteBuffer readBuffer = ByteBuffer.allocate(1024);
     95                 int readBytes = sc.read(readBuffer);
     96                 if(readBytes > 0){
     97                     readBuffer.flip();
     98                     byte[] bytes = new byte[readBuffer.remaining()];
     99                     readBuffer.get(bytes);
    100                     String body = new String(bytes,"UTF-8");
    101                     System.out.println("Now is: " +  body);
    102                     this.stop = true;
    103                 } else if (readBytes < 0){
    104                     //对端链路关闭
    105                     key.cancel();
    106                     sc.close();
    107                 } else ;
    108             }
    109         }
    110     }
    111     private void doConnect() throws IOException{
    112         //如果直接连接成功,则注册到多路复用器上,发送请求消息,读应答
    113         /**
    114          * 首先对SocketChannel的connect()操作进行判断。如果连接成功,则将SocketChannel注册到
    115          * 多路复用器Selector上,注册SelectionKey.OP_READ;如果没有直接连接成功,则说明
    116          * 服务端没有返回TCP握手应答消息,但这并不代表连接失败。而是注册SelectionKey.OP_CONNECT
    117          * 当服务端返回TCP syn-ack消息后,Selector就能轮询到这个SocketChannel处于连接就绪状态
    118          */
    119         //4. 判断是否连接成功,如果连接成功,则直接注册读状态位到多路复用器中
    120         if(socketChannel.connect(new InetSocketAddress(host,port))){
    121             socketChannel.register(selector, SelectionKey.OP_READ);
    122             doWrite(socketChannel);
    123         } else {
    124             //5, 向多路复用器注册OP_CONNECT状态位,监听服务端的TCP ACK应答
    125             socketChannel.register(selector,SelectionKey.OP_CONNECT);
    126         }
    127     }
    128     private void doWrite(SocketChannel sc) throws IOException{
    129         byte[] req = "QUERY TIME ORDER".getBytes();
    130         ByteBuffer writeBuffer = ByteBuffer.allocate(req.length);
    131         writeBuffer.put(req);
    132         writeBuffer.flip();
    133         sc.write(writeBuffer);
    134         if(!writeBuffer.hasRemaining()){
    135             System.out.println("Send order 2 server succeed.");
    136         }
    137     }
    138 }
    NIOTimeClientHandle

      

      服务端执行结果:

      

      客户端执行结果:

      

      NIO优点总结:

      1. 客户端发起的连接操作是异步的,可以通过在多路复用器注册OP_CONNECT等待后续结果,不需要想之前的客户端那样被同步阻塞。

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

      3.  线程模型的优化,采用epoll实现,没有连接句柄数的限制。适合做高性能、高负载的网络服务器。

  • 相关阅读:
    用 js 的 selection range 操作选择区域内容和图片
    jQuery / zepto ajax 全局默认设置
    transform-origin 的定位
    JS和CSS中引号的使用
    JS里引用CSS属性时候的命名
    nodeName,nodeValue,nodeType,typeof 的区别
    我的前端之路启程了
    This dependency was not found: * components/Header in ./node_modules/babel-loader/lib!./node_modules/vue-loader/lib/selector.js?type=script&index=0!./src/pages/Index.vue报错!
    font-face字体图标
    给多个元素绑定事件
  • 原文地址:https://www.cnblogs.com/tcming/p/6754004.html
Copyright © 2020-2023  润新知