注:博主只关注编程实现的方面以及linux部分,部分网络原理讲解和windows实现部分跳过

第 9 章 套接字的多种可选项

9.1 套接字可选项和 I/O 缓冲大小

  • Q:套接字有哪些常用设置选项?

  • Q:用于读取套接字可选项的函数getsockopt

    • #include <sys/socket.h>
      int getsockopt(int sock, int level, int optname,
                     void *optval, socklen_t *optlen);
      // sock:用于查看选项套接字文件描述符
      // level:要查看的可选项的协议层
      // optname:要查看的可选项名
      // optval:保存查看结果的缓冲地址值
      // optlen:向第四个参数optval传递的缓冲大小。调用函数后,
      //        该变量中保存通过第四个参数返回的可选项信息的字节数
      // 成功时返回0,失败时返回-1
      
  • Q:用于更改套接字可选项的函数setsockopt

    • #include <sys/socket.h>
      int setsockopt(int sock, int level, int optname,
                     const void *optval, socklen_t optlen);
      // sock:用于更改选项套接字文件描述符
      // level:要更改的可选项的协议层
      // optname:要更改的可选项名
      // optval:保存要更改的选项信息的缓冲地址值
      // optlen:向第四个参数optval传递的可选项信息的字节数
      // 成功时返回0,失败时返回-1
      
  • Q:用协议层为SOL_SOCKET、名为SO_TYPE的可选项查看套接字类型(TCP或UDP)的程序示例sock_type.c

    • #include <stdio.h>
      #include <stdlib.h>
      #include <unistd.h>
      #include <sys/socket.h>
      
      void error_handling(char * message)
      {
          fputs(message, stderr);
          fputc('\n', stderr);
          exit(1);
      }
      
      int main(int argc, char *argv[])
      {
          int tcp_sock, udp_sock;
          int sock_type;
          socklen_t optlen;
          int state;
      
          optlen = sizeof(sock_type);
          // 分别生成TCP、UDP套接字
          tcp_sock = socket(PF_INET, SOCK_STREAM, 0);
          udp_sock = socket(PF_INET, SOCK_DGRAM, 0);
          // 输出创建TCP、UDP套接字时传入的SOCK_STREAM、SOCK_DGRAM
          printf("SOCK_STREAM: %d\n", SOCK_STREAM);
          printf("SOCK_DGRAM: %d\n", SOCK_DGRAM);
      
          // 获取套接字类型信息
          // 如果是TCP套接字,将获得SOCK_STREAM常数值1
          state = getsockopt(tcp_sock, SOL_SOCKET, SO_TYPE,
                             (void*)&sock_type, &optlen);
          if (state) {
              error_handling("getsockopt() error!");
          }
          printf("Socket type one: %d\n", sock_type);
      
          // 如果是UDP套接字,则获得SOCK_DGRAM的常数值2
          state = getsockopt(tcp_sock, SOL_SOCKET, SO_TYPE,
                             (void*)&sock_type, &optlen);
          if (state) {
              error_handling("getsockopt() error!");
          }
          printf("Socket type two: %d\n", sock_type);
      
          return 0;
      }
      
    • 编译运行

    • shiqi@pc:~/network/ch09$ gcc sock_type.c -o socktype
      shiqi@pc:~/network/ch09$ ./socktype
      SOCK_STREAM: 1
      SOCK_DGRAM: 2
      Socket type one: 1
      Socket type two: 1
      
  • Q:读取创建套接字时默认的I/O缓冲大小的实例程序get_buf.c

    • #include <stdio.h>
      #include <stdlib.h>
      #include <unistd.h>
      #include <sys/socket.h>
      
      void error_handling(char * message)
      {
          fputs(message, stderr);
          fputc('\n', stderr);
          exit(1);
      }
      
      int main(int argc, char *argv[])
      {
          int sock;
          int snd_buf, rcv_buf, state;
          socklen_t len;
      
          sock = socket(PF_INET, SOCK_STREAM, 0);
          len = sizeof(snd_buf);
          state = getsockopt(sock, SOL_SOCKET, SO_SNDBUF,
                             (void *)&snd_buf, &len);
          if (state) {
              error_handling("getsockopt() error");
          }
          printf("Output buffer size: %d\n", snd_buf);
      
          len = sizeof(rcv_buf);
          state = getsockopt(sock, SOL_SOCKET, SO_RCVBUF,
                             (void *)&rcv_buf, &len);
          if (state) {
              error_handling("getsockopt() error");
          }
          printf("Input buffer size: %d\n", rcv_buf);
      
          return 0;
      }
      
    • 编译运行

    • shiqi@pc:~/network/ch09$ gcc get_buf.c -o getbuf
      shiqi@pc:~/network/ch09$ ./getbuf
      Output buffer size: 16384
      Input buffer size: 131072
      
  • Q:更改套接字I/O缓冲大小的示例程序set_buf.c

    • #include <stdio.h>
      #include <stdlib.h>
      #include <unistd.h>
      #include <sys/socket.h>
      
      void error_handling(char * message)
      {
          fputs(message, stderr);
          fputc('\n', stderr);
          exit(1);
      }
      
      int main(int argc, char *argv[])
      {
          int sock;
          int snd_buf = 1024*3, rcv_buf = 1024*3;
          int state;
          socklen_t len;
      
          sock = socket(PF_INET, SOCK_STREAM, 0);
          // 更改 I/O 接收缓冲大小
          state = setsockopt(sock, SOL_SOCKET, 
                             SO_RCVBUF, (void *)&rcv_buf, 
                             sizeof(rcv_buf));
          if (state) {
              error_handling("setockopt() error!");
          }
      
          // 更改 I/O 发送缓冲大小
          state = setsockopt(sock, SOL_SOCKET, SO_SNDBUF, 
                             (void *)&snd_buf, sizeof(snd_buf));
          if (state) {
              error_handling("setockopt() error!");
          }
      
          len = sizeof(snd_buf);
          // 读取 I/O 发送缓冲大小
          state = getsockopt(sock, SOL_SOCKET, SO_SNDBUF,
                             (void *)&snd_buf, &len);
          if (state) {
              error_handling("getsockopt() error");
          }
          printf("Output buffer size: %d\n", snd_buf);
      
          len = sizeof(rcv_buf);
          // 读取 I/O 接收缓冲大小
          state = getsockopt(sock, SOL_SOCKET, SO_RCVBUF,
                             (void *)&rcv_buf, &len);
          if (state) {
              error_handling("getsockopt() error");
          }
          printf("Input buffer size: %d\n", rcv_buf);
      
          return 0;
      }
      
    • 编译运行

    • shiqi@pc:~/network/ch09$ gcc set_buf.c -o setbuf
      shiqi@pc:~/network/ch09$ ./setbuf
      Output buffer size: 6144
      Input buffer size: 6144
      

