# 透視 Linux 內核:Socket 機制的底層架構與運行邏輯深度解析

在由 Linux 操作系統構建的龐大網絡生態中,Socket 作為網絡通信的核心樞紐,承載著不同主機間應用進程的數據交互重任。無論是日常的網頁瀏覽、在線游戲,還是復雜的分布式系統通信,Socket 都在幕后扮演著關鍵角色。盡管多數開發者對 Socket API 的使用駕輕就熟,但對于其在內核中的底層實現機制卻知之甚少。本文將深入 Linux 內核的網絡子系統,從數據結構、工作流程到協議交互等多個維度,全面剖析 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 協議的客戶端和服務器:

  1. 服務端和客戶端初始化 socket,得到文件描述符;
  2. 服務端調用 bind,綁定 IP 地址和端口;
  3. 服務端調用 listen,進行監聽;
  4. 服務端調用 accept,等待客戶端連接;
  5. 客戶端調用 connect,向服務器端的地址和端口發起連接請求;
  6. 服務端 accept 返回 用于傳輸的 socket的文件描述符;
  7. 客戶端調用 write 寫入數據;服務端調用 read 讀取數據;
  8. 客戶端斷開連接時,會調用 close,那么服務端 read 讀取數據的時候,就會讀取到了 EOF,待處理完數據后,服務端調用 close,表示連接關閉。

這里需要注意的是,服務端調用 accept 時,連接成功了會返回一個已完成連接的 socket,后續用來傳輸數據;所以,監聽的 socket 和真正用來傳送數據的 socket,是「兩個」 socket,一個叫作監聽 socket,一個叫作已完成連接 socket;成功連接建立之后,雙方開始通過 read 和 write 函數來讀寫數據,就像往一個文件流里面寫東西一樣。

2.2在文件系統中的角色

秉承 Linux"一切皆文件" 的設計哲學,Socket 被納入文件系統進行統一管理。每個 Socket 都對應一個文件描述符,這使得 Socket 的操作接口與普通文件 I/O 操作保持一致。通過這種設計,開發者可以使用標準的文件操作函數(如 read、write)進行網絡數據的收發,極大地簡化了編程模型,同時也便于系統對網絡資源進行統一管理。

例如,在使用 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 可以理解為網絡通信的邏輯端點。在瀏覽器與 Web 服務器的交互過程中,客戶端 Socket 負責發起連接請求,服務器端 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 電腦的某個進程,從操作上來看,就是發數據給遠端和從遠端接收數據,也就是寫數據和讀數據。

但這里有兩個問題:

  1. 接收端和發送端可能不止一個,因此需要用 IP 和端口做區分,IP 用來定位是哪臺電腦,端口用來定位是這臺電腦上的哪個進程。
  2. 發送端和接收端的傳輸方式有很多區別,如可靠的 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_sockstruct 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 協議的流式套接字為例,連接建立需要通過三次握手來完成 。三次握手的過程如下:

  1. 第一次握手:客戶端向服務器發送一個 SYN(同步)報文段,該報文段中包含客戶端的初始序列號(Sequence Number,簡稱 Seq),假設為 x 。此時,客戶端進入 SYN_SENT 狀態,等待服務器的響應。這個過程就好比客戶端給服務器打電話說:“我想和你建立連接,這是我的初始序號 x”。
  2. 第二次握手:服務器接收到客戶端的 SYN 報文段后,會回復一個 SYN-ACK(同步確認)報文段 。該報文段中包含服務器的初始序列號,假設為 y,同時 ACK(確認)字段的值為 x + 1,表示服務器已經收到客戶端的 SYN 報文段,并且確認號為客戶端的序列號加 1。此時,服務器進入 SYN_RCVD 狀態。這就像是服務器接起電話回應客戶端:“我收到你的連接請求了,這是我的初始序號 y,我確認收到了你的序號 x”。
  3. 第三次握手:客戶端接收到服務器的 SYN-ACK 報文段后,會發送一個 ACK 報文段給服務器 。該報文段的 ACK 字段的值為 y + 1,表示客戶端已經收到服務器的 SYN-ACK 報文段,并且確認號為服務器的序列號加 1。此時,客戶端進入 ESTABLISHED 狀態,服務器接收到 ACK 報文段后也進入 ESTABLISHED 狀態,連接建立成功。這相當于客戶端再次回應服務器:“我收到你的回復了,連接建立成功,我們可以開始通信了”。

三次握手的作用在于確保雙方的通信能力正常,并且能夠同步初始序列號,為后續的數據傳輸建立可靠的基礎 。通過三次握手,客戶端和服務器都能確認對方可以正常接收和發送數據,避免了舊連接請求的干擾,保證了連接的唯一性和正確性。

