如何获取非阻塞套接字connect()?

13

我这里有一个相当简单的问题。我需要与许多主机同时通信,但我不需要任何同步,因为每个请求都非常自给自足。

基于这个原因,我选择使用异步套接字,而不是滥用线程。但我现在有一个小问题:

异步东西运作得很好,但当我连接100个主机时,如果我得到100个超时(超时=10秒),那么我需要等待1000秒,才能发现所有的连接都失败了。

有没有任何方法可以获得非阻塞套接字连接?我的套接字已经设置为非阻塞,但对connect()的调用仍然是阻塞的。

缩短超时时间不是可接受的解决方案。

我正在使用Python进行此操作,但我想在这种情况下编程语言并不重要。

我真的需要使用线程吗?

6个回答

8
使用 select 模块,可以等待多个非阻塞套接字的 I/O 完成。这里有更多关于 select 的信息。引用页面内容如下:

在 C 语言中,编写 select 相当复杂。 在 Python 中,它非常简单,但是 它与 C 版本非常接近, 如果你理解了 Python 中的 select, 你在 C 中也不会遇到太大的麻烦。

ready_to_read, ready_to_write, in_error = select.select(
                  potential_readers, 
                  potential_writers, 
                  potential_errs, 
                  timeout)

您需要传递三个列表给 select 函数:第一个列表包含所有可能想要尝试读取的套接字,第二个列表包含所有可能想要尝试写入的套接字,最后一个列表(通常为空)包含您想要检查错误的套接字。请注意,套接字可以进入多个列表中。调用 select 是阻塞的,但您可以设置超时时间。一般来说,这是一个明智的做法 - 给它一个很长的超时时间(比如一分钟),除非您有充分的理由不这样做。
作为回报,您将获得三个列表。它们包含实际可读、可写和出现错误的套接字。每个列表都是对应列表的子集(可能为空)。如果将一个套接字放入多个输入列表中,则它将只在一个输出列表中(最多)。
如果套接字在输出可读列表中,则可以几乎确定在该套接字上执行 recv 将返回某些内容。可写列表也是同样的道理。您将能够 send 一些数据,也许不是您想要的全部,但是一些总比没有好。 (实际上,任何相当健康的套接字都将被视为可写 - 这只意味着出站网络缓冲区空间可用。)
如果您有一个 "服务器" 套接字,请将它放入潜在读取者列表中。如果它出现在可读列表中,您的 accept 将(几乎肯定)有效。如果您创建了一个新的套接字以连接到其他人,请将其放入潜在写入者列表中。如果它出现在可写列表中,则您有很大的机会成功连接。

他明确表示他在connect()上被阻止了。select()只告诉你哪些是可读或可写的。 - JimB
1
请看我回答的最后一段。使用“select”多路复用,您无需等待1000秒才能执行有用的工作。通过短暂的超时,即使所有端点未连接,您仍然可以执行有用的工作,并且只需短暂等待。当然,Twisted也是一种选择,但正如您自己所说,“它有点难以理解”。 - Vinay Sajip
啊,我明白问题出在哪了……他设置了一个超时时间,这意味着套接字必须是阻塞的。 - JimB
我没有明确设置任何内容,我正在使用Python的asyncore模块,它似乎是select()的一个包装器。我创建了另一个简短的测试脚本,只是创建了一个套接字并将其设置为非阻塞状态,但它仍然在连接时阻塞,只是在读取时不会阻塞。 - Tom
@Tom - 看到了吗,你没有提到你正在使用timeout选项的asyncore,所以我猜想你在使用socket.settimeout(),这会设置为阻塞模式。你在使用什么平台和Python版本 - 在我的系统上,使用setblocking(0)并不会在connect时阻塞。 - JimB

7

很不幸,没有例子代码能够展示这个错误,所以很难看出这个块是从哪里来的。

他做了一些类似于:

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setblocking(0)
s.connect(("www.nonexistingname.org", 80))

socket模块在内部使用getaddrinfo,该操作是阻塞的,特别是当主机名不存在时。符合标准的dns客户端会等待一段时间,以查看名称是否真的不存在,或者只是存在一些缓慢的dns服务器。

解决方法是仅连接到IP地址或使用允许非阻塞请求的dns客户端,例如pydns


这基本上就是问题的核心。看起来我遇到了DNS问题。我的应用程序(至少在初始阶段)的行为与端口扫描器非常相似:我依赖于非常快的结果,无论连接是否成功。对不存在的主机名使用getaddrinfo也会阻塞非阻塞套接字,这很糟糕(对我来说)。我可能还会连接到许多不存在的主机,而我不能承受在每个不存在的主机上等待10秒钟的时间。 - Tom
1
我的目的非常不同,但是通过改变顺序来固定它。即先连接,然后设置阻塞。 - Ben
@Ben那也解决了我的问题!谢谢! - jeromej
有趣的是,我遇到了完全相同的问题,但是当我将 www.nonexistingname.org 替换为 127.0.0.1 时,问题仍然存在。 - Mike