9.2 SO_REUSEADDR

  • Q:Time-wait状态下,服务器发生bind() error错误程序示例reuseadr_eserver.c
    • 如果在服务器端和客户端已建立连接的状态下,向服务器端控制台输入CTRL+C,即强制关闭服务器端。相当于模拟服务器端向客户端发送 FIN 消息。以这种方式终止程序,服务端若用统一端口号重新运行,将输出「bind() error」,无法再次运行,这种情况下,大约过3分钟即可重新运行服务器端

    • 解决方案可在套接字的可选项中更改 SO_REUSEADDR 为1,可将 Time-wait 状态下的套接字端口号重新分配给新的套接字,把程序中的注释去掉即可

    • #include <stdio.h>
      #include <stdlib.h>
      #include <string.h>
      #include <unistd.h>
      #include <arpa/inet.h>
      #include <sys/socket.h>
      
      #define TRUE 1
      #define FALSE 0
      
      void error_handling(char * message)
      {
          fputs(message, stderr);
          fputc('\n', stderr);
          exit(1);
      }
      
      int main(int argc, char *argv[])
      {
          int serv_sock, clnt_sock;
          char message[30];
          int option, str_len;
          socklen_t optlen, clnt_adr_sz;
          struct sockaddr_in serv_adr, clnt_adr;
          if (argc != 2) {
              printf("Usage: %s <port>\n", argv[0]);
              exit(1);
          }
      
          serv_sock = socket(PF_INET, SOCK_STREAM, 0);
          if (serv_sock == -1) {
              error_handling("socket() error");
          }
      
          /*
          optlen = sizeof(option);
          option = TRUE;
          setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR,
                     (void *)&option, optlen);
          */
      
          memset(&serv_adr, 0, sizeof(serv_adr));
          serv_adr.sin_family = AF_INET;
          serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
          serv_adr.sin_port = htons(atoi(argv[1]));
      
          if (bind(serv_sock,
                   (struct sockaddr *)&serv_adr,
                   sizeof(serv_adr))) {
              error_handling("bind() error");
          }
          if (listen(serv_sock, 5) == -1) {
              error_handling("listen() error");
          }
          clnt_adr_sz = sizeof(clnt_adr);
          clnt_sock = accept(serv_sock,
                             (struct sockaddr *)&clnt_adr,
                             &clnt_adr_sz);
          while ((str_len = read(clnt_sock,
                                 message,
                                 sizeof(message))) != 0) {
              write(clnt_sock, message, str_len);
              write(1, message, str_len);
          }
          close(clnt_sock);
          close(serv_sock);
      
          return 0;
      }
      
    • 编译运行,客户端使用第四章的客户端即可

    • shiqi@pc:~/network/ch09$ gcc reuseadr_eserver.c -o eserver
      shiqi@pc:~/network/ch09$ ./eserver 9898
      mmu
      ^C
      shiqi@pc:~/network/ch09$ ./eserver 9898
      bind() error
      

