??????? 基于 TCP 的套接字編程的所有客戶端和服務器端都是從調用socket 開始,它返回一個套接字描述符。客戶端隨后調用connect 函數,服務器端則調用 bind、listen 和accept 函數。
??????? 使用套接口客戶機服務器的的例子
?
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h> int port = 8000;void main() {struct sockaddr_in sin;struct sockaddr_in pin;int sock_descriptor;int temp_sock_descriptor;int address_size;char buf[16384];int i, len;sock_descriptor = socket(AF_INET, SOCK_STREAM, 0);if (sock_descriptor == -1) {perror("call to socket");exit(1);}bzero(&sin, sizeof(sin));sin.sin_family = AF_INET;sin.sin_addr.s_addr = INADDR_ANY;sin.sin_port = htons(port);if (bind(sock_descriptor, (struct sockaddr *)&sin, sizeof(sin)) == -1) {perror("call to bind");exit(1);}if (listen(sock_descriptor, 20) == -1) {perror("call to listen");exit(1);}printf("Accepting connections ...\n");while(1) {temp_sock_descriptor =accept(sock_descriptor, (struct sockaddr *)&pin,&address_size);if (temp_sock_descriptor == -1) {perror("call to accept");exit(1);}if (recv(temp_sock_descriptor, buf, 16384, 0) == -1) { //接收套接口數據并存入bufperror("call to recv");exit(1);// 如果調用recv失敗,則exit(1)異常退出.exit(1)值返回給操作系統。}printf("received from client:%s\n", buf);// for this server example, we just convert the// characters to upper case:len = strlen(buf);for (i=0; i<len; i++) buf[i] = toupper(buf[i]); //將buf[]中的數據做大寫轉換,對收到的數據進行處理。if (send(temp_sock_descriptor, buf, len, 0) == -1) {//將處理后的數據發送perror("call to send");exit(1);// 如果調用recv失敗,則exit(1)異常退出.exit(1)值返回給操作系統。}close(temp_sock_descriptor);}
}
??????
??????? 基于 TCP 的套接字編程的所有客戶端和服務器端都是從調用socket 開始,它返回一個套接字描述符。客戶端隨后調用connect 函數,服務器端則調用 bind、listen 和accept 函數。套接字通常使用標準的close 函數關閉,但是也可以使用 shutdown 函數關閉套接字。下面針對套接字編程實現過程中所調用的函數進程分析。以下是基于 TCP 套接字編程的流程圖:
??????? socket 函數
??????? 套接字是通信端點的抽象,實現端對端之間的通信。與應用程序要使用文件描述符訪問文件一樣,訪問套接字需要套接字描述符。任何套接字編程都必須調用socket 函數獲得套接字描述符,這樣才能對套接字進行操作。以下是該函數的描述:
/* 套接字 */ /* * 函數功能:創建套接字描述符; * 返回值:若成功則返回套接字非負描述符,若出錯返回-1; * 函數原型: */ #include <sys/socket.h> int socket(int family, int type, int protocol); /* * 說明: * socket類似與open對普通文件操作一樣,都是返回描述符,后續的操作都是基于該描述符; * family 表示套接字的通信域,不同的取值決定了socket的地址類型,其一般取值如下: * (1)AF_INET IPv4因特網域 * (2)AF_INET6 IPv6因特網域 * (3)AF_UNIX Unix域 * (4)AF_ROUTE 路由套接字 * (5)AF_KEY 密鑰套接字 * (6)AF_UNSPEC 未指定 * * type確定socket的類型,常用類型如下: * (1)SOCK_STREAM 有序、可靠、雙向的面向連接字節流套接字 * (2)SOCK_DGRAM 長度固定的、無連接的不可靠數據報套接字 * (3)SOCK_RAW 原始套接字 * (4)SOCK_SEQPACKET 長度固定、有序、可靠的面向連接的有序分組套接字 * * protocol指定協議,常用取值如下: * (1)0 選擇type類型對應的默認協議 * (2)IPPROTO_TCP TCP傳輸協議 * (3)IPPROTO_UDP UDP傳輸協議 * (4)IPPROTO_SCTP SCTP傳輸協議 * (5)IPPROTO_TIPC TIPC傳輸協議 * */
????? ?? connect 函數
??????? 在處理面向連接的網絡服務時,例如 TCP ,交換數據之前必須在請求的進程套接字和提供服務的進程套接字之間建立連接。TCP 客戶端可以調用函數connect 來建立與 TCP 服務器端的一個連接。該函數的描述如下:
/* * 函數功能:建立連接,即客戶端使用該函數來建立與服務器的連接; * 返回值:若成功則返回0,出錯則返回-1; * 函數原型: */ #include <sys/socket.h> int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen); /* * 說明: * sockfd是系統調用的套接字描述符,即由socket函數返回的套接字描述符; * servaddr是目的套接字的地址,該套接字地址結構必須包含目的IP地址和目的端口號,即想與之通信的服務器地址; * addrlen是目的套接字地址的大小; * * 如果sockfd沒有綁定到一個地址,connect會給調用者綁定一個默認地址,即內核會確定源IP地址,并選擇一個臨時端口號作為源端口號; */
??????? TCP 客戶端在調用函數 connect 前不必非得調用 bind 函數,因為內核會確定源 IP 地址,并選擇一個臨時端口作為源端口號。若 TCP 套接字調用connect 函數將建立 TCP 連接(執行三次握手),而且僅在連接建立成功或出錯時才返回,其中出錯返回可能有以下幾種情況:
??????? 若 TCP 客戶端沒有收到 SYN 報文段的響應,則返回 ETIMEOUT 錯誤;
??????? 若客戶端的 SYN 報文段的響應是 RST (表示復位),則表明該服務器主機在我們指定的端口上沒有進程在等待與之連接。只是一種硬錯誤,客戶端一接收到 RST 就立即返回ECONNERFUSED 錯誤;
??????? RST 是 TCP 在發生錯誤時發送的一種 TCP 報文段。產生 RST 的三個條件時:
??????? 目的地為某端口的 SYN 到達,然而該端口上沒有正在監聽的服務器;
??????? TCP 想取消一個已有連接;
??????? TCP 接收到一個不存在的連接上的報文段;
??????? 若客戶端發出的 SYN 在中某個路由器上引發一個目的地不可達的 ICMP 錯誤,這是一個軟錯誤。客戶端主機內核保存該消息,并在一定的時間間隔繼續發送 SYN (即重發)。在某規定的時間后仍未收到響應,則把保存的消息(即 ICMP 錯誤)作為EHOSTUNREACH 或ENETUNREACH 錯誤返回給進行。
??????? bind 函數
????? 調用函數 socket 創建套接字描述符時,該套接字描述符是存儲在它的協議族空間中,沒有具體的地址,要使它與一個地址相關聯,可以調用函數bind 使其與地址綁定。客戶端的套接字關聯的地址一般可由系統默認分配,因此不需要指定具體的地址。若要為服務器端套接字綁定地址,可以通過調用函數 bind 將套接字綁定到一個地址。下面是該函數的描述:
/* 套接字的基本操作 */ /* * 函數功能:將協議地址綁定到一個套接字;其中協議地址包含IP地址和端口號; * 返回值:若成功則返回0,若出錯則返回-1; * 函數原型: */ #include <sys/socket.h> int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); /* * 說明: * sockfd 為套接字描述符; * addr是一個指向特定協議地址結構的指針; * addrlen是地址結構的長度; */
??????? 對于 TCP 協議,調用 bind 函數可以指定一個端口號,或指定一個 IP 地址,也可以兩者都指定,還可以都不指定。若 TCP 客戶端或服務器端不調用bind 函數綁定一個端口號,當調用connect 或 listen 函數時,內核會為相應的套接字選擇一個臨時端口號。一般 TCP 客戶端使用內核為其選擇一個臨時的端口號,而服務器端通過調用bind 函數將端口號與相應的套接字綁定。進程可以把一個特定的 IP 地址捆綁到它的套接字上,但是這個 IP 地址必須屬于其所在主機的網絡接口之一。對于 TCP 客戶端,這就為在套接字上發送的 IP 數據報指派了源 IP 地址。對于 TCP 服務器端,這就限定該套接字只接收那些目的地為這個 IP 地址的客戶端連接。TCP 客戶端一般不把 IP 地址捆綁到它的套接字上。當連接套接字時,內核將根據所用外出網絡接口來選擇源 IP 地址,而所用外出接口則取決于到達服務器端所需的路徑。若 TCP 服務器端沒有把 IP 地址捆綁到它的套接字上,內核就把客戶端發送的 SYN 的目的 IP 地址作為服務器端的源 IP 地址。
??????? 在地址使用方面有下面一些限制:
??????? 在進程所運行的機器上,指定的地址必須有效,不能指定其他機器的地址;
??????? 地址必須和創建套接字時的地址族所支持的格式相匹配;
??????? 端口號必須不小于1024,除非該進程具有相應的特權(超級用戶);
??????? 一般只有套接字端點能夠與地址綁定,盡管有些協議允許多重綁定??????? listen 函數
??????? 在編寫服務器程序時需要使用監聽函數 listen 。服務器進程不知道要與誰連接,因此,它不會主動地要求與某個進程連接,只是一直監聽是否有其他客戶進程與之連接,然后響應該連接請求,并對它做出處理,一個服務進程可以同時處理多個客戶進程的連接。listen 函數描述如下:
/* * 函數功能:接收連接請求; * 函數原型: */ #include <sys/socket.h> int listen(int sockfd, int backlog);//若成功則返回0,若出錯則返回-1; /* * sockfd是套接字描述符; * backlog是該進程所要入隊請求的最大請求數量; */
??????? listen 函數僅由 TCP 服務器調用,它有以下兩種作用:
??????? 當 socket 函數創建一個套接字時,若它被假設為一個主動套接字,即它是一個將調用connect 發起連接的客戶端套接字。listen 函數把一個未連接的套接字轉換成一個被動套接字,指示內核應該接受指向該套接字的連接請求;
??????? listen 函數的第二個參數規定內核應該為相應套接字排隊的最大連接個數;
??????? listen 函數一般應該在調用socket 和bind 這兩個函數之后,并在調用 accept 函數之前調用。 內核為任何一個給定監聽套接字維護兩個隊列:
??????? 未完成連接隊列,每個這樣的 SYN 報文段對應其中一項:已由某個客戶端發出并到達服務器,而服務器正在等待完成相應的 TCP 三次握手過程。這些套接字處于 SYN_REVD 狀態;
??????? 已完成連接隊列,每個已完成 TCP 三次握手過程的客戶端對應其中一項。這些套接字處于 ESTABLISHED 狀態;
??????? accept 函數
??????? accept 函數由 TCP 服務器調用,用于從已完成連接隊列隊頭返回下一個已完成連接。如果已完成連接隊列為空,那么進程被投入睡眠。該函數的返回值是一個新的套接字描述符,返回值是表示已連接的套接字描述符,而第一個參數是服務器監聽套接字描述符。一個服務器通常僅僅創建一個監聽套接字,它在該服務器的生命周期內一直存在。內核為每個由服務器進程接受的客戶連接創建一個已連接套接字(表示 TCP 三次握手已完成),當服務器完成對某個給定客戶的服務時,相應的已連接套接字就會被關閉。該函數描述如下:
/* 函數功能:從已完成連接隊列隊頭返回下一個已完成連接;若已完成連接隊列為空,則進程進入睡眠; * 函數原型: */ int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);//返回值:若成功返回套接字描述符,出錯返回-1; /* * 說明: * 參數 cliaddr 和 addrlen 用來返回已連接的對端(客戶端)的協議地址; * * 該函數返回套接字描述符,該描述符連接到調用connect函數的客戶端; * 這個新的套接字描述符和原始的套接字描述符sockfd具有相同的套接字類型和地址族,而傳給accept函數的套接字描述符sockfd沒有關聯到這個鏈接, * 而是繼續保持可用狀態并接受其他連接請求; * 若不關心客戶端協議地址,可將cliaddr和addrlen參數設置為NULL,否則,在調用accept之前,應將參數cliaddr設為足夠大的緩沖區來存放地址, * 并且將addrlen設為指向代表這個緩沖區大小的整數指針; * accept函數返回時,會在緩沖區填充客戶端的地址并更新addrlen所指向的整數為該地址的實際大小; * * 若沒有連接請求等待處理,accept會阻塞直到一個請求到來;
??????? fork 和 exec 函數
/* 函數功能:創建子進程; * 返回值: * (1)在子進程中,返回0; * (2)在父進程中,返回新創建子進程的進程ID; * (3)若出錯,則范回-1; * 函數原型: */ #include <unistd.h> pid_t fork(void); /* 說明: * 該函數調用一次若成功則返回兩個值: * 在調用進程(即父進程)中,返回新創建進程(即子進程)的進程ID; * 在子進程返回值是0; * 因此,可以根據返回值判斷進程是子進程還是父進程; */ /* exec 序列函數 */ /* * 函數功能:把當前進程替換為一個新的進程,新進程與原進程ID相同; * 返回值:若出錯則返回-1,若成功則不返回; * 函數原型: */ #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 個函數的區別如下: * (1)待執行的程序文件是 文件名 還是由 路徑名 指定; * (2)新程序的參數是 一一列出 還是由一個 指針數組 來引用; * (3)把調用進程的環境傳遞給新程序 還是 給新程序指定新的環境; */
??????? exec 6個函數在函數名和使用語法的規則上都有細微的區別,下面就從可執行文件查找方式、參數傳遞方式及環境變量這幾個方面進行比較。
??????? 查找方式:前4個函數的查找方式都是完整的文件目錄路徑 pathname ,而最后兩個函數(也就是以p結尾的兩個函數)可以只給出文件名 filename,系統就會自動按照環境變量 “$PATH” 所指定的路徑進行查找。
??????? 參數傳遞方式:exec 序列函數的參數傳遞有兩種方式:一種是逐個列舉的方式,而另一種則是將所有參數整體構造指針數組傳遞。在這里是以函數名的第5位字母來區分的,字母為 “l”(list)的表示逐個列舉參數的方式,其語法為 const char *arg;字母為 “v”(vertor)的表示將所有參數整體構造指針數組傳遞,其語法為 char *const argv[]。讀者可以觀察 execl()、execle()、execlp() 的語法與 execv()、execve()、execvp() 的區別。這里的參數實際上就是用戶在使用這個可執行文件時所需的全部命令選項字符串(包括該可執行程序命令本身)。要注意的是,這些參數必須以NULL結束。
??????? 環境變量:exec 序列函數可以默認系統的環境變量,也可以傳入指定的環境變量。這里以 “e”(environment)結尾的兩個函數 execle() 和 execve() 就可以在 envp[] 中指定當前進程所使用的環境變量。
表 1 exec 序列函數的總結 前4位 統一為:exec 第5位 l:參數傳遞為逐個列舉方式 execl、execle、execlp v:參數傳遞為構造指針數組方式 execv、execve、execvp 第6位 e:可傳遞新進程環境變量 execle、execve p:可執行文件查找方式為文件名 execlp、execvp
??????? 其關系如下圖:
??????? 并發服務器
??????? 當要求一個服務器同時為多個客戶服務時,需要并發服務器。TCP 并發服務器,它們為每個待處理的客戶端連接調用 fork 函數派生一個子進程。當一個連接建立時,accept 返回,服務器接著調用 fork 函數,然后由子進程服務客戶端,父進程則等待另一個連接,此時,父進程必須關閉已連接套接字。
??????? close 和 shutdown 函數
??????? 當要關閉套接字時,可使用 close 和 shutdown 函數,其描述如下:
/* 函數功能:關閉套接字,若是在 TCP 協議中,并終止 TCP 連接; * 返回值:若成功則返回0,若出錯則返回-1; * 函數原型: */
#include <unistd.h>
int close(int sockfd); /* * 函數功能:關閉套接字上的輸入或輸出; * 返回值:若成功則返回0,若出錯返回-1; * 函數原型: */
#include <sys/socket.h>
int shutdown(int sockfd, int how);
/* * 說明: * sockfd表示待操作的套接字描述符; * how表示具體操作,取值如下: * (1)SHUT_RD 關閉讀端,即不能接收數據 * (2)SHUT_WR 關閉寫端,即不能發送數據 * (3)SHUT_RDWR 關閉讀、寫端,即不能發送和接收數據 * */
??????? getsockname 和 getpeername 函數
??????? 為了獲取已綁定到套接字的地址,我們可以調用函數 getsockname 來實現:
/* * 函數功能:獲取已綁定到一個套接字的地址; * 返回值:若成功則返回0,若出錯則返回-1; * 函數原型: */ #include <sys/socket.h> int getsockname(int sockfd, struct sockaddr *addr, socklen_t *alenp); /* * 說明: * 調用該函數之前,設置alenp為一個指向整數的指針,該整數指定緩沖區sockaddr的大小; * 返回時,該整數會被設置成返回地址的大小,如果該地址和提供的緩沖區長度不匹配,則將其截斷而不報錯; */ /* * 函數功能:獲取套接字對方連接的地址; * 返回值:若成功則返回0,若出錯則返回-1; * 函數原型: */ #include <sys/socket.h> int getpeername(int sockfd, struct sockaddr *addr, socklen_t *alenp); /* * 說明: * 該函數除了返回對方的地址之外,其他功能和getsockname一樣; */