回射程序:客户进程向服务器发送数据,服务器回送该数据,然后客户进程将其显示在 stdout。
server.c Code:
#include "unp.h"
extern void str_echo(int);
int main(int argc, char **argv) {
int listenfd, connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
err_sys("socket() error");
exit(0);
}
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT); // SERV_PORT 在 unp.h 定义,其值为 9877
if(bind(listenfd, (SA *)&servaddr, sizeof(servaddr)) < 0) {
err_sys("bind error");
exit(1);
}
if(listen(listenfd, LISTENQ) < 0) {
err_sys("listen error");
exit(1);
}
for( ; ; ) {
clilen = sizeof(cliaddr);
if((connfd = accept(listenfd, (SA *)&cliaddr, &clilen)) < 0) {
err_sys("accept error");
exit(1);
}
if((childpid = fork()) < 0) {
err_sys("fork error");
} else if(childpid == 0) {
if(close(listenfd) < 0) err_sys("close error");
str_echo(connfd);
exit(0);
}
if(close(connfd) < 0) err_sys("close error");
}
}
str_echo.c Code:
#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");
}
client.c Code:
#include "unp.h"
extern void str_cli(FILE*, int);
int main(int argc, char **argv) {
int sockfd;
struct sockaddr_in servaddr;
if(argc != 2)
err_quit("usage: tcpcli <IPaddress>");
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
err_sys("socket() error");
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
if(inet_pton(AF_INET, argv[1], &servaddr.sin_addr) < 0)
err_sys("inet_pton error");
if(connect(sockfd, (SA *)&servaddr, sizeof(servaddr)) < 0)
err_sys("connet error");
str_cli(stdin, sockfd);
exit(0);
}
str_cli.c Code:
#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);
}
}
【首先我们可以发现,server.c 中有执行 fork,但并没有出现 wait 或 waitpid 函数来避免处理僵尸进程】
那么是不是直接在 server.c 中的 for( ; ; ) 内添加 wait(&status) 就可以了呢?
【显然这样是不正确的】,使用 fork 的原因就是为了并发,直接调用 wait 会使得该程序一直阻塞,直到有一个子进程终止,并是一个僵尸进程。
所以,好的处理方式是,编写 SIGCHLD 信号处理函数。因为当子进程状态改变(不仅仅是子进程终止)时,会向父进程发送一个 SIGCHLD 信号。
即:
void sig_chld(int signo) {
pid_t pid;
int stat;
pid = wait(&stat);
printf("child %d terminated\n", pid);
return ;
}
PS:书上说,signal(SIGCHLD, sig_chld) 只需要在 fork 第一个子进程之前做一次即可。不过我的疑问是,某些系统如 Ubuntu 18.04 对 signal 的处理似乎是早期的处理方式,在一次处理成功返回后,对该信号又变回了系统默认处理方式。
【不过是否这样就足够了呢?】
【并不是。】假设客户端连续向服务端发送了 5 个连接请求,在连接全部成功建立之后,这 5 个连接又同时成功关闭。
那么也就意味着,服务器父进程会连续收到 5 个 SIGCHLD 信号,而 Unix 信号默认是不排队的。
所以实际运行,最后可以发现,仍然存在 4 个僵尸进程。
所以,最终的 sig_chld 代码是这样的:
void sig_chld(int signo) {
pid_t pid;
int stat;
while((pid = waitpid(-1, &stat, WNOHANG)) > 0)
printf("child %d terminated\n", pid);
return ;
}
【注意】waitpid 的第三个参数 WNOHANG 指明不会阻塞调用进程,而是在子进程没有结束时返回 0。具体参考 waitpid 函数详解。不能用循环调用 wait 也是出于这个原因,wait 会使调用进程阻塞。(父进程在子进程状态改变时收到 SIGCHLD)
感觉使用 if 是不是也可以???
【其次是对于 accept 函数,它是一个慢系统调用】
所以,对于 server.c 中,我们应该考虑其被中断而又未被系统自动重启的情况。
即,我们应该以如下的方式更改 server.c 中的代码。
// 关于改进:accept 是慢系统调用,我们应该处理其被信号中断,而系统又不自动重启 accept 的情况。
if((connfd = accpet(listenfd, (SA *) &cliaddr, &clilen)) < 0) {
if(errno == EINTR) continue;
else err_sys("accept error");
}
【accept 返回之前连接中止】
即连接已经建立。假设此时的已完成连接队列只有一个项,accept 在返回之前该连接被中止了。
如何处理这种中止的连接依赖于不同的实现。POSIX 对其的处理方式是,将其 errno 设置为 ECONNABORTED,表示非致命中止错误。通常这种情况下,服务器忽略它,再次调用 accept 即可。
【服务器(子)进程终止】
如下列情况所示:
hjm@hjm-Inspiron:~/InterviewPreparation/unp/Examples/TCP_Examples(回射程序)$ ./server.out &
[1] 15344
hjm@hjm-Inspiron:~/InterviewPreparation/unp/Examples/TCP_Examples(回射程序)$ ./client.out 127.0.0.1
hahahahah
hahahahah
kulekulekule
kulekulekule
child 15347 terminated //(在此时,我们在另一个终端利用 kill 杀死了服务器的子进程)
another line
str_cli: server terminated prematurely
hjm@hjm-Inspiron:~/InterviewPreparation/unp/Examples/TCP_Examples(回射程序)$
【注意】我是在同一台电脑上运行的客户端程序与服务端程序。上面的 child 15347 terminated 的输出来自后台进程组 server.out
当键入 “another line” 时,str_cli 调用 writen,客户 TCP 接着把数据发送给服务器,这是可以的。
服务器进程已关闭,则向客户进程发送了 FIN。
而服务器对于收到的客户进程发送的数据,再响应一个 RST。(RST 是复位报文,以通知对方关闭链接或者重新建立链接)
但是由于客户在 writen 之后立马调用了 Readline,此时客户进程是阻塞在 Readline 的,然后客户进程收到 FIN,Readline 返回 0 (表示 EOF),即打印出错信息 “server terminated prematurely”,客户进程结束。所以客户实际上并没有看到这个 RST。
PS:当然,也有可能先收到 RST,此时的 Readline 返回出错,并设置 errno 为 ECONNRESET。
【本例子的问题在于】:当 FIN 到达套接字时,客户正在阻塞 fgets 调用上。客户实际上在应对两个描述符——套接字与用户输入,它不能单纯阻塞在这两个源中的某个特定源的输入上,而是应该阻塞在其中任何一个源的输入上(是指每个源都阻塞?)。
【如果不理会 Readline 返回的错误,继续执行写操作会如何?】
当一个进程向某个已收到 RST 的套接字执行写操作时,内核向该进程发送一个 SIGPIPE 信号。该信号的默认行为是终止进程。但不论该进程是从该信号的捕捉函数返回,还是忽略了该信号,写操作都会返回出错,并设置 errno 为 EPIPE 错误。
即,假设 str_cli 代码更改如下:
void str_cli(FILE *fp, int sockfd) {
char sendline[MAXLINE], recvline[MAXLINE];
while(Fgets(sendline, MAXLINE, fp) != NULL) {
Writen(sockfd, sendline, 1);
sleep(1);
Writen(sockfd, sendline + 1, strlen(sendline) - 1);
if(Readline(sockfd, recvline, MAXLINE) == 0)
err_quit("str_cli: server terminated prematurely");
Fputs(recvline, stdout);
}
}
【终端】
hjm@hjm-Inspiron:~/InterviewPreparation/unp/Examples/TCP_Examples(回射程序)$ ./client.out 127.0.0.1
jjj
jjj
child 27723 terminated //(在此时,我们在另一个终端利用 kill 杀死了服务器的子进程)
bye
Broken pipe // 本行由 shell 显示
由于 shell 的不同,有可能最后一句话并不会被显示。
所以我们可以在需要的时候自行编写 SIGPIPE 的信号处理函数。。不过仍需要【注意】:当有多个套接字产生这个信号是,程序是无法判断 SIGPIPE 来自于哪个套接字的。
【服务器崩溃未重启/或服务器主机突然变得不可达】
同样,客户进程在调用 writen 将数据写入缓冲区后便返回,阻塞于 Readline 的调用。
在这两种情况下,即客户进程不会收到任何报文,于是客户进程会持续重传数据。
当重传次数超过一定限制后,Readline 返回错误,若为服务器崩溃,设置 errno 为 ETIMEDOUT;否则会接收到一个 ICMP 消息,Readline 返回错误并设置 errno 为 EHOSTUNREACH 或 ENETUNREACH。
【服务器主机崩溃后重启】
与上崩溃未重启类似。不过这种情况下,服务器重启后会接收到客户进程的数据。而服务器崩溃后重启意味着它的 TCP 丢失了崩溃前的所有连接信息,因此会响应一个 RST。
当然,这种情况是出现在服务器崩溃后,客户进程仍有向服务器发送数据。对于这种情况下,即使客户进程没有主动发送数据也要能检测出来就需要依靠其他的技术。
【服务器主机关机】
init 进程通常先给所有进程发送 SIGTERM 信号(该信号可被捕获),等待一段固定时间
然后给所有仍然在运行的进程发送 SIGKILL 信号(不能被捕获)
当服务器子进程终止时,所有打开着的描述符都被关闭,发送 FIN 给客户端 TCP。之后客户进程发生的事和当【服务器进程中止】时客户进程发生的事相同。