1. 程式人生 > 其它 >第二章 BIO與NIO

第二章 BIO與NIO

一、BIO

1、服務端程式:

1 package bio;
 2 
 3 import java.io.BufferedReader;
 4 import java.io.IOException;
 5 import java.io.InputStreamReader;
 6 import java.io.PrintWriter;
 7 import java.net.ServerSocket;
 8 import java.net.Socket;
 9 import java.util.Date;
10 
11 public class BioServer {
12 
13     public static void main(String[] args) throws IOException {
14         ServerSocket serverSocket = new ServerSocket(8081);
15         Socket clientSocket = null;
16         while(true){
17             clientSocket = serverSocket.accept();//如果沒有客戶端接入,主執行緒阻塞在這裡
18             new Thread(new ServerHandler(clientSocket)).start();
19         }
20         //finall關閉serverSocket
21     }
22 
23 }
24 
25 class ServerHandler implements Runnable{
26     private Socket clientSocket;
27 
28     public ServerHandler(Socket clientSocket) {
29         this.clientSocket = clientSocket;
30     }
31 
32     @Override
33     public void run() {
34         try {
35             BufferedReader reader = new BufferedReader(new InputStreamReader(this.clientSocket.getInputStream()));
36             PrintWriter writer = new PrintWriter(this.clientSocket.getOutputStream(), true);
37             while(true){
38                 String body = reader.readLine();
39                 if (body==null){
40                     break;
41                 }
42                 System.out.println(body);
43                 writer.println(new Date().toString() + "->" + body);
44             }
45         } catch (IOException e) {
46             e.printStackTrace();
47         }
48         //finally關閉資源:流和socket
49     }
50 }

 1 package bio;
 2 
 3 import java.io.BufferedReader;
 4 import java.io.IOException;
 5 import java.io.InputStreamReader;
 6 import java.io.PrintWriter;
 7 import java.net.ServerSocket;
 8 import java.net.Socket;
 9 import java.util.Date;
10 
11 public class BioServer {
12 
13     public static void main(String[] args) throws IOException {
14         ServerSocket serverSocket = new ServerSocket(8081);
15         Socket clientSocket = null;
16         while(true){
17             clientSocket = serverSocket.accept();//如果沒有客戶端接入,主執行緒阻塞在這裡
18             new Thread(new ServerHandler(clientSocket)).start();
19         }
20         //finall關閉serverSocket
21     }
22 
23 }
24 
25 class ServerHandler implements Runnable{
26     private Socket clientSocket;
27 
28     public ServerHandler(Socket clientSocket) {
29         this.clientSocket = clientSocket;
30     }
31 
32     @Override
33     public void run() {
34         try {
35             BufferedReader reader = new BufferedReader(new InputStreamReader(this.clientSocket.getInputStream()));
36             PrintWriter writer = new PrintWriter(this.clientSocket.getOutputStream(), true);
37             while(true){
38                 String body = reader.readLine();
39                 if (body==null){
40                     break;
41                 }
42                 System.out.println(body);
43                 writer.println(new Date().toString() + "->" + body);
44             }
45         } catch (IOException e) {
46             e.printStackTrace();
47         }
48         //finally關閉資源:流和socket
49     }
50 }
  • 服務端使用8081埠開啟服務,不斷接入客戶端請求,每接入一個請求,都建立一個執行緒來處理這個請求。
  • 處理邏輯:讀取客戶端傳來的資訊,之後想客戶端寫資訊。

2、客戶端程式:

1 package bio;
 2 
 3 import java.io.BufferedReader;
 4 import java.io.IOException;
 5 import java.io.InputStreamReader;
 6 import java.io.PrintWriter;
 7 import java.net.Socket;
 8 
 9 public class BioClient {
10     public static void main(String[] args) throws IOException {
11         Socket clientSocket = new Socket("127.0.0.1", 8081);
12         BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
13         PrintWriter writer = new PrintWriter(clientSocket.getOutputStream(), true);
14         writer.println("haha");
15         String resp = reader.readLine();
16         System.out.println(resp);
17         //finall關閉serverSocket
18     }
19 }
 1 package bio;
 2 
 3 import java.io.BufferedReader;
 4 import java.io.IOException;
 5 import java.io.InputStreamReader;
 6 import java.io.PrintWriter;
 7 import java.net.Socket;
 8 
 9 public class BioClient {
10     public static void main(String[] args) throws IOException {
11         Socket clientSocket = new Socket("127.0.0.1", 8081);
12         BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
13         PrintWriter writer = new PrintWriter(clientSocket.getOutputStream(), true);
14         writer.println("haha");
15         String resp = reader.readLine();
16         System.out.println(resp);
17         //finall關閉serverSocket
18     }
19 }
  • 客戶端建立socket去連線服務端,之後想服務端寫資訊,並且讀取服務端傳來的資訊。

