精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

Redis網絡模型有多強?

數據庫 Redis
本文將從BIO開始介紹,經過NIO、多路復用,最終說回Redis的Reactor模型,力求詳盡。本文與其他文章的不同點主要在于:

如果面試官問我:Redis為什么這么快?

我肯定會說:因為Redis是內存數據庫!如果不是直接把數據放在內存里,甭管怎么優化數據結構、設計怎樣的網絡I/O模型,都不可能達到如今這般的執行效率。

但是這么回答多半會讓我直接回去等通知了。。。因為面試官想聽到的就是數據結構和網絡模型方面的回答,雖然這兩者只是在內存基礎上的錦上添花。

說這些并非為了強調網絡模型并不重要,恰恰相反,它是Redis實現高吞吐量的重要底層支撐,是“高性能”的重要原因,卻不是“快”的直接理由。

本文將從BIO開始介紹,經過NIO、多路復用,最終說回Redis的Reactor模型,力求詳盡。本文與其他文章的不同點主要在于:

1、不會介紹同步阻塞I/O、同步非阻塞I/O、異步阻塞I/O、異步非阻塞I/O等概念,這些術語只是對底層原理的一些概念總結而已,我覺得沒有用。底層原理搞懂了,這些概念根本不重要,我希望讀完本文之后,各位能夠不再糾結這些概念。

2、不會只拿生活中例子來說明問題。之前看過特別多的文章,這些文章舉的“燒水”、“取快遞”的例子真的是深入淺出,但是看懂這些例子會讓我們有一種我們真的懂了的錯覺。尤其對于網絡I/O模型而言,很難找到生活中非常貼切的例子,這種例子不過是已經懂了的人高屋建瓴,對外輸出的一種形式,但是對于一知半解的讀者而言卻猶如鈍刀殺人。

牛皮已經吹出去了,正文開始。

1. 一次I/O到底經歷了什么

我們都知道,網絡I/O是通過Socket實現的,在說明網絡I/O之前,我們先來回顧(了解)一下本地I/O的流程。

舉一個非常簡單的例子,下面的代碼實現了文件的拷貝,將file1.txt的數據拷貝到file2.txt中:

public static void main(String[] args) throws Exception {

FileInputStream in = new FileInputStream("/tmp/file1.txt");
FileOutputStream out = new FileOutputStream("/tmp/file2.txt");

byte[] buf = new byte[in.available()];
in.read(buf);
out.write(buf);
}

這個I/O操作在底層到底經歷了什么呢?下圖給出了說明:

圖片

本地I/O示意圖

大致可以概括為如下幾個過程:

  • in.read(buf)?執行時,程序向內核發起 read()系統調用;
  • 操作系統發生上下文切換,由用戶態(User mode)切換到內核態(Kernel mode),把數據讀取到內核緩沖區 (buffer)中;
  • 內核把數據從內核空間拷貝到用戶空間,同時由內核態轉為用戶態;
  • 繼續執行out.write(buf);

再次發生上下文切換,將數據從用戶空間buffer拷貝到內核空間buffer中,由內核把數據寫入文件。

之所以先拿本地I/O舉個例子,是因為我想說明I/O模型并非僅僅針對網絡IO(雖然網絡I/O最常被我們拿來舉例),本地I/O同樣受到I/O模型的約束。比如在這個例子中,本地I/O用的就是典型的BIO,至于什么是BIO,稍安勿躁,接著往下看。

除此之外,通過本地I/O,我還想向各位說明下面幾件事情:

  • 我們編寫的程序本身并不能對文件進行讀寫操作,這個步驟必須依賴于操作系統,換個詞兒就是「內核」;
  • 一個看似簡單的I/O操作卻在底層引發了多次的用戶空間和內核空間的切換,并且數據在內核空間和用戶空間之間拷貝來拷貝去。

不同于本地I/O是從本地的文件中讀取數據,網絡I/O是通過網卡讀取網絡中的數據,網絡I/O需要借助Socket來完成,所以接下來我們重新認識一下Socket。

2. 什么是Socket

這部分在一定程度上是我的強迫癥作祟,我關于文章對知識點講解的完備性上對自己近乎苛刻。我覺得把Socket講明白對接下來的講解是一件很重要的事情,看過我之前的文章的讀者或許能意識到,我盡量避免把前置知識直接以鏈接的形式展示出來,我認為會割裂整篇文章的閱讀體驗。

不割裂的結果就是文章可能顯得很啰嗦,好像一件事情非得從盤古開天辟地開始講起。因此,如果各位覺得對這個知識點有足夠的把握,就直接略過好了~

我們所做的任何需要和遠程設備進行交互的操作,并非是操作軟件本身進行的數據通信。舉個例子就是我們用瀏覽器刷B站視頻的時候,并非是瀏覽器自身向B站請求視頻數據的,而是必須委托操作系統內核中的協議棧。

圖片

網絡I/O

協議棧就是下邊這些書的代碼實現,里邊包含了TCP/IP及其他各種網絡實現細節,這樣解釋應該好理解吧。

圖片

協議棧就是計算機網絡的實現

而Socket庫就是操作系統提供給我們的,用于調用協議棧網絡功能的一堆程序組件的集合,也就是我們平時聽過的操作系統庫函數,Socket庫和協議棧的關系如下圖所示。

圖片

Socket庫和協議棧的關系

用戶進程向操作系統內核的協議棧發出委托時,需要按照指定的順序來調用 Socket 庫中的程序組件。

本文的所有案例都以TCP協議為例進行講解。

