數據從網卡到應用的過程

歡迎訪問陳同學博客原文

最近看的《網絡是怎樣連接的》非常有趣,真的是 “計算機網絡概論” 圖解趣味版

本文寫寫數據從網卡到應用的過程,內容與圖片很多整理自《網絡是怎樣連接的》、《Tomcat內核設計與剖析》,有的圖片因清晰度不夠我進行了重繪。

總覽

本文圍繞這張圖從下至上展開。假設一個HTTP請求的數據到達網卡,那數據是如何被層層處理併到達應用呢?

網卡

網卡(Network Adapter),也稱網絡適配器,是一個 硬件設備,有全球唯一的 MAC(Media Access Control)地址,MAC地址在網卡生產時就被燒製在ROM中,網卡初始化時恢復到計算機中。

網卡收到的數據是 光信號或電信號,然後將其還原成 數字信息(1和0組成)

下圖是還原的數字信息結構。

根據 FCS(幀校驗序列,Frame Check Sequence) 校驗數據,判斷數據在傳輸過程是否因噪音等影響導致信號失真,從而導致數據錯誤,需要丟棄這種無效的數據包

然後 檢查 數據包中MAC頭部中的 接收方的MAC地址,若不是發給自己,則丟棄數據包;若數據包是發給自己,則將數字信息保存到網卡內部緩衝區。

以上過程網卡自行搞定,不需要CPU參與,CPU也不知道數據包的到達。

網卡驅動

硬件需要驅動程序來控制,就像電腦需要操作系統一樣,而網卡驅動就是CPU控制和使用網卡的程序。

網卡處理完數字信號後,接下來的數據接收需要CPU參與,此時網卡通過中斷將數據包達到的事件通知給CPU。接着,CPU暫停手頭工作,開始用網卡驅動來幹活。

  • 從網卡緩衝區讀取接收到的數據
  • 根據MAC頭部的以太類型字段判斷協議種類並調用處理該協議的軟件(即協議棧)

通常我們接觸的以太類型是 IP協議,因此會調用TCP/IP協議棧來處理。

協議棧

因各層協議看上去像堆疊狀態,也就取名"協議棧"。 像TCP、UDP、IP等協議都是規範,而協議棧則是實現各類協議的網絡控制軟件。例如:Windows、Linux各自對協議進行了實現,因此不同系統之間能夠通訊,與JVM跨平臺原理一致。

IP模塊

當MAC頭部以太類型爲 IP 協議時,網卡驅動數據包交給TCP/IP協議棧來處理。

  • IP模塊會檢查IP頭部以判斷數據是不是發給自己
  • 判斷數據包是否分片,如果分片則緩存起來等待分片全部到達再還原成數據包
  • 根據IP頭部的協議號字段,將包轉給TCP模塊或UDP模塊處理

下面是IP頭部的部分字段。

TCP模塊

下面是TCP報文首部結構。

TCP模塊會根據 標誌位 來進行不同處理,假設服務端收到該報文,會進行如下處理:

  • 如果 SYN=1,表示這是請求連接的包。
    • 首先檢查接收方端口號,然後檢查有沒有與該端口號相同且處於等待連接狀態的套接字
    • 如果沒有,則返回錯誤通知的包;
    • 如果有,則爲這個套接字複製一個新副本,將發送方IP、端口等必要信息寫入套接字,同時分配用於發送緩衝區和接收緩衝區的內存空間。
    • 最後返回給數據客戶端,客戶端會再次確認,這屬於TCP連接三次握手的一部分。
  • 如果是正常數據包,TCP模塊需要檢查該包對應的套接字。然後提取出數據,存放到緩衝區。此時,如果應用程序調用socket的read(),數據就可以轉交給應用程序了。如果應用程序不來獲取數據,數據則一直保存在緩衝區中。

在上述處理過程中,不同協議層會逐層處理,剝洋蔥一樣剔除協議頭部,將數據轉交到上層。而發送數據時,TCP、IP等也會一層層的爲數據包加上頭部。

最後,數據就到應用層,應用層通過socket來操作數據,下面說說socket。

Socket

socket 是什麼

socket 譯爲套接字,由加州大學伯克利分校的研究人員在20世紀80年代早期提出,因此也叫伯克利套接字。其研究人員使得套接字接口適用於任何底層協議,首個實現的協議就是針對TCP/IP。