9.3 TCP_NODELAY

  • Q:Naggle算法是什么?

    • TCP 套接字默认使用 Nagle 算法交换数据,只有收到前一数据的 ACK 消息时,Nagle算法才发送下一数据,在此之前会最大限度地进行缓冲
    • 如左图,为了发送字符串「Nagle」, 将其传递到输出缓冲。这时头字符「N」之前没有其他数据(没有需接收的ACK ),因此立即传输。之后开始等待字符「N」的ACK消息,等待过程中,剩下的「agle」填入输出缓冲。接下来,收到字符「N」的ACK消息后,将输出缓冲的「agle」装入一个数据包发送。也就是说,共需传递4个数据包以传输1个字符串
    • 右图是未使用Nagle算法时发送字符串「Nagle」的过程。假设字符「N」到「e」依序传到输出缓冲。此时的发送过程与ACK接收与否无关,因此数据到达输出缓冲后将立即被发送出去,可以看到,发送字符串「Nagle」时共需10个数据包
    • 由此可知,不使用Nagle算法将对网络流量(Traffic:指网络负载或混杂程度)产生负面影响。即使只传输1个字节的数据,其头信息都有可能是几十个字节。因此,为了提高网络传输效率,必须使用Nagle算法
  • Q:什么时候可以禁用 Nagle 算法?

    • 网络流量并未受太大影响时,不使用Nagle算法要比使用它时传输速度快
    • 最典型的是「传输大文件数据」,将文件数据传入输出缓冲不会花太多时间,因此,即便不使用 Nagle 算法,也会在装满输出缓冲时传输数据包。这不仅不会增加数据包的数量,反而会在无需等待 ACK 的前提下连续传输,因此可以大大提高传输速度
    • 一般情况下,不使用 Nagle 算法可以提高传输速度。但如果无条件放弃使用 Nagle 算法,就会增加过多的网络流量,反而会影响传输。因此,未准确判断数据特性时不应禁用 Nagle 算法
  • Q:如何禁用 Nagle 算法

    • 把套接字可选项 TCP_NODELAY 改为1(True)即可

    • int opt_val = 1;
      setsockopt(sock, IPPROTO_TCP, TCP_NODELAY,
                 (void *)&opt_val, sizeof(opt_val));
      
  • Q:如何查看 Nagle 算法的设置状态

    • 查看 TCP_NODELAY 的值,如果正在使用 Nagle 算法,opt_val 变量中会保存 0,禁用保存 1

    • int opt_val;
      socklen_t opt_len;
      opt_len = sizeof(opt_val);
      getsockopt(sock, IPPROTO_TCP, TCP_NODELAY,
                 (void *)&opt_val, &opt_len);
      

第 10 章 多进程服务器端

10.1 进程概念及应用

  • Q:具有代表性的并发服务端的实现模型和方法

    • 多进程服务器:通过创建多个进程提供服务
    • 多路复用服务器:通过捆绑并统一管理 I/O 对象提供服务
    • 多线程服务器:通过生成与客户端等量的线程提供服务
  • Q:通过fork函数创建进程

    • #include <unistd.h>
      pid_t fork(void);
      // 成功时返回进程ID, 失败时返回 -1
      
    • fork函数将创建调用的进程副本。即并非根据完全不同的程序创建进程,而是复制正在运行的、调用 fork 函数的进程。

    • 两个进程都将执行fork函数调用后的语句(准确说是fork函数返回后)。但因通过同一个进程、复制相同的内存空间,之后的程序流要根据fork函数的返回值加以区分

    • 父进程(Parent Process),即原进程,为调用fork函数的主体:fork函数返回子进程ID

    • 子进程(Child Process),即通过父进程调用 fork 函数复制出的进程:fork函数返回0

  • Q:fork函数示意fork.c

    • #include <stdio.h>
      #include <unistd.h>
      
      int gval = 10;
      
      int main()
      {
          pid_t pid;
          int lval = 20;
          ++gval, lval += 5;
      
          pid = fork();
          if (pid == 0) {
              // 子进程
              gval += 2, lval += 2;
          } else {
              // 父进程
              gval -= 2, lval -= 2;
          }
      
          if (pid == 0) {
              printf("Child Proc: [%d,%d] \n", gval, lval);
          } else {
              printf("Parent Proc: [%d,%d] \n", gval, lval);
          }
      
          return 0;
      }
      
    • 编译运行

    • shiqi@inspiron:~/network$ gcc fork.c -o fork
      shiqi@inspiron:~/network$ ./fork
      Parent Proc: [9,23]
      Child Proc: [13,27]
      