大家可以把數據收發想象成在兩臺計算機之間創建了一條數據通道,計算機通過這條通道進行數據收發的雙向操作,當然,這條通道是邏輯上的,并非實際存在。

圖片TCP連接有邏輯通道

數據通過管道流動這個比較好理解,但是問題在于這條管道雖然只是邏輯上存在,但是這個“邏輯”也不是光用腦袋想想就會出現的。就好比我們手機打電話,你總得先把號碼撥出去呀。

對應到網絡I/O中,就意味著雙方必須創建各自的數據出入口,然后將兩個數據出入口像連接水管一樣接通,這個數據出入口就是上圖中的套接字,就是大名鼎鼎的socket。

  • 客戶端和服務端之間的通信可以被概括為如下4個步驟:
  • 服務端創建socket,等待客戶端連接(創建socket階段);
  • 客戶端創建socket,連接到服務端(連接階段);
  • 收發數據(通信階段);

斷開管道并刪除socket(斷開連接)。

每一步都是通過特定語言的API調用Socket庫,Socket庫委托協議棧進行操作的。socket就是調用Socket庫中程序組件之后的產成品,比如Java中的ServerSocket,本質上還是調用操作系統的Socket庫,因此下文的代碼實例雖然采用Java語言,但是希望各位讀者注意:只有語法上抽象與具體的區別,socket的操作邏輯是完全一致的。

但是,我還是得花點口舌啰嗦一下這幾個步驟的一些細節,為了不至于太枯燥,接下來將這4個步驟和BIO一起講解。

3. 阻塞I/O(Blocking I/O,BIO)

我們先從比較簡單的客戶端開始談起。

3.1 客戶端的socket流程

public class BlockingClient {
public static void main(String[] args){

try {
// 創建套接字 & 建立連接
Socket socket = new Socket("localhost", 8099);
// 向服務端寫數據
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
bufferedWriter.write("我是客戶端,收到請回答!!\n");
bufferedWriter.flush();

BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String line = bufferedReader.readLine();
System.out.println("收到服務端返回的數據:" + line);
} catch (IOException e) {
// 錯誤處理
}
}
}

上面展示了一段非常簡單的Java BIO的客戶端代碼,相信你們一定不會感到陌生,接下來我們一點點分析客戶端的socket操作究竟做了什么。

Socket socket = new Socket("localhost", 8099);

雖然只是簡單的一行語句,但是其中包含了兩個步驟,分別是創建套接字、建立連接,等價于下面兩行偽代碼:

<描述符> = socket(<使用IPv4>, <使用TCP>, ...);
connect(<描述符>, <服務器IP地址和端口號>, ...);

注意:

文中會出現多個關于*ocket的術語,比如Socket庫,就是操作系統提供的庫函數;socket組件就是Socket庫中和socket相關的程序的統稱;socket()函數以及socket(或稱:套接字)就是接下來要講的內容,我會盡量在描述過程中不產生混淆,大家注意根據上下文進行辨析。

3.1.1 何為socket?

上文已經說了,邏輯管道存在的前提是需要各自先創建socket(就好比你打電話之前得先有手機),然后將兩個socket進行關聯。客戶端創建socket非常簡單,只需要調用Socket庫中的socket組件的socket()函數就可以了。

<描述符> = socket(<使用IPv4>, <使用TCP>, ...);

客戶端代碼調用socket()?函數向協議棧申請創建socket,協議棧會根據你的參數來決定socket是IPv4?還是IPv6?,是TCP?還是UDP。除此之外呢?

基本的臟活累活都是協議棧完成的,協議棧想傳遞消息總得知道目的IP和端口吧,要是你用的是TCP?協議,你甚至還得記錄每個包的發送時間以及每個包是否收到回復,否則TCP的超時重傳就不會正常工作。。。等等。。。

因此,協議棧會申請一塊內存空間,在其中存放諸如此類的各種控制信息,協議棧就是根據這些控制信息來工作的,這些控制信息我們就可以理解為是socket的實體。怎么樣,是不是之前感覺虛無縹緲的socket突然鮮活了起來?

我們看一個更鮮活的例子,我在本級上執行netstat -anop命令,得到的每一行信息我們就可以理解為是一個socket,我們重點看一下下圖中標注的兩條。

圖片

這兩條都是redis-server?的socket信息,第1條表示redis-server?服務正在IP為127.0.0.1?,端口為6379?的主機上等待遠程客戶端連接,因為Foreign address為0.0.0.0:*?,表示通信還未開始,IP無法確定,因此State為LISTEN?狀態;第2條表示redis-server?服務已經建立了與IP為127.0.0.1?的客戶端之間的連接,且客戶端使用49968?的端口號,目前該socket的狀態為ESTABLISHED。

協議棧創建完socket之后,會返回一個描述符給應用程序。描述符用來識別不同的socket,可以將描述符理解成某個socket的編號,就好比你去洗澡的時候,前臺會發給你一個手牌,原理差不多。

之后對socket進行的任何操作,只要我們出示自己的手牌,啊呸,描述符,協議棧就能知道我們想通過哪個socket進行數據收發了。

圖片

描述符就是socket的號碼牌

至于為什么不直接返回socket的內存地址以及其他細節,可以參考我之前寫的文章《2>&1到底是什么意思》

3.1.2 何為連接?

connect(<描述符>, <服務器IP地址和端口號>, ...);

socket剛創建的時候,里邊沒啥有用的信息,別說自己即將通信的對象長啥樣了,就是叫啥,現在在哪兒也不知道,更別提協議棧,自然是啥也知道!