5.3數據傳輸

在數據傳輸階段,發送端和接收端的數據流動過程如下:

  1. 發送端:應用程序調用write或send函數將數據發送到 Socket 。這些函數會將應用程序緩沖區中的數據拷貝到 Socket 的發送緩沖區中。然后,內核會根據 Socket 的類型和協議,對數據進行封裝。對于 TCP 套接字,數據會被分割成 TCP 段,并添加 TCP 頭部,包括源端口、目標端口、序列號、確認號等信息;對于 UDP 套接字,數據會被封裝成 UDP 數據報,并添加 UDP 頭部,包含源端口和目標端口。接著,數據會被傳遞到網絡層,添加 IP 頭部,包含源 IP 地址和目標 IP 地址,形成 IP 數據包。最后,IP 數據包通過網絡接口層發送到物理網絡上。
  2. 接收端:數據從物理網絡進入接收端的網絡接口層 。網絡接口層接收到 IP 數據包后,會進行解包,將 IP 頭部去除,然后將數據傳遞到網絡層。網絡層根據 IP 頭部中的目標 IP 地址,判斷該數據包是否是發給本機的。如果是,則去除 IP 頭部,將數據傳遞到傳輸層。傳輸層根據協議類型(TCP 或 UDP),對數據進行相應的處理。對于 TCP 數據,會檢查序列號和確認號,進行流量控制和錯誤重傳等操作;對于 UDP 數據,直接去除 UDP 頭部,將數據傳遞到 Socket 的接收緩沖區。最后,應用程序調用read或recv函數從 Socket 的接收緩沖區中讀取數據到應用程序緩沖區中,完成數據的接收。

5.4連接關閉

對于 TCP 連接,關閉過程需要通過四次揮手來完成 。四次揮手的過程如下:

  1. 第一次揮手:主動關閉方(可以是客戶端或服務器)發送一個 FIN(結束)報文段,表示自己已經沒有數據要發送了,準備斷開連接 。此時,主動關閉方進入 FIN_WAIT_1 狀態。這就像一方說:“我這邊數據發完了,準備斷開連接”。
  2. 第二次揮手:被動關閉方接收到 FIN 報文段后,會發送一個 ACK 確認報文段,表示已收到主動關閉方的斷開請求,并同意斷開連接 。但此時被動關閉方可能還沒有完成數據處理,它需要繼續處理緩沖區中的數據。此時,被動關閉方進入 CLOSE_WAIT 狀態,主動關閉方接收到 ACK 報文段后進入 FIN_WAIT_2 狀態。相當于另一方回應:“我收到你的斷開請求了,等我處理完數據就斷開”。
  3. 第三次揮手:當被動關閉方完成數據處理后,它會向主動關閉方發送一個 FIN 報文段,表示自己的數據也已經發送完畢,準備關閉連接 。此時,被動關閉方進入 LAST_ACK 狀態。即另一方說:“我數據處理完了,現在可以斷開了”。
  4. 第四次揮手:主動關閉方收到被動關閉方的 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 1024int 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;
}
  1. socket 函數:創建一個基于 IPv4 的流式套接字(SOCK_STREAM),用于 TCP 通信。
  2. bind 函數:將套接字綁定到指定的 IP 地址(INADDR_ANY 表示接受任意 IP 地址的連接)和端口(PORT)。
  3. listen 函數:使套接字進入監聽狀態,等待客戶端的連接請求,最大允許同時有MAX_CONNECTIONS個連接請求排隊。
  4. accept 函數:阻塞等待并接受客戶端的連接請求,返回一個新的套接字client_socket,用于與該客戶端進行通信。
  5. recv 函數:從客戶端套接字接收數據,存儲在buffer中。
  6. 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 1024int 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;
}
  1. socket 函數:同樣創建一個基于 IPv4 的流式套接字。
  2. inet_pton 函數:將點分十進制的 IP 地址(SERVER_IP)轉換為網絡字節序的二進制形式,存儲在server_addr.sin_addr中。
  3. connect 函數:向服務器發起連接請求,連接到指定的 IP 地址和端口。
  4. send 函數:向服務器發送數據。
  5. 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 1024int 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;
}
  1. socket 函數:創建一個基于 IPv4 的數據報套接字(SOCK_DGRAM),用于 UDP 通信。
  2. inet_pton 函數:將目標 IP 地址(DEST_IP)轉換為網絡字節序的二進制形式,存儲在dest_addr.sin_addr中。
  3. 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 1024int 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;
}
  1. socket 函數:創建 UDP 套接字。
  2. bind 函數:將套接字綁定到指定的 IP 地址(INADDR_ANY)和端口(PORT),以便接收來自客戶端的數據報。
  3. recvfrom 函數:從客戶端接收 UDP 數據報,數據存儲在buffer中,并獲取發送端的地址信息(client_addr)。

