UNP TCP 實例 (回射程序,未涉及 IO 多路複用) + 各方面需要注意的點

 
回射程序:客戶進程向服務器發送數據,服務器回送該數據,然後客戶進程將其顯示在 stdout。
 
server.c Code

#include "unp.h"

extern void str_echo(int);

int main(int argc, char **argv) {
	int listenfd, connfd;
	pid_t childpid;
	socklen_t clilen;
	struct sockaddr_in cliaddr, servaddr;

	if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
		err_sys("socket() error");
		exit(0);
	}

	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port = htons(SERV_PORT);		// SERV_PORT 在 unp.h 定義,其值爲 9877

	if(bind(listenfd, (SA *)&servaddr, sizeof(servaddr)) < 0) {
		err_sys("bind error");
		exit(1);
	}
	if(listen(listenfd, LISTENQ) < 0) {
		err_sys("listen error");
		exit(1);
	}
	for( ; ; ) {
		clilen = sizeof(cliaddr);
		if((connfd = accept(listenfd, (SA *)&cliaddr, &clilen)) < 0) {
			err_sys("accept error");
			exit(1);
		}
		if((childpid = fork()) < 0) {
			err_sys("fork error");
		} else if(childpid == 0) {
			if(close(listenfd) < 0) err_sys("close error");
			str_echo(connfd);
			exit(0);
		}
		if(close(connfd) < 0) err_sys("close error");
	}
}

 
str_echo.c Code

#include "unp.h"

void str_echo(int sockfd) {
	ssize_t n;
	char buf[MAXLINE];

again:
	while((n = read(sockfd, buf, MAXLINE)) > 0)
		Writen(sockfd, buf, n);

	if(n < 0 && errno == EINTR)
		goto again;
	else if(n < 0)
		err_sys("str_echo: read error");
}

 
client.c Code

#include "unp.h"

extern void str_cli(FILE*, int);

int main(int argc, char **argv) {
	int sockfd;
	struct sockaddr_in servaddr;

	if(argc != 2)
		err_quit("usage: tcpcli <IPaddress>");

	if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
		err_sys("socket() error");

	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(SERV_PORT);
	if(inet_pton(AF_INET, argv[1], &servaddr.sin_addr) < 0)
		err_sys("inet_pton error");
	if(connect(sockfd, (SA *)&servaddr, sizeof(servaddr)) < 0)
		err_sys("connet error");

	str_cli(stdin, sockfd);

	exit(0);
}

 
str_cli.c Code

#include "unp.h"

void str_cli(FILE *fp, int sockfd) {
	char sendline[MAXLINE], recvline[MAXLINE];

	while(Fgets(sendline, MAXLINE, fp) != NULL) {
		Writen(sockfd, sendline, strlen(sendline));

		if(Readline(sockfd, recvline, MAXLINE) == 0)
			err_quit("str_cli: server terminated prematurely");

		Fputs(recvline, stdout);
	}
}

 
 

【首先我們可以發現,server.c 中有執行 fork,但並沒有出現 wait 或 waitpid 函數來避免處理殭屍進程】

  那麼是不是直接在 server.c 中的 for( ; ; ) 內添加 wait(&status) 就可以了呢?

【顯然這樣是不正確的】,使用 fork 的原因就是爲了併發,直接調用 wait 會使得該程序一直阻塞,直到有一個子進程終止,並是一個殭屍進程。

  所以,好的處理方式是,編寫 SIGCHLD 信號處理函數。因爲當子進程狀態改變(不僅僅是子進程終止)時,會向父進程發送一個 SIGCHLD 信號。

即:

	void sig_chld(int signo) {
		pid_t pid;
		int stat;

		pid = wait(&stat);
		printf("child %d terminated\n", pid);
		return ;
	}

PS:書上說,signal(SIGCHLD, sig_chld) 只需要在 fork 第一個子進程之前做一次即可。不過我的疑問是,某些系統如 Ubuntu 18.04 對 signal 的處理似乎是早期的處理方式,在一次處理成功返回後,對該信號又變回了系統默認處理方式。

【不過是否這樣就足夠了呢?】
【並不是。】假設客戶端連續向服務端發送了 5 個連接請求,在連接全部成功建立之後,這 5 個連接又同時成功關閉。

那麼也就意味着,服務器父進程會連續收到 5 個 SIGCHLD 信號,而 Unix 信號默認是不排隊的。

  所以實際運行,最後可以發現,仍然存在 4 個殭屍進程。

所以,最終的 sig_chld 代碼是這樣的:

	void sig_chld(int signo) {
		pid_t pid;
		int stat;

		while((pid = waitpid(-1, &stat, WNOHANG)) > 0)
			printf("child %d terminated\n", pid);
		return ;
	}

【注意】waitpid 的第三個參數 WNOHANG 指明不會阻塞調用進程,而是在子進程沒有結束時返回 0。具體參考 waitpid 函數詳解。不能用循環調用 wait 也是出於這個原因,wait 會使調用進程阻塞。(父進程在子進程狀態改變時收到 SIGCHLD)
  感覺使用 if 是不是也可以???
 
 
 
 
 
 

【其次是對於 accept 函數,它是一個慢系統調用】

  所以,對於 server.c 中,我們應該考慮其被中斷而又未被系統自動重啓的情況。
  即,我們應該以如下的方式更改 server.c 中的代碼。

 //  關於改進:accept 是慢系統調用,我們應該處理其被信號中斷,而系統又不自動重啓 accept 的情況。
	if((connfd = accpet(listenfd, (SA *) &cliaddr, &clilen)) < 0) {
		if(errno == EINTR) continue;
		else err_sys("accept error");
	}

 
 
 
 
 

