亚洲在线久爱草,狠狠天天香蕉网,天天搞日日干久草,伊人亚洲日本欧美

首頁 慕課教程 網絡編程入門教程 網絡編程入門教程 為什么需要非阻塞 Java Socket 編程

為什么需要非阻塞 Java Socket 編程

1. 前言

前面小節介紹的都是阻塞式 Socket 編程。比如,我們最早編寫的 TCP Client/Server 示例程序,客戶端定時發送消息,服務器只是做一個響應。由于只是服務一個客戶端,所以通過阻塞式 Socket 編程勉強能滿足需求。

在 Java 服務器多線程一節,我們介紹了每線程模型線程池模型。通過這兩種多線程模型,服務器可以同時和多個客戶端完成通信。對于每線程模型來說,其核心是為每一個新連接創建一個獨立的子線程,由這個獨立的子線程負責和客戶端完成數據收發。每線程模型的特點是服務器上的線程和客戶端是一對一的。這種解決方案是有很大弊端的,因為系統能夠創建的線程數量是有限的,是無法支撐高并發場景的。對于線程池模型來說,盡管限制了創建的線程的總數,但是由于是阻塞式 Socket,一旦某個線程被分配和客戶端通信,就只能和此客戶端通信,所以在容量上有限制。

對于高并發的應用場景來說,還是得通過非阻塞式 Socket 編程來解決。

首先我們了解一下阻塞式非阻塞式的區別。

2. 阻塞式與非阻塞式模型

我們以 Linux 系統為例,介紹阻塞式非阻塞式的概念。Linux 程序的執行模式分為用戶態內核態,應用程序邏輯運行在用戶態,訪問系統資源的邏輯運行在內核態。其實現代操作系統都是這種模式。

當程序的執行邏輯從用戶態切換到內核態時,會引發上下文的切換,會涉及到數據從用戶態內核態,或者是從內核態用戶態拷貝的問題。這時,系統 API 會提供阻塞式非阻塞式兩種調用方式。比如,我們調用 recv 函數接收 Socket 數據,recv 函數可以選擇阻塞式或者是非阻塞式調用模式,不同的模式,編程風格是完全不同。假如 Socket 的接收緩沖區沒有準備好要接收的數據,如果選擇阻塞式調用,那么應用線程會被阻塞在 recv 調用上,不能繼續執行,線程會處于等待狀態,直到系統準備好數據;如果選擇非阻塞式調用,那么應用線程不會被阻塞,recv 函數會立即返回。當系統準備好數據以后,會觸發一個讀事件,這就要求我們必須通過某種機制監聽讀事件,一般都是通過 I/O 多路復用機制來解決。

我們通過兩張圖來感受一下阻塞式非阻塞式的差異。

阻塞式:

圖片描述

非阻塞式:
圖片描述

從以上兩張圖可以看出,如果 read 函數采用阻塞式調用 ,當內核沒有準備好的數據時,應用線程會被阻塞到 read 調用上,進入等待狀態,直到有數據可以讀取才返回。如果 read 函數采用非阻塞式調用,當內核沒有準備好數據時,read 函數會返回 EAGAIN,線程不會被阻塞。當系統準備好數據以后,會觸發一個讀事件

對于邏輯比較簡單的場景,比如邏輯簡單的客戶端程序,可以采用阻塞式編程模型,這樣實現簡單,容易理解。對于邏輯比較復雜的場景,比如高性能服務器,必須采用非阻塞式編程模型,而且要配合 I/O 多路復用機制。

下來我們就介紹一下如何進行非阻塞式 Socket 編程。

3. Java 非阻塞式 Socket 編程

介紹 Java 非阻塞式 Socket 編程,就得介紹 Java NIO。Java NIO 是 Java New IO API,有時也解釋為 Java Non-blocking IO。通過 Java NIO 可以實現 Java 非阻塞 Socket 編程。

Java NIO 是 Java 1.4 支持的,它將 Socket 數據流抽象為一個 Channel(管道),Socket 數據讀寫是通過 Channel
實現的,并且提供了 Buffer 機制,提高數據讀寫的性能。Java NIO 通常用來編寫高性能 Java 服務器程序。在 Java 1.7 以后,Java NIO 對磁盤文件處理得到了增強,可以將 Socket I/O 和 文件 I/O 融合在 Java NIO 中。

Java NIO 提供的新的類結構如下:

類名稱 功能說明
ServerSocketChannel 表示服務端 TCP Socket 的監聽 Channel。ServerSocketChannel 提供的工廠方法 open,用于創建它的實例;同時它提供了 accept 方法用于在服務器中接收新的客戶端連接請求,返回值是 SocketChannel 類的實例。
SocketChannel SocketChannel 表示一個 TCP 通信 Channel,可以通過它的 open 方法創建,也可以通過 ServerSocketChannel 的 accept 方法創建。
Selector Java I/O 事件多路復用機制,用于同時監聽多個 Channel 的讀、寫、監聽事件
SelectionKey 用于表示具體的事件對象
ByteBuffer 通過 SocketChannel 進行數據讀寫,依賴 ByteBuffer

ServerSocketChannel 和 SocketChannel 同時支持阻塞式非阻塞式,默認是阻塞式??梢酝ㄟ^如下的方法,打開非阻塞式。

// 配置監聽 ServerSocketChannel 為非阻塞模式
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);

// 配置服務器新建立的 SocketChannel 為非阻塞模式
SocketChannel newSock = serverChannel.accept();
newSock.configureBlocking(false);
SocketAddress serverAddr = new InetSocketAddress("127.0.0.1", PORT);
SocketChannel sock = SocketChannel.open(serverAddr);
// 配置客戶端 SocketChannel 為非阻塞
sock.configureBlocking(false);

4. 小結

阻塞式 Socket 編程,程序結構簡單,容易編寫,容易理解。但是由于阻塞式 Socket 編程,在調用 recv、send 讀寫數據的時候,會阻塞線程,所以只能適應簡單的應用場景。對于編寫高性能服務器來說,必須采用非阻塞式 Socket 編程。但是非阻塞式 Socket 編程,程序結構要復雜很多,并且不容易理解,要想編寫健壯、穩定的程序不是一件容易的事情。

編寫 Java 非阻塞 Socket 程序,需要采用 Java NIO API,這些 API 的具體功能、具體用法,在后面小節介紹。