從不同角度來看,其含義不同:

  • 對應用層來說,可通過 socket 與內核中的網絡協議棧通信。應用不能直接使用協議棧,更不能控制網卡驅動。所以 socket 提供了網絡編程的系統調用接口。
  • 從Linux文件系統來說,socket 是一個打開的文件。下面命令找到的就是一個套接字類型的文件。
$ find / -type s
/run/docker.sock

在普通 Java 應用中的文件描述符文件夾中也可以看到,下圖都是一些指向socket的軟鏈接。

$ cd /proc/25693/fd
$ ls -l | grep socket
  • 從Linux內核來說,socket 是一個通信的端點(Endpoint)。

常說 “連接”,通過C/S兩端的socket真的是建立了一個物理通道嗎?

連接實際上指的是通信雙方的信息,套接字中記錄了這些必要信息。下面是兩個連接信息,包含了通信雙方的IP、端口信息。

因此,套接字也可以認爲是一個概念,它包含了通信雙方的信息。

文件描述符

文件描述符(File Description)簡稱fd,是一個正整數,起到一個文件索引的作用,保存了一個指向文件的指針。

每創建或打開文件都會返回一個fd(一個數字),其實主流操作系統將TCP/UDP連接也當做fd管理,每新建一個連接就會返回一個fd。

創建 socket

應用程序申請創建套接字,具體實現由協議棧來搞定。協議棧首先會分配用於存放一個套接字所需要的內存空間,然後往其中寫入控制信息(雙方IP、port等信息)。套接字剛創建時,數據收發還沒有開始,因此需要寫入初始狀態的控制信息。

接下來,需要將這個套接字的fd告訴應用程序。收到fd後,應用程序再向協議棧委託收發數據時,就要提供fd。

另,服務端在接收數據時,每來一個新連接,都會拷貝當前處於等待連接狀態的socket,然後寫入控制信息,而原先的socket則繼續等待新的連接。

socket 編程 demo

一個最簡單的網絡編程例子。

服務端:

// 創建套接字並綁定到8080端口
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
    // 將套接字設置爲等待連接狀態,並阻塞線程
    Socket socket = serverSocket.accept();
    // 讀數據時,其實要向協議棧提供套接字fd, 讀取數據
    System.out.println(IOUtils.toString(socket.getInputStream(), "utf8"));
}

客戶端

// 創建客戶端套接字(會有fd, 完成TCP握手)
Socket socket = new Socket("127.0.0.1", 8080);
// 使用套接字收發數據
socket.getOutputStream().write("Hello World".getBytes());

Socket 基於內核的回調機制

應用通過socket通信,socket保存了通信雙方信息,相當於一個連接信息。當併發量大時,比如 C10K 即單機1W併發連接,1W連接也對應着1W個fd。那如何判斷哪些連接有新數據呢?

  • 傳統IO模型一個連接一個線程處理,然後遍歷連接,CPU光遍歷連接就已經滿負荷。耗費大量線程,頻繁切換線程環境,只適合低併發應用。
  • 進一步,可以一個線程管理多個socket(也就是多個fd),這也是NIO中的select機制。雖然降低了線程數,提高了併發能力,但遍歷的瓶頸一直都在。
  • 問題最終由異步機制搞定。linux內核2.6版本提出了新的多路複用機制 epoll,套接字提供了回調函數,內核從網卡讀取數據後就會回調該函數。

下面是《Tomcat內核設計與剖析》的一張圖。

  • 首先,應用告訴內核對每個套接字感興趣的事件,例如:socket1的讀事件。
  • 當客戶端發送數據過來時,內核從網卡讀取數據,然後調用socket1回調函數,將socket1作爲可讀事件加入事件列表
  • 應用層獲取讀寫事件列表時,拿到的就是實際可讀寫的事件,沒有無效數據。

這種回調機制的最終目的就是避免無效工作,提高處理效率。

小結

應用層開發人員,比如Java工程師,更多的會好奇數據如何從Tomcat到Servlet,這個過程這些應用層框架是如何處理數據的。

信息技術迅猛發展的這些年,雖然應用層技術更新很快,但這些底層設施一直非常經久耐用,非常經典。所以多學習些Linux基礎、計算機網絡等基礎知識,也大有裨益。


歡迎關注公衆號 [陳一樂],一起學習,一起成長

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章