Linux C 網絡基礎編程

基礎知識

在進行網絡編程之前,我們需要簡單回顧一下計算機網絡五層模型的網絡層和傳輸層,這兩層在面向后端編程時用的最多。物理層和鏈路層過于底層,已經完全由內核協議棧實現,不再細述。

這里假設讀者已經對計算機網絡有一個大致的了解。

網絡層

IP協議是四層模型中的核心協議,所有TCP、UDP、ICMP數據都通過IP數據報傳輸。IP提
供一種盡力而為(就是)、無連接的數據報交付服務。不可靠意味著如果傳遞過程不可靠中出現差錯,IP層可以選擇丟棄數據,并且不會主動重傳;無連接意味著IP協議不會記錄傳遞過程中的路徑,那同樣的兩端發生的不同數據報可能會走不同的路徑,并且有可能不按順序到達。

IP地址以及其分類

IP地址用來區分不同的主機在網絡層中的位置。IPv4的地址長度為32位,為了方便描述,通常將其按8位一組分隔,并用.號隔開,這種就是點分十進制,比如192.168.1.1。IPv6的地址長度是128位,一般用8個4位十六進制描述,每個十六進制數描述一個段。IPv6的應用可以解決IP地址缺乏的問題,但是隨著NAT技術的廣泛使用,IPv4目前實際上還是占據了大部分市場。
在早期,每個IP地址會分為兩部分,高位是網絡號,低位是主機號,并且根據網絡號的前綴,將其分為5類地址,A、B、C、D(這是用于組播的)和E(這是保留的地址)類,這種分類方式除了規定了網絡號的前綴,還是劃定了網絡號和主機號的長度。隨著Internet逐漸發展,這種死板的分類方式已經不適應人們的需求。一種相對自由的劃分方式就是采用子網機制:把主機號的前綴作為子網ID,剩余部分作為主機ID,主機ID的長度由本地網絡管理員自行劃定。

子網掩碼可以用來描述主機ID的長度,其長度和IP地址一樣是32,其中高位部分全為1,剩余的部分全為0。前綴部分的長度說明網絡號和子網ID的長度,剩余部分自然就是主機ID了。子網掩碼可以用在局域網內為路由器的路由決策作出參考,而局域網外的路由決策則只和網絡號部分有關。我們可以用點分十進制來描述子網掩碼(比如255.255.255.0),或者在IP地址的后綴中說明網絡號和子網ID的總長度(比如192.168.3.0/24)

?在每個IPv4子網中,主機部分全為1的IP地址被保留為本地廣播地址,比如子網為128.32.1.0/24的子網廣播地址是128.32.1.255。除此以外,特殊地址255.255.255.255被保留為本地廣播地址,它不會被路由器轉發,通常配合UDP/IP和ICMP等協議使用。
隨著CIDR技術的引入,傳統的5類分類方式被廢棄了,IP地址不再按照固定的長度進行劃分,在分配IP地址時,除了要指定分配的網絡號,還需要說明網絡號的長度,可以采用類似子網掩碼的方式進行描述。
下面是一些常見的用于特殊用途的IP地址:

前綴用途
0.0.0.0/8作為源地址時表示本地主機||作為目的地址時,表示任意IP地址
10.0.0.0/8局域網IP地址
172.16.0.0/12局域網IP地址
192.168.0.0/16局域網IP地址
127.0.0.0/8回環地址
169.254.0.0/16鏈路本地地址,通常出現在DHCP自動分配IP未完成時
255.255.255.255/32本地網絡廣播地址

IP數據報的結構

正常的IPv4頭部大小為20個字節(在很少情況會擁有選項,此時頭部不只20個字節),IP頭部的傳輸是按照大端法進行的,對于一個32位值而言,首先傳輸高位8位,然后次高8位,以此類推。因為TCP/IP協議中所有協議的頭部都采用大端法進行傳輸,所以大端法也網絡字節序稱作。由于大部分PC使用的是小端法,所以在構造完頭部之后傳輸之前需要對其執行大小端轉換才行。下圖當中描述IPv4中IP數據報的格式。

  • 版本:4位,數值4指IPv4,6指IPv6。
  • 頭部字段:4位,用來描述IP數據報頭部的長度為多少32位。因此IP數據報頭部最多只有60個字節。
  • 服務類型:8位,描述服務質量和擁塞情況
  • 總長度:16位,描述IP數據報的總長度(包括頭部)為多少字節。這個字段有助于從帶填充的以太網幀取出IP數據報的有效部分(可能這個IP數據報不足46個字節,在以太網幀當中填充了0)。
  • 標識:16位,描述IP數據報的分片編號,同一個大IP數據報分解出來的多個分片擁有相同的標識。
  • 標志:3位,描述是否發生分片,以及是否后續有更多的分片。
  • 片偏移:13位,描述該分片在重組后的大IP數據報當中的位置,以8字節為單位。生存期(TTL):8位,描述一個數據報可以經過路由器的上限,每次路由器轉發時該數值會減一。這個屬性可以避免在環形路由情況下,數據報在網絡中永遠循環。
  • 協議:8位,描述上層協議的類型,最常見的是1(ICMP)、17(UDP)和6(TCP)首部校驗和:16位,IP數據報頭部的校驗和,注意并不檢查載荷部分內容,所以需要上層協議自己檢查。
  • 源IP地址和目的IP地址:各有32位,描述IP數據報的發送者和接收者的IP地址。

?分片和重組

由于IP數據報的總長度限制為65535字節,這遠遠超過了部分鏈路層標準的MTU,當數據從網絡層準備轉移到數據鏈路層時,網絡層會將IP數據報進行分片操作,將分解成若干個獨立的IP數據報(分解之后IP數據的總長度字段改變了),并且在網絡之間獨立傳輸。一旦到達終點的目的主機之后(中間不會重組),目的主機的網絡層會將分片重組成一個大IP數據報。由于重組的過程十分復雜,所以協議設計者應該盡可能避免出現讓IP數據報超過MTU的情況。比如DNS、DHCP等就規定UDP報文長度為512字節。

傳輸層

傳輸控制協議TCP

傳輸控制協議(TCP)是整個四層模型當中最重要的協議,它工作在傳輸層,其目標是在不可靠的逐跳傳輸的網絡層之上,構建一個可靠的、面向連接的、全雙工的端到端協議。

建立連接的三次握手

TCP是一個面向連接的協議,在通信雙方真正交換數據之前,必須先先相互聯系建立一個TCP連接,這個就類似于電話的開頭的“喂”的效果。TCP是一個全雙工協議,雙方都需要對連接狀態進行管理。每一個獨特的TCP都由一個四元組唯一標識,組內包括通信雙方的IP地址和端口號,建立連接的過程通常被稱作是3次握手。雖然是全雙工的通信,但是有一次建立連接和確認行為可以合并在一起,所以只需要傳輸3次報文段即可。下面是其步驟:

  • 客戶端發起連接,發送一個SYN報文給服務端,然后說明自己連接的端口和客戶端初始序列號seq1。
  • 服務端收到SYN報文,也需要發起反方向的連接,所以發送一個SYN報文給服務端,說明自己連接的端口和服務端初始序列號seq2,除此以外,這個報文還可以攜帶一個確認信息,所以把seq1+1作為ACK返回。
  • 客戶端收到服務端的SYN之后,需要確認,所以把seq2+1作為ACK返回給服務端。同時,本次發送的報文可以攜帶數據。

3次握手最主要的目的是為了建立連接,并且交換初始序列號。在TCP連接過程中,如果存在一個老舊的報文(上一次連接時發送的)到達服務端,服務端可以根據其序列號是否合法來決定是否丟棄。有些情況可能出現雙方同時發起連接的情況,這個時候就需要4個報文段來建立連接了。

使用2次握手可不可行?

答案是否定的,因為服務端發起的SYN未確認。一種典型的場景就是客戶端發起SYN,第一個SYN超時并重傳,第二個SYN到達并建立連接,之后再完成連接并關閉,倘若關閉之后,第一個SYN到達服務端,此時服務端就會認為對方建立連接,并回復SYN+ACK,由于沒有確認,所以服務端并不知道客戶端的狀態,此時客戶端完全可能已經關閉,那服務端就會陷入永久等待了

斷開連接的四次揮手

斷開連接的過程要更加復雜一些,通信雙方誰都可以主動斷開連接,但是由于TCP是全雙工連接,所以一個方向斷開并不意味著反向的數據已經傳輸完成,所以每個方向的斷開是相對獨立的,這樣的話兩個方向各有一次斷開和確認,總共需要4次報文段的傳輸,即4次揮手。下面是其具體流程:

  • 主動關閉方發送一個FIN段表示希望斷開連接。
  • 被動關閉方收到FIN段,并且回復一個確認信息。其上層應用會收到一個EOF,被動關閉方繼續傳輸剩余的數據。
  • 被動關閉方發送完數據了,發送一個FIN段。
  • 主動關閉方回復一個確認,并且等待一段時間(2MSL,MSL指單個報文在網絡中的最長生存時間)。
  • 在第2次揮手之后,TCP連接此時處于一種半關閉的狀態。可以任為現在是一個單工通信方式(被動關閉方-->主動關閉方)。
報文頭部

上圖中描述了TCP報文首部的各個字段,具體含義如下:

  • 序號:即SEQ的值
  • 確認需要:即ACK的值,描述預期接收的下一個序列號。注意發送一個ACK和發送一個普通報文的消耗是一樣的。
  • 首部長度:首部的長度是可變的,以32位為單位。首部長度最短為20字節,最長為60字節。
  • URG:緊急。
  • ACK:確認號字段有效。連接建立以后通常一直有效。
  • PSH:推送。
  • RST:重置連接,出現在連接出錯時。
  • SYN:發起連接。
  • FIN:發起關閉。
  • 窗口大小:通告一個窗口大小以限制流量。
  • 校驗和:校驗傳輸中的比特跳變錯誤。
  • 緊急指針:向對端提供一種特殊標識。
  • 最大段大小(MSS),用來描述后續希望接收到的報文段
  • 選項:最常見的選項是
  • 的最大值,這個數值通常受限于MTU,比如MTU為1500,IP數據報頭部為20字節,TCP頭部為20字節,則MSS是1460。

用戶數據報協議UDP

UDP是一種保留消息邊界的簡單的面向數據報的傳輸層協議。它不提供差錯糾正、流量控制和擁塞管理等功能,只提供差錯校驗,但是一旦發現錯誤也只是簡單地丟棄報文,不會通知對端,更不會有重傳。由于功能特別簡單,所以UDP的實現和運行消耗特別地小,故UDP協議可以配合一些應用層協議實現在一些低質量網絡信道上的高效傳輸。許多早期的聊天軟件或者客戶端游戲都采用了基于UDP的應用層協議,這樣能最好地利用性能,同時在比較差的網絡狀態下提供更良好的服務。

報文頭部

UDP的報文結構非常簡單:

  • 長度:指UDP報文的總長度(包括UDP頭部),實際上這個長度是冗余的,報文長度可以根據IP報文長度計算而來。
  • 校驗和:用于最終目的方校驗,出錯的報文會直接丟棄。

Berkeley Socket

TCP/IP協議族標準只規定了網絡各個層次的設計和規范,具體實現則需要由各個操作系統廠商完成。最出名的網絡庫由BSD 4.2版本最先推出,所以稱作,這些API隨后被移植到各大操作系統中,并成為了網絡編程的事實標準。 socket 即套接字是指網絡中伯克利套接字
一種用來建立連接、網絡通信的設備,用戶創建了 socket 之后,可以通過其發起或者接受TCP 連接、可以向 TCP 的發送和接收緩沖區當中讀寫TCP數據段,或者發送 UDP 文本。

地址信息設置

struct sockaddr

