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 有关的内容。
  • 相关阅读:
    C语言,C++,static
    英语自我介绍
    总线接口与计算机通信(五)CAN总线
    总线接口与计算机通信(二)SPI总线
    总线接口与计算机通信(一)I2C总线
    总线接口与计算机通信
    C语言函数指针
    服务器体系(SMP, NUMA, MPP)与共享存储器架构(UMA和NUMA)
    实时调度类
    Linux CFS调度器之唤醒抢占--Linux进程的管理与调度(三十)
  • 原文地址:https://www.cnblogs.com/zhuwbox/p/4214194.html
Copyright © 2011-2022 走看看