透視 Linux 內核:深度剖析 Socket 機制的本質
在 Linux 操作系統構建的網絡世界裏,Socket 宛如縱橫交錯的交通樞紐,承擔着不同應用程序間數據往來的重任。無論是日常瀏覽網頁時,瀏覽器與 Web 服務器間信息的快速交互;還是暢玩網絡遊戲過程中,玩家操作指令與遊戲服務器狀態數據的頻繁傳輸,Socket 都在幕後默默地發揮着關鍵作用。然而,多數開發者在使用 Socket 進行編程時,往往停留在應用層接口的調用層面,對其在內核中的本質運作機制知之甚少。你是否好奇,當你在代碼中輕鬆調用 send、recv 函數實現數據收發時,Linux 內核內部究竟發生了什麼?那些看似簡單的 Socket 連接建立與關閉過程,又涉及到內核哪些複雜的數據結構和精妙算法?
從 Linux 內核的發展歷程來看,Socket 機制並非一蹴而就。早期的 Linux 系統,網絡功能相對簡單,Socket 的設計也較爲基礎。但隨着互聯網的蓬勃發展,網絡應用場景日益複雜多樣,對 Socket 性能、穩定性和兼容性的要求呈指數級增長。這促使 Linux 內核開發者不斷對 Socket 機制進行優化與完善,從數據結構的精心設計,到協議棧交互邏輯的反覆打磨,每一次改進都讓 Socket 能更好地適應複雜多變的網絡環境。如今,Socket 已成爲 Linux 內核網絡子系統中最爲核心且複雜的部分之一,其高效穩定的運行支撐着整個 Linux 網絡生態的繁榮。
接下來,讓我們一同深入 Linux 內核的神祕世界,層層剝開 Socket 機制的 “外衣”,從其底層數據結構、工作流程,到與網絡協議棧的協同運作,全方位、深層次地剖析 Socket 機制的本質,探尋那些隱藏在內核深處,卻又深刻影響着網絡通信效率與可靠性的關鍵技術 。
一、Socket 套接字概述
在網絡編程的廣袤世界裏,Socket(套接字)是一個極爲重要的概念。簡單來說,Socket 是對網絡中不同主機上的應用進程之間進行雙向通信的端點的抽象 。從本質上講,它是應用層與 TCP/IP 協議族通信的中間軟件抽象層,是一組接口,把複雜的 TCP/IP 協議族隱藏在 Socket 接口後面,讓開發者只需面對一組簡單的接口,就能實現網絡通信。就如同我們日常使用電話,無需瞭解電話線路複雜的電路原理和信號傳輸機制,只要拿起聽筒撥號,就能與遠方的人通話,Socket 就是這樣一個方便我們進行網絡通信的 “聽筒”。
Socket 在進程間通信(IPC,Inter - Process Communication)和網絡通信中起着關鍵作用。在本地進程間通信中,我們有管道(PIPE)、命名管道(FIFO)、消息隊列、信號量、共享內存等方式。但當涉及到網絡中的進程通信時,Socket 就成爲了首選工具。網絡中的不同主機,其進程的 PID(進程標識符)在本地雖能唯一標識進程,但在網絡環境下,PID 衝突幾率很大。而 Socket 利用 IP 地址 + 協議 + 端口號的組合,能夠唯一標識網絡中的一個進程,從而巧妙地解決了網絡進程間通信的難題。
比如,我們日常使用的 Web 瀏覽器,當在瀏覽器地址欄輸入網址並回車後,瀏覽器進程就會通過 Socket 向對應的 Web 服務器進程發起連接請求,服務器響應後,雙方通過 Socket 進行數據傳輸,這樣我們就能看到網頁內容了。再如,即時通訊軟件如 QQ、微信,通過 Socket 實現客戶端之間或客戶端與服務器之間的即時消息傳輸;網絡遊戲中,客戶端通過 Socket 連接到遊戲服務器,實現實時的遊戲狀態同步和玩家互動。Socket 就像一座無形的橋樑,跨越網絡的邊界,讓不同主機上的進程能夠順暢地交流。
二、Socket 在 Linux 內核中的地位
2.1Socket 與網絡協議棧的關係
Socket 在 Linux 內核中處於應用層與 TCP/IP 協議棧之間,起着承上啓下的關鍵作用 。從網絡協議棧的角度來看,TCP/IP 協議棧是一個複雜的層次結構,包括網絡接口層、網絡層(IP 層)、傳輸層(TCP、UDP 等)和應用層。而 Socket 就像是一個 “翻譯官”,將應用層的簡單請求 “翻譯” 成 TCP/IP 協議棧能夠理解的指令,同時把協議棧處理後的結果 “翻譯” 迴應用層能夠使用的數據形式。
以常見的 HTTP 請求爲例,當我們在瀏覽器中輸入網址並訪問網頁時,瀏覽器作爲應用層程序,通過 Socket 向 TCP/IP 協議棧發起請求。Socket 首先將請求封裝成符合 TCP 協議格式的數據包,交給傳輸層的 TCP 協議處理。TCP 協議負責建立可靠的連接,進行流量控制和錯誤重傳等操作。然後,數據包被交給網絡層的 IP 協議,IP 協議負責根據目標 IP 地址進行路由選擇,將數據包發送到正確的網絡路徑上。最後,數據包通過網絡接口層到達物理網絡,傳輸到目標服務器。服務器端的 Socket 接收到數據包後,按照相反的流程將數據解包,最終將請求傳遞給 Web 服務器應用程序進行處理。整個過程中,Socket 作爲中間抽象層,隱藏了 TCP/IP 協議棧的複雜性,讓應用程序開發者無需深入瞭解底層協議細節,就能輕鬆實現網絡通信功能。
基於 TCP 協議的客戶端和服務器:
-
服務端和客戶端初始化 socket,得到文件描述符;
-
服務端調用 bind,綁定 IP 地址和端口;
-
服務端調用 listen,進行監聽;
-
服務端調用 accept,等待客戶端連接;
-
客戶端調用 connect,向服務器端的地址和端口發起連接請求;
-
服務端 accept 返回 用於傳輸的 socket 的文件描述符;
-
客戶端調用 write 寫入數據;服務端調用 read 讀取數據;
-
客戶端斷開連接時,會調用 close,那麼服務端 read 讀取數據的時候,就會讀取到了 EOF,待處理完數據後,服務端調用 close,表示連接關閉。
這裏需要注意的是,服務端調用 accept 時,連接成功了會返回一個已完成連接的 socket,後續用來傳輸數據;所以,監聽的 socket 和真正用來傳送數據的 socket,是「兩個」 socket,一個叫作監聽 socket,一個叫作已完成連接 socket;成功連接建立之後,雙方開始通過 read 和 write 函數來讀寫數據,就像往一個文件流裏面寫東西一樣。
2.2 在文件系統中的角色
在 Linux 系統中,一切皆文件,Socket 也不例外。Socket 屬於文件系統的一部分,這一設計理念體現了 Linux 系統的簡潔與統一。每個 Socket 都有一個對應的文件描述符(File Descriptor),文件描述符是一個非負整數,在進程中用於標識打開的文件或 Socket 等 I/O 資源。通過文件描述符,進程可以像操作普通文件一樣對 Socket 進行讀寫操作,這使得 Socket 的操作與其他文件操作具有一致性,大大簡化了編程模型。
例如,在使用 C 語言進行 Socket 編程時,我們可以使用 read 和 write 函數對 Socket 進行數據的接收和發送,就如同對文件進行讀寫操作一樣:
#include <sys/socket.h>
#include <unistd.h>
#include <stdio.h>
int main() {
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("socket creation failed");
return 1;
}
// 假設已經完成Socket的綁定、監聽和連接等操作
char buffer[1024];
ssize_t bytes_read = read(sockfd, buffer, sizeof(buffer));
if (bytes_read > 0) {
buffer[bytes_read] = '\0';
printf("Received data: %s\n", buffer);
} else if (bytes_read == 0) {
printf("Connection closed by peer\n");
} else {
perror("read failed");
}
close(sockfd);
return 0;
}
上述代碼中,read 函數從 Socket 對應的文件描述符 sockfd 中讀取數據,write 函數則用於向 Socket 寫入數據。這種將 Socket 與文件系統統一的設計,使得開發者可以利用熟悉的文件操作函數來處理網絡通信,提高了開發效率和代碼的可維護性。同時,也方便了系統對資源的管理,因爲文件描述符是操作系統管理 I/O 資源的重要方式,Socket 作爲文件系統的一部分,可以納入統一的資源管理體系中。
三、Socket 的本質剖析
3.1 從通信端點角度理解
從通信端點的角度來看,Socket 可以被視爲兩個網絡各自通信連接中的端點 。它是網絡中進程間通信的抽象表示,就像現實生活中,我們通過電話與他人溝通時,電話兩端的聽筒就是通信端點,而 Socket 在網絡通信中就扮演着這樣的 “聽筒” 角色。
以瀏覽器與服務器的通信爲例,當我們在瀏覽器中輸入一個網址,比如 https://www.example.com,瀏覽器會創建一個 Socket 對象,並通過這個 Socket 向服務器的 IP 地址和對應的端口(通常是 80 端口用於 HTTP 協議,443 端口用於 HTTPS 協議)發起連接請求。服務器端也會有一個 Socket 在監聽這個端口,當它接收到客戶端的連接請求後,雙方的 Socket 就建立了一個通信鏈路。在這個鏈路中,客戶端 Socket 將用戶請求的數據(如 HTTP 請求報文)發送出去,服務器端 Socket 接收這些數據並進行處理,然後將處理結果(如 HTTP 響應報文)通過 Socket 返回給客戶端。整個過程中,Socket 作爲通信端點,負責數據的發送和接收,使得瀏覽器和服務器之間能夠實現數據交互,讓我們最終在瀏覽器中看到網頁的內容。
再比如,在即時通訊軟件中,每個客戶端和服務器之間都通過 Socket 建立連接。當用戶 A 發送一條消息給用戶 B 時,用戶 A 的客戶端 Socket 將消息數據發送出去,經過網絡傳輸,到達服務器的 Socket,服務器再將消息轉發到用戶 B 的客戶端 Socket,從而實現消息的傳遞。Socket 就像是網絡世界中各個進程之間溝通的 “大門”,通過它,不同主機上的進程能夠相互交流,實現各種網絡應用的功能。
3.2 基於內核緩衝區的實現
在 Linux 內核中,Socket 本質上是藉助內核緩衝區形成的僞文件 。當應用程序創建一個 Socket 時,內核會爲其分配相應的內核緩衝區,包括髮送緩衝區和接收緩衝區。發送緩衝區用於存儲應用程序要發送的數據,接收緩衝區則用於存儲從網絡中接收到的數據。
從文件操作的角度來看,Socket 與普通文件有很多相似之處。我們可以使用類似文件操作的函數來對 Socket 進行操作,如 read 和 write 函數。當應用程序調用 write 函數向 Socket 寫入數據時,實際上是將數據從應用程序的緩衝區拷貝到 Socket 的發送緩衝區中;而調用 read 函數從 Socket 讀取數據時,是從 Socket 的接收緩衝區中讀取數據到應用程序的緩衝區。這種設計使得 Socket 的操作方式與文件操作方式統一,大大降低了開發者的學習成本和編程難度。
這種基於內核緩衝區的實現方式有諸多優勢。首先,內核緩衝區可以對數據進行緩存,減少了網絡通信的次數,提高了數據傳輸的效率。例如,當應用程序有大量數據要發送時,如果沒有緩衝區,每次都直接發送數據,會導致頻繁的網絡交互,增加網絡開銷。而通過發送緩衝區,應用程序可以將數據先寫入緩衝區,當緩衝區達到一定大小或者滿足一定條件時,再一次性將數據發送出去,這樣就減少了網絡傳輸的次數,提高了傳輸效率。
其次,緩衝區還可以在一定程度上緩解網絡擁塞。當網絡出現擁塞時,數據的傳輸速度會變慢,接收方可能無法及時接收數據。此時,發送緩衝區可以暫時存儲數據,避免數據丟失,等網絡狀況好轉後再繼續發送;接收緩衝區則可以存儲接收到的數據,讓應用程序有足夠的時間來處理這些數據,保證了數據傳輸的穩定性和可靠性。
四、Socket 的類型及設計
4.1Socket 的類型
在 Socket 編程中,不同類型的 Socket 適用於不同的應用場景,它們各自具有獨特的特點和協議基礎。瞭解這些 Socket 類型,對於我們選擇合適的網絡通信方式至關重要。
(1) 流式套接字(SOCK_STREAM)
流式套接字基於 TCP 協議,提供可靠的雙向順序數據流 。在這種類型的 Socket 通信中,數據就像水流一樣,源源不斷且有序地在發送方和接收方之間流動。它具有以下幾個關鍵特點:
-
可靠性:TCP 協議通過一系列機制確保數據的可靠傳輸。例如,它會對發送的數據進行編號,接收方收到數據後會發送確認消息(ACK),如果發送方在一定時間內沒有收到 ACK,就會重發數據,從而保證數據不會丟失。
-
順序性:數據按照發送的順序進行接收,不會出現亂序的情況。這是因爲 TCP 協議在傳輸過程中會對數據進行排序,確保接收方能夠按照正確的順序組裝數據。
-
面向連接:在進行數據傳輸之前,需要先建立連接,就像打電話之前要先撥通對方號碼一樣。連接建立後,雙方纔能進行數據傳輸,傳輸結束後再關閉連接。
以 Web 服務器與客戶端的通信爲例,當我們在瀏覽器中輸入網址並訪問網頁時,瀏覽器會創建一個流式套接字,並通過這個套接字向 Web 服務器發起連接請求。服務器接收到請求後,與瀏覽器建立 TCP 連接。在這個連接上,瀏覽器向服務器發送 HTTP 請求報文,服務器處理請求後,將 HTTP 響應報文通過相同的連接返回給瀏覽器。由於流式套接字的可靠性和順序性,瀏覽器能夠完整、正確地接收到服務器返回的網頁數據,從而正常顯示網頁內容。
(2) 數據報套接字(SOCK_DGRAM)
數據報套接字基於 UDP 協議,提供雙向的數據傳輸,但不保證數據傳輸的可靠性 。與流式套接字相比,它具有以下特點:
-
不可靠性:UDP 協議不保證數據一定能到達目標,也不保證數據的順序和完整性。數據在傳輸過程中可能會丟失、重複或亂序,這是因爲 UDP 沒有像 TCP 那樣的確認和重傳機制。
-
無連接:在數據傳輸前不需要建立連接,就像寄信一樣,直接把信(數據)發送出去即可,不需要事先通知對方。這種方式使得數據報套接字的傳輸效率較高,因爲省去了建立和拆除連接的開銷。
-
固定長度數據傳輸:每個 UDP 數據報都有一個固定的最大長度,超過這個長度的數據需要分割成多個數據報進行傳輸。
以視頻通話應用爲例,視頻通話需要實時傳輸大量的視頻和音頻數據。由於對實時性要求很高,如果採用可靠性高但傳輸延遲較大的 TCP 協議,可能會導致畫面卡頓、聲音延遲等問題。而 UDP 協議的低延遲特性更適合視頻通話場景,雖然可能會丟失一些數據,但只要丟失的數據量在可接受範圍內,視頻和音頻仍然可以正確解析,不會對通話質量產生太大影響。在視頻通話過程中,發送方通過數據報套接字將視頻和音頻數據以 UDP 數據報的形式發送出去,接收方接收到數據後進行實時播放,即使有少量數據丟失,也能通過一些算法進行補償,保證視頻和音頻的流暢播放。
(3) 原始套接字(SOCK_RAW)
原始套接字允許進程直接訪問底層協議,這使得它在網絡協議開發、網絡測試等場景中發揮着重要作用 。與流式套接字和數據報套接字不同,原始套接字可以讀寫內核沒有處理的 IP 數據包,開發者可以通過它來實現自定義的網絡協議,或者對網絡數據包進行更深入的分析和處理。
-
網絡協議開發:在開發新的網絡協議時,原始套接字是必不可少的工具。開發者可以利用它直接操作 IP 數據包,實現新協議的各種功能。例如,假設要開發一種新的物聯網通信協議,就可以通過原始套接字來構建和發送符合該協議格式的 IP 數據包,同時接收和解析來自其他設備的數據包,進行協議的測試和驗證。
-
網絡測試與診斷:在網絡測試和故障診斷中,原始套接字可以幫助我們獲取更詳細的網絡信息。比如,使用 ping 命令時,實際上就是利用原始套接字發送 ICMP(Internet Control Message Protocol)回顯請求報文,並接收 ICMP 回顯應答報文,以此來測試網絡的連通性。再如,在網絡安全領域,通過原始套接字可以捕獲和分析網絡中的數據包,檢測是否存在異常流量或攻擊行爲。
4.2Socket 的設計
現在我們拋開 socket,重新設計一個內核網絡傳輸功能。我們想要將數據從 A 電腦的某個進程發到 B 電腦的某個進程,從操作上來看,就是發數據給遠端和從遠端接收數據,也就是寫數據和讀數據。
但這裏有兩個問題:
-
接收端和發送端可能不止一個,因此需要用 IP 和端口做區分,IP 用來定位是哪臺電腦,端口用來定位是這臺電腦上的哪個進程。
-
發送端和接收端的傳輸方式有很多區別,如可靠的 TCP 協議、不可靠的 UDP 協議,甚至還需要支持基於 icmp 協議的 ping 命令。
爲了支持這些功能,需要定義一個數據結構 sock,在 sock 里加入 IP 和端口字段。這些協議雖然各不相同,但有一些功能相似的地方,可以將不同的協議當成不同的對象類(或結構體),將公共的部分提取出來,通過 “繼承” 的方式複用功能。於是,定義了一些數據結構:sock 是最基礎的結構,維護一些任何協議都有可能會用到的收發數據緩衝區。
在 Linux 內核 2.6 相關的源碼中,sock 結構體的定義可能類似於:
struct sock {
// 相關字段
struct sk_buff_head sk_receive_queue; // 接收數據緩衝區
struct sk_buff_head sk_write_queue; // 發送數據緩衝區
// 其他可能的字段
};
inet_sock 特指用了網絡傳輸功能的 sock,在 sock 的基礎上還加入了 TTL、端口、IP 地址這些跟網絡傳輸相關的字段信息。比如 Unix domain socket,用於本機進程之間的通信,直接讀寫文件,不需要經過網絡協議棧。
可能的定義:
struct inet_sock {
struct sock sk; // 繼承自 sock
__be32 port; // 端口
__be32 saddr; // IP 地址
// 其他相關字段
};
inet_connection_sock 是指面向連接的 sock,在 inet_sock 的基礎上加入面向連接的協議裏相關字段,比如 accept 隊列、數據包分片大小、握手失敗重試次數等。雖然現在提到面向連接的協議就是指 TCP,但設計上 Linux 需要支持擴展其他面向連接的新協議。
例如:
struct inet_connection_sock {
struct inet_sock inet; // 繼承自 inet_sock
struct request_sock_queue accept_queue; // accept 隊列
// 其他相關字段
};
tcp_sock 就是正兒八經的 TCP 協議專用的 sock 結構,在 inet_connection_sock 基礎上還加入了 TCP 特有的滑動窗口、擁塞避免等功能。同樣 UDP 協議也會有一個專用的數據結構,叫 udp_sock。
大概如下:
struct tcp_sock {
struct inet_connection_sock icsk; // 繼承自 inet_connection_sock
// TCP 特有的字段,如滑動窗口、擁塞避免等相關字段
};
有了這套數據結構,將它跟硬件網卡對接一下,就實現了網絡傳輸的功能。
4.3 提供 Socket 層
由於這裏面的代碼複雜,還操作了網卡硬件,需要較高的操作系統權限,再考慮到性能和安全,於是將它放在操作系統內核裏。
爲了讓用戶空間的應用程序使用這部分功能,將這部分功能抽象成簡單的接口,將內核的 sock 封裝成文件。創建 sock 的同時也創建一個文件,文件有個文件描述符 fd,通過它可以唯一確定是哪個 sock。將 fd 暴露給用戶,用戶就可以像操作文件句柄那樣去操作這個 sock 。
struct file{
//文件相關的字段
.....
void *private_data; //指向sock
}
創建 socket 時,其實就是創建了一個文件結構體,並將 private_data 字段指向 sock。有了 sock_fd 句柄後,提供了一些接口,如 send()、recv()、bind()、listen()、connect() 等,這些就是 socket 提供出來的接口。所以說,socket 其實就是個代碼庫或接口層,它介於內核和應用程序之間,提供了一堆接口,讓我們去使用內核功能,本質上就是一堆高度封裝過的接口。
我們平時寫的應用程序裏代碼裏雖然用了 socket 實現了收發數據包的功能,但其 實真正執行網絡通信功能的,不是應用程序,而是 linux 內核。
在操作系統內核空間裏,實現網絡傳輸功能的結構是 sock,基於不同的協議和應用場景,會被泛化爲各種類型的 xx_sock,它們結合硬件,共同實現了網絡傳輸功能。爲了將這部分功能暴露給用戶空間的應用程序使用,於是引入了 socket 層,同時將 sock 嵌入到文件系統的框架裏,sock 就變成了一個特殊的文件,用戶就可以在用戶空間使用文件句柄,也就是 socket_fd 來操作內核 sock 的網絡傳輸能力。
五、Socket 的工作機制
5.1 創建與初始化
當應用程序需要進行網絡通信時,首先會調用 socket 函數來創建一個套接字 。以 C 語言爲例,socket 函數的原型如下:
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
其中,domain 參數指定協議族,如 AF_INET 表示 IPv4 協議族,AF_INET6 表示 IPv6 協議族;type 參數指定套接字類型,如 SOCK_STREAM 表示流式套接字,SOCK_DGRAM 表示數據報套接字;protocol 參數通常設置爲 0,表示使用默認協議。
當應用程序調用 socket 函數時,內核會爲該套接字分配相應的資源,包括內存空間和文件描述符 。內核會在內存中創建一個套接字結構體,用於存儲與該套接字相關的控制信息,如套接字的狀態、連接的對端地址和端口、發送和接收緩衝區等。同時,內核會爲套接字分配一個唯一的文件描述符,並將該文件描述符返回給應用程序。應用程序通過這個文件描述符來標識和操作該套接字,就像通過文件描述符操作普通文件一樣。
5.2 連接建立(僅針對面向連接的套接字)
以 TCP 協議的流式套接字爲例,連接建立需要通過三次握手來完成 。三次握手的過程如下:
-
第一次握手:客戶端向服務器發送一個 SYN(同步)報文段,該報文段中包含客戶端的初始序列號(Sequence Number,簡稱 Seq),假設爲 x 。此時,客戶端進入 SYN_SENT 狀態,等待服務器的響應。這個過程就好比客戶端給服務器打電話說:“我想和你建立連接,這是我的初始序號 x”。
-
第二次握手:服務器接收到客戶端的 SYN 報文段後,會回覆一個 SYN-ACK(同步確認)報文段 。該報文段中包含服務器的初始序列號,假設爲 y,同時 ACK(確認)字段的值爲 x + 1,表示服務器已經收到客戶端的 SYN 報文段,並且確認號爲客戶端的序列號加 1。此時,服務器進入 SYN_RCVD 狀態。這就像是服務器接起電話迴應客戶端:“我收到你的連接請求了,這是我的初始序號 y,我確認收到了你的序號 x”。
-
第三次握手:客戶端接收到服務器的 SYN-ACK 報文段後,會發送一個 ACK 報文段給服務器 。該報文段的 ACK 字段的值爲 y + 1,表示客戶端已經收到服務器的 SYN-ACK 報文段,並且確認號爲服務器的序列號加 1。此時,客戶端進入 ESTABLISHED 狀態,服務器接收到 ACK 報文段後也進入 ESTABLISHED 狀態,連接建立成功。這相當於客戶端再次迴應服務器:“我收到你的回覆了,連接建立成功,我們可以開始通信了”。
三次握手的作用在於確保雙方的通信能力正常,並且能夠同步初始序列號,爲後續的數據傳輸建立可靠的基礎 。通過三次握手,客戶端和服務器都能確認對方可以正常接收和發送數據,避免了舊連接請求的干擾,保證了連接的唯一性和正確性。
5.3 數據傳輸
在數據傳輸階段,發送端和接收端的數據流動過程如下:
-
發送端:應用程序調用 write 或 send 函數將數據發送到 Socket 。這些函數會將應用程序緩衝區中的數據拷貝到 Socket 的發送緩衝區中。然後,內核會根據 Socket 的類型和協議,對數據進行封裝。對於 TCP 套接字,數據會被分割成 TCP 段,並添加 TCP 頭部,包括源端口、目標端口、序列號、確認號等信息;對於 UDP 套接字,數據會被封裝成 UDP 數據報,並添加 UDP 頭部,包含源端口和目標端口。接着,數據會被傳遞到網絡層,添加 IP 頭部,包含源 IP 地址和目標 IP 地址,形成 IP 數據包。最後,IP 數據包通過網絡接口層發送到物理網絡上。
-
接收端:數據從物理網絡進入接收端的網絡接口層 。網絡接口層接收到 IP 數據包後,會進行解包,將 IP 頭部去除,然後將數據傳遞到網絡層。網絡層根據 IP 頭部中的目標 IP 地址,判斷該數據包是否是發給本機的。如果是,則去除 IP 頭部,將數據傳遞到傳輸層。傳輸層根據協議類型(TCP 或 UDP),對數據進行相應的處理。對於 TCP 數據,會檢查序列號和確認號,進行流量控制和錯誤重傳等操作;對於 UDP 數據,直接去除 UDP 頭部,將數據傳遞到 Socket 的接收緩衝區。最後,應用程序調用 read 或 recv 函數從 Socket 的接收緩衝區中讀取數據到應用程序緩衝區中,完成數據的接收。
5.4 連接關閉
對於 TCP 連接,關閉過程需要通過四次揮手來完成 。四次揮手的過程如下:
-
第一次揮手:主動關閉方(可以是客戶端或服務器)發送一個 FIN(結束)報文段,表示自己已經沒有數據要發送了,準備斷開連接 。此時,主動關閉方進入 FIN_WAIT_1 狀態。這就像一方說:“我這邊數據發完了,準備斷開連接”。
-
第二次揮手:被動關閉方接收到 FIN 報文段後,會發送一個 ACK 確認報文段,表示已收到主動關閉方的斷開請求,並同意斷開連接 。但此時被動關閉方可能還沒有完成數據處理,它需要繼續處理緩衝區中的數據。此時,被動關閉方進入 CLOSE_WAIT 狀態,主動關閉方接收到 ACK 報文段後進入 FIN_WAIT_2 狀態。相當於另一方迴應:“我收到你的斷開請求了,等我處理完數據就斷開”。
-
第三次揮手:當被動關閉方完成數據處理後,它會向主動關閉方發送一個 FIN 報文段,表示自己的數據也已經發送完畢,準備關閉連接 。此時,被動關閉方進入 LAST_ACK 狀態。即另一方說:“我數據處理完了,現在可以斷開了”。
-
第四次揮手:主動關閉方收到被動關閉方的 FIN 報文段後,會發送一個 ACK 確認報文段,確認接收到了被動關閉方的斷開請求 。此時,主動關閉方進入 TIME_WAIT 狀態,等待一段時間(通常爲 2 倍的最大報文段生存時間,即 2MSL)後,自動進入 CLOSE 狀態,連接完全關閉。被動關閉方收到 ACK 報文段後,直接進入 CLOSE 狀態。這一步就像是最初發起斷開的一方迴應:“我確認收到你的斷開請求,我們可以徹底斷開了”。
之所以需要四次揮手來確保連接的可靠關閉,是因爲 TCP 連接是全雙工的,每個方向都必須單獨關閉 。在第一次揮手中,主動關閉方只是表示自己不再發送數據,但仍可以接收數據;被動關閉方發送 ACK 確認後,還需要時間處理剩餘數據,處理完後再發送 FIN 報文表示自己也不再發送數據。通過四次揮手,雙方都能確認對方已經完成數據傳輸,並且所有數據都已被正確接收,從而保證了連接關閉的可靠性,避免數據丟失或不完全傳輸。
六、Socket 在 Linux 系統中的應用實例
6.1 簡單的 TCP 服務器與客戶端程序
下面是一個使用 C 語言編寫的簡單 TCP 服務器和客戶端程序示例,通過這個示例,我們可以更直觀地瞭解 Socket 在實際應用中的使用方法。
TCP 服務器代碼(server.c):
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#define PORT 8888
#define MAX_CONNECTIONS 5
#define BUFFER_SIZE 1024
int main() {
// 創建套接字
int server_socket = socket(AF_INET, SOCK_STREAM, 0);
if (server_socket == -1) {
perror("Socket creation failed");
exit(EXIT_FAILURE);
}
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = INADDR_ANY;
// 綁定套接字到指定地址和端口
if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("Bind failed");
close(server_socket);
exit(EXIT_FAILURE);
}
// 監聽連接請求
if (listen(server_socket, MAX_CONNECTIONS) == -1) {
perror("Listen failed");
close(server_socket);
exit(EXIT_FAILURE);
}
printf("Server is listening on port %d...\n", PORT);
while (1) {
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
// 接受客戶端連接請求
int client_socket = accept(server_socket, (struct sockaddr *)&client_addr, &client_addr_len);
if (client_socket == -1) {
perror("Accept failed");
continue;
}
printf("Client connected.\n");
char buffer[BUFFER_SIZE] = {0};
// 接收客戶端發送的數據
ssize_t bytes_read = recv(client_socket, buffer, sizeof(buffer), 0);
if (bytes_read > 0) {
buffer[bytes_read] = '\0';
printf("Received from client: %s\n", buffer);
// 向客戶端發送響應數據
const char *response = "Message received by server";
ssize_t bytes_sent = send(client_socket, response, strlen(response), 0);
if (bytes_sent == -1) {
perror("Send failed");
}
} else if (bytes_read == 0) {
printf("Client disconnected.\n");
} else {
perror("Receive failed");
}
// 關閉客戶端套接字
close(client_socket);
}
// 關閉服務器套接字
close(server_socket);
return 0;
}
-
socket 函數:創建一個基於 IPv4 的流式套接字(SOCK_STREAM),用於 TCP 通信。
-
bind 函數:將套接字綁定到指定的 IP 地址(INADDR_ANY 表示接受任意 IP 地址的連接)和端口(PORT)。
-
listen 函數:使套接字進入監聽狀態,等待客戶端的連接請求,最大允許同時有 MAX_CONNECTIONS 個連接請求排隊。
-
accept 函數:阻塞等待並接受客戶端的連接請求,返回一個新的套接字 client_socket,用於與該客戶端進行通信。
-
recv 函數:從客戶端套接字接收數據,存儲在 buffer 中。
-
send 函數:向客戶端套接字發送響應數據。
TCP 客戶端代碼(client.c):
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#define PORT 8888
#define SERVER_IP "127.0.0.1"
#define BUFFER_SIZE 1024
int main() {
// 創建套接字
int client_socket = socket(AF_INET, SOCK_STREAM, 0);
if (client_socket == -1) {
perror("Socket creation failed");
exit(EXIT_FAILURE);
}
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {
perror("Invalid address/ Address not supported");
close(client_socket);
exit(EXIT_FAILURE);
}
// 連接到服務器
if (connect(client_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("Connect failed");
close(client_socket);
exit(EXIT_FAILURE);
}
printf("Connected to server.\n");
const char *message = "Hello, server!";
// 向服務器發送數據
ssize_t bytes_sent = send(client_socket, message, strlen(message), 0);
if (bytes_sent == -1) {
perror("Send failed");
close(client_socket);
exit(EXIT_FAILURE);
}
char buffer[BUFFER_SIZE] = {0};
// 接收服務器返回的數據
ssize_t bytes_read = recv(client_socket, buffer, sizeof(buffer), 0);
if (bytes_read > 0) {
buffer[bytes_read] = '\0';
printf("Received from server: %s\n", buffer);
} else if (bytes_read == 0) {
printf("Server disconnected.\n");
} else {
perror("Receive failed");
}
// 關閉客戶端套接字
close(client_socket);
return 0;
}
-
socket 函數:同樣創建一個基於 IPv4 的流式套接字。
-
inet_pton 函數:將點分十進制的 IP 地址(SERVER_IP)轉換爲網絡字節序的二進制形式,存儲在 server_addr.sin_addr 中。
-
connect 函數:向服務器發起連接請求,連接到指定的 IP 地址和端口。
-
send 函數:向服務器發送數據。
-
recv 函數:接收服務器返回的數據。
通過這兩個程序,我們可以看到 Socket 在 TCP 通信中的基本應用,服務器端監聽端口並處理客戶端的連接和數據請求,客戶端連接到服務器並進行數據的發送和接收。
6.2UDP 數據傳輸示例
下面是一個使用 UDP 協議進行數據傳輸的代碼示例,展示瞭如何發送和接收 UDP 數據報。
UDP 發送端代碼(sender.c):
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#define PORT 9999
#define DEST_IP "127.0.0.1"
#define BUFFER_SIZE 1024
int main() {
// 創建UDP套接字
int sender_socket = socket(AF_INET, SOCK_DGRAM, 0);
if (sender_socket == -1) {
perror("Socket creation failed");
exit(EXIT_FAILURE);
}
struct sockaddr_in dest_addr;
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(PORT);
if (inet_pton(AF_INET, DEST_IP, &dest_addr.sin_addr) <= 0) {
perror("Invalid address/ Address not supported");
close(sender_socket);
exit(EXIT_FAILURE);
}
while (1) {
char buffer[BUFFER_SIZE] = {0};
printf("Enter message to send (or 'exit' to quit): ");
fgets(buffer, sizeof(buffer), stdin);
buffer[strcspn(buffer, "\n")] = '\0';
if (strcmp(buffer, "exit") == 0) {
break;
}
// 發送UDP數據報
ssize_t bytes_sent = sendto(sender_socket, buffer, strlen(buffer), 0, (struct sockaddr *)&dest_addr, sizeof(dest_addr));
if (bytes_sent == -1) {
perror("Sendto failed");
}
}
// 關閉套接字
close(sender_socket);
return 0;
}
-
socket 函數:創建一個基於 IPv4 的數據報套接字(SOCK_DGRAM),用於 UDP 通信。
-
inet_pton 函數:將目標 IP 地址(DEST_IP)轉換爲網絡字節序的二進制形式,存儲在 dest_addr.sin_addr 中。
-
sendto 函數:向指定的目標地址(dest_addr)發送 UDP 數據報,數據存儲在 buffer 中。
UDP 接收端代碼(receiver.c):
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#define PORT 9999
#define BUFFER_SIZE 1024
int main() {
// 創建UDP套接字
int receiver_socket = socket(AF_INET, SOCK_DGRAM, 0);
if (receiver_socket == -1) {
perror("Socket creation failed");
exit(EXIT_FAILURE);
}
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = INADDR_ANY;
// 綁定套接字到指定地址和端口
if (bind(receiver_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("Bind failed");
close(receiver_socket);
exit(EXIT_FAILURE);
}
printf("Receiver is listening on port %d...\n", PORT);
while (1) {
char buffer[BUFFER_SIZE] = {0};
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
// 接收UDP數據報
ssize_t bytes_read = recvfrom(receiver_socket, buffer, sizeof(buffer), 0, (struct sockaddr *)&client_addr, &client_addr_len);
if (bytes_read > 0) {
buffer[bytes_read] = '\0';
printf("Received from client: %s\n", buffer);
} else if (bytes_read == 0) {
printf("Connection closed.\n");
} else {
perror("Receivefrom failed");
}
}
// 關閉套接字
close(receiver_socket);
return 0;
}
-
socket 函數:創建 UDP 套接字。
-
bind 函數:將套接字綁定到指定的 IP 地址(INADDR_ANY)和端口(PORT),以便接收來自客戶端的數據報。
-
recvfrom 函數:從客戶端接收 UDP 數據報,數據存儲在 buffer 中,並獲取發送端的地址信息(client_addr)。
通過這個 UDP 數據傳輸示例,我們可以看到 UDP 通信的基本流程,發送端將數據報發送到指定的目標地址和端口,接收端在綁定的地址和端口上等待接收數據報。與 TCP 不同,UDP 不需要建立連接,數據報的發送和接收更加簡單直接,但也不保證數據的可靠性和順序性 。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/wvTXvPHFqfLZRFuJTbjgmg