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的思路有一定的了解,尽管现在还不熟悉,但是认真看过,就会发现其套路脉络还是很清楚的。

 

posted @ 2018-07-08 10:55  醉逍遥_001  阅读(302)  评论(0编辑  收藏  举报