Python:如何在recv等待数据时关闭UDP套接字?
我们来看一下这段Python代码:
import socket
import threading
import sys
import select
class UDPServer:
def __init__(self):
self.s=None
self.t=None
def start(self,port=8888):
if not self.s:
self.s=socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.s.bind(("",port))
self.t=threading.Thread(target=self.run)
self.t.start()
def stop(self):
if self.s:
self.s.close()
self.t.join()
self.t=None
def run(self):
while True:
try:
#receive data
data,addr=self.s.recvfrom(1024)
self.onPacket(addr,data)
except:
break
self.s=None
def onPacket(self,addr,data):
print addr,data
us=UDPServer()
while True:
sys.stdout.write("UDP server> ")
cmd=sys.stdin.readline()
if cmd=="start\n":
print "starting server..."
us.start(8888)
print "done"
elif cmd=="stop\n":
print "stopping server..."
us.stop()
print "done"
elif cmd=="quit\n":
print "Quitting ..."
us.stop()
break;
print "bye bye"
这段代码运行了一个交互式的命令行,可以让我启动和停止一个UDP服务器。这个服务器是通过一个类来实现的,类里面启动了一个线程,线程里有一个无限循环,循环中有recv和onPacket的回调函数,这些都放在一个try/except块里,用来捕捉错误并退出循环。
我期望的是,当我在命令行输入“stop”时,套接字会被关闭,并且recvfrom函数会因为文件描述符无效而抛出异常。可是,奇怪的是,即使在调用close之后,recvfrom似乎还是会阻塞线程,继续等待数据。
为什么会出现这种奇怪的情况呢?我之前在C++和JAVA中实现UDP服务器时,一直用这种方式,效果都很好。
我还尝试过用select,把套接字放到xread参数的列表里,想通过select来获取文件描述符中断的事件,而不是通过recvfrom,但select似乎对close也“无动于衷”。
我需要一段独特的代码,能够在Linux和Windows上都保持相同的行为,使用Python 2.5 - 2.6。
谢谢。
2 个回答
这不是Java。这里有一些不错的建议:
- 不要使用线程。要使用异步输入输出。
- 使用更高级的网络框架。
下面是一个使用twisted的例子:
from twisted.internet.protocol import DatagramProtocol
from twisted.internet import reactor, stdio
from twisted.protocols.basic import LineReceiver
class UDPLogger(DatagramProtocol):
def datagramReceived(self, data, (host, port)):
print "received %r from %s:%d" % (data, host, port)
class ConsoleCommands(LineReceiver):
delimiter = '\n'
prompt_string = 'myserver> '
def connectionMade(self):
self.sendLine('My Server Admin Console!')
self.transport.write(self.prompt_string)
def lineReceived(self, line):
line = line.strip()
if line:
if line == 'quit':
reactor.stop()
elif line == 'start':
reactor.listenUDP(8888, UDPLogger())
self.sendLine('listening on udp 8888')
else:
self.sendLine('Unknown command: %r' % (line,))
self.transport.write(self.prompt_string)
stdio.StandardIO(ConsoleCommands())
reactor.run()
示例会话:
My Server Admin Console!
myserver> foo
Unknown command: 'foo'
myserver> start
listening on udp 8888
myserver> quit
通常的解决办法是让一个管道告诉工作线程什么时候结束。
首先,使用
os.pipe
创建一个管道。这会给你一个套接字,程序里有读和写两个端口。它返回的是原始的文件描述符,你可以直接用它们(通过os.read
和os.write
),或者用os.fdopen
转换成 Python 的文件对象。工作线程同时监听网络套接字和管道的读端,使用
select.select
。当管道变得可读时,工作线程就会进行清理并退出。这里要注意,不要去读取数据,忽略它:数据到达本身就是一个信号。当主线程想要结束工作线程时,它会向管道的写端写入一个字节(可以是任何值)。然后,主线程会等待工作线程结束,最后关闭管道(记得要关闭两个端口)。
附注:在多线程程序中,关闭正在使用的套接字是个坏主意。Linux 的 close(2) 手册上说:
在同一个进程的其他线程中,如果文件描述符可能正在被系统调用使用,关闭它们可能是不明智的。因为文件描述符可能会被重新使用,可能会出现一些不明显的竞争条件,导致意想不到的副作用。
所以你最初的做法没有成功,真是幸运!