服務端阻塞的幾個點:

  • serverSocket.accept();//如果沒有客戶端接入,主執行緒阻塞在這裡
  • 輸入流InputStream的read操作也會阻塞:直到“有資料可讀”或者“可用資料讀取完畢”或者“發生異常”
  • 輸出流OutputStream的write操作也會阻塞:直到“所有要傳送的位元組全部寫入”或者“發生異常”

二、NIO

1、服務端程式

1 package nio;
 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.Iterator;
11 
12 public class NioServer {
13     public static void main(String[] args) throws IOException {
14         Selector selector = Selector.open();
15 
16         ServerSocketChannel serverChannel = ServerSocketChannel.open();
17         serverChannel.configureBlocking(false);
18         serverChannel.socket().bind(new InetSocketAddress(8083));//監聽連結8082埠的客戶端socket
19         serverChannel.register(selector, SelectionKey.OP_ACCEPT);//將serverChannel註冊到selector,並監聽接受連線事件
20 
21         while (selector.select() > 0) {//該方法會發生阻塞,直到至少有一個事件"準備就緒"為止
22             Iterator<SelectionKey> it = selector.selectedKeys().iterator();
23             while (it.hasNext()) {
24                 SelectionKey sk = it.next();
25                 if (sk.isAcceptable()) {
26                     SocketChannel clientChannel = serverChannel.accept();//相當於客戶端三次握手
27                     clientChannel.configureBlocking(false);
28                     clientChannel.register(selector, SelectionKey.OP_READ);
29                 } else if (sk.isReadable()) {
30                     SocketChannel clientChannel = (SocketChannel) sk.channel();
31                     ByteBuffer buf = ByteBuffer.allocate(1024);
32                     while (clientChannel.read(buf) > 0) {//將通道中的資料讀到緩衝區,因為clientChannel已經是非阻塞的,所以這裡的read是非阻塞的
33                         buf.flip();//將緩衝區由寫模式切換到讀模式
34                         byte[] bytes = new byte[buf.remaining()];
35                         buf.get(bytes);//將緩衝區中的資料讀取到bytes中
36                         String body = new String(bytes, "UTF-8");
37                         System.out.println("接收到來自客戶端的資訊:" + body);
38                         buf.clear();
39                     }
40                 }
41                 it.remove();
42             }
43         }
44     }
45 }

 1 package nio;
 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.Iterator;
11 
12 public class NioServer {
13     public static void main(String[] args) throws IOException {
14         Selector selector = Selector.open();
15 
16         ServerSocketChannel serverChannel = ServerSocketChannel.open();
17         serverChannel.configureBlocking(false);
18         serverChannel.socket().bind(new InetSocketAddress(8083));//監聽連結8082埠的客戶端socket
19         serverChannel.register(selector, SelectionKey.OP_ACCEPT);//將serverChannel註冊到selector,並監聽接受連線事件
20 
21         while (selector.select() > 0) {//該方法會發生阻塞,直到至少有一個事件"準備就緒"為止
22             Iterator<SelectionKey> it = selector.selectedKeys().iterator();
23             while (it.hasNext()) {
24                 SelectionKey sk = it.next();
25                 if (sk.isAcceptable()) {
26                     SocketChannel clientChannel = serverChannel.accept();//相當於客戶端三次握手
27                     clientChannel.configureBlocking(false);
28                     clientChannel.register(selector, SelectionKey.OP_READ);
29                 } else if (sk.isReadable()) {
30                     SocketChannel clientChannel = (SocketChannel) sk.channel();
31                     ByteBuffer buf = ByteBuffer.allocate(1024);
32                     while (clientChannel.read(buf) > 0) {//將通道中的資料讀到緩衝區,因為clientChannel已經是非阻塞的,所以這裡的read是非阻塞的
33                         buf.flip();//將緩衝區由寫模式切換到讀模式
34                         byte[] bytes = new byte[buf.remaining()];
35                         buf.get(bytes);//將緩衝區中的資料讀取到bytes中
36                         String body = new String(bytes, "UTF-8");
37                         System.out.println("接收到來自客戶端的資訊:" + body);
38                         buf.clear();
39                     }
40                 }
41                 it.remove();
42             }
43         }
44     }
45 }
  • nio三元件:
    • Buffer:用於存取資料,最主要的是ByteBuffer
      • position:下一個將被操作的位元組位置
      • limit:在寫模式下表示可以進行寫的位元組數,在讀模式下表示可以進行讀的位元組數
      • capacity:Buffer的大小
    • Channel:用於傳輸資料,與Buffer相互配合
    • Selector:多路複用器。輪詢註冊在其上的Channel,當發現某個或者多個Channel處於“就緒狀態”後(有新的TCP連結接入、讀、寫事件),從阻塞狀態返回就緒的Channel的SelectionKey集合,之後進行IO操作。
  • selector.select():阻塞,直到有“就緒事件”發生或丟擲異常

