UNP卷1:第五章(TCP客户/服务器程序示例)

最后更新于:2022-04-01 14:48:58

## 1. 经典的回射程序 ### 1) 服务器程序srv.c ~~~ #include <stdio.h> #include <stdlib.h> #include <time.h> #include <sys/socket.h> #include <sys/types.h> #include <fcntl.h> #include <netinet/in.h> #include <errno.h> #define MAXLINE 1024 #define SA struct sockaddr void str_echo(int sockfd); int main(int argc, char **argv) { int listenfd, connfd; int buff[MAXLINE]; pid_t pid; struct sockaddr_in servaddr; struct sockaddr_in cliaddr; socklen_t cliLen; 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(9877); bind(listenfd, (SA *)&servaddr, sizeof(servaddr)); listen(listenfd, 5); for ( ; ; ){ cliLen = sizeof(cliaddr); connfd = accept(listenfd, (SA *)&cliaddr, &cliLen); if ((pid = fork()) == 0){ close(listenfd); str_echo(connfd); _exit(0); } if (waitpid(pid, NULL, 0) != pid){ printf("waitpid error\n"); exit(1); } close(connfd); } return 0; } void str_echo(int sockfd) { ssize_t n; char buf[MAXLINE]; again: while ((n = read(sockfd, buf, MAXLINE)) > 0){ buf[n] = '\0'; write(sockfd, buf, n); } if (n < 0 && errno == EINTR) goto again; else if (n < 0) printf("str_echo:read error\n"); }维护子进程的信息,以便父进程在以后某个时候获取。这些信息包括子进程的进程ID,终止状 ~~~ ### 2) 客户端程序cli.c ~~~ #include <stdio.h> #include <stdlib.h> #include <netinet/in.h> #include <fcntl.h> #define MAXLINE 1024 #define SA struct sockaddr void str_cli(FILE *fp, int sockfd); int main(int argc, char **argv) { int sockfd, n; struct sockaddr_in servaddr; char buff[MAXLINE + 1]; struct sockaddr_in cliaddr; socklen_t cliLen; sockfd = socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(9877); inet_pton(AF_INET, argv[1], &servaddr.sin_addr); connect(sockfd, (SA *)&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 (read(sockfd, recvline, MAXLINE) == 0){ printf("str_cli:server terminated prematurely\n"); return; } fputs(recvline, stdout); } } ~~~ ### 3)程序运行 ### (1)服务器后台启动 ~~~ leichaojian@ThinkPad-T430i:~$ ./srv & [1] 3932 leichaojian@ThinkPad-T430i:~$ netstat -a | grep 9877 tcp 0 0 *:9877 *:* LISTEN ~~~ ### (2)启动客户端,并且键入一行文本 客户端: ~~~ leichaojian@ThinkPad-T430i:~$ ./cli 127.0.0.1 hello world hello world ~~~ 服务端: ~~~ leichaojian@ThinkPad-T430i:~$ netstat -a | grep 9877 tcp 0 0 *:9877 *:* LISTEN tcp 0 0 localhost:9877 localhost:43399 ESTABLISHED tcp 0 0 localhost:43399 localhost:9877 ESTABLISHED ~~~ ### (3)客户端终止 ~~~ leichaojian@ThinkPad-T430i:~$ netstat -a | grep 9877 tcp 0 0 *:9877 *:* LISTEN tcp 0 0 localhost:43399 localhost:9877 TIME_WAIT ~~~ ## 2. 处理信号 ### 1) POSIX信号处理    信号就是告知某个进程发生了某个事件的通知,有时也称为软件中断。信号通常是异步发生的,也就是说进程预先不知道信号的准确发生时刻。    信号可以: (1)由一个进程发给另一个进程。 (2)由内核发给某个进程。    每个信号都有一个与之关联的处置,也称为行为: (1)我们可以提供一个函数,只要有特定信号发生它就会被调用。这样的函数称为信号处理函数,这种行为称为捕获信号。有两个信号不能被捕获,它们是SIGKILL和SIGSTOP。信号处理函数由信号值这个单一的整数参数来调用,且没有返回值,其函数原型如下: ~~~ void handler( int signo ); ~~~ (2)我们可以把某个信号的处置设定为SIG_IGN来忽略它。SIGKILL和SIGSTOP这两个信号不能被忽略。 (3)我们可以把某个信号的处置设定为SIG_DFL来启用它的默认处置。 ### 2) 处理SIGCHLD信号    设置僵尸状态的目的是维护子进程的信息,以便父进程在以后某个时候获取。这些信息包括子进程的进程ID,终止状态以及资源利用信息。如果一个进程终止,而该进程有子进程处于僵尸状态,那么它的所有僵尸子进程的父进程ID将被重置为1(init进程)。继承这些子进程的init进程将清理它们。    而僵尸进程出现时间是在子进程终止后,但是父进程尚未读取这些数据之前。所有解决之道就是保证父进程处理这些数据,我们可以通过wait或者waitpid函数来达到这个要求。    由于子进程的终止必然会产生信号SIGCHLD信号,所以重写TCP服务器程序最终版本: ### (1)服务器程序srv.c ~~~ #include <stdio.h> #include <netinet/in.h> #include <stdlib.h> #include <sys/socket.h> #include <signal.h> #include <errno.h> #define MAXLINE 1024 #define SA struct sockaddr void sig_chld(int signo); typedef void Sigfunc(int); Sigfunc *Signal(int signo, Sigfunc *func); void str_echo(int sockfd); int main(int argc, char **argv) { int listenfd, connfd; pid_t childpid; socklen_t clilen; struct sockaddr_in servaddr, cliaddr; listenfd = socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(9877); servaddr.sin_addr.s_addr = htonl(INADDR_ANY); bind(listenfd, (SA *)&servaddr, sizeof(servaddr)); listen(listenfd, 5); Signal(SIGCHLD, sig_chld); for ( ; ; ){ clilen = sizeof(cliaddr); if ((connfd = accept(listenfd, (SA *)&cliaddr, &clilen)) < 0){ if (errno == EINTR) continue; else{ printf("accept error\n"); exit(-1); } } if ((childpid = fork()) == 0){ close(listenfd); str_echo(connfd); exit(0); } close(connfd); } return 0; } void sig_chld(int signo) { pid_t pid; int stat; while ((pid = waitpid(-1, &stat, WNOHANG)) > 0) printf("child %d terminated\n", pid); return; } Sigfunc *Signal(int signo, Sigfunc *func) { struct sigaction act, oact; act.sa_handler = func; sigemptyset(&act.sa_mask); act.sa_flags = 0; if (signo == SIGALRM){ #ifdef SA_INTERRUPT act.sa_flags |= SA_INTERRUPT; #endif } else { #ifdef SA_RESTART act.sa_flags |= SA_RESTART; #endif } if (sigaction(signo, &act, &oact) < 0) return (SIG_ERR); return (oact.sa_handler); } void str_echo(int sockfd) { char buff[MAXLINE]; int n; for ( ; ; ){ if ((n = read(sockfd, buff, MAXLINE)) > 0){ buff[n] = '\0'; write(sockfd, buff, n); } else if (n < 0 && errno == EINTR) continue; else if (n < 0){ printf("str_echo:read error\n"); return; } else if (n == 0){ break; } } } ~~~ ### (2)客户端测试程序cli.c ~~~ #include <stdio.h> #include <stdlib.h> #include <netinet/in.h> #include <fcntl.h> #define MAXLINE 1024 #define SA struct sockaddr void str_cli(FILE *fp, int sockfd); int main(int argc, char **argv) { int sockfd[5], n; struct sockaddr_in servaddr; struct sockaddr_in cliaddr; int i; for (i = 0; i < 5; i++){ sockfd[i] = socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(9877); inet_pton(AF_INET, argv[1], &servaddr.sin_addr); connect(sockfd[i], (SA *)&servaddr, sizeof(servaddr)); } str_cli(stdin, sockfd[0]); 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 (read(sockfd, recvline, MAXLINE) == 0){ printf("str_cli:server terminated prematurely\n"); return; } fputs(recvline, stdout); } } ~~~ 程序输出如下: 客户端: ~~~ leichaojian@ThinkPad-T430i:~$ ./cli 127.0.0.1 hello world hello world ^C ~~~ 服务端: ~~~ leichaojian@ThinkPad-T430i:~$ ./srv child 9831 terminated child 9835 terminated child 9832 terminated child 9833 terminated child 9834 terminated ^C ~~~ ### 3) 测试僵尸进程的产生 test.c: ~~~ #include <stdio.h> #include <signal.h> #include <sys/wait.h> int main( void ) { pid_t pid; if ( ( pid = fork() ) == 0 ){ printf("child:%d\n", getpid()); exit(0); } sleep( 20 ); if ( pid > 0 ){ printf("parent:%d\n", getpid() ); } return 0; } ~~~ 程序运行: ~~~ leichaojian@ThinkPad-T430i:~$ ./a.out child:14447 parent:14446 ~~~ 在显示child:14447而尚未显示parent:14446(即20秒的睡眠时间),我们执行如下命令: ~~~ leichaojian@ThinkPad-T430i:~$ ps -eo state,pid,cmd | grep '^Z' Z 14447 [a.out] <defunct> ~~~    发现子进程14447果真称为僵尸进程。但是过了20秒后,再次执行时候,则没有任何数据,说明僵尸进程已经被父进程杀死了(就是父进程读取了子进程的数据) ### 4) 服务器进程终止 具体操作如下: ### 1)运行服务器程序,运行客户端程序: 服务端: ~~~ leichaojian@ThinkPad-T430i:~$ ./tcpserv ~~~ 客户端: ~~~ leichaojian@ThinkPad-T430i:~$ ./tcpcli 127.0.0.1 hello hello ~~~ 监视端: ~~~ leichaojian@ThinkPad-T430i:~$ netstat -a | grep 9877 tcp 0 0 *:9877 *:* LISTEN tcp 0 0 localhost:37935 localhost:9877 ESTABLISHED tcp 0 0 localhost:9877 localhost:37935 ESTABLISHED ~~~ ### 2) 终止服务器程序(先终止服务器程序,然后执行监视端,再执行客户端,再执行监视端) 监视端: ~~~ leichaojian@ThinkPad-T430i:~$ netstat -a | grep 9877 tcp 0 0 localhost:9877 localhost:37953 FIN_WAIT2 tcp 1 0 localhost:37953 localhost:9877 CLOSE_WAIT ~~~ 客户端: ~~~ leichaojian@ThinkPad-T430i:~$ ./tcpcli 127.0.0.1 hello hello world str_cli error ~~~ 监视端:(无任何输出,说明客户端进程已经终止,这里终止是产生了信号,强行终止) 来自UNP上的解释是:当一个进程向某个已收到RST的套接字执行写操作时,内核向该进程发送一个SIGPIPE信号。该信号的默认行为是终止进程,因此进程必须捕获它以免不情愿的被终止。 ~~~ leichaojian@ThinkPad-T430i:~$ netstat -a | grep 9877 ~~~ ### 3) 问题出在哪里?    当服务端的FIN到达套接字时,客户正阻塞与fgets调用上。客户实际上在应对两个描述符--套接字和用户输入,它不能单纯阻塞在这两个源中的某个特定源的输入上(正如目前编写的str_cli函数所为),而是应该阻塞在其中任何一个源的输入上,这正是select和poll这两个函数的目的之一。
';