通過這個 UDP 數據傳輸示例,我們可以看到 UDP 通信的基本流程,發送端將數據報發送到指定的目標地址和端口,接收端在綁定的地址和端口上等待接收數據報。與 TCP 不同,UDP 不需要建立連接,數據報的發送和接收更加簡單直接,但也不保證數據的可靠性和順序性 。

結語

通過對 Linux 內核中 Socket 機制的深入剖析,我們不僅了解了其底層實現原理,也認識到其在網絡通信中的重要地位。從數據結構設計到協議交互流程,從文件系統集成到應用層接口,Socket 機制的每一個環節都體現了 Linux 內核設計的精妙之處。隨著網絡技術的不斷發展,Socket 機制也在持續演進,為構建更高效、更可靠的網絡應用提供堅實的基礎。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/76813.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/76813.shtml
英文地址,請注明出處:http://en.pswp.cn/web/76813.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

# 利用遷移學習優化食物分類模型:基于ResNet18的實踐

利用遷移學習優化食物分類模型&#xff1a;基于ResNet18的實踐 在深度學習的眾多應用中&#xff0c;圖像分類一直是一個熱門且具有挑戰性的領域。隨著研究的深入&#xff0c;我們發現利用預訓練模型進行遷移學習是一種非常有效的策略&#xff0c;可以顯著提高模型的性能&#…

Excel提取圖片并自動上傳到文件服務器(OOS),獲取文件鏈接

Excel提取圖片并自動上傳到接口 在實際項目中&#xff0c;我們可能經常會遇到需要批量從Excel文件&#xff08;.xlsx&#xff09;中提取圖片并上傳到特定接口的場景。今天&#xff0c;我就詳細介紹一下如何使用Python實現這一功能&#xff0c;本文會手把手教你搭建一個完整的解…

jmeter利用csv進行參數化和自動斷言

1.測試數據 csv測試數據如下&#xff08;以注冊接口為例&#xff09; 2.jemer參數化csv設置 打開 jmeter&#xff0c;添加好線程組、HTTP信息頭管理器、CSV 數據文件設置、注冊請求、響應斷言、查看結果樹 1&#xff09; CSV 數據文件設置 若 CSV 中數據包含中文&#xff0c;…

騰訊云對象存儲m3u8文件使用騰訊播放器播放

參考騰訊云官方文檔&#xff1a; 播放器 SDK Demo 體驗_騰訊云 重要的一步來了&#xff1a; 登錄騰訊云控制臺&#xff0c;找到對象存儲的存儲桶。 此時&#xff0c;再去刷新剛才創建的播放器html文件&#xff0c;即可看到播放畫面了。

CSS 美化頁面(五)

一、position屬性 屬性值??描述??應用場景?static默認定位方式&#xff0c;元素遵循文檔流正常排列&#xff0c;top/right/bottom/left 屬性無效?。普通文檔流布局&#xff0c;默認布局&#xff0c;無需特殊定位。relative相對定位&#xff0c;相對于元素原本位置進行偏…

Spring MVC 核心注解與文件上傳教程

一、RequestBody 注解詳解 1. 基本使用 作用&#xff1a;從 HTTP 請求體中獲取數據&#xff0c;適用于 POST/PUT 請求。 限制&#xff1a;GET 請求無請求體&#xff0c;不可使用該注解。 示例代碼 Controller RequestMapping("/demo01") public class Demo01Cont…

js原型鏈prototype解釋

function Person(){} var personnew Person() console.log(啊啊,Person instanceof Function);//true console.log(,Person.__proto__Function.prototype);//true console.log(,Person.prototype.__proto__ Object.prototype);//true console.log(,Function.prototype.__prot…

為您的照片提供本地 AI 視覺:使用 Llama Vision 和 ChromaDB 構建 AI 圖像標記器

有沒有花 20 分鐘瀏覽您的文件夾以找到心中的特定圖像或屏幕截圖&#xff1f;您并不孤單。 作為工作中的產品經理&#xff0c;我總是淹沒在競爭對手產品的屏幕截圖、UI 靈感以及白板會議或草圖的照片的海洋中。在我的個人生活中&#xff0c;我總是捕捉我在生活中遇到的事物&am…

Kafka消費者端重平衡流程

重平衡的完整流程需要消費者 端和協調者組件共同參與才能完成。我們先從消費者的視角來審視一下重平衡的流程。在消費者端&#xff0c;重平衡分為兩個步驟&#xff1a;分別是加入組和等待領導者消費者&#xff08;Leader Consumer&#xff09;分配方案。這兩個步驟分別對應兩類…

