如果面試官問我: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
而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進行關聯??蛻舳藙摻╯ocket非常簡單,只需要調用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
-
多路復用
+關注
關注
0文章
37瀏覽量
25552 -
BIO
+關注
關注
0文章
6瀏覽量
9373
發布評論請先 登錄
相關推薦
評論