• netty权威指南学习笔记一——NIO入门(3)NIO


      经过前面的铺垫,在这一节我们进入NIO编程,NIO弥补了原来同步阻塞IO的不足,他提供了高速的、面向块的I/O,NIO中加入的Buffer缓冲区,体现了与原I/O的一个重要区别。在面向流的I/O中,可以将数据直接写入或者将数据直接读到Stream对象中。下面看一些概念

      Buffer缓冲区

      而在NIO中,所有数据都是用缓冲区处理的,在读取数据时,直接读到缓冲区,在写数据时,也是写到缓冲区,任何时候访问NIO中的数据,都是通过缓冲区进行操作。最常用的是个ByteBuffer缓冲区,他提供了一组功能用于操作字节数组。

      Channel通道

      Channel 是一个通道,网络数据通过它进行读写,通道和流的区别在于通道是双向的,流是单向的,一个流必须是读或者写,而通道则可以读写同时进行。Channel可以分为用于网路读写的SelectableChannel和用于文件操作的FileChannel。

      Selector多路复用器

      首先强调一点,多路复用器对于NIO编程非常重要,非常重要,多路复用器提供选择已经就绪的任务的能力。简单的讲,Selector会不断的轮询注册在其上的Channel,如果某个Channel上面发生读或者写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续I/O操作。一个多路复用器可以轮询多个Channel,意味着只需要一个线程负责selector轮询,就可以接入成千上万个客户端。

    现在改造上一节的代码使其成为NIO server端

     1 package com.example.biodemo;
     2 
     3 
     4 import java.io.*;
     5 import java.net.ServerSocket;
     6 import java.net.Socket;
     7 
     8 public class TimeServer {
     9     public static void main(String[] args) throws IOException {
    10         int port = 8090;
    11         if (args != null && args.length > 0) {
    12             try {
    13                 port = Integer.valueOf(args[0]);
    14             } catch (NumberFormatException e) {
    15                 port = 8090;
    16             }
    17         }
    18 //        创建多路复用线程类并初始化多路复用器,绑定端口等以及轮询注册功能
    19         MultiplexerTimeServer timeServer = new MultiplexerTimeServer(port);
    20 //        启动多路复用类线程负责轮询多路复用器Selector,IO数据处理等操作
    21         new Thread(timeServer, "NIO-MultiplexerTimeSever-001").start();
    22 
    23 
    24 
    25 
    26 
    27 
    28 // ===================以下内容注释掉=================================
    29 
    30        /* ServerSocket server = null;
    31         try {
    32             server = new ServerSocket(port);
    33             System.out.println("the timeServer is start in port :" + port);
    34             Socket socket = null;
    35 //           引入线程池start
    36             TimeServerHandlerExecutePool singleExecutor = new TimeServerHandlerExecutePool(50,10000);
    37             while (true) {
    38                 socket = server.accept();
    39 //           替换BIO中new Thread(new TimeServerHandler(socket)).start();为下一行代码
    40                 singleExecutor.execute(new TimeServerHandler(socket));
    41 //           引入线程池end
    42 
    43             }
    44         } finally {
    45             if (server != null) {
    46                 System.out.println("the time server close");
    47                 server.close();
    48                 server = null;
    49             }
    50         }*/
    51 
    52     }
    53 }

    多路复用类代码,其注册轮询功能及请求消息处理返回在这里进行

      1 package com.example.biodemo;
      2 
      3 import java.io.IOException;
      4 import java.net.InetSocketAddress;
      5 import java.nio.ByteBuffer;
      6 import java.nio.channels.SelectionKey;
      7 import java.nio.channels.Selector;
      8 import java.nio.channels.ServerSocketChannel;
      9 import java.nio.channels.SocketChannel;
     10 import java.util.Date;
     11 import java.util.Iterator;
     12 import java.util.Set;
     13 
     14 public class MultiplexerTimeServer implements Runnable {
     15     //    定义多路复用器
     16     private Selector selector;
     17     //    定义ServerSocketChannel
     18     private ServerSocketChannel servChannel;
     19     //    定义停止标识
     20     private boolean stop;
     21 
     22     //    构造函数初始化多路复用器、并绑定监听端口
     23     public MultiplexerTimeServer(int port) {
     24         try {
     25 //            打开一个多路复用器
     26             selector = Selector.open();
     27 //            打开ServerSocketChannel通道
     28             servChannel = ServerSocketChannel.open();
     29 //            绑定监听端口号,并设置backlog为1024
     30             servChannel.socket().bind(new InetSocketAddress(port), 1024);
     31 //            设置severSocketChannel为异步非阻塞模式
     32             servChannel.configureBlocking(false);
     33 //            将ServerSocketChannel 注册到Reactor 线程的多路复用器Selector上,监听ACCEPT事件,并返回一个SelectionKey类型的值
     34             SelectionKey selectionKey = servChannel.register(selector, SelectionKey.OP_ACCEPT);
     35             System.out.println("The time server is start in port:" + port);
     36         } catch (IOException ioe) {
     37             ioe.printStackTrace();
     38 //            资源初始化失败则退出
     39             System.exit(1);
     40         }
     41     }
     42 
     43     public void stop() {
     44         this.stop = true;
     45     }
     46 
     47     @Override
     48     public void run() {
     49 //        在线程中遍历轮询多路复用器selector
     50         while (!stop) {
     51             try {
     52              /*selector.select();选择一些I/O操作已经准备好的channel。每个channel对应着一个key。这个方法是一个阻塞的选择操作。
     53               当至少有一个通道被选择时才返回。当这个方法被执行时,当前线程是允许被中断的。*/
     54 //            该方法是阻塞的,选择一组键,其相应的通道已为 I/O 操作准备就绪。最多等1s,如果还没有就绪的就返回0
     55             /*如果 timeout为正,则select(long timeout)在等待有通道被选择时至多会阻塞timeout毫秒
     56               如果timeout为零,则永远阻塞直到有至少一个通道准备就绪。
     57               timeout不能为负数*/
     58                 selector.select(1000);
     59 //            当有处于就绪状态的channel时,返回该channel的selectionKey集合,此通道是已准备就绪的键集,已选择键集(I/O操作已就绪返回key)始终是键集的一个子集。
     60                 Set<SelectionKey> selectionKeys = selector.selectedKeys();
     61                 Iterator<SelectionKey> it = selectionKeys.iterator();
     62 //            Selector如果发现channel有OP_ACCEPT或READ事件发生,下列遍历就会进行。
     63                 SelectionKey key = null;
     64                 while (it.hasNext()) {
     65                     // 来一个事件 第一次触发一个accepter线程,SocketReadHandler
     66                     key = it.next();
     67 //                    从iterator中移除该元素
     68                     it.remove();
     69                     try {
     70 //                      去对该已经准备就绪的I/O操作进行处理
     71                         dispatch(key);
     72                     } catch (Exception e) {
     73 //                        删除处理完不为空的键,关闭相关通道
     74                         if (key != null) {
     75                             key.cancel();
     76                             if (key.channel() != null) {
     77                                 key.channel().close();
     78                             }
     79                         }
     80                     }
     81                 }
     82             } catch (IOException ioe1) {
     83                 ioe1.printStackTrace();
     84             }
     85         }
     86 //        最后关闭多路复用器
     87         if (selector != null) {
     88             try {
     89                 selector.close();
     90             } catch (IOException e) {
     91                 e.printStackTrace();
     92             }
     93         }
     94     }
     95 //该方法用于处理轮询到的I/O已经就绪的SelectionKey键
     96     private void dispatch(SelectionKey key) throws IOException {
     97         if (key.isValid()) {
     98 //            处理请求接入的信息
     99             if (key.isAcceptable()) {
    100 //                接受新连接,通过SelectionKey获取其通道
    101                 ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
    102 //                接收客户端连接请求并创建SocketChannel实例
    103                 SocketChannel sc = ssc.accept();
    104                 sc.configureBlocking(false);
    105 //                添加新连接到多路复用器上
    106                 sc.register(selector, SelectionKey.OP_READ);
    107             }
    108             if (key.isReadable()) {
    109 //                本方法体读取客户端发来的请求数据
    110                 SocketChannel sc = (SocketChannel) key.channel();
    111 //                开辟一个缓冲区,这里开辟了1M
    112                 ByteBuffer readBuffer = ByteBuffer.allocate(1024);
    113 //                读取请求码流,返回值>0,读到字节数,返回值=0,没有读到字节数,返回值<0,说明链路已经关闭,
    114 //                需要关闭SocketChannel
    115                 int readBytes = sc.read(readBuffer);
    116                 if (readBytes > 0) {
    117 //                    读到字节数后的解码操作,对 readBuffer 进行 flip 操作, 作用是将缓冲区当前的 limit 设置为 position
    118 //                    position 设置为 0,用于后续对缓冲区的读取操作。
    119                     readBuffer.flip();
    120 //                    根据缓冲区的可读的字节个数创建字节数组
    121                     byte[] bytes = new byte[readBuffer.remaining()];
    122 //                   调用 ByteBuffer的 get 操作将缓冲区可读的字节数复制到新创建的字节数组中
    123                     readBuffer.get(bytes);
    124 //                    调用字符串中的构造函数创建请求消息体并打印。
    125                     String body = new String(bytes, "utf-8");
    126                     System.out.println("The time server receive order :" + body);
    127                     String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new Date(System.currentTimeMillis()).toString() : "BAD ORDER";
    128 //                    将应答消息异步发送给客户端
    129                     doWrite(sc, currentTime);
    130                 } else if (readBytes < 0) {
    131 //                    对端链路关闭
    132                     key.cancel();
    133                     sc.close();
    134                 } else {
    135                     ;//读到0字节,忽略。
    136                 }
    137             }
    138         }
    139     }
    140 
    141     private void doWrite(SocketChannel channel, String response) throws IOException {
    142         if (response != null && response.trim().length() > 0) {
    143 //            将字符创编码为字节数组
    144             byte[] bytes = response.getBytes();
    145 //            根据字节数组大小创建缓冲区
    146             ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
    147 //            复制字节数组内容进入缓冲区
    148             writeBuffer.put(bytes);
    149 //            进行flip操作,作用同上面
    150             writeBuffer.flip();
    151 //            将缓冲区中的字节数组发送出去
    152             channel.write(writeBuffer);
    153         }
    154     }
    155 }

     下面是客户端的详细代码及注释,需要注意的是在客户端代码判断是否连接成功时需要进行两步判断(需要判断连接状态和连接结果是否成功),第二步判断成功需要注册状态,否则客户端发送不了消息。

     1 package com.example.biodemo;
     2 
     3 import java.io.*;
     4 import java.net.Socket;
     5 
     6 public class TimeClient {
     7     public static void main(String[] args) {
     8         int port = 8090;
     9         if (args != null && args.length > 0) {
    10             try {
    11                 port = Integer.valueOf(args[0]);
    12             } catch (NumberFormatException ne) {
    13                 port = 8090;
    14             }
    15         }
    16         new Thread(new TimeClientHandles("127.0.0.1",port),"TimeClient-001").start();
    17       /* 代码改造注释掉以下代码
    18        Socket socket = null;
    19         BufferedReader in = null;
    20         PrintWriter out = null;
    21         try {
    22             socket = new Socket("127.0.0.1", port);
    23             System.out.println(socket.getInputStream());
    24             in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
    25             out = new PrintWriter(socket.getOutputStream(), true);
    26             out.println("QUERY TIME ORDER");
    27             System.out.println("send order 2 server succeed.");
    28             String resp = in.readLine();
    29             System.out.println("now is :" + resp);
    30         } catch (IOException e1) {
    31 
    32         } finally {
    33             if (out != null) {
    34                 out.close();
    35                 out = null;
    36             }
    37 
    38             if (in != null) {
    39                 try {
    40                     in.close();
    41                 } catch (IOException e2) {
    42                     e2.printStackTrace();
    43                 }
    44                 in = null;
    45                 if (socket != null) {
    46                     try {
    47                         socket.close();
    48                     } catch (IOException e3) {
    49                         e3.printStackTrace();
    50                     }
    51 
    52                 }
    53                 socket = null;
    54             }
    55         }*/
    56     }
    57 }

    客户端HandleInput代码

      1 package com.example.biodemo;
      2 
      3 import java.io.IOException;
      4 import java.net.InetSocketAddress;
      5 import java.nio.ByteBuffer;
      6 import java.nio.channels.ClosedChannelException;
      7 import java.nio.channels.SelectionKey;
      8 import java.nio.channels.Selector;
      9 import java.nio.channels.SocketChannel;
     10 import java.util.Iterator;
     11 import java.util.Set;
     12 
     13 //该类用来处理异步连接和读写操作
     14 public class TimeClientHandles implements Runnable {
     15     private String host;
     16     private int port;
     17     private Selector selector;
     18     private SocketChannel socketChannel;
     19     private volatile boolean stop;
     20 
     21     //    构造函数初始化并连接服务器
     22     public TimeClientHandles(String host, int port) {
     23         this.host = host == null ? "127.0.0.1" : host;
     24         this.port = port;
     25         try {
     26             selector = Selector.open();
     27             socketChannel = SocketChannel.open();
     28             socketChannel.configureBlocking(false);
     29         } catch (IOException e) {
     30             e.printStackTrace();
     31             System.exit(1);
     32         }
     33     }
     34 
     35     @Override
     36     public void run() {
     37 //        发送连接请求
     38         try {
     39             doConnection();
     40         }catch (Exception e){
     41             e.printStackTrace();
     42             System.exit(1);
     43         }
     44 //      在循环体内轮询多路复用器Selector,当有就绪的Channel时,执行handleInput(key);
     45         while (!stop) {
     46             try {
     47                 selector.select(1000);
     48                 Set<SelectionKey> keys = selector.selectedKeys();
     49                 Iterator<SelectionKey> it = keys.iterator();
     50                 SelectionKey key = null;
     51                 while (it.hasNext()) {
     52                     key = it.next();
     53                     it.remove();
     54                     try {
     55                         handleInput(key);
     56                     } catch (Exception e) {
     57                         if (key != null) {
     58                             key.cancel();
     59                             if (key.channel() != null) {
     60                                 key.channel().close();
     61                             }
     62                         }
     63                     }
     64 
     65                 }
     66             } catch (IOException e) {
     67                 e.printStackTrace();
     68             }
     69 
     70         }
     71     }
     72 
     73     private void handleInput(SelectionKey key) throws IOException {
     74         if (key.isValid()) {
     75 //            判断是否连接成功(需要判断连接状态和连接结果是否成功)
     76             SocketChannel sc = (SocketChannel) key.channel();
     77 //            连接状态判断,是连接状态返回true,判断的是服务端是否已经返回ACK应答消息
     78             if (key.isConnectable()) {
     79 //                是连接状态则需要对连接结果进行判断,如果true,说明客户端连接成功,如果flase则抛出IO异常,连接失败
     80                 if(sc.finishConnect()){
     81                     sc.register(selector, SelectionKey.OP_READ);
     82                     doWrite(sc);
     83                 }else {
     84 //                    连接失败则进程退出
     85                     System.exit(1);
     86                 }
     87             }
     88             if (key.isReadable()) {
     89                 ByteBuffer readBuffer = ByteBuffer.allocate(1024);
     90                 int readBytes = sc.read(readBuffer);
     91                 if (readBytes > 0) {
     92                     readBuffer.flip();
     93                     byte[] bytes = new byte[readBuffer.remaining()];
     94                     readBuffer.get(bytes);
     95                     String body = new String(bytes, "utf-8");
     96                     System.out.println("Now is :" + body);
     97                     this.stop = true;
     98                 } else if (readBytes < 0) {
     99                     key.cancel();
    100                     sc.close();
    101                 } else {
    102                     ;//没有读到字节什么都不做
    103                 }
    104             }
    105         }
    106     }
    107 
    108     private void doConnection() {
    109         try {
    110 //            如果直接连接成功,则注册到多路复用器上,并注册SelectionKey.OP_READ,发送请求消息,读应答
    111             if (socketChannel.connect(new InetSocketAddress(host, port))) {
    112                 socketChannel.register(selector, SelectionKey.OP_READ);
    113                 doWrite(socketChannel);
    114             } else {
    115 //                连接不成功说明服务端没有返回TCP握手应答消息,但是不代表连接失败,注册socketChannel到多路复用器上,
    116 //                并注册SelectionKey.OP_CONNECT,当服务器返回TCP syn-ack 消息后,Selector 就能轮询到这个SocketChannel
    117 //                处于连接就绪状态
    118                 socketChannel.register(selector, SelectionKey.OP_CONNECT);
    119             }
    120         } catch (IOException e) {
    121             e.printStackTrace();
    122         }
    123     }
    124 
    125     private void doWrite(SocketChannel socketChannel) throws IOException {
    126         byte[] req = "QUERY TIME ORDER".getBytes();
    127         ByteBuffer writeBuffer = ByteBuffer.allocate(req.length);
    128         writeBuffer.put(req);
    129         writeBuffer.flip();
    130         socketChannel.write(writeBuffer);
    131         if (!writeBuffer.hasRemaining()) {
    132             System.out.println("send order 2 server succeed.");
    133         }
    134 
    135     }
    136 }

      通过上面代码的学习,我们对NIO的思路有一定的了解,尽管现在还不熟悉,但是认真看过,就会发现其套路脉络还是很清楚的。

  • 相关阅读:
    排列数组所有情况
    查到的结果的某个字段在一串字符串之中
    element组件化跳转和路由式跳转
    vue路由and组件操作
    事件 绑定,取消冒泡,拖拽 ,点击,事件委托习题
    窗口属性 和DOM 元素尺寸位置 及习题加强
    DOM树的增删改查 和 Date定时任务
    JS DOM 初做了解,习题笔记
    struts配置及检验
    第一个JSP登录跳转
  • 原文地址:https://www.cnblogs.com/xiaoyao-001/p/9278093.html
Copyright © 2020-2023  润新知