《UNIX網絡編程 》學習筆記 (五)

第五章 TCP客戶/服務器程序示例
5.1 概述
編寫一個完成的echo 程序,來講解TCP客戶/服務器的編寫流程。
本章的TCP客戶/服務器模型:
                    標準輸入 --->fgets--->TCP客戶程序------>write------->read--->TCP服務器
                    標準輸出 <---fputs<---TCP客戶程序<-----read <------write<----TCP服務器
5.2 TCP的echo服務器程序:main函數
在使用以下內核版本的系統中編譯通過:Linux version 2.6.38-8-generic (buildd@vernadsky) (gcc version 4.5.2 (Ubuntu/Linaro 4.5.2-8ubuntu3) )
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

#define SERV_PORT 5508
#define LISTENQ 10

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

	struct sockaddr_in	cliaddr,servaddr;

	memset( (struct sockaddr *)&servaddr,0,sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port = htons(SERV_PORT);
	
	if( (listenfd = socket(AF_INET,SOCK_STREAM,0)) == -1){
		printf("create socket failure");
		exit(1);
	}
	if(bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr)) == -1){
		printf("call bind failure");
		exit(1);
	}
	if(listen(listenfd,LISTENQ) == -1){
		printf("call listen failure");
		exit(1);
	}

	for(;;){
		
		clilen = sizeof(cliaddr);
		if( (connfd = accept(listenfd,(struct sockaddr *)&cliaddr,&clilen)) == -1){
			printf("call accept failure");
		}
		if( (childpid = fork()) == 0){
			close(listenfd);
			str_echo(connfd);/**process the request**/
			exit(0);	
		}
		close(connfd);
	}
}

5.3  TCP 的echo 服務器程序的 str_echo 函數
str_echo 具體處理每個客戶的請求。讀取客戶發送的數據並echo給客戶。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

#define SERV_PORT 5508
#define LISTENQ 10
#define MAXLINE 1024

void
str_echo(int sockfd)
{
	ssize_t n ;
	char buf[MAXLINE];
again:
	while( (n = read(sockfd,buf,MAXLINE))>0)
		write(sockfd,buf,n);

	if(n < 0 && errno == EINTR)
		goto again;
	
	else
		if(n<0){
			printf("read error");
			exit(0);
		}
		
}

5.4 TCP 的 echo 客戶程序的main函數

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

#define SERV_PORT 5508
#define LISTENQ 10
#define MAXLINE 1024

int
main(int argc, char **argv)
{
	if(argc!=2){
		printf("Parameter error !!");
		exit(1);
	}

	int sockfd;
	struct sockaddr_in	servaddr;

	memset(&servaddr,0,sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	inet_pton(AF_INET,argv[1],&servaddr.sin_addr);
	servaddr.sin_port = htons(SERV_PORT);

	sockfd = socket(AF_INET,SOCK_STREAM,0);
	connect(sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr));
	
	str_cli(stdin,sockfd);
	exit(0);	
}

5.5 TCP的echo客戶程序的str_cli 函數

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

#define SERV_PORT 5508
#define LISTENQ 10
#define MAXLINE 1024


/**
 *沒有添加 write 和 read 的錯誤處理*
**/
void str_cli(FILE *fp,int sockfd)
{
	char SendLine[MAXLINE],RecvLine[MAXLINE];
	char *ptr;
	size_t retsz,wtsz,rdsz;
		
	while(fgets(SendLine,MAXLINE,fp)!=NULL){
		
		wtsz = rdsz = strlen(SendLine);
		ptr = SendLine;			
		while(wtsz > 0){
			
			retsz = write(sockfd,ptr,wtsz);
			wtsz -= retsz;
			ptr += retsz;					
		}
		ptr = RecvLine;	
		while(rdsz > 0){
			
			retsz = read(sockfd,ptr,rdsz);
			rdsz -= retsz;
			ptr += retsz;			
		}
		
		*ptr = '\0';
		fputs(RecvLine,stdout);
	}
}

5.6 5.7 略過;

5.8 POSIX 信號處理

(1)信號(signal)也稱爲軟件中斷(software interrupt),就是告知某個進程發生了某個事件的通知。

(2)信號通常是異步發生的,也就是說進程預先不知道信號發生的準確時刻。

(3)信號可以由一個進程發給自身或另一個進程,也可以由內核發給某個進程。
(4)每一個信號都有一個與之關聯的處置(disposition)也稱爲行爲(action).通過調用sigaction來設定一個信號的的行爲,並有三種選擇。
(5)sigaction設置信號行爲的三種選擇:
((1))調用sigaction設置一個函數,當指定的信號發生時,就調用這個函數,這樣的函數稱爲信號處理函數(signal handler).這種行爲稱爲捕獲(catching).有兩個信號不能被捕獲,分別是:SIGKILL,SIGSTOP 。
  信號處理函數(signal handler)的原型:void handler(int signo);
 大多數信號只要求我們調用sigaction並設置信號處理函數。但像 SIGIO,SIGPOLL,SIGURG等等一些信號還需要捕獲它的進程做些額外的工作。
((2))可以把某個信號的處置設置爲SIG_IGN(ignore)來忽略它。SIGKILL 和SIGSTOP這兩個信號不能被忽略。
((3))可以把某個信號的處置設置爲SIG_DFL(default)來啓用它的默認處置。默認處置通常是在收到信號後終止進程。但有一些進程的默認處置是忽略。

(6)signal 函數

(1)建立信號處置的POSIX方法是調用sigaction函數。也可以調用signal函數,但是signal函數不是POSIX標準函數,並且是不可移植的。

