Unix网络编程中,TCP CS程序示例的第五章有哪些关键点?

2026-05-25 06:461阅读0评论SEO资讯
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计3843个文字,预计阅读时间需要16分钟。

Unix网络编程中,TCP C/S程序示例的第五章有哪些关键点?

TCP客户端/服务器程序示例+系列文章导航:《Unix 网络编程笔记+目标+ECHO-Application 结构如下:graph LRA[标准输入/输出] -- fgets --> B[TCP-Client] -- writen/read --> C[TCP-Server] -- readline/writen --> B -- fputs

TCP客户/服务器程序示例

系列文章导航:《Unix 网络编程》笔记

目标

ECHO-Application 结构如下:

graph LR; A[标准输入/输出] --fgets--> B[TCP-Client] --writen/read--> C[TCP-Server] C --readline/writen--> B --fputs--> A

除此之外,还有:

  • Client 和 Server 启动时发生什么
  • Client 正常终止时发生什么
  • Server 先意外终止会发生什么
程序代码 服务端

#include "unp.h" int main(int argc, char **argv) { int listenfd, connfd; pid_t childpid; socklen_t clilen; struct sockaddr_in cliaddr, servaddr; // 创建 Socket listenfd = Socket(AF_INET, SOCK_STREAM, 0); // 初始化连接参数 bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); // 绑定 Bind(listenfd, (SA *)&servaddr, sizeof(servaddr)); // 开始监听 Listen(listenfd, LISTENQ); for (;;) { clilen = sizeof(cliaddr); // 服务器阻塞, 等待请求 connfd = Accept(listenfd, (SA *)&cliaddr, &clilen); if ((childpid = Fork()) == 0) { /* child process */ Close(listenfd); /* close listening socket */ str_echo(connfd); /* process the request */ exit(0); } Close(connfd); /* parent closes connected socket */ } }

#include "unp.h" void str_echo(int sockfd) { ssize_t n; char buf[MAXLINE]; again: while ((n = read(sockfd, buf, MAXLINE)) > 0) Writen(sockfd, buf, n); if (n < 0 && errno == EINTR) goto again; else if (n < 0) err_sys("str_echo: read error"); } 客户端

#include "unp.h" int main(int argc, char **argv) { int sockfd; struct sockaddr_in servaddr; if (argc != 2) err_quit("usage: tcpcli <IPaddress>"); sockfd = Socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(SERV_PORT); Inet_pton(AF_INET, argv[1], &servaddr.sin_addr); Connect(sockfd, (SA *) &servaddr, sizeof(servaddr)); str_cli(stdin, sockfd); /* do it all */ exit(0); }

#include "unp.h" void str_cli(FILE *fp, int sockfd) { char sendline[MAXLINE], recvline[MAXLINE]; while (Fgets(sendline, MAXLINE, fp) != NULL) { Writen(sockfd, sendline, strlen(sendline)); if (Readline(sockfd, recvline, MAXLINE) == 0) err_quit("str_cli: server terminated prematurely"); Fputs(recvline, stdout); } } 正常情况

当我们把服务器和客户端都启动后,可以通过命令查看网络的情况:

[root@centos-5610 Unix_Network]# netstat -a | grep 9877 tcp 0 0 0.0.0.0:9877 0.0.0.0:* LISTEN tcp 0 0 localhost:9877 localhost:38160 ESTABLISHED tcp 0 0 localhost:38160 localhost:9877 ESTABLISHED

  • 第一个是服务器的父进程,状态为 LISTEN,监听范围和可接受范围如上所示
  • 第二个是客户端进程
  • 第三个是服务器的子进程,为客户端提供具体的服务

连接的正常断开

我们在客户端输入 EOF (Ctrl + D),之后会发生一系列事情:

sequenceDiagram autonumber participant cs as cli_str participant cm as cli_main participant sm as serv_main_child participant ss as serv_str cs ->> cm: fgets获得EOF,函数返回 cm ->> cm: 执行完毕, 调用 exit 结束 cm ->> ss: 关闭 cli 打开的所有描述符,并发送 FIN 给客户 note over cm: FIN_WAIT_1 ss ->> sm: readline 接受到 FIN,返回0,函数返回 sm ->> sm: 执行完毕,调用 exit 结束子进程 note over sm: CLOSE_WAIT sm ->> cm: 关闭所有打开的描述符,发送ACK note over sm: LAST_ACK note over cm: FIN_WAIT_2 sm ->> cm: FIN note over cm: TIME_WAIT cm ->> sm: ACK note over sm: CLOSED

