套接字接口
套接字接口(socket interface)是一組函數,他們和Unix I/O函數結合起來,用于創建網絡應用。大多數現代系統上都實現套接字接口,包括所有的Unix變種,Windows和Macintosh系統。
從Unix內核的角度來看,一個套接字就是通信的一個端點。從Unix程序的角度來看,套接字就是一個有相應描述符的打開文件。
因特網的套接字地址存放在如下代碼所示的類型為sockaddr_in的16字節結構中。對于因特網應用,sin_family成員是AF_INEF,sin_port成員是一個16位的端口號,而sin_addr成員就是一個32位的IP地址。IP地址和端口號總是以網絡字節順序(大端法)存放的。
connect,bind和accept函數要求一個指向與協議相關的套接字地址結構的指針。套接字接口的設計者面臨的問題是,如何定義這些函數,使之能接受各種類型的套接字地址結構。現在,我們可以使用通用的void*指針,那時在C中并不存在這種類型的指針。解決辦法是定義套接字函數要求一個指向通用sockaddr結構的指針,然后要求應用程序將與協議特定的結構的指針強制轉換成這個通用結構。
Socket函數
在我們的代碼中總是帶這樣的參數來調用socket函數:
clientfd = Socket(AF_INET, SOCK_STREAM, 0);
其中,AF_INET表明我們正在使用因特網,而SOCK_STREAM表示這個套接字是因特網連接的一個端點。socket返回的clientfd描述符僅是部分打開的,還不能用于讀寫。如何完成打開套接字的工作,取決與我們是客戶端還是服務器端。
connect函數
客戶端通過調用connect函數來建立和服務器的連接。
connect函數試圖與套接字地址為serv_addr的服務器建立一個因特網連接,其中addrlen是sizeof(sockaddr_in)。connect函數會阻塞,一直到連接成功建立或發生錯誤。如果成功,sockfd描述符現在就準備好了可以讀寫了,并且得到的連接是由套接字對
(x:y, serv_addr.sin_addr:serv_addr.sin_port)
刻畫的,其中x表示客戶端IP地址,而y表示臨時端口,他唯一的確定了客戶端主機上客戶端進程。
open_clientfd函數
我們發現將socket和connect函數包裝成一個叫做open_clientfd的輔助函數是很方便的,客戶端可以用它來和服務器建立連接。
open_clientfd函數和運行在主機hostname上的服務器建立一個連接,并在知名端口port上監聽連接請求。他返回一個打開的套接字描述符,該描述符準備好了,可以用UNIX I/O函數做輸入和輸出。
/* DNS host entry structure */ struct hostent{char *h_name; /* Official domain name of host */char **h_aliases; /* Null-terminated array of domain names */int h_addrtype; /* Host address type (AF_INET)*/int h_length; /* Length of an address, in bytes*/char **h_addr_list; /* Null-terminated array of in_addr structs */ } /* Internet-style socket address structure */ struct sockaddr_in {unsigned short sin_family; /* Address family (always AF_INET) */unsigned short sin_port; /* Port number in network byte order */struct in_addr sin_addr;/* IP address in network byte order */unsigned char sin_zero[8]; /* Pad to sizeof(struct sockaddr) */ ] int open_clientfd(char *hostname, int port) {int clientfd;struct hostent *hp;struct sockaddr_in serveraddr;if ((clientfd = socket(AF_INEF,SOCK_STREAM, 0)) < 0)return -1;/* Fill in the server's IP address and port*/if ((hp = gethostbyname(hostname)) == Null)return -2;bzero((char *) &serveraddr, sizeof(serveraddr));serveraddr.sin_family = AF_INET;bcopy((char *)hp->h_addr_list[0], (char *)&serveraddr.sin_addr.s_addr, hp->h_length);serveraddr.sin_port = htons(port);if(connect(clientfd, (SA *) &serveraddr, sizeof(serveraddr)) < 0)return -1;return clientfd; ]在創建了套接字描述符后,我們檢索服務器DNS主機條目,并拷貝主機條目中的第一個IP地址(已經是按照網絡字節順序了)到服務器的套接字地址結構。在用按照網絡字節順序的服務器的知名端口號初始化套接字地址結構之后,我們發起了一個到服務器的連接請求。當connect函數返回時,我們返回套接字描述符給客戶端,客戶端就可以立即開始用Unix I/O和服務器通信了。
bind函數
剩下的套接字函數bind,listen和accept被服務器用來和客戶端建立連接。
bind函數告訴內核將my_addr中的服務器套接字地址和套接字描述符sockfd聯系起來。參數addrlen就是sizeof(sockaddr_in).
listen函數
客戶端是發起連接請求的主動實體。服務器是等待來自客戶端的連接請求的被動實體。默認情況下,內核會認為socket函數創建的描述符對應于主動套接字(active socket),它存在于一個連接的客戶端。服務器調用listen函數告訴內核,描述符是被服務器而不是客戶端使用的。
listen函數將sockfd從一個主動套接字轉化為一個監聽套接字(listening socket),該套接字可以接受來自客戶端的連接請求。backlog參數暗示了內核在開始拒絕連接請求之前,應該放入隊列中等待的未完成連接請求的數量。backlog參數的確切含義要求對TCP/IP協議的理解,這超出了我們的討論范圍。通常我們會把它設置成一個較大的值,比如1024.
open_listenfd函數
我們發現將socket,bind和listen函數結合成一個叫做open_listenfd的輔助函數是很有幫助的,服務器可以用它來創建一個監聽描述符。
open_listenfd函數打開和返回一個監聽描述符,這個描述符準備好在知名端口port上接受連接請求。
int open_listenfd(int port){int listenfd, optval = 1;struct sockaddr_in serveraddr;if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)return - 1;if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (const void *)&optval, sizeof(int)) < 0)return -1;bzero((char *) &serveraddr, sizeof(serveraddr));serveraddr.sin_family = AF_INET;serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);serveraddr.sin_port = htons((unsigned short)port);if (bind(listenfd, (SA *)&serveraddr, sizeof(serveraddr)) < 0)return -1;if (listen(listenfd,LISTENQ) < 0)return - 1;return listenfd; }accept函數
服務器通過調用accept函數來等待來自客戶端的連接請求:
accept函數等待來自客戶端的連接請求到達監聽描述符listendfd,然后再addr中填寫客戶端的套接字地址,并返回一個已連接描述符(connected descriptor),這個描述符可被用來利用UNIX I/O函數與客戶端通信。
監聽描述符和已連接描述符之間的區別使很多人感到迷惑。監聽描述符是作為客戶端連接請求的一個端點。典型的,他被創建一次,并存在于服務器的整個生命周期。已連接描述符是客戶端和服務器之間已經建立起來了的連接的一個端點。服務器每次接受連接請求時都會創建一次,它只存在于服務器為一個客戶端服務的過程中。
第一步中,服務器調用accept,等待連接請求到達監聽描述符,具體的我們設定為描述符3。在第二步中,客戶端調用connect函數,發送一個連接請求到listenfd。第三步,accept函數打開一個新的已連接描述符connfd(我們假設是描述符4),在clientfd和connfd之間建立連接,并且隨后返回connfd給應用程序。客戶端從connect返回,在這一點以后,客戶端和服務器就可以分別通過讀和寫clientfd和connfd來回傳送數據了。
echo客戶端和服務器端的示例
上述代碼展示了一個echo客戶端的代碼,在和服務器建立接連后,客戶端進入一個循環,反復從標準輸入讀取文本行,發送文本行給服務器,從服務區讀取回送的行,并輸出結果到標準輸出。當fgets在標準輸入上遇到EOF時,或者因為用戶在鍵盤上鍵入ctrl-d,或者因為在一個重定向的輸入文件中用盡了所有的文本行時,循環就終止了。
循環終止之后,客戶端關閉描述符。這會導致發送一個EOF通知服務器,當服務器從他的rio_readlineb函數收到一個為零的返回碼時,就會檢測到這個結果。在關閉他的描述符后,客戶端就終止了。既然客戶端內核在一個進程終止時會自動關閉所有打開的描述符,顯示的Close就沒有必要了。不過,顯示的關閉已經打開的任何描述符是一個良好的編程習慣。
總結
- 上一篇: GhostBSD安装Fcitx中文输入法
- 下一篇: 中国非处方药(OTC)市场营销策略及十四