因此,第1件事情就是應用程序需要把服務器的IP地址和端口號告訴協議棧,有了街道和門牌號,接下來協議棧就可以去找服務器了。

對于服務器也是一樣的情況,服務器也有自己的socket,在接收到客戶端的信息的同時,服務器也得知道客戶端的IP和端口號?啊,要不然只能單線聯系了。因此對客戶端做的第1件事情就有了要求,必須把客戶端自己的IP?以及端口號告知服務器,然后兩者就可以愉快的聊天了。

這就是3次握手。

一句話概括連接的含義:連接實際上是通信的雙方交換控制信息,并將必要的控制信息保存在各自的socket中的過程。

連接過后,每個socket就被4個信息唯一標識,通常我們稱為四元組:

圖片

socket四元組

趁熱打鐵,我們趕緊再說一說服務器端創建socket以及接受連接的過程。

3.2 服務端的socket流程

public class BIOServerSocket {
public static void main(String[] args){
ServerSocket serverSocket = null;

try {
serverSocket = new ServerSocket(8099);
System.out.println("啟動服務:監聽端口:8099");
// 等待客戶端的連接過來,如果沒有連接過來,就會阻塞
while (true) {
// 表示阻塞等待監聽一個客戶端連接,返回的socket表示連接的客戶端信息
Socket socket = serverSocket.accept();
System.out.println("客戶端:" + socket.getPort());
// 表示獲取客戶端的請求報文
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
// 讀操作也是阻塞的
String clientStr = bufferedReader.readLine();
System.out.println("收到客戶端發送的消息:" + clientStr);

BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
bufferedWriter.write("ok\n");
bufferedWriter.flush();
}
} catch (IOException e) {
// 錯誤處理
} finally {
// 其他處理
}
}
}

上面一段是非常簡單的Java BIO的服務端代碼,代碼的含義就是:

  • 創建socket;
  • 將socket設置為等待連接狀態;
  • 接受客戶端連接;
  • 收發數據。

這些步驟調用的底層代碼的偽代碼如下:

// 創建socket
<Server描述符> = socket(<使用IPv4>, <使用TCP>, ...);
// 綁定端口號
bind(<Server描述符>, <端口號等>, ...);
// 設置socket為等待連接狀態
listen(<Server描述符>, ...);
// 接受客戶端連接
<新描述符> = accept(<Server描述符>, ...);
// 從客戶端連接中讀取數據
<讀取的數據長度> = read(<新描述符>, <接受緩沖區>, <緩沖區長度>);
// 向客戶端連接中寫數據
write(<新描述符>, <發送的數據>, <發送的數據長度>);

3.2.1 創建socket

創建socket這一步和客戶端沒啥區別,不同的是這個socket我們稱之為等待連接socket(或監聽socket)。

3.2.2 綁定端口號

bind()?函數會將端口號寫入上一步生成的監聽socket中,這樣一來,監聽socket就完整保存了服務端的IP和端口號。

3.2.3 listen()的真正作用

listen(<Server描述符>, <最大連接數>);

很多小伙伴一定會對這個listen()?有疑問,監聽socket都已經創建完了,端口也已經綁定完了,為什么還要多調用一個listen()呢?

我們剛說過監聽socket和客戶端創建的socket沒什么區別,問題就出在這個沒什么區別上。

socket被創建出來的時候都默認是一個主動socket,也就說,內核會認為這個socket之后某個時候會調用connect()?主動向別的設備發起連接。這個默認對客戶端socket來說很合理,但是監聽socket可不行,它只能等著客戶端連接自己,因此我們需要調用listen()將監聽socket從主動設置為被動,明確告訴內核:你要接受指向這個監聽socket的連接請求!

此外,listen()的第2個參數也大有來頭!監聽socket真正接受的應該是已經完整完成3次握手的客戶端,那么還沒完成的怎么辦?總得找個地方放著吧。于是內核為每一個監聽socket都維護了兩個隊列:

  • 半連接隊列(未完成連接的隊列)

這里存放著暫未徹底完成3次握手的socket(為了防止半連接,這里存放的其實是占用內存極小的request _sock,但是我們直接理解成socket就行了),這些socket的狀態稱為SYN_RCVD。

  • 已完成連接隊列

每個已完成TCP3次握手的客戶端連接對應的socket就放在這里,這些socket的狀態為ESTABLISHED。

文字太多了,有點干,上個圖!

圖片

listen與3次握手

解釋一下動圖中的內容:

  • 客戶端調用connect()?函數,開始3次握手,首先發送一個SYN X?的報文(X是個數字,下同);
  • 服務端收到來自客戶端的SYN?,然后在監聽socket對應的半連接隊列中創建一個新的socket,然后對客戶端發回響應SYN Y?,捎帶手對客戶端的報文給個ACK;
  • 直到客戶端完成第3次握手,剛才新創建的socket就會被轉移到已連接隊列;
  • 當進程調用accept()時,會將已連接隊列頭部的socket返回;如果已連接隊列為空,那么進程將被睡眠,直到已連接隊列中有新的socket,進程才會被喚醒,將這個socket返回。

第4步就是阻塞的本質啊,朋友們!

3.3 答疑時間

Q1.隊列中的對象是socket嗎?

呃。。。乖,咱就把它當成socket就好了,這樣容易理解,其實具體里邊存放的數據結構是啥,我也很想知道,等我寫完這篇文章,我研究完了告訴你。

Q2.accept()這個函數你還沒講是啥意思呢?

accept()?函數是由服務端調用的,用于從已連接隊列中返回一個socket描述符;如果socket為阻塞式的,那么如果已連接隊列為空,accept()進程就會被睡眠。BIO恰好就是這個樣子。

