基本 TCP 套接字編程講解
基于 TCP 的套接字編程的所有客戶端和服務(wù)器端都是從調(diào)用socket 開始,它返回一個套接字描述符??蛻舳穗S后調(diào)用connect 函數(shù),服務(wù)器端則調(diào)用 bind、listen 和accept 函數(shù)。套接字通常使用標(biāo)準(zhǔn)的close 函數(shù)關(guān)閉,但是也可以使用 shutdown 函數(shù)關(guān)閉套接字。下面針對套接字編程實(shí)現(xiàn)過程中所調(diào)用的函數(shù)進(jìn)程分析。以下是基于 TCP 套接字編程的流程圖:
socket 函數(shù)
套接字是通信端點(diǎn)的抽象,實(shí)現(xiàn)端對端之間的通信。與應(yīng)用程序要使用文件描述符訪問文件一樣,訪問套接字需要套接字描述符。任何套接字編程都必須調(diào)用socket 函數(shù)獲得套接字描述符,這樣才能對套接字進(jìn)行操作。以下是該函數(shù)的描述:
- /* 套接字 */
- /*
- * 函數(shù)功能:創(chuàng)建套接字描述符;
- * 返回值:若成功則返回套接字非負(fù)描述符,若出錯返回-1;
- * 函數(shù)原型:
- */
- #include <sys/socket.h>
- int socket(int family, int type, int protocol);
- /*
- * 說明:
- * socket類似與open對普通文件操作一樣,都是返回描述符,后續(xù)的操作都是基于該描述符;
- * family 表示套接字的通信域,不同的取值決定了socket的地址類型,其一般取值如下:
- * (1)AF_INET IPv4因特網(wǎng)域
- * (2)AF_INET6 IPv6因特網(wǎng)域
- * (3)AF_UNIX Unix域
- * (4)AF_ROUTE 路由套接字
- * (5)AF_KEY 密鑰套接字
- * (6)AF_UNSPEC 未指定
- *
- * type確定socket的類型,常用類型如下:
- * (1)SOCK_STREAM 有序、可靠、雙向的面向連接字節(jié)流套接字
- * (2)SOCK_DGRAM 長度固定的、無連接的不可靠數(shù)據(jù)報(bào)套接字
- * (3)SOCK_RAW 原始套接字
- * (4)SOCK_SEQPACKET 長度固定、有序、可靠的面向連接的有序分組套接字
- *
- * protocol指定協(xié)議,常用取值如下:
- * (1)0 選擇type類型對應(yīng)的默認(rèn)協(xié)議
- * (2)IPPROTO_TCP TCP傳輸協(xié)議
- * (3)IPPROTO_UDP UDP傳輸協(xié)議
- * (4)IPPROTO_SCTP SCTP傳輸協(xié)議
- * (5)IPPROTO_TIPC TIPC傳輸協(xié)議
- *
- */
connect 函數(shù)
在處理面向連接的網(wǎng)絡(luò)服務(wù)時,例如 TCP ,交換數(shù)據(jù)之前必須在請求的進(jìn)程套接字和提供服務(wù)的進(jìn)程套接字之間建立連接。TCP 客戶端可以調(diào)用函數(shù)connect 來建立與 TCP 服務(wù)器端的一個連接。該函數(shù)的描述如下:
- /*
- * 函數(shù)功能:建立連接,即客戶端使用該函數(shù)來建立與服務(wù)器的連接;
- * 返回值:若成功則返回0,出錯則返回-1;
- * 函數(shù)原型:
- */
- #include <sys/socket.h>
- int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);
- /*
- * 說明:
- * sockfd是系統(tǒng)調(diào)用的套接字描述符,即由socket函數(shù)返回的套接字描述符;
- * servaddr是目的套接字的地址,該套接字地址結(jié)構(gòu)必須包含目的IP地址和目的端口號,即想與之通信的服務(wù)器地址;
- * addrlen是目的套接字地址的大??;
- *
- * 如果sockfd沒有綁定到一個地址,connect會給調(diào)用者綁定一個默認(rèn)地址,即內(nèi)核會確定源IP地址,并選擇一個臨時端口號作為源端口號;
- */
TCP 客戶端在調(diào)用函數(shù) connect 前不必非得調(diào)用 bind 函數(shù),因?yàn)閮?nèi)核會確定源 IP 地址,并選擇一個臨時端口作為源端口號。若 TCP 套接字調(diào)用connect 函數(shù)將建立 TCP 連接(執(zhí)行三次握手),而且僅在連接建立成功或出錯時才返回,其中出錯返回可能有以下幾種情況:
若 TCP 客戶端沒有收到 SYN 報(bào)文段的響應(yīng),則返回 ETIMEOUT 錯誤;
若客戶端的 SYN 報(bào)文段的響應(yīng)是 RST (表示復(fù)位),則表明該服務(wù)器主機(jī)在我們指定的端口上沒有進(jìn)程在等待與之連接。只是一種硬錯誤,客戶端一接收到 RST 就立即返回ECONNERFUSED 錯誤;
RST 是 TCP 在發(fā)生錯誤時發(fā)送的一種 TCP 報(bào)文段。產(chǎn)生 RST 的三個條件時:
目的地為某端口的 SYN 到達(dá),然而該端口上沒有正在監(jiān)聽的服務(wù)器;
TCP 想取消一個已有連接;
TCP 接收到一個不存在的連接上的報(bào)文段;
若客戶端發(fā)出的 SYN 在中某個路由器上引發(fā)一個目的地不可達(dá)的 ICMP 錯誤,這是一個軟錯誤??蛻舳酥鳈C(jī)內(nèi)核保存該消息,并在一定的時間間隔繼續(xù)發(fā)送 SYN (即重發(fā))。在某規(guī)定的時間后仍未收到響應(yīng),則把保存的消息(即 ICMP 錯誤)作為EHOSTUNREACH 或ENETUNREACH 錯誤返回給進(jìn)行。#p#
bind 函數(shù)
調(diào)用函數(shù) socket 創(chuàng)建套接字描述符時,該套接字描述符是存儲在它的協(xié)議族空間中,沒有具體的地址,要使它與一個地址相關(guān)聯(lián),可以調(diào)用函數(shù)bind 使其與地址綁定。客戶端的套接字關(guān)聯(lián)的地址一般可由系統(tǒng)默認(rèn)分配,因此不需要指定具體的地址。若要為服務(wù)器端套接字綁定地址,可以通過調(diào)用函數(shù) bind 將套接字綁定到一個地址。下面是該函數(shù)的描述:
- /* 套接字的基本操作 */
- /*
- * 函數(shù)功能:將協(xié)議地址綁定到一個套接字;其中協(xié)議地址包含IP地址和端口號;
- * 返回值:若成功則返回0,若出錯則返回-1;
- * 函數(shù)原型:
- */
- #include <sys/socket.h>
- int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- /*
- * 說明:
- * sockfd 為套接字描述符;
- * addr是一個指向特定協(xié)議地址結(jié)構(gòu)的指針;
- * addrlen是地址結(jié)構(gòu)的長度;
- */
對于 TCP 協(xié)議,調(diào)用 bind 函數(shù)可以指定一個端口號,或指定一個 IP 地址,也可以兩者都指定,還可以都不指定。若 TCP 客戶端或服務(wù)器端不調(diào)用bind 函數(shù)綁定一個端口號,當(dāng)調(diào)用connect 或 listen 函數(shù)時,內(nèi)核會為相應(yīng)的套接字選擇一個臨時端口號。一般 TCP 客戶端使用內(nèi)核為其選擇一個臨時的端口號,而服務(wù)器端通過調(diào)用bind 函數(shù)將端口號與相應(yīng)的套接字綁定。進(jìn)程可以把一個特定的 IP 地址捆綁到它的套接字上,但是這個 IP 地址必須屬于其所在主機(jī)的網(wǎng)絡(luò)接口之一。對于 TCP 客戶端,這就為在套接字上發(fā)送的 IP 數(shù)據(jù)報(bào)指派了源 IP 地址。對于 TCP 服務(wù)器端,這就限定該套接字只接收那些目的地為這個 IP 地址的客戶端連接。TCP 客戶端一般不把 IP 地址捆綁到它的套接字上。當(dāng)連接套接字時,內(nèi)核將根據(jù)所用外出網(wǎng)絡(luò)接口來選擇源 IP 地址,而所用外出接口則取決于到達(dá)服務(wù)器端所需的路徑。若 TCP 服務(wù)器端沒有把 IP 地址捆綁到它的套接字上,內(nèi)核就把客戶端發(fā)送的 SYN 的目的 IP 地址作為服務(wù)器端的源 IP 地址。
在地址使用方面有下面一些限制:
在進(jìn)程所運(yùn)行的機(jī)器上,指定的地址必須有效,不能指定其他機(jī)器的地址;
地址必須和創(chuàng)建套接字時的地址族所支持的格式相匹配;
端口號必須不小于1024,除非該進(jìn)程具有相應(yīng)的特權(quán)(超級用戶);
一般只有套接字端點(diǎn)能夠與地址綁定,盡管有些協(xié)議允許多重綁定;
listen 函數(shù)
在編寫服務(wù)器程序時需要使用監(jiān)聽函數(shù) listen 。服務(wù)器進(jìn)程不知道要與誰連接,因此,它不會主動地要求與某個進(jìn)程連接,只是一直監(jiān)聽是否有其他客戶進(jìn)程與之連接,然后響應(yīng)該連接請求,并對它做出處理,一個服務(wù)進(jìn)程可以同時處理多個客戶進(jìn)程的連接。listen 函數(shù)描述如下:
- /*
- * 函數(shù)功能:接收連接請求;
- * 函數(shù)原型:
- */
- #include <sys/socket.h>
- int listen(int sockfd, int backlog);//若成功則返回0,若出錯則返回-1;
- /*
- * sockfd是套接字描述符;
- * backlog是該進(jìn)程所要入隊(duì)請求的最大請求數(shù)量;
- */
listen 函數(shù)僅由 TCP 服務(wù)器調(diào)用,它有以下兩種作用:
當(dāng) socket 函數(shù)創(chuàng)建一個套接字時,若它被假設(shè)為一個主動套接字,即它是一個將調(diào)用connect 發(fā)起連接的客戶端套接字。listen 函數(shù)把一個未連接的套接字轉(zhuǎn)換成一個被動套接字,指示內(nèi)核應(yīng)該接受指向該套接字的連接請求;
listen 函數(shù)的第二個參數(shù)規(guī)定內(nèi)核應(yīng)該為相應(yīng)套接字排隊(duì)的最大連接個數(shù);
listen 函數(shù)一般應(yīng)該在調(diào)用socket 和bind 這兩個函數(shù)之后,并在調(diào)用 accept 函數(shù)之前調(diào)用。 內(nèi)核為任何一個給定監(jiān)聽套接字維護(hù)兩個隊(duì)列:
未完成連接隊(duì)列,每個這樣的 SYN 報(bào)文段對應(yīng)其中一項(xiàng):已由某個客戶端發(fā)出并到達(dá)服務(wù)器,而服務(wù)器正在等待完成相應(yīng)的 TCP 三次握手過程。這些套接字處于 SYN_REVD 狀態(tài);
已完成連接隊(duì)列,每個已完成 TCP 三次握手過程的客戶端對應(yīng)其中一項(xiàng)。這些套接字處于 ESTABLISHED 狀態(tài);
accept 函數(shù)
accept 函數(shù)由 TCP 服務(wù)器調(diào)用,用于從已完成連接隊(duì)列隊(duì)頭返回下一個已完成連接。如果已完成連接隊(duì)列為空,那么進(jìn)程被投入睡眠。該函數(shù)的返回值是一個新的套接字描述符,返回值是表示已連接的套接字描述符,而第一個參數(shù)是服務(wù)器監(jiān)聽套接字描述符。一個服務(wù)器通常僅僅創(chuàng)建一個監(jiān)聽套接字,它在該服務(wù)器的生命周期內(nèi)一直存在。內(nèi)核為每個由服務(wù)器進(jìn)程接受的客戶連接創(chuàng)建一個已連接套接字(表示 TCP 三次握手已完成),當(dāng)服務(wù)器完成對某個給定客戶的服務(wù)時,相應(yīng)的已連接套接字就會被關(guān)閉。該函數(shù)描述如下:
- /* 函數(shù)功能:從已完成連接隊(duì)列隊(duì)頭返回下一個已完成連接;若已完成連接隊(duì)列為空,則進(jìn)程進(jìn)入睡眠;
- * 函數(shù)原型:
- */
- int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);//返回值:若成功返回套接字描述符,出錯返回-1;
- /*
- * 說明:
- * 參數(shù) cliaddr 和 addrlen 用來返回已連接的對端(客戶端)的協(xié)議地址;
- *
- * 該函數(shù)返回套接字描述符,該描述符連接到調(diào)用connect函數(shù)的客戶端;
- * 這個新的套接字描述符和原始的套接字描述符sockfd具有相同的套接字類型和地址族,而傳給accept函數(shù)的套接字描述符sockfd沒有關(guān)聯(lián)到這個鏈接,
- * 而是繼續(xù)保持可用狀態(tài)并接受其他連接請求;
- * 若不關(guān)心客戶端協(xié)議地址,可將cliaddr和addrlen參數(shù)設(shè)置為NULL,否則,在調(diào)用accept之前,應(yīng)將參數(shù)cliaddr設(shè)為足夠大的緩沖區(qū)來存放地址,
- * 并且將addrlen設(shè)為指向代表這個緩沖區(qū)大小的整數(shù)指針;
- * accept函數(shù)返回時,會在緩沖區(qū)填充客戶端的地址并更新addrlen所指向的整數(shù)為該地址的實(shí)際大小;
- *
- * 若沒有連接請求等待處理,accept會阻塞直到一個請求到來;
#p#fork 和 exec 函數(shù)
- /* 函數(shù)功能:創(chuàng)建子進(jìn)程;
- * 返回值:
- * (1)在子進(jìn)程中,返回0;
- * (2)在父進(jìn)程中,返回新創(chuàng)建子進(jìn)程的進(jìn)程ID;
- * (3)若出錯,則范回-1;
- * 函數(shù)原型:
- */
- #include <unistd.h>
- pid_t fork(void);
- /* 說明:
- * 該函數(shù)調(diào)用一次若成功則返回兩個值:
- * 在調(diào)用進(jìn)程(即父進(jìn)程)中,返回新創(chuàng)建進(jìn)程(即子進(jìn)程)的進(jìn)程ID;
- * 在子進(jìn)程返回值是0;
- * 因此,可以根據(jù)返回值判斷進(jìn)程是子進(jìn)程還是父進(jìn)程;
- */
- /* exec 序列函數(shù) */
- /*
- * 函數(shù)功能:把當(dāng)前進(jìn)程替換為一個新的進(jìn)程,新進(jìn)程與原進(jìn)程ID相同;
- * 返回值:若出錯則返回-1,若成功則不返回;
- * 函數(shù)原型:
- */
- #include <unistd.h>
- int execl(const char *pathname, const char *arg, ...);
- int execv(const char *pathnam, char *const argv[]);
- int execle(const char *pathname, const char *arg, ... , char *const envp[]);
- int execve(const char *pathnam, char *const argv[], char *const envp[]);
- int execlp(const char *filename, const char *arg, ...);
- int execvp(const char *filename, char *const argv[]);
- /* 6 個函數(shù)的區(qū)別如下:
- * (1)待執(zhí)行的程序文件是 文件名 還是由 路徑名 指定;
- * (2)新程序的參數(shù)是 一一列出 還是由一個 指針數(shù)組 來引用;
- * (3)把調(diào)用進(jìn)程的環(huán)境傳遞給新程序 還是 給新程序指定新的環(huán)境;
- */
exec 6個函數(shù)在函數(shù)名和使用語法的規(guī)則上都有細(xì)微的區(qū)別,下面就從可執(zhí)行文件查找方式、參數(shù)傳遞方式及環(huán)境變量這幾個方面進(jìn)行比較。
查找方式:前4個函數(shù)的查找方式都是完整的文件目錄路徑 pathname ,而最后兩個函數(shù)(也就是以p結(jié)尾的兩個函數(shù))可以只給出文件名 filename,系統(tǒng)就會自動按照環(huán)境變量 “$PATH” 所指定的路徑進(jìn)行查找。
參數(shù)傳遞方式:exec 序列函數(shù)的參數(shù)傳遞有兩種方式:一種是逐個列舉的方式,而另一種則是將所有參數(shù)整體構(gòu)造指針數(shù)組傳遞。在這里是以函數(shù)名的第5位字母來區(qū)分的,字母為 “l”(list)的表示逐個列舉參數(shù)的方式,其語法為 const char *arg;字母為 “v”(vertor)的表示將所有參數(shù)整體構(gòu)造指針數(shù)組傳遞,其語法為 char *const argv[]。讀者可以觀察 execl()、execle()、execlp() 的語法與 execv()、execve()、execvp() 的區(qū)別。這里的參數(shù)實(shí)際上就是用戶在使用這個可執(zhí)行文件時所需的全部命令選項(xiàng)字符串(包括該可執(zhí)行程序命令本身)。要注意的是,這些參數(shù)必須以NULL結(jié)束。
環(huán)境變量:exec 序列函數(shù)可以默認(rèn)系統(tǒng)的環(huán)境變量,也可以傳入指定的環(huán)境變量。這里以 “e”(environment)結(jié)尾的兩個函數(shù) execle() 和 execve() 就可以在 envp[] 中指定當(dāng)前進(jìn)程所使用的環(huán)境變量。
- 表 1 exec 序列函數(shù)的總結(jié)
- 前4位 統(tǒng)一為:exec
- 第5位 l:參數(shù)傳遞為逐個列舉方式 execl、execle、execlp
- v:參數(shù)傳遞為構(gòu)造指針數(shù)組方式 execv、execve、execvp
- 第6位 e:可傳遞新進(jìn)程環(huán)境變量 execle、execve
- p:可執(zhí)行文件查找方式為文件名 execlp、execvp
其關(guān)系如下圖:
并發(fā)服務(wù)器
當(dāng)要求一個服務(wù)器同時為多個客戶服務(wù)時,需要并發(fā)服務(wù)器。TCP 并發(fā)服務(wù)器,它們?yōu)槊總€待處理的客戶端連接調(diào)用 fork 函數(shù)派生一個子進(jìn)程。當(dāng)一個連接建立時,accept 返回,服務(wù)器接著調(diào)用 fork 函數(shù),然后由子進(jìn)程服務(wù)客戶端,父進(jìn)程則等待另一個連接,此時,父進(jìn)程必須關(guān)閉已連接套接字。
close 和 shutdown 函數(shù)
當(dāng)要關(guān)閉套接字時,可使用 close 和 shutdown 函數(shù),其描述如下:
- /* 函數(shù)功能:關(guān)閉套接字,若是在 TCP 協(xié)議中,并終止 TCP 連接;
- * 返回值:若成功則返回0,若出錯則返回-1;
- * 函數(shù)原型:
- */
- #include <unistd.h>
- int close(int sockfd);
- /*
- * 函數(shù)功能:關(guān)閉套接字上的輸入或輸出;
- * 返回值:若成功則返回0,若出錯返回-1;
- * 函數(shù)原型:
- */
- #include <sys/socket.h>
- int shutdown(int sockfd, int how);
- /*
- * 說明:
- * sockfd表示待操作的套接字描述符;
- * how表示具體操作,取值如下:
- * (1)SHUT_RD 關(guān)閉讀端,即不能接收數(shù)據(jù)
- * (2)SHUT_WR 關(guān)閉寫端,即不能發(fā)送數(shù)據(jù)
- * (3)SHUT_RDWR 關(guān)閉讀、寫端,即不能發(fā)送和接收數(shù)據(jù)
- *
- */
getsockname 和 getpeername 函數(shù)
為了獲取已綁定到套接字的地址,我們可以調(diào)用函數(shù) getsockname 來實(shí)現(xiàn):
- /*
- * 函數(shù)功能:獲取已綁定到一個套接字的地址;
- * 返回值:若成功則返回0,若出錯則返回-1;
- * 函數(shù)原型:
- */
- #include <sys/socket.h>
- int getsockname(int sockfd, struct sockaddr *addr, socklen_t *alenp);
- /*
- * 說明:
- * 調(diào)用該函數(shù)之前,設(shè)置alenp為一個指向整數(shù)的指針,該整數(shù)指定緩沖區(qū)sockaddr的大??;
- * 返回時,該整數(shù)會被設(shè)置成返回地址的大小,如果該地址和提供的緩沖區(qū)長度不匹配,則將其截?cái)喽粓?bào)錯;
- */
- /*
- * 函數(shù)功能:獲取套接字對方連接的地址;
- * 返回值:若成功則返回0,若出錯則返回-1;
- * 函數(shù)原型:
- */
- #include <sys/socket.h>
- int getpeername(int sockfd, struct sockaddr *addr, socklen_t *alenp);
- /*
- * 說明:
- * 該函數(shù)除了返回對方的地址之外,其他功能和getsockname一樣;
- */