Loading

《Unix 网络编程》16:非阻塞式 I/O

非阻塞式 I/O

★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★
本文信息本文信息防爬虫替换信息
作者网站LYMTICShttps://lymtics.top
作者LYMTICS(樵仙)https://lymtics.top
联系方式me@tencent.mlme@tencent.ml
原文标题《Unix 网络编程》16:非阻塞式 I/O - 樵仙 - 博客园《Unix 网络编程》16:非阻塞式 I/O - 樵仙 - 博客园
原文地址https://www.cnblogs.com/lymtics/p/16364065.htmlhttps://www.cnblogs.com/lymtics/p/16364065.html
  • 如果您看到了此内容,则本文可能是恶意爬取原作者的文章,建议返回原站阅读,谢谢您的支持
  • 原文会不断地更新和完善排版和样式会更加适合阅读,并且有相关配图
  • 如果爬虫破坏了上述链接,可以访问 `lymtics.top` 获取更多信息
★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★

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

阅读此文时,如果有哪一段看不懂了,请务必回忆一下所谓“非阻塞”的含义!

概述

★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★
本文信息本文信息防爬虫替换信息
作者网站LYMTICShttps://lymtics.top
作者LYMTICS(樵仙)https://lymtics.top
联系方式me@tencent.mlme@tencent.ml
原文标题《Unix 网络编程》16:非阻塞式 I/O - 樵仙 - 博客园《Unix 网络编程》16:非阻塞式 I/O - 樵仙 - 博客园
原文地址https://www.cnblogs.com/lymtics/p/16364065.htmlhttps://www.cnblogs.com/lymtics/p/16364065.html
  • 如果您看到了此内容,则本文可能是恶意爬取原作者的文章,建议返回原站阅读,谢谢您的支持
  • 原文会不断地更新和完善排版和样式会更加适合阅读,并且有相关配图
  • 如果爬虫破坏了上述链接,可以访问 `lymtics.top` 获取更多信息
★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★

套接字的默认状态是阻塞的,这意味着当发出一个不能立即完成的套接字调用时,其进程将被投入睡眠,等待响应的操作完成。

可能阻塞的套接字调用可以分为如下四类:

(如果不清晰可以尝试放大查看)

注意,返回一个错误并不意味着程序会退出执行,实际上是设置了一个标志位,我们可以忽略这个错误

另外,我们可以之后用 select 来看相应的操作是否完成

非阻塞读写

★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★
本文信息本文信息防爬虫替换信息
作者网站LYMTICShttps://lymtics.top
作者LYMTICS(樵仙)https://lymtics.top
联系方式me@tencent.mlme@tencent.ml
原文标题《Unix 网络编程》16:非阻塞式 I/O - 樵仙 - 博客园《Unix 网络编程》16:非阻塞式 I/O - 樵仙 - 博客园
原文地址https://www.cnblogs.com/lymtics/p/16364065.htmlhttps://www.cnblogs.com/lymtics/p/16364065.html
  • 如果您看到了此内容,则本文可能是恶意爬取原作者的文章,建议返回原站阅读,谢谢您的支持
  • 原文会不断地更新和完善排版和样式会更加适合阅读,并且有相关配图
  • 如果爬虫破坏了上述链接,可以访问 `lymtics.top` 获取更多信息
★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★

非阻塞 IO 版本

预备

回忆之前的版本:

我们之前在 IO 多路复用中,用 SELECT 改进了我们 str_cli 的代码,使得它可以同时监听多个描述符的事件,但是,这仍然属于阻塞式 IO,其在发生阻塞时的行为和我们上面图片展示的相似。

我们要写的版本的注意事项:

  • 非阻塞 IO 的加入让缓冲区管理显著地复杂化了
  • 在套接字上使用标准 IO 存在潜在的问题和困难,所以我们将继续避免使用它们

缓冲区模型:

我们要维护两个缓冲区:

  • to 容纳从标准输入到服务器去的数据
  • fr 容纳从服务器到标准输出的数据

如下图所示:

代码