Q3.accept()為什么不直接把監聽socket返回呢?

因為在隊列中的socket經過3次握手過程的控制信息交換,socket的4元組的信息已經完整了,用做socket完全沒問題。

監聽socket就像一個客服,我們給客服打電話,然后客服找到解決問題的人,幫助我們和解決問題的人建立聯系,如果直接把監聽socket返回,而不使用連接socket,就沒有socket繼續等待連接了。

哦對了,accept()返回的socket也有個名字,叫連接socket。

3.4 BIO究竟阻塞在哪里

拿Server端的BIO來說明這個問題,阻塞在了serverSocket.accept()?以及bufferedReader.readLine()這兩個地方。有什么辦法可以證明阻塞嗎?

簡單的很!你在serverSocket.accept();? 的下一行打個斷點,然后debug模式運行BIOServerSocket?,在沒有客戶端連接的情況下,這個斷點絕不會觸發!同樣,在bufferedReader.readLine();下一行打個斷點,在已連接的客戶端發送數據之前,這個斷點絕不會觸發!

readLine()?的阻塞還帶來一個非常嚴重的問題,如果已經連接的客戶端一直不發送消息,readLine()?進程就會一直阻塞(處于睡眠狀態),結果就是代碼不會再次運行到accept()?,這個ServerSocket沒辦法接受新的客戶端連接。

解決這個問題的核心就是別讓代碼卡在readLine()?就可以了,我們可以使用新的線程來readLine()?,這樣代碼就不會阻塞在readLine()上了。

3.5 改造BIO

改造之后的BIO長這樣,這下子服務端就可以隨時接受客戶端的連接了,至于啥時候能read到客戶端的數據,那就讓線程去處理這個事情吧。

public class BIOServerSocketWithThread {
public static void main(String[] args){
ServerSocket serverSocket = null;

try {
serverSocket = new ServerSocket(8099);
System.out.println("啟動服務:監聽端口:8099");
// 等待客戶端的連接過來,如果沒有連接過來,就會阻塞
while (true) {
// 表示阻塞等待監聽一個客戶端連接,返回的socket表示連接的客戶端信息
Socket socket = serverSocket.accept(); //連接阻塞
System.out.println("客戶端:" + socket.getPort());
// 表示獲取客戶端的請求報文
new Thread(new Runnable() {
@Override
public void run(){
try {
BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(socket.getInputStream())
);
String clientStr = bufferedReader.readLine();
System.out.println("收到客戶端發送的消息:" + clientStr);

BufferedWriter bufferedWriter = new BufferedWriter(
new OutputStreamWriter(socket.getOutputStream())
);
bufferedWriter.write("ok\n");
bufferedWriter.flush();
} catch (Exception e) {
//...
}

}
}).start();
}
} catch (IOException e) {
// 錯誤處理
} finally {
// 其他處理
}
}
}

事情的順利進展不禁讓我們飄飄然,我們居然是使用高階的多線程技術解決了BIO的阻塞問題,雖然目前每個客戶端都需要一個單獨的線程來處理,但accept()?總歸不會被readLine()卡死了。

圖片

BIO改造之后

所以我們改造完之后的程序是不是就是非阻塞IO了呢?

想多了。。。我們只是用了點奇技淫巧罷了,改造完的代碼在系統調用層面該阻塞的地方還是阻塞,說白了,Java提供的API完全受限于操作系統提供的系統調用,在Java語言級別沒能力改變底層BIO的事實!

3.6 掀開BIO的遮羞布

接下來帶大家看一下改造之后的BIO代碼在底層都調用了哪一些系統調用,讓我們在底層上對上文的內容加深一下理解。

給大家打個氣,接下來的內容其實非常好理解,大家跟著文章一步步地走,一定能看得懂,如果自己動手操作一遍,那就更好了。

對了,我下來使用的JDK版本是JDK8。

strace是Linux上的一個程序,該程序可以追蹤并記錄參數后邊運行的進程對內核進行了哪些系統調用。

strace -ff -o out java BIOServerSocketWithThread

其中:

  • -o:

將系統調用的追蹤信息輸出到out?文件中,不加這個參數,默認會輸出到標準錯誤stderr。

  • -ff

如果指定了-o?選項,strace?會追蹤和程序相關的每一個進程的系統調用,并將信息輸出到以進程id為后綴的out文件中。舉個例子,比如BIOServerSocketWithThread程序運行過程中有一個ID為30792的進程,那么該進程的系統調用日志會輸出到out.30792這個文件中。

我們運行strace命令之后,生成了很多個out文件。

圖片

這么多進程怎么知道哪個是我們需要追蹤的呢?我就挑了一個容量最大的文件進行查看,也就是out.30792,事實上,這個文件也恰好是我們需要的,截取一下里邊的內容給大家看一下。

圖片

可以看到圖中的有非常多的行,說明我們寫的這么幾行代碼其實默默調用了非常多的系統調用,拋開細枝末節,看一下上圖中我重點標注的系統調用,是不是就是上文中我解釋過的函數?我再詳細解釋一下每一步,大家聯系上文,會對BIO的底層理解的更加通透。

  • 生成監聽socket,并返回socket描述符7?,接下來對socket進行操作的函數都會有一個參數為7;
  • 將8099?端口綁定到監聽socket,bind?的第一個參數就是7,說明就是對監聽socket進行的操作;
  • listen()將監聽socket(參數為7)設置為被動接受連接的socket,并且將隊列的長度設置為50;