【accept 返回之前連接中止】
  即連接已經建立。假設此時的已完成連接隊列只有一個項,accept 在返回之前該連接被中止了。

  如何處理這種中止的連接依賴於不同的實現。POSIX 對其的處理方式是,將其 errno 設置爲 ECONNABORTED,表示非致命中止錯誤。通常這種情況下,服務器忽略它,再次調用 accept 即可。
 
 
 
 
 
 
 

【服務器(子)進程終止】

如下列情況所示:

hjm@hjm-Inspiron:~/InterviewPreparation/unp/Examples/TCP_Examples(回射程序)$ ./server.out &
[1] 15344
hjm@hjm-Inspiron:~/InterviewPreparation/unp/Examples/TCP_Examples(回射程序)$ ./client.out 127.0.0.1
hahahahah
hahahahah
kulekulekule
kulekulekule
child 15347 terminated		//(在此時,我們在另一個終端利用 kill 殺死了服務器的子進程)
another line                                 
str_cli: server terminated prematurely
hjm@hjm-Inspiron:~/InterviewPreparation/unp/Examples/TCP_Examples(回射程序)$ 

【注意】我是在同一臺電腦上運行的客戶端程序與服務端程序。上面的 child 15347 terminated 的輸出來自後臺進程組 server.out
 
  當鍵入 “another line” 時,str_cli 調用 writen,客戶 TCP 接着把數據發送給服務器,這是可以的。

  服務器進程已關閉,則向客戶進程發送了 FIN。

  而服務器對於收到的客戶進程發送的數據,再響應一個 RST。(RST 是復位報文,以通知對方關閉鏈接或者重新建立鏈接)

  但是由於客戶在 writen 之後立馬調用了 Readline,此時客戶進程是阻塞在 Readline 的,然後客戶進程收到 FIN,Readline 返回 0 (表示 EOF),即打印出錯信息 “server terminated prematurely”,客戶進程結束。所以客戶實際上並沒有看到這個 RST。

PS:當然,也有可能先收到 RST,此時的 Readline 返回出錯,並設置 errno 爲 ECONNRESET。
 
【本例子的問題在於】:當 FIN 到達套接字時,客戶正在阻塞 fgets 調用上。客戶實際上在應對兩個描述符——套接字與用戶輸入,它不能單純阻塞在這兩個源中的某個特定源的輸入上,而是應該阻塞在其中任何一個源的輸入上(是指每個源都阻塞?)。
 
 
 
 
 
 
 

【如果不理會 Readline 返回的錯誤,繼續執行寫操作會如何?】

 當一個進程向某個已收到 RST 的套接字執行寫操作時,內核向該進程發送一個 SIGPIPE 信號。該信號的默認行爲是終止進程。但不論該進程是從該信號的捕捉函數返回,還是忽略了該信號,寫操作都會返回出錯,並設置 errno 爲 EPIPE 錯誤。

即,假設 str_cli 代碼更改如下:

	void str_cli(FILE *fp, int sockfd) {
		char sendline[MAXLINE], recvline[MAXLINE];
		while(Fgets(sendline, MAXLINE, fp) != NULL) {
			Writen(sockfd, sendline, 1);
			sleep(1);
			Writen(sockfd, sendline + 1, strlen(sendline) - 1);
			if(Readline(sockfd, recvline, MAXLINE) == 0)
				err_quit("str_cli: server terminated prematurely");
			Fputs(recvline, stdout);
		}
	}

【終端】

hjm@hjm-Inspiron:~/InterviewPreparation/unp/Examples/TCP_Examples(回射程序)$ ./client.out 127.0.0.1
jjj
jjj
child 27723 terminated  //(在此時,我們在另一個終端利用 kill 殺死了服務器的子進程)
bye
Broken pipe		// 本行由 shell 顯示

由於 shell 的不同,有可能最後一句話並不會被顯示。

  所以我們可以在需要的時候自行編寫 SIGPIPE 的信號處理函數。。不過仍需要【注意】:當有多個套接字產生這個信號是,程序是無法判斷 SIGPIPE 來自於哪個套接字的。
 
 
 
 
 
 
 

【服務器崩潰未重啓/或服務器主機突然變得不可達】

  同樣,客戶進程在調用 writen 將數據寫入緩衝區後便返回,阻塞於 Readline 的調用。

  在這兩種情況下,即客戶進程不會收到任何報文,於是客戶進程會持續重傳數據。

  當重傳次數超過一定限制後,Readline 返回錯誤,若爲服務器崩潰,設置 errno 爲 ETIMEDOUT;否則會接收到一個 ICMP 消息,Readline 返回錯誤並設置 errno 爲 EHOSTUNREACH 或 ENETUNREACH。
 
 
 
 
 
 
 
 
【服務器主機崩潰後重啓】

  與上崩潰未重啓類似。不過這種情況下,服務器重啓後會接收到客戶進程的數據。而服務器崩潰後重啓意味着它的 TCP 丟失了崩潰前的所有連接信息,因此會響應一個 RST。

  當然,這種情況是出現在服務器崩潰後,客戶進程仍有向服務器發送數據。對於這種情況下,即使客戶進程沒有主動發送數據也要能檢測出來就需要依靠其他的技術。
 
 
 
 
 
 
 
【服務器主機關機】

  init 進程通常先給所有進程發送 SIGTERM 信號(該信號可被捕獲),等待一段固定時間

  然後給所有仍然在運行的進程發送 SIGKILL 信號(不能被捕獲)

  當服務器子進程終止時,所有打開着的描述符都被關閉,發送 FIN 給客戶端 TCP。之後客戶進程發生的事和當【服務器進程中止】時客戶進程發生的事相同。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章