一段超级长的代码,但是只要记住它的大概流程就可以更好地理解整个程序:

  • 首先是初始化代码,包括设置描述符为非阻塞、select 相关的初始化、前文提到的几个计数指针
  • 然后是对 select 监听的各种事件的处理:标准输入可读、套接字可读、标准输出可写、套接字可写
  • 在每个事件的处理中,要尝试读取数据,并根据是否成功做进一步的操作

strclinonb.c

void str_cli(FILE* fp, int sockfd) {
    int maxfdp1, val, stdineof;
    ssize_t n, nwritten;
    fd_set rset, wset;
    char to[MAXLINE], fr[MAXLINE];
    char *toiptr, *tooptr, *friptr, *froptr;

    // 使用 fcntl 把所用的 3 个描述符都设置为非阻塞
    // 连接服务器的套接字、标准输入和标准输出
    val = Fcntl(sockfd, F_GETFL, 0);
    Fcntl(sockfd, F_SETFL, val | O_NONBLOCK);

    val = Fcntl(STDIN_FILENO, F_GETFL, 0);
    Fcntl(STDIN_FILENO, F_SETFL, val | O_NONBLOCK);

    val = Fcntl(STDOUT_FILENO, F_GETFL, 0);
    Fcntl(STDOUT_FILENO, F_SETFL, val | O_NONBLOCK);

    // 初始化上图中提到的那几个指针
    toiptr = tooptr = to;
    friptr = froptr = fr;
    stdineof = 0;

    // 计算最大描述符,并加一,之后调用 select 时会用到
    maxfdp1 = max(max(STDIN_FILENO, STDOUT_FILENO), sockfd) + 1;

    for (;;) {
        // 为 select 做准备
        FD_ZERO(&rset);
        FD_ZERO(&wset);
        if (stdineof == 0 && toiptr < &to[MAXLINE])
            FD_SET(STDIN_FILENO, &rset); /* read from stdin */
        if (friptr < &fr[MAXLINE])
            FD_SET(sockfd, &rset); /* read from socket */
        if (tooptr != toiptr)
            FD_SET(sockfd, &wset); /* data to write to socket */
        if (froptr != friptr)
            FD_SET(STDOUT_FILENO, &wset); /* data to write to stdout */

        // 等待上述条件之一为真,这里我们没有设置超时
        Select(maxfdp1, &rset, &wset, NULL, NULL);
        /* end nonb1 */
        /* include nonb2 */
        // 如果标准输入可读
        if (FD_ISSET(STDIN_FILENO, &rset)) {
            // 尝试读取,注意我们现在已经使用非阻塞模式了
            if ((n = read(STDIN_FILENO, toiptr, &to[MAXLINE] - toiptr)) < 0) {
                // 我们忽略了 EWOULDBLOCK 错误,因为这通常情况下不应该发生
                // (这意味着 select 告诉我们可读, read 却读取失败)
                if (errno != EWOULDBLOCK)
                    // 读取失败,报错
                    err_sys("read error on stdin");

            // n == 0 说明标准输入处理到此结束
            } else if (n == 0) {
                // 输出信息和时间,以表示这个 EOF
                fprintf(stderr, "%s: EOF on stdin\n", gf_time());
                // 设置 stdineof = 1, 表示输入结束
                // 后面会根据此标志进行关闭等操作 
                stdineof = 1; /* all done with stdin */
                if (tooptr == toiptr)
                    Shutdown(sockfd, SHUT_WR); /* send FIN */

            // 读取到了数据
            } else {
                fprintf(stderr, "%s: read %d bytes from stdin\n", gf_time(), n);
                toiptr += n;           /* # just read */
                // 打开套接字相关的描述符
                FD_SET(sockfd, &wset); /* try and write to socket below */
            }
        }

        // 如果 socket 可读,注意上面5行前设置了这个标志
        if (FD_ISSET(sockfd, &rset)) {
            // 尝试读取
            if ((n = read(sockfd, friptr, &fr[MAXLINE] - friptr)) < 0) {
                // 如果是 EWOULDBLOCK, 则不阻塞,而重新进入下一个 if 中
                if (errno != EWOULDBLOCK)
                    err_sys("read error on socket");
            } else if (n == 0) {
                fprintf(stderr, "%s: EOF on socket\n", gf_time());
                // 如果前面设置了这个标志,说明是从标准输入读入了 EOF
                // 所以正常结束
                if (stdineof)
                    return; /* normal termination */
                // 否则,报错
                else
                    err_quit("str_cli: server terminated prematurely");

            } else {
                fprintf(stderr, "%s: read %d bytes from socket\n", gf_time(), n);
                friptr += n;                  /* # just read */
                FD_SET(STDOUT_FILENO, &wset); /* try and write below */
            }
        }
        /* end nonb2 */
        /* include nonb3 */
        // 标准输出是否准备就绪,并且有可以写的数据,注意这里有两个判断条件
        if (FD_ISSET(STDOUT_FILENO, &wset) && ((n = friptr - froptr) > 0)) {
            if ((nwritten = write(STDOUT_FILENO, froptr, n)) < 0) {
                if (errno != EWOULDBLOCK)
                    err_sys("write error to stdout");

            } else {
                fprintf(stderr, "%s: wrote %d bytes to stdout\n", gf_time(),
                        nwritten);
                froptr += nwritten; /* # just written */
                // 如果赶上了,就归位
                if (froptr == friptr)
                    froptr = friptr = fr; /* back to beginning of buffer */
            }
        }

        // 检查 socket 是否可写,并且有能够发送的
        if (FD_ISSET(sockfd, &wset) && ((n = toiptr - tooptr) > 0)) {
            if ((nwritten = write(sockfd, tooptr, n)) < 0) {
                if (errno != EWOULDBLOCK)
                    err_sys("write error to socket");

            } else {
                fprintf(stderr, "%s: wrote %d bytes to socket\n", gf_time(),
                        nwritten);
                tooptr += nwritten; /* # just written */
                // 归位
                if (tooptr == toiptr) {
                    toiptr = tooptr = to; /* back to beginning of buffer */
                    // 由于 tooptr == toiptr ,所以没有需要发送的数据了
                    // 检查标志,如果用户输入了 EOF,就发送一个 FIN
                    if (stdineof)
                        Shutdown(sockfd, SHUT_WR); /* send FIN */
                }
            }
        }
    }
}
/* end nonb3 */