實際上就是System.out.println("啟動服務:監聽端口:8099");?這一句的系統調用,只不過中文被編碼了,所以我特意把:8099圈出來證明一下;

額外說兩點:

其一:可以看到,這么一句簡單的打印輸出在底層實際調用了兩次write系統調用,這就是為什么不推薦在生產環境下使用打印語句的原因,多少會影響系統性能;

其二:write()?的第一個參數為1?,也是文件描述符,表示的是標準輸出stdout?,關于標準輸入、標準輸出、標準錯誤和文件描述符之間的關系可以參見《2>&1到底是什么意思》。

系統調用阻塞在了poll()?函數,怎么看出來的阻塞?out文件的每一行運行完畢都會有一個 = 返回值?,而poll()?目前沒有返回值,因此阻塞了。實際上poll()?系統調用對應的Java語句就是serverSocket.accept();。

不對啊?為什么底層調用的不是accept()?而是poll()?poll()?應該是多路復用才是啊。在JDK4之前,底層確實直接調用的是accept()?,但是之后的JDK對這一步進行了優化,除了調用accept()?,還加上了poll()。poll()?的細節我們下文再說,這里可以起碼證明了poll()函數依然是阻塞的,所以整個BIO的阻塞邏輯沒有改變。

接下來我們起一個客戶端對程序發起連接,直接用Linux上的nc程序即可,比較簡單:

nc localhost 8099

發起連接之后(但并未主動發送信息),out.30792的內容發生了變化:

圖片

poll()?函數結束阻塞,程序接著調用accept()?函數返回一個連接socket,該socket的描述符為8;

就是System.out.println("客戶端:" + socket.getPort());的底層調用;

底層使用clone()?創造了一個新進程去處理連接socket,該進程的pid為31168,因此JDK8的線程在底層其實就是輕量級進程;

回到poll()函數繼續阻塞等待新客戶端連接。

由于創建了一個新的進程,因此在目錄下對多出一個out.31168的文件,我們看一下該文件的內容:

圖片

發現子進程阻塞在了recvfrom()?這個系統調用上,對應的Java源碼就是bufferedReader.readLine();,直到客戶端主動給服務端發送消息,阻塞才會結束。

3.7 BIO總結

到此為止,我們就通過底層的系統調用證明了BIO在accept()?以及readLine()上的阻塞。最后用一張圖來結束BIO之旅。

圖片

BIO模型

BIO之所以是BIO,是因為系統底層調用是阻塞的,上圖中的進程調用recv,其系統調用直到數據包準備好并且被復制到應用程序的緩沖區或者發生錯誤為止才會返回,在此整個期間,進程是被阻塞的,啥也干不了。

4. 非阻塞I/O(NonBlocking I/O)

上文花了太多的筆墨描述BIO,接下來的非阻塞IO我們只抓主要矛盾,其余參考BIO即可。

如果你看過其他介紹非阻塞IO的文章,下面這個圖片你多少會有點眼熟。

圖片NIO模型

非阻塞IO指的是進程發起系統調用之后,內核不會將進程投入睡眠,而是會立即返回一個結果,這個結果可能恰好是我們需要的數據,又或者是某些錯誤。

你可能會想,這種非阻塞帶來的輪詢有什么用呢?大多數都是空輪詢,白白浪費CPU而已,還不如讓進程休眠來的合適。

4.1 Java的非阻塞實現

這個問題暫且擱置一下,我們先看Java在語法層面是如何提供非阻塞功能的,細節慢慢聊。

public class NoBlockingServer {

public static List<SocketChannel> channelList = new ArrayList<>();

public static void main(String[] args) throws InterruptedException {

try {
// 相當于serverSocket
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 將監聽socket設置為非阻塞
serverSocketChannel.configureBlocking(false);
serverSocketChannel.socket().bind(new InetSocketAddress(8099));
while (true) {
// 這里將不再阻塞
SocketChannel socketChannel = serverSocketChannel.accept();

if (socketChannel != null) {
// 將連接socket設置為非阻塞
socketChannel.configureBlocking(false);
channelList.add(socketChannel);
} else {
System.out.println("沒有客戶端連接!!!");
}

for (SocketChannel client : channelList) {
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// read也不阻塞
int num = client.read(byteBuffer);
if (num > 0) {
System.out.println("收到客戶端【" + client.socket().getPort() + "】數據:" + new String(byteBuffer.array()));
} else {
System.out.println("等待客戶端【" + client.socket().getPort() + "】寫數據");
}
}

// 加個睡眠是為了避免strace產生大量日志,否則不好追蹤
Thread.sleep(1000);

}

} catch (IOException e) {
e.printStackTrace();
}
}
}

Java提供了新的API,ServerSocketChannel?以及SocketChannel?,相當于BIO中的ServerSocket和Socket。此外,通過下面兩行的配置,將監聽socket和連接socket設置為非阻塞。

// 將監聽socket設置為非阻塞
serverSocketChannel.configureBlocking(false);

// 將連接socket設置為非阻塞
socketChannel.configureBlocking(false);

我們上文強調過,Java自身并沒有將socket設置為非阻塞的本事,一定是在某個時間點上,操作系統內核提供了這個功能,才使得Java設計出了新的API來提供非阻塞功能。

之所以需要上面兩行代碼的顯式設置,也恰好說明了內核是默認將socket設置為阻塞狀態的,需要非阻塞,就得額外調用其他系統調用。我們通過man?命令查看一下socket()這個方法(截圖的中間省略了一部分內容):

man 2 socket

圖片

