socket阻塞、非阻塞的影响
一个 socket 是否设置为阻塞模式,只会影响到 connect/accept/send/recv 等四个 socket API 函数,不会影响到 select/poll/epoll_wait 函数,后三个函数的超时或者阻塞时间是由其函数自身参数控制的。
(一)原理分析
下面详细的解释,为了方便解释,在这之前我们先明确几个基础概念:
connfd:创建 socket,主动发起连接的一端(客户端),该端调用 connect 函数主动发起连接;
listenfd:创建 socket,绑定地址和端口,调用 listen 函数发起侦听的一端(服务端);
clientfd:调用 accept 函数接受连接,由 accept 函数返回的 socket(服务端)。
accept 函数并不参与三次握手过程,accept 函数从已经连接的队列中取出连接,返回 clientfd,最后客户端与服务端分别通过 connfd 和 clientf 进行通信(调用 send 或者 recv 函数)。
(二)socket 是否阻塞对下列 API 造成的影响
1. 当 connfd 被设置成阻塞模式时(默认行为,无需设置),connect 函数会一直阻塞到连接成功或超时或出错,超时值需要修改内核参数
2. 当 connfd 被设置成非阻塞模式,无论连接是否建立成功,connect 函数都会立刻返回,那如何判断 connect 函数是否连接成功呢?
接下来使用 select 、poll、 epoll_wait 实现异步 connect 函数去判断 socket 是否可写即可,当然,Linux 系统上还需要额外加一步——使用 getsockopt 函数判断此时 socket 是否有错误,这就是所谓的异步 connect 或者叫非阻塞 connect
1. 当 listenfd 设置成阻塞模式(默认行为,无需额外设置)时,如果连接 pending 队列中有需要处理的连接,accept 函数会立即返回,否则会一直阻塞下去,直到有新的连接到来。
2. 当 listenfd 设置成非阻塞模式,无论连接 pending 队列中是否有需要处理的连接,accept 都会立即返回,不会阻塞。如果有连接,则 accept 返回一个大于 0 的值,这个返回值即是我们上文所说的 clientfd;如果没有连接,accept 返回值小于 0,错误码 errno 为 EWOULDBLOCK(或者是 EAGAIN,这两个错误码值相等)。
1. 当 connfd 或 clientfd 设置成阻塞模式时:
send 函数会尝试发送数据,如果对端因为 TCP 窗口太小导致本端无法将数据发送出去,send 函数会一直阻塞直到对端 TCP 窗口变大足以发数据或者超时;
recv 函数则正好相反,如果此时没有数据可收获,recv函数会一直阻塞直到收取到数据或者超时,有的话,取到数据后返回。
send 和 recv 函数的超时时间可以分别使用 SO_SNDTIMEO 和 SO_RCVTIMEO 两个 socket 选项来设置。
2. 当 connfd 或 clientfd 设置成非阻塞模式时,
send 和 recv 函数都会立即返回,send 函数即使因为对端 TCP 窗口太小发不出去也会立即返回,recv 函数如果无数据可收也会立即返回,此时这两个函数的返回值都是 -1,错误码 errno 是 EWOULDBLOCK(或 EAGIN,与上面同)。
这种情况下,send 和 recv 函数的返回值有三种情形,分别是大于 0,等于0 和小于 0,总结如下表:
(三)select/poll/epoll_wait 函数的等待或超时时间
select、poll、epoll_wait 函数的超时时间分别有传给各自函数的时间参数决定的,我们来看下这三个函数的签名:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); int poll(struct pollfd *fds, nfds_t nfds, int timeout); int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
三个函数最后一个参数是 timeout,只不过 select 函数的 timeout 参数的类型是一个结构体指针,这个结构的定义如下
struct timeval { long tv_sec; /* seconds */ long tv_usec; /* microseconds */ };
select 函数的总超时时间是 timeout->tv_sec 和 timeout->tv_usec 之和, 前者的时间单位是秒,后者的时间单位是微秒。
select 函数的 timeout 参数含义有三种:
- 当 timeout 为 NULL 时,select 函数将一直阻塞下去,直到出错或者绑定其上的 socket 有事件;
- 当 timeout->tv_sec 和 timeout->tv_usec 同时为 0 时,select 函数会检查一下绑定在其上的 socket 是否有事件,然后立刻返回;
- 当 timeout->tv_sec 和 timeout->tv_usec 之和大于 0 时,select 函数检测到绑定其上的 socket 有时间才会返回或者阻塞时长为 timeout->tv_sec + timeout->tv_usec 。
poll 和 epoll_wait 函数的超时时间为毫秒,设置为 0,和 select 函数一样,检测一下绑定其上的 socket 是否有事件,然后立即返回。
(四)使用 epoll 模型是否要将 socket 设置成非阻塞的
答案是需要的。
1. listenfd 和 clientfd
epoll 模型通常用于服务端,那讨论的 socket 只有 listenfd 和 clientfd 了。
边沿触发只触发一次,需要程序一次读完所有数据,一般在read/recv/accept的外层需要加一个循环,直到没有数据,但是当某次循环下数据恰好读完后再调用read/recv/accept程序会阻塞,所以需要将socket设置成非阻塞。
而水平触发则会多次触发,直到数据被读完,所以一般读取一次后继续调用epoll_wait,数据如果没有读完则epoll_wait仍会返回,所以在水平触发模式下可以使用阻塞socket。
总结:水平触发的情况下可以使用阻塞socket,而边沿触发需要将socket设置成非阻塞。
建议:建议都将socket设置成非阻塞,比如当调用write/send函数发送数据时,若缓冲区空间不足,如果使用阻塞套接字,则read/send操作会阻塞。
这种情况下,一般将待发送的数据存入自己的缓冲区,再监听socket的可写事件,再依次将数据发送出去。
2. clientfd
现在就剩下 clientfd 了,如果不将 clientfd 设置成非阻塞模式,那么一旦 epoll_wait 检测到读或者写事件返回后,接下来处理 clientfd 的读或写事件,如果对端因为 TCP 窗口太小,send 函数刚好不能将数据全部发送出去,将会造成阻塞,进而导致整个服务“卡住”。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了