2025年五大ETL數據集成工具推薦

ETL工具作為打通數據孤島的核心引擎&#xff0c;直接影響著企業的決策效率與業務敏捷性。本文精選五款實戰型ETL解決方案&#xff0c;從零門檻的國產免費工具到國際大廠企業級平臺&#xff0c;助您找到最適合的數據集成利器。 一、谷云科技ETLCloud&#xff1a;國產數據集成工…

PageIndex:構建無需切塊向量化的 Agentic RAG

引言 你是否對長篇專業文檔的向量數據庫檢索準確性感到失望&#xff1f;傳統的基于向量的RAG系統依賴于語義相似性而非真正的相關性。但在檢索中&#xff0c;我們真正需要的是相關性——這需要推理能力。當處理需要領域專業知識和多步推理的專業文檔時&#xff0c;相似度搜索常…

ubuntu20.04 遠程桌面Xrdp方式

1&#xff0c;Ubuntu 安裝Xrdp 方法 1.1&#xff0c;安裝xrdp sudo apt install xrdp 1.2&#xff0c;檢查xrdp狀態 sudo systemctl status xrdp 1.3&#xff0c;加入ssl-cert sudo adduser xrdp ssl-cert 1.4&#xff0c;重啟xrdp服務 sudo systemctl restart xrdp 最后…

Java學習手冊:RESTful API 設計原則

一、RESTful API 概述 REST&#xff08;Representational State Transfer&#xff09;即表述性狀態轉移&#xff0c;是一種軟件架構風格&#xff0c;用于設計網絡應用程序。RESTful API 是符合 REST 原則的 Web API&#xff0c;通過使用 HTTP 協議和標準方法&#xff08;GET、…

Spring Boot 核心注解全解:@SpringBootApplication背后的三劍客

大家好呀&#xff01;&#x1f44b; 今天我們要聊一個超級重要的Spring Boot話題 - 那個神奇的主類注解SpringBootApplication&#xff01;很多小伙伴可能每天都在用Spring Boot開發項目&#xff0c;但你真的了解這個注解背后的秘密嗎&#xff1f;&#x1f914; 別擔心&#x…

weibo_har鴻蒙微博分享,單例二次封裝,鴻蒙微博,微博登錄

weibo_har鴻蒙微博分享&#xff0c;單例二次封裝&#xff0c;鴻蒙微博 HarmonyOS 5.0.3 Beta2 SDK&#xff0c;原樣包含OpenHarmony SDK Ohos_sdk_public 5.0.3.131 (API Version 15 Beta2) &#x1f3c6;簡介 zyl/weibo_har是微博封裝使用&#xff0c;支持原生core使用 &a…

tomcat集成redis實現共享session

中間件&#xff1a;Tomcat、Redis、Nginx jar包要和tomcat相匹配 jar包&#xff1a;commons-pool2-2.2.jar、jedis-2.5.2.jar、tomcat-redis-session-manage-tomcat7.jar 配置Tomcat /conf/context.xml <?xml version1.0 encodingutf-8?> <!--Licensed to the A…

JavaScript 擴展Array類方法實現數組求和

題目描述&#xff1a;使用原型對象擴展Array類&#xff0c;實現返回數字型數組的和 <script>const arr [1,2,3,4,5,6]Array.prototype.sum function(){return this.reduce((prev,item)>prev item,0)}console.log(arr.sum())</script>求和函數中this 指向調用…

中間件--ClickHouse-11--部署示例(Linux宿主機部署,Docker容器部署)

一、Linux宿主機部署 1、環境準備 操作系統&#xff1a;推薦使用 CentOS 7/8 或 Ubuntu 18.04/20.04。硬件要求&#xff1a; 至少 2 核 CPU 和 4GB 內存。足夠的磁盤空間&#xff08;根據數據量評估&#xff09;。CPU需支持SSE4.2指令集&#xff08;可通過以下命令檢查&#…

鴻蒙NEXT開發權限工具類(申請授權相關)(ArkTs)

import abilityAccessCtrl, { Permissions } from ohos.abilityAccessCtrl; import { bundleManager, common, PermissionRequestResult } from kit.AbilityKit; import { BusinessError } from ohos.base; import { ToastUtil } from ./ToastUtil;/*** 權限工具類&#xff08;…

LVGL學習(二)(lv_label,lv_btn)

3-1_標簽(lv_label) 一、標簽的組成&#xff08;盒子模型&#xff09;?? 標簽由三個核心模塊構成&#xff0c;類似便簽紙的??分層設計??&#xff1a; ??LV_PART_MAIN&#xff08;主體層&#xff09;?? ??功能??&#xff1a;相當于便簽紙的"紙面"&…