(上述如套接字的操作其实是在内核完成的,这里为了简便所以标在了对应的线程上)

如下,可以看到客户端的 TIME_WAIT 状态持续了一段时间

[root@centos-5610 Unix_Network]# netstat -a | grep 9877 tcp 0 0 0.0.0.0:9877 0.0.0.0:* LISTEN tcp 0 0 localhost:38160 localhost:9877 TIME_WAIT [root@centos-5610 Unix_Network]# netstat -a | grep 9877 tcp 0 0 0.0.0.0:9877 0.0.0.0:* LISTEN POSIX信号处理 僵死进程

背景

在上述程序中,其实子进程结束后,会向父进程发送一个 SIGCHLD 信号,我们这里没有捕捉,默认行为为被忽略。

既然父进程未加处理,子进程于是进入僵死状态,如下状态 Z 所示:

[root@centos-5610 Unix_Network]# ps -t pts/0 -o pid,ppid,stat,tty,args,wchan PID PPID STAT TT COMMAND WCHAN 2008 1771 S pts/0 ./tcpserv01 inet_csk_accept 2382 2008 Z pts/0 [tcpserv01] <defunct> do_exit

或如下所示:

[root@centos-5610 tcpcliserv]# ps PID TTY TIME CMD 1771 pts/0 00:00:00 bash 2008 pts/0 00:00:00 tcpserv01 2382 pts/0 00:00:00 tcpserv01 <defunct> 2555 pts/0 00:00:00 tcpserv01 <defunct> 2654 pts/0 00:00:00 tcpserv01 <defunct> 2886 pts/0 00:00:00 tcpserv01 <defunct> 3238 pts/0 00:00:00 tcpserv01 <defunct> 6685 pts/0 00:00:00 ps

为什么会有僵死进程

设置僵死的目的是维护子进程的信息,以便父进程在以后某个时候获取这些信息(包括进程 ID、终止状态、资源利用情况)

父进程终止了,还有人管这些僵死进程吗

如果父进程也终止了,而其有处于僵死状态的子进程,那么子进程的父进程会被设置为 1(init 进程的 ID),init 进程会清理他们(wait,后续讲解)

僵死进程的坏处

他们占用内核的空间,最终可能导致我们耗尽处理资源,所以我们必须处理僵死进程。

信号基础

信号就是告知某个进程发生了某个事件的通知,有时也称为软件中断。

信号的来源

  • 一个进程发送给另一个进程(或自身)
  • 由内核发给某个进程

信号的处理

通过调用 sigaction 函数设定一个信号的处理,并有三种选择:

  • 设置一个信号处理函数。SIGKILL 和 SIGSTOP 不能被捕获
  • 设定为 SIG_IGN 来忽略它。同样,上述两个信号不能被忽略
  • 设定为 SIG_DFL 来启用他的默认处置。默认处置通常是终止进程
signal

sigaction 函数太过于复杂,所以一般我们会调用 signal 函数。

但是 signal 函数由于历史和标准的原因在不同的系统上实现不一致,所以我们实现自己的 signal 方法。其签名如下:

void (*signal(int signo, void (*func)(int)))(int);

我们会做一些处理,简化其表示:

typedef void Sigfunc(int); Sigfunc *signal(int signo, Sigfunc * func);

signal 函数如下:

#include "unp.h" Sigfunc *signal(int signo, Sigfunc *func) { struct sigaction act, oact; act.sa_handler = func; sigemptyset(&act.sa_mask); act.sa_flags = 0; if (signo == SIGALRM) { #ifdef SA_INTERRUPT act.sa_flags |= SA_INTERRUPT; /* SunOS 4.x */ #endif } else { #ifdef SA_RESTART act.sa_flags |= SA_RESTART; /* SVR4, 44BSD */ #endif } if (sigaction(signo, &act, &oact) < 0) return (SIG_ERR); return (oact.sa_handler); } 处理 SIGCHLD 信号

建立一个俘获 SIGCHLD 信号的信号处理函数,在函数体中调用 wait(后面会提到):

