Python:如何在recv等待数据时关闭UDP套接字?

1 投票
2 回答
5323 浏览
提问于 2025-04-15 23:11

我们来看一下这段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服务器。这个服务器是通过一个类来实现的,类里面启动了一个线程,线程里有一个无限循环,循环中有recvonPacket的回调函数,这些都放在一个try/except块里,用来捕捉错误并退出循环。

我期望的是,当我在命令行输入“stop”时,套接字会被关闭,并且recvfrom函数会因为文件描述符无效而抛出异常。可是,奇怪的是,即使在调用close之后,recvfrom似乎还是会阻塞线程,继续等待数据。

为什么会出现这种奇怪的情况呢?我之前在C++和JAVA中实现UDP服务器时,一直用这种方式,效果都很好。

我还尝试过用select,把套接字放到xread参数的列表里,想通过select来获取文件描述符中断的事件,而不是通过recvfrom,但select似乎对close也“无动于衷”。

我需要一段独特的代码,能够在Linux和Windows上都保持相同的行为,使用Python 2.5 - 2.6。

谢谢。

2 个回答

1

这不是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
4

通常的解决办法是让一个管道告诉工作线程什么时候结束。

  1. 首先,使用 os.pipe 创建一个管道。这会给你一个套接字,程序里有读和写两个端口。它返回的是原始的文件描述符,你可以直接用它们(通过 os.reados.write),或者用 os.fdopen 转换成 Python 的文件对象。

  2. 工作线程同时监听网络套接字和管道的读端,使用 select.select。当管道变得可读时,工作线程就会进行清理并退出。这里要注意,不要去读取数据,忽略它:数据到达本身就是一个信号。

  3. 当主线程想要结束工作线程时,它会向管道的写端写入一个字节(可以是任何值)。然后,主线程会等待工作线程结束,最后关闭管道(记得要关闭两个端口)。

附注:在多线程程序中,关闭正在使用的套接字是个坏主意。Linux 的 close(2) 手册上说:

在同一个进程的其他线程中,如果文件描述符可能正在被系统调用使用,关闭它们可能是不明智的。因为文件描述符可能会被重新使用,可能会出现一些不明显的竞争条件,导致意想不到的副作用。

所以你最初的做法没有成功,真是幸运!

撰写回答