如何从python中的另一个线程中止socket.recvfrom()?

时间:2011-09-16 19:03:29

标签: python multithreading udp pthreads

这看起来像是How do I abort a socket.recv() from another thread in Python的副本,但事实并非如此,因为我想在线程中中止recvfrom(),这是UDP,而不是TCP。

这可以通过poll()或select.select()来解决吗?

4 个答案:

答案 0 :(得分:5)

如果要从另一个线程取消阻止UDP读取,请将其发送给数据报!

RGDS, 马丁

答案 1 :(得分:3)

处理这种异步中断的好方法是旧的C管道技巧。您可以在套接字和管道上创建管道并使用select / poll:现在,当您需要中断接收器时,您只需将一个字符发送到管道。

  • 优点:
    • 可以同时用于UDP和TCP
    • 协议不可知
  • 缺点:
    • 管道上的select / poll在Windows上不可用,在这种情况下,您应该将其替换为另一个用作通知 pipe 的UDP套接字

起点

interruptable_socket.py

import os
import socket
import select


class InterruptableUdpSocketReceiver(object):
    def __init__(self, host, port):
        self._host = host
        self._port = port
        self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self._r_pipe, self._w_pipe = os.pipe()
        self._interrupted = False

    def bind(self):
        self._socket.bind((self._host, self._port))

    def recv(self, buffersize, flags=0):
        if self._interrupted:
            raise RuntimeError("Cannot be reused")
        read, _w, errors = select.select([self._r_pipe, self._socket], [], [self._socket])
        if self._socket in read:
            return self._socket.recv(buffersize, flags)
        return ""

    def interrupt(self):
        self._interrupted = True
        os.write(self._w_pipe, "I".encode())

测试套件:

test_interruptable_socket.py

import socket
from threading import Timer
import time
from interruptable_socket import InterruptableUdpSocketReceiver
import unittest


class Sender(object):
    def __init__(self, destination_host, destination_port):
        self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
        self._dest = (destination_host, destination_port)

    def send(self, message):
        self._socket.sendto(message, self._dest)

class Test(unittest.TestCase):
    def create_receiver(self, host="127.0.0.1", port=3010):
        receiver = InterruptableUdpSocketReceiver(host, port)
        receiver.bind()
        return receiver

    def create_sender(self, host="127.0.0.1", port=3010):
        return Sender(host, port)

    def create_sender_receiver(self, host="127.0.0.1", port=3010):
        return self.create_sender(host, port), self.create_receiver(host, port)

    def test_create(self):
        self.create_receiver()

    def test_recv_async(self):
        sender, receiver = self.create_sender_receiver()
        start = time.time()
        send_message = "TEST".encode('UTF-8')
        Timer(0.1, sender.send, (send_message, )).start()
        message = receiver.recv(128)
        elapsed = time.time()-start
        self.assertGreaterEqual(elapsed, 0.095)
        self.assertLess(elapsed, 0.11)
        self.assertEqual(message, send_message)

    def test_interrupt_async(self):
        receiver = self.create_receiver()
        start = time.time()
        Timer(0.1, receiver.interrupt).start()
        message = receiver.recv(128)
        elapsed = time.time()-start
        self.assertGreaterEqual(elapsed, 0.095)
        self.assertLess(elapsed, 0.11)
        self.assertEqual(0, len(message))

    def test_exception_after_interrupt(self):
        sender, receiver = self.create_sender_receiver()
        receiver.interrupt()
        with self.assertRaises(RuntimeError):
            receiver.recv(128)


if __name__ == '__main__':
    unittest.main()

演进