2、客戶端程式

 1 package nio;
 2 
 3 import java.io.IOException;
 4 import java.net.InetSocketAddress;
 5 import java.nio.ByteBuffer;
 6 import java.nio.channels.SocketChannel;
 7 
 8 public class NioClient {
 9     public static void main(String[] args) throws IOException {
10         SocketChannel clientChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8083));//向服務端發出連線請求,服務端會通過accept()方法實現三次握手後,建立連線
11         clientChannel.configureBlocking(false);
12         ByteBuffer buf = ByteBuffer.allocate(1024);//分配在JVM堆中:capacity=1024;position=0;limit=1024
13 //        ByteBuffer buf = ByteBuffer.allocateDirect(1024);//分配在堆外記憶體(實體記憶體)中
14         buf.put("abcde".getBytes());//將資料寫入緩衝區buf中:capacity=1024;position=5;limit=1024
15         buf.flip();//將緩衝區從寫模式切換到讀模式:capacity=1024;position=0;limit=5
16         clientChannel.write(buf);//將緩衝區的資料寫入通道:capacity=1024;position=5;limit=5
17         buf.clear();//清空緩衝區:capacity=1024;position=0;limit=1024
18         clientChannel.close();
19     }
20 }

附:ByteBuffer使用JVM堆記憶體和使用堆外記憶體的區別:(圖片來自尚矽谷的NIO教程)

  • 使用堆記憶體:
    • 應用程式將資料寫入使用者地址空間(JVM)中,之後將使用者地址空間中的資料拷貝到核心地址空間(作業系統)
  • 使用堆外記憶體:
    • 直接在實體記憶體開闢空間,應用程式直接將資料寫入實體記憶體,之後作業系統將其寫入硬碟 - “零拷貝”

三、BIO與NIO的比較

1、執行緒數

  • BIO:一個客戶端連線就要使用一個服務端執行緒來進行處理
    • 可能會有大量的想愛你成處於休眠狀態,只是等待輸入或輸出(阻塞)
    • 為每個執行緒分配呼叫棧,大約1M,服務端記憶體吃緊
    • 即使服務端記憶體很大,執行緒數太大會導致執行緒上下文切換浪費大量時間
  • NIO:一個服務端執行緒操作一個Selector,就可以處理成千上萬的客戶端連線

2、阻塞情況

  • BIO:讀、寫、接受連線都會發生阻塞
  • NIO:只有Selector.select()會阻塞,其實是等待Channel上的“就緒事件”

3、面向物件

  • BIO:面向流
  • NIO:面向Buffer

4、適合點

  • BIO:如果你有少量的連線使用非常高的頻寬,一次傳送大量的資料,也許典型的IO伺服器實現可能非常契合
  • NIO:如果需要管理同時開啟的成千上萬個連線,這些連線每次只是傳送少量的資料,例如聊天伺服器,實現NIO的伺服器可能是一個優勢。

四、Reactor模型

主從模型:

  • 主執行緒池:
    • 全部由NIO執行緒組成,使用執行緒池是因為擔心效能問題
    • 接收客戶端連線請求,可能包含認證
    • 接收到客戶端的連線請求並處理完成(比如認證)後,將創建出來的SocketChannel(檢視上邊的NIOServer類)註冊到次執行緒池的某一條NIO執行緒上,之後這條NIO執行緒進行IO操作。
  • 次執行緒池:
    • 全部由NIO執行緒組成
    • 進行IO操作(編解碼、業務邏輯等)