UNP学习笔记(第五章 TCP客户/服务程序实例)
我们将在本章使用前一章中介绍的基本函数编写一个完整的TCP客户/服务器程序实例
这个简单得例子是执行如下步骤的一个回射服务器:
TCP回射服务器程序
1 #include "unp.h" 2 3 int 4 main(int argc, char **argv) 5 { 6 int listenfd, connfd; 7 pid_t childpid; 8 socklen_t clilen; 9 struct sockaddr_in cliaddr, servaddr; 10 11 listenfd = Socket(AF_INET, SOCK_STREAM, 0); 12 13 bzero(&servaddr, sizeof(servaddr)); 14 servaddr.sin_family = AF_INET; 15 servaddr.sin_addr.s_addr = htonl(INADDR_ANY); 16 servaddr.sin_port = htons(SERV_PORT); 17 18 Bind(listenfd, (SA *) &servaddr, sizeof(servaddr)); 19 20 Listen(listenfd, LISTENQ); 21 22 for ( ; ; ) { 23 clilen = sizeof(cliaddr); 24 connfd = Accept(listenfd, (SA *) &cliaddr, &clilen); 25 26 if ( (childpid = Fork()) == 0) { /* child process */ 27 Close(listenfd); /* close listening socket */ 28 str_echo(connfd); /* process the request */ 29 exit(0); 30 } 31 Close(connfd); /* parent closes connected socket */ 32 } 33 }
str_echo函数
1 #include "unp.h" 2 3 void 4 str_echo(int sockfd) 5 { 6 ssize_t n; 7 char buf[MAXLINE]; 8 9 again: 10 while ( (n = read(sockfd, buf, MAXLINE)) > 0) 11 Writen(sockfd, buf, n); 12 13 if (n < 0 && errno == EINTR) 14 goto again; 15 else if (n < 0) 16 err_sys("str_echo: read error"); 17 }
TCP回射客户程序
1 #include "unp.h" 2 3 int 4 main(int argc, char **argv) 5 { 6 int sockfd; 7 struct sockaddr_in servaddr; 8 9 if (argc != 2) 10 err_quit("usage: tcpcli <IPaddress>"); 11 12 sockfd = Socket(AF_INET, SOCK_STREAM, 0); 13 14 bzero(&servaddr, sizeof(servaddr)); 15 servaddr.sin_family = AF_INET; 16 servaddr.sin_port = htons(SERV_PORT); 17 Inet_pton(AF_INET, argv[1], &servaddr.sin_addr); 18 19 Connect(sockfd, (SA *) &servaddr, sizeof(servaddr)); 20 21 str_cli(stdin, sockfd); /* do it all */ 22 23 exit(0); 24 }
str_cli函数
1 #include "unp.h" 2 3 void 4 str_cli(FILE *fp, int sockfd) 5 { 6 char sendline[MAXLINE], recvline[MAXLINE]; 7 8 while (Fgets(sendline, MAXLINE, fp) != NULL) { 9 10 Writen(sockfd, sendline, strlen(sendline)); 11 12 if (Readline(sockfd, recvline, MAXLINE) == 0) 13 err_quit("str_cli: server terminated prematurely"); 14 15 Fputs(recvline, stdout); 16 } 17 }
正常启动
在后台启动服务器
服务器启动后,它将阻塞于accept调用。运行netstat程序来检查服务器监听套接字状态
端口9877是我们服务器使用的端口,netstat用星号“*”表示一个为0的IP地址(INADDR_ANY 通配地址)或为0的端口号
使用环回地址启动客户端
客户端程序调用socket和connest,将阻塞带fgets调用。
服务器中的accept返回,然后调用fork,子进程调用str_echo,阻塞于read函数。父进程再次调用accept并阻塞。
此时,我们有3个都在睡眠(即已阻塞):客户进程、服务器父进程和服务器子进程
使用nestat给出对应所建立TCP连接。
第一个是服务器父进程,第二个是客户进程,第三个是服务器子进程。
正常终止程序
我们输入两行数据,每行都得到回射。我们接着键入终端EOF字符Ctrl+D以终止客户(导致fgets返回一个空指针)
POSIX信号处理
关于信号我们可以查看以前写的apue学习笔记 http://www.cnblogs.com/runnyu/p/4641346.html
关于进程可以查看 http://www.cnblogs.com/runnyu/p/4638913.html
这是后面章节的基本知识(例如signal、wait函数)
处理SIGCHLD信号
设置僵死状态的目的是维护子进程的信息,以便父进程在以后某个时候获取。
处理僵死进程
在一个进程终止或者停止时,将SIGCHLD信号发送给其父进程。按系统默认将忽略此信号。
我们可以在listen调用之后捕获SIGCHLD信号用来处理僵死进程
1 Signal(SIGCHLD,sig_chld); 2 3 4 #include "unp.h" 5 6 void 7 sig_chld(int signo) 8 { 9 pid_t pid; 10 int stat; 11 12 pid = wait(&stat); 13 printf("child %d terminated\n", pid); 14 return; 15 }
处理被中断的系统调用
当阻塞与某个慢系统调用(如accept,read)的一个进程捕获某个信号且相应信号处理函数返回时,该系统调用可能返回EINTR错误。
有些内核自动重启某些被中断的系统调用。不过为了便于移植,我们在编写程序时必须对慢系统调用返回EINTR有所准备。
例如,为了处理被中断的accept。我们把上面accept的调用改成如下所示
for ( ; ; ) { clilen = sizeof(cliaddr); if((connfd=accept(listenfd,(SA *)&cliaddr,&clilen))<0){ if(errno==EINTR) continue;/* back to for() */ else err_sys("accept error"); }
这段代码所做的事情就是自己重启被中断的系统调用
wait和waipid
建立一个信号处理函数并在其中调用wait并不足以防止出现僵死进程。
考虑5个客户端同时结束,服务器子进程同时发送5次SIGCHLD信号,因为UNIX信号一般是不排队的,因此信号处理函数可能只执行一次,而留下4个僵死进程。
正确的解决办法是调用waitpid而不是wait,下面给出正确处理SIGCHLD的sig_chld函数。
1 #include "unp.h" 2 3 void 4 sig_chld(int signo) 5 { 6 pid_t pid; 7 int stat; 8 9 while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0) 10 printf("child %d terminated\n", pid); 11 return; 12 }
我们在一个循环内调用waitpid(-1代表等待任意子进程),以获取所有已终止子进程的状态。
我们必须指定WNOHANG选项,它告知waitpid在尚未终止的子进程在运行时不要阻塞。
下面给出我们的服务器程序的最终版本
1 #include "unp.h" 2 3 int 4 main(int argc, char **argv) 5 { 6 int listenfd, connfd; 7 pid_t childpid; 8 socklen_t clilen; 9 struct sockaddr_in cliaddr, servaddr; 10 void sig_chld(int); 11 12 listenfd = Socket(AF_INET, SOCK_STREAM, 0); 13 14 bzero(&servaddr, sizeof(servaddr)); 15 servaddr.sin_family = AF_INET; 16 servaddr.sin_addr.s_addr = htonl(INADDR_ANY); 17 servaddr.sin_port = htons(SERV_PORT); 18 19 Bind(listenfd, (SA *) &servaddr, sizeof(servaddr)); 20 21 Listen(listenfd, LISTENQ); 22 23 Signal(SIGCHLD, sig_chld); /* must call waitpid() */ 24 25 for ( ; ; ) { 26 clilen = sizeof(cliaddr); 27 if ( (connfd = accept(listenfd, (SA *) &cliaddr, &clilen)) < 0) { 28 if (errno == EINTR) 29 continue; /* back to for() */ 30 else 31 err_sys("accept error"); 32 } 33 34 if ( (childpid = Fork()) == 0) { /* child process */ 35 Close(listenfd); /* close listening socket */ 36 str_echo(connfd); /* process the request */ 37 exit(0); 38 } 39 Close(connfd); /* parent closes connected socket */ 40 } 41 }
服务器进程终止
1.我们在同一个主机上启动服务器和客户端,并在客户上键入一行文本,以验证一切正常。
2.找到服务器子进程的进程ID,并执行kill命令杀死它。这导致向客户发送一个FIN,而客户则相应以一个ACK。
3.SIGCHLD信号被发送给服务器父进程,并得到正确处理
4.客户端接收来自服务器TCP的FIN并相应一个ACK,然后问题是客户进程阻塞在fgets调用上,等待从终端接收一行文本。
5.键入netstat命令,以观察套接字状态
可以看到,TCP连接中止序列的前半部分已经完成
6.在客户上再键入一行文本
str_cli调用writen,客户TCP接着把数据发送给服务器。
TCP允许这么做,因为客户TCP接收到FIN只是表示服务器进程已关闭了连接的服务器端,从而不再往其中发送任何数据而已。FIN的接收并没有告知客户TCP服务器进程已经终止。
当服务器TCP接收到来自客户的数据时,既然先前打开那个套接字的进程已经终止,于是相应以一个RST
然而客户进程看不到这个RST,因为它在调用writen后立即调用readlind,并且由于第二步接收的FIN,所调用的readline立即返回0。
于是以出错信息“server terminated prematurely”退出,客户端终止,关闭所有打开的描述符。
当FIN到达套接字时,客户正阻塞在fgets调用上。客户实际上在应对两个描述符--套接字和用户输入。
事实上程序不应该阻塞到两个源中某个特定源的输入上,而是应该阻塞在其中任何一个源的输入上,这正是select和poll这两个函数的目的之一。
下一章我们将重新编写str_cli函数:一旦杀死服务器子进程,客户就会立即被告知已收到的FIN。
SIGPIPE信号
当一个进程向某个已收到RST的套接字执行写操作(返回EPIPE错误)时,内核向该进程发送一个SIGPIPE信号。该信号的默认行为是终止进程,因此进程必须捕获它以免不情愿地被终止。
服务器主机崩溃
使用下面步骤来模拟服务器崩溃:
在不同主机上运行客户和服务器。先启动服务器,再启动客户(键入一行文本以确定连接工作正常),然后从网络上断开服务器主机,在客户上键入另一行文本。
1.当服务器主机崩溃时,已有的网络连接上不发出任何东西。
2.我们在客户上键入一行文本,它由writen写入内核,再由客户TCP作为一个数据分节送出。客户随后阻塞于readline调用。
3.客户TCP持续重传数据分节,试图从服务器上接收一个ACK。如果在放弃重传前服务器主机没有重新启动,则客户进程返回一个错误。
既然客户阻塞在readline调用上,该调用将返回一个错误。假设服务器主机已崩溃,从而客户的数据分节根本没有相应,那么所返回的错误是ETIMEOUT。
然后如果某个中间路由器判断服务器主机已不可达,从而相应一个“destination unreachable”ICMP消息,那么所返回的错误是EHOSTUNREACH或ENETUNREACH。
服务器主机崩溃后重启
当服务器主机崩溃后重启时,它的TCP丢失了崩溃前的所有连接信息,因此服务器TCP对于所收到的来自客户的数据分节相应一个RST
当客户TCP收到该RST时,客户正阻塞与readline调用,导致调用返回ECONNERESET错误。
服务器主机关机
UNIX系统关机时,init进程通常先给所有进程发送SIGTERM信号(可以被捕获),等待一段固定的时间,然后给所有还在运行的进程发送SIGKILL信号(不能捕获)。
当服务器进程接收到信号终止时,它所打开的描述符都被关闭,随后发生与上面服务器进程终止所讨论的一样。
例子:在客户与服务器之间传递文本串
对两个数求和的str_echo函数
1 #include "unp.h" 2 3 void 4 str_cli(FILE *fp, int sockfd) 5 { 6 char sendline[MAXLINE], recvline[MAXLINE]; 7 8 while (Fgets(sendline, MAXLINE, fp) != NULL) { 9 10 Writen(sockfd, sendline, strlen(sendline)); 11 12 if (Readline(sockfd, recvline, MAXLINE) == 0) 13 err_quit("str_cli: server terminated prematurely"); 14 15 Fputs(recvline, stdout); 16 } 17 }
例子:在客户与服务器之间传递二进制结构
头文件sum.h
1 struct args { 2 long arg1; 3 long arg2; 4 }; 5 6 struct result { 7 long sum; 8 };
发送两个二进制整数给服务器的str_cli函数
1 #include "unp.h" 2 #include "sum.h" 3 4 void 5 str_cli(FILE *fp, int sockfd) 6 { 7 char sendline[MAXLINE]; 8 struct args args; 9 struct result result; 10 11 while (Fgets(sendline, MAXLINE, fp) != NULL) { 12 13 if (sscanf(sendline, "%ld%ld", &args.arg1, &args.arg2) != 2) { 14 printf("invalid input: %s", sendline); 15 continue; 16 } 17 Writen(sockfd, &args, sizeof(args)); 18 19 if (Readn(sockfd, &result, sizeof(result)) == 0) 20 err_quit("str_cli: server terminated prematurely"); 21 22 printf("%ld\n", result.sum); 23 } 24 }
对两个二进制整数求和的str_echo函数
1 #include "unp.h" 2 #include "sum.h" 3 4 void 5 str_echo(int sockfd) 6 { 7 ssize_t n; 8 struct args args; 9 struct result result; 10 11 for ( ; ; ) { 12 if ( (n = Readn(sockfd, &args, sizeof(args))) == 0) 13 return; /* connection closed by other end */ 14 15 result.sum = args.arg1 + args.arg2; 16 Writen(sockfd, &result, sizeof(result)); 17 } 18 }