6
你需要同样并行连接,因为当你设置超时时套接字会阻塞。或者,你可以不设置超时,并使用选择模块。
你可以使用asyncore模块中的dispatcher类来实现这一点。看一下基本的http客户端示例。该类的多个实例不会在连接上相互阻塞。你也可以使用线程轻松完成此操作,我认为这使得跟踪套接字超时更容易,但由于你已经在使用异步方法,所以最好保持相同的方式。
例如,以下内容在我所有的Linux系统上都有效。
import asyncore, socket

class client(asyncore.dispatcher):
    def __init__(self, host):
        self.host = host
        asyncore.dispatcher.__init__(self)
        self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
        self.connect((host, 22))

    def handle_connect(self):
        print 'Connected to', self.host

    def handle_close(self):
        self.close()

    def handle_write(self):
        self.send('')

    def handle_read(self):
        print ' ', self.recv(1024)

clients = []
for i in range(50, 100):
    clients.append(client('cluster%d' % i))

asyncore.loop()

在 cluster50 - cluster100 中,有许多机器无响应或不存在。这会立即开始打印:

Connected to cluster50
  SSH-2.0-OpenSSH_4.3

Connected to cluster51
  SSH-2.0-OpenSSH_4.3

Connected to cluster52
  SSH-2.0-OpenSSH_4.3

Connected to cluster60
  SSH-2.0-OpenSSH_4.3

Connected to cluster61
  SSH-2.0-OpenSSH_4.3

...

然而,这并没有考虑到getaddrinfo,它必须阻塞。如果您在解析DNS查询时遇到问题,一切都必须等待。您可能需要自己单独收集DNS查询,并在异步循环中使用IP地址。
如果您想要比asyncore更大的工具包,请看一下Twisted Matrix。它有点难以入门,但它是您可以获取的最好的Python网络编程工具包。

好的,我在这里要道歉。我直接从Python文档中复制了代码,所以那不是我的代码,我认为它是正确的。但它并没有起作用。经常有人给我建议,他们甚至没有验证过。我从来没有想到我的操作系统会成为问题,而不是代码,所以我认为你只是另一个认为自己很聪明,复制粘贴文档代码而没有检查是否有效的人。对此再次道歉。今天我浪费了6个小时,扔掉了3个完整版本,最终发现MacOS是问题所在。 - Tom
顺便说一下,我和我的朋友在他的Linux盒子上再次测试了这个问题,即使是getAddrInfo看起来也不会在那里阻塞。我们收到了一个错误:[Errno 115] 操作正在进行。因此,理论上,即使是非响应式主机的asyncore也可以在Linux上工作。 - Tom
@Tom - 没问题,我同意这里有很多不知情的答案,尤其是在非Windows领域。更糟糕的是,这些不知情的团队最终会互相投票,使得很难得到正确的答案。 - JimB
我们得到一个错误:[Errno 115] 操作正在进行中。因此,理论上即使是在非响应主机上,asyncore 在 linux 中也可以工作 - 我非常肯定它确实可以,只是我没法让我的 DNS 失效到足以挂起以验证它。 - JimB
@JimB,请回答关于asyncio模块的问题? - Vova

4

使用Twisted

它是一个用Python编写的异步网络引擎,支持多种协议,并且您可以添加自己的协议。它可用于开发客户端和服务器。连接时不会阻塞。


2
Twisted 带来如此多的快乐。我每天都在使用它,并试图说服那些与并发有困难的人它会让他们的生活变得更加轻松。当然,我的同事至少能看到其中的区别。 - Dustin
1
我以前用过Twisted,它很不错,但是文档也很复杂。而且将我的源代码集成到其中会很困难。你确定它在连接时不会阻塞吗?如果是的话,我可能会尝试使用它。 - Tom

1
当使用非阻塞套接字进行socket.connect时,预计会首先出现BlockingIOError。请参见TCP Connect error 115 Operation in Progress What is the Cause?,了解其原因的解释。
解决方案是捕获并忽略异常,或者使用socket.connect_ex代替socket.connect,因为该方法不会引发异常。特别注意Python文档中其描述的最后一句话:

socket.connect_ex(address)

connect(address)相似,但对于由C级别的connect()调用返回的错误,返回一个错误指示符号而不是引发异常(其他问题,例如“未找到主机”,仍可能引发异常)。如果操作成功,则错误指示符号为0,否则为errno变量的值。这对于支持异步连接非常有用。

来源:https://docs.python.org/3/library/socket.html#socket.socket.connect_ex 如果您想继续使用socket.connect,您可以捕获并忽略相关的EINPROGRESS错误:
>>> import socket
>>> 
>>> # bad
>>> s = socket.socket()
>>> s.setblocking(False)
>>> s.connect(("127.0.0.1", 8080))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
BlockingIOError: [Errno 115] Operation now in progress
>>> 
>>> # good
>>> s = socket.socket()
>>> s.setblocking(False)
>>> try:
...     s.connect(("127.0.0.1", 8080))
... except OSError as exc:
...     if exc.errno != 115:  # EINPROGRESS
...         raise
... 
>>> 

0
你看过asyncore模块了吗?也许正是你所需要的。

我正在使用这个,但它仍然在连接时阻塞。 - Tom

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