Linux,套接字,非阻塞连接

26

我想创建一个非阻塞的连接。

socket.connect(); // returns immediately

为此,我使用另一个线程、一个无限循环和Linux epoll。示例伪代码如下:

// in another thread
{
  create_non_block_socket();
  connect();

  epoll_create();
  epoll_ctl(); // subscribe socket to all events
  while (true)
  {
    epoll_wait(); // wait a small time(~100 ms)
    check_socket(); // check on EPOLLOUT event
  }
}

如果我先运行服务器再运行客户端,一切正常。但如果我先运行客户端,等待一小段时间后再运行服务器,则客户端无法连接。我做错了什么?也许可以用其他方式解决?

如果您正在启动另一个线程来执行连接操作,为什么要异步进行呢?此外,最好将其余通信也放在其中。 - Martin James
那么,如果不使用epoll和非阻塞方式该怎么做呢?如果我只是调用connect(),那么它会阻塞并等待连接(我是对的吗?)。但是如果我想将这个连接线程加入到主线程中,我无法这样做,因为连接线程将处于阻塞状态。如果我理解有误,请见谅。 - herolover
1
这不是“异步”。这是非阻塞的。 - user207421
3个回答

55

进行异步连接时应遵循以下步骤:

  • 使用socket(..., SOCK_NONBLOCK, ...)创建套接字
  • 使用connect(fd, ...)启动连接
  • 如果返回值既不是0也不是EINPROGRESS,则出现错误中止
  • 等待直到fd被标记为准备好输出
  • 使用getsockopt(fd, SOL_SOCKET, SO_ERROR, ...)检查套接字的状态
  • 完成

无需循环 - 除非你想处理EINTR

如果客户端先启动,则在最后一步会看到错误ECONNREFUSED。如果发生这种情况,请关闭套接字并从头开始。

不看到更多细节很难确定你的代码有什么问题。我猜测你的check_socket操作中没有处理错误。


1
@DreamWarrior:很奇怪。看一下connect(2)connect(3),并搜索poll。两个手册都指出,你应该等待指示,即_socket_是_writable_。你能提供一个最小的例子来展示意外的行为吗? - nosid
手册中指出:“可以通过选择写入套接字来选择(2)或轮询(2)完成”。 我猜关键词是“完成”。 由于它从未完成过,因为它从未收到SYN-ACK(或RST,这完成了握手,但导致失败),所以它从未变得可写。 - DreamWarrior
1
@DreamWarrior:我无法重现您所描述的问题。我编写了一个最小化的测试程序,并且它使用POLLOUT正确报告了ETIMEDOUT - nosid
1
这不是“异步连接”。这是非阻塞连接。考虑到程序除了等待成功或失败之外什么也不做,这种方法完全是徒劳的。更明智的做法是以阻塞模式进行连接,然后在接下来的任何操作中(如果有的话)再转换为非阻塞模式。 - user207421
6
getsockopt(fd, SOL_SOCKET, SO_ERROR, ...)返回0,且so_error为0时,并不意味着套接字已连接。这意味着目前还没有发生错误。在这种情况下,您需要调用getpeername(),如果getpeername()返回0,则表示套接字已连接。如果套接字未连接,则getpeername()返回-1,并将errno设置为ENOTCONN。getsockopt(fd, SOL_SOCKET, SO_ERROR, ...)可以告诉您有关连接被拒绝的信息,但无法告诉您有关已连接套接字的信息。您需要使用getpeername()或其他方法来确保套接字已连接。 - Alexandre Fenyo
显示剩余8条评论

9

有几种方法可以测试非阻塞连接是否成功:

  1. 首先调用 getpeername(),如果它失败并返回错误 ENOTCONN,则连接失败。然后调用 getsockopt() 函数以获取套接字上挂起的错误。
  2. 调用 read() 并将长度设置为 0。如果读取失败,则连接失败;read() 的 errno 表示连接失败的原因,如果返回 0 则表示连接成功。
  3. 再次调用 connect(),如果 errno 是 EISCONN,则表示已经连接,第一个连接成功。

参考资料:UNIX 网络编程卷1。


请注意:read() man page中写道:“如果count为零,read()可能会检测到下面描述的错误。在没有任何错误的情况下,或者如果read()不检查错误,则计数为0的read()返回零并且没有其他影响。”因此,它可能会检测到错误。 - VL-80

3

D.J. Bernstein总结了各种方法如何检查异步connect()调用是否成功。其中许多方法在某些系统中具有缺点,因此编写可移植代码变得意外地困难。如果任何人想阅读所有可能的方法及其缺点,请查看此文档

对于那些只想要简短版本的人,最可移植的方法是以下内容:

一旦系统将套接字标记为可写入状态,首先调用getpeername()来查看它是否已连接。如果调用成功,则表示套接字已连接,并且您可以开始使用它。如果该调用由于ENOTCONN而失败,则连接失败。要找出原因,请尝试从套接字读取一个字节read(fd, &ch, 1),同样会失败,但您收到的错误就是如果它不是非阻塞的,您将收到的connect()的错误。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接