梦想还在,生活当继续!
一、前言
linux
下,用 python
的非阻塞 socket
通信时,遇到了 BlockingIOError: [Errno 11] Resource temporarily unavailable
错误。
翻译报错信息 Resource temporarily unavailable
为:“资源暂时不可用”。
在我的代码里,使用了“epoll
+ 非阻塞 socket
” 的模式。因此猜测,在有 socket
还未创建完成的情况下,使用它发送消息导致报错,错误的理解为这个 socket
资源暂时不可用。-.-
后来上网查找相关资料,发现并非如此。根据网上资料,我得到两种不同的答案。
答案一:
首先,是我把套接字设置为异步的。然后,在使用 write
发送数据时采取的方式是循环发送大量的数据;由于是异步的,write\send
将要发送的数据提交到发送缓冲区后是立即返回的,并不需要对端确认数据已接收。在这种情况下是很有可能出现发送缓冲区被填满,导致 write\send
无法再向缓冲区提交要发送的数据。因此就产生了 Resource temporarily unavailable
的错误。EAGAIN
的意思也很明显,就是要你再次尝试。
答案二:
在 Linux
进行非阻塞的 socket
接收数据时经常出现 Resource temporarily unavailable
,errno
代码为11(EAGAIN
),这是什么意思?
这表明你在非阻塞模式下调用了阻塞操作,在该操作没有完成就返回这个错误,这个错误不会破坏 socket
的同步,不用管它,下次循环接着 recv
就可以。
两种答案不同,但是感觉都很有道理。这让我迷惑了,于是决定自己研究研究。记录下来,分享的同时,也方便自己以后回顾。
二、BlockingIOError
首先,我想知道 python
中的 BlockingIOError
具体指什么。
在 python
官方文档中,找到了对 BlockingIOError
异常的说明,如下:
exception BlockingIOError:
Raised when an operation would block on an object (e.g. socket) set for non-blocking operation. Corresponds to errno EAGAIN, EALREADY, EWOULDBLOCK and EINPROGRESS.
我将它翻译为:“当在设置为非阻塞操作的对象(例如:套接字)上,执行阻塞操作时触发。对应的错误类型有:EAGAIN
, EALREADY
, EWOULDBLOCK
和 EINPROGRESS
。”
linux
下,BlockingIOError: [Errno 11]
即为 EAGAIN
错误。
windows
上,EAGAIN
的名字叫做 EWOULDBLOCK
。对应的报错信息为:
“BlockingIOError: [WinError 10035]
无法立即完成一个非阻止性套接字操作”。
官网的这个说明,依然没让我明白为什么会出现“资源不可用”。但它大概描述了如何触发 EAGAIN
,于是我决定复现它,继续研究。
三、EAGAIN
复现
据我了解,可以触发 EAGAIN
错误的操作有:accept()
、recv()
、send()
。
connect()
方法也会阻塞,但返回的是 EINPROGRESS
错误,表示连接操作正在进行中,但是仍未完成。
因此,我在 linux
下使用 python3
,分别复现三种函数触发 EAGAIN
异常。
3.1、accept()
触发
accept()
触发很简单,一个程序就能完成,代码如下:
# _*_ coding:utf-8 _*_
"""
tcp服务端
"""
import socket
def tcp_server():
server = socket.socket()
# 设置为非阻塞
server.setblocking(False)
address = ('0.0.0.0', 80)
server.bind(address)
server.listen(3)
cli, addr = server.accept()
if __name__ == '__main__':
tcp_server()
运行代码,报错如下:
Traceback (most recent call last):
File "tcp_server.py", line 19, in <module>
tcp_server()
File "tcp_server.py", line 15, in tcp_server
cli, addr = server.accept()
File "/usr/lib64/python3.6/socket.py", line 205, in accept
fd, addr = self._accept()
BlockingIOError: [Errno 11] Resource temporarily unavailable
可以看到,在 server.accept()
时,触发了 [Errno 11]
。
3.2、recv()
触发
recv()
触发,需要两个程序,一个服务端,一个客户端。在服务端 recv()
客户端发送的消息时,触发异常。
服务端代码,如下:
# _*_ coding:utf-8 _*_
"""
tcp服务端
"""
import socket
def tcp_server():
server = socket.socket()
address = ('0.0.0.0', 80)
server.bind(address)
server.listen(3)
cli, addr = server.accept()
# 将返回的客户端连接socket设置为非阻塞
cli.setblocking(False)
cli.recv(1024)
if __name__ == '__main__':
tcp_server()
客户端代码,如下:
# _*_ coding:utf-8 _*_
"""
tcp客户端
"""
import socket
def tcp_client():
ip_port = ('127.0.0.1', 80)
client = socket.socket()
client.connect(ip_port)
client.send('hello world!'.encode())
print(f'send data success.')
if __name__ == '__main__':
tcp_client()
首先,执行服务端代码;然后,执行客户端代码访问服务端,服务端将触发错误。
服务端,报错如下:
Traceback (most recent call last):
File "tcp_server.py", line 21, in <module>
tcp_server()
File "tcp_server.py", line 17, in tcp_server
cli.recv(1024)
BlockingIOError: [Errno 11] Resource temporarily unavailable
可以看到,在服务端 recv(1024)
时,触发了 [Errno 11]
。
3.3、send()
触发
send()
触发,同样需要一个服务端和一个客户端。当服务端接收客户端连接后,不及时 recv()
客户端的消息,在 while
循环中 sleep()
。这会导致客户端多次发送消息后,在 send()
函数触发异常。
服务端代码,如下:
# _*_ coding:utf-8 _*_
"""
tcp服务端
"""
import socket
import time
def tcp_server():
server = socket.socket()
address = ('0.0.0.0', 80)
server.bind(address)
server.listen(3)
cli, addr = server.accept()
while 1:
time.sleep(10)
if __name__ == '__main__':
tcp_server()
客户端代码,如下:
# _*_ coding:utf-8 _*_
"""
tcp客户端
"""
import socket
def tcp_client():
ip_port = ('127.0.0.1', 80)
client = socket.socket()
client.connect(ip_port)
# 设置为非阻塞
client.setblocking(False)
while 1:
client.send('hello world!'.encode())
print(f'send data success.')
if __name__ == '__main__':
tcp_client()
首先,运行服务端程序;然后,运行客户端程序,访问服务端。
客户端在多次 send()
后,报错如下:
.
.
.
send data success.
send data success.
send data success.
Traceback (most recent call last):
File "tcp_client.py", line 20, in <module>
tcp_client()
File "tcp_client.py", line 14, in tcp_client
client.send('hello world!'.encode())
BlockingIOError: [Errno 11] Resource temporarily unavailable
可以看到,客户端在很多次 send()
后,触发了 [Errno 11]
。
四、原因分析
从 accept
和 recv
的触发来看,“答案二”似乎更正确,即:
“在非阻塞模式下调用了阻塞操作,在该操作没有完成就返回这个错误”。
但是,从 send
的触发来看,send
了很多次后,才会触发 [Errno 11]
。“答案一”好像更有道理,即:
“由于是异步的,write\send
将要发送的数据提交到发送缓冲区后是立即返回的,并不需要对端确认数据已接收。在这种情况下是很有可能出现发送缓冲区被填满,导致 write\send
无法再向缓冲区提交要发送的数据。因此就产生了 Resource temporarily unavailable
的错误。”。
两种答案看着都有自己的道理,但我还是没明白,它们和“资源暂时不可用”有什么关联。
为什么 EAGAIN
异常给出的提示信息是“[Errno 11] Resource temporarily unavailable
”呢?
继续研究分析后,我得出了自己的结论,依然按照accept()
、recv()
、send()
三个函数来分析说明。
4.1、accept()
分析
当我们创建一个 socket
监听某个端口后,所有完成三次握手的客户端连接,都会按照到达先后顺序被放入主线 socket
的等待连接队列中。先放入的先被取出
当 socket
执行 listen()
后,可以调用 accept()
函数从等待连接队列中取出一个连接请求,并创建一个新的 socket
用于与客户端通信,然后返回。
阻塞模式下,主线 socket
调用 accept
后,如果等待队列中没有新的请求,就会一直阻塞,直到可以从队列中取出新的请求才返回。
非租塞模式下,如果等待队列中没有可取的连接,accept()
也会立马返回,并抛出 BlockingIOError: [Errno 11] Resource temporarily unavailable
异常。
此时,我理解的资源不可用,是指等待连接队列中没有数据可取!
如果,等待连接队列中有新连接可取,阻塞模式和非阻塞模式下的 accept()
是没有区别的。
4.2、recv()
分析
socekt
调用 recv()
接收消息时,并不是直接从对端 socket
获取数据,而是从接收缓冲区读取数据。
阻塞模式下,如果接收缓冲区没有数据可读,recv()
会一直阻塞,直到有数据可读。
非阻塞模式下,recv()
从接收缓冲区读取数据时,如果没有数据,也会立马返回,并抛出 BlockingIOError: [Errno 11] Resource temporarily unavailable
异常。
此时,我理解的资源不可用,是指接收缓冲区没有数据可读!
如果,接收缓冲区中有数据可读,阻塞模式和非阻塞模式下的 recv()
是没有区别的。
4.3、send()
分析
socket
使用 send()
方法发送数据时,会先将数据提交到发送缓冲区,由内核通过网络发送出去。
在阻塞模式下,send()
操作会等待所有数据均被复制到发送缓冲区后才会返回。
例如,如果发送缓冲总大小为 1024,现在已经复制了 1023 个字节到发送缓冲区,那么剩余的可用发送缓冲区大小为 1 字节。如果继续发送数据,执行下一个 send()
操作,并且 send
的数据长度大于 1。此时,send()
中的待发送数据是无法全部被写入到发送缓冲区的,send()
将会阻塞,直到内核取走发送缓冲区中部分数据,send()
的所有数据全部被写入发送缓冲区后才返回。
使用 sock.getsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF)
可以查看 socket
的 发送缓冲区大小。
非阻塞模式下,send()
发送数据时,如果发送缓冲区可用大小不足以支持 send()
写入全部数据,send()
方法也会立马返回,并抛出 BlockingIOError: [Errno 11] Resource temporarily unavailable
异常。
通过前面 send()
方法触发异常的代码,可以看到在服务端一直没有读取网络数据,导致客户端发送缓冲区中数据处理不及时,而客户端又在循环发送大量数据,当发送缓冲区被写满后,继续发送数据触发了资源不可用异常。
此时,我理解的资源不可用,是指发送缓冲区没有足够空间可用!
如果,发送缓冲区中还有足够空间,允许send()
函数提交指定的所有数据,阻塞模式和非阻塞模式下的 send()
是没有区别的。
五、EAGAIN
处理
知道触发 EAGAIN
的原因后,处理就比较简单了。
对非阻塞 socket
而言,EAGAIN
其实不能算是真正的错误。抛出 EAGAIN
异常,只是想告诉我们稍后再试。
我将 EAGAIN
的处理分为两种情况。
5.1、accept()
、recv()
处理
对于 accept()
,recv()
引起的 EAGAIN
异常,我们可以直接捕获异常,然后 pass
掉。
# accept()处理
try:
client, address = sock.accept()
except BlockingIOError as err:
pass
# recv()处理
try:
data = sock.recv(1024)
except BlockingIOError as err:
pass
这样处理,不用担心会漏掉某些连接或数据接收。下次循环,接着 accept()
、recv()
,不会有任何影响。
5.2、send()
处理
对于 send()
引起的 EAGAIN
异常,不能直接 pass
。作为数据发送方,直接 pass
掉 EAGAIN
会导致数据丢失。
我们可以,尝试 sleep(1)
,给内核足够的时间取走发送缓冲区中的数据,然后再次尝试发送。
try:
sock.send('hello world!'.encode())
except BlockingIOError as err:
time.sleep(1)
sock.send('hello world!'.encode())
这样的处理方式虽然简单,但不是最好的。如果接收方程序存在异常,导致接收方 recv()
的速度远远小于发送方 send()
的速度。那么,可能 sleep(1)
会导致我们的程序性能急剧下降。
比较好的做法是结合 poll
、epoll
等,暂时保存下发送失败的数据,将对应 socket
丢入事件循环中,等待可写事件触发,再次发送。
事件循环的示例写出会比较长,但是不难,感兴趣的朋友可以研究研究~~ 如果有任何问题,欢迎讨论。
tip:以上分析,如有问题,欢迎指正!
END.
原创不易,点个赞呗!
如果喜欢,欢迎随意赞赏,动力支援,请作者喝奶茶
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)