我們可以看到socket()?函數提供了SOCK_NONBLOCK?這個類型,可以通過fcntl()這個方法將socket從默認的阻塞修改為非阻塞,不管是對監聽socket還是連接socket都是一樣的。

4.2 Java的非阻塞解釋

現在解釋上面提到的問題:這種非阻塞帶來的輪詢有什么用?觀察一下上面的代碼就可以發現,我們全程只使用了1個main線程就解決了所有客戶端的連接以及所有客戶端的讀寫操作。

serverSocketChannel.accept();會立即返回調用結果。

返回的結果如果是一個SocketChannel?對象(系統調用底層就是個socket描述符),說明有客戶端連接,這個SocketChannel?就表示了這個連接;然后利用socketChannel.configureBlocking(false);?將這個連接socket設置為非阻塞。這個設置非常重要,設置之后對連接socket所有的讀寫操作都變成了非阻塞,因此接下來的client.read(byteBuffer);?并不會阻塞while循環,導致新的客戶端無法連接。再之后將該連接socket加入到channelList隊列中。

如果返回的結果為空(底層系統調用返回了錯誤),就說明現在還沒有新的客戶端要連接監聽socket,因此程序繼續向下執行,遍歷channelList?隊列中的所有連接socket,對連接socket進行讀操作。而讀操作也是非阻塞的,會理解返回一個整數,表示讀到的字節數,如果>0,則繼續進行下一步的邏輯處理;否則繼續遍歷下一個連接socket。

下面給出一張accept()返回一個連接socket情況下的動圖,希望對大家理解整個流程有幫助。

非阻塞IO

4.3 掀開非阻塞IO的底褲

我將上面的程序在CentOS下再次用strace程序追蹤一下,具體步驟不再贅述,下面是out日志文件的內容(我忽略了絕大多數沒用的)。

圖片非阻塞IO的系統調用分析

4.4 非阻塞IO總結

圖片

NIO模型

再放一遍這個圖,有一個細節需要大家注意,系統調用向內核要數據時,內核的動作分成兩步:

等待數據(從網卡緩沖區拷貝到內核緩沖區)

拷貝數據(數據從內核緩沖區拷貝到用戶空間)

只有在第1步時,系統調用是非阻塞的,第2步進程依然需要等待這個拷貝過程,然后才能返回,這一步是阻塞的。

非阻塞IO模型僅用一個線程就能處理所有操作,對比BIO的一個客戶端需要一個線程而言進步還是巨大的。但是他的致命問題在于會不停地進行系統調用,不停的進行accept()?,不停地對連接socket進行read()操作,即使大部分時間都是白忙活。要知道,系統調用涉及到用戶空間和內核空間的多次轉換,會嚴重影響整體性能。

所以,一個自然而言的想法就是,能不能別讓進程瞎輪詢。

比如有人告訴進程監聽socket是不是被連接了,有的話進程再執行accept()?;比如有人告訴進程哪些連接socket有數據從客戶端發送過來了,然后進程只對有數據的連接socket進行read()。

這個方案就是I/O多路復用。

責任編輯:武曉燕 來源: 蟬沐風的碼場
相關推薦

2025-10-11 10:04:00

2025-04-24 00:40:00

大數據數字化人工智能

2024-06-05 13:09:26

2013-08-16 11:26:56

2016-07-25 13:26:40

大型機大機IBM

2020-07-08 09:36:03

Kubernetes容器開發

2020-04-12 22:23:45

Kubernetes容器網絡

2020-09-23 14:20:07

Kubernetes容器網絡模型

2022-04-12 08:00:17

socket 編程網絡編程網絡 IO 模型

2020-09-28 12:34:38

Python代碼開發

2021-07-27 22:51:22

互聯網災害技術

2023-02-09 08:57:11

Callable異步java

2019-04-08 08:44:10

TCPIP網絡協議

2021-11-18 23:00:22

Kubernetes容器工具

2022-02-21 10:21:17

網絡IO模型

2022-05-02 18:45:33

Kubernetes網絡模型

2017-09-10 11:17:04

SkyLake

2022-08-29 11:05:03

Wi-Fi 7Wi-Fi 6無線網絡

2025-06-17 00:22:00

點贊
收藏

51CTO技術棧公眾號