10.2 进程和僵尸进程

  • Q:产生僵尸进程的原因

    • 调用fork函数产生子进程的两种终止方式
      • 传递参数并调用exit函数
      • main函数中执行return语句并返回值
    • 向 exit 函数传递的参数值和 main 函数的 return 语句返回的值都回传递给操作系统。而操作系统不会销毁子进程,直到把这些值传递给产生该子进程的父进程,处在这种状态下的进程就是僵尸进程
    • 即将子进程变成僵尸进程的是操作系统
    • 僵尸进程被销毁的时机是,操作系统向创建子进程的父进程传递子进程的 exit 参数值或 return 语句的返回值后
    • 操作系统不会主动把这些值传递给父进程。只有父进程主动发起请求(函数调用)的时候,操作系统才会传递该值,即如果父进程未主动要求获得子进程结束状态值,操作系统将一直保存,并让子进程长时间处于僵尸进程状态
  • Q:创建僵尸进程示例zombie.c

    • #include <stdio.h>
      #include <unistd.h>
      
      int main()
      {
          pid_t pid = fork();
          if (pid == 0) {
              //子进程
              puts("Hi, I am a child process");
          } else {
              // 父进程
              // 输出子进程ID,可通过该值查看子进程状态(是否为僵尸进程)
              printf("Child Process ID: %d \n", pid);
              // 如果父进程终止,处于僵尸状态的子进程将同时销毁
              // 因此延缓父进程的执行以验证僵尸进程
              sleep(30);
          }
      
          if (pid == 0) {
              puts("End child proess");
          } else {
              puts("End parent process");
          }
      
          return 0;
      }
      
    • shiqi@inspiron:~/network$ gcc zombie.c -o zombie
      shiqi@inspiron:~/network$ ./zombie
      Child Process ID: 24767
      Hi, I am a child process
      End child proess
      End parent process
      
    • (base) shiqi@inspiron:~/network$ ps aux
      USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
      shiqi    24766  0.0  0.0   4504   720 pts/1    S+   16:42   0:00 ./zombie
      shiqi    24767  0.0  0.0      0     0 pts/1    Z+   16:42   0:00 [zombie] <defunct>
      
  • Q:销毁僵尸进程 1:利用 wait 函数

    • #include <sys/wait.h>
      pid_t wait(int * statloc);
      // 成功时返回终止的子进程 ID ,失败时返回 -1
      
    • 调用此函数时如果已有子进程终止,那么子进程终止时传递的返回值(exit 函数的参数返回值,main 函数的 return 返回值)将保存到该函数的参数所指的内存空间。但函数参数指向的单元中还包含其他信息,因此需要用下列宏进行分离

      • WIFEXITED:子进程正常终止时返回真
      • WEXITSTATUS:返回子进程的返回值
    • 即向 wait 函数传递变量 status 的地址时,调用 wait 函数后应编写如下代码:

    • if (WIFEXITED(status)) {
          // 是正常终止的吗?
          puts("Normal termination!");
          // 获取返回值
          printf("Child pass num: %d", WEXITSTATUS(status));
      }
      
    • 调用 wait 函数时,如果没有已经终止的子进程,那么程序将阻塞(Blocking)直到有子进程终止,因此要谨慎调用该函数

  • Q:wait函数示例wait.c

    • #include <stdio.h>
      #include <stdlib.h>
      #include <unistd.h>
      #include <sys/wait.h>
      
      int main()
      {
          int status;
          // 这里的子进程将通过 return 语句终止
          pid_t pid = fork();
      
          if (pid == 0) {
              return 3;
          } else {
              printf("Child PID: %d \n", pid);
              // 这里的子进程将通过 exit() 函数终止
              pid = fork();
              if (pid == 0) {
                  exit(7);
              } else {
                  printf("Child PID: %d \n", pid);
      
                  // 之前终止的子进程相关信息将被保存到 status 中
                  // 同时相关子进程被完全销毁
                  wait(&status);
                  // 通过 WIFEXITED 来验证子进程是否正常终止
                  // 如果正常终止,则调用 WEXITSTATUS 宏输出子进程返回值
                  if (WIFEXITED(status)) {
                      printf("Child send one: %d \n", WEXITSTATUS(status));
                  }
      
                  // 因为之前创建了两个进程,所以再次调用 wait 函数和宏
                  wait(&status);
                  if (WIFEXITED(status)) {
                      printf("Child send two: %d \n", WEXITSTATUS(status));
                  }
                  // 暂停父进程,此时可查看子进程的状态
                  sleep(30);
              }
          }
      
          return 0;
      }
      
    • shiqi@inspiron:~/network$ gcc wait.c -o wait
      shiqi@inspiron:~/network$ ./wait
      Child PID: 24876
      Child PID: 24877
      Child send one: 3
      Child send two: 7
      
  • Q:销毁僵尸进程 2:使用 waitpid 函数

    • waitpid可以防止阻塞

    • #include <sys/wait.h>
      pid_t waitpid(pid_t pid, int * statloc, int options);
      // pid: 等待终止的目标子进程的ID,若传递-1,则与wait函数相同
      //      可以等待任意子进程终止
      // statloc: 与wait函数的statloc参数一样
      // options: 传递头文件 sys/wait.h 中声明的常量WNOHANG,
      //          即使没有终止的子进程也不会进入阻塞状态,
      //          而是返回0并退出函数
      // 成功时返回终止的子进程ID 或 0 ,失败时返回 -1
      
  • Q:waitpid函数示例waitpid.c

    • #include <stdio.h>
      #include <stdlib.h>
      #include <unistd.h>
      #include <sys/wait.h>
      
      int main()
      {
          int status;
          // 这里的子进程将通过 return 语句终止
          pid_t pid = fork();
      
          if (pid == 0) {
              return 3;
          } else {
              printf("Child PID: %d \n", pid);
              // 这里的子进程将通过 exit() 函数终止
              pid = fork();
              if (pid == 0) {
                  exit(7);
              } else {
                  printf("Child PID: %d \n", pid);
      
                  // 之前终止的子进程相关信息将被保存到 status 中
                  // 同时相关子进程被完全销毁
                  wait(&status);
                  // 通过 WIFEXITED 来验证子进程是否正常终止
                  // 如果正常终止,则调用 WEXITSTATUS 宏输出子进程返回值
                  if (WIFEXITED(status)) {
                      printf("Child send one: %d \n", 
                             WEXITSTATUS(status));
                  }
      
                  // 因为之前创建了两个进程,所以再次调用 wait 函数和宏
                  wait(&status);
                  if (WIFEXITED(status)) {
                      printf("Child send two: %d \n", 
                             WEXITSTATUS(status));
                  }
                  // 暂停父进程,此时可查看子进程的状态
                  sleep(30);
              }
          }
      
          return 0;
      }
      
    • shiqi@pc:~/network$ gcc waitpid.c -o waitpid
      shiqi@pc:~/network$ ./waitpid
      sleep 3 sec.
      sleep 3 sec.
      sleep 3 sec.
      sleep 3 sec.
      sleep 3 sec.
      Child send 24
      