Unix网络编程中,TCP C/S程序示例的第五章有哪些关键点?

void sig_chld(int signo) { pid_t pid; int stat; pid = wait(&stat); printf("child %d terminated\n", pid); return; }

在 Listen 方法后调用:(必须在 fork 前调用,且只能执行一次)

Listen(listenfd, LISTENQ); Signal(SIGCHLD, sig_chld);

此时就不会再出现僵死进程了。

被中断的系统调用

慢系统调用

如 accept 等函数,如果没有用户连接,将一直阻塞下去,把这样的系统调用称为慢系统调用。

满系统调用的中断

如前一节我们处理 SIGCHLD 信号时,当系统阻塞于一个慢系统调用时,而该进程又捕获了一个信号,且相应的信号处理函数返回时,该系统调用可能会返回一个 IENTER 错误。

有些系统可能会自动重启某些被中断的系统调用,但是出于对程序的可移植性考虑,我们应该对此有所准备。

for (;;) { clilen = sizeof(cliaddr); if ((connfd = accept(listenfd, (SA *)&cliaddr, &clilen)) < 0) { if (errno == EINTER) { continue; } else { err_sys("XXX"); } } }

这种方式对 accept 以及诸如 read、write、select、open 之类的函数来说都是合适的,但是如前面所说,connect 函数不能重启。

wait 和 waitpid

#include <sys/wait.h> pid_t wait(int *statloc); pid_t waitpid(pid_t pid, int *statloc, int options);

相同之处

均返回已终止子进程的 ID,以及通过 statloc 指针返回的子进程终止状态

一些宏 WIFEXIST、WEXITSTATUS 可以用来查看其信息

不同之处

如果调用 wait 的进程没有已终止的子进程,则阻塞至第一个现有子进程终止为止

而 waitpid 可以通过 pid 和 options 参数来进行更多的控制

wait 的问题

如果我们用多台客户端发送请求,然后同时终止,如下:

多个 SIGCHLD 信号会到达,但是 wait 只会被执行一次,导致会留下 4 个僵死进程,如果是在不同的机器上执行的,则更为不确定。

具体原因可以参考 问题:Linux 信号处理,当连续给一个进程同时发送多个信号时,部分信号丢失而未得到处理

用 waitpid 可以解决这个问题:

#include "unp.h" void sig_chld(int signo) { pid_t pid; int stat; while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0) printf("child %d terminated\n", pid); return; }

  • WNOHANG 表示如果没有终止的子进程就阻塞(因此我们可以用 while 循环)
异常情况 accept 返回前连接中止

连接刚刚建立,客户端就发送一个 RST。

什么样的场景下会发生这种事情?我在网上简单检索了一下,但是没有找到典型的发生场景。

书上给出的例子是 Web 服务器比较繁忙

如何处理这种情况取决于具体的实现。

服务器进程终止

这里指的是服务器的子进程,也就是提供具体服务的那个进程。

我们先把 server/client 启动,然后把子进程关闭掉,观察现象:

[root@centos-5610 tcpcliserv]# ./tcpcli01 127.0.0.1 >>1 str_cli: server terminated prematurely

  • 如果我们什么也不做,那么客户端会一直被 fgets 阻塞,它对外界发生的事情一无所知
  • 如果我们发送什么新的信息,那么会出现一个报错信息

过程解释

  • 服务器的相关 socket 关闭后,会发送一个 FIN 个客户端
  • 客户端 socket 虽然接收到了,但是这只表示服务器进程关闭了连接的服务器端,从而不在往其中发送任何消息了,但并没有告知客户 TCP 服务器进程已经终止;所以客户端还是可以发送 writen 的
  • 当服务器 TCP 接收到来自客户的数据时,由于该 TCP 已经被关闭,所以会相应一个 RST
  • 客户端在调用 write 后便进入 readline,于是接收到了 TCP 之前发送到的 FIN (客户端没有接收到 RST),这将使 readline 返回 0,程序结束
  • 客户端进行关闭资源的各项操作

本例的问题在于:

  • 客户端同时应对了两个描述符:套接字和用户输入
  • 客户端应该阻塞在其中任何一个源的输入上,而不是单纯地阻塞在这两个源中某个特定源的输入上

