隨著網絡編程(C/C++)的學習,發現自己對於基本套接字編程有些細節還是不太清楚,然後又查看了對應的書籍和資料,趁著這段時間把這些基礎知識梳理了一下,便於自己回顧,同時大家也可以作為參考 :) ~
struct in_addr { in_addr_t s_addr; // 32-bit IPv4 address //network byte ordered } struct sockaddr_in { sa_family_t sin_family; //AF_INET in_port_t sin_port; //16-bit TCP or UDP port nummber, network byte ordered struct in_addr sin_addr; //32-bit IPv4 address, network byte ordered char sin_zero[8]; //unused }
sockaddr_in是網絡套接字地址結構,大小為16字節,定義在<netinet/in>頭文件中,一般我們在程序中是使用該結構體,但是作為參數傳遞給套接字函數時需要強轉為sockaddr類型,注意該結構體中port和addr成員是網絡序的(大端結構)。
struct sockaddr { sa_family_t sa_family; //address family: AF_XXX value char sa_data[14]; //protocol-specific address }
sockaddr是通過套接字地址結構,當作為參數傳遞給套接字函數時,套接字地址結構總是以指針方式來使用,比如bind/accept/connect函數等。
#include <netinet/in.h> uint16_t htons(uint16_t host16bitvalue); uint32_t htonl(uint32_t host32bitvalue); uint16_t ntohs(uint16_t net16bitvalue); uint32_t ntohl(uint32_t net32bitvalue);
Linux提供了4個函數來完成主機字節序和網絡字節序之間的轉換。這些函數名字中,h表示host,n表示net,s表示short,l表示long。使用這些函數時,並不關心主機字節序和網絡字節序的真實值,也就是為大端還是小端,要做的只是調用適當的函數在主機和網絡字節序之間轉換為某個特定值。
#include <arpa/inet.h> int inet_aton(const char *strptr, struct in_addr *addrptr); // 返回:若字符有效則為1,否則為0 in_addr_t inet_addr(const char *strptr); // 返回:若字符串有效則為32位二進制網絡字節序地址,否則為INADDR_NONE char *inet_ntoa(struct in_addr inaddr); // 返回:指向一個點分十進制數串的地址
inet_aton、inet_addr和inet_ntoa在點分十進制數串(比如"192.168.1.1")與它長度為32位的網絡字節序二進制值間轉換IPv4地址。在調用inet_addr時需特別注意,inet_ntoa函數的輸入參數是unsigned int型的ip地址,返回的卻是指向ip字符串的指針,很明顯,ip字符串所占的內存是在函數內部分配的,而我們並不需要釋放該內存,所以,它分配的內存是靜態的,內部使用static變量存儲IP點分十進制數串,也就是說第二次調用該函數時會覆蓋第一次調用該函數時的內存。
#include <arpa/inet.h> int inet_pton(int family, const char *strptr, void *addrptr); // 返回:成功為1,輸入不是有效表達式返回0,出錯為-1 const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len); // 返回:成功為指向結果的指針,出錯為NULL
這兩個函數對於IPv4和IPv6都適用,p代表表達式(presentation)、n表示數值(numeric)。第一個函數嘗試轉化由strptr指針所指的字符串,通過addptr指針存放二進制結果,成功返回1,如果對指定的family而言輸入的不是有效的表達格式,那麼返回0
inet_ntop進行相反的操作,如果len的值太小,不足以存放表達式結果,則返回一個空指針,並置error為ENOSPC。inet_ntop函數的strptr參數不可以是一個空指針,調用者必須為目標存儲單元分配內存並制定其大小,調用成功時,這個指針就是該函數返回值。
為了執行網絡IO,一個進程必須做的第一件事就是調用socket函數,指定期望的通信協議類型(比如使用IPv4的TCP、使用IPv6的UDP、Unix域字節流協議)和套接字字類型(字節流、數據報或原始套接字)。
#include <sys/socket.h> int socket(int family, int type, int protocol); // 成功返回非負描述符,出錯-1
family指定協議族,type指定套接字類型,protocol指定某個協議類型常值,或者設為0。
family的值有:
type的值有:
protocol的值有:
socket函數在成功時返回一個小的非負整數值,與文件描述符類似,成為套接字描述符,為了得到這個描述符,需要指定協議族和套接字類型,但是並沒有指定本地協議地址和遠端協議地址。
#include <sys/socket.h> int connect(int sockfd, const struct sockaddr* servaddr, socklen_t addrlen); // 返回:成功為0,出錯-1
TCP客戶用connect函數來建立一個與TCP服務器連接,sockfd是由socket函數返回的套接字描述符,第二個、第三個參數分別是指向一個套接字地址結構的指針和該結構的大小,套接字結構必須含有服務器的IP地址和端口號。注意:如果connect失敗後,就必須close當前的套接字描述符並重新調用socket。客戶端在調用connect前不必非得調用bind函數(比如UDP客戶端編程中一般就不用調用bind),內核會確定源IP地址,並選擇一個臨時端口作為源端口。
如果是TCP套接字,調用connect函數將激發TCP的三次握手過程,而且僅在連接建立成功或出錯時才返回。注意:connect是在接收到服務端響應的SYN+ACK時的返回的,也就是三次握手的第二次動作之後。
UDP是可以調用connect函數的,但是UDP的connect函數和TCP的connect函數調用確是大相徑庭的,這裡沒有三次握手過程。內核只是檢查是否存在立即可知的錯誤(比如目的地址不可達),記錄對端的IP和端口號,然後立即返回調用進程。使用了connect的UDP編程就可不必使用sendto函數了,直接使用write/read即可。
#include <sys/socket.h> int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen); // 返回:成功為0,出錯-1
bind函數把一個本地協議地址賦予一個套接字,它只是把一個協議地址賦予一個套接字,至於協議地址的含義則取決於協議本身。第二個參數指向協議地址結構的指針,第三個參數是協議地址的長度,對於TCP,調用bind函數可以指定一個端口號,或指定一個IP地址,或兩者都指定,也可以兩者都不指定。
bind函數綁定特定的IP地址必須屬於其所在主機的網絡接口之一,服務器在啟動時綁定它們眾所周知的端口,如果一個TCP客戶端或服務端未曾調用bind綁定一個端口,當調用connect或listen時,內核就要為響應的套接字選擇一個臨時端口。讓內核選擇臨時端口對於TCP客戶端來說是正常的額,然後對於TCP服務端來說確實罕見的,因為服務端通過他們眾所周知的端口被大家認識的。
#include <sys/socket.h> int listen(int sockfd, int backlog); // 返回:成功返回0,出錯-1
socket創建一個套接字時,它被假設為一個主動套接字,也就是說,它是一個將調用connect發起連接的一個客戶套接字。listen函數把一個未連接的套接字轉換為一個被動套接字,指示內核應接受指向該套接字的連接請求,調用listen函數將導致套接字從CLOSEE狀態轉換到LISTEN狀態。第二個參數規定了內核應為相應套接字排隊的最大連接個數。
圖片來自《UNIX網絡編程-卷一》
backlog參數在不同的系統中有不同的解釋,不過大致類似。UNP(第3版)給出的定義為:listen()的backlog應該指定某個給定套接字上內核為之排隊的最大已完成連接數。
當一個客戶端SYN達到時,若這些隊列是滿的,TCP就忽略該分節,也即是不發送RST,這樣做是暫時的,客戶端將重新發送SYN,期望不就就能得到服務。假如服務端響應一個RST,客戶端的connect就會返回錯誤,而不是讓重傳機制來處理,這樣客戶無法區分SYN的RST是因為"該端口沒有在監聽"還是"該端口在監聽,只不過它的隊列滿了"。
在三路握手完成之後,但在服務端調用accept之前到達的數據應由服務端TCP排隊,最大數據量為相應已連接套接字的接收緩沖區大小。
在TCP服務端套接字編程中,執行完listen後,而沒有執行accept,客戶端是可以成功建立連接的,只不過是該連接被加入到了已連接隊列中,當調用accept時會被提取出來。
#include <sys/socket.h> int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen); // 返回:成功返回已連接描述符(非負),出錯-1
accept函數有TCP服務器調用,用於從已完成隊列中列頭返回下一個已完成連接,如果已完成隊列為空,則進程被投入睡眠(如果該套接字為阻塞方式的話)。如果accept成功,那麼其返回值是由內核自動生成的一個全新套接字,代表與返回客戶的TCP連接,函數的第一個參數為監聽套接字,返回值為已連接套接字。
#include <unistd.h> int close(int sockfd); // 若成功返回0,出錯-1
close一個TCP套接字的默認行為是把該套接字標記為已關閉,然後立即返回到調用進程。注意,close實質把該套接字引用值減1,如果該引用值大於0,則對應的套接字不會被真正關掉。
#include <sys/socket.h> int getsockname(int sockfd, struct sockaddr *localaddr, &addrlen); int getpeername(int sockfd, struct sockaddr *peeraddr, &addrlen); // 返回:成功為0, 出錯為-1
getsockname獲取sockfd對應的本端socket地址,並將其存儲於address參數指定的內存地址,該socket長度存儲於addrlen指向的變量中。getpeername獲取遠端的socket地址。
UDP客戶端如果調用connect之後也是可以使用getpeername的。
#include <sys/socket.h> ssize recv(int sockfd, void *buff, size_t nbytes, int flags); ssize send(int sockfd, void *buff, size_t nbytes, int flags); // 返回:成功為讀入或寫入的字節數,出錯為-1
TCP流數據讀寫操作函數。flag取值如下所示:
注意的是,flags參數只對send和recv的當前調用有效,當然也可以通過setsockopt系統調用永久性地修 改socket的某些屬性。
#include <sys/socket.h> ssize_t recvfrom(int sockfd, void *buf, size_t nbytes, int flags, struct sockaddr *from, socklen_t *addrlen); ssize_t recvto(int sockfd, void *buf, size_t nbytes, int flags, struct sockaddr *to, socklen_t addrlen); // 返回:成功為讀或寫的字節數,失敗為-1
recvfrom和snedto的前3個參數和read/write的前3個參數一樣。flags表示設置的標志值,簡單的UDP程序可以直接設置為0,最後兩個參數表示服務端地址(對於sendto來說)或者是對端地址(對於recvfrom來說)。如果不關心對端的地址,則設置為NULL,此時addrlen也可以設置為NULL了。
注意:recvfrom和sendto也可以應用於TCP編程,不過一般不這樣用。UDP編程會有數據包的丟失問題,因為UDP是不可靠的,如果一個客戶的數據包丟失,客戶端將永遠阻塞在recvfrom函數調用;類似的,如果客戶數據到達了服務端,然後響應數據包丟失了,則客戶永遠阻塞在recvfrom調用。為了防止這樣的問題出現,一般可以給recvfrom設置一個超時時間。簡單的UDP使用recvfrom和sendto函數例子:探索UDP套接字編程。
參考資料:
1、UNIX網絡編程卷1:套接字聯網API(第3版) 中文高清帶完整書簽 PDF 下載見 http://www.linuxidc.com/Linux/2014-04/100222.htm
2、探索UDP套接字編程