基本TCP套接字編程
connect函數
#include<sys/socket.h> int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen) 返回:若成功則爲0,若出錯則爲-1
客戶端調用connect前不必非得調用bind函數,因爲內核會確定源IP地址,並選擇一個臨時端口作爲源端口。
如果是TCP套接字,調用connect會激發TCP的三次握手,而且僅在連接建立成功或出錯時才返回(默認阻塞)。出錯的情況有:
現在的大多數主機一般是小端序,網絡字節序是大端序。
listen函數
當用socket函數創建一個套接字時,它被假設爲一個主動套接字,listen函數將其轉換爲一個被動套接字,指示內核應接受指向該套接字的連接請求。調用listen函數導致套接從CLOSED狀態轉換到LISTEN狀態。
#include <sys/socket.h> int listen(int sockfd, int backlog); 返回值:成功的時候返回0;出錯的時候返回-1,並且設置errno.
backlog表示該套接字排隊的最大連接個數。被定義爲未完成連接隊列和已完成連接隊列總和的最大值。
accept函數
#include <sys/socket.h> int accept(int sockfd, struct sockaddr* cliaddr, socklen_t* addrlen); 返回值:若成功則爲非負描述符,若出錯則爲-1
由TCP服務器調用,用於從已完成連接隊列隊頭返回下一個已完成連接。若隊列爲空,則進程被投入睡眠(默認阻塞)。
第一個參數爲監聽套接字,返回值爲已連接套接字。
網絡編程可能會遇到的三種情況:
當阻塞於某個慢系統調用的一個進程捕獲某個信號且相應信號處理函數返回時,該系統調用可能返回一個EINTR錯誤,表示系統調用被中斷。並非所有的被中斷系統調用都可以自動重啓。因此我們需要自己重啓被中斷的系統調用。
for (;;){ clilen = sizeof(cliaddr); if ((connfd = accept(listenfd, (SA*)&cliaddr, &clilen)) < 0) { if (errno = EINTR) continue; else err_sys("accept error"); } }
對於accept以及諸如read、write、select和open之類的函數來說,這是合適的。但connect函數不能重啓。如果connect返回EINTR,我們就能再次調用它,否則將返回一個錯誤。當connect被一個捕捉的信號中斷而且不自動重啓時,這時已發起的三次握手會繼續進行,但我們必須調用select來等待連接的完成。
SIGCHLD的信號處理函數應使用waitpid函數以免留下殭屍進程。(非阻塞waidpid)
while ((pid = waitpid(-1, &stat, WNOHANG)) > 0) {}
accept返回前連接中止
三次握手完成從而連接建立之後,客戶TCP卻發送了一個RST。在服務端看來,就在該連接已由TCP排隊,等着服務器進程調用accept時RST到達。稍後,服務端調用accept。大多數實現返回一個錯誤給服務器進程,作爲accept的返回結果,POSIX指出返回的errno值必須是ECONNABORTED,這時服務器會忽略它,再次調用accpet來處理其它連接。
close函數
每個文件或套接字都有一個引用計數,表示當前打開着的引用該文件或套接字的描述符的個數。close函數只是將相應的引用計數減1。因此,若已連接的套接字是在子進程中處理的,父進程對每個由accept返回的已連接套接字都應調用close,否則將耗盡文件描述符。
若我們確實想在某個TCP連接上發送一個FIN,我們應使用shutdown函數。
客戶套接字的處理:
服務器進程終止
void str_cli(FILE fp, int sockfd) {
int maxfdp1;
fd_set rset;
char sendline[MAXLINE], recvline[MAXLINE];
FD_ZERO(&rset);
for ( ; ; ) {
FD_SET(fileno(fp), &rset);
FD_SET(sockfd, &rset);
maxfdp1 = max(fileno(fp), sockfd) + 1;
Select(maxfdp1, &rset, NULL, NULL, NULL);
if (FD_ISSET(sockfd, &rset)) { / socket is readable /
if (Readline(sockfd, recvline, MAXLINE) == 0)
err_quit(「str_cli: server terminated prematurely」);
Fputs(recvline, stdout);
}
if (FD_ISSET(fileno(fp), &rset)) { / input is readable /
if (Fgets(sendline, MAXLINE, fp) == NULL)
return; / all done */
Writen(sockfd, sendline, strlen(sendline));
}
} }
服務器主機崩潰
int readable_timeo(int fd, int sec) {
fd_set rset;
struct timeval tv;
FD_ZERO(&rset);
FD_SET(fd, &rset);
tv.tv_sec = sec;
tv.tv_usec = 0;
return(select(fd+1, &rset, NULL, NULL, &tv));
/* 4> 0 if descriptor is readable */ }void dg_cli(FILE *fp, int sockfd, const SA pservaddr, socklen_t
servlen) {
int n;
char sendline[MAXLINE], recvline[MAXLINE + 1];
while (Fgets(sendline, MAXLINE, fp) != NULL) {
Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);
if (Readable_timeo(sockfd, 5) == 0) {
fprintf(stderr, 「socket timeout\n」);
} else {
n = Recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);
recvline[n] = 0; / null terminate */
Fputs(recvline, stdout);
}
} }
上述情況只有客戶端向服務器主機發送數據時才能檢測出它已崩潰,如果我們希望不主動發送數據也能檢測出服務器主機的崩潰,需要設置SO_KEEPALIVE選項。
服務器主機崩潰後重啓
此時在客戶TCP重傳數據分節時,若服務器主機崩潰後重啓了,服務器的TCP已經丟失了崩潰前的所有連接信息,因此服務器TCP對於所收到的來自客戶的數據分節響應一個RST。當客戶端收到該RST時,返回ECONNRESET錯誤。
服務器主機關機
Unix系統關機時,init進程通常先給所有進程發送SIGTERM信號(可被捕獲,默認終止進程),然後等待一段固定的時間(5~20秒),然後給所有仍在運行的進程發送SIGKILL信號(不可被捕獲)。這麼做留給所有運行的進程一小段時間來清除和終止。這時發生的情況就與服務器進程終止的情況是一樣的了。