10.3 信号处理

  • Q:信号注册函数原型signal

    • #include <signal.h>
      void (*signal(int signo, void (*func)(int)))(int);
      // 为了在产生信号时调用,返回之前注册的函数指针
      // 函数名: signal
      // 参数: int signo, void(*func)(int)
      // 返回类型: 参数为int型,返回void型函数指针
      
    • 第一个参数是特殊情况信息,第二个参数为特殊情况下将要调用的函数的地址值(指针)。发生第一个参数代表的情况时,调用第二个参数所指的函数

    • 可以在 signal 函数中注册的部分特殊情况和对应的函数

      • SIGALRM:已到通过调用 alarm 函数注册时间
      • SIGINT:输入 ctrl+c
      • SIGCHLD:子进程终止
  • Q:alarm函数原型

    • #include <unistd.h>
      unsigned int alarm(unsigned int seconds);
      // 返回 0 或以秒为单位的距 SIGALRM 信号发生所剩时间
      
    • 如果调用该函数的同时向它传递一个正整形参数,相应时间后(以秒为单位)将产生 SIGALRM 信号

    • 若向该函数传递为 0 ,则之前对 SIGALRM 信号的预约将取消

    • 如果通过该函数预约信号后未指定该信号对应的处理函数,则(通过调用 signal 函数)终止进程,不做任何处理

  • Q:信号处理示例signal.c

    • #include <stdio.h>
      #include <unistd.h>
      #include <signal.h>
      
      // 信号处理器(Handler)
      void timeout(int sig)
      {
          if (sig == SIGALRM) {
              puts("Time out!");
          }
          // 每隔2秒重复产生 SIGALRM 信号
          alarm(2);
      }
      
      void keycontrol(int sig)
      {
          if (sig == SIGINT) {
              puts("CTRL+C pressed");
          }
      }
      
      int main()
      {
          int i;
          // 注册 SIGALRM、SIGINT 信号及相应处理器
          signal(SIGALRM, timeout);
          signal(SIGINT, keycontrol);
          alarm(2);
      
          for (i = 0; i < 3; ++i) {
              puts("wait...");
              sleep(100);
          }
      
          return 0;
      }
      
    • 产生信号时,将唤醒由于调用 sleep 函数而进入阻塞状态的进程,进程一旦被唤醒,就不会再进入睡眠状态,即使还未到 sleep 函数中规定的时间也是如此

    • 编译运行,第一次运行是没有任何输入的运行结果,第二次在运行过程中输入CTRL+C

    • shiqi@pc:~/network/ch10$ gcc signal.c -o signal
      shiqi@pc:~/network/ch10$ ./signal
      wait...
      Time out!
      wait...
      Time out!
      wait...
      Time out!
      shiqi@pc:~/network/ch10$ ./signal
      wait...
      ^CCTRL+C pressed
      wait...
      ^CCTRL+C pressed
      wait...
      Time out!
      
  • Q:信号处理函数sigaction

    • #include <signal.h>
      int sigaction(int signo, const struct sigaction * act,
                    struct sigaction * oldact);
      // signo:传递信号信息,同signal函数
      // act:对应于第一个参数的信号处理函数(信号处理器)信息
      // oldact:通过此参数获取之前注册的信号处理函数指针,若不需要则传递0
      // 成功时返回 0 ,失败时返回 -1
      
    • 需要声明并初始化 sigaction 结构体变量来调用 sigaction

    • struct sigaction {
          void (*sa_handler)(int);
          sigset_t sa_mask;
          int sa_flags;
      }
      // sa_handler:保存信号处理函数的指针值
      // sa_mask和sa_flags:用于指定信号相关的选项和特性,
      //                   所有位均初始化为0即可
      
  • Q:sigaction函数的程序示例

    • #include <stdio.h>
      #include <unistd.h>
      #include <signal.h>
      
      void timeout(int sig)
      {
          if (sig == SIGALRM) {
              puts("Time out!");
          }
          alarm(2);
      }
      
      int main()
      {
          int i;
      
          // 为了注册信号处理函数,声明 sigaction 结构体变量
          // 并在 sa_handler 成员中保存函数指针值
          struct sigaction act;
          act.sa_handler = timeout;
          // sigemptyset 函数将 sa_mask 成员的所有位初始化为0
          sigemptyset(&act.sa_mask);
          act.sa_flags = 0;
          sigaction(SIGALRM, &act, 0);
      
          // 注册 SIGALRM 信号的处理器。
          // 调用 alarm 函数预约2秒后发生 SIGALRM 信号
          alarm(2);
      
          for (int i = 0; i < 3; ++i) {
              puts("wait...");
              sleep(100);
          }
      
          return 0;
      }
      
    • 编译运行

    • shiqi@pc:~/network/ch10$ gcc sigaction.c -o sigaction
      shiqi@pc:~/network/ch10$ ./sigaction
      wait...
      Time out!
      wait...
      Time out!
      wait...
      Time out!
      
  • Q:使用信号处理技术消灭僵尸进程的示例程序remove_zombie.c

    • 使用子进程终止时会向父进程产生SIGCHLD信号的特性

    • #include <stdio.h>
      #include <stdlib.h>
      #include <unistd.h>
      #include <signal.h>
      #include <sys/wait.h>
      
      void read_childproc(int sig)
      {
          int status;
          pid_t id = waitpid(-1, &status, WNOHANG);
          if (WIFEXITED(status)) {
              printf("Removed proc id: %d \n", id);
              printf("Child send: %d \n", WEXITSTATUS(status));
          }
      }
      
      int main(int argc, char *argv[])
      {
          pid_t pid;
          struct sigaction act;
          act.sa_handler = read_childproc;
          sigemptyset(&act.sa_mask);
          act.sa_flags = 0;
          sigaction(SIGCHLD, &act, 0);
      
          pid = fork();
          if (pid == 0) {
              // 子进程执行区域
              puts("Hi, I'm child process");
              sleep(10);
              return 12;
          } else {
              // 父进程执行区域
              printf("Child proc id: %d\n", pid);
              pid = fork();
              if (pid == 0) {
                  // 另一子程序执行区域
                  puts("Hi! I'm child process2");
                  sleep(5);
                  exit(24);
              } else {
                  int i;
                  printf("Child proc id: %d\n", pid);
                  for (i = 0; i < 5; ++i) {
                      puts("wait...");
                      sleep(5);
                  }
              }
          }
      
          return 0;
      }
      
    • 编译运行

    • shiqi@pc:~/network/ch10$ gcc remove_zombie.c -o zombie
      shiqi@pc:~/network/ch10$ ./zombie
      Child proc id: 24086
      Hi, I'm child process
      Child proc id: 24087
      wait...
      Hi! I'm child process2
      wait...
      Removed proc id: 24087
      Child send: 24
      wait...
      Removed proc id: 24086
      Child send: 12
      wait...
      wait...
      