其中使用到了一个可以返回当前时间的函数,代码如下:

gf_time.c

char* gf_time(void) {
    struct timeval tv;
    time_t t;
    static char str[30];
    char* ptr;

    if (gettimeofday(&tv, NULL) < 0)
        err_sys("gettimeofday error");

    t = tv.tv_sec; /* POSIX says tv.tv_sec is time_t; some BSDs don't agree. */
    ptr = ctime(&t);
    strcpy(str, &ptr[11]);
    /* Fri Sep 13 00:00:00 1986\n\0 */
    /* 0123456789012345678901234 5  */
    snprintf(str + 8, sizeof(str) - 8, ".%06ld", tv.tv_usec);

    return (str);
}

运行

原文使用了 tcpdump 进行操作,但是由于我没有学习这个,并且按照原文的方法无法正确使用,所以这里只列出相关的操作,之后再做补充

用如下命令捕获去往或来自端口 7 的TCP分节,输出存储在 tcpd 中:

tcpdump -w tcpd tcp and port 7

运行服务器程序,对应在 tcpcliserv 目录下的任一服务器代码,记得修改端口

运行客户端程序,其中 2000.lines 是一个 2000 行的文件:

tcpcli02 127.0.0.1 < 2000.lines > out 2> diag

最后,查看区别:

diff 2000.lines out

如下是其的时间线:

多进程版本

思考

你可能也感觉到了,上面的代码十分复杂,为了程序的效率而提升代码的复杂度可能是得不偿失的。

每当我们发现需要使用非阻塞 IO 时,更简单的方法是把应用程序任务划分到多个进程或多个线程中。

