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 造成的影响

Linux fcntl函数

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 设置成非阻塞模式时,

sendrecv 函数都会立即返回,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 参数含义有三种:

  1. 当 timeout 为 NULL 时,select 函数将一直阻塞下去,直到出错或者绑定其上的 socket 有事件;
  2. 当 timeout->tv_sec 和 timeout->tv_usec 同时为 0 时,select 函数会检查一下绑定在其上的 socket 是否有事件,然后立刻返回;
  3. 当 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 函数刚好不能将数据全部发送出去,将会造成阻塞,进而导致整个服务“卡住”。

 

posted @ 2023-03-20 20:59  ImreW  阅读(330)  评论(0编辑  收藏  举报