10.4 基于多任务的并发服务器

  • Q:基于进程的并发服务器模型是怎样的?实现步骤是怎样

    • 每当有客户端请求服务(连接请求)时,回声服务器端都创建子进程以提供服务
    • 1.回声服务器端(父进程)通过调用 accept 函数受理连接请求
    • 2.此时获取的套接字文件描述符创建并传递给子进程
    • 3.子进程利用传递来的文件描述符提供服务
  • Q:实现并发服务器的基于多进程实现的回声服务器echo_mpserv.c

    • 注意,在调用 fork 函数后,要将无关的套接字文件描述符关掉

    • #include <stdio.h>
      #include <stdlib.h>
      #include <string.h>
      #include <unistd.h>
      #include <signal.h>
      #include <sys/wait.h>
      #include <arpa/inet.h>
      #include <sys/socket.h>
      
      #define BUF_SIZE 30
      
      void error_handling(char *message)
      {
          fputs(message, stderr);
          fputc('\n', stderr);
          exit(1);
      }
      
      void read_childproc(int sig)
      {
          pid_t pid;
          int status;
          pid = waitpid(-1, &status, WNOHANG);
          printf("removed proc id: %d \n", pid);
      }
      
      int main(int argc, char * argv[])
      {
          int serv_sock, clnt_sock;
          struct sockaddr_in serv_adr, clnt_adr;
      
          pid_t pid;
          struct sigaction act;
          socklen_t adr_sz;
          int str_len, state;
          char buf[BUF_SIZE];
        
          if (argc != 2) {
              printf("Usage: %s <port>\n", argv[0]);
              exit(1);
          }
      
          // 防止产生僵尸进程
          act.sa_handler = read_childproc;
          sigemptyset(&act.sa_mask);
          act.sa_flags = 0;
          state = sigaction(SIGCHLD, &act, 0);
      
          serv_sock = socket(PF_INET, SOCK_STREAM, 0);
          memset(&serv_adr, 0, sizeof(serv_adr));
          serv_adr.sin_family = AF_INET;
          serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
          serv_adr.sin_port = htons(atoi(argv[1]));
      
          if (bind(serv_sock,
                   (struct sockaddr *)&serv_adr,
                   sizeof(serv_adr)) == -1) {
              error_handling("bind() error");
          }
      
          if (listen(serv_sock, 5) == -1) {
              error_handling("listen() error");
          }
        
          while (1) {
              adr_sz  = sizeof(clnt_adr);
              // 这个套接字fork之后,父子进程均会有
              clnt_sock = accept(serv_sock,
                                 (struct sockaddr *)&clnt_adr,
                                 &adr_sz);
              if (clnt_sock == -1) {
                  continue;
              } else {
                  puts("new client connected...");
              }
              pid = fork();
              if (pid == -1) {
                  close(clnt_sock);
                  continue;
              } else if (pid == 0) {
                  // 子进程运行区域
                  // 服务器套接字文件描述符同样也传递到子进程中
                  close(serv_sock);
                  while ((str_len = read(clnt_sock, buf, BUF_SIZE)) != 0) {
                      write(clnt_sock, buf, str_len);
                  }
                  close(clnt_sock);
                  puts("client disconnected...");
                  return 0;
              } else {
                  close(clnt_sock);
              }
          }
          close(serv_sock);
          return 0;
      }
      
    • 编译运行

    • 服务端

    • shiqi@pc:~/network/ch10$ gcc echo_mpserv.c -o mpserv
      shiqi@pc:~/network/ch10$ ./mpserv 9190
      new client connected...
      new client connected...
      client disconnected...
      removed proc id: 24342
      client disconnected...
      removed proc id: 24227
      
    • 客户端1

    • shiqi@pc:~/network/ch04$ ./eclient 127.0.0.1 9190
      Connceted........
      Input message(Q to quit): zz
      Message from server: zz
      Input message(Q to quit): ad
      Message from server: ad
      Input message(Q to quit): q
      
    • 客户端2

    • shiqi@pc:~/network/ch04$ ./eclient 127.0.0.1 9190
      Connceted........
      Input message(Q to quit): zmmz
      Message from server: zmmz
      Input message(Q to quit): q
      

