WebServer为什么需要将socket设置为非阻塞?

一些基本概念解释

1.socket文件描述符有哪些

网络中的客户端和服务器进行连接通信时需要建立连接,服务器端需要两个socket文件描述符,分别是建立连接时需要的监听文件描述符listenfd和连接完成后的已连接文件描述符clientfd。客户端需要一个文件描述符connfd负责建立连接和通信。
在这里插入图片描述
accept函数不参与三次握手过程,accept函数从已经连接的队列中取出连接,返回clientfd,最后客户端和服务器分别通过connfd和clientfd进行通信。

2.socket文件描述符设置为阻塞的影响

int clientfd = accept(listenFd_, (struct sockaddr *)&addr, &len);

listenfd文件描述符设置为阻塞后,则如果此时没有连接到达,listenfd阻塞,则会导致accept()函数阻塞。

len = readv(clientfd, iov, 2);
len = writev(clientfd, iov_, iovCnt_);

clientfd文件描述符设置为阻塞后,则如果此时没有数据到达,则会导致读函数阻塞。
如果对端因为TCP窗口太小,不能将数据全部发出去,将会造成写函数阻塞。

二、使用epoll模型将socket设置为非阻塞

epoll模型通常用于服务端,那么讨论的socket只有listenfd和clientfd了。

在需要使用IO复用函数统一管理各个fd的前提下,如果不将clientfd设置成非阻塞模式,那么一旦epoll_wait检测到读或者写事件返回后,接下来处理clientfd的读或者写事件,如果对端因为TCP窗口太小,send函数刚好不能将数据全部发出去,将会造成阻塞,进而导致整个服务“卡住”
对于listenfd,如果不用IO复用函数去管理listenfd,那么不一定要设置为非阻塞,listenfd如果不设置成非阻塞的,那么accept函数在没有新连接时就会阻塞;如果使用IO复用函数统一管理各个fd,那么就必须将listenfd设置为非阻塞的

1.listenfd非阻塞

使用IO多路复用API epoll时,如果设置listenfd阻塞,如果不能建立连接,会在accept处阻塞,从而阻塞整个主线程使其不能执行下去。

2.clientfd非阻塞

先简单说一下将clientfd设置为非阻塞的两个原因:
1.使用epoll的ET模式
2.epoll返回读写事件,但不一定真的可读写

(1)epoll的ET模式

WebServer使用了epoll的IO多路复用技术。

其流程简单的说就是服务器把要监听的listenfd以及clientfd注册到epoll对象上,因为epoll使用事件驱动的机制,内核里维护了一个双向链表来记录就绪事件,当某个socket有事件发生时,通过回调函数,内核会将其加入这个就绪事件链表中。而用户调用epoll_wait时,就只会返回有事件发生的文件描述符的个数,并把就绪事件从内核拷贝到用户态便于程序进行逐个处理。

epoll支持两种事件触发模式:

  • 边缘触发(edge-triggered,ET)
  • 水平触发(level-triggered,LT)

使用边缘触发模式时,当被监控的 Socket 描述符上有可读事件发生时,服务器端只会从 epoll_wait 中苏醒一次,即I/O事件只会通知一次,因此我们程序要保证一次性将内核缓冲区的数据读取完;
使用水平触发模式时,当被监控的Socket 上有可读事件发生时,服务器端不断地从 epoll_wait 中苏醒,即I/O 事件会一直通知,直到内核缓冲区数据被 read函数读完才结束,目的是告诉我们有数据需要读取;

使用边缘触发模式,I/O 事件发生时只会通知一次,而且我们不知道到底能读写多少数据,所以在收到通知后应尽可能地读写数据,以免错失读写的机会。因此,我们会循环从文件描述符读写数据,那么如果文件描述符是阻塞的,没有数据可读写时,进程会阻塞在读写函数那里,程序就没办法继续往下执行。所以,边缘触发模式一般和非阻塞 I/O 搭配使用,程序会一直执行 I/O 操作,直到系统调用(如 read 和 write)返回错误,错误类型为 EAGAIN 或 EWOULDBLOCK。

一般来说,边缘触发的效率比水平触发的效率要高,因为边缘触发可以减少 epoll_wait 的系统调用次数,系统调用也是有一定的开销的的,毕竟也存在上下文的切换。

select/poll 只有水平触发模式,epoll 默认的触发模式是水平触发,但是可以根据应用场景设置为边缘触发模式。

(2)epoll返回读写事件,但不一定真的可读写

Linux 手册关于 select 的内容中有如下说明:

Under 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 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() 可能会将一个 socket 文件描述符报告为 “准备读取”,而后续的读取块却没有。例如,当数据已经到达,但经检查后发现有错误的校验和而被丢弃时,就会发生这种情况。也有可能在其他情况下,文件描述符被错误地报告为就绪。因此,在不应该阻塞的 socket 上使用 O_NONBLOCK 可能更安全。

也就是说多路复用 API(select、poll、epoll) 返回的事件可能因为一些原因导致并不一定可读写的,如果使用阻塞 I/O, 那么在调用 read/write 时则会发生程序阻塞,因此最好搭配非阻塞 I/O,以便应对极少数的特殊情况。

小结

说了这么多,总结一下将clientfd设置为非阻塞的两个原因:
1.使用ET模式,需要不断进行读取,而如果无剩余数据时进行读取,如果使用阻塞模式会导致read函数阻塞;使用非阻塞模式函数会立刻返回;
2.epoll上虽然有读写事件返回,但事件因为一写异常,其实不一定可读写,如果使用阻塞IO会导致在调用 read/write 时则会发生程序阻塞,因此最好搭配非阻塞 I/O,以便应对极少数的特殊情况。

另外,一个socket是否设置为阻塞模式,只会影响到connect/accept/send/recv四个socket API函数,不会影响到select/poll/epoll_wait函数,后三个函数的超时或者阻塞时间是由其函数自身参数控制的。
所以对于webserver整个的主进程来说,我们使用epoll_wait,其实也是阻塞。

参考链接:
Unix/Linux编程:使用epoll时需要将socket设为非阻塞吗?
IO - 同步,异步,阻塞,非阻塞 (亡羊补牢篇)
I/O 多路复用:select/poll/epoll

  • 7
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值