我們主要以IPv4為例介紹網絡的地址結構。主要涉及的結構體有 struct in_addr 、 struct sockaddr 、 struct sockaddr_in 。其中 struct sockaddr 是一種通用的地址結構,它可以描述一個IPv4或者IPv6的結構,所有涉及到地址的接口都使用了該類型的參數,但是過于通用的結果是直接用它來描述一個具體的IP地址和端口號十分困難。所以用戶一般先使用struct sockaddr_in 來構造地址,再將其進行強制類型轉換成 struct sockaddr 以作為網絡接口的參數。

sockaddr_in

struct sockaddr_in 是一個在 C 語言中用于網絡編程的結構體,它主要用于表示 IPv4 地址和端口號。

struct sockaddr_in {uint8_t sin_len;         // 地址長度(可選字段,不是所有平臺都使用)sa_family_t sin_family;  // 地址族uint16_t sin_port;       // 端口號struct in_addr sin_addr; // IPv4 地址char sin_zero[8];        // 填充字段,用于對齊
};

字段說明

  • sin_len

    • 類型uint8_t(無符號8位整數)

    • 作用:指定結構體的長度(以字節為單位)。這個字段在某些系統(如某些版本的 BSD 系統)中是必需的,但在大多數現代系統中(如 Linux)通常不使用。

    • :如果使用,通常設置為 sizeof(struct sockaddr_in)

  • sin_family

    • 類型sa_family_t(通常是無符號16位整數)

    • 作用:指定地址族,用于標識地址類型。

      • AF_INET:表示 IPv4 地址族(這是最常見的值)。

      • 其他值(如 AF_UNIXAF_INET6)通常不用于 sockaddr_in 結構體。

  • sin_port

    • 類型uint16_t(無符號16位整數)

    • 作用:表示網絡端口號。

    • :端口號以網絡字節序(大端序)存儲。在使用時,通常需要通過 htons() 函數將主機字節序轉換為網絡字節序,例如:

struct sockaddr_in addr;
addr.sin_port = htons(80); // 將主機字節序的80轉換為網絡字節序

?sin_addr

  • 類型struct in_addr

  • 作用:表示 IPv4 地址。

  • 結構

struct in_addr {uint32_t s_addr; // IPv4 地址
};

s_addr:IPv4 地址,以網絡字節序存儲。可以使用 inet_addr()inet_pton() 函數將點分十進制字符串(如 "192.168.1.1")轉換為網絡字節序的整數,例如:

struct sockaddr_in addr;
addr.sin_addr.s_addr = inet_addr("192.168.1.1");
  • sin_zero
  • 類型char[8]
  • 作用:填充字段,用于對齊結構體。在實際使用中,通常不需要手動設置這個字段,它主要用于確保結構體的大小和對齊方式與 struct sockaddr 一致。

大小端轉換

網絡字節序即大端法。對于應用TCP/IP協議規定,當數據在網絡中傳輸的時候,一律使用層協議的載荷部分,如果不需要第三方工具檢測內容,可以不進行大小端轉換(因為接收方和發送方都是主機字節序即小端法)。但是對于其他層次的頭部部分,在發送之前就一定要進行小端到大端的轉換了(因為網絡中的通信以及 tcpdump 、 netstat 等命令都是以大端法來解析內容的)。
下面是整數大小端轉換相關的函數。?

字節序概述

  1. 主機字節序(Host Byte Order)

    • 依賴于具體計算機系統的架構。

    • 在大多數現代計算機系統(如 x86 和 x86_64 架構的機器)中,主機字節序是小端序(Little-Endian),即低位字節存儲在低地址處。

    • 在某些系統(如某些 PowerPC 架構的機器)中,主機字節序是大端序(Big-Endian)。

  2. 網絡字節序(Network Byte Order)

    • 標準的網絡通信協議(如 TCP/IP)使用大端序(Big-Endian),即高位字節存儲在低地址處。

    • 這種字節序在跨平臺網絡通信中確保數據的一致性。

htonl
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
  • 作用:將32位的主機字節序整數轉換為網絡字節序。

  • 參數

    • hostlong:一個32位的無符號整數,表示主機字節序的值。

  • 返回值

    • 返回轉換后的32位無符號整數,表示網絡字節序的值。

  • 使用場景

    • 在發送32位數據(如IP地址)到網絡之前,需要將其從主機字節序轉換為網絡字節序。一般用于轉換IP地址

htons
#include <arpa/inet.h>
uint16_t htons(uint16_t hostshort);
  • 作用:將16位的主機字節序整數轉換為網絡字節序。

  • 參數

    • hostshort:一個16位的無符號整數,表示主機字節序的值。

  • 返回值

    • 返回轉換后的16位無符號整數,表示網絡字節序的值。

  • 使用場景

    • 在發送16位數據(如端口號)到網絡之前,需要將其從主機字節序轉換為網絡字節序。一般用于轉換端口號。

int main() {uint16_t port = 80; // 主機字節序的端口號uint16_t net_port = htons(port); // 轉換為網絡字節序(小端轉大端)uint32_t ip = 0xC0A80101; // 主機字節序的 IP 地址(192.168.1.1)uint32_t net_ip = htonl(ip); // 轉換為網絡字節序printf("Host port: %d\n", port);printf("Network port: %d\n", net_port);printf("Host IP: 0x%X\n", ip);printf("Network IP: 0x%X\n", net_ip);return 0;
}
ntohl
#include <arpa/inet.h>
uint32_t ntohl(uint32_t netlong);
  • 作用:將32位的網絡字節序整數轉換為主機字節序。

  • 參數

    • netlong:一個32位的無符號整數,表示網絡字節序的值。

  • 返回值

    • 返回轉換后的32位無符號整數,表示主機字節序的值。

  • 使用場景

    • 在從網絡接收32位數據(如IP地址)后,需要將其從網絡字節序轉換為主機字節序。一般用于轉換IP地址。

ntohs
#include <arpa/inet.h>
uint16_t ntohs(uint16_t netshort);
  • 作用:將16位的網絡字節序整數轉換為主機字節序。

  • 參數

    • netshort:一個16位的無符號整數,表示網絡字節序的值。

  • 返回值

    • 返回轉換后的16位無符號整數,表示主機字節序的值。

  • 使用場景

    • 在從網絡接收16位數據(如端口號)后,需要將其從網絡字節序轉換為主機字節序。一般用于轉換端口號

示例:

int main() {uint32_t hostlong = 0x12345678; // 主機字節序的32位整數uint16_t hostshort = 0x1234;   // 主機字節序的16位整數// 轉換為主機字節序到網絡字節序uint32_t netlong = htonl(hostlong);uint16_t netshort = htons(hostshort);printf("Host long: 0x%X\n", hostlong);printf("Network long: 0x%X\n", netlong);printf("Host short: 0x%X\n", hostshort);printf("Network short: 0x%X\n", netshort);// 轉換為網絡字節序到主機字節序uint32_t converted_hostlong = ntohl(netlong);uint16_t converted_hostshort = ntohs(netshort);printf("Converted host long: 0x%X\n", converted_hostlong);printf("Converted host short: 0x%X\n", converted_hostshort);return 0;
}

IP地址轉換

inet_aton

將點分十進制字符串(如 "192.168.1.1")轉換為二進制形式的 IPv4 地址,并存儲在 struct in_addr 結構體中。

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp);

參數

  • cp:指向點分十進制字符串的指針(如 "192.168.1.1")。

  • inp指向 struct in_addr 結構體的指針,用于存儲轉換后的二進制 IPv4 地址。

返回值

  • 成功時返回 1

  • 失敗時返回 0(例如,輸入的字符串格式不正確)。

inet_ntoa

將二進制形式的 IPv4 地址(存儲在 struct in_addr 結構體中)轉換為點分十進制字符串。

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
char *inet_ntoa(struct in_addr in);

參數

  • in:包含二進制 IPv4 地址的 struct in_addr 結構體。

返回值

  • 返回一個指向靜態分配的字符串的指針,該字符串表示點分十進制形式的 IPv4 地址。

  • 注意:返回的字符串是靜態分配的,因此每次調用 inet_ntoa 都會覆蓋之前的返回值。如果需要保存結果,建議將其復制到其他變量中。

示例:

#include <stdio.h>
#include <arpa/inet.h>int main(int argc,char* argv[]){//./inet_aton 127.0.0.1struct sockaddr_in addr;inet_aton(argv[1],&addr.sin_addr);//將點分十進制轉換成32位網絡字節序printf("addr = %x\n",addr.sin_addr.s_addr);printf("addr = %s\n",inet_ntoa(addr.sin_addr));//將32位網絡字節序轉換成點分十進制return 0;
}
/*輸出結果
(base) ubuntu@ubuntu:~/MyProject/Linux/net$ ./ip 192.168.1.1
addr = 101a8c0
addr = 192.168.1.1
*/

注意事項

線程安全性

inet_ntoa 不是線程安全的,因為它返回的是靜態分配的字符串。如果需要在多線程環境中使用,建議使用 inet_ntop 函數,它允許指定目標緩沖區。

inet_pton

將點分十進制字符串(IPv4)或冒號十六進制字符串(IPv6)轉換為二進制形式的 IP 地址。

int inet_pton(int af, const char *src, void *dst);
  • af:地址族,可以是 AF_INET(IPv4)或 AF_INET6(IPv6)。

  • src:指向點分十進制(IPv4)或冒號十六進制(IPv6)字符串的指針。

  • dst:指向存儲轉換后的二進制 IP 地址的緩沖區。

返回值

  • 成功時返回 1

  • 輸入字符串無效時返回 0

  • 地址族不支持時返回 -1

inet_ntop

將二進制形式的 IP 地址轉換為點分十進制字符串(IPv4)或冒號十六進制字符串(IPv6)。

const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
  • 參數

    • af:地址族,可以是 AF_INET(IPv4)或 AF_INET6(IPv6)。

    • src:指向二進制 IP 地址的指針。

    • dst:指向存儲轉換后的字符串的緩沖區。

    • size:緩沖區的大小。

  • 返回值

    • 成功時返回指向 dst 的指針。

    • 失敗時返回 NULL

示例:

int main(int argc,char* argv[]){struct sockaddr_in addr;inet_pton(AF_INET ,argv[1],&addr.sin_addr);//將點分十進制轉換成32位網絡字節序printf("addr = %x\n",addr.sin_addr.s_addr);char ip_addr[20];printf("addr = %s\n",inet_ntop(AF_INET, &addr.sin_addr.s_addr, ip_addr, 20));//將32位網絡字節序轉換成點分十進制return 0;
}
/*
輸出結果:
(base) ubuntu@ubuntu:~/MyProject/Linux/net$ ./ip2 192.168.1.1
addr = 101a8c0
addr = 192.168.1.1
*/

inet_addr?

用于將點分十進制的 IP 地址字符串(如 "192.168.1.1")轉換為一個 32 位的二進制形式的 IPv4 地址。

uint32_t inet_addr(const char *cp);

?參數

  • cp

    • 類型const char *

    • 作用:指向點分十進制格式的 IPv4 地址字符串(如 "192.168.1.1")。

返回值

  • 成功時返回 32 位的二進制形式的 IPv4 地址(網絡字節序)。

  • 如果輸入的字符串格式無效,返回 INADDR_NONE(通常定義為 0xFFFFFFFF)。

使用場景

inet_addr 函數通常用于將人類可讀的 IP 地址字符串轉換為程序可以使用的二進制形式。這在設置套接字地址結構(如 struct sockaddr_in)時非常有用。

在實際使用中,如果只需要簡單的 IPv4 地址轉換,inet_addr 是一個方便的選擇。可以直接返回 32 位的二進制地址。如果需要更復雜的處理或支持 IPv6,建議使用 inet_pton 或 inet_aton (線程不安全)