10.5 分割TCP的I/O程序

  • Q:分割I/O程序是什么?

    • 分割数据收发过程,分割后,不同进程分别负责输入和输出
    • 优点:
    • 1.程序的实现更简单
    • 2.提高频繁交换数据的程序性能
    • 图10-6左侧演示的是之前的回声客户端数据交换方式,右侧演示的是分割IO后的客户端数据传输方式。服务器端相同,不同的是客户端区域。分割I/O后的客户端发送数据时不必考虑接收数据的情况,因此可以连续发送数据,由此提高同一时间内传输的数据量。这种差异在网速较慢时尤为明显
  • Q:回声客户端的I/O程序分割示例echo_mpclient.c

    • #include <stdio.h>
      #include <stdlib.h>
      #include <string.h>
      #include <unistd.h>
      #include <arpa/inet.h>
      #include <sys/socket.h>
      
      #define BUF_SIZE 30
      
      void error_handling(char *message)
      {
          fputs(message, stderr);
          fputc('\n', stderr);
          exit(1);
      }
      
      void read_routine(int sock, char *buf)
      {
          while (1) {
              int str_len = read(sock, buf, BUF_SIZE);
              if (str_len == 0) {
                  return;
              }
              buf[str_len] = 0;
              printf("Message from server: %s", buf);
          }
      }
      
      void write_routine(int sock, char *buf)
      {
          while (1) {
              fgets(buf, BUF_SIZE, stdin);
              if (!strcmp(buf, "q\n") || !strcmp(buf, "Q\n")) {
                  // 调用shutdown函数向服务器端传递EOF
                  // return后即可调用主函数的close函数传递EOF
                  // 因为主函数的fork函数复制了文件描述符,
                  // 无法通过1次close函数调用传递EOF,
                  // 因此需要通过shutdown函数另外传递
                  shutdown(sock, SHUT_WR);
                  return;
              }
              write(sock, buf, strlen(buf));
          }
      }
      
      int main(int argc, char *argv[])
      {
          int sock;
          pid_t pid;
          char buf[BUF_SIZE];
          struct sockaddr_in serv_adr;
      
          if (argc != 3) {
              printf("Usage : %s <IP> <port>\n", argv[0]);
              exit(1);
          }
      
          sock = socket(PF_INET, SOCK_STREAM, 0);
          memset(&serv_adr, 0, sizeof(serv_adr));
          serv_adr.sin_family = AF_INET;
          serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
          serv_adr.sin_port = htons(atoi(argv[2]));
      
          if (connect(sock,
                      (struct sockaddr *)&serv_adr,
                      sizeof(serv_adr)) == -1) {
              error_handling("connect() error!");
          }
      
          pid = fork();
          if (pid == 0) {
              write_routine(sock, buf);
          } else {
              read_routine(sock, buf);
          }
          close(sock);
      
          return 0;
      }
      
    • 编译运行,服务端可用本章的echo_mpserv.c

    • shiqi@pc:~/network/ch10$ gcc echo_mpclient.c -o mpclient
      shiqi@pc:~/network/ch10$ ./mpclient 127.0.0.1 9190
      px
      Message from server: px
      rt
      Message from server: rt
      q
      

第11章 进程间通信

