本文试对UNP一书中截止到第六章的回射服务模型进行剖析,重点在于讲解代码为什么怎么做。下面的代码分别对应书中的5-12 tcpserv04.c
,6-13 strcliselect02.c
。
服务器代码:
#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;
}
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");
}
int main(int argc, char **argv)
{
int listenfd, connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
Bind(listenfd, (SA *)&servaddr, sizeof(servaddr));
Listen(listenfd, LISTENQ);
signal(SIGCHLD, sig_chld);
for (;;)
{
clilen = sizeof(cliaddr);
if ((connfd = Accept(listenfd, (SA *)&cliaddr, &clilen)) < 0)
{
if (errno == EINTR)
continue;
else
err_sys("accept error");
}
// 子进程退出会导致信号SIGCHLD到来,它会引起sig_chld的异步执行,当从sig_chld()返回时,accept系统调用被打断
// bsd4.4会自动重启accept系统调用,否则就会触发一个
if ((childpid = Fork()) == 0)
{
Close(listenfd);
str_echo(connfd);
exit(0);
}
Close(connfd);
}
}
客户端代码:
#include <unp.h>
void str_cli(FILE *fp, int sockfd)
{
int maxfdp1, stdineof = 0;
fd_set rset;
char buf[MAXLINE];
int n;
FD_ZERO(&rset);
for (;;)
{
if (!stdineof)
FD_SET(fileno(fp), &rset);
FD_SET(sockfd, &rset);
maxfdp1 = max(fileno(fp), sockfd) + 1;
Select(maxfdp1, &rset, NULL, NULL, NULL);
if (FD_ISSET(sockfd, &rset))
{
if ( (n = Read(sockfd, buf, MAXLINE)) == 0)
{
if (stdineof == 1)
return;
else
err_quit("str_cli: server terminated prematurely");
}
Write(fileno(stdout), buf, n);
}
if (FD_ISSET(fileno(fp), &rset))
{
if ((n = read(fileno(fp), buf, MAXLINE)) == 0) {
stdineof = 1;
Shutdown(sockfd, SHUT_WR);
FD_CLR(fileno(fp), &rset);
continue;
}
Writen(sockfd, buf, n);
}
}
}
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, &servaddr, sizeof(servaddr));
str_cli(stdin, sockfd);
exit(0);
}
运行逻辑
服务器
- 创建监听套接字,监听本主机任意地址上的SERV_PORT(9877)端口,设置对SIGCHLD的handler。
- 调用阻塞式accept,等待已完成连接的到来(TCP连接的三次握手是在内核中完成的)。
- 服务器得到一个TCP连接以后,就调用fork()派生子进程,同时关闭子进程中的监听套接字,然后调用str_echo()函数。
- 在str_echo()函数中,循环阻塞式read并回射给客户端,直到收到FIN结束子进程。
客户端
- 创建主动套接字,向指定IP地址的SERV_PORT端口发起connect连接。
- 运行str_cli()函数,同时设置fd_set初值为输入文件描述符
fileno(fp)
和套接字sockfd
。调用阻塞式select监听二者。 - sockfd对应TCP连接,当服务器端传回数据时其变为可读;fileno(fp)对应标准输入,当用户输入时变为可读。二者任何之一变为可读状态都会是select设置相应fd_set并返回。
- if判断是哪个fd触发的select返回,然后读取TCP连接数据或者读取用户输入并发送TCP数据。
要点
服务器
-
服务器必须捕获子进程的终止信号(第41行),并在其中循环调用非阻塞式waitpid(第6行)。如果不捕获子进程的终止信号,那么子进程的数据将一直保留在内存中占用系统资源(除非父进程退出)。另外因为可能有多个子进程同时结束,那么服务器进程将收到多个连续的SIGCHLD,而UNIX信号默认是不排队的,所以有可能只能触发一次sig_chld()调用,我们需要在这一次sig_chld()调用中将所有已经结束的子进程全部回收(未结束的子进程不需要考虑,因为它们的终止将会引起新的SIGCHLD信号和下一次sig_chld()调用)。
waitpid(-1, &stat, WNOHANG)
将等待任意一个已终止的子进程,如果没有已终止的待回收子进程那么就立即返回0,这就实现了一次sig_chld()调用收集所有待回收子进程。 -
**服务器需要处理满系统调用accept(第47行)和read(第19行)被中断的问题。**因为当阻塞于慢系统调用的一个进程捕获某个信号后,当信号处理函数返回时,此系统调用被中断并返回一个EINTR错误(尽管有些系统的内核可以自动重启这个被中断的系统调用)。所以我们要在识别出errno为EINTR时重启此系统调用。
-
**服务器在fork()后需要分别关闭父进程的已连接套接字(第60行)和子进程的监听套接字(第56行)。**因为fork()后文件父进程的文件描述符会被复制到子进程中,而且我们要知道每个文件或套接字都有一个引用计数,只有计数到达0时相关资源才会被清理。如果我们不及时关闭本进程不用的套接字,那么另一个进程close掉这个套接字后清理工作就不能及时进行(要等到本进程退出时自动关闭所有套接字时才会清理)。
客户端
-
**客户端必须使用select轮询sockfd和fileno(fp)(第18行)。**如果采用简单的线性模型(gets->write->read),即阻塞于用户输入,然后将用户输入发送给TCP接收方,等待回射数据后输出到显示器。那么当服务器异常终止时,此时服务器发送FIN并被客户所接受,那么客户接下来对sockfd的读写都不被允许,但问题在于客户进程往往被阻塞在等待用户输入上,只有在用户输入后客户write发送数据后(服务器将返回RST,但时间原因客户进程将在收到RST前执行read()),再尝试调用read()函数时才会得到EOF错误,这是一种用户体验并不好的延迟错误告知。 而是用select轮询后,在用户输入结束前,客户将立即得到FIN的通知,并在第26行退出。
-
**客户端必须使用stdineof标志来标记用户输入是否结束(第13、23、33行),并只有在从sockfd读取数据结束时才退出进程(第21行)。**首先,用户输入结束并不能立即退出程序,因为在大批量输入时(尤见于从文件输入时),客户端发送数据后还要等待服务器回射然后再输出到显示器上,如果在回射前就退出程序,那么就会导致回射数据丢失。讽刺的是,在非select的线性模型中并不会发生这个问题,在那里,客户进程一定等到write->read后再退出。
-
**客户端应使用shutdown(sockfd)来代替close(sockfd)。**主要的原因是close只有在将sockfd的引用计数减到0时才开始四次挥手,而shutdown立即发送FIN给服务器。另一个原因是shutdown可以只关闭写端或读端,而调用close后将关闭读写两端即无法对sockfd进行读写操作,我们需要的只是半关闭写这一半,故用shutdown。