激情无码人妻又粗又大| 久久久噜噜噜www成人网| 国产喷水福利在线视频| 一区二区国产在线| 欧美大胆人体bbbb| 五十路熟女丰满大屁股| 国产综合在线观看| 久久99蜜桃精品| 欧美极品少妇全裸体| 黄色性生活一级片| 日本午夜免费一区二区| 亚洲一区影音先锋| 青青草国产精品| 国产乱色精品成人免费视频| 激情欧美日韩一区| 一区二区三区精品99久久| 国产探花在线观看视频| 免费成人动漫| 又紧又大又爽精品一区二区| 久精品国产欧美| 国产精品美女一区| 男人的天堂亚洲在线| 久久视频精品在线| 伊人网在线视频观看| 国产精品亚洲欧美一级在线 | 欧美日韩你懂的| 拔插拔插海外华人免费| 在线观看美女网站大全免费| 成人成人成人在线视频| 成人黄色生活片| 国产一级淫片a视频免费观看| 午夜精品国产| 日韩有码在线电影| 久久av无码精品人妻系列试探| 91av俱乐部| 国产一区二区三区成人| 亚洲一区欧美激情| 欧美成人小视频| 麻豆一区在线观看| 精品精品99| 亚洲男人天堂2024| 欲求不满的岳中文字幕| 老司机亚洲精品一区二区| 在线精品观看国产| 欧美aⅴ在线观看| mm视频在线视频| 一区二区三区精品视频在线| 中文字幕中文字幕一区三区| 高清在线观看av| 久久精品一区二区三区不卡牛牛| 国产一区二区高清不卡| 亚洲精品免费在线观看视频| 寂寞少妇一区二区三区| 成人有码在线视频| 国产又粗又黄视频| 九九在线精品视频| 国产啪精品视频| 亚洲视频一区在线播放| 免费观看一级特黄欧美大片| 国产精品精品久久久久久| 国产一级18片视频| 久久国产66| 欧美日韩久久一区| 精品国产视频在线| 亚洲天堂精品一区| 欧美超碰在线| 久久精品最新地址| 美女毛片在线观看| 亚洲精品美女91| 欧美亚洲另类在线| 午夜精品免费观看| 久久国产精品一区二区| 91精品视频播放| www香蕉视频| 99久久免费精品| 人禽交欧美网站免费| a天堂在线资源| 亚洲免费资源在线播放| 精品无码av无码免费专区| 污污片在线免费视频| 亚洲无人区一区| 国产精品秘入口18禁麻豆免会员| 男人最爱成人网| 欧美日韩视频专区在线播放| 国产亚洲视频一区| 高清精品xnxxcom| 亚洲欧洲一区二区三区在线观看| 国产在线免费av| 国产综合色产| 日本亚洲欧美三级| 一本色道久久综合熟妇| 成人丝袜高跟foot| 青青草原亚洲| 欧美日韩在线视频免费观看| 欧美日韩日本国产| 可以看污的网站| 国内露脸中年夫妇交换精品| 亚洲深夜福利在线| 欧美成人三级视频| 丝袜美腿成人在线| yellow视频在线观看一区二区| 色视频在线观看免费| 国产精品国产自产拍在线| 97超碰国产精品| 日韩av大片站长工具| 欧美一区二区在线看| 欧洲一级黄色片| 女人天堂亚洲aⅴ在线观看| 97av在线影院| av天堂一区二区三区| 久久久久国产成人精品亚洲午夜| 国产精品av免费| 蜜桃视频在线观看播放| 欧美巨大另类极品videosbest| 动漫美女无遮挡免费| 精品久久久久中文字幕小说| 欧美激情在线狂野欧美精品| 中文字幕 欧美激情| 成人国产精品免费| 中文字幕久久综合| 天堂√中文最新版在线| 日韩三级视频在线看| 亚洲色图日韩精品| 国产精品主播| 国产精品视频500部| 好了av在线| 欧美综合视频在线观看| 538国产视频| 欧美三级午夜理伦三级中文幕| 国产精品久久久av| 污污网站在线免费观看| 亚洲一区视频在线观看视频| 国产福利精品一区二区三区| 国产精品免费99久久久| 97精品一区二区视频在线观看| 国产女人18毛片18精品| 国产精品欧美综合在线| 成年人视频网站免费观看| 韩国女主播一区二区三区| 欧美精品中文字幕一区| 国产免费黄色大片| 亚洲欧洲日韩av| 污视频网站观看| 成人在线一区| 国产精品久久中文| 国产视频二区在线观看| 日韩欧美一区二区在线| 香蕉网在线播放| 午夜亚洲性色福利视频| 久久久久久九九九九| 国产极品在线观看| 日韩福利在线播放| 日本中文字幕在线| 久久精品夜色噜噜亚洲a∨| 91黄色小网站| 成人一级毛片| 91免费综合在线| 调教一区二区| 亚洲精品一区二区三区99| 国产亚洲欧美久久久久| 岛国精品在线观看| 日本午夜激情视频| 欧美福利在线播放网址导航| 91国产视频在线| 每日更新av在线播放| 91国偷自产一区二区三区成为亚洲经典| 免费看污片网站| 美国三级日本三级久久99| 五月天亚洲综合情| 欧美成人高潮一二区在线看| 成人自拍av| 在线播放国产精品| 国产精品乱码一区二区| 亚洲精品ww久久久久久p站| 国产精久久久久| 在线精品一区二区| 欧美日韩亚洲一区二区三区在线观看 | 成人午夜免费在线| 九九在线精品| 成人免费观看网址| 2018av在线| 一区二区三区国产在线观看| 国产精品无码久久av| 亚洲电影一级黄| 2019男人天堂| 成人一道本在线| 妓院一钑片免看黄大片| 亚洲成人免费| 免费国产一区二区| 不卡一区视频| 日本午夜精品理论片a级appf发布| 91看片在线观看| 精品国产91洋老外米糕| 五月婷婷激情五月| 亚洲一区二区在线视频| 欧美成人国产精品一区二区| 国产精品亚洲午夜一区二区三区| 欧美 国产 日本| 欧美在线二区| 精品成人一区二区三区四区| 2019日韩中文字幕mv| 亚洲欧美日本伦理| 91香蕉亚洲精品| 性欧美1819sex性高清| 九九久久国产精品| 搞黄视频在线观看| 亚洲福利视频网| 国产又粗又大又黄| 日本高清不卡一区| 久久精品视频9| 亚洲日本丝袜连裤袜办公室| 男女黄床上色视频| 懂色av一区二区在线播放| www.欧美日本| 亚洲一区二区毛片| 国产免费xxx| 日韩欧美伦理| 久久99精品久久久久久久青青日本| 亚洲欧洲专区| 国产精品国产亚洲伊人久久 | 欧美日在线观看| 久久久久性色av无码一区二区| 欧美激情自拍偷拍| 波多野结衣福利| bt欧美亚洲午夜电影天堂| 永久av免费在线观看| 美女视频一区二区三区| 欧美成人精品欧美一级乱| 激情久久综合| 国产亚洲黄色片| 国产精品av久久久久久麻豆网| 制服国产精品| 99久精品视频在线观看视频| 五月天久久狠狠| 教室别恋欧美无删减版| 久久久久资源| 日韩伦理一区二区三区| 国内精品视频免费| 黄色欧美网站| 国产精品视频免费一区二区三区| 日韩免费一级| 97久久夜色精品国产九色| 国产亚洲久久| 亚洲最大福利视频网站| 精品一区二区三区四区五区| 成人精品一区二区三区电影黑人| 国产精品诱惑| 91在线网站视频| 久久国产精品美女| 97超碰资源| 波多野结衣在线一区二区| 国产精品v欧美精品v日韩| 99精品中文字幕在线不卡 | 日本波多野结衣在线| 欧美成人三级在线| 成人爽a毛片一区二区| 精品99久久久久久| 日本不卡视频一区二区| 亚洲欧美另类中文字幕| 国产三级视频在线播放线观看| 国产一区二区黄| 午夜在线免费观看视频| 久久中文久久字幕| 国产乱码在线| 欧美中文字幕视频| 91亚洲精品| 亚洲a级在线观看| 国产精品网在线观看| 久久99精品久久久久久久久久| 国产精品欧美在线观看| 天堂精品视频| 欧美区日韩区| 欧美a在线视频| 美女视频第一区二区三区免费观看网站| 男人添女人下面免费视频| 国内精品写真在线观看| 乱码一区二区三区| 久久午夜色播影院免费高清| 极品尤物一区二区| 亚洲在线中文字幕| 老熟妇仑乱一区二区av| 欧美人伦禁忌dvd放荡欲情| 亚洲h视频在线观看| 亚洲精品一二区| 暖暖日本在线观看| 国内精品久久久| 韩日一区二区| 国产精品视频福利| 四季av在线一区二区三区| 久久99久久99精品| 美女视频黄 久久| 中文字幕在线视频播放| 国产欧美一区二区精品婷婷 | 天天插天天干天天操| 国产99亚洲| 五月天视频一区| 国产中文字幕乱人伦在线观看| 亚洲一区国产一区| 最新天堂在线视频| a美女胸又www黄视频久久| 欧美三级视频网站| 亚洲国产成人91porn| 在线观看毛片av| 日韩高清a**址| caoporn免费在线| 国产成人福利网站| 99a精品视频在线观看| 午夜一区二区三区| 国产精品久久国产愉拍| 日韩精品aaa| 国产亚洲一区字幕| 日本网站在线免费观看| 久久99精品久久久久久久久久久久| 久久综合亚洲社区| 丰满的护士2在线观看高清| 国产精品久久久久久久久久免费 | 精品视频在线一区二区| 91精品国产高清| 欧美久久亚洲| 亚洲精品在线视频观看| 国产欧美高清| 韩国一区二区三区四区| 国产精品国产三级国产aⅴ原创 | 尤物视频在线看| 国产精品激情av在线播放 | 国v精品久久久网| 网站永久看片免费| 日本乱人伦aⅴ精品| 同心难改在线观看| 欧美国产日韩一区二区三区| 欧美一级免费| 欧美一区国产一区| 国产亚洲毛片在线| 怡红院一区二区| 一区二区三区日本| 国产毛片在线视频| 最近中文字幕日韩精品 | free性护士videos欧美| 亚洲一区久久久| 女同性一区二区三区人了人一| 成人免费在线观看视频网站| 国产午夜精品一区二区三区四区 | 鲁大师成人一区二区三区| 国产精品扒开腿做爽爽爽a片唱戏| 亚洲精品国产精华液| 国产高中女学生第一次| 理论片在线不卡免费观看| 亚洲国产91视频| 国产手机视频在线观看| 极品尤物av久久免费看| 天海翼在线视频| 欧美一区二区私人影院日本| 黄色一级片在线观看| 91中文在线视频| 欧美日韩综合| 一区二区免费在线观看视频 | 亚洲中文一区二区三区| 日韩在线观看免费| 国产一区二区三区亚洲综合 | 狂野欧美性猛交xxxxx视频| 色婷婷激情综合| 污污污www精品国产网站| 夜夜亚洲天天久久| 丰满少妇被猛烈进入| 5252色成人免费视频| 蜜桃一区二区| 少妇一级淫免费放| 国产精品狼人久久影院观看方式| 9999在线观看| 亚洲国产1区| 亚洲天堂成人av| 91国偷自产一区二区使用方法| 亚洲视频tv| 亚洲一区二区日本| 国产精品国码视频| 99久久久久久久久久| 欧美性猛片aaaaaaa做受| 超碰在线无需免费| 国产三区二区一区久久| 日韩精品视频网| 黄色片在线观看网站| 亚洲精品狠狠操| 成人精品高清在线视频| 青青视频免费在线| 2024国产精品视频| 一起草av在线| 97视频在线观看免费高清完整版在线观看 | 男人舔女人下部高潮全视频| 欧美人体做爰大胆视频| 国产网站在线| 亚洲欧洲日本国产| 成人免费高清在线观看| 中文字幕 国产| 97精品视频在线观看| 天天久久综合| 爱爱免费小视频| 日韩一区二区高清| 日韩pacopacomama| 亚洲精品国产精品自产a区红杏吧 亚洲精品国产精品乱码不99按摩 亚洲精品国产精品久久清纯直播 亚洲精品国产精品国自产在线 | 大尺度做爰床戏呻吟舒畅|