1. 程式人生 > >Unix網路程式設計讀書筆記(三)

Unix網路程式設計讀書筆記(三)

這一章正式開始網路程式設計的內容,先將書中的示例編寫如下:

首先是伺服器端:

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

#define SERV_PORT 6666
#define LISTENQ 14
#define MAXLINE 100

void str_echo(int sockfd);

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

	bzero(&servaddr,sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port = htons(SERV_PORT);

	bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr));

	listen(listenfd,LISTENQ);

	for(;;){
		clilen = sizeof(cliaddr);
		connfd = accept(listenfd,(struct sockaddr*)&cliaddr,&clilen);

		if((childpid=fork())==0){
			close(listenfd);
			str_echo(connfd);
			exit(0);
		}

		close(connfd);
	}
	

	return 0;
}

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\n");
		exit(1);
	}
		

	
}

然後是客戶端程式:
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

#define SERV_PORT 6666
#define MAXLINE 100

void str_cli(FILE* fp,int sockfd);
ssize_t Readline(int fd,void* vptr,size_t maxlen);

int main(int argc,char* argv[])
{
	int sockfd;
	struct sockaddr_in servaddr;
	
	sockfd = socket(AF_INET,SOCK_STREAM,0);

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

	connect(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr));

	str_cli(stdin,sockfd);

	return 0;
}

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

	while(fgets(sendline,MAXLINE,fp)!=NULL){
		write(sockfd,sendline,strlen(sendline));

		if(Readline(sockfd,recvline,MAXLINE)==0){
			printf("str_cli:server terminated prematurely\n");
			exit(0);
		}

		fputs(recvline,stdout);
	}
}

ssize_t Readline(int fd,void* vptr,size_t maxlen)
{
	ssize_t n,rc;
	char c,*ptr;
	
	ptr = vptr;

	for(n=1;n<maxlen;n++){
	again:
		if((rc=read(fd,&c,1))==1){
			*ptr++ = c;
			if(c=='\n') break;
		}else if(rc==0){
			*ptr = 0;
			return (n-1);
		}else{
			if(errno==EINTR) goto again;
			return (-1);
		}
	}
	*ptr = 0;
	return (n);
}

程式執行結果如下:
./src_5_2 &
[1] 4512

使用netstat命令檢查伺服器監聽套接字的狀態:
netstat -a
啟用Internet連線 (伺服器和已建立連線的)
Proto Recv-Q Send-Q Local Address           Foreign Address         State      
tcp        0      0 *:6666                  *:*                     LISTEN     

6666就是咱們設定的埠號,666。*代表通配地址。當前狀態為LISTEN。

此時再次使用netstat命令,檢視套接字狀態

netstat -a | grep 6666
tcp        0      0 *:6666                  *:*                     LISTEN     
tcp        0      0 localhost:38280         localhost:6666          ESTABLISHED
tcp        0      0 localhost:6666          localhost:38280         ESTABLISHED

第一行的套接字代表伺服器的監聽套接字。

第二行的套接字代表客戶端的套接字。

第三行的套接字代表伺服器的已連線套接字。

寫至此處我感覺在上一篇讀書筆記中存在一處錯誤,在accept函式返回後,監聽套接字仍然處於監聽狀態,而僅有已連線套接字處於建立狀態。

最後通過ps命令對程序中的狀態進行檢視:

ps -t pts/8 -o pid,ppid,tty,stat,argc,command,wchan
  PID  PPID TT       STAT ARGC COMMAND                     WCHAN
 2363  2357 pts/8    Ss      - bash                        wait
 2428  2363 pts/8    S       - ./src_5_2                   inet_csk_accept
 2434  2363 pts/8    S+      - ./src_5_4 127.0.0.1         wait_woken
 2435  2428 pts/8    S       - ./src_5_2                   sk_wait_data

Linux程序阻塞於accept時,輸出inet_csk_accept。

Linux程序阻塞於終端I/O時,輸出wait_woken。

Linux程序阻塞於套接字輸入或輸出時,輸出sk_wait_data。

通過crtl+D正常終止客戶端,再次使用netstat命令檢視套接字狀態。

netstat -a | grep 6666
tcp        0      0 *:6666                  *:*                     LISTEN     
tcp        0      0 localhost:38465         localhost:6666          TIME_WAIT  

此時客戶端套接字已經進入TIME_WAIT狀態。在伺服器方面,監聽套接字仍然處於LISTEN狀態,而已連線套接字套接字則完全關閉。

讓我們來回顧一下已連線套接字是如何關閉的。

首先是客戶端呼叫exit關閉自己的描述符,則由客戶開啟的套接字由核心關閉。

  1. 這一過程導致客戶TCP傳送一個FIN給伺服器,客戶端套接字首先進入FIN_WAIT_1狀態。
  2. 此時伺服器接收FIN同時傳送ACK,已連線套接字狀態變為CLOSE_WAIT狀態。
  3. 客戶端套接字接收ACK後進入FIN_WAIT_2狀態。

在伺服器已連線套接字接收到FIN時,read函式返回0,注意此處客戶端並沒有顯示的傳送一個0位元組的資料,伺服器是通過接收到FIN而使read函式返回0的。

此時伺服器的子程序返回,已連線套接字也會被關閉。由子程序來關閉已連線套接字會引發TCP連線終止序列的最後兩個分節。

  1. 伺服器向客戶傳送FIN,並進入LAST_ACK狀態。
  2. 客戶端接收FIN併發送ACK,此時套接字狀態進入TIME_WAIT。
  3. 伺服器接收客戶端發來的ACK,已連線套接字進入CLOSED狀態。

客戶端在等待2MSL後也將進入CLOSED狀態。

再來看幾種出現故障的情況

1)伺服器程序終止

在這種情況下,伺服器的子程序被殺死,此時導致已連線套接字引用計數為0,導致TCP連線終止工作開始。但這種情況與正常終止的情況是相反的:

  1. 伺服器向客戶端套接字傳送FIN,狀態進入FIN_WAIT1。
  2. 客戶接收FIN並響應一個ACK,狀態進入CLOSE_WAIT。
  3. 伺服器接收ACK,狀態進入FIN_WAIT2。

但此時客戶程序仍然阻塞於fgets函式,套接字並未關閉,因此無法完成連線終止的後半部分。

此時使用netstat命令檢視套接字狀態。

netstat -a | grep 6666
tcp        0      0 *:6666                  *:*                     LISTEN     
tcp        1      0 localhost:39881         localhost:6666          CLOSE_WAIT 
tcp        0      0 localhost:6666          localhost:39881         FIN_WAIT2  

此時在客戶端中輸入資料,程式執行結果如下:
./src_5_4 127.0.0.1 //在此之前伺服器子程序已經被殺死
another line
str_cli:server terminated prematurely

當我們輸入“another line”時,TCP仍然會將資料發往伺服器,但此時先前開啟套接字的程序已經終止,於是響應以一個RST。再來看客戶端程式,客戶端在呼叫wrtite將資料發往伺服器後,使用read系統呼叫從套接字中讀出資料,但由於在套接字上已經接收到FIN,因此read系統呼叫返回0(表示EOF)。

2)向已收到RST的套接字執行寫操作

當這種情況發生時,核心向該程序傳送SIGPIPE訊號。這裡要與上一個例子區別開來:在上一個例子中,是向收到FIN的客戶套接字再次寫入資料,但向一個已接收FIN的套接字寫入資料是沒有問題的。但向已經關閉的已連線套接字傳送資料,則會導致伺服器響應RST,但向已經接收到RST的套接字寫入資料則是一個錯誤。

將客戶程式稍作修改:

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

	while(fgets(sendline,MAXLINE,fp)!=NULL){
		write(sockfd,sendline,1);
		sleep(1);
		write(sockfd,sendline+1,strlen(sendline)-1);

		if(Readline(sockfd,recvline,MAXLINE)==0){
			printf("str_cli:server terminated prematurely\n");
			exit(0);
		}

		fputs(recvline,stdout);
	}
}

將寫操作變為兩個是讓第一個write引發RST,再讓第二個write引發SIGPIPE。

執行結果如下:

./src_5_14 127.0.0.1
hello world
hello world
bye

並未像書中展示的那樣提示錯誤。