多进程代码

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

    if ((pid = Fork()) == 0) { /* child: server -> stdout */
        while (Readline(sockfd, recvline, MAXLINE) > 0)
            Fputs(recvline, stdout);

        kill(getppid(), SIGTERM); /* in case parent still running */
        exit(0);
    }

    /* parent: stdin -> server */
    while (Fgets(sendline, MAXLINE, fp) != NULL)
        Writen(sockfd, sendline, strlen(sendline));

    Shutdown(sockfd, SHUT_WR); /* EOF on stdin, send FIN */
    pause();
    return;
}

注意点如下:

  • 父子进程可以共享一个套接字,以及接收和发送缓冲区

  • 当父进程读取到 EOF 时,调用 Shutdown 而不是 Close,从而子进程可以继续正常工作

  • 如果服务器方面断开连接,则子进程有必要告诉父进程不要再发了,所以子进程发送 SIGTERM 信号

    另一种方法是子进程终止,使得父进程捕获一个 SIGTERM 信号

  • 父进程读取完数据后,会调用 pause 等待子进程完成;子进程完成会发送一个 SIGTERM 信号,由于我们没有捕捉这个信号,所以其默认行为为终止;这样做的目的是为了统计时间

总结

我们已经编写了 str_cli 函数的四个不同的版本,总结如下:

版本 对上一版本的改进 运行时间 代码量
停等版本 - 354s 12行
select 加阻塞版本 像流水线一样发送
用 select 同时监听套接字和文件描述符
12.3s 40行
非阻塞 IO 版本(本章) 缓冲区不可用时直接进入下一轮循环
不会因为要等待缓冲区的准备而浪费时间
6.9s 130行
fork 版本(本章) 简化了代码的逻辑
缓冲区分给了不同的进程,不需要使用非阻塞 IO
8.5s 20行

后文还会提到线程化版本

一个思考,为什么在这个案例中,非阻塞 IO 比 fork 版本要快?是因为线程切换的成本吗?

非阻塞 connect

★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★
本文信息本文信息防爬虫替换信息
作者网站LYMTICShttps://lymtics.top
作者LYMTICS(樵仙)https://lymtics.top
联系方式me@tencent.mlme@tencent.ml
原文标题《Unix 网络编程》16:非阻塞式 I/O - 樵仙 - 博客园《Unix 网络编程》16:非阻塞式 I/O - 樵仙 - 博客园
原文地址https://www.cnblogs.com/lymtics/p/16364065.htmlhttps://www.cnblogs.com/lymtics/p/16364065.html
  • 如果您看到了此内容,则本文可能是恶意爬取原作者的文章,建议返回原站阅读,谢谢您的支持
  • 原文会不断地更新和完善排版和样式会更加适合阅读,并且有相关配图
  • 如果爬虫破坏了上述链接,可以访问 `lymtics.top` 获取更多信息
★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★

三个用途:

  • 完成一个 connect 需要至少一个 RTT,这个时间可以做别的事情
  • 可以使用这个技术同时建立多个连接,如 Web 浏览器
  • 给 connect 设置超时(其中部分方法在此文中已经提到过了)

两个细节:

  • 尽管套接字是非阻塞的,但是如果连接到的服务器在同一个主机上,连接通常立即建立

  • 源自 Berkeley 的实现有关于 select 和非阻塞 connect 的一下两个规则::

    1. 当连接建立成功,描述符变为可写
    2. 当连接建立遇到错误时,描述符变为可读可写

时间获取客户程序

代码

首先是修改时间获取程序的发起代码:

if (connect_nonb(sockfd, (SA*) &servaddr, sizeof(servaddr), 0) < 0)
  err_sys("connect error");

如下是 connect_nonb.c

