透視Linux內(nèi)核:深度剖析Socket機(jī)制的本質(zhì)
在Linux操作系統(tǒng)構(gòu)建的網(wǎng)絡(luò)世界里,Socket 宛如縱橫交錯(cuò)的交通樞紐,承擔(dān)著不同應(yīng)用程序間數(shù)據(jù)往來的重任。無論是日常瀏覽網(wǎng)頁時(shí),瀏覽器與 Web 服務(wù)器間信息的快速交互;還是暢玩網(wǎng)絡(luò)游戲過程中,玩家操作指令與游戲服務(wù)器狀態(tài)數(shù)據(jù)的頻繁傳輸,Socket 都在幕后默默地發(fā)揮著關(guān)鍵作用。然而,多數(shù)開發(fā)者在使用 Socket 進(jìn)行編程時(shí),往往停留在應(yīng)用層接口的調(diào)用層面,對(duì)其在內(nèi)核中的本質(zhì)運(yùn)作機(jī)制知之甚少。你是否好奇,當(dāng)你在代碼中輕松調(diào)用send、recv函數(shù)實(shí)現(xiàn)數(shù)據(jù)收發(fā)時(shí),Linux 內(nèi)核內(nèi)部究竟發(fā)生了什么?那些看似簡單的 Socket 連接建立與關(guān)閉過程,又涉及到內(nèi)核哪些復(fù)雜的數(shù)據(jù)結(jié)構(gòu)和精妙算法?
從 Linux 內(nèi)核的發(fā)展歷程來看,Socket 機(jī)制并非一蹴而就。早期的 Linux 系統(tǒng),網(wǎng)絡(luò)功能相對(duì)簡單,Socket 的設(shè)計(jì)也較為基礎(chǔ)。但隨著互聯(lián)網(wǎng)的蓬勃發(fā)展,網(wǎng)絡(luò)應(yīng)用場景日益復(fù)雜多樣,對(duì) Socket 性能、穩(wěn)定性和兼容性的要求呈指數(shù)級(jí)增長。這促使 Linux 內(nèi)核開發(fā)者不斷對(duì) Socket 機(jī)制進(jìn)行優(yōu)化與完善,從數(shù)據(jù)結(jié)構(gòu)的精心設(shè)計(jì),到協(xié)議棧交互邏輯的反復(fù)打磨,每一次改進(jìn)都讓 Socket 能更好地適應(yīng)復(fù)雜多變的網(wǎng)絡(luò)環(huán)境。如今,Socket 已成為 Linux 內(nèi)核網(wǎng)絡(luò)子系統(tǒng)中最為核心且復(fù)雜的部分之一,其高效穩(wěn)定的運(yùn)行支撐著整個(gè) Linux 網(wǎng)絡(luò)生態(tài)的繁榮。
接下來,讓我們一同深入 Linux 內(nèi)核的神秘世界,層層剝開 Socket 機(jī)制的 “外衣”,從其底層數(shù)據(jù)結(jié)構(gòu)、工作流程,到與網(wǎng)絡(luò)協(xié)議棧的協(xié)同運(yùn)作,全方位、深層次地剖析 Socket 機(jī)制的本質(zhì),探尋那些隱藏在內(nèi)核深處,卻又深刻影響著網(wǎng)絡(luò)通信效率與可靠性的關(guān)鍵技術(shù) 。
一、Socket套接字概述
在網(wǎng)絡(luò)編程的廣袤世界里,Socket(套接字)是一個(gè)極為重要的概念。簡單來說,Socket 是對(duì)網(wǎng)絡(luò)中不同主機(jī)上的應(yīng)用進(jìn)程之間進(jìn)行雙向通信的端點(diǎn)的抽象 。從本質(zhì)上講,它是應(yīng)用層與 TCP/IP 協(xié)議族通信的中間軟件抽象層,是一組接口,把復(fù)雜的 TCP/IP 協(xié)議族隱藏在 Socket 接口后面,讓開發(fā)者只需面對(duì)一組簡單的接口,就能實(shí)現(xiàn)網(wǎng)絡(luò)通信。就如同我們?nèi)粘J褂秒娫?,無需了解電話線路復(fù)雜的電路原理和信號(hào)傳輸機(jī)制,只要拿起聽筒撥號(hào),就能與遠(yuǎn)方的人通話,Socket 就是這樣一個(gè)方便我們進(jìn)行網(wǎng)絡(luò)通信的 “聽筒”。
Socket 在進(jìn)程間通信(IPC,Inter - Process Communication)和網(wǎng)絡(luò)通信中起著關(guān)鍵作用。在本地進(jìn)程間通信中,我們有管道(PIPE)、命名管道(FIFO)、消息隊(duì)列、信號(hào)量、共享內(nèi)存等方式。但當(dāng)涉及到網(wǎng)絡(luò)中的進(jìn)程通信時(shí),Socket 就成為了首選工具。網(wǎng)絡(luò)中的不同主機(jī),其進(jìn)程的 PID(進(jìn)程標(biāo)識(shí)符)在本地雖能唯一標(biāo)識(shí)進(jìn)程,但在網(wǎng)絡(luò)環(huán)境下,PID 沖突幾率很大。而 Socket 利用 IP 地址 + 協(xié)議 + 端口號(hào)的組合,能夠唯一標(biāo)識(shí)網(wǎng)絡(luò)中的一個(gè)進(jìn)程,從而巧妙地解決了網(wǎng)絡(luò)進(jìn)程間通信的難題。
圖片
比如,我們?nèi)粘J褂玫?Web 瀏覽器,當(dāng)在瀏覽器地址欄輸入網(wǎng)址并回車后,瀏覽器進(jìn)程就會(huì)通過 Socket 向?qū)?yīng)的 Web 服務(wù)器進(jìn)程發(fā)起連接請求,服務(wù)器響應(yīng)后,雙方通過 Socket 進(jìn)行數(shù)據(jù)傳輸,這樣我們就能看到網(wǎng)頁內(nèi)容了。再如,即時(shí)通訊軟件如 QQ、微信,通過 Socket 實(shí)現(xiàn)客戶端之間或客戶端與服務(wù)器之間的即時(shí)消息傳輸;網(wǎng)絡(luò)游戲中,客戶端通過 Socket 連接到游戲服務(wù)器,實(shí)現(xiàn)實(shí)時(shí)的游戲狀態(tài)同步和玩家互動(dòng)。Socket 就像一座無形的橋梁,跨越網(wǎng)絡(luò)的邊界,讓不同主機(jī)上的進(jìn)程能夠順暢地交流。
二、Socket在Linux內(nèi)核中的地位
2.1Socket與網(wǎng)絡(luò)協(xié)議棧的關(guān)系
Socket 在 Linux 內(nèi)核中處于應(yīng)用層與 TCP/IP 協(xié)議棧之間,起著承上啟下的關(guān)鍵作用 。從網(wǎng)絡(luò)協(xié)議棧的角度來看,TCP/IP 協(xié)議棧是一個(gè)復(fù)雜的層次結(jié)構(gòu),包括網(wǎng)絡(luò)接口層、網(wǎng)絡(luò)層(IP 層)、傳輸層(TCP、UDP 等)和應(yīng)用層。而 Socket 就像是一個(gè) “翻譯官”,將應(yīng)用層的簡單請求 “翻譯” 成 TCP/IP 協(xié)議棧能夠理解的指令,同時(shí)把協(xié)議棧處理后的結(jié)果 “翻譯” 回應(yīng)用層能夠使用的數(shù)據(jù)形式。
以常見的 HTTP 請求為例,當(dāng)我們在瀏覽器中輸入網(wǎng)址并訪問網(wǎng)頁時(shí),瀏覽器作為應(yīng)用層程序,通過 Socket 向 TCP/IP 協(xié)議棧發(fā)起請求。Socket 首先將請求封裝成符合 TCP 協(xié)議格式的數(shù)據(jù)包,交給傳輸層的 TCP 協(xié)議處理。TCP 協(xié)議負(fù)責(zé)建立可靠的連接,進(jìn)行流量控制和錯(cuò)誤重傳等操作。然后,數(shù)據(jù)包被交給網(wǎng)絡(luò)層的 IP 協(xié)議,IP 協(xié)議負(fù)責(zé)根據(jù)目標(biāo) IP 地址進(jìn)行路由選擇,將數(shù)據(jù)包發(fā)送到正確的網(wǎng)絡(luò)路徑上。最后,數(shù)據(jù)包通過網(wǎng)絡(luò)接口層到達(dá)物理網(wǎng)絡(luò),傳輸?shù)侥繕?biāo)服務(wù)器。服務(wù)器端的 Socket 接收到數(shù)據(jù)包后,按照相反的流程將數(shù)據(jù)解包,最終將請求傳遞給 Web 服務(wù)器應(yīng)用程序進(jìn)行處理。整個(gè)過程中,Socket 作為中間抽象層,隱藏了 TCP/IP 協(xié)議棧的復(fù)雜性,讓應(yīng)用程序開發(fā)者無需深入了解底層協(xié)議細(xì)節(jié),就能輕松實(shí)現(xiàn)網(wǎng)絡(luò)通信功能。
圖片
基于 TCP 協(xié)議的客戶端和服務(wù)器:
- 服務(wù)端和客戶端初始化 socket,得到文件描述符;
- 服務(wù)端調(diào)用 bind,綁定 IP 地址和端口;
- 服務(wù)端調(diào)用 listen,進(jìn)行監(jiān)聽;
- 服務(wù)端調(diào)用 accept,等待客戶端連接;
- 客戶端調(diào)用 connect,向服務(wù)器端的地址和端口發(fā)起連接請求;
- 服務(wù)端 accept 返回 用于傳輸?shù)?socket的文件描述符;
- 客戶端調(diào)用 write 寫入數(shù)據(jù);服務(wù)端調(diào)用 read 讀取數(shù)據(jù);
- 客戶端斷開連接時(shí),會(huì)調(diào)用 close,那么服務(wù)端 read 讀取數(shù)據(jù)的時(shí)候,就會(huì)讀取到了 EOF,待處理完數(shù)據(jù)后,服務(wù)端調(diào)用 close,表示連接關(guān)閉。
這里需要注意的是,服務(wù)端調(diào)用 accept 時(shí),連接成功了會(huì)返回一個(gè)已完成連接的 socket,后續(xù)用來傳輸數(shù)據(jù);所以,監(jiān)聽的 socket 和真正用來傳送數(shù)據(jù)的 socket,是「兩個(gè)」 socket,一個(gè)叫作監(jiān)聽 socket,一個(gè)叫作已完成連接 socket;成功連接建立之后,雙方開始通過 read 和 write 函數(shù)來讀寫數(shù)據(jù),就像往一個(gè)文件流里面寫東西一樣。
2.2在文件系統(tǒng)中的角色
在 Linux 系統(tǒng)中,一切皆文件,Socket 也不例外。Socket 屬于文件系統(tǒng)的一部分,這一設(shè)計(jì)理念體現(xiàn)了 Linux 系統(tǒng)的簡潔與統(tǒng)一。每個(gè) Socket 都有一個(gè)對(duì)應(yīng)的文件描述符(File Descriptor),文件描述符是一個(gè)非負(fù)整數(shù),在進(jìn)程中用于標(biāo)識(shí)打開的文件或 Socket 等 I/O 資源。通過文件描述符,進(jìn)程可以像操作普通文件一樣對(duì) Socket 進(jìn)行讀寫操作,這使得 Socket 的操作與其他文件操作具有一致性,大大簡化了編程模型。
例如,在使用 C 語言進(jìn)行 Socket 編程時(shí),我們可以使用read和write函數(shù)對(duì) Socket 進(jìn)行數(shù)據(jù)的接收和發(fā)送,就如同對(duì)文件進(jìn)行讀寫操作一樣:
#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;
}
// 假設(shè)已經(jīng)完成Socket的綁定、監(jiān)聽和連接等操作
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函數(shù)從 Socket 對(duì)應(yīng)的文件描述符sockfd中讀取數(shù)據(jù),write函數(shù)則用于向 Socket 寫入數(shù)據(jù)。這種將 Socket 與文件系統(tǒng)統(tǒng)一的設(shè)計(jì),使得開發(fā)者可以利用熟悉的文件操作函數(shù)來處理網(wǎng)絡(luò)通信,提高了開發(fā)效率和代碼的可維護(hù)性。同時(shí),也方便了系統(tǒng)對(duì)資源的管理,因?yàn)槲募枋龇遣僮飨到y(tǒng)管理 I/O 資源的重要方式,Socket 作為文件系統(tǒng)的一部分,可以納入統(tǒng)一的資源管理體系中。
三、Socket 的本質(zhì)剖析
3.1從通信端點(diǎn)角度理解
從通信端點(diǎn)的角度來看,Socket 可以被視為兩個(gè)網(wǎng)絡(luò)各自通信連接中的端點(diǎn) 。它是網(wǎng)絡(luò)中進(jìn)程間通信的抽象表示,就像現(xiàn)實(shí)生活中,我們通過電話與他人溝通時(shí),電話兩端的聽筒就是通信端點(diǎn),而 Socket 在網(wǎng)絡(luò)通信中就扮演著這樣的 “聽筒” 角色。
以瀏覽器與服務(wù)器的通信為例,當(dāng)我們在瀏覽器中輸入一個(gè)網(wǎng)址,比如https://www.example.com,瀏覽器會(huì)創(chuàng)建一個(gè) Socket 對(duì)象,并通過這個(gè) Socket 向服務(wù)器的 IP 地址和對(duì)應(yīng)的端口(通常是 80 端口用于 HTTP 協(xié)議,443 端口用于 HTTPS 協(xié)議)發(fā)起連接請求。服務(wù)器端也會(huì)有一個(gè) Socket 在監(jiān)聽這個(gè)端口,當(dāng)它接收到客戶端的連接請求后,雙方的 Socket 就建立了一個(gè)通信鏈路。在這個(gè)鏈路中,客戶端 Socket 將用戶請求的數(shù)據(jù)(如 HTTP 請求報(bào)文)發(fā)送出去,服務(wù)器端 Socket 接收這些數(shù)據(jù)并進(jìn)行處理,然后將處理結(jié)果(如 HTTP 響應(yīng)報(bào)文)通過 Socket 返回給客戶端。整個(gè)過程中,Socket 作為通信端點(diǎn),負(fù)責(zé)數(shù)據(jù)的發(fā)送和接收,使得瀏覽器和服務(wù)器之間能夠?qū)崿F(xiàn)數(shù)據(jù)交互,讓我們最終在瀏覽器中看到網(wǎng)頁的內(nèi)容。
再比如,在即時(shí)通訊軟件中,每個(gè)客戶端和服務(wù)器之間都通過 Socket 建立連接。當(dāng)用戶 A 發(fā)送一條消息給用戶 B 時(shí),用戶 A 的客戶端 Socket 將消息數(shù)據(jù)發(fā)送出去,經(jīng)過網(wǎng)絡(luò)傳輸,到達(dá)服務(wù)器的 Socket,服務(wù)器再將消息轉(zhuǎn)發(fā)到用戶 B 的客戶端 Socket,從而實(shí)現(xiàn)消息的傳遞。Socket 就像是網(wǎng)絡(luò)世界中各個(gè)進(jìn)程之間溝通的 “大門”,通過它,不同主機(jī)上的進(jìn)程能夠相互交流,實(shí)現(xiàn)各種網(wǎng)絡(luò)應(yīng)用的功能。
3.2基于內(nèi)核緩沖區(qū)的實(shí)現(xiàn)
在 Linux 內(nèi)核中,Socket 本質(zhì)上是借助內(nèi)核緩沖區(qū)形成的偽文件 。當(dāng)應(yīng)用程序創(chuàng)建一個(gè) Socket 時(shí),內(nèi)核會(huì)為其分配相應(yīng)的內(nèi)核緩沖區(qū),包括發(fā)送緩沖區(qū)和接收緩沖區(qū)。發(fā)送緩沖區(qū)用于存儲(chǔ)應(yīng)用程序要發(fā)送的數(shù)據(jù),接收緩沖區(qū)則用于存儲(chǔ)從網(wǎng)絡(luò)中接收到的數(shù)據(jù)。
從文件操作的角度來看,Socket 與普通文件有很多相似之處。我們可以使用類似文件操作的函數(shù)來對(duì) Socket 進(jìn)行操作,如read和write函數(shù)。當(dāng)應(yīng)用程序調(diào)用write函數(shù)向 Socket 寫入數(shù)據(jù)時(shí),實(shí)際上是將數(shù)據(jù)從應(yīng)用程序的緩沖區(qū)拷貝到 Socket 的發(fā)送緩沖區(qū)中;而調(diào)用read函數(shù)從 Socket 讀取數(shù)據(jù)時(shí),是從 Socket 的接收緩沖區(qū)中讀取數(shù)據(jù)到應(yīng)用程序的緩沖區(qū)。這種設(shè)計(jì)使得 Socket 的操作方式與文件操作方式統(tǒng)一,大大降低了開發(fā)者的學(xué)習(xí)成本和編程難度。
這種基于內(nèi)核緩沖區(qū)的實(shí)現(xiàn)方式有諸多優(yōu)勢。首先,內(nèi)核緩沖區(qū)可以對(duì)數(shù)據(jù)進(jìn)行緩存,減少了網(wǎng)絡(luò)通信的次數(shù),提高了數(shù)據(jù)傳輸?shù)男?。例如,?dāng)應(yīng)用程序有大量數(shù)據(jù)要發(fā)送時(shí),如果沒有緩沖區(qū),每次都直接發(fā)送數(shù)據(jù),會(huì)導(dǎo)致頻繁的網(wǎng)絡(luò)交互,增加網(wǎng)絡(luò)開銷。而通過發(fā)送緩沖區(qū),應(yīng)用程序可以將數(shù)據(jù)先寫入緩沖區(qū),當(dāng)緩沖區(qū)達(dá)到一定大小或者滿足一定條件時(shí),再一次性將數(shù)據(jù)發(fā)送出去,這樣就減少了網(wǎng)絡(luò)傳輸?shù)拇螖?shù),提高了傳輸效率。
其次,緩沖區(qū)還可以在一定程度上緩解網(wǎng)絡(luò)擁塞。當(dāng)網(wǎng)絡(luò)出現(xiàn)擁塞時(shí),數(shù)據(jù)的傳輸速度會(huì)變慢,接收方可能無法及時(shí)接收數(shù)據(jù)。此時(shí),發(fā)送緩沖區(qū)可以暫時(shí)存儲(chǔ)數(shù)據(jù),避免數(shù)據(jù)丟失,等網(wǎng)絡(luò)狀況好轉(zhuǎn)后再繼續(xù)發(fā)送;接收緩沖區(qū)則可以存儲(chǔ)接收到的數(shù)據(jù),讓應(yīng)用程序有足夠的時(shí)間來處理這些數(shù)據(jù),保證了數(shù)據(jù)傳輸?shù)姆€(wěn)定性和可靠性。
四、Socket的類型及設(shè)計(jì)
4.1Socket的類型
在 Socket 編程中,不同類型的 Socket 適用于不同的應(yīng)用場景,它們各自具有獨(dú)特的特點(diǎn)和協(xié)議基礎(chǔ)。了解這些 Socket 類型,對(duì)于我們選擇合適的網(wǎng)絡(luò)通信方式至關(guān)重要。
(1)流式套接字(SOCK_STREAM)
流式套接字基于 TCP 協(xié)議,提供可靠的雙向順序數(shù)據(jù)流 。在這種類型的 Socket 通信中,數(shù)據(jù)就像水流一樣,源源不斷且有序地在發(fā)送方和接收方之間流動(dòng)。它具有以下幾個(gè)關(guān)鍵特點(diǎn):
- 可靠性:TCP 協(xié)議通過一系列機(jī)制確保數(shù)據(jù)的可靠傳輸。例如,它會(huì)對(duì)發(fā)送的數(shù)據(jù)進(jìn)行編號(hào),接收方收到數(shù)據(jù)后會(huì)發(fā)送確認(rèn)消息(ACK),如果發(fā)送方在一定時(shí)間內(nèi)沒有收到 ACK,就會(huì)重發(fā)數(shù)據(jù),從而保證數(shù)據(jù)不會(huì)丟失。
- 順序性:數(shù)據(jù)按照發(fā)送的順序進(jìn)行接收,不會(huì)出現(xiàn)亂序的情況。這是因?yàn)?TCP 協(xié)議在傳輸過程中會(huì)對(duì)數(shù)據(jù)進(jìn)行排序,確保接收方能夠按照正確的順序組裝數(shù)據(jù)。
- 面向連接:在進(jìn)行數(shù)據(jù)傳輸之前,需要先建立連接,就像打電話之前要先撥通對(duì)方號(hào)碼一樣。連接建立后,雙方才能進(jìn)行數(shù)據(jù)傳輸,傳輸結(jié)束后再關(guān)閉連接。
以 Web 服務(wù)器與客戶端的通信為例,當(dāng)我們在瀏覽器中輸入網(wǎng)址并訪問網(wǎng)頁時(shí),瀏覽器會(huì)創(chuàng)建一個(gè)流式套接字,并通過這個(gè)套接字向 Web 服務(wù)器發(fā)起連接請求。服務(wù)器接收到請求后,與瀏覽器建立 TCP 連接。在這個(gè)連接上,瀏覽器向服務(wù)器發(fā)送 HTTP 請求報(bào)文,服務(wù)器處理請求后,將 HTTP 響應(yīng)報(bào)文通過相同的連接返回給瀏覽器。由于流式套接字的可靠性和順序性,瀏覽器能夠完整、正確地接收到服務(wù)器返回的網(wǎng)頁數(shù)據(jù),從而正常顯示網(wǎng)頁內(nèi)容。
(2)數(shù)據(jù)報(bào)套接字(SOCK_DGRAM)
數(shù)據(jù)報(bào)套接字基于 UDP 協(xié)議,提供雙向的數(shù)據(jù)傳輸,但不保證數(shù)據(jù)傳輸?shù)目煽啃?。與流式套接字相比,它具有以下特點(diǎn):
- 不可靠性:UDP 協(xié)議不保證數(shù)據(jù)一定能到達(dá)目標(biāo),也不保證數(shù)據(jù)的順序和完整性。數(shù)據(jù)在傳輸過程中可能會(huì)丟失、重復(fù)或亂序,這是因?yàn)?UDP 沒有像 TCP 那樣的確認(rèn)和重傳機(jī)制。
- 無連接:在數(shù)據(jù)傳輸前不需要建立連接,就像寄信一樣,直接把信(數(shù)據(jù))發(fā)送出去即可,不需要事先通知對(duì)方。這種方式使得數(shù)據(jù)報(bào)套接字的傳輸效率較高,因?yàn)槭∪チ私⒑筒鸪B接的開銷。
- 固定長度數(shù)據(jù)傳輸:每個(gè) UDP 數(shù)據(jù)報(bào)都有一個(gè)固定的最大長度,超過這個(gè)長度的數(shù)據(jù)需要分割成多個(gè)數(shù)據(jù)報(bào)進(jìn)行傳輸。
以視頻通話應(yīng)用為例,視頻通話需要實(shí)時(shí)傳輸大量的視頻和音頻數(shù)據(jù)。由于對(duì)實(shí)時(shí)性要求很高,如果采用可靠性高但傳輸延遲較大的 TCP 協(xié)議,可能會(huì)導(dǎo)致畫面卡頓、聲音延遲等問題。而 UDP 協(xié)議的低延遲特性更適合視頻通話場景,雖然可能會(huì)丟失一些數(shù)據(jù),但只要丟失的數(shù)據(jù)量在可接受范圍內(nèi),視頻和音頻仍然可以正確解析,不會(huì)對(duì)通話質(zhì)量產(chǎn)生太大影響。在視頻通話過程中,發(fā)送方通過數(shù)據(jù)報(bào)套接字將視頻和音頻數(shù)據(jù)以 UDP 數(shù)據(jù)報(bào)的形式發(fā)送出去,接收方接收到數(shù)據(jù)后進(jìn)行實(shí)時(shí)播放,即使有少量數(shù)據(jù)丟失,也能通過一些算法進(jìn)行補(bǔ)償,保證視頻和音頻的流暢播放。
(3)原始套接字(SOCK_RAW)
原始套接字允許進(jìn)程直接訪問底層協(xié)議,這使得它在網(wǎng)絡(luò)協(xié)議開發(fā)、網(wǎng)絡(luò)測試等場景中發(fā)揮著重要作用 。與流式套接字和數(shù)據(jù)報(bào)套接字不同,原始套接字可以讀寫內(nèi)核沒有處理的 IP 數(shù)據(jù)包,開發(fā)者可以通過它來實(shí)現(xiàn)自定義的網(wǎng)絡(luò)協(xié)議,或者對(duì)網(wǎng)絡(luò)數(shù)據(jù)包進(jìn)行更深入的分析和處理。
- 網(wǎng)絡(luò)協(xié)議開發(fā):在開發(fā)新的網(wǎng)絡(luò)協(xié)議時(shí),原始套接字是必不可少的工具。開發(fā)者可以利用它直接操作 IP 數(shù)據(jù)包,實(shí)現(xiàn)新協(xié)議的各種功能。例如,假設(shè)要開發(fā)一種新的物聯(lián)網(wǎng)通信協(xié)議,就可以通過原始套接字來構(gòu)建和發(fā)送符合該協(xié)議格式的 IP 數(shù)據(jù)包,同時(shí)接收和解析來自其他設(shè)備的數(shù)據(jù)包,進(jìn)行協(xié)議的測試和驗(yàn)證。
- 網(wǎng)絡(luò)測試與診斷:在網(wǎng)絡(luò)測試和故障診斷中,原始套接字可以幫助我們獲取更詳細(xì)的網(wǎng)絡(luò)信息。比如,使用 ping 命令時(shí),實(shí)際上就是利用原始套接字發(fā)送 ICMP(Internet Control Message Protocol)回顯請求報(bào)文,并接收 ICMP 回顯應(yīng)答報(bào)文,以此來測試網(wǎng)絡(luò)的連通性。再如,在網(wǎng)絡(luò)安全領(lǐng)域,通過原始套接字可以捕獲和分析網(wǎng)絡(luò)中的數(shù)據(jù)包,檢測是否存在異常流量或攻擊行為。
4.2Socket的設(shè)計(jì)
現(xiàn)在我們拋開socket,重新設(shè)計(jì)一個(gè)內(nèi)核網(wǎng)絡(luò)傳輸功能。我們想要將數(shù)據(jù)從 A 電腦的某個(gè)進(jìn)程發(fā)到 B 電腦的某個(gè)進(jìn)程,從操作上來看,就是發(fā)數(shù)據(jù)給遠(yuǎn)端和從遠(yuǎn)端接收數(shù)據(jù),也就是寫數(shù)據(jù)和讀數(shù)據(jù)。
但這里有兩個(gè)問題:
- 接收端和發(fā)送端可能不止一個(gè),因此需要用 IP 和端口做區(qū)分,IP 用來定位是哪臺(tái)電腦,端口用來定位是這臺(tái)電腦上的哪個(gè)進(jìn)程。
- 發(fā)送端和接收端的傳輸方式有很多區(qū)別,如可靠的 TCP 協(xié)議、不可靠的 UDP 協(xié)議,甚至還需要支持基于 icmp 協(xié)議的 ping 命令。
為了支持這些功能,需要定義一個(gè)數(shù)據(jù)結(jié)構(gòu) sock,在 sock 里加入 IP 和端口字段。這些協(xié)議雖然各不相同,但有一些功能相似的地方,可以將不同的協(xié)議當(dāng)成不同的對(duì)象類(或結(jié)構(gòu)體),將公共的部分提取出來,通過“繼承”的方式復(fù)用功能。于是,定義了一些數(shù)據(jù)結(jié)構(gòu):sock 是最基礎(chǔ)的結(jié)構(gòu),維護(hù)一些任何協(xié)議都有可能會(huì)用到的收發(fā)數(shù)據(jù)緩沖區(qū)。
在 Linux 內(nèi)核 2.6 相關(guān)的源碼中,sock 結(jié)構(gòu)體的定義可能類似于:
struct sock {
// 相關(guān)字段
struct sk_buff_head sk_receive_queue; // 接收數(shù)據(jù)緩沖區(qū)
struct sk_buff_head sk_write_queue; // 發(fā)送數(shù)據(jù)緩沖區(qū)
// 其他可能的字段
};
inet_sock 特指用了網(wǎng)絡(luò)傳輸功能的 sock,在 sock 的基礎(chǔ)上還加入了 TTL、端口、IP 地址這些跟網(wǎng)絡(luò)傳輸相關(guān)的字段信息。比如 Unix domain socket,用于本機(jī)進(jìn)程之間的通信,直接讀寫文件,不需要經(jīng)過網(wǎng)絡(luò)協(xié)議棧。
可能的定義:
struct inet_sock {
struct sock sk; // 繼承自 sock
__be32 port; // 端口
__be32 saddr; // IP 地址
// 其他相關(guān)字段
};
inet_connection_sock 是指面向連接的 sock,在 inet_sock 的基礎(chǔ)上加入面向連接的協(xié)議里相關(guān)字段,比如 accept 隊(duì)列、數(shù)據(jù)包分片大小、握手失敗重試次數(shù)等。雖然現(xiàn)在提到面向連接的協(xié)議就是指 TCP,但設(shè)計(jì)上 Linux 需要支持?jǐn)U展其他面向連接的新協(xié)議。
例如:
struct inet_connection_sock {
struct inet_sock inet; // 繼承自 inet_sock
struct request_sock_queue accept_queue; // accept 隊(duì)列
// 其他相關(guān)字段
};
tcp_sock 就是正兒八經(jīng)的 TCP 協(xié)議專用的 sock 結(jié)構(gòu),在 inet_connection_sock 基礎(chǔ)上還加入了 TCP 特有的滑動(dòng)窗口、擁塞避免等功能。同樣 UDP 協(xié)議也會(huì)有一個(gè)專用的數(shù)據(jù)結(jié)構(gòu),叫 udp_sock。
大概如下:
struct tcp_sock {
struct inet_connection_sock icsk; // 繼承自 inet_connection_sock
// TCP 特有的字段,如滑動(dòng)窗口、擁塞避免等相關(guān)字段
};
有了這套數(shù)據(jù)結(jié)構(gòu),將它跟硬件網(wǎng)卡對(duì)接一下,就實(shí)現(xiàn)了網(wǎng)絡(luò)傳輸?shù)墓δ堋?/span>
4.3提供 Socket 層
由于這里面的代碼復(fù)雜,還操作了網(wǎng)卡硬件,需要較高的操作系統(tǒng)權(quán)限,再考慮到性能和安全,于是將它放在操作系統(tǒng)內(nèi)核里。
為了讓用戶空間的應(yīng)用程序使用這部分功能,將這部分功能抽象成簡單的接口,將內(nèi)核的 sock 封裝成文件。創(chuàng)建 sock 的同時(shí)也創(chuàng)建一個(gè)文件,文件有個(gè)文件描述符 fd,通過它可以唯一確定是哪個(gè) sock。將fd暴露給用戶,用戶就可以像操作文件句柄那樣去操作這個(gè) sock 。
struct file{
//文件相關(guān)的字段
.....
void *private_data; //指向sock
}
創(chuàng)建socket時(shí),其實(shí)就是創(chuàng)建了一個(gè)文件結(jié)構(gòu)體,并將private_data字段指向sock。有了 sock_fd 句柄后,提供了一些接口,如 send()、recv()、bind()、listen()、connect() 等,這些就是 socket 提供出來的接口。所以說,socket 其實(shí)就是個(gè)代碼庫或接口層,它介于內(nèi)核和應(yīng)用程序之間,提供了一堆接口,讓我們?nèi)ナ褂脙?nèi)核功能,本質(zhì)上就是一堆高度封裝過的接口。
我們平時(shí)寫的應(yīng)用程序里代碼里雖然用了socket實(shí)現(xiàn)了收發(fā)數(shù)據(jù)包的功能,但其 實(shí)真正執(zhí)行網(wǎng)絡(luò)通信功能的,不是應(yīng)用程序,而是linux內(nèi)核。
在操作系統(tǒng)內(nèi)核空間里,實(shí)現(xiàn)網(wǎng)絡(luò)傳輸功能的結(jié)構(gòu)是sock,基于不同的協(xié)議和應(yīng)用場景,會(huì)被泛化為各種類型的xx_sock,它們結(jié)合硬件,共同實(shí)現(xiàn)了網(wǎng)絡(luò)傳輸功能。為了將這部分功能暴露給用戶空間的應(yīng)用程序使用,于是引入了socket層,同時(shí)將sock嵌入到文件系統(tǒng)的框架里,sock就變成了一個(gè)特殊的文件,用戶就可以在用戶空間使用文件句柄,也就是socket_fd來操作內(nèi)核sock的網(wǎng)絡(luò)傳輸能力。
五、Socket 的工作機(jī)制
5.1創(chuàng)建與初始化
當(dāng)應(yīng)用程序需要進(jìn)行網(wǎng)絡(luò)通信時(shí),首先會(huì)調(diào)用 socket 函數(shù)來創(chuàng)建一個(gè)套接字 。以 C 語言為例,socket 函數(shù)的原型如下:
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
其中,domain參數(shù)指定協(xié)議族,如AF_INET表示 IPv4 協(xié)議族,AF_INET6表示 IPv6 協(xié)議族;type參數(shù)指定套接字類型,如SOCK_STREAM表示流式套接字,SOCK_DGRAM表示數(shù)據(jù)報(bào)套接字;protocol參數(shù)通常設(shè)置為 0,表示使用默認(rèn)協(xié)議。
當(dāng)應(yīng)用程序調(diào)用 socket 函數(shù)時(shí),內(nèi)核會(huì)為該套接字分配相應(yīng)的資源,包括內(nèi)存空間和文件描述符 。內(nèi)核會(huì)在內(nèi)存中創(chuàng)建一個(gè)套接字結(jié)構(gòu)體,用于存儲(chǔ)與該套接字相關(guān)的控制信息,如套接字的狀態(tài)、連接的對(duì)端地址和端口、發(fā)送和接收緩沖區(qū)等。同時(shí),內(nèi)核會(huì)為套接字分配一個(gè)唯一的文件描述符,并將該文件描述符返回給應(yīng)用程序。應(yīng)用程序通過這個(gè)文件描述符來標(biāo)識(shí)和操作該套接字,就像通過文件描述符操作普通文件一樣。
5.2連接建立(僅針對(duì)面向連接的套接字)
以 TCP 協(xié)議的流式套接字為例,連接建立需要通過三次握手來完成 。三次握手的過程如下:
- 第一次握手:客戶端向服務(wù)器發(fā)送一個(gè) SYN(同步)報(bào)文段,該報(bào)文段中包含客戶端的初始序列號(hào)(Sequence Number,簡稱 Seq),假設(shè)為 x 。此時(shí),客戶端進(jìn)入 SYN_SENT 狀態(tài),等待服務(wù)器的響應(yīng)。這個(gè)過程就好比客戶端給服務(wù)器打電話說:“我想和你建立連接,這是我的初始序號(hào) x”。
- 第二次握手:服務(wù)器接收到客戶端的 SYN 報(bào)文段后,會(huì)回復(fù)一個(gè) SYN-ACK(同步確認(rèn))報(bào)文段 。該報(bào)文段中包含服務(wù)器的初始序列號(hào),假設(shè)為 y,同時(shí) ACK(確認(rèn))字段的值為 x + 1,表示服務(wù)器已經(jīng)收到客戶端的 SYN 報(bào)文段,并且確認(rèn)號(hào)為客戶端的序列號(hào)加 1。此時(shí),服務(wù)器進(jìn)入 SYN_RCVD 狀態(tài)。這就像是服務(wù)器接起電話回應(yīng)客戶端:“我收到你的連接請求了,這是我的初始序號(hào) y,我確認(rèn)收到了你的序號(hào) x”。
- 第三次握手:客戶端接收到服務(wù)器的 SYN-ACK 報(bào)文段后,會(huì)發(fā)送一個(gè) ACK 報(bào)文段給服務(wù)器 。該報(bào)文段的 ACK 字段的值為 y + 1,表示客戶端已經(jīng)收到服務(wù)器的 SYN-ACK 報(bào)文段,并且確認(rèn)號(hào)為服務(wù)器的序列號(hào)加 1。此時(shí),客戶端進(jìn)入 ESTABLISHED 狀態(tài),服務(wù)器接收到 ACK 報(bào)文段后也進(jìn)入 ESTABLISHED 狀態(tài),連接建立成功。這相當(dāng)于客戶端再次回應(yīng)服務(wù)器:“我收到你的回復(fù)了,連接建立成功,我們可以開始通信了”。
三次握手的作用在于確保雙方的通信能力正常,并且能夠同步初始序列號(hào),為后續(xù)的數(shù)據(jù)傳輸建立可靠的基礎(chǔ) 。通過三次握手,客戶端和服務(wù)器都能確認(rèn)對(duì)方可以正常接收和發(fā)送數(shù)據(jù),避免了舊連接請求的干擾,保證了連接的唯一性和正確性。
5.3數(shù)據(jù)傳輸
在數(shù)據(jù)傳輸階段,發(fā)送端和接收端的數(shù)據(jù)流動(dòng)過程如下:
- 發(fā)送端:應(yīng)用程序調(diào)用write或send函數(shù)將數(shù)據(jù)發(fā)送到 Socket 。這些函數(shù)會(huì)將應(yīng)用程序緩沖區(qū)中的數(shù)據(jù)拷貝到 Socket 的發(fā)送緩沖區(qū)中。然后,內(nèi)核會(huì)根據(jù) Socket 的類型和協(xié)議,對(duì)數(shù)據(jù)進(jìn)行封裝。對(duì)于 TCP 套接字,數(shù)據(jù)會(huì)被分割成 TCP 段,并添加 TCP 頭部,包括源端口、目標(biāo)端口、序列號(hào)、確認(rèn)號(hào)等信息;對(duì)于 UDP 套接字,數(shù)據(jù)會(huì)被封裝成 UDP 數(shù)據(jù)報(bào),并添加 UDP 頭部,包含源端口和目標(biāo)端口。接著,數(shù)據(jù)會(huì)被傳遞到網(wǎng)絡(luò)層,添加 IP 頭部,包含源 IP 地址和目標(biāo) IP 地址,形成 IP 數(shù)據(jù)包。最后,IP 數(shù)據(jù)包通過網(wǎng)絡(luò)接口層發(fā)送到物理網(wǎng)絡(luò)上。
- 接收端:數(shù)據(jù)從物理網(wǎng)絡(luò)進(jìn)入接收端的網(wǎng)絡(luò)接口層 。網(wǎng)絡(luò)接口層接收到 IP 數(shù)據(jù)包后,會(huì)進(jìn)行解包,將 IP 頭部去除,然后將數(shù)據(jù)傳遞到網(wǎng)絡(luò)層。網(wǎng)絡(luò)層根據(jù) IP 頭部中的目標(biāo) IP 地址,判斷該數(shù)據(jù)包是否是發(fā)給本機(jī)的。如果是,則去除 IP 頭部,將數(shù)據(jù)傳遞到傳輸層。傳輸層根據(jù)協(xié)議類型(TCP 或 UDP),對(duì)數(shù)據(jù)進(jìn)行相應(yīng)的處理。對(duì)于 TCP 數(shù)據(jù),會(huì)檢查序列號(hào)和確認(rèn)號(hào),進(jìn)行流量控制和錯(cuò)誤重傳等操作;對(duì)于 UDP 數(shù)據(jù),直接去除 UDP 頭部,將數(shù)據(jù)傳遞到 Socket 的接收緩沖區(qū)。最后,應(yīng)用程序調(diào)用read或recv函數(shù)從 Socket 的接收緩沖區(qū)中讀取數(shù)據(jù)到應(yīng)用程序緩沖區(qū)中,完成數(shù)據(jù)的接收。
5.4連接關(guān)閉
對(duì)于 TCP 連接,關(guān)閉過程需要通過四次揮手來完成 。四次揮手的過程如下:
- 第一次揮手:主動(dòng)關(guān)閉方(可以是客戶端或服務(wù)器)發(fā)送一個(gè) FIN(結(jié)束)報(bào)文段,表示自己已經(jīng)沒有數(shù)據(jù)要發(fā)送了,準(zhǔn)備斷開連接 。此時(shí),主動(dòng)關(guān)閉方進(jìn)入 FIN_WAIT_1 狀態(tài)。這就像一方說:“我這邊數(shù)據(jù)發(fā)完了,準(zhǔn)備斷開連接”。
- 第二次揮手:被動(dòng)關(guān)閉方接收到 FIN 報(bào)文段后,會(huì)發(fā)送一個(gè) ACK 確認(rèn)報(bào)文段,表示已收到主動(dòng)關(guān)閉方的斷開請求,并同意斷開連接 。但此時(shí)被動(dòng)關(guān)閉方可能還沒有完成數(shù)據(jù)處理,它需要繼續(xù)處理緩沖區(qū)中的數(shù)據(jù)。此時(shí),被動(dòng)關(guān)閉方進(jìn)入 CLOSE_WAIT 狀態(tài),主動(dòng)關(guān)閉方接收到 ACK 報(bào)文段后進(jìn)入 FIN_WAIT_2 狀態(tài)。相當(dāng)于另一方回應(yīng):“我收到你的斷開請求了,等我處理完數(shù)據(jù)就斷開”。
- 第三次揮手:當(dāng)被動(dòng)關(guān)閉方完成數(shù)據(jù)處理后,它會(huì)向主動(dòng)關(guān)閉方發(fā)送一個(gè) FIN 報(bào)文段,表示自己的數(shù)據(jù)也已經(jīng)發(fā)送完畢,準(zhǔn)備關(guān)閉連接 。此時(shí),被動(dòng)關(guān)閉方進(jìn)入 LAST_ACK 狀態(tài)。即另一方說:“我數(shù)據(jù)處理完了,現(xiàn)在可以斷開了”。
- 第四次揮手:主動(dòng)關(guān)閉方收到被動(dòng)關(guān)閉方的 FIN 報(bào)文段后,會(huì)發(fā)送一個(gè) ACK 確認(rèn)報(bào)文段,確認(rèn)接收到了被動(dòng)關(guān)閉方的斷開請求 。此時(shí),主動(dòng)關(guān)閉方進(jìn)入 TIME_WAIT 狀態(tài),等待一段時(shí)間(通常為 2 倍的最大報(bào)文段生存時(shí)間,即 2MSL)后,自動(dòng)進(jìn)入 CLOSE 狀態(tài),連接完全關(guān)閉。被動(dòng)關(guān)閉方收到 ACK 報(bào)文段后,直接進(jìn)入 CLOSE 狀態(tài)。這一步就像是最初發(fā)起斷開的一方回應(yīng):“我確認(rèn)收到你的斷開請求,我們可以徹底斷開了”。
之所以需要四次揮手來確保連接的可靠關(guān)閉,是因?yàn)?TCP 連接是全雙工的,每個(gè)方向都必須單獨(dú)關(guān)閉 。在第一次揮手中,主動(dòng)關(guān)閉方只是表示自己不再發(fā)送數(shù)據(jù),但仍可以接收數(shù)據(jù);被動(dòng)關(guān)閉方發(fā)送 ACK 確認(rèn)后,還需要時(shí)間處理剩余數(shù)據(jù),處理完后再發(fā)送 FIN 報(bào)文表示自己也不再發(fā)送數(shù)據(jù)。通過四次揮手,雙方都能確認(rèn)對(duì)方已經(jīng)完成數(shù)據(jù)傳輸,并且所有數(shù)據(jù)都已被正確接收,從而保證了連接關(guān)閉的可靠性,避免數(shù)據(jù)丟失或不完全傳輸。
六、Socket 在Linux系統(tǒng)中的應(yīng)用實(shí)例
6.1簡單的 TCP 服務(wù)器與客戶端程序
下面是一個(gè)使用 C 語言編寫的簡單 TCP 服務(wù)器和客戶端程序示例,通過這個(gè)示例,我們可以更直觀地了解 Socket 在實(shí)際應(yīng)用中的使用方法。
TCP 服務(wù)器代碼(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() {
// 創(chuàng)建套接字
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);
}
// 監(jiān)聽連接請求
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};
// 接收客戶端發(fā)送的數(shù)據(jù)
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);
// 向客戶端發(fā)送響應(yīng)數(shù)據(jù)
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");
}
// 關(guān)閉客戶端套接字
close(client_socket);
}
// 關(guān)閉服務(wù)器套接字
close(server_socket);
return 0;
}
- socket 函數(shù):創(chuàng)建一個(gè)基于 IPv4 的流式套接字(SOCK_STREAM),用于 TCP 通信。
- bind 函數(shù):將套接字綁定到指定的 IP 地址(INADDR_ANY 表示接受任意 IP 地址的連接)和端口(PORT)。
- listen 函數(shù):使套接字進(jìn)入監(jiān)聽狀態(tài),等待客戶端的連接請求,最大允許同時(shí)有MAX_CONNECTIONS個(gè)連接請求排隊(duì)。
- accept 函數(shù):阻塞等待并接受客戶端的連接請求,返回一個(gè)新的套接字client_socket,用于與該客戶端進(jìn)行通信。
- recv 函數(shù):從客戶端套接字接收數(shù)據(jù),存儲(chǔ)在buffer中。
- send 函數(shù):向客戶端套接字發(fā)送響應(yīng)數(shù)據(jù)。
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() {
// 創(chuàng)建套接字
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);
}
// 連接到服務(wù)器
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!";
// 向服務(wù)器發(fā)送數(shù)據(jù)
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};
// 接收服務(wù)器返回的數(shù)據(jù)
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");
}
// 關(guān)閉客戶端套接字
close(client_socket);
return 0;
}
- socket 函數(shù):同樣創(chuàng)建一個(gè)基于 IPv4 的流式套接字。
- inet_pton 函數(shù):將點(diǎn)分十進(jìn)制的 IP 地址(SERVER_IP)轉(zhuǎn)換為網(wǎng)絡(luò)字節(jié)序的二進(jìn)制形式,存儲(chǔ)在server_addr.sin_addr中。
- connect 函數(shù):向服務(wù)器發(fā)起連接請求,連接到指定的 IP 地址和端口。
- send 函數(shù):向服務(wù)器發(fā)送數(shù)據(jù)。
- recv 函數(shù):接收服務(wù)器返回的數(shù)據(jù)。
通過這兩個(gè)程序,我們可以看到 Socket 在 TCP 通信中的基本應(yīng)用,服務(wù)器端監(jiān)聽端口并處理客戶端的連接和數(shù)據(jù)請求,客戶端連接到服務(wù)器并進(jìn)行數(shù)據(jù)的發(fā)送和接收。
6.2UDP 數(shù)據(jù)傳輸示例
下面是一個(gè)使用 UDP 協(xié)議進(jìn)行數(shù)據(jù)傳輸?shù)拇a示例,展示了如何發(fā)送和接收 UDP 數(shù)據(jù)報(bào)。
UDP 發(fā)送端代碼(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() {
// 創(chuàng)建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;
}
// 發(fā)送UDP數(shù)據(jù)報(bào)
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");
}
}
// 關(guān)閉套接字
close(sender_socket);
return 0;
}
- socket 函數(shù):創(chuàng)建一個(gè)基于 IPv4 的數(shù)據(jù)報(bào)套接字(SOCK_DGRAM),用于 UDP 通信。
- inet_pton 函數(shù):將目標(biāo) IP 地址(DEST_IP)轉(zhuǎn)換為網(wǎng)絡(luò)字節(jié)序的二進(jìn)制形式,存儲(chǔ)在dest_addr.sin_addr中。
- sendto 函數(shù):向指定的目標(biāo)地址(dest_addr)發(fā)送 UDP 數(shù)據(jù)報(bào),數(shù)據(jù)存儲(chǔ)在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() {
// 創(chuàng)建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數(shù)據(jù)報(bào)
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");
}
}
// 關(guān)閉套接字
close(receiver_socket);
return 0;
}
- socket 函數(shù):創(chuàng)建 UDP 套接字。
- bind 函數(shù):將套接字綁定到指定的 IP 地址(INADDR_ANY)和端口(PORT),以便接收來自客戶端的數(shù)據(jù)報(bào)。
- recvfrom 函數(shù):從客戶端接收 UDP 數(shù)據(jù)報(bào),數(shù)據(jù)存儲(chǔ)在buffer中,并獲取發(fā)送端的地址信息(client_addr)。
通過這個(gè) UDP 數(shù)據(jù)傳輸示例,我們可以看到 UDP 通信的基本流程,發(fā)送端將數(shù)據(jù)報(bào)發(fā)送到指定的目標(biāo)地址和端口,接收端在綁定的地址和端口上等待接收數(shù)據(jù)報(bào)。與 TCP 不同,UDP 不需要建立連接,數(shù)據(jù)報(bào)的發(fā)送和接收更加簡單直接,但也不保證數(shù)據(jù)的可靠性和順序性 。