zoukankan      html  css  js  c++  java
  • UNIX 网络编程第五章读书笔记

    刚看完 UNIX 第五章内容,我想按照自己的方式将自己获得的知识梳理一遍,以便日后查看!先贴上一段简单的 TCP 服务器端代码:

     1 #include <sys/socket.h>
     2 #include <netinet/in.h>
     3 #include <stdio.h>
     4 #include <error.h>
     5 #include <unistd.h>
     6 #include <string.h>
     7 #include <stdlib.h>
     8 
     9 #define MAXLINE 5
    10 #define SA struct sockaddr
    11 int main()
    12 {
    13     int listenfd, connfd;
    14     pid_t childpid;
    15     int readn, writen;
    16     socklen_t clilen;
    17     char buf[MAXLINE];
    18     struct sockaddr_in servaddr, cliaddr;
    19     //创建监听套接字
    20     if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    21     {
    22         printf("socket() error!");
    23         exit(0);
    24     }
    25     //先要对协议地址进行清零
    26     bzero(&servaddr,sizeof(servaddr));
    27     //设置为 IPv4 or IPv6
    28     servaddr.sin_family = AF_INET;
    29     //绑定本地端口号
    30     servaddr.sin_port    = htons(9804);
    31     //任何一个 IP 地址,让内核自行选择
    32     servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    33     //绑定套接口到本地协议地址
    34     if(bind(listenfd, (SA *) &servaddr,sizeof(servaddr)) < 0)
    35     {
    36         printf("bind() error!");
    37         exit(0);
    38     }
    39     //服务器开始监听
    40     if(listen(listenfd,5) < 0)
    41     {
    42         printf("listen() error!");
    43         exit(0);
    44     }
    45     for(;;)
    46     {
    47         clilen = sizeof(cliaddr);
    48         //accept 的后面两个参数都是值-结果参数,他们的保留的远程连接电脑的信息,如果不管新远程连接电脑的信息,可以将这两个参数设置为 NULL
    49         connfd = accept(listenfd, (SA *) &cliaddr, &clilen);
    50         if(connfd < 0)
    51         {
    52             continue;
    53         }
    54         //我们采用 TCP 并发服务器模式,每个客户一个进程
    55         if((childpid = fork()) == 0)
    56         {
    57             //子进程关闭 listenfd:因为父子进程都各拥有一个 listefd,而子进程不负责监听,所以关闭 listenfd,以免浪费资源!
    58             close(listenfd);
    59             //子进程做的事很简单,从套接字上读取数据然后再写回
    60             while((readn = read(connfd, buf,MAXLINE)) > 0)
    61             {
    62                 writen = write(connfd, buf, readn);
    63                 if(writen < 0)
    64                 {
    65                     printf("writen() error!");
    66                     continue;
    67                 }
    68                 else    
    69                 {
    70                     printf("write %d bytes!
    ", writen);
    71                 }    
    72             }
    73             exit(0);
    74         }
    75         //父进程关闭 connfd,类似的父进程只负责监听,不需要 connfd
    76         close(connfd);
    77     }
    78 
    79 }

      以上是一个最基本的 TCP 服务器端代码,其功能很简单:一旦一个客户发起连接,服务器端就 fork() 一个进程为其服务,该进程从其所拥有的连接套接字读数据然后再将数据写回套接字。客户端运行效果如下:

      

      当然,我们不是简简单单的要说明这样一个 TCP 服务器,稍微看过 TCP 套接口编程的都能写出以上程序。我们要讨论的是由该程序所涉及到的和即将要涉及到的各个知识点。

     TCP 三次握手协议和四次分手协议我已在以前的一篇博文中TCP连接的建立和终止中描述过,这里我们不再赘述!我们现在用此实例来演示一遍 TCP 连接的建立与终止。我们在本机运行以上代码:

      

      我们查看本机 9804 端口的状态:netstat -a | grep 9804

      

      就会发现此端口处于 listen---监听状态。这时候的服务器端代码阻塞于 listen() 函数,等待客户端发起连接。我们再运行客户端,并输入测试字符,以说明连接正常。

      

      运行 netstat 查看与 9804 端口有关的端口的状态:netstat -a | grep 9804

      

      可以看到与 9804 端口有关的连接对都处于 ESTABLISHED 状态。说明客户端与服务器端已经完成 TCP 连接的建立,可以进行数据的发送了。此时我们查看进程信息,可以看到如下的服务器端为客户端创建了一个处理进程:

      

      其中 7290 与 7293 都是子进程,7009 是 ./myserver 进程的父进程号!现在我们演示 TCP 连接的终止:我们在同时按下 ctrl 和 D(相当于发送 EOF 给服务器进程),此时,我们再观察进程信息:

      

      我们会发现进程号为 7293 的进程的状态是 Z+,也就是所谓的僵尸进程。为什么?我们按下 CTRL 和 D向服务器发送 EOF ,服务器代码 的 read() 函数返回 0,此时 while 循环退出,执行 exit(0) 函数,该子进程终止,子进程终止的时候会向父进程发送 SIGCHLD 信号,但是我们在服务器代码中未捕获该信号,终止进程未被及时处理,即成为僵尸进程。僵尸进程由进程号为 1 的 Init 进程代为管理

      以上,我们可以大概的了解到 TCP 连接的建立与终止的过程以及僵尸进程形成的原因。我们知道,僵尸进程是无用的进程且其会消耗系统资源,所以我们应该及时处理僵尸进程。之前我们说过,子进程终止的时候会向父进程发送 SIGCHLD 信号,我们先说说什么是信号?以下摘抄自 UNP 5.8节;

      信号就是通知某个进程发生了某个事件,有时也成为软件中断。信号通常异步发送,也就是进程预先不知到信号准确发生时间。信号可以 

        . 由一个进程发送给另外一个进程。

        . 由内核发给某个进程。

      那么,进程收到信号该怎么办呢?有以下三种选择:

        1.提供一个函数,它将在特定信号发生的时候被调用,这样的函数我们称为信号处理,这种行为我们称为信号捕获。所有的信号处理函数原型都是:void handler(int singo)

        2.将该信号设置为 SIG_IGN 来忽略它。SIGKILL 和 SIGSTOP 这两个信号不能被忽略。

        3.可以将信号的处置设定为 SIG_DFI 来启用它的缺省处置。

      接下来,我们说下如何处理上面说的 SIGCHLD 信号:既然会形成僵尸进程,我们肯定不能再忽略该信号,而是选择捕获它。给该信号安装一个执行函数,那么该怎么安装呢?直接调用系统函数 signal(),该函数有两个参数,第一个参数就是你要监视的信号,第二个参数就是执行函数的函数指针。如是,我们修改服务器端代码如下:

     1 #include <sys/socket.h>
     2 #include <netinet/in.h>
     3 #include <stdio.h>
     4 #include <error.h>
     5 #include <unistd.h>
     6 #include <string.h>
     7 #include <stdlib.h>
     8 #include <sys/wait.h>
     9 #include <signal.h>
    10 #define MAXLINE 5
    11 #define SA struct sockaddr
    12 
    13 void sig_child(int signo)
    14 {
    15     pid_t pid;
    16     int stat;
    17     pid = wait(&stat);
    18     printf("child %d terminated
    ", pid);
    19     return;
    20 }
    21 int main()
    22 {
    23     int listenfd, connfd;
    24     pid_t childpid;
    25     int readn, writen;
    26     socklen_t clilen;
    27     char buf[MAXLINE];
    28     struct sockaddr_in servaddr, cliaddr;
    29     //创建监听套接字
    30     if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    31     {
    32         printf("socket() error!");
    33         exit(0);
    34     }
    35     //先要对协议地址进行清零
    36     bzero(&servaddr,sizeof(servaddr));
    37     //设置为 IPv4 or IPv6
    38     servaddr.sin_family = AF_INET;
    39     //绑定本地端口号
    40     servaddr.sin_port    = htons(9805);
    41     //任何一个 IP 地址,让内核自行选择
    42     servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    43     //绑定套接口到本地协议地址
    44     if(bind(listenfd, (SA *) &servaddr,sizeof(servaddr)) < 0)
    45     {
    46         printf("bind() error!");
    47         exit(0);
    48     }
    49     //服务器开始监听
    50     if(listen(listenfd,5) < 0)
    51     {
    52         printf("listen() error!");
    53         exit(0);
    54     }
    55     signal(SIGCHLD, sig_child);
    56     for(;;)
    57     {
    58         clilen = sizeof(cliaddr);
    59         //accept 的后面两个参数都是值-结果参数,他们的保留的远程连接电脑的信息,如果不管新远程连接电脑的信息,可以将这两个参数设置为 NULL
    60         connfd = accept(listenfd, (SA *) &cliaddr, &clilen);
    61         if(connfd < 0)
    62         {
    63             continue;
    64         }
    65         //我们采用 TCP 并发服务器模式,每个客户一个进程
    66         if((childpid = fork()) == 0)
    67         {
    68             //子进程关闭 listenfd:因为父子进程都各拥有一个 listefd,而子进程不负责监听,所以关闭 listenfd,以免浪费资源!
    69             close(listenfd);
    70             //子进程做的事很简单,从套接字上读取数据然后再写回
    71             while((readn = read(connfd, buf,MAXLINE)) > 0)
    72             {
    73                 writen = write(connfd, buf, readn);
    74                 if(writen < 0)
    75                 {
    76                     printf("writen() error!");
    77                     continue;
    78                 }
    79                 else    
    80                 {
    81                     printf("write %d bytes!
    ", writen);
    82                 }    
    83             }
    84             exit(0);
    85         }
    86         //父进程关闭 connfd,类似的父进程只负责监听,不需要 connfd
    87         close(connfd);
    88     }
    89 
    90 }

      对比之前版本,我们只增加了 13 - 20 行的信号处理函数与 55 行的信号捕获代码。再次运行服务器端代码(这次我们服务器程序取名叫:myserver9805),查看进程信息如下:

       

      可以看到两个 ./myserver9805 一个是父进程,其 pid 为 9101,另一个是子进程其 pid 为 9111,其父进程为 9101。其中的 ./myclient9805 127.0.0.1 是客户端进程。现在我们再次在客户端按下 CTRL + D:

      

      服务器端进程会调用捕获函数,打印

        child 9111 terminated

      再次查看进程信息,如下:

      

      可以看到,此时没有形成僵尸进程!(请忽略进程编号为 7293 的僵尸进程,这是之前留下的,囧!)。至此,SIGCHLD 信号处理完毕,其实也很简单嘛。现在我们回头看看信号捕获函数:

    13 void sig_child(int signo)
    14 {
    15     pid_t pid;
    16     int stat;
    17     pid = wait(&stat);
    18     printf("child %d terminated
    ", pid);
    19     return;
    20 }
      其中调用了 wait() 函数,该函数自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。而值-结果参数 stat 保留的就是 wait 函数收集到的进程信息。
      说到 wait() 函数,我们不得不说一下 waitpid() 函数,因为实际过程中用该函数替代 wait() 函数。我们修改客户端程序,让其建立 5 个连接,也就是说服务器端会创建 5 个进程来处理这些连接,查看进程信息,如下:
      

      可以看出,一共有 5 个进程,父进程 PID 是 9101。此时我们在客户端再按下 CTRL + D,再次查看进程信息:
      
      发现,还是会有一个僵尸进程,为什么呢?因为当父进程同时收到很多 SIGCHLD 信号时 信号函数只执行一次或多次,但是不能肯定一定是 5 次,所以会导致还会有僵尸进程。基于此原因,我们用 waitpid() 代替 wait() 函数,修改信号捕获函数如下:
    void sig_child(int signo)
    {
      pid_t pid;
      int stat;
      while(pid = waitpid(-1, &stat, WNOHANG) > 0)
        printf("child %d terminated ",pid);
    }
      看到如上代码,可能会有人会说将上面的 waitpid 换成 wait 不也行。可我们不要忘了当有尚未终止的子进程时会阻塞,而 waitpid 函数可以通过设置第三个参数为 WNOHANG 来告知 waitpid ,当有尚未终止的子进程的时候不要阻塞,所以就可以通过循环处理所有终止的子进程,故不会有僵尸进程残留!
      以上我们通过该简单 TCP 服务器代码简单的讲解了信号有关的知识点,让我们初窥信号,当然更具体的知识点应该去拜读 UNP 这本书,这里只是起个抛砖引玉的作用!接下来我们再依此介绍 I/O 复用模型。UNP 第六章介绍了五种 I/O 模型,具体如下:
        1.阻塞 I/O 模型。
        2.非阻塞 I/O 模型。
        3.I/O 复用模型。 
        4.信号驱动模型。
        5.异步 I/O 模型。
      我们主要介绍 1 和 3,其他 3 中类型不在本文范围内。还是以上面那个 TCP 服务器代码作切入点:再次运行该代码,并运行客户端,发送 Hello,world!测试连接正常:
      
      现在客户端停留在等待输入的界面,我们在另一个终端杀死服务器端的子进程:
      
      发现客户端没有任何反映。现在,我们尝试在客户端再次输入: another line,结果返回 server terminated prematurely!:
      
      为什么会这样子?这里我们先贴出客户端代码,借用 UNP 5.4 节提供的客户端代码的:
     1 #include <sys/socket.h>
     2 #include <netinet/in.h>
     3 #include <stdio.h>
     4 #include <error.h>
     5 #include <unistd.h>
     6 #include <string.h>
     7 #include <stdlib.h>
     8 #include <sys/wait.h>
     9 #include <signal.h>
    10 #define MAXLINE 5
    11 #define SA struct sockaddr
    12 
    13 void str_cli(FILE *fp, int sockfd)
    14 {
    15     char sendline[MAXLINE], recvline[MAXLINE];
    16     while(fgets(sendline,MAXLINE,fp) != NULL)
    17     {
    18         writen(sockfd, sendline, strlen(sendline));
    19         if(readline(sockfd, recvline, MAXLINE) == 0)
    20         {
    21             printf("str_cli:server terminated prematurely!
    ");
    22             exit(0);
    23         }
    24         fputs(recvline, stdout);
    25     }
    26 }
    27 int main(int argc, char **argv)
    28 {
    29     int sockfd;
    30     struct sockaddr_in servaddr;
    31     if(argc != 2)
    32     {
    33         printf("useage: tcpcli <IPaddress>");
    34         exit(0);
    35     }
    36     if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    37     {
    38         printf("socket() error!");
    39         exit(0);
    40     }
    41     bzero(&servaddr, sizeof(servaddr));
    42     servaddr.sin_family = AF_INET;
    43     servaddr.sin_port = htons(9806);
    44     
    45     if(inet_pton(AF_INET, argv[1], &servaddr.sin_addr) < 0)
    46     {
    47         printf("inet_pton() error!");
    48         exit(0);
    49     }
    50     if(connect(sockfd, (SA *) &servaddr, sizeof(servaddr)) < 0)
    51     {
    52         printf("inet_pton() error!");
    53         exit(0);
    54     }
    55     str_cli(stdin, sockfd);
    56     exit(0);
    57 }
    View Code
        1. 当我们杀死子进程,服务器子进程会向客户端发送一个 FIN,而客户端则响应一个 ACK。这是 TCP 连接终止的前一半工作。
        2. SIGCHLD 信号发送给服务器进程,并得到正确处理。
        3. 客户端上没有发生任何特殊的事情。虽然客户端响应了一个 ACK,然而客户端进程阻塞在 fgets 调用上 (见以上客户端代码),等待从终端接受一行文本。所以没有任何反应。
        4. 此时,我们用 netstat 查看套接口状态:
        (一个netstat 是在杀死子进程之前运行一个是在杀死子进程之后运行的)
        上图很完全符合 TCP 连接终止过程的状态变化。因为只完成了 TCP 连接终止的前一半,所以主动关闭的一方(这里是服务器端)处于 FIN_WAIT2 状态,被动关闭一方(这里是客户端)处于 CLOSE_WAIT 状态。
        5.我们在客户端上键入 another line ,返回: str_cli: server terminated prematurely 。当我们键入 another line 的时候,客户端调用 writen,客户 TCP 接着把数据发送给服务器。TCP 允许这么做,因为 TCP 收到 FIN 只是表示服务器进程已经关闭了连接端的服务器端,从而不再往其中发送任何数据而已。不代表连接的客户端不能往连接中发送数据。FIN 的接收并没有告知客户端 TCP 服务器端进程已经终止(本例中确实已经终止)。当服务器 TCP 接收到来自客户端的数据时,既然先前打开那个套接口的进程已经终止,于是响应一个 RST。
        6.然而客户端进程看不到这个 RST ,因为它在调用 writen 后立即调用 readline,并且由于第 1 步接受的 FIN,所调用的 readline 立即返回 0(表示 EOF),所以执行 if 中代码,输出错误信息:server terminated prematurely(服务器进程过早终止)。然后退出!
        7.客户端终止时,它所有打开着的描述字都被关闭。

      从上个例子可以看出,服务器端终止的时候,客户端并没有及时的感觉到。问题在于:当 FIN 到达套接口的时候,客户正阻塞在 fgets 调用上。客户实际上在应对两个描述字---套接口描述字和用户数如,它不能单纯的阻塞在两个源中的某个特定源上。这时候,我们希望服务器进程需要一种预先告知内核内核的能力,使得进程指定的一个或多个 I/O 条件就绪(也就是说输入已经准备好读取,或者描述字已经能承接更多的输出),它就通知进程。这个能力称为 I/O 复用,是由 select 和 poll 和更高级的 epoll 支持的。到这里,我们终于搞清楚什么是 I/O 复用了。。。 I/O 复用典型使用下列网络应用场合:
        1.当客户处理多个描述字(通常是交互式输入和网络套机口),必须使用 I/O 复用。
        2.一个客户同时处理多个套接口是可能的,不过比较少见。
        3.如果一个 TCP 服务器端既要处理监听套接字又要处理已连接套接口,一般要用 I/O 复用。
        4.如果一个服务器既要处理 TCP,又要处理 UDP,一般使用 I/O 复用。
        5.如果一个服务器要处理多个服务或者协议,一般就要用 I/O 复用。
      限于篇幅,我们将会单独用一篇博客来写 select、poll 和 epoll 有关的内容。
  • 相关阅读:
    打印九九乘法表
    PAT (Basic Level) Practice (中文) 1091 N-自守数 (15分)
    PAT (Basic Level) Practice (中文)1090 危险品装箱 (25分) (单身狗进阶版 使用map+ vector+数组标记)
    PAT (Basic Level) Practice (中文) 1088 三人行 (20分)
    PAT (Basic Level) Practice (中文) 1087 有多少不同的值 (20分)
    PAT (Basic Level) Practice (中文)1086 就不告诉你 (15分)
    PAT (Basic Level) Practice (中文) 1085 PAT单位排行 (25分) (map搜索+set排序+并列进行排行)
    PAT (Basic Level) Practice (中文) 1083 是否存在相等的差 (20分)
    PAT (Basic Level) Practice (中文) 1082 射击比赛 (20分)
    PAT (Basic Level) Practice (中文) 1081 检查密码 (15分)
  • 原文地址:https://www.cnblogs.com/zhuwbox/p/4214194.html
Copyright © 2011-2022 走看看