基本TCP套接字編程

基本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的三次握手,而且僅在連接建立成功或出錯時才返回(默認阻塞)。出錯的情況有:

  • TCP客戶沒有收到SYN分節的響應,則返回ETIMEOUT錯誤
  • 若對客戶的響應的RST,則表明服務主機在我們指定的端口上沒有進程等待與之連接(服務器可能沒在運行)。客戶一收到RST就馬上返回ECONNREFUSED錯誤。
  • 若客戶發出的SYN在某個路由器上引發了目的地不可達的ICMP錯誤。若在規定時間內仍未收到響應,則返回EHOSTUNREACH或ENETUNREACH錯誤。

現在的大多數主機一般是小端序,網絡字節序是大端序。

listen函數

當用socket函數創建一個套接字時,它被假設爲一個主動套接字,listen函數將其轉換爲一個被動套接字,指示內核應接受指向該套接字的連接請求。調用listen函數導致套接從CLOSED狀態轉換到LISTEN狀態。

#include <sys/socket.h>
int listen(int sockfd, int backlog);
返回值:成功的時候返回0;出錯的時候返回-1,並且設置errno.

backlog表示該套接字排隊的最大連接個數。被定義爲未完成連接隊列和已完成連接隊列總和的最大值。

  • 未完成連接隊列。客戶發送的SYN已到達服務端,但服務器正在等待完成相應的TCP三次握手過程。這些套接字處於SYN_RCVD狀態。
  • 已完成連接隊列。每個已完成TCP三次握手的套接字對應其中的一項。這些套接字處於ESTABLISHED狀態。

在這裏插入圖片描述

  • 如果三次握手正常完成,該套接字從未完成連接隊列移到已完成隊列的隊尾。當進程調用accept時,已完成連接隊列中的隊頭項將返回給進程,或者如果說隊列爲空,那麼進程將被投入睡眠,直到TCP在該隊列中放入一項才喚醒它。
  • 當一個客戶的SYN到達時,若隊列是滿的,TCP就忽略該SYN,而不會發送RST,因爲客戶在規定時間內會重發SYN。
  • 在三次握手完成之後,但在服務器調用accept之前到達的數據應由服務器TCP排除,最大數據量爲相應已連接套接字的接收緩衝區大小。

accept函數

#include <sys/socket.h>
int accept(int sockfd, struct sockaddr* cliaddr, socklen_t* addrlen);
返回值:若成功則爲非負描述符,若出錯則爲-1

由TCP服務器調用,用於從已完成連接隊列隊頭返回下一個已完成連接。若隊列爲空,則進程被投入睡眠(默認阻塞)。
第一個參數爲監聽套接字,返回值爲已連接套接字。

網絡編程可能會遇到的三種情況:

  • fork子進程時,必須捕獲SIGCHLD信號。(以免產生殭屍進程)
  • 當捕獲信號時,必須處理被中斷的系統調用。

當阻塞於某個慢系統調用的一個進程捕獲某個信號且相應信號處理函數返回時,該系統調用可能返回一個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函數。

客戶套接字的處理

  • 如果對方發送數據,那麼套接字可讀,並且read返回一個大於0的值(讀入字節數);
  • 如果對方發送了FIN(對端進程終止),那麼該套接字變爲可讀,並且read返回0(EOF);
  • 如果對方發送RST(對端主機崩潰並重啓),那麼該套接字變爲可讀,並且read返回-1,errno中含有確切錯誤碼;

服務器進程終止

  • 服務器子進程終止,作爲進程終止處理的前半部分,子進程中的所有打開着的描述符都被關閉。這就導致向客戶發送了一個FIN,而客戶則響應一個ACK。這就是TCP連接終止工作的前半部分。服務器子進程向父進程發送SIGCHLD信號。
  • 客戶端中使用select監控套接字是否可讀。此時服務端發送的FIN,使套接字變爲可讀,read返回0,客戶進程返回信息(「str_cli:server terminated prematurely」)並終止,它所有打開的描述符都被關閉。
  • 若客戶端沒有使用諸如select的函數來監控套接字是否可讀,在收到服務端的FIN後,客戶端向socket發送數據,TCP服務端收到來自客戶的數據時,既然先前打開那個套接字的服務器子進程已經終止,於是響應一個RST。當一個進程向某個已收到RST的套接字執行寫操作時,內核向該進程發送SIGPIPE信號,該信號默認是終止進程。此時寫操作返回EPIPE錯誤,應採取措施終止進程。

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));
}
} }

服務器主機崩潰

  • 當服務器主機崩潰時,已有的網絡連接上不發出任何東西
  • 若客戶端發送一個數據包,會發現客戶TCP會持續重傳數據分節,試圖從服務器上接收一個ACK。若在規定時間內(約9分鐘),服務器未重啓,或者是服務器網絡上不可達,此時客戶端返回的錯誤會是ETIMEOUT或EHOSTUNREACH(ENETUNREACH)。如果我們想盡快檢測出這種錯誤,我們可以爲諸如recvfrom的函數設置超時。

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信號(不可被捕獲)。這麼做留給所有運行的進程一小段時間來清除和終止。這時發生的情況就與服務器進程終止的情況是一樣的了。