int connect_nonb(int sockfd, const SA* saptr, socklen_t salen, int nsec) {
    int flags, n, error;
    socklen_t len;
    fd_set rset, wset;
    struct timeval tval;

    // 设置为非阻塞
    flags = Fcntl(sockfd, F_GETFL, 0);
    Fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);

    error = 0;
    // 发起非阻塞 connect
    if ((n = connect(sockfd, saptr, salen)) < 0)
        // 忽略 E.IN.PROGRESS 错误
        if (errno != EINPROGRESS)
            return (-1);

    /* Do whatever we want while the connect is taking place. */

    // 处理直接就建立好了的情况
    if (n == 0)
        goto done; /* connect completed immediately */

    // 用 select 建立对连接情况的监听
    // 同时还有超时相关的处理操作
    FD_ZERO(&rset);
    FD_SET(sockfd, &rset);
    wset = rset;
    tval.tv_sec = nsec;
    tval.tv_usec = 0;

    // 监听,等于 0 说明此时发生了超时
    if ((n = Select(sockfd + 1, &rset, &wset, NULL, nsec ? &tval : NULL)) == 0) {
        close(sockfd); /* timeout */
        errno = ETIMEDOUT;
        return (-1);
    }

    // 如果发生可读或可写事件:
    if (FD_ISSET(sockfd, &rset) || FD_ISSET(sockfd, &wset)) {
        len = sizeof(error);
        // 由于有可能在调用 select 之前就已经成功建立了连接,套接字变得可读可写
        // 这和建立连接错误时的行为是一致的,所以我们调用 getsockopt 并检查是否有待处理的错误来处理这种情况
        // 这里存在兼容性问题,getsockopt 不同的实现上行为不太一致,只需知道这里进行错误判断就行了
        if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) < 0)
            return (-1); /* Solaris pending error */
    } else
        err_quit("select error: sockfd not set");

done:
    Fcntl(sockfd, F_SETFL, flags); /* restore file status flags */

    if (error) {
        close(sockfd); /* just in case */
        errno = error;
        return (-1);
    }
    return (0);
}

移植性问题

我们的关键问题是:怎样判断连接是否建立成功

因为:有可能在调用 select 之前,连接就已经建立成功,而且对方的数据已经到来,在这种情况下,套接口描述符是既可读又可写,这与套接口描述符出错时是一样的

上面的代码是如何解决的:使用 getsockopt 查看是否返回错误信息

但是:getsockopt 的返回值在不同的实现上是不同的

  • 源自 Berkeley 的实现是返回 0 ,待处理的错误值存储在 errno 中
  • 而源自 Solaris 的实现是返回 -1 ,待处理的错误存储在 errno 中

尽管如此,上面的代码逻辑可以处理这两种情况

一些可以替代 getsockopt 的其他的解决方法包括:

  1. 调用获取对端 socket 地址的 getpeername 代替 getsockopt 。如果调用 getpeername 失败,getpeername 返回 ENOTCONN ,表示连接建立失败,之后我们必须再以 SO_ERROR 调用 getsockopt 得到套接口描述符上的待处理错误
  2. 调用 read ,读取长度为 0 字节的数据。如果连接建立失败,则 read 会返回 -1 ,且相应的 errno 指明了连接失败的原因;如果连接建立成功,read 应该返回 0
  3. 再调用一次 connect 。它应该失败,如果错误 errno 是 EISCONN ,就表示套接口已经建立,而且第一次连接是成功的;否则,连接就是失败的

非阻塞 connect 是网络编程中最不易移植的部分。避免这一问题的一个较简单的技术是为每一个连接创建一个线程。

判断被中断的 connect:

对于一个正常的阻塞式套接字,如果其上的 connect 调用在 TCP 连接完成前被终止,则它将返回 E.INTE,我们不能再调用 connect ,这样将返回 E.ADDR.IN.USE 错误。

这种情景下我们只能用像本章这样的方法,对该套接字是否连接成功进行判断。

个人备注

这里我们确实没有在 connect 上阻塞了,但是却是在 select 上阻塞(或者说在 connect_nonb 这个函数上阻塞了)

如果我们确实想做一些其他的事情,似乎只能对 connect_nonb 这个函数进行修改,这样不就有一些不实用了

Web客户程序

模型

假设一个网页中有若干张图片,则有若干种可能的加载方式:

  1. 一次只能发起一个请求,需要等数据读取完毕后再发起另一个
  2. 一次可以发送 N 个请求

