UNP前六章 回射服务模型 解析

​ 本文试对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);
}

运行逻辑

服务器
  1. 创建监听套接字,监听本主机任意地址上的SERV_PORT(9877)端口,设置对SIGCHLD的handler。
  2. 调用阻塞式accept,等待已完成连接的到来(TCP连接的三次握手是在内核中完成的)。
  3. 服务器得到一个TCP连接以后,就调用fork()派生子进程,同时关闭子进程中的监听套接字,然后调用str_echo()函数。
  4. 在str_echo()函数中,循环阻塞式read并回射给客户端,直到收到FIN结束子进程。
客户端
  1. 创建主动套接字,向指定IP地址的SERV_PORT端口发起connect连接。
  2. 运行str_cli()函数,同时设置fd_set初值为输入文件描述符fileno(fp)和套接字sockfd。调用阻塞式select监听二者。
  3. sockfd对应TCP连接,当服务器端传回数据时其变为可读;fileno(fp)对应标准输入,当用户输入时变为可读。二者任何之一变为可读状态都会是select设置相应fd_set并返回。
  4. if判断是哪个fd触发的select返回,然后读取TCP连接数据或者读取用户输入并发送TCP数据。

要点

服务器
  1. 服务器必须捕获子进程的终止信号(第41行),并在其中循环调用非阻塞式waitpid(第6行)。如果不捕获子进程的终止信号,那么子进程的数据将一直保留在内存中占用系统资源(除非父进程退出)。另外因为可能有多个子进程同时结束,那么服务器进程将收到多个连续的SIGCHLD,而UNIX信号默认是不排队的,所以有可能只能触发一次sig_chld()调用,我们需要在这一次sig_chld()调用中将所有已经结束的子进程全部回收(未结束的子进程不需要考虑,因为它们的终止将会引起新的SIGCHLD信号和下一次sig_chld()调用)。waitpid(-1, &stat, WNOHANG)将等待任意一个已终止的子进程,如果没有已终止的待回收子进程那么就立即返回0,这就实现了一次sig_chld()调用收集所有待回收子进程。

  2. **服务器需要处理满系统调用accept(第47行)和read(第19行)被中断的问题。**因为当阻塞于慢系统调用的一个进程捕获某个信号后,当信号处理函数返回时,此系统调用被中断并返回一个EINTR错误(尽管有些系统的内核可以自动重启这个被中断的系统调用)。所以我们要在识别出errno为EINTR时重启此系统调用。

  3. **服务器在fork()后需要分别关闭父进程的已连接套接字(第60行)和子进程的监听套接字(第56行)。**因为fork()后文件父进程的文件描述符会被复制到子进程中,而且我们要知道每个文件或套接字都有一个引用计数,只有计数到达0时相关资源才会被清理。如果我们不及时关闭本进程不用的套接字,那么另一个进程close掉这个套接字后清理工作就不能及时进行(要等到本进程退出时自动关闭所有套接字时才会清理)。

客户端
  1. **客户端必须使用select轮询sockfd和fileno(fp)(第18行)。**如果采用简单的线性模型(gets->write->read),即阻塞于用户输入,然后将用户输入发送给TCP接收方,等待回射数据后输出到显示器。那么当服务器异常终止时,此时服务器发送FIN并被客户所接受,那么客户接下来对sockfd的读写都不被允许,但问题在于客户进程往往被阻塞在等待用户输入上,只有在用户输入后客户write发送数据后(服务器将返回RST,但时间原因客户进程将在收到RST前执行read()),再尝试调用read()函数时才会得到EOF错误,这是一种用户体验并不好的延迟错误告知。 而是用select轮询后,在用户输入结束前,客户将立即得到FIN的通知,并在第26行退出。

  2. **客户端必须使用stdineof标志来标记用户输入是否结束(第13、23、33行),并只有在从sockfd读取数据结束时才退出进程(第21行)。**首先,用户输入结束并不能立即退出程序,因为在大批量输入时(尤见于从文件输入时),客户端发送数据后还要等待服务器回射然后再输出到显示器上,如果在回射前就退出程序,那么就会导致回射数据丢失。讽刺的是,在非select的线性模型中并不会发生这个问题,在那里,客户进程一定等到write->read后再退出。

  3. **客户端应使用shutdown(sockfd)来代替close(sockfd)。**主要的原因是close只有在将sockfd的引用计数减到0时才开始四次挥手,而shutdown立即发送FIN给服务器。另一个原因是shutdown可以只关闭写端或读端,而调用close后将关闭读写两端即无法对sockfd进行读写操作,我们需要的只是半关闭写这一半,故用shutdown。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值