深入理解套接字編程

網(wǎng)絡編程套接字

套接字地址結(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ù)角虫。

  1. 未完成連接隊列:每一個這樣的SYN分節(jié)對應其中一項,已由某個客戶發(fā)出并到達服務器委造,而服務器正在等待完成相應的TCP三路握手過程上遥。這些套接字處于SYN_RCVD狀態(tài)。
  2. 已完成連接隊列:每個完成TCP三路握手過程的客戶對應其中一項争涌,這些套接字處于ESTABLISHED狀態(tài)。
未連接隊列 & 已連接隊列
TCP 3次握手

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套接字編程

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末自沧,一起剝皮案震驚了整個濱河市坟奥,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌拇厢,老刑警劉巖爱谁,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異孝偎,居然都是意外死亡访敌,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門衣盾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來寺旺,“玉大人,你說我怎么就攤上這事势决∽杷埽” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵果复,是天一觀的道長陈莽。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么走搁? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任独柑,我火速辦了婚禮,結(jié)果婚禮上私植,老公的妹妹穿的比我還像新娘忌栅。我一直安慰自己,他們只是感情好兵琳,可當我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布狂秘。 她就那樣靜靜地躺著,像睡著了一般躯肌。 火紅的嫁衣襯著肌膚如雪者春。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天清女,我揣著相機與錄音钱烟,去河邊找鬼。 笑死嫡丙,一個胖子當著我的面吹牛拴袭,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播曙博,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼拥刻,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了父泳?” 一聲冷哼從身側(cè)響起般哼,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎惠窄,沒想到半個月后蒸眠,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡杆融,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年楞卡,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片脾歇。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡蒋腮,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出藕各,到底是詐尸還是另有隱情徽惋,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布座韵,位于F島的核電站险绘,受9級特大地震影響踢京,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜宦棺,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一瓣距、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧代咸,春花似錦蹈丸、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至思瘟,卻和暖如春荸百,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背滨攻。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工够话, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人光绕。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓女嘲,卻偏偏與公主長得像,于是被迫代替她去往敵國和親诞帐。 傳聞我的和親對象是個殘疾皇子欣尼,可洞房花燭夜當晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容