先描述一下整體的流程及思路:
客戶端從標准輸入讀取一行文本,發送給服務器,服務器收到文本后,將文本直接返回給客戶端,即回顯。整體采用TCP協議完成。
客戶端大致代碼:
socket,connect函數略去
char sendline[1024],recvline[1024];
while( fgets(sendline, 1024, stdin) != NULL){ //從標准輸入讀取
writen(sockfd,sendline,strlen(sendline)); //發送給服務器,Sockfd就是與服務器聯通的Socket
if(readline(sockfd, recvline, 1024) == 0) //從服務器接收
err_quit("Server terminated!");
fputs(recvline, stdout); //顯示在屏幕上
}
服務器端大致代碼:
socket,bind,listen函數略去
while(1){
socklen_t clilen = sizeof(cliaddr);
connfd = Accept(listenfd, (const SocketAddr_in *)&cliaddr, &clilen);
if((childpid = fork()) == 0){ //服務器端采用多進程來處理每一個客戶連接
Close(listenfd); //對於子進程來說,Listenfd無用,可以直接關閉
str_server(connfd); //處理客戶請求,詳見下面
exit(0);
}
close(connfd);
}
void str_server(int sockfd){
ssize_t n;
char buf[1024];
again:
while( (n = read(sockfd, buf, 1024)) > 0)
writen(sockfd, buf, n);
if( n < 0 && errno == EINTR) //之后會說明為什么在這里要處理EINTR
goto again;
else if( n < 0)
err_sys("read error");
}
一、 服務器端用於與客戶端通信的子進程終止
可以啟動客戶/服務器,然后使用Kill命令殺死服務器子進程。這時根據TCP連接中止的四步(不明白的先百度一下,稍候的博客中我會對TCP四步斷開連接進行詳細講述),子進程所有打開的描述符都被關閉,這就導致向客戶發送一個FIN,同時客戶TCP則響應一個ACK。對於服務器端,SIGCHLD信號會發送到服務器父進程中,由於代碼中並未寫處理SIGCHLD的功能,所以默認被忽略掉。
這里要注意,客戶端並未發生任何特殊之事,它並沒有立刻得到服務器子進程崩潰的消息,而是阻塞在fgets(sendline, 1024, stdin)系統調用上,這時我們在客戶端繼續鍵入文本並發送給服務器,由於處理與客戶通信的子進程已經關閉,所以會響應一個RST。但是客戶卻看不到這個RST,因為它在調用writen后立即調用readline,由於之前已經收到FIN,所以readline立即返回0,顯示server terminate.也就是說,客戶端並未獲得RST所告知的錯誤信息。另外,大家注意到,在服務器子進程終止時,客戶端並未立刻得到通知,因為它阻塞在Fgets調用上,雖然客戶端已經收到FIN。
如果服務器子進程終止,客戶端不通過發送數據也能知道的話,就必須不能讓自己阻塞在Fgets上,這里需要用到I/O多路復用的技術,即select和poll函數。
二、通信過程中網絡斷開,或者說服務器主機崩潰
可以在通信過程中拔下網線。。。。和(一)中所講述的一樣,當客戶端發送給服務器數據時,由於網絡已經斷開,因此writen(其實與send相似)發給服務器數據后,客戶TCP會持續重傳數據分節,以試圖從服務器獲得一個ACK,但是,源自Berkeley的實現重傳大約等待9分鍾才放棄重傳,9分鍾后才給客戶進程返回一個錯誤,多數是ETIMEDOUT,即超時錯誤。而在實際應用中,9分鍾顯然太長了,因此需要對readline調用設置一個超時。
但問題仍然存在,因為我們仍然向服務器發送了數據后才檢測出服務器主機崩潰或者網絡斷開了,如果在網絡斷開時客戶就想知道,需要使用SO_KEEPALIVE套接字選項,即一直保持聯通,當斷開網絡時,他會導致客戶端直接返回錯誤信號,從而捕獲它並進行處理。
三、通信過程中服務器主機崩潰,但之后重啟
這與(二)所不同的地方是:當服務器重啟后,網絡仍然是聯通的,只是與客戶通信的進程消失了。這樣以來,當重啟后的服務器收到客戶消息后,由於之前的通信進程已經終止,所以會響應一個RST給客戶端,並導致客戶端readline返回ECONNREST錯誤。假如一個應用對於檢測服務器是否崩潰很重要,即和(二)所說一樣,也就需要SO_KEEPALIVE套接字選項,另外一種技術是采用客戶/服務器心搏函數,這個我不太了解。
四、通信過程中服務器關機
系統關機時,init進程通常先給所有進程發送SIGTERM信號,各進程需要捕獲該信號並做好善后工作。等待一段時間后(5~20秒,這也就是為什么當我們關機時顯示正在關機中的原因),再發送SIGKILL信號給所有進程。這時服務器進程終止。同(一)中所述一樣,客戶端也需要再發數據才能得到該信息。同樣,客戶端需要IO多路復用才能獲得服務器的這種狀態。
五、 阻塞在慢系統調用上時捕獲信號,也就是處理被中斷的系統調用
所謂慢系統調用,我們可以用Accept函數來描述它,當服務器端Listen后,就會阻塞在Accept函數上等待到來的客戶,當三次握手后,Accept返回。此處需要注意的時,有可能一直沒有客戶連接,Accept有可能永遠阻塞下去,永遠無法返回,這類系統調用就可以認為是慢系統調用。包括fgets, readline等。
現有如此場景:有5個客戶端與服務器保持連接,也就是服務器端有父進程+5個子進程,現在5個子進程同時終止,顯然服務器端的5個子進程會終止返回,發送SIGCHLD信號給父進程,此時父進程阻塞在Accept慢系統調用上,這時內核會導致Accept返回一個EINTR錯誤(被中斷的系統能夠調用),如果沒有if( n < 0 && errno == EINTR),則服務器會直接終止,當然,此處是將EINTR寫在了服務器子進程中,更常用的方法是將if( errno == EINTR)的處理寫在Accept函數之后。也就是說,不能講EINTR視為一個硬錯誤,他只是阻塞在慢系統調用上的一個中斷,我們需要捕獲它並Continue我們的程序。
綜述:
在本篇文章中,我沒有寫具體的代碼,只是講述了大概的工作及通信思路,只有這些思路原理都明白了,看他人的代碼或者自己寫才能有方案。
對於文章中所講述的IO多路復用、TCP斷開連接的詳細過程,有時間的話在之后的博客中會進行介紹。。。