(2)signal 函數的第一個參數是信號名,第二個參數或爲指向函數的指針或爲常值SIG_IGN或SIG_DFL.

(3)sigaction 函數的簡化調用,自定義一個signal函數,在這個函數中調用 sigaction函數,從而達到簡化調用的目的。自定義signal函數的代碼如下:

#include <signal.h>

typedef void Sigfunc(int);

/**
 *自定義signal函數,簡單的對sigaction函數進行封裝。
**/

Sigfunc *
signal(int signo,Sigfunc *func)
{
	struct sigaction act,oact;

	act.sa_handler = func;//sigaction的信號處理函數
	sigemptyset(&act.sa_mask);//調用信號處理函數期間將被阻塞的信號集
	act.sa_flags = 0;
	
	if(sigaction(signo,&act,&oact)<0)
		return (NULL);
	else
		return (oact.sa_handler);
	
}
(4)信號處理函數一旦安裝,便一直安裝着。

(5)信號處理函數運行期間,被遞交的信號是阻塞的,並且sa_mask中設置的所有信號也將被阻塞。

(6)如果一個信號被阻塞期間產生了多次,那麼信號解阻塞後通常只遞交一次。UNIX的信號默認不排隊。

(7)sigprocmask函數可以選擇性的阻塞或者解阻塞一組信號。

5.9 處理SIGCHLD信號

(1)設置僵死(zombie)狀態的目的是維護子進程的信息。以便父進程在以後莫個時刻獲取子進程的進程ID,終止狀態以及資源利用信息。

(2)如果一個進程終止,而該進程有子進程正處於僵死狀態,那麼所有僵死子進程的父進程ID將被重置爲1(init進程),init進程將清理這些僵死進程。

(3)如果fork子進程,那麼就要wait它們,以防止它們變成僵死進程。

(4)捕獲SIGCHLD信號,並在信號處理函數中wait子進程,我們始終應該調用waitpid而非wait來處理子進程。

(5)我們始終應該檢查慢系統調用是否返回EINTR錯誤。並決定是否重啓這些系統調用。(一些系統會自動重啓被中斷的系統調用)。

(6)connect不能被重啓,當connect函數被信號中斷且不自動重啓時,我們必須調用select來等待連接完成。

5.10 wait和waitpid函數

#include <sys/wait.h>

pid_t  wait(int *static);

pid_t  waitpid(pid_t pid,int *static ,int options);

返回值:成功返回進程ID,出錯返回返回0或-1;

參數:int *static ,wait 和waitpid返回時會將子進程的終止狀態(一個整數)存放在static中。

pit_t pid 想等待的進程ID號。-1表示等待第一個結束的子進程。

int options 附加選項,常用的是WNOHANG,告知內核在沒有以終止子進程時不要阻塞。

wait和waitpid的區別: wait 等待第一個結束的子進程,如果沒有結束的子進程,wait將阻塞。waitpid 通過參數設置,可以在沒有子進程結束時waitpid不阻塞。

5.11 accept返回前連接終止

 Berkeley 的實現在內核中處理終止的連接。POSIX 規定返回一個ECONNABORTED 的 errno. 

5.12 服務器進程終止

如果向一個服務進程已終止的服務器發起連接,服務器將返回一個RST 信號。

5.13 SIGPIPE 信號

向一個接收到FIN的套接字寫數據會收到RST,

向一個已接收到RST的套接字寫數據將引發SIGPIPE信號。並且寫操作返回EPIPE錯誤。

SIGPIPE信號的默認行爲是終止進程。

5.14 服務器主機崩潰

如果服務器主機崩潰沒有對客戶做出響應,將返回ETIMEOUT錯誤。

如果中間路由器檢測到服務器主機不可達,將響應destination unreachable的ICMP消息,內核將返回EHOSTUNREACH錯誤。

5.15 服務器主機崩潰重啓

當服務器主機崩潰重啓後,它的所有連接都已經丟失,因此服務器TCP對所收到的來自客戶的數據分節響應一個RST。

5.16 服務器主機關機

unix系統關機時,init進程通常先給所有進程發送SIGTERM信號。等待5-20秒後給所有仍然在運行的進程發送SIGKILL信號,這麼做的目的是給進程一小段時間來清除和終止。

5.17 TCP程序例子小結

需要通信的客戶/服務器程序在通信之前都要指定套接字對。【本地IP地址,本地端口號,外地IP地址,外地端口】。

客戶程序的本地IP地址和本地端口號通常是內核分配。服務程序的本地IP地址和端口號有bind函數指定。

5.18 數據格式

網絡傳遞數據遇到的一些問題:

(1)不同的實現以不同的格式存儲二進制數,最常見的是大端字節序和小端字節序。

(2)不同的實現在存儲相同的C數據類型上可能存在差異,例如32位系統中的long 爲32位,64位系統中的long爲64位。

(3)不同的實現給結構打包的方式存在差異,取決於各種數據類型所用的位數以及機器的對齊限制,因此,穿越套接字傳送二進制結構絕不明智。

解決上述問題的兩個常用方法:

(1)把所有的數值數據作爲文本串來傳遞,前提是客戶和服務器機器具有相同的字符集。

(2)顯式定義所支持數據類型的二進制格式(位數,大端或小端字節序),並以這樣的格式在客戶與服務器之間傳遞所有數據。

5.19 小結

這一章通過一個客戶/服務器程序展示了在編寫網絡程序要遇到的問題,包括 信號捕獲,處理僵死進程,服務器主機發生錯誤的幾種情況。還講解了在客戶和服務器之間傳送的數據的格式。

















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