域名和IP地址的對應關系

IP層通過IP地址的結構進行路由選擇最終找到一條通往目的地的路由,但是一些著名的網站如果采用IP地址的方式提供地址,用戶將無法記憶,所以更多的時候需要一個方便人類記憶域名(比如www.kernel.org)作為其實際IP地址(145.40.73.55)的別名,顯然我們需要的一種機制去建立域名和IP地址的映射關系,一種方法是修改本機的hosts文件 /etc/hosts ,但是更加通用的方案是利用DNS協議,去訪問一個DNS服務器,服務器當中存儲了域名和IP地址的映射關系。與這個操作相關的函數是 gethostbyname ,下面是其用法:

struct hostent

用于存儲從域名解析服務(如 DNS)中獲取的主機信息,包括主機名、別名、IP 地址等。這個結構體定義在 <netdb.h> 頭文件中。

#include<netdb.h>struct hostent {char *h_name;        // 主機的官方名稱char **h_aliases;    // 主機的別名列表(以 NULL 結尾的數組)int h_addrtype;      // 地址類型(通常是 AF_INET 或 AF_INET6)int h_length;        // 地址的長度(以字節為單位)char **h_addr_list;  // 主機的地址列表(以 NULL 結尾的數組)
};

字段說明

  • h_name

    • 類型char *

    • 作用:主機的官方名稱(通常是主機的域名,如 "example.com")。

  • h_aliases

    • 類型char **

    • 作用:主機的別名列表,是一個以 NULL 結尾的字符串數組。主機可能有多個別名,這些別名存儲在這個數組中。

  • h_addrtype

    • 類型int

    • 作用:地址類型,通常為 AF_INET(IPv4)或 AF_INET6(IPv6)。

  • h_length

    • 類型int

    • 作用:地址的長度(以字節為單位)。對于 IPv4 地址,長度為 4 字節;對于 IPv6 地址,長度為 16 字節。

  • h_addr_list

    • 類型char **

    • 作用:主機的地址列表,是一個以 NULL 結尾的字符串數組。每個地址都是一個二進制形式的 IP 地址,存儲為字節數組。第一個地址通常是最主要的地址。

gethostbyname

根據主機名或域名獲取主機的 IP 地址信息。?

struct hostent *gethostbyname(const char *name);

參數

  • name:指向主機名或域名的字符串指針(如 "example.com""localhost")。

返回值

  • 成功時返回一個指向 struct hostent 的指針。

  • 失敗時返回 NULL,可以通過 h_errno 獲取錯誤原因(h_errno 是一個全局變量,用于存儲主機名解析的錯誤代碼)。

錯誤代碼

  • HOST_NOT_FOUND:主機名未找到。

  • TRY_AGAIN:暫時無法解析主機名(可能是 DNS 服務器未響應)。

  • NO_RECOVERY:無法從錯誤中恢復。

  • NO_ADDRESS:主機名有效,但沒有找到對應的地址。

  • gethostbyname 不是線程安全的,因為它返回的是靜態分配的 struct hostent。在多線程環境中,建議使用 getaddrinfo,它返回動態分配的結構體。
  • gethostbyname 僅支持 IPv4 地址。如果需要支持 IPv6,建議使用 getaddrinfo,因為它可以同時處理 IPv4 和 IPv6 地址。

示例:

int main() {struct hostent *host;char *hostname = "www.taobao.com";// 獲取主機信息host = gethostbyname(hostname);if (host == NULL) {perror("gethostbyname");return 1;}// 打印主機的官方名稱printf("Official name: %s\n", host->h_name);// 打印主機的別名printf("Aliases:\n");for (char **alias = host->h_aliases; *alias != NULL; alias++) {printf("  %s\n", *alias);}// 打印主機的地址類型printf("Address type: %s\n", (host->h_addrtype == AF_INET) ? "AF_INET" : "AF_INET6");// 打印主機的地址printf("Addresses:\n");for (char **addr = host->h_addr_list; *addr != NULL; addr++) {printf("  %s\n", inet_ntoa(*((struct in_addr *)*addr)));}return 0;
}
/*
(base) ubuntu@ubuntu:~/MyProject/Linux/net$ ./dns
Official name: www.taobao.com.danuoyi.tbcache.com
Aliases:www.taobao.com
Address type: AF_INET
Addresses:222.192.186.120222.192.186.122
*/

struct addrinfo

struct addrinfo {int ai_flags;          // 查詢標志int ai_family;         // 地址族(如 AF_INET 或 AF_INET6)int ai_socktype;       // 套接字類型(如 SOCK_STREAM 或 SOCK_DGRAM)int ai_protocol;       // 協議(如 IPPROTO_TCP 或 IPPROTO_UDP)socklen_t ai_addrlen;  // 地址長度struct sockaddr *ai_addr; // 地址結構體char *ai_canonname;    // 規范化的主機名struct addrinfo *ai_next; // 指向下一個結果的指針
};
  • ai_flags

    • 類型int

    • 作用:查詢標志,用于指定查詢的偏好選項。常見的標志包括:

      • AI_PASSIVE:用于服務器端,表示返回的地址適用于 bind 函數。

      • AI_CANONNAME:返回規范化的主機名。

      • AI_NUMERICHOST:要求 node 是一個數字形式的地址(如 IP 地址)。

      • AI_NUMERICSERV:要求 service 是一個數字形式的端口號。

      • AI_V4MAPPED:如果查詢 IPv6 地址,但主機只有 IPv4 地址,則返回 IPv4 映射的 IPv6 地址。

      • AI_ALL:返回所有匹配的地址(IPv4 和 IPv6)。

      • AI_ADDRCONFIG:僅返回當前主機支持的地址族。

  • ai_family

    • 類型int

    • 作用:地址族,指定地址的類型。常見的值包括:

      • AF_INET:IPv4 地址。

      • AF_INET6:IPv6 地址。

      • AF_UNSPEC:不指定地址族,允許返回 IPv4 或 IPv6 地址。

  • ai_socktype

    • 類型int

    • 作用:套接字類型,指定套接字的類型。常見的值包括:

      • SOCK_STREAM:TCP 套接字。

      • SOCK_DGRAM:UDP 套接字。

      • SOCK_RAW:原始套接字。

  • ai_protocol

    • 類型int

    • 作用:協議類型,指定使用的協議。常見的值包括:

      • IPPROTO_TCP:TCP 協議。

      • IPPROTO_UDP:UDP 協議。

      • IPPROTO_RAW:原始協議。

  • ai_addrlen

    • 類型socklen_t

    • 作用:地址的長度(以字節為單位)。

  • ai_addr

    • 類型struct sockaddr *

    • 作用:指向地址結構體的指針,存儲主機的地址信息。根據 ai_family 的值,可以將其強制轉換為 struct sockaddr_in(IPv4)或 struct sockaddr_in6(IPv6)。

  • ai_canonname

    • 類型char *

    • 作用:規范化的主機名。如果設置了 AI_CANONNAME 標志,此字段將包含主機的規范化名稱。

  • ai_next

    • 類型struct addrinfo *

    • 作用:指向鏈表中的下一個 struct addrinfo 結構體的指針。如果為 NULL,表示鏈表結束。

getaddrinfo

getaddrinfo 用于根據主機名或服務名獲取主機的地址信息。它是一個現代的替代品,用于替代傳統的 gethostbynamegetservbyname 等函數,因為它支持 IPv4 和 IPv6,并且提供了更靈活的接口。?

int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res);

參數

  • node

    • 類型const char *

    • 作用:主機名或 IP 地址字符串(如 "example.com""192.168.1.1")。

    • 可選:如果為 NULL,則表示本地主機。

  • service

    • 類型const char *

    • 作用:服務名或端口號字符串(如 "http""80")。

    • 可選:如果為 NULL,則不解析服務名。

  • hints

    • 類型const struct addrinfo *

    • 作用:一個指向 struct addrinfo 的指針,用于指定查詢的偏好選項。

    • 可選:如果為 NULL,則默認查詢所有可能的地址和協議。

  • res

    • 類型struct addrinfo **

    • 作用:一個指向指針的指針,用于存儲查詢結果。查詢結果是一個鏈表,每個節點都是一個 struct addrinfo 結構體。

返回值

  • 成功時返回 0

  • 失敗時返回一個非零的錯誤碼(如 EAI_AGAINEAI_NONAME 等)。

#include<54func.h>
int main(int argc, char const *argv[])
{ARGS_CHECK(argc, 2);struct addrinfo hints, *res;int ret;char ipstr[INET6_ADDRSTRLEN];memset(&hints, 0, sizeof(hints));hints.ai_family = AF_UNSPEC;hints.ai_socktype = SOCK_STREAM;ret = getaddrinfo(argv[1], "https", &hints, &res);THREAD_ERROR_CHECK(ret, "getaddrinfo");printf("IP addresses for %s:\n\n", argv[1]);for(struct addrinfo *p = res; p != NULL; p = p->ai_next){void *addr;char *ipver;if(p->ai_family == AF_INET){struct sockaddr_in *ipv4 = (struct sockaddr_in *)p->ai_addr;addr = &(ipv4->sin_addr);ipver = "IPV4";}else{struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)p->ai_addr;addr = &(ipv6->sin6_addr);ipver = "IPV6";}inet_ntop(p->ai_family, addr, ipstr, sizeof(ipstr));printf("  %s: %s\n", ipver, ipstr);}freeaddrinfo(res); //釋放動態分配的內存return 0;
}
/*
(base) ubuntu@ubuntu:~/MyProject/Linux/net$ ./dns2 www.taobao.com
IP addresses for www.taobao.com:IPV6: 2001:da8:20d:40db:3::3d2IPV6: 2001:da8:20d:40db:3::3d1IPV4: 222.192.186.120IPV4: 222.192.186.122
*/

?TCP 通信

下面是使用TCP通信的流程圖:

socket

socket 函數用于創建一個 socket 套接字。它是網絡通信的端點。通過 socket 函數,程序可以創建一個用于網絡通信的套接字,并指定其類型和協議。

int socket(int domain, int type, int protocol);

?參數

  • domain

    • 類型int

    • 作用:指定地址族(協議族),常見的值包括:

      • AF_INET:IPv4 地址族。

      • AF_INET6:IPv6 地址族。

      • AF_UNIXAF_LOCAL:本地通信(Unix 域套接字)。

      • AF_UNSPEC:不指定地址族,通常用于 getaddrinfo 的結果。

  • type

    • 類型int

    • 作用:指定套接字類型,常見的值包括:

      • SOCK_STREAM:面向連接的流式套接字(TCP)。

      • SOCK_DGRAM:無連接的數據報套接字(UDP)。

      • SOCK_RAW:原始套接字,允許直接訪問底層協議。

      • SOCK_SEQPACKET:有序的、可靠的、面向連接的、固定大小的數據報套接字。

      • SOCK_RDM:可靠的無連接數據報套接字。

  • protocol

    • 類型int

    • 作用指定協議,通常為 0,表示使用默認協議。常見的協議包括:

      • IPPROTO_TCP:TCP 協議(用于 SOCK_STREAM)。

      • IPPROTO_UDP:UDP 協議(用于 SOCK_DGRAM)。

      • IPPROTO_RAW:原始協議(用于 SOCK_RAW)。

返回值

  • 成功時返回一個非負的套接字描述符(文件描述符)。

  • 失敗時返回 -1,并設置 errno 以指示錯誤原因。

bind

bind 函數是網絡編程中用于將一個套接字綁定到一個本地地址和端口的函數。它通常用于服務器端,用于指定服務器監聽的本地地址和端口。綁定后,套接字會與指定的地址和端口關聯起來,從而允許服務器接收來自客戶端的連接請求或數據。服務器建立連接時必須使用bind綁定端口,客戶端一般不需要。

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

