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 }
View Code

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 }
View Code

 

 

 

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 }
View Code

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 }
View Code

 

 

 

正常启动

在后台启动服务器

服务器启动后,它将阻塞于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 }
View Code

 

 

 

处理被中断的系统调用

当阻塞与某个慢系统调用(如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 }
View Code

我们在一个循环内调用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 }
View Code

 

 

 

服务器进程终止

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 }
View Code

 

 

例子:在客户与服务器之间传递二进制结构

头文件sum.h

1 struct args {
2   long    arg1;
3   long    arg2;
4 };
5 
6 struct result {
7   long    sum;
8 };
View Code

发送两个二进制整数给服务器的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 }
View Code

对两个二进制整数求和的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 }
View Code

 

posted @ 2015-07-19 19:56  Runnyu  阅读(874)  评论(0编辑  收藏  举报