这正是后文 select 和 poll 这两个函数的目的之一;后文经过修改,即可让程序立刻对服务器的 FIN 进行处理

SIGPIPE 信号

  • 前文:接收到客户端的 FIN 后,仍然可以 write 写数据
  • 但是,如果接收到了服务器的 RST,此时如果再写数据,就会由内核向进程发送一个 SIGPIPE 信号;此信号的默认行为是终止进程
  • 我们可以捕获该信号,不过无论是否捕获,readline 还是会返回一个 EPIPE 错误
服务器主机崩溃

例如服务器宕机了,这种情况下服务器来不及交代“遗言”就挂掉了。

发生的事情

  • 如果客户端不发送消息,则会想上文提到的场景一样,永远等下去
  • 如果发送消息,则会由于接收不到服务器的响应而不断尝试重新发送,书中等待了 9 分钟才放弃发送,返回 ETIMEDOUT,如果被路由器判定不可达,则返回 EHOSTUNREACHENETUNREACH

改进

  • 对于上述第一种问题,可以采用后文的 SO-KEEPALIVE 套接字选项
  • 第二个人问题可以对 readline 设置一个超时

如果服务器重启

  • 尽管重启了,但是 TCP 套接字的信息都丢失了
  • 只能对发过来的请求说:我认识你吗(RST)
  • 客户端 readline 接收到 RST 后,返回 ECONNRESET 错误
服务器主机关机

Unix 系统关机时,会“先礼后兵”:

  • 先发送 SIGTERM 信号给所有进程,在一段时间后再发送 SIGKILL 信号
  • 接收到 SIGTERM 进程一般会进行一些善后操作,如果进程不捕获这个信号,那他的默认行为就是终止进程
  • SIGKILL 会让所有进程终止,自然也会释放套接字等信息
数据格式

由于如下的问题:

  1. 不同的实现以不同的方式存储二进制,如大小端字节序
  2. 不同的实现在存储相同的 C 数据类型上的差异
  3. 不同的实现给结构打包的方式存在差异

所以通过套接字传输二进制数据是不明智的。

解决方法有:

  1. 把所有的数值数据作为文本串来传递
  2. 显示定义所支持数据类型的二进制格式,并传输此格式的数据,如 RPC 通常包括这种技术

本文共计3843个文字,预计阅读时间需要16分钟。

Unix网络编程中,TCP C/S程序示例的第五章有哪些关键点?

TCP客户端/服务器程序示例+系列文章导航:《Unix 网络编程笔记+目标+ECHO-Application 结构如下:graph LRA[标准输入/输出] -- fgets --> B[TCP-Client] -- writen/read --> C[TCP-Server] -- readline/writen --> B -- fputs

TCP客户/服务器程序示例

系列文章导航:《Unix 网络编程》笔记

目标

ECHO-Application 结构如下:

graph LR; A[标准输入/输出] --fgets--> B[TCP-Client] --writen/read--> C[TCP-Server] C --readline/writen--> B --fputs--> A

除此之外,还有:

  • Client 和 Server 启动时发生什么
  • Client 正常终止时发生什么
  • Server 先意外终止会发生什么
程序代码 服务端

#include "unp.h" int main(int argc, char **argv) { int listenfd, connfd; pid_t childpid; socklen_t clilen; struct sockaddr_in cliaddr, servaddr; // 创建 Socket listenfd = Socket(AF_INET, SOCK_STREAM, 0); // 初始化连接参数 bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); // 绑定 Bind(listenfd, (SA *)&servaddr, sizeof(servaddr)); // 开始监听 Listen(listenfd, LISTENQ); for (;;) { clilen = sizeof(cliaddr); // 服务器阻塞, 等待请求 connfd = Accept(listenfd, (SA *)&cliaddr, &clilen); if ((childpid = Fork()) == 0) { /* child process */ Close(listenfd); /* close listening socket */ str_echo(connfd); /* process the request */ exit(0); } Close(connfd); /* parent closes connected socket */ } }

#include "unp.h" void str_echo(int sockfd) { ssize_t n; char buf[MAXLINE]; again: while ((n = read(sockfd, buf, MAXLINE)) > 0) Writen(sockfd, buf, n); if (n < 0 && errno == EINTR) goto again; else if (n < 0) err_sys("str_echo: read error"); } 客户端

