使用 I/O 多路复用时的非阻塞套接字

Non blocking sockets when using I/O multiplexing

在使用 I/O 多路复用 API(如 poll(2)epoll(2) 时,我应该使用非阻塞还是阻塞 TCP 套接字?

有些人建议在这里使用非阻塞套接字,但是 I/O 多路复用 API 无论如何都会通知您是否有数据要读取,所以这里的阻塞套接字有什么问题?

Non-blocking 当您更喜欢错误响应(EWOULDBLOCK / EAGAIN)而不是线程等待(阻塞)直到 IO 操作成为可能时,使用 IO。

这就引出了IO多路复用是如何实现的问题

如果您使用的是 thread-per-connection 模型(或 process-per-connection),使用阻塞 IO 可能更舒适。

但是,如果同一个线程服务于多个IO对象,阻塞IO将是危险的 并可能使整个应用程序停止。

单线程服务多个IO对象时,最好使用non-blockingIO

请注意,在轮询时(使用 select / pollepoll/kqueue),问题一开始可能并不明显。

因为 IO 操作仅由已经“知道”IO 操作不会阻塞的代码路径执行(它被轮询并已知是可用操作)。

这掩盖了一个问题,即在代码的某处可能会直接调用 IO 操作而不先进行轮询,从而导致阻塞 IO 调用,从而使应用程序停止运行。

如果您的 TCP 服务器是 single-threaded 并使用阻塞 I/O,那么很可能连接到它的任何客户端都可以通过仅发送来拒绝对所有其他客户端的服务a partial-message,或者在服务器发送数据后拒绝从其 TCP 套接字读取任何数据。在前一种情况下,服务器可能会阻塞很长时间(也许永远)等待从客户端接收到整个消息;在此期间,服务器将无法响应其他客户端。在后一种情况下,服务器将阻塞很长时间(可能永远)等待客户端读取一些 TCP 数据,这样 server-socket 的 send-buffer 就可以被耗尽以容纳更多传出数据给那个客户。

避免该问题的一种方法是将服务器的所有套接字设置为 non-blocking I/O 模式;这样服务器就知道它永远不会在 recv()send() 调用中“卡住”,因此可以保持对所有客户端的响应,而不管任何特定客户端是否表现良好。在 non-blocking 设计中,服务器唯一阻塞的地方是在 select()poll() 或类似的地方,因为这些调用被设计为 return 只要任何客户端需要服务,而不是而不是只阻塞一个客户端。 (权衡是 non-blocking I/O 你的服务器的 buffering/queueing 逻辑需要更复杂一些,因为你不能再假设任何特定的固定字节数将被发送或在任何给定的发送或接收操作期间收到)

另一种避免问题的方法是制作一个multi-threaded服务器;这样做的好处是每个客户端都有自己的线程,因此 badly-behaved 客户端只会阻塞自己的线程,而不会阻塞为其他客户端提供服务的线程。缺点是现在您的服务器是 multi-threaded,多线程引入了所有额外的陷阱。

(而且,为了完整起见,第三种方法只是简单地忽略 badly-behaved/poorly-connected 客户的可能性,并使用 single-threaded/blocking 模型。这对于预期客户是玩具示例的效果很好non-hostile,他们连接的网络可靠,但在现实生活中效果不佳)