參數

  • sockfd

    • 類型int

    • 作用:套接字描述符,由 socket 函數創建。它標識了服務器用于通信的套接字。

  • addr

    • 類型const struct sockaddr *

    • 作用:指向 struct sockaddr 或其派生類型(如 struct sockaddr_instruct sockaddr_in6)的指針,存儲本地地址信息。對于 IPv4,通常使用 struct sockaddr_in;對于 IPv6,使用 struct sockaddr_in6

  • addrlen

    • 類型socklen_t

    • 作用addr 指向的地址結構體的大小(以字節為單位)。對于 struct sockaddr_in,大小通常為 sizeof(struct sockaddr_in);對于 struct sockaddr_in6,大小通常為 sizeof(struct sockaddr_in6)

返回值

  • 成功時返回 0

  • 失敗時返回 -1,并設置 errno 以指示錯誤原因。

?示例:

int main(int argc, char *argv[]) {ARGS_CHECK(argc, 3);int sockfd;struct sockaddr_in server_addr;// 創建一個 TCP 套接字sockfd = socket(AF_INET, SOCK_STREAM, 0);ERROR_CHECK(sockfd, -1, "socket");// 初始化服務器地址memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;          // 地址族server_addr.sin_addr.s_addr = inet_addr(argv[1]);server_addr.sin_port = htons(atoi(argv[2]));        // 服務器端口號// 綁定套接字到本地地址int ret = bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));ERROR_CHECK(ret, -1, "bind");// 關閉套接字close(sockfd);return 0;
}

connect

客戶端使用 connect 來建立和TCP服務端的連接。

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

參數

  • sockfd

    • 類型int

    • 作用:套接字描述符,由 socket 函數創建。它標識了客戶端用于通信的套接字。

  • addr

    • 類型const struct sockaddr *

    • 作用:指向 struct sockaddr 或其派生類型(如 struct sockaddr_instruct sockaddr_in6)的指針,存儲服務器的地址信息。對于 IPv4,通常使用 struct sockaddr_in;對于 IPv6,使用 struct sockaddr_in6

  • addrlen

    • 類型socklen_t

    • 作用addr 指向的地址結構體的大小(以字節為單位)。對于 struct sockaddr_in,大小通常為 sizeof(struct sockaddr_in);對于 struct sockaddr_in6,大小通常為 sizeof(struct sockaddr_in6)

返回值

  • 成功時返回 0

  • 失敗時返回 -1,并設置 errno 以指示錯誤原因。

listen

listen 函數是網絡編程中用于將一個套接字轉換為被動套接字(即監聽套接字)的函數。它通常用于服務器端,用于使套接字進入監聽狀態,等待客戶端的連接請求。listen 函數是 TCP 服務器編程中的關鍵步驟之一。

int listen(int sockfd, int backlog);

