1. 程式人生 > >NIO網路程式設計 I/O 模型

NIO網路程式設計 I/O 模型

前言

  前面的文章講解了I/O 模型緩衝區(Buffer)通道(Channel)選擇器(Selector),這些都是關於NIO的特點,偏於理論一些,這篇文章LZ將通過利用這些知識點來實現NIO的伺服器和客戶端,當然了,只是一個簡單的demo,但是對於NIO的學習來說,足夠了,麻雀雖小但五臟俱全。話不多說,開始:

NIO服務端:

 1 public class EchoServer {
 2     private static int PORT = 8000;
 3 
 4     public static void main(String[] args) throws Exception {
5 // 先確定埠號 6 int port = PORT; 7 if (args != null && args.length > 0) { 8 port = Integer.parseInt(args[0]); 9 } 10 // 開啟一個ServerSocketChannel 11 ServerSocketChannel ssc = ServerSocketChannel.open(); 12 // 獲取ServerSocketChannel繫結的Socket
13 ServerSocket ss = ssc.socket(); 14 // 設定ServerSocket監聽的埠 15 ss.bind(new InetSocketAddress(port)); 16 // 設定ServerSocketChannel為非阻塞模式 17 ssc.configureBlocking(false); 18 // 開啟一個選擇器 19 Selector selector = Selector.open(); 20 // 將ServerSocketChannel註冊到選擇器上去並監聽accept事件
21 ssc.register(selector, SelectionKey.OP_ACCEPT); 22 while (true) { 23 // 這裡會發生阻塞,等待就緒的通道 24 int n = selector.select(); 25 // 沒有就緒的通道則什麼也不做 26 if (n == 0) { 27 continue; 28 } 29 // 獲取SelectionKeys上已經就緒的通道的集合 30 Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); 31 // 遍歷每一個Key 32 while (iterator.hasNext()) { 33 SelectionKey sk = iterator.next(); 34 // 通道上是否有可接受的連線 35 if (sk.isAcceptable() && sk.isValid()) { 36 ServerSocketChannel ssc1 = (ServerSocketChannel)sk.channel(); 37 SocketChannel sc = ssc1.accept(); 38 sc.configureBlocking(false); 39 sc.register(selector, SelectionKey.OP_READ); 40 } 41 // 通道上是否有資料可讀 42 else if (sk.isReadable() && sk.isValid()) { 43 readDataFromSocket(sk); 44 } 45 iterator.remove(); 46 } 47 } 48 } 49 50 private static ByteBuffer bb = ByteBuffer.allocate(1024); 51 52 // 從通道中讀取資料 53 protected static void readDataFromSocket(SelectionKey sk) throws Exception { 54 SocketChannel sc = (SocketChannel)sk.channel(); 55 bb.clear(); 56 while (sc.read(bb) > 0) { 57 bb.flip(); 58 while (bb.hasRemaining()) { 59 System.out.print((char)bb.get()); 60 } 61 System.out.println(); 62 bb.clear(); 63 } 64 } 65 }

 程式碼中的註釋其實已經很詳細了,再解釋一下:

  ❤ 5~9行:確定要監聽的埠號,這裡選擇的是8000;

  ❤ 10~17行:這裡是伺服器的程式,所以選擇的通道是ServerSocketChannel,同時獲取到它對應的Socket,也就是ServerSocket 因為使用的是NIO,所以將通道設定為非阻塞模式(17行),並繫結埠號8000;

  ❤ 18~21行:開啟一個選擇器,註冊當前通道感興趣的事件為Accept,也就是監聽來自客戶端的Socket資料;

  ❤ 22~24行:呼叫選擇器的Select()方法,等待來自於客戶端的Socket資料。程式會阻塞在這裡不會繼續讓下走,直到客戶端有Socket資料到來為止;在這裡就可以看出,NIO並不是一種非阻塞IO,因為NIO會阻塞在Selector的select()方法上。

  ❤ 25~28行:如果select()方法返回值為0的話,表明當前沒有準備就緒的通道,所以下面的程式碼都沒有必要執行,所以跳過當前迴圈,進行下一次的迴圈;

  ❤ 29~33行:獲取到已經就緒的通道集合,並對其進行迭代迴圈,集合的泛型是SelectionKey,之前的文章講過,選擇鍵用於封裝特定的通道;

  ❤ 35~44行:這裡是處理資料的核心點,做了兩件事:

    (1)程式碼進入36行,表明該通道上已經有資料到來了,接下來做的事情是將對應的SocketChannel註冊到選擇器上,通過傳入OP_READ標記,告訴選擇器我們關心新的Socket通道什麼時候可以準備好讀資料。

    (2)程式碼進入43行,表明該通道已經可以讀取資料了,此時呼叫readDataFromSocket()方法讀取通道中的資料。

  ❤ 45行:將鍵移除。這樣的話才能在通道下一次變為“就緒”時,Selector將再次將其新增到所選的鍵集合。

 NIO客戶端:

 1 public class EchoClient {
 2 
 3         private static final String STR = "Hello NIO!";
 4         private static final String REMOTE_IP = "127.0.0.1";
 5         private static final int THREAD_COUNT = 5;
 6 
 7         private static class NonBlockingSocketThread extends Thread {
 8             public void run() {
 9                 try {
10                     int port = 8000;
11                     SocketChannel sc = SocketChannel.open();
12                     sc.configureBlocking(false);
13                     sc.connect(new InetSocketAddress(REMOTE_IP, port));
14                     while (!sc.finishConnect()) {
15                         System.out.println("同" + REMOTE_IP + "的連線正在建立,請稍等!");
16                         Thread.sleep(10);
17                     }
18                     System.out.println("連線已建立,待寫入內容至指定ip+埠!時間為" + System.currentTimeMillis());
19                     String writeStr = STR + this.getName();
20                     ByteBuffer bb = ByteBuffer.allocate(writeStr.length());
21                     bb.put(writeStr.getBytes());
22                     bb.flip(); // 寫緩衝區的資料之前一定要先反轉(flip)
23                     sc.write(bb);
24                     bb.clear();
25                     sc.close();
26                 }
27                 catch (IOException e) {
28                     e.printStackTrace();
29                 }
30                 catch (InterruptedException e) {
31                     e.printStackTrace();
32                 }
33             }
34         }
35 
36         public static void main(String[] args) throws Exception {
37             NonBlockingSocketThread[] nbsts = new NonBlockingSocketThread[THREAD_COUNT];
38             for (int i = 0; i < THREAD_COUNT; i++)
39                 nbsts[i] = new NonBlockingSocketThread();
40             for (int i = 0; i < THREAD_COUNT; i++)
41                 nbsts[i].start();
42             // 一定要join保證執行緒程式碼先於sc.close()執行,否則會有AsynchronousCloseException
43             for (int i = 0; i < THREAD_COUNT; i++)
44                 nbsts[i].join();
45         }
46 }

 客戶端的程式碼就是向伺服器傳送資料就行,使用了NonBlockingSocketThread執行緒。

執行結果:

  先執行服務端:

  空白,很正常,因為在監聽客戶端資料的到來,此時並沒有資料。

執行客戶端:

  看到5個執行緒的資料已經發送,此時服務端的執行情況是:

資料全部接收到並列印,看到左邊的方框還是紅色的,說明這5個執行緒的資料接收、列印完畢之後,再繼續等待著客戶端的資料的到來。

Selector的關鍵點:

  (1)註冊一個ServerSocketChannel到Selector中,這個通道的作用只是為了監聽客戶端是否有資料到來(資料到來的意思是假如總共有100位元組的資料,如果來了一個位元組的資料,那麼這就算資料到來了),只要有資料到來,就把特定通道註冊到Selector中,並指定其感興趣的事件為讀事件;

  (2)ServerSocketChannel和SocketChannel(通道里面的是客戶端的資料)共同存在Selector中,只要有註冊的事件到來,Selector就會取消阻塞狀態,遍歷SelectionKey集合,繼續註冊讀事件的通道或者從通道中讀取資料。

參考:https://www.cnblogs.com/xrq730/p/5186065.html