#include "unp.h" int main(int argc, char **argv) { int sockfd; struct sockaddr_in servaddr; if (argc != 2) err_quit("usage: tcpcli <IPaddress>"); sockfd = Socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(SERV_PORT); Inet_pton(AF_INET, argv[1], &servaddr.sin_addr); Connect(sockfd, (SA *) &servaddr, sizeof(servaddr)); str_cli(stdin, sockfd); /* do it all */ exit(0); }

#include "unp.h" void str_cli(FILE *fp, int sockfd) { char sendline[MAXLINE], recvline[MAXLINE]; while (Fgets(sendline, MAXLINE, fp) != NULL) { Writen(sockfd, sendline, strlen(sendline)); if (Readline(sockfd, recvline, MAXLINE) == 0) err_quit("str_cli: server terminated prematurely"); Fputs(recvline, stdout); } } 正常情况

当我们把服务器和客户端都启动后,可以通过命令查看网络的情况:

[root@centos-5610 Unix_Network]# netstat -a | grep 9877 tcp 0 0 0.0.0.0:9877 0.0.0.0:* LISTEN tcp 0 0 localhost:9877 localhost:38160 ESTABLISHED tcp 0 0 localhost:38160 localhost:9877 ESTABLISHED

  • 第一个是服务器的父进程,状态为 LISTEN,监听范围和可接受范围如上所示
  • 第二个是客户端进程
  • 第三个是服务器的子进程,为客户端提供具体的服务

连接的正常断开

我们在客户端输入 EOF (Ctrl + D),之后会发生一系列事情:

sequenceDiagram autonumber participant cs as cli_str participant cm as cli_main participant sm as serv_main_child participant ss as serv_str cs ->> cm: fgets获得EOF,函数返回 cm ->> cm: 执行完毕, 调用 exit 结束 cm ->> ss: 关闭 cli 打开的所有描述符,并发送 FIN 给客户 note over cm: FIN_WAIT_1 ss ->> sm: readline 接受到 FIN,返回0,函数返回 sm ->> sm: 执行完毕,调用 exit 结束子进程 note over sm: CLOSE_WAIT sm ->> cm: 关闭所有打开的描述符,发送ACK note over sm: LAST_ACK note over cm: FIN_WAIT_2 sm ->> cm: FIN note over cm: TIME_WAIT cm ->> sm: ACK note over sm: CLOSED

(上述如套接字的操作其实是在内核完成的,这里为了简便所以标在了对应的线程上)

如下,可以看到客户端的 TIME_WAIT 状态持续了一段时间

[root@centos-5610 Unix_Network]# netstat -a | grep 9877 tcp 0 0 0.0.0.0:9877 0.0.0.0:* LISTEN tcp 0 0 localhost:38160 localhost:9877 TIME_WAIT [root@centos-5610 Unix_Network]# netstat -a | grep 9877 tcp 0 0 0.0.0.0:9877 0.0.0.0:* LISTEN POSIX信号处理 僵死进程

背景

在上述程序中,其实子进程结束后,会向父进程发送一个 SIGCHLD 信号,我们这里没有捕捉,默认行为为被忽略。

既然父进程未加处理,子进程于是进入僵死状态,如下状态 Z 所示:

[root@centos-5610 Unix_Network]# ps -t pts/0 -o pid,ppid,stat,tty,args,wchan PID PPID STAT TT COMMAND WCHAN 2008 1771 S pts/0 ./tcpserv01 inet_csk_accept 2382 2008 Z pts/0 [tcpserv01] <defunct> do_exit

或如下所示:

[root@centos-5610 tcpcliserv]# ps PID TTY TIME CMD 1771 pts/0 00:00:00 bash 2008 pts/0 00:00:00 tcpserv01 2382 pts/0 00:00:00 tcpserv01 <defunct> 2555 pts/0 00:00:00 tcpserv01 <defunct> 2654 pts/0 00:00:00 tcpserv01 <defunct> 2886 pts/0 00:00:00 tcpserv01 <defunct> 3238 pts/0 00:00:00 tcpserv01 <defunct> 6685 pts/0 00:00:00 ps

为什么会有僵死进程

设置僵死的目的是维护子进程的信息,以便父进程在以后某个时候获取这些信息(包括进程 ID、终止状态、资源利用情况)

