如何使用非阻塞套接字进行connect()连接?

6
在Python中,我想在一个设置为非阻塞的套接字上使用socket.connect()。当我尝试这样做时,该方法总是抛出一个BlockingIOError。当我忽略错误(如下所示)时,程序按预期执行。当我在连接之后将套接字设置为非阻塞时,就不会出现错误。当我使用select.select()来确保套接字可读或可写时,仍然会出现错误。 testserver.py
import socket
import select

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setblocking(0)

host = socket.gethostname()
port = 1234

sock.bind((host, port))
sock.listen(5)

while True:
    select.select([sock], [], [])
    con, addr = sock.accept()
    message = con.recv(1024).decode('UTF-8')
    print(message)

testclient.py

import socket
import select

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setblocking(0)

host = socket.gethostname()
port = 1234

try:
    sock.connect((host, port))
except BlockingIOError as e:
    print("BlockingIOError")

msg = "--> From the client\n"

select.select([], [sock], [])
if sock.send(bytes(msg, 'UTF-8')) == len(msg):
    print("sent ", repr(msg), " successfully.")

sock.close()

终端 1

$ python testserver.py
--> From the client

终端 2

$ python testclient.py
BlockingIOError
sent  '--> From the client\n'  successfully.

这段代码正确运行,除了第一个connect()出现的BlockingIOError。该错误的文档解释如下:当一个对象(例如套接字)被设置为非阻塞操作时,如果一个操作将会被阻塞,则会引发此错误。

我该如何使用设置为非阻塞的套接字进行正确的connect()连接?我能使connect()变为非阻塞吗?还是直接忽略这个错误比较合适?


这个链接可能会对你有所帮助: https://dev59.com/PXM_5IYBdhLWcg3w1G-N - akash12300
@Akash1993,是的,之前我看到那个问题时,很难将其与我的简单示例联系起来。我想避免使用asyncio;它似乎过于复杂了。这确实澄清了错误是由于connect在不应该阻塞时阻塞引起的(以及blockingioerror文档)。问题更多的是“有没有一种方法使connect()非阻塞”。 - MikeJava
2个回答

4
当使用非阻塞套接字进行socket.connect时,首先会出现BlockingIOError异常是比较常见的。请参见TCP Connect error 115 Operation in Progress What is the Cause?以了解其原因。基本上,套接字还没有准备好,会引发BlockingIOError: [Errno 115] Operation now in progress,也称为EINPROGRESS
解决方法是捕获并忽略异常,或者使用socket.connect_ex代替socket.connect,因为该方法不会引发异常。特别注意Python文档中该方法描述的最后一句话:

socket.connect_ex(address)

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

来源: https://docs.python.org/zh-cn/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
... 
>>> 

2
这里的技巧是,在第一次选择完成后,然后需要再次调用sock.connect只有在从connect收到成功的返回状态之后,套接字才会连接。 只需在第一次调用select完成后添加这两行:之后
print("first select completed")
sock.connect((host, port))

编辑:
后续。我之前错误地表示需要调用sock.connect进行额外的调用。但是,如果您希望在其自己的代码路径中处理连接失败,则这是发现原始非阻塞调用connect是否成功的好方法。

在C代码中实现这一传统方法的说明在此处: Async connect and disconnect with epoll (Linux)

这涉及调用getsockopt。您也可以在Python中执行此操作,但是从sock.getsockopt返回的结果是一个bytes对象。如果它代表一个失败,则需要将其转换为整数errno值,并将其映射到字符串(或异常或任何您需要与外界通信的内容)。再次调用sock.connect已经将errno值映射到适当的异常。

解决方案2: 您还可以将sock.setblocking(0)的调用推迟到连接完成后。


1
我发布的代码按照预期工作,并且我已经编辑了问题以使其更加明确。问题在于它在第一次连接时抛出BlockingIOError,而不是在连接成功时。如果您检查我发布的终端输出,也许会更清楚。 - MikeJava
解决方案2是完全正确的!实际上,我在我的问题中提到了它;我想知道是否有一种方法在套接字设置为非阻塞之后进行这样的操作。 - MikeJava
我不明白这个。你似乎在问“是否有一种方法可以在请求非阻塞行为后获得阻塞行为?”如果你没有调用sock.setblocking(0),默认行为是阻塞的。 - Gil Hamilton
这更接近于“有没有办法从connect()获取非阻塞行为?”还是“在请求非阻塞行为后,处理阻塞行为的适当方式是什么?”无论哪个是正确的问题。越来越清楚的是,connect()不能是非阻塞的。正确答案应该包括这一点。 - MikeJava
在“connect”的上下文中,“非阻塞”意味着您启动连接过程,但不立即等待其完成。您的原始代码就是这样做的。这正是我在上面详细讨论的内容。您的代码实际上会在“select”中阻塞,但它不需要这样做。(我假设这是示例代码的原因。)如果您不想在“select”中阻塞,可以提供第四个“timeout”参数,并将其值设置为0,这将把“select”转换为轮询。 - Gil Hamilton
但是如果我的代码中connect是非阻塞的,为什么我会收到BlockingIOError?这真的让我困惑。你关于select的说法肯定是对的,我也试过了。实际上,即使没有使用select,它也可以正常工作,这让我感到很奇怪;不过这是另一个问题了。 - MikeJava

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