对于阻塞式 connect,效率比较低,因此本章的代码将用非阻塞式 IO 进行改善

整体流程如下:

  • 先读取一个首页
  • 然后根据首页中的内容加载图片资源

(当然这里为了简化操作,只是模拟了这个过程)

代码

在所有的文件中通用的头文件 web.h

// 定义常量
#define MAXFILES 20 // 最大连接数量
#define SERV "80" /* port number or service name */

// 文件的结构
struct file {
    char* f_name; // 对应命令行输入的文件路径
    char* f_host; // 服务器所在的主机名或服务器地址
    int f_fd;     // 文件的套接字描述符
    int f_flags;  // 指定准备对文件进行的操作
} file[MAXFILES];

/// 定义操作常量
#define F_CONNECTING 1 /* connect() in progress */
#define F_READING 2    /* connect() complete; now reading */
#define F_DONE 4       /* all done */

#define GET_CMD "GET %s HTTP/1.0\r\n\r\n"

/* globals */
int nconn, nfiles, nlefttoconn, nlefttoread, maxfd;
fd_set rset, wset;

/* function prototypes */
void home_page(const char*, const char*);
void start_connect(struct file*);
void write_get_cmd(struct file*);

主函数 web.c

int main(int argc, char** argv) {
    int i, fd, n, maxnconn, flags, error;
    char buf[MAXLINE];
    fd_set rs, ws;

    if (argc < 5)
        err_quit("usage: web <#conns> <hostname> <homepage> <file1> ...");
    // 用户设置的最大连接数
    maxnconn = atoi(argv[1]);

    // 以来自命令行参数的相关信息填写 file 数组
    nfiles = min(argc - 4, MAXFILES);
    for (i = 0; i < nfiles; i++) {
        file[i].f_name = argv[i + 4];
        file[i].f_host = argv[2];
        file[i].f_flags = 0;
    }
    printf("nfiles = %d\n", nfiles);

    // 加载首页
    home_page(argv[2], argv[3]);

    // 初始化描述符
    FD_ZERO(&rset);
    FD_ZERO(&wset);
    // 最大描述符为 -1 ,表示还没开始
    maxfd = -1;
    // 剩余需要读取的数量 - 剩余需要连接的数量 - 文件的数量
    nlefttoread = nlefttoconn = nfiles;
    nconn = 0; // 连接数

    // 循环部分
    while (nlefttoread > 0) {
        // 如果没有达到最大并行连接数,并且另有连接需要建立
        while (nconn < maxnconn && nlefttoconn > 0) {
            /* 4find a file to read */
            for (i = 0; i < nfiles; i++)
                if (file[i].f_flags == 0)
                    break;
            if (i == nfiles)
                err_quit("nlefttoconn = %d but nothing found", nlefttoconn);
            // 那就找到一个尚未处理的文件,然后调用 start_conn 发起另一个请求
            start_connect(&file[i]);
            // 活跃连接数 + 1
            nconn++;
            // 没有处理的文件数量 - 1
            nlefttoconn--;
        }

        // select 监听事件
        rs = rset;
        ws = wset;
        n = Select(maxfd + 1, &rs, &ws, NULL, NULL);

        // 对于每一个文件
        for (i = 0; i < nfiles; i++) {
            flags = file[i].f_flags;
            if (flags == 0 || flags & F_DONE)
                continue;
            fd = file[i].f_fd;
            // 标志位为连接中,套接字可读可写
            if (flags & F_CONNECTING &&
                (FD_ISSET(fd, &rs) || FD_ISSET(fd, &ws))) {
                n = sizeof(error);
                // 使用了前文的方法判断套接字是否成功打开
                if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &n) < 0 ||
                    error != 0) {
                    err_ret("nonblocking connect failed for %s",
                            file[i].f_name);
                }
                /* 4connection established */
                printf("connection established for %s\n", file[i].f_name);
                FD_CLR(fd, &wset);       /* no more writeability test */
                write_get_cmd(&file[i]); /* write() the GET command */

            // 标志位为读、描述符可读
            } else if (flags & F_READING && FD_ISSET(fd, &rs)) {
                // 读取
                if ((n = Read(fd, buf, sizeof(buf))) == 0) {
                    printf("end-of-file on %s\n", file[i].f_name);
                    Close(fd);
                    file[i].f_flags = F_DONE; /* clears F_READING */
                    FD_CLR(fd, &rset);
                    nconn--;
                    nlefttoread--;
                } else {
                    printf("read %d bytes from %s\n", n, file[i].f_name);
                }
            }
        }
    }
    exit(0);
}