父进程终止了,还有人管这些僵死进程吗

如果父进程也终止了,而其有处于僵死状态的子进程,那么子进程的父进程会被设置为 1(init 进程的 ID),init 进程会清理他们(wait,后续讲解)

僵死进程的坏处

他们占用内核的空间,最终可能导致我们耗尽处理资源,所以我们必须处理僵死进程。

信号基础

信号就是告知某个进程发生了某个事件的通知,有时也称为软件中断。

信号的来源

  • 一个进程发送给另一个进程(或自身)
  • 由内核发给某个进程

信号的处理

通过调用 sigaction 函数设定一个信号的处理,并有三种选择:

  • 设置一个信号处理函数。SIGKILL 和 SIGSTOP 不能被捕获
  • 设定为 SIG_IGN 来忽略它。同样,上述两个信号不能被忽略
  • 设定为 SIG_DFL 来启用他的默认处置。默认处置通常是终止进程
signal

sigaction 函数太过于复杂,所以一般我们会调用 signal 函数。

但是 signal 函数由于历史和标准的原因在不同的系统上实现不一致,所以我们实现自己的 signal 方法。其签名如下:

void (*signal(int signo, void (*func)(int)))(int);

我们会做一些处理,简化其表示:

typedef void Sigfunc(int); Sigfunc *signal(int signo, Sigfunc * func);

signal 函数如下:

#include "unp.h" Sigfunc *signal(int signo, Sigfunc *func) { struct sigaction act, oact; act.sa_handler = func; sigemptyset(&act.sa_mask); act.sa_flags = 0; if (signo == SIGALRM) { #ifdef SA_INTERRUPT act.sa_flags |= SA_INTERRUPT; /* SunOS 4.x */ #endif } else { #ifdef SA_RESTART act.sa_flags |= SA_RESTART; /* SVR4, 44BSD */ #endif } if (sigaction(signo, &act, &oact) < 0) return (SIG_ERR); return (oact.sa_handler); } 处理 SIGCHLD 信号

建立一个俘获 SIGCHLD 信号的信号处理函数,在函数体中调用 wait(后面会提到):

Unix网络编程中,TCP C/S程序示例的第五章有哪些关键点?

void sig_chld(int signo) { pid_t pid; int stat; pid = wait(&stat); printf("child %d terminated\n", pid); return; }

在 Listen 方法后调用:(必须在 fork 前调用,且只能执行一次)

Listen(listenfd, LISTENQ); Signal(SIGCHLD, sig_chld);

此时就不会再出现僵死进程了。

被中断的系统调用

慢系统调用

如 accept 等函数,如果没有用户连接,将一直阻塞下去,把这样的系统调用称为慢系统调用。

满系统调用的中断

如前一节我们处理 SIGCHLD 信号时,当系统阻塞于一个慢系统调用时,而该进程又捕获了一个信号,且相应的信号处理函数返回时,该系统调用可能会返回一个 IENTER 错误。

有些系统可能会自动重启某些被中断的系统调用,但是出于对程序的可移植性考虑,我们应该对此有所准备。

for (;;) { clilen = sizeof(cliaddr); if ((connfd = accept(listenfd, (SA *)&cliaddr, &clilen)) < 0) { if (errno == EINTER) { continue; } else { err_sys("XXX"); } } }

这种方式对 accept 以及诸如 read、write、select、open 之类的函数来说都是合适的,但是如前面所说,connect 函数不能重启。

wait 和 waitpid

#include <sys/wait.h> pid_t wait(int *statloc); pid_t waitpid(pid_t pid, int *statloc, int options);

相同之处

均返回已终止子进程的 ID,以及通过 statloc 指针返回的子进程终止状态

一些宏 WIFEXIST、WEXITSTATUS 可以用来查看其信息

不同之处

如果调用 wait 的进程没有已终止的子进程,则阻塞至第一个现有子进程终止为止

而 waitpid 可以通过 pid 和 options 参数来进行更多的控制

wait 的问题

如果我们用多台客户端发送请求,然后同时终止,如下:

多个 SIGCHLD 信号会到达,但是 wait 只会被执行一次,导致会留下 4 个僵死进程,如果是在不同的机器上执行的,则更为不确定。

具体原因可以参考 问题:Linux 信号处理,当连续给一个进程同时发送多个信号时,部分信号丢失而未得到处理

