UNIX网络编程笔记(4):简单的回射程序
上一講中我們通過調用fork函數實現了一個簡單的并發時間獲取服務器。這是一個簡單的并發服務器框架,然而這里使用這個框架實現一個簡單的回射服務器會出現一個問題,這個問題就是僵尸子進程。
1、回射程序
下圖是這個回射程序的主要結構:
客戶向服務器發送一行文字,服務器回射回來,然后客戶再輸出到屏幕上。這里的fgets、fputs、read、write函數都是標準庫函數或系統調用,唯一要說明的就是這個readline函數,這個函數讀取文件中的一行,調用read系統調用:
int 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); }這里的服務器程序還是遵循上一講中的流程,不同的是處理請求的過程,服務器處理請求時調用一個函數str_echo,返回客戶寫入的一行,代碼如下: #include <sys/socket.h> #include <errno.h> #include <string.h> #include <strings.h> #include <netinet/in.h> #include <arpa/inet.h> #include <stdio.h> #include <time.h> #include <unistd.h> #define MAXLINE 1024 void str_echo(int sockfd); int main(int argc,char *argv[]) {int listenfd;struct sockaddr_in servaddr;char buff[MAXLINE];time_t ticks;if((listenfd=socket(AF_INET,SOCK_STREAM,0))<0){printf("socket error\n");return 0;}bzero(&servaddr,sizeof(servaddr));servaddr.sin_family=AF_INET;servaddr.sin_port=htons(5000);servaddr.sin_addr.s_addr=htonl(INADDR_ANY);if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0){printf("bind error\n");return 0;}if(listen(listenfd,5)<0){printf("listen error\n");return 0;}int connfd;socklen_t len;struct sockaddr_in cliaddr;pid_t pid;for(;;){len=sizeof(cliaddr);if((connfd=accept(listenfd,(struct sockaddr*)&cliaddr,&len))<0){printf("accept error\n");return 0;}if((pid=fork())==0){if(close(listenfd)<0){printf("close listenfd error\n");return 0;}printf("[PID]%ld Receive a connection from:%s.%d\n",(long)getpid(),inet_ntop(AF_INET,&cliaddr.sin_addr,buff,sizeof(buff)),ntohs(cliaddr.sin_port));str_echo(connfd);if(close(connfd)<0){printf("close child connfd error\n");return 0;}return 0;}if(close(connfd)<0){printf("close parent connfd error\n");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");return;} }客戶端程序也大體相同,首先從標準輸入中讀取一行,寫入套接字發送給服務器,接收到套接字后將套接字的內容輸出到標準輸出上,代碼如下: #include <sys/socket.h> #include <errno.h> #include <stdio.h> #include <string.h> #include <strings.h> #include <netinet/in.h> #include <arpa/inet.h> #define MAXLINE 1024 void str_cli(FILE *fp,int sockfd); int readline(int fd,void *vptr,size_t maxlen); int main(int argc,char *argv[]) {int sockfd;char recvline[MAXLINE];if(argc!=2||strcmp(argv[1],"--help")==0){printf("Usage:%s <IPaddress>\n",argv[0]);return 0;}if((sockfd=socket(AF_INET,SOCK_STREAM,0))<0){printf("socket error\n");return 0;}struct sockaddr_in servaddr;bzero(&servaddr,sizeof(servaddr));servaddr.sin_family=AF_INET;servaddr.sin_port=htons(5000);if(inet_pton(AF_INET,argv[1],&servaddr.sin_addr)<=0){printf("inet_pton error for %s\n",argv[1]);return 0;}if(connect(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0){printf("connect error\n");return 0;}str_cli(stdin,sockfd);return 0; } void str_cli(FILE *fp,int sockfd) {char sendline[MAXLINE],recvline[MAXLINE];printf("puts:");while(fgets(sendline,MAXLINE,fp)){write(sockfd,sendline,strlen(sendline));if(readline(sockfd,recvline,MAXLINE)==0){printf("str_cli:server terminated prematurely\n");return;}printf("gets:");fputs(recvline,stdout);printf("puts:");} } int 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); }下面是運行結果:(1)啟動服務器:
(2)客戶發起請求并輸入文字:
服務器成功返回并輸出到標準輸出上。
(3)服務器端:
2、程序執行時發生了什么?
1、啟動時
(1)服務器啟動后阻塞在accept函數上,等待客戶連接。客戶發起連接后accept函數返回,服務器進入子進程調用str_echo函數,函數調用read系統調用,并阻塞在這里,因為客戶還沒有輸入文字;
(2)客戶建立連接后,調用str_cli函數,函數調用fgets并阻塞,等待用戶輸入;
(3)與此同時,服務器父進程關閉連接描述符connfd,重新阻塞在accept函數上,等待下一個客戶連接;
以上就是連接建立后發生的事情。
2、執行時
(1)執行時用戶在客戶端輸入文字,fgets函數返回,str_cli函數將輸入寫入套接字,發送給服務器子進程,然后調用readline,readline函數調用read,并阻塞于read,因為客戶等待服務器的應答;
(2)服務器子進程調用read讀取客戶輸入,寫入套接字發送給客戶作為應答并繼續阻塞在read函數上;
(3)客戶調用readline函數讀取服務器子進程應答并將數據寫入標準輸出;
(4)循環進行直到客戶得到用戶的EOF輸入;
3、終止時
(1)用戶在客戶輸入EOF字符,fgets返回NULL指針,于是str_cli函數返回;
(2)str_cli函數返回到main函數,main然后通過return終止;
(3)進程終止后關閉所有打開的描述符,因此客戶打開的套接字由內核關閉,導致客戶TCP發送一個FIN給服務器,服務器以ACK響應,這就是TCP終止的前半部分;
(4)當服務器接收到FIN時,服務器子進程阻塞于readline函數,readline函數返回0,導致str_echo函數返回到main;
(5)服務器子進程通過return終止;
(6)服務器子進程打開的所有描述符關閉。由子進程來關閉已連接套接字會引發TCP連接終止序列的最后兩個分節:一個從服務器到客戶的FIN和客戶到服務器的ACK。至此,連接完全終止;
3、有什么問題?
上面的進程終止還有一部分內容,就是服務器子進程終止時,會給父進程發送一個SIGCHLD信號。這里我們沒有捕獲信號,而該信號的默認行為是被忽略。由于父進程中沒有處理該信號,子進程于是進入僵死狀態,即僵尸進程。可以使用ps命令查看:
可以看到,進程號是4228的進程是僵尸進程,而在我們的服務器運行時,客戶連接的子進程ID正好是4228。
僵尸進程必須處理。下一講來用信號處理函數處理僵尸進程。
總結
以上是生活随笔為你收集整理的UNIX网络编程笔记(4):简单的回射程序的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: UNIX网络编程笔记(3):简单的并发服
- 下一篇: LOL英雄联盟男刀锋泰隆出装和出招顺序?