文章目錄
重要概念
同步和異步
概念 | 含義 |
---|---|
同步 |
IO 時,Java 發起 IO 的線程自己處理 IO 讀寫,因此需要等待 IO 操作完成或者輪詢地去查看 IO 操作是否就緒 |
異步 |
IO 時,Java 發起 IO 的線程將 IO 讀寫委託給操作系統,並將數據緩衝區地址和大小傳給操作系統,當 IO 完成時 Java 線程會得到通知 |
阻塞和非阻塞
概念 | 含義 |
---|---|
阻塞 |
使用阻塞 IO 時,Java 發起 IO 的線程會一直阻塞到讀寫完成才返回 |
非阻塞 |
使用非阻塞 IO 時,不管是否能讀寫,發起 IO 的線程會馬上得到一個返回值,進而繼續執行其他代碼 |
1. BIO(同步阻塞IO)
1.1 BIO 的處理流程
BIO
的流程通常是服務端啓動一個ServerSocket
,然後在客戶端啓動Socket
來連接服務端進行通信,默認情況下服務端需要對每個請求建立一個線程進行處理,而客戶端發送請求後線程會等待獲得響應後才繼續執行
Blocking IO
的特點就是在IO執行的兩個階段,等待數據和拷貝數據,都被阻塞了。大部分的socket
接口都是阻塞型的,而所謂阻塞型接口是指系統調用(一般是IO接口)不返回調用結果並讓當前線程一直阻塞,直到該系統調用獲得結果或者超時出錯時才返回
1.2 使用示例
1.2.1 客戶端
public class Client {
//默認的端口號
private static int DEFAULT_SERVER_PORT = 10086;
private static String DEFAULT_SERVER_IP = "127.0.0.1";
private static String QUIT_MSG = "q";
private static Socket socket;
public static void start() {
try {
socket = new Socket(DEFAULT_SERVER_IP, DEFAULT_SERVER_PORT);
System.out.println("Client: start, port " + DEFAULT_SERVER_PORT);
} catch (IOException e) {
e.printStackTrace();
}
}
public static boolean send(String exp) {
if (QUIT_MSG.equals(exp)) {
System.out.println("Client: quit");
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return false;
}
System.out.println("Client: send " + exp);
InputStreamReader inputStreamReader;
BufferedReader in;
PrintWriter out;
try {
inputStreamReader = new InputStreamReader(socket.getInputStream());
in = new BufferedReader(inputStreamReader);
out = new PrintWriter(socket.getOutputStream(), true);
out.println(exp);
System.out.println("Client: receive " + in.readLine());
return true;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
}
1.2.2 服務端
-
服務器類
public class Server { //默認的端口號 private static int DEFAULT_PORT = 10086; private static ServerSocket server; public static void start() { start(DEFAULT_PORT); } public synchronized static void start(int port) { if (server != null) return; new Thread(() -> { try { // 通過構造函數創建ServerSocket,如果端口合法且空閒,服務端就監聽成功 server = new ServerSocket(port); System.out.println("Server: start, port " + port); // 無限循環監聽客戶端連接,如果沒有客戶端接入,將阻塞在accept操作上 while (true) { Socket socket = server.accept(); //當有新的客戶端接入時,會執行下面的代碼,創建一個新的線程處理這條 Socket 連接 new Thread(new ServerHandler(socket)).start(); } } catch (IOException e) { e.printStackTrace(); } finally { //一些必要的清理工作 if (server != null) { System.out.println("Server: close"); try { server.close(); } catch (IOException e) { e.printStackTrace(); } server = null; } } }, "Server").start(); } }
-
服務器處理者
public class ServerHandler implements Runnable { private Socket socket; public ServerHandler(Socket socket) { this.socket = socket; } @Override public void run() { InputStreamReader inputStreamReader; BufferedReader in; PrintWriter out; try { inputStreamReader = new InputStreamReader(socket.getInputStream()); in = new BufferedReader(inputStreamReader); out = new PrintWriter(socket.getOutputStream(), true); String exp; while (null != (exp = in.readLine())) { // 通過BufferedReader讀取一行,如果已經讀到輸入流尾部或者對端關閉,返回null,退出循環 System.out.println("Server: receive " + exp); String result = "Server: send " + exp; out.println(result); } System.out.println("Server: handler quit"); } catch (Exception e) { e.printStackTrace(); } finally { try { if (null != socket) socket.close(); } catch (IOException e) { e.printStackTrace(); } } } }
1.2.3 測試類
public class Test {
public static void main(String[] args) throws InterruptedException {
Server.start();
//避免客戶端先於服務器啓動前執行代碼
Thread.sleep(100);
//運行客戶端
Client.start();
while (Client.send(new Scanner(System.in).nextLine()));
}
}
2. NIO(同步非阻塞IO)
NIO
本身是基於事件驅動思想來完成的,主要解決的是BIO
的大併發問題:
在使用
同步 I/O
的網絡應用中,如果同時處理多個客戶端請求,就必須使用多線程來處理,也就是將每一個客戶端請求分配給一個線程來單獨處理。這樣做會帶來一個問題,由於每創建一個線程就要爲這個線程分配一定的內存空間,而操作系統本身資源是有限的,這樣就對線程的總數構成了一定的限制。如果客戶端的請求過多,服務端程序可能會因爲不堪重負而拒絕客戶端的請求,甚至服務器可能會因此而癱瘓。
NIO 解決了線程創建過多的問題,不過在 NIO 的處理方式中,當一個請求進來,開啓線程進行處理可能會等待後端應用的資源(JDBC連接等),其實這個線程就被阻塞了,當併發量上來時還是會有和 BIO 一樣的問題
NIO 基於 Reactor,當socket
有流可讀或可寫入 socket
時,操作系統會相應的通知應用程序進行處理,應用再將流讀取到緩衝區或寫入操作系統。 也就是說,這個時候已經不是一個連接就要對應一個處理線程了,而是有效的請求,對應一個線程,當連接沒有數據時,是沒有工作線程來處理的
BIO 與 NIO 比較重要的不同點:
- 使用 BIO 的時候往往會引入多線程,每個連接對應一個單獨的線程;而 NIO 線程處理的都是有效連接(數據就緒),且一個線程可以分管處理多個連接上的就緒數據,節省線程資源開銷
- BIO 面向流,NIO 面向緩衝區
2.1 NIO 的處理流程
當用戶線程發起 I/O
請求後,會將 socket
連接及關注事件註冊到服務端的selector(多路複用器,os級別線程)
上,selector
循環遍歷註冊其上的 socket
連接,看是否有其關注事件就緒,如果連接有數據就緒後,就通知應用程序建立線程進行數據讀寫,也就是實際的讀寫操作是由應用程序完成的,而不是操作系統
2.2 使用示例
2.2.1 客戶端
-
客戶端
public class Client { private static String DEFAULT_HOST = "127.0.0.1"; private static int DEFAULT_PORT = 10086; private static String QUIT_MSG = "q"; private static ClientHandler clientHandle; public static void start() { start(DEFAULT_HOST, DEFAULT_PORT); } public static synchronized void start(String ip, int port) { if (clientHandle != null) { return; } clientHandle = new ClientHandler(ip, port); new Thread(clientHandle, "Client").start(); } //向服務器發送消息 public static boolean sendMsg(String msg) throws Exception { if (QUIT_MSG.equals(msg)) { System.out.println("Client: exit"); clientHandle.stop(); return false; } clientHandle.sendMsg(msg); return true; } }
-
客戶端處理者
public class ClientHandler implements Runnable { private String host; private int port; private Selector selector; private SocketChannel socketChannel; private volatile boolean started; public ClientHandler(String ip, int port) { this.host = ip; this.port = port; try { //創建選擇器 selector = Selector.open(); //打開監聽通道 socketChannel = SocketChannel.open(); //如果爲 true,則此通道將被置於阻塞模式;如果爲 false,則此通道將被置於非阻塞模式 socketChannel.configureBlocking(false); started = true; System.out.println("Client: start, port " + port); } catch (IOException e) { e.printStackTrace(); System.exit(1); } } public void stop() { started = false; } @Override public void run() { try { doConnect(); } catch (IOException e) { e.printStackTrace(); System.exit(1); } //循環遍歷selector while (started) { try { //無論是否有讀寫事件發生,selector每隔1s被喚醒一次 selector.select(1000); Set<SelectionKey> keys = selector.selectedKeys(); Iterator<SelectionKey> it = keys.iterator(); SelectionKey key; while (it.hasNext()) { key = it.next(); it.remove(); try { handleInput(key); } catch (Exception e) { if (key != null) { key.cancel(); if (key.channel() != null) { key.channel().close(); } } } } } catch (Exception e) { e.printStackTrace(); System.exit(1); } } //selector關閉後會自動釋放裏面管理的資源 if (selector != null) try { selector.close(); } catch (Exception e) { e.printStackTrace(); } } private void handleInput(SelectionKey key) throws IOException { if (key.isValid()) { SocketChannel sc = (SocketChannel) key.channel(); if (key.isConnectable()) { if (sc.finishConnect()) { System.out.println("Client: connect to server"); } else { System.exit(1); } } //讀消息 if (key.isReadable()) { //創建ByteBuffer,並開闢一個1M的緩衝區 ByteBuffer buffer = ByteBuffer.allocate(1024); //讀取請求碼流,返回讀取到的字節數 int readBytes = sc.read(buffer); //讀取到字節,對字節進行編解碼 if (readBytes > 0) { //將緩衝區當前的limit設置爲position=0,用於後續對緩衝區的讀取操作 buffer.flip(); //根據緩衝區可讀字節數創建字節數組 byte[] bytes = new byte[buffer.remaining()]; //將緩衝區可讀字節數組複製到新建的數組中 buffer.get(bytes); String result = new String(bytes, StandardCharsets.UTF_8); System.out.println("Client: receive " + result); } else if (readBytes < 0) { //鏈路已經關閉,釋放資源 key.cancel(); sc.close(); } } } } //異步發送消息 private void doWrite(SocketChannel channel, String request) throws IOException { //將消息編碼爲字節數組 byte[] bytes = request.getBytes(); //根據數組容量創建ByteBuffer ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length); //將字節數組複製到緩衝區 writeBuffer.put(bytes); //flip操作 writeBuffer.flip(); //發送緩衝區的字節數組 channel.write(writeBuffer); } private void doConnect() throws IOException { if (socketChannel.connect(new InetSocketAddress(host, port))) { System.out.println("Client: already connect to Server"); } else { socketChannel.register(selector, SelectionKey.OP_CONNECT); } } public void sendMsg(String msg) throws Exception { socketChannel.register(selector, SelectionKey.OP_READ); doWrite(socketChannel, msg); } }
2.2.2 服務端
-
服務器
public class Server { private static int DEFAULT_PORT = 10086; private static ServerHandler serverHandle; public static void start() { start(DEFAULT_PORT); } public static synchronized void start(int port) { if (serverHandle != null) { return; } serverHandle = new ServerHandler(port); new Thread(serverHandle, "Server").start(); } }
-
服務端處理者
public class ServerHandler implements Runnable { private Selector selector; private ServerSocketChannel serverChannel; private volatile boolean started; /** * 構造方法 * * @param port 指定要監聽的端口號 */ public ServerHandler (int port) { try { //創建選擇器 selector = Selector.open(); //打開監聽通道 serverChannel = ServerSocketChannel.open(); //如果爲 true,則此通道將被置於阻塞模式;如果爲 false,則此通道將被置於非阻塞模式 serverChannel.configureBlocking(false); //綁定端口 backlog設爲1024 serverChannel.socket().bind(new InetSocketAddress(port), 1024); //監聽客戶端連接請求 serverChannel.register(selector, SelectionKey.OP_ACCEPT); //標記服務器已開啓 started = true; System.out.println("Server: start, port " + port); } catch (IOException e) { e.printStackTrace(); System.exit(1); } } public void stop() { started = false; } @Override public void run() { //循環遍歷selector while (started) { try { //無論是否有讀寫事件發生,selector每隔1s被喚醒一次 selector.select(1000); Set<SelectionKey> keys = selector.selectedKeys(); Iterator<SelectionKey> it = keys.iterator(); SelectionKey key; while (it.hasNext()) { key = it.next(); it.remove(); try { handleInput(key); } catch (Exception e) { if (key != null) { key.cancel(); if (key.channel() != null) { key.channel().close(); } } } } } catch (Throwable t) { t.printStackTrace(); } } //selector關閉後會自動釋放裏面管理的資源 if (selector != null) try { selector.close(); } catch (Exception e) { e.printStackTrace(); } } private void handleInput(SelectionKey key) throws IOException { if (key.isValid()) { //處理新接入的請求消息 if (key.isAcceptable()) { ServerSocketChannel ssc = (ServerSocketChannel) key.channel(); //通過ServerSocketChannel的accept創建SocketChannel實例 //完成該操作意味着完成TCP三次握手,TCP物理鏈路正式建立 SocketChannel sc = ssc.accept(); //設置爲非阻塞的 sc.configureBlocking(false); //註冊處理讀就緒事件 sc.register(selector, SelectionKey.OP_READ); } // 讀就緒事件觸發讀消息 if (key.isReadable()) { SocketChannel sc = (SocketChannel) key.channel(); //創建ByteBuffer,並開闢一個1M的緩衝區 ByteBuffer buffer = ByteBuffer.allocate(1024); //讀取請求碼流,返回讀取到的字節數 int readBytes = sc.read(buffer); //讀取到字節,對字節進行編解碼 if (readBytes > 0) { //將緩衝區當前的limit設置爲position=0,用於後續對緩衝區的讀取操作 buffer.flip(); //根據緩衝區可讀字節數創建字節數組 byte[] bytes = new byte[buffer.remaining()]; //將緩衝區可讀字節數組複製到新建的數組中 buffer.get(bytes); String exp = new String(bytes, StandardCharsets.UTF_8); System.out.println("Server: receive " + exp); //處理數據 String result; try { result = "Server: send " + exp; } catch (Exception e) { result = "Error: " + e.getMessage(); } //發送應答消息 doWrite(sc, result); } else if (readBytes < 0) { //鏈路已經關閉,釋放資源 key.cancel(); sc.close(); } } } } //異步發送應答消息 private void doWrite(SocketChannel channel, String response) throws IOException { //將消息編碼爲字節數組 byte[] bytes = response.getBytes(); //根據數組容量創建ByteBuffer ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length); //將字節數組複製到緩衝區 writeBuffer.put(bytes); //flip操作 writeBuffer.flip(); //發送緩衝區的字節數組 channel.write(writeBuffer); } }
2.2.3 測試類
public class Test {
public static void main(String[] args) throws InterruptedException {
Server.start();
//避免客戶端先於服務器啓動前執行代碼
Thread.sleep(100);
//運行客戶端
Client.start();
while (Client.send(new Scanner(System.in).nextLine()));
}
}
3. AIO(異步非阻塞IO)
3.1 AIO 的處理流程
與NIO
不同,AIO 基於 Proactor,進行讀寫操作時,只須直接調用 API
的read/write
方法。這兩種方法都是異步的,對於讀操作而言,當有流可讀取時,操作系統會將可讀的流傳入read
方法的緩衝區,並通知應用程序;對於寫操作而言,當操作系統將write
方法傳遞的流寫入完畢時,也會主動通知應用程序。 即可以理解爲,read/write
方法都是異步的,完成後會主動調用數據接收方的回調函數
3.2 使用示例
3.2.1 客戶端
-
客戶端
public class Client { private static String DEFAULT_HOST = "127.0.0.1"; private static int DEFAULT_PORT = 10086; private static String QUIT_MSG = "q"; private static AsyncClientHandler clientHandle; public static void start() { start(DEFAULT_HOST, DEFAULT_PORT); } public static synchronized void start(String ip, int port) { if (clientHandle != null) { return; } clientHandle = new AsyncClientHandler(ip, port); new Thread(clientHandle, "Client").start(); } //向服務器發送消息 public static boolean sendMsg(String msg) { if (QUIT_MSG.equals(msg)) { clientHandle.stop(); return false; } clientHandle.sendMsg(msg); return true; } }
-
客戶端處理者
public class AsyncClientHandler implements CompletionHandler<Void, AsyncClientHandler>, Runnable { private AsynchronousSocketChannel clientChannel; private String host; private int port; private CountDownLatch latch; public AsyncClientHandler(String host, int port) { this.host = host; this.port = port; try { //創建異步的客戶端通道 clientChannel = AsynchronousSocketChannel.open(); } catch (IOException e) { e.printStackTrace(); } } @Override public void run() { //創建CountDownLatch等待 latch = new CountDownLatch(1); //發起異步連接操作,回調接口就是這個類本身,如果連接成功會回調completed方法 clientChannel.connect(new InetSocketAddress(host, port), this, this); try { latch.await(); } catch (InterruptedException e1) { e1.printStackTrace(); } try { clientChannel.close(); System.out.println("Client: close"); } catch (IOException e) { e.printStackTrace(); } } public void stop() { latch.countDown(); } //連接服務器成功 //意味着TCP三次握手完成 @Override public void completed(Void result, AsyncClientHandler attachment) { System.out.println("Client: connect to server"); } //連接服務器失敗 @Override public void failed(Throwable exc, AsyncClientHandler attachment) { System.err.println("Client: connect to server fail"); exc.printStackTrace(); try { clientChannel.close(); latch.countDown(); } catch (IOException e) { e.printStackTrace(); } } //向服務器發送消息 public void sendMsg(String msg) { byte[] req = msg.getBytes(); ByteBuffer writeBuffer = ByteBuffer.allocate(req.length); writeBuffer.put(req); writeBuffer.flip(); //異步寫 clientChannel.write(writeBuffer, writeBuffer, new ClientWriteHandler(clientChannel, latch)); } }
-
客戶端寫處理
public class ClientWriteHandler implements CompletionHandler<Integer, ByteBuffer> { private AsynchronousSocketChannel clientChannel; private CountDownLatch latch; public ClientWriteHandler(AsynchronousSocketChannel clientChannel, CountDownLatch latch) { this.clientChannel = clientChannel; this.latch = latch; } @Override public void completed(Integer result, ByteBuffer buffer) { //完成全部數據的寫入 if (buffer.hasRemaining()) { clientChannel.write(buffer, buffer, this); } else { //讀取數據 ByteBuffer readBuffer = ByteBuffer.allocate(1024); clientChannel.read(readBuffer, readBuffer, new ClientReadHandler(clientChannel, latch)); } } @Override public void failed(Throwable exc, ByteBuffer attachment) { System.err.println("Client: send fail"); try { clientChannel.close(); } catch (IOException e) { e.printStackTrace(); } finally { latch.countDown(); } } }
-
客戶端讀處理
public class ClientReadHandler implements CompletionHandler<Integer, ByteBuffer> { private AsynchronousSocketChannel clientChannel; private CountDownLatch latch; public ClientReadHandler(AsynchronousSocketChannel clientChannel, CountDownLatch latch) { this.clientChannel = clientChannel; this.latch = latch; } @Override public void completed(Integer result, ByteBuffer buffer) { buffer.flip(); byte[] bytes = new byte[buffer.remaining()]; buffer.get(bytes); String body; body = new String(bytes, StandardCharsets.UTF_8); System.out.println("Client: receive " + body); } @Override public void failed(Throwable exc, ByteBuffer attachment) { System.err.println("Client: receive fail"); try { clientChannel.close(); } catch (IOException e) { e.printStackTrace(); } finally { latch.countDown(); } } }
3.2.2 服務端
-
服務器
public class Server { private static int DEFAULT_PORT = 10086; private static AsyncServerHandler SERVER_HANDLER; public static void start() { start(DEFAULT_PORT); } public static synchronized void start(int port) { if (SERVER_HANDLER != null) { System.out.println("Server: already start"); return; } SERVER_HANDLER = new AsyncServerHandler(port); new Thread(SERVER_HANDLER, "Server").start(); } }
-
服務端處理者
public class AsyncServerHandler implements Runnable { public CountDownLatch latch; public AsynchronousServerSocketChannel serverSocketChannel; public AsyncServerHandler(int port) { try { //創建服務端通道 serverSocketChannel = AsynchronousServerSocketChannel.open(); //綁定端口 serverSocketChannel.bind(new InetSocketAddress(port)); System.out.println("Server: start, port " + port); } catch (IOException e) { e.printStackTrace(); } } @Override public void run() { // CountDownLatch 初始化 // 作用:在完成一組正在執行的操作之前,允許當前的線程一直阻塞,防止服務端執行完成後退出, 也可以使用 while(true) + sleep latch = new CountDownLatch(1); //用於接收客戶端的連接 serverSocketChannel.accept(this, new AcceptHandler()); try { latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } } }
-
服務端接受連接處理
public class AcceptHandler implements CompletionHandler<AsynchronousSocketChannel, AsyncServerHandler> { private static int CLIENT_COUNT = 0; @Override public void completed(AsynchronousSocketChannel channel, AsyncServerHandler attachment) { //繼續接受其他客戶端的請求 CLIENT_COUNT++; System.out.println("Server: client num " + CLIENT_COUNT); attachment.serverSocketChannel.accept(attachment, this); //創建新的Buffer ByteBuffer buffer = ByteBuffer.allocate(1024); //異步讀 第三個參數爲接收消息回調的業務Handler channel.read(buffer, buffer, new ReadHandler(channel)); } @Override public void failed(Throwable exc, AsyncServerHandler serverHandler) { exc.printStackTrace(); serverHandler.latch.countDown(); } }
-
服務端讀處理
public class ReadHandler implements CompletionHandler<Integer, ByteBuffer> { //用於讀取半包消息和發送應答 private AsynchronousSocketChannel channel; public ReadHandler(AsynchronousSocketChannel channel) { this.channel = channel; } //讀取到消息後的處理 @Override public void completed(Integer result, ByteBuffer buffer) { //flip操作 buffer.flip(); byte[] message = new byte[buffer.remaining()]; buffer.get(message); String expr = new String(message, StandardCharsets.UTF_8); System.out.println("Server: receive " + expr); String resultMsg; try { resultMsg = "Server: send " + expr; } catch (Exception e) { resultMsg = "Error:" + e.getMessage(); } //向客戶端發送消息 doWrite(resultMsg); } //發送消息 private void doWrite(String result) { byte[] bytes = result.getBytes(); ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length); writeBuffer.put(bytes); writeBuffer.flip(); //異步寫數據 參數與前面的read一樣 channel.write(writeBuffer, writeBuffer, new WriteHandler(channel)); } @Override public void failed(Throwable exc, ByteBuffer attachment) { try { this.channel.close(); } catch (IOException e) { e.printStackTrace(); } } }
-
服務端寫處理
public class WriteHandler implements CompletionHandler<Integer, ByteBuffer> { private AsynchronousSocketChannel channel; public WriteHandler(AsynchronousSocketChannel channel) { this.channel = channel; } @Override public void completed(Integer result, ByteBuffer buffer) { //如果沒有發送完,就繼續發送直到完成 if (buffer.hasRemaining()) { channel.write(buffer, buffer, this); } else { //發送完畢,準備讀取數據 ByteBuffer readBuffer = ByteBuffer.allocate(1024); //異步讀 第三個參數爲接收消息回調的業務 Handler channel.read(readBuffer, readBuffer, new ReadHandler(channel)); } } @Override public void failed(Throwable exc, ByteBuffer attachment) { try { channel.close(); } catch (IOException ignored) { } } }
3.2.3 測試類
public class Test {
public static void main(String[] args) throws Exception {
//運行服務器
Server.start();
//避免客戶端先於服務器啓動前執行代碼
Thread.sleep(100);
//運行客戶端
Client.start();
System.out.println("please input:");
Scanner scanner = new Scanner(System.in);
while (Client.sendMsg(scanner.nextLine()));
scanner.close();
}
}