用 waitpid 可以解决这个问题:

#include "unp.h" void sig_chld(int signo) { pid_t pid; int stat; while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0) printf("child %d terminated\n", pid); return; }

  • WNOHANG 表示如果没有终止的子进程就阻塞(因此我们可以用 while 循环)
异常情况 accept 返回前连接中止

连接刚刚建立,客户端就发送一个 RST。

什么样的场景下会发生这种事情?我在网上简单检索了一下,但是没有找到典型的发生场景。

书上给出的例子是 Web 服务器比较繁忙

如何处理这种情况取决于具体的实现。

服务器进程终止

这里指的是服务器的子进程,也就是提供具体服务的那个进程。

我们先把 server/client 启动,然后把子进程关闭掉,观察现象:

[root@centos-5610 tcpcliserv]# ./tcpcli01 127.0.0.1 >>1 str_cli: server terminated prematurely

  • 如果我们什么也不做,那么客户端会一直被 fgets 阻塞,它对外界发生的事情一无所知
  • 如果我们发送什么新的信息,那么会出现一个报错信息

过程解释

  • 服务器的相关 socket 关闭后,会发送一个 FIN 个客户端
  • 客户端 socket 虽然接收到了,但是这只表示服务器进程关闭了连接的服务器端,从而不在往其中发送任何消息了,但并没有告知客户 TCP 服务器进程已经终止;所以客户端还是可以发送 writen 的
  • 当服务器 TCP 接收到来自客户的数据时,由于该 TCP 已经被关闭,所以会相应一个 RST
  • 客户端在调用 write 后便进入 readline,于是接收到了 TCP 之前发送到的 FIN (客户端没有接收到 RST),这将使 readline 返回 0,程序结束
  • 客户端进行关闭资源的各项操作

本例的问题在于:

  • 客户端同时应对了两个描述符:套接字和用户输入
  • 客户端应该阻塞在其中任何一个源的输入上,而不是单纯地阻塞在这两个源中某个特定源的输入上

这正是后文 select 和 poll 这两个函数的目的之一;后文经过修改,即可让程序立刻对服务器的 FIN 进行处理

SIGPIPE 信号

  • 前文:接收到客户端的 FIN 后,仍然可以 write 写数据
  • 但是,如果接收到了服务器的 RST,此时如果再写数据,就会由内核向进程发送一个 SIGPIPE 信号;此信号的默认行为是终止进程
  • 我们可以捕获该信号,不过无论是否捕获,readline 还是会返回一个 EPIPE 错误
服务器主机崩溃

例如服务器宕机了,这种情况下服务器来不及交代“遗言”就挂掉了。

发生的事情

  • 如果客户端不发送消息,则会想上文提到的场景一样,永远等下去
  • 如果发送消息,则会由于接收不到服务器的响应而不断尝试重新发送,书中等待了 9 分钟才放弃发送,返回 ETIMEDOUT,如果被路由器判定不可达,则返回 EHOSTUNREACHENETUNREACH

改进

  • 对于上述第一种问题,可以采用后文的 SO-KEEPALIVE 套接字选项
  • 第二个人问题可以对 readline 设置一个超时

如果服务器重启

  • 尽管重启了,但是 TCP 套接字的信息都丢失了
  • 只能对发过来的请求说:我认识你吗(RST)
  • 客户端 readline 接收到 RST 后,返回 ECONNRESET 错误
服务器主机关机

Unix 系统关机时,会“先礼后兵”:

  • 先发送 SIGTERM 信号给所有进程,在一段时间后再发送 SIGKILL 信号
  • 接收到 SIGTERM 进程一般会进行一些善后操作,如果进程不捕获这个信号,那他的默认行为就是终止进程
  • SIGKILL 会让所有进程终止,自然也会释放套接字等信息
数据格式

由于如下的问题:

  1. 不同的实现以不同的方式存储二进制,如大小端字节序
  2. 不同的实现在存储相同的 C 数据类型上的差异
  3. 不同的实现给结构打包的方式存在差异

所以通过套接字传输二进制数据是不明智的。

解决方法有:

  1. 把所有的数值数据作为文本串来传递
  2. 显示定义所支持数据类型的二进制格式,并传输此格式的数据,如 RPC 通常包括这种技术