加载首页的代码 home_page.c

void home_page(const char* host, const char* fname) {
    int fd, n;
    char line[MAXLINE];

    // 建立连接
    fd = Tcp_connect(host, SERV); /* blocking connect() */

    n = snprintf(line, sizeof(line), GET_CMD, fname);
    Writen(fd, line, n);

    for (;;) {
        // 读取应答(不做任何处理)
        if ((n = Read(fd, line, MAXLINE)) == 0)
            break; /* server closed connection */

        printf("read %d bytes of home page\n", n);
        /* do whatever with data */
    }
    printf("end-of-file on home page\n");
    // 关闭连接
    Close(fd);
}

start_connect:非阻塞的发起连接:

void start_connect(struct file* fptr) {
    int fd, flags, n;
    struct addrinfo* ai;

    // 调用我们自己的 host_serv 查找并转换主机名和服务名
    // 返回指向某个 addrinfo 结构数组的一个指针
    ai = Host_serv(fptr->f_host, SERV, 0, SOCK_STREAM);

    // 创建套接字
    fd = Socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
    fptr->f_fd = fd;
    printf("start_connect for %s, fd %d\n", fptr->f_name, fd);

    // 设置为非阻塞
    flags = Fcntl(fd, F_GETFL, 0);
    Fcntl(fd, F_SETFL, flags | O_NONBLOCK);

    /* 4Initiate nonblocking connect to the server. */
    // 发起非阻塞 connect
    if ((n = connect(fd, ai->ai_addr, ai->ai_addrlen)) < 0) {
        if (errno != EINPROGRESS)
            err_sys("nonblocking connect error");
        // 设置文件状态
        fptr->f_flags = F_CONNECTING;
        // 设置 select 
        FD_SET(fd, &rset); /* select for reading and writing */
        FD_SET(fd, &wset);
        if (fd > maxfd)
            maxfd = fd;

    } else if (n >= 0)       /* connect is already done */
        // 连接直接建立好了,那就写数据
        write_get_cmd(fptr); /* write() the GET command */
}

write_get_cmd:写输出的相关代码:

void write_get_cmd(struct file* fptr) {
    int n;
    char line[MAXLINE];

    n = snprintf(line, sizeof(line), GET_CMD, fptr->f_name);
    Writen(fptr->f_fd, line, n);
    printf("wrote %d bytes for %s\n", n, fptr->f_name);

    // 设置状态为正在读取
    fptr->f_flags = F_READING; /* clears F_CONNECTING */

    // 设置标志位,表明已经准备好了提供输入
    FD_SET(fptr->f_fd, &rset); /* will read server's reply */
    if (fptr->f_fd > maxfd)
        maxfd = fptr->f_fd;
}

总结

时序图如下:

sequenceDiagram participant wc as web.c participant hp as home_page.c participant sc as start_connect.c participant wg as write_get_cmd.c note over wc: 读取用户命令行的输入,保存起来 wc ->> hp: 加载首页 note over hp: 建立连接<br>读取数据<br>关闭连接 hp ->> wc: return note over wc: 初始化 select 和其他变量 loop 当文件读取还没有完成 loop 当还没有达到最大连接数 note over wc: 找到一个没有连接的地址 wc -->> sc: 发起非阻塞了连接 note over sc: 创建套接字<br>发起非阻塞连接 alt 连接直接建立了 sc ->> wg: 读取文件内容 end sc -->> wc: return end loop 对于每个已经建立连接但还没处理完的文件 alt 标志位为连接中,描述符可读可写 note over wc: 检查是否成功建立连接<br>设置状态位 else 标志位为读取中,描述符可读 wc ->> wg: 读取文件内容 wg ->> wc: return end end end

