套接字地址結(jié)構(gòu)
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是網(wǎng)絡套接字地址結(jié)構(gòu),大小為16字節(jié)审姓,定義在<netinet/in>頭文件中滞乙,一般我們在程序中是使用該結(jié)構(gòu)體,但是作為參數(shù)傳遞給套接字函數(shù)時需要強轉(zhuǎn)為sockaddr類型腌乡,注意該結(jié)構(gòu)體中port和addr成員是網(wǎng)絡序的(大端結(jié)構(gòu))。
struct sockaddr {
sa_family_t sa_family; //address family: AF_XXX value
char sa_data[14]; //protocol-specific address
}
sockaddr是通過套接字地址結(jié)構(gòu)夜牡,當作為參數(shù)傳遞給套接字函數(shù)時与纽,套接字地址結(jié)構(gòu)總是以指針方式來使用,比如bind/accept/connect函數(shù)等塘装。
htons急迂、ntohs、htonl和ntohl函數(shù)
#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個函數(shù)來完成主機字節(jié)序和網(wǎng)絡字節(jié)序之間的轉(zhuǎn)換蹦肴。這些函數(shù)名字中僚碎,h表示host,n表示net冗尤,s表示short听盖,l表示long。使用這些函數(shù)時裂七,并不關(guān)心主機字節(jié)序和網(wǎng)絡字節(jié)序的真實值皆看,也就是為大端還是小端,要做的只是調(diào)用適當?shù)暮瘮?shù)在主機和網(wǎng)絡字節(jié)序之間轉(zhuǎn)換為某個特定值背零。
ient_aton腰吟、inet_addr和inet_ntoa函數(shù)
#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位二進制網(wǎng)絡字節(jié)序地址徙瓶,否則為INADDR_NONE
char *inet_ntoa(struct in_addr inaddr); // 返回:指向一個點分十進制數(shù)串的地址
inet_aton毛雇、inet_addr和inet_ntoa在點分十進制數(shù)串(比如"192.168.1.1")與它長度為32位的網(wǎng)絡字節(jié)序二進制值間轉(zhuǎn)換IPv4地址。在調(diào)用inet_addr時需特別注意侦镇,inet_ntoa函數(shù)的輸入?yún)?shù)是unsigned int型的ip地址灵疮,返回的卻是指向ip字符串的指針,很明顯壳繁,ip字符串所占的內(nèi)存是在函數(shù)內(nèi)部分配的震捣,而我們并不需要釋放該內(nèi)存,所以闹炉,它分配的內(nèi)存是靜態(tài)的蒿赢,內(nèi)部使用static變量存儲IP點分十進制數(shù)串,也就是說第二次調(diào)用該函數(shù)時會覆蓋第一次調(diào)用該函數(shù)時的內(nèi)存渣触。
inet_pton和inet_ntop函數(shù)
#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); // 返回:成功為指向結(jié)果的指針嗅钻,出錯為NULL
這兩個函數(shù)對于IPv4和IPv6都適用皂冰,p代表表達式(presentation)店展、n表示數(shù)值(numeric)。第一個函數(shù)嘗試轉(zhuǎn)化由strptr指針所指的字符串灼擂,通過addptr指針存放二進制結(jié)果壁查,成功返回1,如果對指定的family而言輸入的不是有效的表達格式剔应,那么返回0。
inet_ntop進行相反的操作语御,如果len的值太小峻贮,不足以存放表達式結(jié)果,則返回一個空指針应闯,并置error為ENOSPC纤控。inet_ntop函數(shù)的strptr參數(shù)不可以是一個空指針,調(diào)用者必須為目標存儲單元分配內(nèi)存并制定其大小碉纺,調(diào)用成功時船万,這個指針就是該函數(shù)返回值。
socket函數(shù)
為了進行網(wǎng)絡通信骨田,一個進程必須做的第一件事就是調(diào)用socket函數(shù)耿导,指定期望的通信協(xié)議類型(比如使用IPv4的TCP、使用IPv6的UDP态贤、Unix域字節(jié)流協(xié)議)和套接字字類型(字節(jié)流舱呻、數(shù)據(jù)報或原始套接字)。
#include <sys/socket.h>
int socket(int family, int type, int protocol); // 成功返回非負描述符悠汽,出錯-1
family指定協(xié)議族箱吕,type指定套接字類型,protocol指定某個協(xié)議類型常值柿冲,或者設為0茬高。
family的值有:
- AF_INET IPv4協(xié)議
- AF_INET6 Ipv6協(xié)議
- AF_LOCAL Unix協(xié)議域
- AF_ROUTE 路由套接字
- AF_KEY 秘鑰套接字
type的值有:
- SOCK_STREAM 字節(jié)流套接字
- SOCK_DGRAM 數(shù)據(jù)報套接字
- SOCK_SEQPACKET 有序分組套接字
- SOCK_RAW 原始套接字
protocol的值有:
- IPPROTO_CP TCP傳輸協(xié)議
- IPPROTO_UDP UDP傳輸協(xié)議
- IPPROTO_SCTP SCTP傳輸協(xié)議
socket函數(shù)在成功時返回一個小的非負整數(shù)值,與文件描述符類似假抄,稱為套接字描述符怎栽,為了得到這個描述符,需要指定協(xié)議族和套接字類型慨亲,但是并沒有指定本地協(xié)議地址和遠端協(xié)議地址婚瓜。
connect函數(shù)
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr* servaddr, socklen_t addrlen); // 返回:成功為0,出錯-1
TCP客戶用connect函數(shù)來建立一個與TCP服務器連接刑棵,sockfd是由socket函數(shù)返回的套接字描述符巴刻,第二個、第三個參數(shù)分別是指向一個套接字地址結(jié)構(gòu)的指針和該結(jié)構(gòu)的大小蛉签,套接字結(jié)構(gòu)必須含有服務器的IP地址和端口號胡陪。注意:如果connect失敗后沥寥,就必須close當前的套接字描述符并重新調(diào)用socket∧客戶端在調(diào)用connect前不必非得調(diào)用bind函數(shù)(比如UDP客戶端編程中一般就不用調(diào)用bind)邑雅,內(nèi)核會確定源IP地址,并選擇一個臨時端口作為源端口妈经。
如果是TCP套接字淮野,調(diào)用connect函數(shù)將出發(fā)TCP的三次握手過程,而且僅在連接建立成功或出錯時才返回吹泡。注意:connect是在接收到服務端響應的SYN+ACK時的返回的骤星,也就是三次握手的第二次動作之后。
UDP也是可以調(diào)用connect函數(shù)的爆哑,但是UDP的connect函數(shù)和TCP的connect函數(shù)調(diào)用確是大相徑庭的洞难,這里沒有三次握手過程。內(nèi)核只是檢查是否存在立即可知的錯誤(比如目的地址不可達)揭朝,記錄對端的IP和端口號队贱,然后立即返回調(diào)用進程。使用了connect的UDP編程就可不必使用sendto函數(shù)了潭袱,直接使用write/read即可柱嫌。
bind函數(shù)
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen); // 返回:成功為0,出錯-1
bind函數(shù)把一個本地協(xié)議地址賦予一個套接字敌卓,它只是把一個協(xié)議地址賦予一個套接字慎式,至于協(xié)議地址的含義則取決于協(xié)議本身。第二個參數(shù)指向協(xié)議地址結(jié)構(gòu)的指針趟径,第三個參數(shù)是協(xié)議地址的長度瘪吏,對于TCP,調(diào)用bind函數(shù)可以指定一個端口號蜗巧,或指定一個IP地址掌眠,或兩者都指定,也可以兩者都不指定幕屹。
bind函數(shù)綁定特定的IP地址必須屬于其所在主機的網(wǎng)絡接口之一蓝丙,服務器在啟動時綁定它們眾所周知的端口,如果一個TCP客戶端或服務端未曾調(diào)用bind綁定一個端口望拖,當調(diào)用connect或listen時渺尘,內(nèi)核就要為響應的套接字選擇一個臨時端口。讓內(nèi)核選擇臨時端口對于TCP客戶端來說是正常的说敏,然而對于TCP服務端來說確實罕見的鸥跟,因為服務端通過他們眾所周知的端口被大家認識的。
listen函數(shù)
#include <sys/socket.h>
int listen(int sockfd, int backlog); // 返回:成功返回0,出錯-1
socket創(chuàng)建一個套接字時医咨,它被假設為一個主動套接字枫匾,也就是說,它是一個將調(diào)用connect發(fā)起連接的一個客戶套接字拟淮。listen函數(shù)把一個未連接的套接字轉(zhuǎn)換為一個被動套接字干茉,指示內(nèi)核應接受指向該套接字的連接請求,調(diào)用listen函數(shù)將導致套接字從CLOSEE狀態(tài)轉(zhuǎn)換到LISTEN狀態(tài)很泊。第二個參數(shù)規(guī)定了內(nèi)核應為相應套接字排隊的最大連接個數(shù)角虫。
- 未完成連接隊列:每一個這樣的SYN分節(jié)對應其中一項,已由某個客戶發(fā)出并到達服務器委造,而服務器正在等待完成相應的TCP三路握手過程上遥。這些套接字處于SYN_RCVD狀態(tài)。
- 已完成連接隊列:每個完成TCP三路握手過程的客戶對應其中一項争涌,這些套接字處于ESTABLISHED狀態(tài)。
backlog參數(shù)在不同的系統(tǒng)中有不同的解釋辣恋,不過大致類似亮垫。UNP(第3版)給出的定義為:listen()的backlog應該指定某個給定套接字上內(nèi)核為之排隊的最大已完成連接數(shù)。
當一個客戶端SYN達到時伟骨,若這些隊列是滿的饮潦,TCP就忽略該分節(jié),也即是不發(fā)送RST携狭,這樣做是暫時的继蜡,客戶端將重新發(fā)送SYN,期望不久就能得到服務逛腿。假如服務端響應一個RST稀并,客戶端的connect就會返回錯誤冲九,而不是讓重傳機制來處理荧关,這樣客戶無法區(qū)分SYN的RST是因為"該端口沒有在監(jiān)聽"還是"該端口在監(jiān)聽,只不過它的隊列滿了"格仲。
在三路握手完成之后搁廓,但在服務端調(diào)用accept之前到達的數(shù)據(jù)應由服務端TCP排隊引颈,最大數(shù)據(jù)量為相應已連接套接字的接收緩沖區(qū)大小。
在TCP服務端套接字編程中境蜕,執(zhí)行完listen后蝙场,而沒有執(zhí)行accept,客戶端是可以成功建立連接的粱年,只不過是該連接被加入到了已連接隊列中售滤,當調(diào)用accept時會被提取出來。
accept函數(shù)
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen); // 返回:成功返回已連接描述符(非負)逼泣,出錯-1
accept函數(shù)由TCP服務器調(diào)用趴泌,用于從已完成隊列中列頭返回下一個已完成連接舟舒,如果已完成隊列為空,則進程被投入睡眠(如果該套接字為阻塞方式的話)嗜憔。如果accept成功秃励,那么其返回值是由內(nèi)核自動生成的一個全新套接字,代表與客戶建立的TCP連接吉捶,函數(shù)的第一個參數(shù)為監(jiān)聽套接字夺鲜,返回值為已連接套接字。
close函數(shù)
#include <unistd.h>
int close(int sockfd); // 若成功返回0呐舔,出錯-1
close一個TCP套接字的默認行為是把該套接字標記為已關(guān)閉币励,然后立即返回到調(diào)用進程。注意珊拼,close實質(zhì)把該套接字引用值減1食呻,如果該引用值大于0,則對應的套接字不會被真正關(guān)掉澎现;否則內(nèi)核關(guān)閉該套接字仅胞。
服務器、客戶端交互流程圖
getsockname和getpeername函數(shù)
#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參數(shù)指定的內(nèi)存地址妹蔽,該socket長度存儲于addrlen指向的變量中椎眯。getpeername獲取遠端的socket地址。UDP客戶端如果調(diào)用connect之后也是可以使用getpeername的胳岂。
recv和send函數(shù)
#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); // 返回:成功為讀入或?qū)懭氲淖止?jié)數(shù)编整,出錯為-1
TCP流數(shù)據(jù)讀寫操作函數(shù)。flag取值如下所示:
MSG_OOB 對于send旦万,表明將要發(fā)送帶外數(shù)據(jù)闹击,TCP連接上只有一個字節(jié)可以作為帶外數(shù)據(jù)發(fā)送,對于recv成艘,本標志表明即將要讀入的是帶外數(shù)據(jù)而不是普通數(shù)據(jù)赏半。
MSG_PEEK 該標志適用于recv和recvfrom,它允許我們查看已讀取的數(shù)據(jù)淆两。注意的是断箫,flags參數(shù)只對send和recv的當前調(diào)用有效,當然也可以通過setsockopt系統(tǒng)調(diào)用永久性地修改socket的某些屬性秋冰。
recvfrom和sendto函數(shù)
#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); // 返回:成功為讀或?qū)懙淖止?jié)數(shù)仲义,失敗為-1
recvfrom和snedto的前3個參數(shù)和read/write的前3個參數(shù)一樣。flags表示設置的標志值,簡單的UDP程序可以直接設置為0埃撵,最后兩個參數(shù)表示服務端地址(對于sendto來說)或者是對端地址(對于recvfrom來說)赵颅。如果不關(guān)心對端的地址,則設置為NULL暂刘,此時addrlen也可以設置為NULL了饺谬。
注意:recvfrom和sendto也可以應用于TCP編程,不過一般不這樣用谣拣。UDP編程會有數(shù)據(jù)包的丟失問題募寨,因為UDP是不可靠的,如果一個客戶的數(shù)據(jù)包丟失森缠,客戶端將永遠阻塞在recvfrom函數(shù)調(diào)用拔鹰;類似的,如果客戶數(shù)據(jù)到達了服務端贵涵,然后響應數(shù)據(jù)包丟失了列肢,則客戶永遠阻塞在recvfrom調(diào)用。為了防止這樣的問題出現(xiàn)宾茂,一般可以給recvfrom設置一個超時時間例书。簡單的UDP使用recvfrom和sendto函數(shù)例子:探索UDP套接字編程。
參考資料:
1刻炒、《UNIX網(wǎng)絡編程-卷一》
2、探索UDP套接字編程