Unix網(wǎng)絡(luò)編程學(xué)習(xí)筆記之基于TCP套接字編程
1. socket函數(shù)
int socket(int family, int type,int protocol)
成返回一個(gè)套接字描述符。錯(cuò)誤返回-1
其中family指定協(xié)議族,一般IPv4為AF_INET, IPv6為AF_INET6。
其中type指定套接字類型,字節(jié)流:SOCK_STREAM. 數(shù)據(jù)報(bào):SOCK_DGRAM。
一般情況下通過family和type的組合都可以唯一確定一個(gè)套接字類型。所以一般我們就把protocol設(shè)為0就可以了。
有時(shí)在某些特殊情況下,family和type的組合不是都是有效的,這時(shí)我們就要給protocol指定一些特殊的值了。
2. connect函數(shù)
int connect(int sockfd, const struct sockaddr * servaddr, socklen_t addrlen);
連接服務(wù)器,其中servaddr是服務(wù)器的地址。
如果是TCP套接字,connect會(huì)觸發(fā)三次握手。
從前文可以知道,當(dāng)客戶端接收到服務(wù)器端的對SYN的響應(yīng)的時(shí)候,connect函數(shù)就返回,若客戶端發(fā)送的SYN出錯(cuò),或者響應(yīng)的ACK出錯(cuò)都會(huì)引起connect函數(shù)出錯(cuò)。成功返回0,出錯(cuò)返回-1且errno被設(shè)置。
注意:如果connect出錯(cuò),不能直接重新connect。必須要先關(guān)閉這個(gè)套接字,然后重新socket-connect。
3. bind函數(shù)
int bind(int sockfd, const struct sockaddr* servaddr, socklen_t addrlen)
成功返回0,錯(cuò)誤返回-1
為指定的套接字綁定一個(gè)本地的套接字地址。
(1) 一般服務(wù)器端需要綁定一個(gè)公開的端口號(hào),而服務(wù)器端一般綁定Ip時(shí)是INADDR_ANY,意為當(dāng)accept時(shí),內(nèi)核會(huì)從本地IP地址中選擇一個(gè)本地IP賦值。這對于一臺(tái)機(jī)器上有多個(gè)網(wǎng)絡(luò)接口時(shí),是很有影響的。
而通常機(jī)器只有一個(gè)網(wǎng)絡(luò)接口,則我們也使用這種方式,是因?yàn)槲覀儾槐匾獙懛?wù)器本地的IP(硬編碼),這樣寫使得我們的程序有好的移植性。
(2) 一般客戶端socket函數(shù)之后就直接connect了,不進(jìn)行bind,因?yàn)槲覀兺ǔ2恍柚付蛻舳说腎p和端口號(hào)。讓內(nèi)核自動(dòng)賦值就可以了。
(3) IPv4中的INADDR_ANY通常為0,所以我們?yōu)槠滟x值時(shí),是使用如下格式:
servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
因?yàn)閟in_addr是一個(gè)結(jié)構(gòu)體,所以我們使用sin_addr.s_addr來使用其整數(shù)形式賦值。
4. listen函數(shù)
int listen(int sockfd, intbacklog)
成功返回0,錯(cuò)誤返回-1
注意這里的listen并不是我們通常理解的監(jiān)聽的意思,因?yàn)樘捉幼植皇窃谶@里阻塞的,而是在accept阻塞的。
Listen只做兩件事:
(1) 把socket函數(shù)創(chuàng)建的套接字,設(shè)為被動(dòng)套接字。因?yàn)閟ocket函數(shù)默認(rèn)創(chuàng)建主動(dòng)套接字,主動(dòng)套接字:是需要connect去主動(dòng)連接的。
(2) 規(guī)定了內(nèi)核應(yīng)該為連接套接字排隊(duì)的***個(gè)數(shù)。
內(nèi)核是如何進(jìn)行連接排隊(duì)的?
內(nèi)核維護(hù)兩個(gè)隊(duì)列,未完成連接隊(duì)列,已完成連接隊(duì)列。
未完成連接隊(duì)列:客戶SYN到達(dá)后,就被放入未完成連接隊(duì)列隊(duì)尾。
已完成連接隊(duì)列:客戶完成了三次握手之后,就把它放入已完成隊(duì)列隊(duì)尾。
然后進(jìn)程調(diào)用accept,就從已完成隊(duì)列隊(duì)首項(xiàng)返回給進(jìn)程。
這里的疑問?服務(wù)器端不是一直在accept阻塞嗎,怎么這里還提到進(jìn)程調(diào)用accept這個(gè)說法?
因?yàn)檫@里的情況是在多個(gè)客戶端幾乎同時(shí)達(dá)到連接時(shí),其中某一個(gè)連接發(fā)生的情況,因?yàn)槲覀儗懛?wù)器端程序時(shí),都是把a(bǔ)ccept寫在一個(gè)循環(huán)內(nèi)的,所以某個(gè)客戶的SYN到達(dá),可能這時(shí)并沒有執(zhí)行到accept,所以這里說等到進(jìn)程調(diào)用accept時(shí)。也就是說,系統(tǒng)在已完成連接隊(duì)列為空時(shí),accept才會(huì)阻塞。
注意這里所說的backlog是兩個(gè)隊(duì)列之和,但實(shí)際情況下,一般內(nèi)核允許排隊(duì)的個(gè)數(shù)都要略大于這個(gè)值。
5. accept函數(shù)
int accept(int sockfd, struct sockaddr* cliaddr , socklen_t* addrlen)
成功返回描述符,錯(cuò)誤返回-1
接受客戶連接,如果已完成連接隊(duì)列中有數(shù)據(jù),則讀取隊(duì)頭,返回一個(gè)已連接套接字描述符。如果已完成連接隊(duì)列為空,則阻塞。
成功返回返回一個(gè)已連接套接字描述符,失敗返回負(fù)值。
注意:第三個(gè)參數(shù)為整型的地址。因?yàn)閍ccept函數(shù)是從內(nèi)核得到的套接字。如果程序?qū)蛻舳说奶捉幼值刂凡桓信d趣,則可以把后面兩個(gè)參數(shù)都設(shè)為NULL。
一個(gè)服務(wù)器通常只有一個(gè)監(jiān)聽套接字,而為每個(gè)客戶創(chuàng)建一個(gè)已連接套接字。
6. getsockname和getpeername
int getsockname(int sockfd, struct sockaddr* addr, socklen_t* addrlen)
返回和套接字描述符sockfd關(guān)聯(lián)的本地套接字地址
int getpeername(int sockfd, struct sockaddr* addr, socklen_t* addrlen)
返回和套接字描述符sockfd關(guān)聯(lián)的對端套接字地址
顯然這兩個(gè)函數(shù)都是從內(nèi)核中得到套接字地址,所以第三個(gè)參數(shù)是整型的地址。
注意:
(1) 一般客戶端沒有bind,所以在connect之后,才可以調(diào)用getsockname/getpeername。
(2) 一般服務(wù)器端bind端口之后,可以調(diào)用getsockname獲取端口號(hào)。
一般服務(wù)器端bind的是通配地址,所以一般不可以獲取監(jiān)聽套接字描述符的關(guān)聯(lián)ip地址,而是獲取已連接套接字描述符關(guān)聯(lián)的ip地址。
(3) POSIX允許對未bind的套接字調(diào)用getsockname,所以該函數(shù)適合任何已打開的套接字描述符(即調(diào)用socket函數(shù)返回的套接字描述符都叫已打開的套接字描述符),只是不一定輸出的是什么。
插入知識(shí):
1. socket這幾個(gè)函數(shù)都是一樣的,成功返回0/描述符,失敗返回-1。所以它們判斷成功的條件都是一樣的。
2. RST分組,RST分組是TCP在發(fā)生錯(cuò)誤時(shí)發(fā)送的一種TCP分組。
產(chǎn)生RST的條件:
(1) 一個(gè)目的地為某端口的SYN到達(dá),然后本機(jī)沒有正在監(jiān)聽該端口的程序,此時(shí)本機(jī)就發(fā)送一個(gè)RST。
(2) TCP想要取消一個(gè)連接。
(3) TCP接受到一個(gè)不存在的連接的分組。即某個(gè)客戶端沒有連接,就往服務(wù)器發(fā)送數(shù)據(jù),這時(shí)服務(wù)器就會(huì)給這個(gè)客戶端發(fā)送RST。
其實(shí)RST的意思就是讓對方重新連接的意思。