性能问题:

  • 同时连接数越多,理论上性能越好,但是性能的提升会越来越少
  • 如果网络中存在拥塞,此时发送过多的请求会让网络更加拥堵,从而起到相反的效果

非阻塞 accept

★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★
本文信息本文信息防爬虫替换信息
作者网站LYMTICShttps://lymtics.top
作者LYMTICS(樵仙)https://lymtics.top
联系方式me@tencent.mlme@tencent.ml
原文标题《Unix 网络编程》16:非阻塞式 I/O - 樵仙 - 博客园《Unix 网络编程》16:非阻塞式 I/O - 樵仙 - 博客园
原文地址https://www.cnblogs.com/lymtics/p/16364065.htmlhttps://www.cnblogs.com/lymtics/p/16364065.html
  • 如果您看到了此内容,则本文可能是恶意爬取原作者的文章,建议返回原站阅读,谢谢您的支持
  • 原文会不断地更新和完善排版和样式会更加适合阅读,并且有相关配图
  • 如果爬虫破坏了上述链接,可以访问 `lymtics.top` 获取更多信息
★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★

似乎不需要

回忆一下:

  1. accept 函数由 TCP server 调用,用于从已完成连接队列队头返回下一个已完成连接;如果该队列为空,则睡眠(阻塞)
  2. accept 非阻塞是指,调用时如果已完成连接队列为空,则不进行阻塞,直接返回

当有一个已经完成的连接准备好 accept 时,select 将作为可读描述符返回该连接的监听套接字。

因此,如果我们使用 select 在某个监听套接字上等待一个外来连接,那就没有必要把该监听套接字设置为非阻塞,这是因为如果 select 告诉我们该套接字上已经有连接就绪,那么随后的 accept 就不应该阻塞。

所以我们似乎不需要这个功能?

但是有问题

但是如果使用阻塞版本,可能会有一个定时问题:

如下代码,假如我们在关闭时发送一个 RST

int main(int argc, char** argv) {
    int sockfd;
    struct linger ling;
    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));

  	// 当建立连接后,设置如下两个参数
  	// 由于这两个参数的设置,下面我们关闭的时候就会发送一个 RST
    ling.l_onoff = 1; /* cause RST to be sent on close() */
    ling.l_linger = 0;
    Setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &ling, sizeof(ling));
    Close(sockfd);

    exit(0);
}

而服务器又比较繁忙,它无法在 select 返回监听套接字的可读条件后就立马调用 accept

当客户在服务器调用 accept 之前终止某个连接时,源自 Berkeley 的实现不把这个终止的连接返回给服务器,而其他实现应该返回 E.CONN.ABORTED 错误,却往往代之以返回 E.PROTO 错误;

考虑在前面一种情况下的这个过程:

  • 客户建立一个连接,然后终止它
  • select 向服务器进程返回可读,服务器过了若干时间才调用 accept
  • 在服务器从 select 返回到调用 accept 期间,服务器 TCP 收到了客户的 RST
  • 这个已完成的连接被服务器 TCP 驱逐出队列,我们假设此时队列中没有其他已完成的连接
  • 服务器调用 accept ,但是由于没有任何已完成的连接,服务器阻塞
  • 服务器一直阻塞在 accept 调用上,无法处理其他已经就绪的描述符,直到有新的连接建立起来

解决办法

  1. 当使用 select 获悉某个监听套接字上何时有已经完成的连接准备好被 accept 时,总是把这个监听套接字设置为非阻塞
  2. 在后续的 accept 调用中忽略如下表格中的错误
错误 实现来源 发生时机
E.WOULD.BLOCK Berkeley 客户终止连接时
E.CONN.ABORTED POSIX 客户终止连接时
E.PROTO SVR4 客户终止连接时
E.INTE 如果有信号被捕获
posted @ 2022-06-10 21:34  樵仙  阅读(99)  评论(0编辑  收藏  举报