第二章 BIO與NIO
阿新 • • 發佈:2022-05-22
一、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操作。
- Buffer:用於存取資料,最主要的是ByteBuffer
- 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操作(編解碼、業務邏輯等)