?一旦啟用了 listen 之后,操作系統就知道該套接字是服務端的套接字,操作系統內核就不再啟用其發送和接收緩沖區,轉而在內核區維護兩個隊列結構:半連接隊列 和 全連接隊列。半連接隊列用于管理成功第一次握手的連接,全連接隊列用于管理已經完成三次握手的隊列。 backlog 在有些操作系統用來指明半連接隊列和全連接隊列的長度之和,一般填一列個正數即可。如果隊列已經滿了,那么服務端受到任何再發起的連接都會直接丟棄(大部分操作系統中服務端不會回復RST,以方便客戶端自動重傳)

    ?參數

    • sockfd

      • 類型int

      • 作用:套接字描述符,由 socket 函數創建。它標識了服務器用于通信的套接字。

    • backlog

      • 類型int

      • 作用:指定未完成連接隊列的最大長度。當有多個客戶端同時嘗試連接時,backlog 參數決定了服務器可以暫存的未完成連接的數量。

    返回值

    • 成功時返回 0

    • 失敗時返回 -1,并設置 errno 以指示錯誤原因。

    accept

    accept 函數由服務端調用,用于從全連接隊列中取出下一個已經完成的TCP連接。如果全連接隊列為空,那么 accept 會陷入阻塞。一旦全連接隊列中到來新的連接,此時 accept 操作就會就緒,這種就緒是讀操作就緒,所以可以使用 select 函數的讀集合進行監聽。當accept 執行完了之后,內核會創建一個新的套接字文件對象 ,該文件對象關聯的文件描述符是 accept 的返回值,文件對象當中最重要的結構是一個發送緩沖區和接收緩沖區,可以用于服務端通過TCP連接發送和接收TCP段。

    區分兩個套接字是非常重要的。通過把舊的管理連接隊列的套接字稱作監聽套接字,而新的用于發送和接收TCP段的套接字稱作已連接套接字。通常來說,監聽套接字會一直存在,負責建立各個不同的TCP連接(只要源IP、源端口、目的IP、目的端口四元組任意一個字段有區別,就是一個新的TCP連接),而某一條單獨的TCP連接則是由其對應的已連接套接字進行數據通信的。客戶端使用 close 關閉套接字或者服務端使用 close 關閉已連接套接字的時候就是主動發起斷開連接四次揮手的過程。

    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

    ?參數

    • sockfd

      • 類型int

      • 作用:監聽套接字描述符,由 socket 函數創建并經過 listen 轉換為被動套接字。它標識了服務器用于接收連接請求的套接字。

    • addr

      • 類型struct sockaddr *

      • 作用:指向 struct sockaddr 或其派生類型(如 struct sockaddr_instruct sockaddr_in6)的指針,用于存儲客戶端的地址信息。如果不需要客戶端的地址信息,可以設置為 NULL

    • addrlen

      • 類型socklen_t *

      • 作用:指向一個 socklen_t 類型的變量,該變量在調用前應設置為 addr 指向的地址結構體的大小(以字節為單位)。調用后,addrlen 指向的變量將被設置為實際存儲在 addr 中的地址長度。如果 addrNULL,則 addrlen 也應為 NULL

    返回值

    • 成功時返回一個新的套接字描述符,用于與客戶端通信。

    • 失敗時返回 -1,并設置 errno 以指示錯誤原因。

    send

    send 和 recv 用于將數據在用戶態空間和內核態的緩沖區之間進行傳輸,無論是客戶端還是服務端均可使用,但是只能用于TCP連接。將數據拷貝到內核態并不意味著會馬上傳輸,而是會根據時機再由內核協議棧按照協議的規范進行分節,通常緩沖區如果數據過多會分節成MSS的大小,然后根據窗口條件傳輸到網絡層之中。

    send 函數是網絡編程中用于向已連接的套接字發送數據的函數。它通常用于 TCP 套接字,但也可以用于已連接的 UDP 套接字。send 函數是 write 函數在網絡編程中的等價物,用于將數據發送到對端。

    ssize_t send(int sockfd, const void *buf, size_t len, int flags);

    參數

    • sockfd

      • 類型int

      • 作用:套接字描述符,標識了用于發送數據的套接字。該套接字必須已經通過 connect 函數連接到對端(對于 TCP 套接字)或通過 sendto 函數發送數據(對于 UDP 套接字)。

    • buf

      • 類型const void *

      • 作用:指向要發送的數據的緩沖區。

    • len

      • 類型size_t

      • 作用:要發送的數據的長度(以字節為單位)。

    • flags

      • 類型int

      • 作用:發送標志,通常設置為 0。可以設置的標志包括:

        • MSG_OOB:發送帶外數據(僅適用于 TCP 套接字)。

        • MSG_DONTROUTE:跳過路由表,直接發送數據(僅適用于 TCP 套接字)。

        • MSG_DONTWAIT:非阻塞發送(僅適用于非阻塞套接字)。

        • MSG_NOSIGNAL:發送數據時不會產生 SIGPIPE 信號(僅適用于 TCP 套接字)。

    返回值

    • 成功時返回實際發送的字節數(可能小于請求的字節數)。

    • 失敗時返回 -1,并設置 errno 以指示錯誤原因。

    recv

    recv 函數是網絡編程中用于從已連接的套接字接收數據的函數。它通常用于 TCP 套接字,但也可以用于已連接的 UDP 套接字。recv 函數是 read 函數在網絡編程中的等價物,用于從對端接收數據。

    ssize_t recv(int sockfd, void *buf, size_t len, int flags);

    參數

    • sockfd

      • 類型int

      • 作用:套接字描述符,標識了用于接收數據的套接字。該套接字必須已經通過 connect 函數連接到對端(對于 TCP 套接字)或通過 bind 函數綁定到本地地址(對于 UDP 套接字)。

    • buf

      • 類型void *

      • 作用:指向接收數據的緩沖區。

    • len

      • 類型size_t

      • 作用:緩沖區的大小(以字節為單位),表示可以接收的最大數據量。

    • flags

      • 類型int

      • 作用:接收標志,通常設置為 0。可以設置的標志包括:

        • MSG_OOB:接收帶外數據(僅適用于 TCP 套接字)。

        • MSG_PEEK:查看數據但不移除它(即“偷看”數據)。

        • MSG_WAITALL:阻塞直到接收到請求的所有數據。

        • MSG_DONTWAIT:非阻塞接收(僅適用于非阻塞套接字)。

    返回值

    • 成功時返回接收到的字節數。

    • 如果對端關閉連接,返回 0

    • 失敗時返回 -1,并設置 errno 以指示錯誤原因。

    示例:使用以上函數完成一次簡單的服務端和客戶端之間的信息發送

    client.c
    int main(int argc, char const *argv[])
    {ARGS_CHECK(argc,3);int sockfd;struct sockaddr_in client_addr;sockfd = socket(AF_INET, SOCK_STREAM, 0);ERROR_CHECK(sockfd, -1, "socket");memset(&client_addr, 0, sizeof(client_addr));client_addr.sin_family = AF_INET;client_addr.sin_addr.s_addr = inet_addr(argv[1]);client_addr.sin_port = htons(atoi(argv[2]));int ret = connect(sockfd, (struct sockaddr*)&client_addr, sizeof(client_addr));sleep(3);char *str = "hi I am client\n";char buf[50];ret = send(sockfd, str, strlen(str), 0);ERROR_CHECK(ret, -1, "send");ret = recv(sockfd, buf, sizeof(buf), 0);ERROR_CHECK(ret, -1, "recv");printf("%s\n", buf);return 0;
    }server.c
    int main(int argc, char const *argv[])
    {ARGS_CHECK(argc,3);int sockfd;struct sockaddr_in server_addr;sockfd = socket(AF_INET, SOCK_STREAM, 0);ERROR_CHECK(sockfd, -1, "socket");memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = inet_addr(argv[1]);server_addr.sin_port = htons(atoi(argv[2]));int ret = bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));ERROR_CHECK(ret, -1, "bind");printf("waiting connect...\n");ret = listen(sockfd, 10);           //放入監聽集合ERROR_CHECK(ret, -1, "listen");int newFd = accept(sockfd, NULL, NULL);  //從就緒集合中取出printf("client connected!\n");char buf[50];char *str = "hello";memset(buf, 0, sizeof(buf));ret = recv(newFd, buf, sizeof(buf), 0);ERROR_CHECK(ret, 0, "recv");printf("%s\n", buf);printf("send hello to client\n");send(newFd, str, strlen(str), 0);close(newFd);close(sockfd);return 0;
    }//client
    (base) ubuntu@ubuntu:~/MyProject/Linux/net$ ./client 127.0.0.1 1255
    hello
    //server
    (base) ubuntu@ubuntu:~/MyProject/Linux/net$ ./server 127.0.0.1 1255
    waiting connect...
    client connected!
    hi I am clientsend hello to client
    

    需要特別注意的是, send 和 recv 的次數和網絡上傳輸的TCP段的數量沒有關系,多次的send 和 recv 可能只需要一次TCP段的傳輸。另外一方面,TCP是一種流式的通信協議,消息是以字節流的方式在信道中傳輸,這就意味著一個重要的事情,消息和消息之間是沒有邊界的。在不加額外約定的情況下,通信雙方并不知道發送和接收到底有沒有接收完一個消息,有可能多個消息會在一次傳輸中被發送和接收(江湖俗稱"粘包"),也有有可能一個消息需要多個傳輸才能被完整的發送和接收(江湖俗稱"半包")。

    實戰:使用select實現TCP客戶端與服務端即時聊天

    基于TCP的聊天程序的實現思路和之前利用管道實現即時聊天的思路是一致的。客戶端和服務端都需要使用 select 這種IO多路復用機制監聽讀事件,客戶端需要監聽套接字的讀緩沖區以及標準輸入,服務端需要監聽已連接套接字的讀緩沖區以及標準輸入。

    //client.c
    int main(int argc, char const *argv[])
    {ARGS_CHECK(argc,3);int sockfd;struct sockaddr_in client_addr;sockfd = socket(AF_INET, SOCK_STREAM, 0);ERROR_CHECK(sockfd, -1, "socket");memset(&client_addr, 0, sizeof(client_addr));client_addr.sin_family = AF_INET;client_addr.sin_addr.s_addr = inet_addr(argv[1]);client_addr.sin_port = htons(atoi(argv[2]));printf("connecting to server\n");int ret = connect(sockfd, (struct sockaddr*)&client_addr, sizeof(client_addr));printf("connected\n");char buf[1024];fd_set rdset;while (1){FD_ZERO(&rdset);FD_SET(STDIN_FILENO, &rdset);FD_SET(sockfd, &rdset);select(sockfd+1, &rdset, NULL, NULL, NULL);if(FD_ISSET(STDIN_FILENO, &rdset)){memset(buf, 0, sizeof(buf));ret = read(STDIN_FILENO, buf, sizeof(buf));ERROR_CHECK(ret, -1, "read");if(strcmp(buf, "disconnect\n") == 0){printf("disconnected\n");close(sockfd);break;}send(sockfd, buf, strlen(buf)-1, 0);}if(FD_ISSET(sockfd, &rdset)){memset(buf, 0, sizeof(buf));ret = recv(sockfd, buf, sizeof(buf), 0);ERROR_CHECK(ret, -1, "read");if(ret == 0){printf("server disconnected\n");break;}printf("server: %s\n",buf);}}close(sockfd);return 0;
    }
    /server.c
    int main(int argc, char const *argv[])
    {ARGS_CHECK(argc,3);int sockfd;struct sockaddr_in server_addr;sockfd = socket(AF_INET, SOCK_STREAM, 0);ERROR_CHECK(sockfd, -1, "socket");memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = inet_addr(argv[1]);server_addr.sin_port = htons(atoi(argv[2]));int ret = bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));ERROR_CHECK(ret, -1, "bind");printf("waiting to connect...\n");ret = listen(sockfd, 10);           //放入監聽集合ERROR_CHECK(ret, -1, "listen");int newFd = accept(sockfd, NULL, NULL);  //從就緒集合中取出ERROR_CHECK(newFd, -1, "accept");printf("client connected!\n");char buf[1024];fd_set rdset;while (1){FD_ZERO(&rdset);FD_SET(STDIN_FILENO, &rdset);FD_SET(newFd, &rdset);select(newFd+1, &rdset, NULL, NULL, NULL);if(FD_ISSET(STDIN_FILENO, &rdset)){memset(buf, 0, sizeof(buf));ret = read(STDIN_FILENO, buf, sizeof(buf));ERROR_CHECK(ret, -1, "read");if(strcmp(buf, "disconnect\n") == 0){printf("disconnected\n");close(newFd);close(sockfd);break;}send(newFd, buf, strlen(buf)-1, 0);}if(FD_ISSET(newFd, &rdset)){memset(buf, 0, sizeof(buf));ret = recv(newFd, buf, sizeof(buf), 0);ERROR_CHECK(ret, -1, "read");if(ret == 0){printf("client disconnected\n");close(newFd);close(sockfd);return 0;}printf("client: %s\n",buf);}}return 0;
    }
    /*
    //server
    (base) ubuntu@ubuntu:~/MyProject/Linux/net$ ./server 192.168.1.2 1236
    waiting to connect...
    client connected!
    client: hello i am client!
    hi i am server!
    what are you doing?
    client: emmm... learning linux c.
    client: i want to offline, byebye!
    ok
    client disconnected
    //client
    (base) ubuntu@ubuntu:~/MyProject/Linux/net$ ./client 192.168.1.2 1236
    connecting to server
    connected
    hello i am client! 
    server: hi i am server!
    server: what are you doing?
    emmm... learning linux c.
    i want to offline, byebye!
    server: ok
    ^C
    */
    

    TIME_WAIT和setsockopt

    如果是服務端主動調用 close 斷開的連接,即服務端是四次揮手的主動關閉方,由之前的TCP狀態轉換圖可知,主動關閉方在最后會處于一個固定2MSL時長的TIME_WAIT等待時間。在此狀態期間,如果嘗試使用 bind 系統調用對重復的地址進行綁定操作,那么會報錯。

    $ ./server_tcpchat1 192.168.135.132 2778
    bind: Address already in use
    $ netstat -an|grep 2778
    tcp
    0
    0 192.168.135.132:2778
    192.168.135.133:57466
    TIME_WAIT

    使用 select 對 socket 進行斷線重連

    服務端除了接收緩沖區和標準輸入以外,還有一個操作也會造成阻塞,那就是 accept 操作,實際上服務端可以使用 select 管理監聽套接字,檢查其全連接隊列是否存在已經建好的連接,如果存在連接,那么其讀事件即 accept 操作便就緒。將監聽套接字加入監聽會導致服務端的代碼發生一個結構變化:

    • 每次重新調用 select 之前需要提前準備好要監聽的文件描述符,這些文件描述符當中可能會包括新的已連接套接字的文件描述符。
    • select 的第一個參數應當足夠大,從而避免無法監聽到新的已連接套接字的文件描述符(它們的數值可能會比較大)。
    • 需要處理 accept 就緒的情況。
    #include<54func.h>int main(int argc, char const *argv[])
    {ARGS_CHECK(argc,3);int sockfd, newFd;struct sockaddr_in server_addr;sockfd = socket(AF_INET, SOCK_STREAM, 0);ERROR_CHECK(sockfd, -1, "socket");memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = inet_addr(argv[1]);server_addr.sin_port = htons(atoi(argv[2]));int ret = bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));ERROR_CHECK(ret, -1, "bind");printf("waiting to connect...\n");ret = listen(sockfd, 10);           //放入監聽集合ERROR_CHECK(ret, -1, "listen");//needMonitorSetorset 是目前需要監聽的集合,為rdset提供監聽集合,它本身并不會被監聽char buf[1024];fd_set rdset, needMonitorSetorset;FD_ZERO(&rdset);FD_ZERO(&needMonitorSetorset);   FD_SET(STDIN_FILENO, &needMonitorSetorset);FD_SET(sockfd, &needMonitorSetorset);while (1){memcpy(&rdset, &needMonitorSetorset, sizeof(rdset));ret = select(10, &rdset, NULL, NULL, NULL);ERROR_CHECK(ret, -1, "select");if(FD_ISSET(STDIN_FILENO, &rdset)){memset(buf, 0, sizeof(buf));ret = read(STDIN_FILENO, buf, sizeof(buf));ERROR_CHECK(ret, -1, "read");if(strcmp(buf, "disconnect\n") == 0){printf("disconnected\n");close(newFd);close(sockfd);break;}send(newFd, buf, strlen(buf)-1, 0);}else if(FD_ISSET(newFd, &rdset)){memset(buf, 0, sizeof(buf));ret = recv(newFd, buf, sizeof(buf), 0);ERROR_CHECK(ret, -1, "read");if(ret == 0){printf("client disconnected\n");close(newFd);FD_CLR(newFd, &needMonitorSetorset);continue;}printf("client: %s\n",buf);}else if(FD_ISSET(sockfd, &rdset)){struct sockaddr_in cliAddr;memset(&cliAddr, 0, sizeof(cliAddr));socklen_t sockLen = sizeof(cliAddr);printf("sockLen=%d\n",sockLen);newFd = accept(sockfd, (struct sockaddr*)&cliAddr, &sockLen);ERROR_CHECK(newFd, -1, "accept");FD_SET(newFd, &needMonitorSetorset);printf("sockLen = %d\n",sockLen);printf("newFd = %d is connected\n", newFd);printf("ip is: %s, port is: %d\n", inet_ntoa(cliAddr.sin_addr), ntohs(cliAddr.sin_port));}}close(newFd);close(sockfd);return 0;
    }
    /*
    client
    (base) ubuntu@ubuntu:~/MyProject/Linux/net$ ./client 192.168.1.1 1236
    connecting to server
    connected
    hello server
    server: what are you doing?
    I will go
    ^C
    (base) ubuntu@ubuntu:~/MyProject/Linux/net$ ./client 192.168.1.1 1236
    connecting to server
    connected
    I am back!
    byebye!
    ^C
    server
    (base) ubuntu@ubuntu:~/MyProject/Linux/net$ ./server2 192.168.1.1 1236
    waiting to connect...
    sockLen=16
    sockLen = 16
    newFd = 4 is connected
    ip is: 192.168.1.1, port is: 58290
    client: hello server
    what are you doing?
    client: I will go
    client disconnected
    sockLen=16
    sockLen = 16
    newFd = 4 is connected
    ip is: 192.168.1.1, port is: 60632
    client: I am back!
    client: byebye!
    client disconnected
    ^C
    */

    實戰:使用epoll實現TCP客戶端與服務端即時聊天

    在之前一節文章中,對IO多路復用的 select 和 epoll 進行了總結,實際開發中,epoll用的更廣泛且效率也更高。在實現方面,epoll基本上與select差不多也是進行初始化將監聽集合加入即可,不過簡化掉了每次循環都需要將監聽對象重新加入的步驟。一次加入,即可永久監聽。

    client.cint main(int argc, char const *argv[])
    {ARGS_CHECK(argc, 3);int sockfd = socket(AF_INET, SOCK_STREAM, 0);ERROR_CHECK(sockfd, -1, "socket");struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = inet_addr(argv[1]);server_addr.sin_port = htons(atoi(argv[2]));printf("waiting for connect\n");int ret = connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));ERROR_CHECK(ret, -1, "coonect");printf("connected\n");int epfd = epoll_create(1);ERROR_CHECK(epfd, -1, "epoll_create");struct epoll_event ev, evs[2];ev.events = EPOLLIN;ev.data.fd = STDIN_FILENO;ret = epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev);ERROR_CHECK(ret, -1, "epoll_ctl");ev.data.fd = sockfd;ret = epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);ERROR_CHECK(ret, -1, "epoll_ctl");char buf[1024];int readyNum = 0;while (1){readyNum = epoll_wait(epfd, evs, 2, -1);for(int i = 0; i < readyNum; i++){if(evs[i].data.fd ==  STDIN_FILENO){memset(buf, 0, sizeof(buf));read(STDIN_FILENO, buf, sizeof(buf));send(sockfd, buf, strlen(buf), 0);}else if(evs[i].data.fd == sockfd){memset(buf, 0, sizeof(buf));ret = read(sockfd, buf, sizeof(buf));if(ret == 0){printf("disconnected\n");close(sockfd);return 0;}printf("server: %s", buf);}  }}return 0;
    }server.cint main(int argc, char const *argv[])
    {ARGS_CHECK(argc, 3);int sockfd = socket(AF_INET, SOCK_STREAM, 0);ERROR_CHECK(sockfd, -1, "socket");struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = inet_addr(argv[1]);server_addr.sin_port = htons(atoi(argv[2]));printf("waiting for connect\n");int ret = bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));ERROR_CHECK(ret, -1, "bind");ret = listen(sockfd, 10);           //放入監聽集合ERROR_CHECK(ret, -1, "listen");int newfd = accept(sockfd, NULL, NULL);ERROR_CHECK(newfd, -1, "accept");printf("connected:%d\n", newfd);int epfd = epoll_create(1);ERROR_CHECK(epfd, -1, "epoll_create");struct epoll_event ev, evs[2];ev.events = EPOLLIN;ev.data.fd = STDIN_FILENO;ret = epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev);ERROR_CHECK(ret, -1, "epoll_ctl");ev.data.fd = newfd;ret = epoll_ctl(epfd, EPOLL_CTL_ADD, newfd, &ev);ERROR_CHECK(ret, -1, "epoll_ctl");char buf[1024];int readyNum = 0;while (1){readyNum = epoll_wait(epfd, evs, 2, -1);for(int i = 0; i < readyNum; i++){if(evs[i].data.fd ==  STDIN_FILENO){memset(buf, 0, sizeof(buf));read(STDIN_FILENO, buf, sizeof(buf));send(newfd, buf, strlen(buf), 0);}else if(evs[i].data.fd == newfd){memset(buf, 0, sizeof(buf));ret = read(newfd, buf, sizeof(buf));if(ret == 0){printf("disconnected\n");close(newfd);close(sockfd);return 0;}printf("client: %s", buf);}  }}return 0;
    }
    //client
    base) ubuntu@ubuntu:~/MyProject/Linux/net$ ./client5 172.20.74.205 1236
    waiting for connect
    connected
    hello i am client!
    how do you do?
    server: very good!
    server: thank you!
    server: goodbye
    ok
    ^C
    //server
    (base) ubuntu@ubuntu:~/MyProject/Linux/net$ ./server5 172.20.74.205 1236
    waiting for connect
    connected:4
    client: hello i am client!
    client: how do you do?
    very good!
    thank you!
    goodbye
    client: ok
    disconnected
    

    UDP通信

    UDP相對于TCP減少了建立連接部分,客戶端設置好通信地址后直接使用 sendto 和 recvfrom 即可通信,服務端綁定好套接字后不需要監聽。直接阻塞等待客戶端。需要注意的是,服務端一般不知道客戶端的地址,但客戶端知道服務端的信息,需要先等待客戶端發送消息才能確定客戶端的地址。

    ?sendto

    sendto 函數是網絡編程中用于向指定地址發送數據的函數,通常用于無連接的套接字(如 UDP 套接字)。它允許發送方指定目標地址和端口,而不需要事先建立連接。sendto 函數是 UDP 通信中的關鍵函數,也可以用于已連接的 TCP 套接字。

    ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);

    參數

    • sockfd

      • 類型int

      • 作用:套接字描述符,標識了用于發送數據的套接字。該套接字可以是 UDP 套接字或已連接的 TCP 套接字。

    • buf

      • 類型const void *

      • 作用:指向要發送的數據的緩沖區。

    • len

      • 類型size_t

      • 作用:要發送的數據的長度(以字節為單位)。

    • flags

      • 類型int

      • 作用:發送標志,通常設置為 0。可以設置的標志包括:

        • MSG_OOB:發送帶外數據(僅適用于 TCP 套接字)。

        • MSG_DONTROUTE:跳過路由表,直接發送數據(僅適用于 TCP 套接字)。

        • MSG_DONTWAIT:非阻塞發送(僅適用于非阻塞套接字)。

        • MSG_NOSIGNAL:發送數據時不會產生 SIGPIPE 信號(僅適用于 TCP 套接字)。

    • dest_addr

      • 類型const struct sockaddr *

      • 作用:指向目標地址結構體的指針,存儲目標地址和端口信息。對于 IPv4,通常使用 struct sockaddr_in;對于 IPv6,使用 struct sockaddr_in6

    • addrlen

      • 類型socklen_t

      • 作用dest_addr 指向的地址結構體的大小(以字節為單位)。對于 struct sockaddr_in,大小通常為 sizeof(struct sockaddr_in);對于 struct sockaddr_in6,大小通常為 sizeof(struct sockaddr_in6)

    返回值

    • 成功時返回實際發送的字節數(可能小于請求的字節數)。

    • 失敗時返回 -1,并設置 errno 以指示錯誤原因。

    recvfrom

    recvfrom 函數是網絡編程中用于從無連接的套接字(如 UDP 套接字)接收數據的函數。它允許接收方獲取發送方的地址信息,而不需要事先建立連接。recvfrom 函數是 UDP 通信中的關鍵函數,也可以用于已連接的 TCP 套接字。

    ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

    參數

    • sockfd

      • 類型int

      • 作用:套接字描述符,標識了用于接收數據的套接字。該套接字可以是 UDP 套接字或已連接的 TCP 套接字。

    • buf

      • 類型void *

      • 作用:指向接收數據的緩沖區。

    • len

      • 類型size_t

      • 作用:緩沖區的大小(以字節為單位),表示可以接收的最大數據量。

    • flags

      • 類型int

      • 作用:接收標志,通常設置為 0。可以設置的標志包括:

        • MSG_PEEK:查看數據但不移除它(即“偷看”數據)。

        • MSG_WAITALL:阻塞直到接收到請求的所有數據。

        • MSG_DONTWAIT:非阻塞接收(僅適用于非阻塞套接字)。

    • src_addr

      • 類型struct sockaddr *

      • 作用:指向 struct sockaddr 或其派生類型(如 struct sockaddr_instruct sockaddr_in6)的指針,用于存儲發送方的地址信息。如果不需要發送方的地址信息,可以設置為 NULL

    • addrlen

      • 類型socklen_t *(必須取地址)

      • 作用:指向一個 socklen_t 類型的變量,該變量在調用前應設置為 src_addr 指向的地址結構體的大小(以字節為單位)。調用后,addrlen 指向的變量將被設置為實際存儲在 src_addr 中的地址長度。如果 src_addrNULL,則 addrlen 也應為 NULL

    返回值

    • 成功時返回接收到的字節數。

    • 如果對端關閉連接,返回 0

    • 失敗時返回 -1,并設置 errno 以指示錯誤原因。

    在使用UDP進行的通信的時候,要特別注意的是這是一個無連接的協議。一方面調用socket 函數的時候需要設置 SOCK_DGRAM 選項,而且因為沒有建立連接的過程,所以必須總是由客戶端先調用 sendto 發送消息給服務端,這樣服務端才能知道對端的地址信息,從進入后續的通信。下面是使用UDP通信的一個例子:

    //client
    int main(int argc, char const *argv[])
    {ARGS_CHECK(argc, 3);int sockfd, ret;sockfd = socket(AF_INET, SOCK_DGRAM, 0);ERROR_CHECK(sockfd, -1, "socket");struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = inet_addr(argv[1]);server_addr.sin_port = htons(atoi(argv[2]));printf("initialize raady\n");char buf[1024];socklen_t len = sizeof(server_addr);ret = read(STDIN_FILENO, buf, sizeof(buf));ERROR_CHECK(ret, -1, "read");ret = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&server_addr, len);ERROR_CHECK(ret, -1, "sendto");ret = recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr *)&server_addr, &len);ERROR_CHECK(ret, -1, "recvfrom");printf("server:%s\n", buf);ret = read(STDIN_FILENO, buf, sizeof(buf));ERROR_CHECK(ret, -1, "read");ret = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&server_addr, len);ERROR_CHECK(ret, -1, "sendto");close(sockfd);return 0;
    }
    //server
    int main(int argc, char const *argv[])
    {ARGS_CHECK(argc, 3);int sockfd, ret;sockfd = socket(AF_INET, SOCK_DGRAM, 0);ERROR_CHECK(sockfd, -1, "socket");struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = inet_addr(argv[1]);server_addr.sin_port = htons(atoi(argv[2]));ret = bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));ERROR_CHECK(ret, -1, "bind");printf("initialize raady\n");char buf[1024];struct sockaddr_in client_addr;memset(&client_addr, 0, sizeof(client_addr));socklen_t len = sizeof(client_addr);ret = recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr *)&client_addr, &len);ERROR_CHECK(ret, -1, "recvfrom");printf("client: %s\n", buf);ret = read(STDIN_FILENO, buf, sizeof(buf));ERROR_CHECK(ret, -1, "read");ret = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&client_addr, len);ERROR_CHECK(ret, -1, "sendto");ret = recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr *)&client_addr, &len);ERROR_CHECK(ret, -1, "recvfrom");printf("client: %s\n", buf);close(sockfd);return 0;
    }
    //client
    (base) ubuntu@ubuntu:~/MyProject/Linux/net$ ./client3 192.168.1.1 1236
    initialize raady
    hello
    server:what are you doing ?
    hahaha
    //server
    (base) ubuntu@ubuntu:~/MyProject/Linux/net$ ./server3 192.168.1.1 1236
    initialize raady
    client: hellowhat are you doing ?
    client: hahaha
    e you doing ?

    ?可以發現UDP是一種保留消息邊界的協議,無論用戶態空間分配的空間是否足夠 recvfrom總是會取出一個完整UDP報文,那么沒有拷貝的用戶態內存的數據會直接丟棄。

    實戰:使用UDP的即時聊天

    類似基于TCP的即時聊天通信,使用UDP也可以實現即時聊天通信,考慮到UDP是無連接協議,客戶端需要首先發送一個消息讓服務端知道客戶端的地址信息,然后再使用 select 監聽網絡讀緩沖區和標準輸入即可。

    需要特別注意的是,UDP通信不存在連接建立和斷開過程,所以服務端無法知道客戶端是否已經關閉套接字。

    //client.c
    int main(int argc, char const *argv[])
    {ARGS_CHECK(argc, 3);struct sockaddr_in server_addr;server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = inet_addr(argv[1]);server_addr.sin_port = htons(atoi(argv[2]));int sockfd = socket(AF_INET, SOCK_DGRAM, 0);ERROR_CHECK(sockfd, -1, "socket");char *serverInf = "client is connected";sendto(sockfd, serverInf, strlen(serverInf), 0, (struct sockaddr*)&server_addr, sizeof(server_addr));char buf[1024] = {0};fd_set rdset;int ret;socklen_t len = sizeof(server_addr);while(1){FD_ZERO(&rdset);FD_SET(STDIN_FILENO, &rdset);FD_SET(sockfd, &rdset);select(sockfd + 1, &rdset, NULL, NULL, NULL);if(FD_ISSET(STDIN_FILENO, &rdset)){memset(buf, 0, sizeof(buf));read(STDIN_FILENO, buf, sizeof(buf));sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr*)&server_addr, len); }else if(FD_ISSET(sockfd, &rdset)){memset(buf, 0, sizeof(buf));ret = recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr*)&server_addr, &len);if(ret == 0){printf("disconnected\n");break;}printf("server: %s",buf);    }}close(sockfd);return 0;
    }
    //server.cint main(int argc, char const *argv[])
    {ARGS_CHECK(argc, 3);struct sockaddr_in server_addr;server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = inet_addr(argv[1]);server_addr.sin_port = htons(atoi(argv[2]));int sockfd = socket(AF_INET, SOCK_DGRAM, 0);ERROR_CHECK(sockfd, -1, "socket");int ret = bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));ERROR_CHECK(ret, -1, "bind");char buf[1024] = {0};struct sockaddr_in client_addr;socklen_t len = sizeof(client_addr);ret = recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr*)&client_addr, &len);ERROR_CHECK(ret, -1, "recvfrom");printf("%s\n", buf);fd_set rdset;while(1){FD_ZERO(&rdset);FD_SET(STDIN_FILENO, &rdset);FD_SET(sockfd, &rdset);select(sockfd + 1, &rdset, NULL, NULL, NULL);if(FD_ISSET(STDIN_FILENO, &rdset)){memset(buf, 0, sizeof(buf));read(STDIN_FILENO, buf, sizeof(buf));sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr*)&client_addr, len); }else if(FD_ISSET(sockfd, &rdset)){memset(buf, 0, sizeof(buf));ret = recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr*)&client_addr, &len);if(ret == 0){printf("disconnected\n");break;}printf("client: %s",buf);    }}close(sockfd);return 0;
    }
    //client
    (base) ubuntu@ubuntu:~/MyProject/Linux/net$ ./client4 192.168.1.1 1235
    hello I am client!
    what are you doing?
    server: not need to know
    server: goodbye!
    OK
    ^C
    //server
    (base) ubuntu@ubuntu:~/MyProject/Linux/net$ ./server4 172.20.74.205 1235
    hello I am client!client: what are you doing?
    not need to know
    goodbye!
    client: OK

    socket屬性調整

    getsockopt

    getsockopt 函數是網絡編程中用于獲取套接字選項的函數。它允許程序員查詢套接字的當前配置,獲取各種參數的值,例如緩沖區大小、超時時間、是否允許重用地址等。

    int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);

    參數

    • sockfd

      • 類型int

      • 作用:套接字描述符,標識了要查詢選項的套接字。

    • level

      • 類型int

      • 作用:指定選項所在的協議級別。常見的值包括:

        • SOL_SOCKET:套接字級別的選項。

        • IPPROTO_TCP:TCP 協議級別的選項。

        • IPPROTO_IP:IP 協議級別的選項。

        • IPPROTO_IPV6:IPv6 協議級別的選項。

    • optname

      • 類型int

      • 作用:指定要查詢的選項名稱。常見的選項包括:

        • SO_REUSEADDR:是否允許重用本地地址和端口。

        • SO_REUSEPORT:是否允許重用端口。

        • SO_BROADCAST:是否允許發送廣播數據。

        • SO_KEEPALIVE:是否啟用 TCP 保活機制。

        • SO_LINGER:套接字關閉時的行為。

        • SO_RCVBUF:接收緩沖區大小。

        • SO_SNDBUF:發送緩沖區大小。

        • TCP_NODELAY:是否禁用 Nagle 算法。

    • optval

      • 類型void *

      • 作用:指向存儲選項值的緩沖區。查詢結果將存儲在這個緩沖區中。

    • optlen

      • 類型socklen_t *

      • 作用:指向一個 socklen_t 類型的變量,該變量在調用前應設置為 optval 指向的緩沖區的大小(以字節為單位)。調用后,optlen 指向的變量將被設置為實際存儲在 optval 中的值的大小。

    返回值

    • 成功時返回 0

    • 失敗時返回 -1,并設置 errno 以指示錯誤原因。

    setsockopt

    setsockopt 函數是網絡編程中用于設置套接字選項的函數。它允許程序員在套接字級別上配置各種參數,從而影響套接字的行為。這些選項可以包括緩沖區大小、超時時間、是否允許重用地址等。

    int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

    參數

    • sockfd

      • 類型int

      • 作用:套接字描述符,標識了要設置選項的套接字。

    • level

      • 類型int

      • 作用:指定選項所在的協議級別。同 getsockopt 選項

    • optname

      • 類型int

      • 作用:指定要設置的選項名稱。同 getsockopt 選項

    • optval

      • 類型const void *

      • 作用:指向存儲選項值的緩沖區。選項值的類型取決于 optname

    • optlen

      • 類型socklen_t

      • 作用optval 指向的緩沖區的大小(以字節為單位)。

    返回值

    • 成功時返回 0

    • 失敗時返回 -1,并設置 errno 以指示錯誤原因。

    其他系統調用

    socketpair

    用于創建一對相互連接的套接字。這對套接字可以用于進程間通信(IPC)。

    #include <sys/types.h>
    #include <sys/socket.h>int socketpair(int domain, int type, int protocol, int sv[2]);

    參數說明

    1. domain:

      • 指定套接字的通信域(協議族)。對于 socketpair(),通常使用 AF_UNIXAF_LOCAL,表示本地通信。

    2. type:

      • 指定套接字的類型。常見的類型包括:

        • SOCK_STREAM:流式套接字,提供可靠的雙向字節流。

        • SOCK_DGRAM:數據報套接字,提供無連接的、不可靠的、固定大小的數據報。

        • SOCK_SEQPACKET:有序的、可靠的、固定大小的數據報。

        • SOCK_RAW:原始套接字,用于直接訪問協議層。

    3. protocol:

      • 指定使用的協議。對于 AF_UNIX,通常設置為 0,表示默認協議。

    4. sv[2]:

      • 一個數組,用于存儲創建的兩個套接字的文件描述符。sv[0]sv[1] 是一對相互連接的套接字。

    返回值

    • 成功:返回 0,并通過 sv 參數返回兩個套接字的文件描述符。

    • 失敗:返回 -1,并通過 errno 設置錯誤碼。

    示例:使用 socketpair 進行父子進程間通信

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/socket.h>
    #include <string.h>int main() {int sv[2];pid_t pid;// 創建一對套接字if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == -1) {perror("socketpair");exit(EXIT_FAILURE);}// 創建子進程pid = fork();if (pid == -1) {perror("fork");exit(EXIT_FAILURE);}if (pid == 0) {// 子進程close(sv[0]); // 關閉父進程端的套接字char buffer[100];int n;// 從父進程接收數據if ((n = read(sv[1], buffer, sizeof(buffer))) == -1) {perror("read");exit(EXIT_FAILURE);}buffer[n] = '\0'; // 確保字符串以空字符結尾printf("Child received: %s\n", buffer);// 向父進程發送數據const char *msg = "Hello from child\n";if (write(sv[1], msg, strlen(msg)) == -1) {perror("write");exit(EXIT_FAILURE);}close(sv[1]); // 關閉子進程端的套接字exit(EXIT_SUCCESS);} else {// 父進程close(sv[1]); // 關閉子進程端的套接字const char *msg = "Hello from parent\n";// 向子進程發送數據if (write(sv[0], msg, strlen(msg)) == -1) {perror("write");exit(EXIT_FAILURE);}char buffer[100];int n;// 從子進程接收數據if ((n = read(sv[0], buffer, sizeof(buffer))) == -1) {perror("read");exit(EXIT_FAILURE);}buffer[n] = '\0'; // 確保字符串以空字符結尾printf("Parent received: %s\n", buffer);close(sv[0]); // 關閉父進程端的套接字wait(NULL);   // 等待子進程退出}return 0;
    }

    socketpair創建套接子 與 使用管道通信相比 有以下一些區別:

    • socketpair():

      • 適用于需要雙向通信的場景。這一點是它的優勢

      • 提供更靈活的通信機制,支持多種套接字類型。

      • 可以擴展到網絡通信。

    • pipe():

      • 適用于簡單的單向通信場景。

      • 實現簡單,開銷小。

      • 僅限于本地進程間通信。

    sendmsg

    sendmsg() 是一個用于發送消息的系統調用,它比 send()write() 更為通用和強大。它允許發送帶有多種附加信息(如文件描述符)的消息,通常用于高級的套接字編程。

    #include <sys/socket.h>ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

    參數說明

    • sockfd:

      • 套接字文件描述符,表示要發送消息的套接字。

    • msg:

      • 指向 struct msghdr 結構的指針,該結構定義了要發送的消息的內容和格式。struct msghdr 的定義如下:

    struct msghdr {void         *msg_name;       // 可選的地址socklen_t     msg_namelen;    // 地址長度struct iovec *msg_iov;        // 散列(scatter/gather)數組size_t        msg_iovlen;     // 散列數組中的元素數量void         *msg_control;    // 可選的控制信息size_t        msg_controllen; // 控制信息的長度int           msg_flags;      // 標志(通常為 0)
    };
    • msg_namemsg_namelen:用于指定目標地址(例如在 UDP 套接字中)。對于已連接的套接字(如 TCP),通常設置為 NULL0

    • msg_iovmsg_iovlen:定義了要發送的數據。msg_iov 是一個指向 struct iovec 數組的指針,每個 struct iovec 包含以下內容:

    struct iovec {void  *iov_base;    // 數據的起始地址size_t iov_len;     // 數據的長度
    };
    • msg_controlmsg_controllen:用于發送控制信息(如輔助數據)。控制信息通常用于傳遞文件描述符(通過 SCM_RIGHTS)或其他協議特定的信息。

    • msg_flags:通常設置為 0,但在某些情況下可以設置為 MSG_DONTWAIT(非阻塞模式)或其他標志。

    • flags:

      • 用于控制消息發送的行為。常見的標志包括:

        • MSG_DONTWAIT:非阻塞模式,即使套接字設置為阻塞模式,也會立即返回。

        • MSG_NOSIGNAL:防止發送 SIGPIPE 信號(當對方關閉連接時)。

        • MSG_EOR:表示消息結束(僅適用于某些協議)。

    返回值

    • 成功:返回發送的字節數。

    • 失敗:返回 -1,并通過 errno 設置錯誤碼。

    使用場景

    sendmsg() 的主要優勢在于它可以同時發送多個數據塊(通過 msg_iovmsg_iovlen)和控制信息(通過 msg_controlmsg_controllen)。這使得它特別適合以下場景:

    1. 發送文件描述符:通過 SCM_RIGHTS,可以在進程間傳遞文件描述符。

    2. 發送大量數據:通過散列(scatter/gather)I/O,可以高效地發送多個內存塊。

    3. 高級協議控制:支持協議特定的控制信息。

    recvmsg

    recvmsg() 是一個用于接收消息的系統調用,與 sendmsg() 配合使用,支持從套接字接收復雜的消息。它不僅可以接收普通的數據,還可以接收控制信息(如文件描述符)。recvmsg() 是一個功能強大的工具,特別適用于需要處理散列(scatter/gather)I/O 或傳遞輔助數據的場景。

    #include <sys/socket.h>ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

    ?參數說明

    • sockfd:

      • 套接字文件描述符,表示要接收消息的套接字。

    • msg:

      • 指向 struct msghdr 結構的指針,該結構定義了接收消息的格式和內容。

    • msg_namemsg_namelen:用于接收發送方的地址(例如在 UDP 套接字中)。對于已連接的套接字(如 TCP),通常設置為 NULL0
    • msg_iovmsg_iovlen:定義了接收數據的緩沖區。msg_iov 是一個指向 struct iovec 數組的指針。

    • msg_controlmsg_controllen:用于接收控制信息(如輔助數據)。控制信息通常用于接收文件描述符(通過 SCM_RIGHTS)或其他協議特定的信息。

    • msg_flags:由 recvmsg() 設置,表示消息的接收狀態(如是否為帶外數據)。

    返回值

    • 成功:返回接收到的字節數。

    • 失敗:返回 -1,并通過 errno 設置錯誤碼。

    注意事項

    • 控制信息大小msg_controllen 必須足夠大,以容納控制信息。使用 CMSG_SPACE() 宏來計算所需的空間。

    • 文件描述符傳遞:通過 SCM_RIGHTS 傳遞文件描述符時,接收端會獲得一個有效的文件描述符,但發送端的文件描述符不會被關閉。

    • 協議支持recvmsg()sendmsg() 主要用于套接字編程,但某些協議(如 TCP)可能不支持某些控制信息。

    • msg_flags:接收完成后,msg_flags 會被設置為消息的實際狀態(如是否為帶外數據)。如果需要檢查這些標志,可以在調用后檢查 msg_flags 的值。

    示例:使用 sendmsg 和 recvmsg 進行父子進程通信

    #define BUFFER_SIZE 1024// 發送文件描述符
    void send_fd(int sockfd, int fd_to_send) {struct msghdr msg = {0};struct iovec iov[1];char buffer[BUFFER_SIZE] = "Hello from parent";struct cmsghdr *cmsg;int *fd_ptr;// 初始化 msghdr 結構msg.msg_iov = iov;msg.msg_iovlen = 1;iov[0].iov_base = buffer;iov[0].iov_len = strlen(buffer) + 1;// 初始化控制信息msg.msg_control = malloc(CMSG_SPACE(sizeof(int)));msg.msg_controllen = CMSG_SPACE(sizeof(int));cmsg = CMSG_FIRSTHDR(&msg);cmsg->cmsg_level = SOL_SOCKET;cmsg->cmsg_type = SCM_RIGHTS;cmsg->cmsg_len = CMSG_LEN(sizeof(int));fd_ptr = (int *)CMSG_DATA(cmsg);*fd_ptr = fd_to_send;// 發送消息if (sendmsg(sockfd, &msg, 0) == -1) {perror("sendmsg");exit(EXIT_FAILURE);}free(msg.msg_control);
    }// 接收文件描述符
    int recv_fd(int sockfd) {struct msghdr msg = {0};struct iovec iov[1];char buffer[BUFFER_SIZE];struct cmsghdr *cmsg;int *fd_ptr;int received_fd = -1;// 初始化 msghdr 結構msg.msg_iov = iov;msg.msg_iovlen = 1;iov[0].iov_base = buffer;iov[0].iov_len = sizeof(buffer);// 初始化控制信息msg.msg_control = malloc(CMSG_SPACE(sizeof(int)));msg.msg_controllen = CMSG_SPACE(sizeof(int));// 接收消息if (recvmsg(sockfd, &msg, 0) == -1) {perror("recvmsg");exit(EXIT_FAILURE);}// 提取文件描述符cmsg = CMSG_FIRSTHDR(&msg);if (cmsg && cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SCM_RIGHTS) {fd_ptr = (int *)CMSG_DATA(cmsg);received_fd = *fd_ptr;}free(msg.msg_control);return received_fd;
    }int main() {int sv[2];pid_t pid;// 創建套接字對if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == -1) {perror("socketpair");exit(EXIT_FAILURE);}// 創建子進程pid = fork();if (pid == -1) {perror("fork");exit(EXIT_FAILURE);}if (pid == 0) {// 子進程close(sv[0]); // 關閉父進程端的套接字// 接收文件描述符int received_fd = recv_fd(sv[1]);if (received_fd == -1) {fprintf(stderr, "Failed to receive file descriptor.\n");exit(EXIT_FAILURE);}// 從接收到的文件描述符讀取數據char buffer[BUFFER_SIZE];ssize_t n = read(received_fd, buffer, sizeof(buffer));if (n == -1) {perror("read");exit(EXIT_FAILURE);}buffer[n] = '\0';printf("Child received message: %s\n", buffer);close(received_fd);close(sv[1]);exit(EXIT_SUCCESS);} else {// 父進程close(sv[1]); // 關閉子進程端的套接字// 打開一個文件描述符(例如標準輸入)int fd_to_send = STDIN_FILENO;// 發送文件描述符send_fd(sv[0], fd_to_send);close(sv[0]);wait(NULL); // 等待子進程退出}return 0;
    }

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

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

    相關文章

    循環神經網絡--NLP基礎

    一、簡單介紹NLP&#xff08;Natural Language Processing&#xff09;&#xff1a;自然語言處理是人工智能和語言領域的一個分支&#xff0c;它涉及計算機和人類語言之間的相互作用。二、NLP基礎概念詞表&#xff08;詞庫&#xff09;&#xff1a;文本數據集出現的所有單詞的集…

    【Android】約束布局總結(1)

    三三要成為安卓糕手 零&#xff1a;創建布局文件方式 1&#xff1a;創建步驟ctrl alt 空格 設置根元素2&#xff1a;處理老版本約束布局 在一些老的工程中&#xff0c;constrainlayout可能沒有辦法被直接使用&#xff0c;這里需要手動添加依賴implementation androidx.const…

    S7-200 SMART 數字量 I/O 組態指南:從參數設置到實戰案例

    在工業自動化控制中&#xff0c;PLC 的數字量輸入&#xff08;DI&#xff09;和輸出&#xff08;DO&#xff09;是連接傳感器、執行器與控制系統的 “神經末梢”。西門子 S7-200 SMART 作為一款高性價比的小型 PLC&#xff0c;其數字量 I/O 的靈活組態直接影響系統的穩定性與響…

    可調諧激光器原理與設計 【DFB 與 DBR 激光器剖析】

    可調諧激光器原理與設計 【DFB 與 DBR 激光器剖析】1. 可調諧激光器的原理與分類簡介2. DFB 與 DBR 激光器結構原理比較2.1 DFB&#xff08;Distributed Feedback Laser&#xff09;激光器2.2 DBR&#xff08;Distributed Bragg Reflector&#xff09;激光器2.3 DFB 激光器與 D…

    【前端工程化】前端項目開發過程中如何做好通知管理?

    在企業級后臺系統中&#xff0c;通知是保障團隊協作、監控系統狀態和及時響應問題的重要手段。與 C 端產品不同&#xff0c;B 端更關注構建完成、部署狀態、異常報警等關鍵節點的推送機制。 本文主要圍繞通知場景、通知內容、通知渠道、自動化集成等方面展開&#xff0c;適用于…

    MySQL 9.4.0創新版發布,AI開始輔助編寫發布說明

    2025 年 7 月 22 日&#xff0c;MySQL 9.4.0 正式發布。 作為一個創新版&#xff0c;MySQL 9.4.0 最大的創新應該就是使用 Oracle HeatWave GenAI 作為助手幫助編寫版本發布說明了。難道下一步要開始用 AI 輔助編寫數據庫文檔了&#xff1f; 該版本包含的核心功能更新以及問題修…

    基于WebSockets和OpenCV的安卓眼鏡視頻流GPU硬解碼實現

    基于WebSockets和OpenCV的安卓眼鏡視頻流GPU硬解碼實現 前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家&#xff0c;覺得好請收藏。點擊跳轉到網站。 1. 項目概述 本項目旨在實現一個通過WebSockets接收…

    人大金倉 kingbase 連接數太多, 清理數據庫連接數

    問題描述 kingbase 連接數太多, 清理數據庫連接數 [rootFCVMDZSZNST25041 ~]# su root [rootFCVMDZSZNST25041 ~]# [rootFCVMDZSZNST25041 ~]# su kingbase [kingbaseFCVMDZSZNST25041 root]$ [kingbaseFCVMDZSZNST25041 root]$ ksql could not change directory to "/r…

    SpringMVC相關基礎知識

    1. servlet.multipart 大小配置 SpringBoot 文件上傳接口中有 MultipartFile 類型的文件參數,上傳較大文件時報錯: org.springframework.web.multipart.MaxUploadSizeExceededException: Maximum upload size exceeded; nested exception is java.lang.IllegalStateExceptio…

    HCIP第一次實驗報告

    一.實驗需求及拓撲圖&#xff1a;二.實驗需求分析根據提供的網絡拓撲圖和實驗要求&#xff0c;以下是對實驗需求的詳細分析&#xff1a;R5作為ISP:R5只能進行IP地址配置&#xff0c;其所有接口均配置為公有IP地址。認證方式:R1和R5之間使用PPP的PAP認證&#xff0c;R5為主認證方…

    React入門學習——指北指南(第五節)

    React 交互性:過濾與條件渲染 在前文我們學習了 React 中事件處理和狀態管理的基礎。本節將聚焦兩個重要的進階技巧 ——條件渲染(根據狀態動態顯示不同 UI)和列表過濾(根據條件篩選數據),這兩者是構建交互式應用的核心能力,能讓界面根據用戶操作呈現更智能的響應。 條…

    學習嵌入式的第二十九天-數據結構-(2025.7.16)線程控制:互斥與同步

    以下是您提供的文本內容的排版整理版本。我已根據內容主題將其分為幾個主要部分&#xff08;互斥鎖、信號量、死鎖、IPC進程間通信、管道操作&#xff09;&#xff0c;并使用清晰的結構組織信息&#xff1a;代碼片段用代碼塊格式&#xff08;指定語言為C&#xff09;突出顯示。…

    COZE官方文檔基礎知識解讀第六期 ——數據庫和知識庫

    一&#xff0c;一鍵直連數據上傳&#xff0c;存儲&#xff0c;使用 火山方舟的數據庫和知識庫的核心&#xff0c;都是基于開源的數據庫產品&#xff08;mysql&#xff0c;向量數據庫等&#xff09;&#xff0c;將數據庫交互的邏輯封裝在后端&#xff0c;與前端做耦合&#xff0…

    生產環境使用云服務器(centOS)部署和使用MongoDB

    部署MongoDB流程1. ?安裝MongoDB?版本選擇建議?CentOS 7?&#xff1a;推薦MongoDB 4.4.x&#xff08;兼容性好&#xff09;?CentOS 8/9?&#xff1a;建議最新穩定版&#xff08;如6.0&#xff09;&#xff0c;需單獨安裝mongodb-database-tools安裝步驟1.添加官方倉庫# 添…

    思博倫第二到三層測試儀(打流儀)TestCenter 2U硬件安裝及機箱加電_雙極未來

    &#xff08;1&#xff09;安裝板卡&#xff1a;上圖中共 4 個紅色線框&#xff0c;上邊兩個紅色線條框住的是機箱的左右兩側導軌&#xff0c;下邊兩條紅色 線條框住的是板卡拉手條&#xff08;用于承載板卡PCB的金屬板&#xff09;左右兩邊的邊沿。 安裝時將拉手條兩邊的邊沿與…

    【華為】筆試真題訓練_20250611

    本篇博客旨在記錄自已的筆試刷題的練習&#xff0c;里面注有詳細的代碼注釋以及和個人的思路想法&#xff0c;希望可以給同道之人些許幫助。本人也是小白&#xff0c;水平有限&#xff0c;如果文章中有什么錯誤或遺漏之處&#xff0c;望各位可以在評論區指正出來&#xff0c;各…

    新浪微博APP v14.5.0:連接世界的社交媒體平臺

    新浪微博APP 是一款廣受歡迎的社交媒體應用程序&#xff0c;憑借其強大的功能和豐富的社交生態&#xff0c;成為用戶獲取信息、表達觀點、互動交流的重要平臺。最新版 v14.5.0 內置了微博助手 v2.3.0&#xff0c;進一步提升了用戶體驗和功能多樣性。 軟件功能 1. 發布微博 用…

    靜態枚舉返回(簡單實現字典功能)

    枚舉緩存策略的實現與應用 通過靜態Map緩存枚舉類的Class對象&#xff0c;避免每次請求時重復反射加載。核心實現是一個包含枚舉類名與對應Class映射的Registry類&#xff1a; public class EnumRegistry {private static final Map<String, Class<?>> ENUM_MAP …

    深分頁性能問題分析與優化實踐

    在日常測試工作中&#xff0c;我們經常會遇到分頁查詢接口&#xff0c;例如&#xff1a; GET /product/search?keyword&pageNum1&pageSize10乍看之下&#xff0c;這樣的分頁接口似乎并無性能問題&#xff0c;響應時間也很快。但在一次性能壓測中&#xff0c;我們復現了…

    LeetCode——1957. 刪除字符使字符串變好

    通過萬歲&#xff01;&#xff01;&#xff01; 題目&#xff1a;給你一個字符串&#xff0c;然后讓你刪除幾個字符串&#xff0c;讓他變成好串&#xff0c;好串的定義就是不要出現連續的3個一樣的字符。思路&#xff1a;首先就是要遍歷字符串。我們將要返回的字符串定義為ret&…