为什么我的socket.makefile对象在关闭后仍然阻塞?

2 投票
2 回答
2419 浏览
提问于 2025-04-16 22:06

如果我使用 socket.makefile,然后关闭文件对象和底层的socket,那么后续调用 read 时会抛出一个异常,这正是我想要的。例如,下面的代码按我预期的那样工作:

import socket
from time import sleep
from threading import Thread

ADDR = ("localhost", 4321)

def listener(sock):
    client,addr = sock.accept()
    sleep(1)
    client.close()
    sock.close()

server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_sock.bind(ADDR)
server_sock.listen(1)
Thread(target=listener, args=[server_sock]).start()

sock = socket.create_connection(ADDR)
f = sock.makefile("r+b", bufsize=0)

f.close()
sock.close()
f.read(8)    # throws an exception, as I'd expect

但是,如果在文件/socket仍然打开的情况下调用 read,那么这个调用会被阻塞,接着如果我关闭socket,read 方法仍然不会返回。实际上,它会一直挂着,直到另一端关闭socket。下面的代码展示了这种让人烦恼的行为:

import socket
from time import sleep
from threading import Thread

ADDR = ("localhost", 4321)

def reader(f):
    print("about to read")
    print("we read %r" % f.read(8))
    print("finished reading")

def listener(sock):
    client, addr = sock.accept()
    sleep(3)
    client.close()
    sock.close()

server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_sock.bind(ADDR)
server_sock.listen(1)
Thread(target=listener, args=[server_sock]).start()

sock = socket.create_connection(ADDR)
f = sock.makefile("r+b", bufsize=0)
Thread(target=reader, args=[f]).start()

sleep(1)
print("closing pseudo-file and socket")
f.close()
sock.close()
sleep(1)
print("we still haven't finished reading!")

这对我来说是个严重的问题,因为我想在一个线程中进行阻塞调用 f.read,但同时又希望能够关闭socket,让这个线程从调用中返回(可能通过抛出异常)并退出。然而,发生的情况是,只要另一端不关闭socket,这个调用就会永远阻塞。

那么,有没有办法让 Thread1 在通过 socket.makefile 创建的类文件对象上调用 read,然后让 Thread2 关闭socket,从而使 Thread1 停止在 read 调用上阻塞呢?

编辑:我尝试重写我的程序,完全使用 gevent 及其socket和Greenlet的多线程方法,但我的程序仍然表现得完全一样:

from gevent import sleep, socket, spawn

ADDR = ("localhost", 4321)

def reader(f):
    print("about to read")
    print("we read %r" % f.read(8))
    print("finished reading")

def listener(sock):
    client, addr = sock.accept()
    sleep(3)
    client.close()
    sock.close()

server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_sock.bind(ADDR)
server_sock.listen(1)
spawn(listener, server_sock)

sock = socket.create_connection(ADDR)
f = sock.makefile("r+b", bufsize=0)
spawn(reader, f)

sleep(1)
print("closing pseudo-file and socket")
f.close()
sock.close()

sleep(1)
print("we still haven't finished reading!")
sleep(2)

我很惊讶地发现,即使使用 gevent sockets,调用 read 仍然会阻塞,即使底层的socket已经关闭。如果没有办法防止这种情况,我可能只能接受Thomas那句令人沮丧的“这是不可能的”回答 :(

2 个回答

4

在这种情况下,我以前成功地使用过 eventlet,而现在 gevent 也能做类似的事情:

它们通过一种叫做“猴子补丁”的方法,修改了套接字库,让它可以使用非阻塞的输入输出。下面是你可以尝试的一个例子:

>>> from gevent import monkey; monkey.patch_socket()
>>> import socket

然后看看这会对你的结果产生什么影响

1

是的,线程在处理一些事情时会表现得很糟糕,尤其是当多个线程同时操作像套接字这样的东西时。这种情况是很难让它正常工作的。你无法强制一个正在读取数据的线程中断这个读取操作或者结束它——在它还在读取的时候关闭套接字,反而更可能导致你的整个程序崩溃(或者更糟糕的情况),而不是达到你想要的效果。

处理这种情况的正确方法是使用非阻塞读取。像Twisted这样的事件框架可以帮助你解决这个问题。

撰写回答