阻塞式IO的回射服务器

版本一
  • 客户
include "unp.h"

void str_cli(FILE* fp, int sockfd);

int main(int argc, char **argv)
{
    int sockfd;
    struct sockaddr_in server_name;
    socklen_t server_name_len = sizeof(server_name);

    if (argc != 2)
       err_quit("usage: IP address");

    if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
       err_quit("socket error");

    bzero(&server_name, sizeof(server_name));
    server_name.sin_family = AF_INET;
    server_name.sin_port = htons(8080);
    inet_pton(AF_INET, argv[1], &server_name.sin_addr);

    connect(sockfd, (SA*)&server_name, server_name_len);

    str_cli(stdin, sockfd);
}

void str_cli(FILE* fp, int sockfd)
{
    char sendline[MAXLINE], recvline[MAXLINE];

    while( fgets(sendline, MAXLINE, fp) != NULL)
    {
        write(sockfd, sendline, strlen(sendline));
        if ( readline(sockfd, recvline, MAXLINE) == 0)
             err_quit("str_cli: server terminated prematurely");

        fputs(recvline, stdout);
    }
}
  • 服务器
#include "unp.h"

void str_echo(int sockfd);

int main(int argc, char** argv)
{
    int listenfd, connfd;
    struct sockaddr_in name, client_name;
    socklen_t client_name_len;
    pid_t child;

    if ( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
       err_quit("socket error");

    bzero(&name, sizeof(name));
    name.sin_family = AF_INET;
    name.sin_port = htons(8080);
    name.sin_addr.s_addr = htonl(INADDR_ANY);

    if ( bind(listenfd, (SA*)&name, sizeof(name)) < 0)
         err_quit("bind");

    if ( listen(listenfd, LISTENQ) < 0)
        err_quit("listen");

    for (; ;) {
        connfd = accept(listenfd, (SA*)&client_name, &client_name_len);
        if ( (child = fork()) == 0) {
            close(listenfd);
            str_echo(connfd);
            exit(0);
        }
        close(connfd);
    }
}

void str_echo(int sockfd)
{
    ssize_t n;
    char buf[MAXLINE];

    while( (n = read(sockfd, buf, MAXLINE)) > 0)
        write(sockfd, buf, n);
    if (n < 0)
       err_sys("str_echo: read error");
}
  • 启动服务器:
./s.out
netstat -nlp | grep 8080    // 查询监听端口为8080的网络进程
tcp        0      0 0.0.0.0:8080            0.0.0.0:*               LISTEN      11459/s.out 
  • 启动客户
./c.out 127.0.0.1

此时客户调用 socket 和 connect,后者引起TCP三路握手,当三路握手完成后,客户阻塞于 str_cli 中的 fgets 中,因为我们还没有输入什么。

而服务器在accept, fork后,子进程调用str_echo,然后阻塞于read之中。另一方面,父进程再次调用accept阻塞,等待客户连接。

至此有3个进程都在睡眠,客户进程,服务器父进程,服务器子进程。

  • 正常终止
    • 当我们在客户上键入EOF,fgets返回一个空指针,于是str_cli函数返回main,客户进程终止。
    • 进程终止时会关闭所有打开描述符,客户TCP将会发送一个FIN给服务器。服务器发送ACK响应,此时服务器套接字处于CLOSE_WAIT,客户套接字处于FIN_WAIT_2状态。
    • 当服务器接收到FIN时,服务器子进程阻塞于readline调用,于是readline返回0。str_echo函数返回,服务器子进程调用exit终止。
    • 服务器子进程打开的描述符随之关闭,这回导致发送最后2个分节,一个从服务器到客户的FIN和一个从客户到服务器的ACK。
阻塞式IO的回射服务器_第1张图片
TCP关闭.jpg
  • 服务器子进程终止时,会给父进程发送一个SIGCHLD信号。该信号的默认行为是忽略,因为我们没有回收子进程,于是子进程变成僵尸进程。
    amdin     11496  0.0  0.0      0     0 pts/14   Z+   20:50   0:00 [s.out]   
    
版本二

增加服务器回收子进程的处理,防止僵尸进程,仍然有缺陷。

#include "unp.h"

void etr_echo(int sockfd);
void sig_chld(int signo);

int main(int argc, char **argv)
{
    ......  
    signal(SIGCHLD, sig_chld);

    for ( ; ;)
    {
        if ( (connfd = accept(listenfd, (SA*)&cliaddr, &clilen)) < 0)
            if (errno == EINTR)
                continue;
            else
                err_sys("acccept error");

        if ( (child = fork()) == 0)
        {
            close(listenfd);
            str_echo(connfd);
            exit(0);
        }
        close(connfd);
    }
}

void sig_chld(int signo)
{
    pid_t pid;
    int stat;

    pid = wait(&stat);    // 回收子进程的资源
    printf("child %d terminated\n", pid);
    return;
}

但是这个版本仍然有问题,因为POSIX的系统上信号处理有以下几点:
1、一旦安装了信号处理函数们就会一直安装上。(早期的系统是执行一次 就拆除一次)
2、在一个信号处理函数运行期间,正在被递交的信号是阻塞的。
3、如果一个信号在被阻塞期间产生了一次或多次,那么该信号被接触阻塞之后通常只递交一次,亦即是说,Unix的信号默认是不排队的。

所以假设我们模拟5个客户连接,然后再让这5个客户同一时间终止,就会使服务器的5个进程在同一时刻终止,也就意味着同时有5个SIGCHLD信号递交给父进程,而根据上面信号特点。这意味着并不能将子进程全部回收掉。

// 模拟5个客户连接
for (int i= 0; i < 5; i++) {
    sockfd[i] = socket(AF_INET, SOCK_STREAM, 0);
    bzero(&server, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(8080);
    inet_pton(sockfd[i], argv[1], &server.sin_addr);
  
    connect(sockfd[i], (SA*)&server, sizeof(server)); 
}
str_cli(stdin, sockfd[0]);
exit(0);
// 测试果然有僵尸进程
amdin     12344  0.0  0.0      0     0 pts/2    Z+   00:08   0:00 [s1.out] 
amdin     12345  0.0  0.0      0     0 pts/2    Z+   00:08   0:00 [s1.out] 
amdin     12346  0.0  0.0      0     0 pts/2    Z+   00:08   0:00 [s1.out] 
版本三
// 仅仅修改信号处理函数
void sig_chld(int signo)
{
    pid_t pid;
    int stat;
    
    while( (pid = waitpid(-1, &stat, WNOHANG)) > 0)
        printf("child %d terminated\n", pid);   

    return;
}
  • wait和waitpid的区别
#include 
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);
                                        均返回:若成功则返回进程ID,若出错则为0或者-1

如果调用wait的进程没有已终止的子进程,不过有一个或多个子进程人仍在执行,那么wait将一直阻塞到现有子进程第一个终止为止。

而waitpid给了我们更多的控制:
pid参数表示我们想等待的进程ID参数,-1表示等待第一个终止的子进程。
options参数允许我们指定附加选项。最常用的的选项是WNOHANG,它告知内核在没有已终止子进程时不要阻塞。

特殊情况处理
  • 服务器主机崩溃
    TCP客户会发送数据到服务器,然后等待服务器发回的ACK。此时由于没有收到ACK,TCP客户会持续重传数据分节,超过一定时间后会发送一个RST(从首次重传数据分节到最后发送RST,时间大概为9分钟左右)。
  • 服务器崩溃后重启
    如果服务器崩溃,客户不主动给服务器发送数据,那么它将不会知道服务器主机已经崩溃。当服务器主机崩溃后重启时,它的TCP丢失了崩溃前的所有连接信息,因此服务器TCP对于所收到的来自客户的数据分节响应一个RST。

参考资料
《UNIX 网络编程》3th [美] W.Richard Stevens,Bill Fenner,Andrew M. Rudoff

你可能感兴趣的:(阻塞式IO的回射服务器)