11.1 进程间通信的基本概念

  • Q:创建管道的pipe函数原型

    • #include <unistd.h>
      int pipe(int filedes[2]);
      // filedes[0]:通过管道接收数据时使用的文件描述符,即管道出口
      // filedes[1]:通过管道传输数据时使用的文件描述符,即管道入口
      // 成功时返回0,失败时返回-1
      
  • Q:pipe函数的示例程序pipe1.c,父进程与子进程进行数据交换

    • #include <stdio.h>
      #include <unistd.h>
      
      #define BUF_SIZE 30
      
      int main(int argc, char *argv[])
      {
          int fds[2];
          char str[] = "Who are you?";
          char buf[BUF_SIZE];
          pid_t pid;
      
          pipe(fds);
          pid = fork();
          if (pid == 0) {
              write(fds[1], str, sizeof(str));
          } else {
              read(fds[0], buf, BUF_SIZE);
              puts(buf);
          }
          return 0;
      }
      
    • 编译运行

    • shiqi@pc:~/network/ch11$ gcc pipe1.c -o pipe
      shiqi@pc:~/network/ch11$ ./pipe
      Who are you?
      
  • Q:2个进程使用1个管道进行双向数据交换的程序示例pipe2.c

    • #include <stdio.h>
      #include <unistd.h>
      
      #define BUF_SIZE 30
      
      int main(int argc, char *argv[])
      {
          int fds[2];
          char str1[] = "Who are you?";
          char str2[] = "Thank you for your message";
          char buf[BUF_SIZE];
          pid_t pid;
      
          pipe(fds);
          pid = fork();
          if (pid == 0) {
              write(fds[1], str1, sizeof(str1));
              // 这个不能注释,注释后,下一个read会把管道内数据取走
              // 而父进程则阻塞在read函数中无限等待
              sleep(2);
              read(fds[0], buf, BUF_SIZE);
              printf("Child proc output: %s\n", buf);
          } else {
              read(fds[0], buf, BUF_SIZE);
              printf("Parent proc output: %s\n", buf);
              write(fds[1], str2, sizeof(str2));
              sleep(3);
          }
          return 0;
      }
      
    • 编译运行

    • shiqi@pc:~/network/ch11$ gcc pipe2.c -o pipe2
      shiqi@pc:~/network/ch11$ ./pipe2
      Parent proc output: Who are you?
      Child proc output: Thank you for your message
      
  • Q:2个进程使用2个管道进行双向数据交换的程序示例pipe3.c

    • #include <stdio.h>
      #include <unistd.h>
      
      #define BUF_SIZE 30
      
      int main(int argc, char *argv[])
      {
          int fds1[2], fds2[2];
          char str1[] = "Who are you?";
          char str2[] = "Thank you for your message";
          char buf[BUF_SIZE];
          pid_t pid;
      
          pipe(fds1);
          pipe(fds2);
          pid = fork();
          if (pid == 0) {
              write(fds1[1], str1, sizeof(str1));
              read(fds2[0], buf, BUF_SIZE);
              printf("Child proc output: %s\n", buf);
          } else {
              read(fds1[0], buf, BUF_SIZE);
              printf("Parent proc output: %s\n", buf);
              write(fds2[1], str2, sizeof(str2));
              sleep(3);
          }
          return 0;
      }
      
    • 编译运行

    • shiqi@pc:~/network/ch11$ gcc pipe3.c -o pipe3
      shiqi@pc:~/network/ch11$ ./pipe3
      Parent proc output: Who are you?
      Child proc output: Thank you for your message
      

11.2 运用进程间通信

  • Q:使用多进程的回声服务器端,并将回声客户端传输的字符串按序保持到文件中的程序示例echo_storeserv.c

    • #include <stdio.h>
      #include <stdlib.h>
      #include <string.h>
      #include <unistd.h>
      #include <signal.h>
      #include <sys/wait.h>
      #include <arpa/inet.h>
      #include <sys/socket.h>
      
      #define BUF_SIZE 30
      
      void error_handling(char *message)
      {
          fputs(message, stderr);
          fputc('\n', stderr);
          exit(1);
      }
      
      void read_childproc(int sig)
      {
          pid_t pid;
          int status;
          pid = waitpid(-1, &status, WNOHANG);
          printf("removed proc id: %d \n", pid);
      }
      
      int main(int argc, char * argv[])
      {
          int serv_sock, clnt_sock;
          struct sockaddr_in serv_adr, clnt_adr;
          int fds[2];
      
          pid_t pid;
          struct sigaction act;
          socklen_t adr_sz;
          int str_len, state;
          char buf[BUF_SIZE];
      
          if (argc != 2) {
              printf("Usage : %s <port>\n", argv[0]);
              exit(1);
          }
      
          act.sa_handler = read_childproc;
          sigemptyset(&act.sa_mask);
          act.sa_flags = 0;
          state = sigaction(SIGCHLD, &act, 0);
      
          serv_sock = socket(PF_INET, SOCK_STREAM, 0);
          memset(&serv_adr, 0, sizeof(serv_adr));
          serv_adr.sin_family = AF_INET;
          serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
          serv_adr.sin_port = htons(atoi(argv[1]));
      
          if (bind(serv_sock,
                   (struct sockaddr *)&serv_adr,
                   sizeof(serv_adr)) == -1) {
              error_handling("bind() error");
          }
          if (listen(serv_sock, 5) == -1) {
              error_handling("listen() error");
          }
      
          pipe(fds);
          pid = fork();
          if (pid == 0) {
              FILE * fp = fopen("echomsg.txt", "wt");
              char msgbuf[BUF_SIZE];
              int i, len;
      
              for (i = 0; i < 10; ++i) {
                  len = read(fds[0], msgbuf, BUF_SIZE);
                  fwrite((void *)msgbuf, 1, len, fp);
              }
              fclose(fp);
              return 0;
          }
        
          while (1) {
              adr_sz = sizeof(clnt_adr);
              clnt_sock = accept(serv_sock,
                                 (struct sockaddr *)&clnt_adr,
                                 &adr_sz);
              if (clnt_sock == -1) {
                  continue;
              } else {
                  puts("new client connected...");
              }
      
              pid = fork();
              if (pid == 0) {
                  close(serv_sock);
                  while ((str_len = read(clnt_sock, buf, BUF_SIZE)) != 0) {
                      write(clnt_sock, buf, str_len);
                      write(fds[1], buf, str_len);
                  }
                  close(clnt_sock);
                  puts("client disconnected...");
                  return 0;
              } else {
                  close(clnt_sock);
              }
          }
      
          return 0;
      }
      
    • 编译运行

    • shiqi@pc:~/network/ch11$ gcc echo_storeserv.c -o serv
      shiqi@pc:~/network/ch11$ ./serv 9190
      new client connected...
      client disconnected...
      removed proc id: 25075
      
Last Updated:
Contributors: Shiqi Lu