现在这段代码只是一个起点。为了使它更通用,我看到我们应该修复以下问题:

  1. 接口:在中断情况下返回空消息不是很划算,最好是使用异常来处理它
  2. 泛化:我们应该只有一个函数在socket.recv()之前调用,将中断扩展到其他recv方法变得非常简单
  3. 可移植性:要将简单端口移植到Windows,我们应该隔离对象中的异步通知,为我们的操作系统选择正确的实现
  4. 首先,我们更改test_interrupt_async()以检查异常而不是空消息:

    from interruptable_socket import InterruptException
    
    def test_interrupt_async(self):
        receiver = self.create_receiver()
        start = time.time()
        with self.assertRaises(InterruptException):
            Timer(0.1, receiver.interrupt).start()
            receiver.recv(128)
        elapsed = time.time()-start
        self.assertGreaterEqual(elapsed, 0.095)
        self.assertLess(elapsed, 0.11)
    

    在此之后,我们可以将return ''替换为raise InterruptException,然后测试再次通过。

    准备扩展版本可以是:

    interruptable_socket.py

    import os
    import socket
    import select
    
    
    class InterruptException(Exception):
        pass
    
    
    class InterruptableUdpSocketReceiver(object):
        def __init__(self, host, port):
            self._host = host
            self._port = port
            self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            self._async_interrupt = AsycInterrupt(self._socket)
    
        def bind(self):
            self._socket.bind((self._host, self._port))
    
        def recv(self, buffersize, flags=0):
            self._async_interrupt.wait_for_receive()
            return self._socket.recv(buffersize, flags)
    
        def interrupt(self):
            self._async_interrupt.interrupt()
    
    
    class AsycInterrupt(object):
        def __init__(self, descriptor):
            self._read, self._write = os.pipe()
            self._interrupted = False
            self._descriptor = descriptor
    
        def interrupt(self):
            self._interrupted = True
            self._notify()
    
        def wait_for_receive(self):
            if self._interrupted:
                raise RuntimeError("Cannot be reused")
            read, _w, errors = select.select([self._read, self._descriptor], [], [self._descriptor])
            if self._descriptor not in read:
                raise InterruptException
    
        def _notify(self):
            os.write(self._write, "I".encode())
    

    现在包含更多recv函数,实现Windows版本或处理套接字超时变得非常简单。

答案 2 :(得分:0)

在服务器和客户端套接字上实现quit命令。应该像这样工作:

Thread1: 
    status: listening
    handler: quit

Thread2: client
    exec: socket.send "quit"  ---> Thread1.socket @ host:port

Thread1: 
    status: socket closed()

答案 3 :(得分:0)

这里的解决办法是强行关闭socket。问题是这样做的方法是特定于操作系统的,而 Python 在抽象方法或后果方面做得不好。基本上,您需要在套接字上执行shutdown(),然后执行close()。在 Linux 等 POSIX 系统上,关闭是强制 recvfrom 停止的关键因素(仅调用 close() 不会这样做)。在 Windows 上,shutdown() 不影响 recvfrom 并且 close() 是关键元素。这正是您在 C 中实现此代码并使用本机 POSIX 套接字或 Winsock 套接字时会看到的行为,因此 Python 在这些调用之上提供了一个非常薄的层。

在 POSIX 和 Windows 系统上,此调用序列会导致引发 OSError。但是,异常的位置及其详细信息是特定于操作系统的。在 POSIX 系统上,在调用 shutdown() 时引发异常,并且异常的 errno 值设置为 107(传输端点未连接)。在 Windows 系统上,在调用 recvfrom() 时会引发异常,并且异常的 winerror 值设置为 10038(尝试对非套接字进行操作)。这意味着无法以与操作系统无关的方式执行此操作,代码必须考虑 Windows 和 POSIX 行为和错误。这是我写的一个简单示例:

import socket
import threading
import time

class MyServer(object):
    def __init__(self, port:int=0):
        if port == 0:
            raise AttributeError('Invalid port supplied.')

        self.port = port
        self.socket = socket.socket(family=socket.AF_INET,
                type=socket.SOCK_DGRAM)
        self.socket.bind(('0.0.0.0', port))

        self.exit_now = False

        print('Starting server.')
        self.thread = threading.Thread(target=self.run_server,
                args=[self.socket])
        self.thread.start()

    def run_server(self, socket:socket.socket=None):
        if socket is None:
            raise AttributeError('No socket provided.')

        buffer_size = 4096

        while self.exit_now == False:
            data = b''
            try:
                data, address = socket.recvfrom(buffer_size)
            except OSError as e:
                if e.winerror == 10038:
                    # Error is, "An operation was attempted on something that
                    # is not a socket".  We don't care.
                    pass
                else:
                    raise e
            if len(data) > 0:
                print(f'Received {len(data)} bytes from {address}.')

    def stop(self):
        self.exit_now = True
        try:
            self.socket.shutdown(socket.SHUT_RDWR)
        except OSError as e:
            if e.errno == 107:
                # Error is, "Transport endpoint is not connected".
                # We don't care.
                pass
            else:
                raise e
        self.socket.close()
        self.thread.join()
        print('Server stopped.')


if __name__ == '__main__':
    server = MyServer(5555)
    time.sleep(2)
    server.stop()
    exit(0)