逐步构建HTTP服务器(三)——IO多路复用+非阻塞IO

逐步构建HTTP服务器(三)——IO多路复用+非阻塞IO

为什么使用非阻塞?

在(一)中提到我们目前的使用的都是阻塞的socket。

  1. 考虑套接字发送缓冲区已满,write阻塞,而此时又有接受缓冲区可供读取。引入非阻塞IO,可避免进程在可做任何有效工作期间发生阻塞。

  2. man select 中提到使用select时搭配非阻塞socket会更安全。

    On Linux, select() may report a socket file descriptor as "ready for reading", while nevertheless a subsequent read blocks. This could for example happen when data has arrived but upon examination has the wrong checksum and is discarded. There may be other circumstances in which a file descriptor is spuriously reported as ready. Thus it may be safer to use O_NONBLOCK on sockets that should not block.

    在Linux上,select()可能会将套接字文件描述符报告为“准备读取”,但随后的读取会阻塞。例如,当数据已到达但检查时校验和错误并被丢弃时,可能会发生这种情况。在其他情况下,可能会错误地将文件描述符报告为就绪。因此,在不应阻塞的套接字上使用O_NONBLOCK可能更安全。

    为什么 IO 多路复用要搭配非阻塞 IO?

IO多路复用和非阻塞IO

Linux IO模式及 select、poll、epoll详解

尝试非阻塞IO

IO多路复用以epoll为例。

与之前不同,我们需要一个非阻塞的socket,用来监听和接受新连接。

// 获得非阻塞socket:SOCK_NONBLOCK
int listenfd = socket(AF_INET, SOCK_STREAM | SOCK_ NONBLOCK, IPPROTO_TCP);

// 接受连接,创建新socket时,也需通过FLAGS设为非阻塞
int connfd = accept4(listenfd, (sockaddr *) &cliaddr, &clilen, SOCK_NONBLOCK);

其他代码我们先不改动,看看会不会有什么问题,只看对socket有操作的函数:

read():当输入操作不能被满足(对于TCP套接字,即接受缓冲区至少有一个字节的数据可读)则立即返回一个EAGAIN/EWOLDBLOCK错误(通过errno.h中的errno获取最后一次系统的错误代码)。

write():对于TCP,当发送缓冲区中根本没有空间,立即返回一个EAGAIN错误。

accept():当无新的连接到达,accept会立即返回一个EAGAIN错误。

while (1)
{
    int nready = epoll_wait(epollfd, events, OPENMAX, -1);

    for (int i = 0; i != nready; i++)
    {
        // new connection
        if (events[i].data.fd == listenfd)
        {
            ...
        }
        else
        {
            if ((n = read(events[i].data.fd, buf, MAXLINE)) < 0)
            {
                // error
            }
            // close
            else if (n == 0)
            {
                ...
            }
            else
            {
                // !!!
                write(events[i].data.fd, buf, n);
            }
        }
    }
}

仔细看代码,会发现:一旦调用write时,发送缓冲区满了,导致EAGAIN,那么这条消息将永远丢失发送不出去。

简单想法:调用write:1. 一个字节都没发送出去;2. 发送了一部分;3. 全部发送出去。我们可以将未发送出去的部分保存起来,然后利用epoll_wait关注写事件,等该套接字写事件就绪(即发送缓冲区有空间了),就继续尝试write。直至发送完成,取消关注该套接字的写事件。

所以,我们不仅要关注监听套接字新连接套接字的读事件,同时也要关注那些未完成发送的套接字的写事件。

posted @ 2021-08-08 21:35  ithepug  阅读(80)  评论(0编辑  收藏  举报