简体   繁体   English

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

[英]How do I abort a socket.recvfrom() from another thread in python?

This looks like a duplicate of How do I abort a socket.recv() from another thread in Python , but it's not, since I want to abort recvfrom() in a thread, which is UDP, not TCP.这看起来像是How do I abort a socket.recv() from another thread in Python 的副本,但事实并非如此,因为我想在一个线程中中止 recvfrom(),该线程是 UDP,而不是 TCP。

Can this be solved by poll() or select.select() ?这可以通过 poll() 或 select.select() 解决吗?

If you want to unblock a UDP read from another thread, send it a datagram!如果您想解除对另一个线程的 UDP 读取的阻塞,请向它发送数据报!

Rgds, Martin Rgds, 马丁

A good way to handle this kind of asynchronous interruption is the old C pipe trick.处理这种异步中断的一个好方法是旧的 C 管道技巧。 You can create a pipe and use select / poll on both socket and pipe: Now when you want interrupt receiver you can just send a char to the pipe.您可以创建一个管道并在套接字和管道上使用select / poll :现在,当您想要中断接收器时,您只需向管道发送一个字符即可。

  • pros:优点:
    • Can work both for UDP and TCP可以同时用于 UDP 和 TCP
    • Is protocol agnostic协议不可知
  • cons:缺点:
    • select/poll on pipes are not available on Windows, in this case you should replace it by another UDP socket that use as notification pipe选择/轮询管道在 Windows 上不可用,在这种情况下,您应该将其替换为另一个用作通知管道的UDP 套接字

Starting point初始点

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())

A test suite:一个测试套件:

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()

Evolution进化

Now this code is just a starting point.现在这段代码只是一个起点。 To make it more generic I see we should fix follow issues:为了使它更通用,我认为我们应该解决以下问题:

  1. Interface : return empty message in interrupt case is not a good deal, is better to use an exception to handle it接口:在中断情况下返回空消息不好处理,最好使用异常来处理它
  2. Generalization : we should have just a function to call before socket.recv() , extend interrupt to others recv methods become very simple概括:我们应该在socket.recv()之前调用一个函数,将中断扩展到其他recv方法变得非常简单
  3. Portability : to make simple port it to windows we should isolate the async notification in a object to choose the right implementation for our operating system可移植性:为了将它简单地移植到 Windows,我们应该隔离对象中的异步通知,以便为我们的操作系统选择正确的实现

First of all we change test_interrupt_async() to check exception instead empty message:首先,我们将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)

After this we can replace return '' by raise InterruptException and the tests pass again.在此之后,我们可以通过raise InterruptException替换return ''并且测试再次通过。

The ready to extend version can be :准备扩展的版本可以是:

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())

Now wraps more recv function, implement a windows version or take care of socket timeouts become really simple.现在包装更多recv函数,实现 Windows 版本或处理套接字超时变得非常简单。

The solution here is to forcibly close the socket.这里的解决办法是强行关闭socket。 The problem is that the method for doing this is OS-specific and Python does not do a good job of abstracting the way to do it or the consequences.问题在于,这样做的方法是特定于操作系统的,而 Python 并没有很好地抽象出方法或后果。 Basically, you need to do a shutdown() followed by a close() on the socket.基本上,您需要在套接字上执行shutdown(),然后执行close()。 On POSIX systems such as Linux, the shutdown is the key element in forcing recvfrom to stop (a call to close() alone won't do it).在 Linux 等 POSIX 系统上,关闭是强制 recvfrom 停止的关键因素(仅调用 close() 不会这样做)。 On Windows, shutdown() does not affect the recvfrom and the close() is the key element.在 Windows 上,shutdown() 不影响 recvfrom 并且 close() 是关键元素。 This is exactly the behavior that you would see if you were implementing this code in C and using either native POSIX sockets or Winsock sockets, so Python is providing a very thin layer on top of those calls.这正是您在 C 中实现此代码并使用本机 POSIX 套接字或 Winsock 套接字时会看到的行为,因此 Python 在这些调用之上提供了一个非常薄的层。

On both POSIX and Windows systems, this sequence of calls results in an OSError being raised.在 POSIX 和 Windows 系统上,此调用序列会导致引发 OSError。 However, the location of the exception and the details of it are OS-specific.但是,异常的位置及其详细信息是特定于操作系统的。 On POSIX systems, the exception is raised on the call to shutdown() and the errno value of the exception is set to 107 (Transport endpoint is not connected).在 POSIX 系统上,在调用 shutdown() 时引发异常,并且异常的 errno 值设置为 107(传输端点未连接)。 On Windows systems, the exception is raised on the call to recvfrom() and the winerror value of the exception is set to 10038 (An operation was attempted on something that is not a socket).在 Windows 系统上,在调用 recvfrom() 时会引发异常,并且异常的 winerror 值设置为 10038(尝试对非套接字进行操作)。 This means that there's no way to do this in an OS-agnositc way, the code has to account for both Windows and POSIX behavior and errors.这意味着无法以与操作系统无关的方式执行此操作,代码必须考虑 Windows 和 POSIX 行为和错误。 Here's a simple example I wrote up:这是我写的一个简单的例子:

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)

To properly close a tcp socket in python, you have to call socket.shutdown(arg) before calling socket.close().要在 python 中正确关闭 tcp 套接字,您必须在调用 socket.close() 之前调用 socket.shutdown(arg)。 See the python socket documentation, the part about shutdown.参见python socket文档,关于关机的部分。

If the socket is UDP, you can't call socket.shutdown(...), it would raise an exception.如果套接字是 UDP,则不能调用 socket.shutdown(...),它会引发异常。 And calling socket.close() alone would, like for tcp, keep the blocked operations blocking.单独调用 socket.close() 会像 tcp 一样,保持阻塞的操作阻塞。 close() alone won't interrupt them. close() 单独不会打断他们。

Many suggested solutions (not all), don't work or are seen as cumbersome as they involve 3rd party libraries.许多建议的解决方案(并非全部)不起作用或被视为麻烦,因为它们涉及 3rd 方库。 I haven't tested poll() or select().我还没有测试过 poll() 或 select()。 What does definately work, is the following:什么绝对有效,如下:

firstly, create an official Thread object for whatever thread is running socket.recv(), and save the handle to it.首先,为运行 socket.recv() 的任何线程创建一个正式的 Thread 对象,并将句柄保存到它。 Secondly, import signal.其次,导入信号。 Signal is an official library, which enables sending/recieving linux/posix signals to processes (read its documentation). Signal 是一个官方库,它可以向进程发送/接收 linux/posix 信号(阅读其文档)。 Thirdly, to interrupt, assuming that handle to your thread is called udpThreadHandle:第三,要中断,假设您的线程的句柄称为 udpThreadHandle:

signal.pthread_kill(udpthreadHandle.ident, signal.SIGINT)

and ofcourse, in the actual thread/loop doing the recieving:当然,在实际的线程/循环中进行接收:

try:
    while True:
       myUdpSocket.recv(...)
except KeyboardInterrupt:
    pass

Notice, the exception handler for KeyboardInterrupt (generated by SIGINT), is OUTSIDE the recieve loop.请注意,KeyboardInterrupt 的异常处理程序(由 SIGINT 生成)在接收循环之外。 This silently terminates the recieve loop and its thread.这会默默地终止接收循环及其线程。

Implement a quit command on the server and client sockets.在服务器和客户端套接字上实现退出命令。 Should work something like this:应该像这样工作:

Thread1: 
    status: listening
    handler: quit

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

Thread1: 
    status: socket closed()

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM