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關閉自己的描述符,則由客戶開啟的套接字由核心關閉。
- 這一過程導致客戶TCP傳送一個FIN給伺服器,客戶端套接字首先進入FIN_WAIT_1狀態。
- 此時伺服器接收FIN同時傳送ACK,已連線套接字狀態變為CLOSE_WAIT狀態。
- 客戶端套接字接收ACK後進入FIN_WAIT_2狀態。
在伺服器已連線套接字接收到FIN時,read函式返回0,注意此處客戶端並沒有顯示的傳送一個0位元組的資料,伺服器是通過接收到FIN而使read函式返回0的。
此時伺服器的子程序返回,已連線套接字也會被關閉。由子程序來關閉已連線套接字會引發TCP連線終止序列的最後兩個分節。
- 伺服器向客戶傳送FIN,並進入LAST_ACK狀態。
- 客戶端接收FIN併發送ACK,此時套接字狀態進入TIME_WAIT。
- 伺服器接收客戶端發來的ACK,已連線套接字進入CLOSED狀態。
客戶端在等待2MSL後也將進入CLOSED狀態。
再來看幾種出現故障的情況
1)伺服器程序終止
在這種情況下,伺服器的子程序被殺死,此時導致已連線套接字引用計數為0,導致TCP連線終止工作開始。但這種情況與正常終止的情況是相反的:
- 伺服器向客戶端套接字傳送FIN,狀態進入FIN_WAIT1。
- 客戶接收FIN並響應一個ACK,狀態進入CLOSE_WAIT。
- 伺服器接收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
並未像書中展示的那樣提示錯誤。