基礎知識
在進行網絡編程之前,我們需要簡單回顧一下計算機網絡五層模型的網絡層和傳輸層,這兩層在面向后端編程時用的最多。物理層和鏈路層過于底層,已經完全由內核協議棧實現,不再細述。
這里假設讀者已經對計算機網絡有一個大致的了解。
網絡層
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_UNIX
或AF_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 等命令都是以大端法來解析內容的)。
下面是整數大小端轉換相關的函數。?
字節序概述
主機字節序(Host Byte Order)
依賴于具體計算機系統的架構。
在大多數現代計算機系統(如 x86 和 x86_64 架構的機器)中,主機字節序是小端序(Little-Endian),即低位字節存儲在低地址處。
在某些系統(如某些 PowerPC 架構的機器)中,主機字節序是大端序(Big-Endian)。
網絡字節序(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
用于根據主機名或服務名獲取主機的地址信息。它是一個現代的替代品,用于替代傳統的 gethostbyname
和 getservbyname
等函數,因為它支持 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_AGAIN
、EAI_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_UNIX
或AF_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_in
或struct 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_in
或struct 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_in
或struct sockaddr_in6
)的指針,用于存儲客戶端的地址信息。如果不需要客戶端的地址信息,可以設置為NULL
。
addrlen
類型:
socklen_t *
作用:指向一個
socklen_t
類型的變量,該變量在調用前應設置為addr
指向的地址結構體的大小(以字節為單位)。調用后,addrlen
指向的變量將被設置為實際存儲在addr
中的地址長度。如果addr
為NULL
,則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_in
或struct sockaddr_in6
)的指針,用于存儲發送方的地址信息。如果不需要發送方的地址信息,可以設置為NULL
。
addrlen
類型:
socklen_t *(必須取地址)
作用:指向一個
socklen_t
類型的變量,該變量在調用前應設置為src_addr
指向的地址結構體的大小(以字節為單位)。調用后,addrlen
指向的變量將被設置為實際存儲在src_addr
中的地址長度。如果src_addr
為NULL
,則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]);
參數說明
domain
:指定套接字的通信域(協議族)。對于
socketpair()
,通常使用AF_UNIX
或AF_LOCAL
,表示本地通信。
type
:指定套接字的類型。常見的類型包括:
SOCK_STREAM
:流式套接字,提供可靠的雙向字節流。SOCK_DGRAM
:數據報套接字,提供無連接的、不可靠的、固定大小的數據報。SOCK_SEQPACKET
:有序的、可靠的、固定大小的數據報。SOCK_RAW
:原始套接字,用于直接訪問協議層。
protocol
:指定使用的協議。對于
AF_UNIX
,通常設置為0
,表示默認協議。
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_name
和msg_namelen
:用于指定目標地址(例如在 UDP 套接字中)。對于已連接的套接字(如 TCP),通常設置為NULL
和0
。msg_iov
和msg_iovlen
:定義了要發送的數據。msg_iov
是一個指向struct iovec
數組的指針,每個struct iovec
包含以下內容:
struct iovec {void *iov_base; // 數據的起始地址size_t iov_len; // 數據的長度
};
msg_control
和msg_controllen
:用于發送控制信息(如輔助數據)。控制信息通常用于傳遞文件描述符(通過SCM_RIGHTS
)或其他協議特定的信息。msg_flags
:通常設置為0
,但在某些情況下可以設置為MSG_DONTWAIT
(非阻塞模式)或其他標志。flags
:用于控制消息發送的行為。常見的標志包括:
MSG_DONTWAIT
:非阻塞模式,即使套接字設置為阻塞模式,也會立即返回。MSG_NOSIGNAL
:防止發送SIGPIPE
信號(當對方關閉連接時)。MSG_EOR
:表示消息結束(僅適用于某些協議)。
返回值
成功:返回發送的字節數。
失敗:返回
-1
,并通過errno
設置錯誤碼。
使用場景
sendmsg()
的主要優勢在于它可以同時發送多個數據塊(通過 msg_iov
和 msg_iovlen
)和控制信息(通過 msg_control
和 msg_controllen
)。這使得它特別適合以下場景:
發送文件描述符:通過
SCM_RIGHTS
,可以在進程間傳遞文件描述符。發送大量數據:通過散列(scatter/gather)I/O,可以高效地發送多個內存塊。
高級協議控制:支持協議特定的控制信息。
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_name
和msg_namelen
:用于接收發送方的地址(例如在 UDP 套接字中)。對于已連接的套接字(如 TCP),通常設置為NULL
和0
。msg_iov
和msg_iovlen
:定義了接收數據的緩沖區。msg_iov
是一個指向struct iovec
數組的指針。
msg_control
和msg_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;
}