套接字的默认状态是阻塞。表示当发出一个不能立即完成的套接字调用时,其进程将被投入睡眠,等待相应的操作完成。可能阻塞的套接字调用分为以下四类。
-
输入操作,包括
read
、readv
、recv
、recvfrom
、recvmsg
. 如果某个进程对一个阻塞 tcp 套接字调用这些输入函数之一,在该套接字的接收缓冲区没有数据可读时,进程将投入睡眠,直到有数据到达。对阻塞 udp 套接字如果其接收缓冲区为空,进程将投入睡眠,直到有 udp 数据报到达。对于非阻塞套接字,如果输入操作不能被满足,那么调用将立即返回
EWOULDBLOCK
错误。 -
输出操作,包括
write
、writev
、send
、sendto
、sendmsg
. 对于 tcp 套接字,内核将数据从应用进程的缓冲区复制到套接字的发送缓冲区。对于阻塞的套接字,如果发送缓冲区中没有空间,进程将投入睡眠,直到有空间为止。对于非阻塞 tcp 套接字,如果发送缓冲区没有空间,输出函数调用将立即返回EWOULDBLOCK
错误,如果是有一些空间,那么返回值是实际赋值的字节数。对于 udp 套接字,它不存在真正的发送缓冲区,所以它不会因与 tcp 套接字一样的原因阻塞,但可能会因其它的原因而阻塞。 -
接受外来连接,即 accept 函数。如果对一个阻塞套接字调用 accept,并且尚无新的连接到达,调用进程将被投入睡眠。对一个非阻塞套接字调用 accept,无新连接到达,accept 调用立即返回
EWOULDBLOCK
-
发起外出连接,即 tcp 的 connect 函数。connect 会一直等到客户收到对于自己的 SYN 的 ACK 为止才返回。这意味着 tcp 的每个 connect 总会阻塞其调用进程至少一个到服务器的 RTT 时间。对于非阻塞 tcp 套接字调用 connect,即使连接不能立即建立,但连接的建立照样会发起 (发送 SYN),但会返回
EINPROGRESS
。**注意:**这与之前三种情况不同,不是返回的EWOULDBLOCK
非阻塞 connect
当在一个非阻塞 tcp 套接字上调用 connect 时,connect 将立即返回 EINPROGRESS
错误 (注意一种特殊情况: 在本机上建立 tcp 连接 connect 可能会返回成功),不过已经发起的 tcp 三次握手仍在进行,接着可以使用 select 来检测这个连接或成功或失败。非阻塞 connect 有三个用途:
- 可以把三次握手叠加到其他处理上。完成一个 connect 至少一个 RTT 时间,从局域网可能是几毫秒到几百毫秒甚至广域网上的几秒。这一段时间也许有我们想要执行的其他处理工作可执行。
- 可以利用此同时建立多个连接。
- 利用 select 的超时时间来缩短 connect 的超时。即在指定时间内检测 socket 是否可用。如果超时发生,需要主动关闭套接字,防止已经启动的三次握手继续进行。
被中断的 connect
假设正常的阻塞式套接字,其上的 connect 调用在 tcp 三次握手完成前被中断,将发生什么?如果中断的 connect 不由内核自动重启,那么将返回 EINTR
。不能再次调用 connect 等到未完成的连接继续完成,这样会导致返回 EADDRINUSE
.
这种情况下就只能利用 select 来进行监听。
注:源自 Berkeley 的实现 (和 POSIX),对 select 和非阻塞 connect 有两个规则:(1) 当连接成功建立时,描述符为可写;(2) 当连接建立失败时,描述符变为可读又可写。
非阻塞 accept
当有一个已完成的连接准备好被 accept 时,select 将作为可读描述符返回该连接的监听套接字。因此,如果使用 select 来监听某个套接字上等待的一个外来连接,那就没有必要把监听套接字设置为非阻塞。
但这里存在一个潜在问题:利用了 IO 多路复用的 accept 正常情况下不会被阻塞,即使是阻塞 tcp 套接字。但考虑一下情况:
- 客户端申请建立连接,并建立后发送 RST 终止连接
- select 向服务器返回监听套接字可读,但过一段时间再 accept
- 服务器从 select 返回到调用 accept 期间,服务器收到客户的 RST
- 该连接被弹出已完成连接队列,假设队列中没有其他连接,那么服务器将阻塞在 accept,无法处理其他事务,除非新来一个连接
可以发现,以上情况并不满足正常情况的 IO 多路复用。所以解决方法是将监听套接字设置为非阻塞。