逐步构建HTTP服务器(三)——IO多路复用+非阻塞IO
逐步构建HTTP服务器(三)——IO多路复用+非阻塞IO
为什么使用非阻塞?
在(一)中提到我们目前的使用的都是阻塞的socket。
-
考虑套接字发送缓冲区已满,write阻塞,而此时又有接受缓冲区可供读取。引入非阻塞IO,可避免进程在可做任何有效工作期间发生阻塞。
-
在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
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。直至发送完成,取消关注该套接字的写事件。
所以,我们不仅要关注监听套接字和新连接套接字的读事件,同时也要关注那些未完成发送的套接字的写事件。