簡體   English   中英

如何從 python 中的另一個線程中止 socket.recvfrom()?

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

這看起來像是How do I abort a socket.recv() from another thread in Python 的副本,但事實並非如此,因為我想在一個線程中中止 recvfrom(),該線程是 UDP,而不是 TCP。

這可以通過 poll() 或 select.select() 解決嗎?

如果您想解除對另一個線程的 UDP 讀取的阻塞,請向它發送數據報!

Rgds, 馬丁

處理這種異步中斷的一個好方法是舊的 C 管道技巧。 您可以創建一個管道並在套接字和管道上使用select / poll :現在,當您想要中斷接收器時,您只需向管道發送一個字符即可。

  • 優點:
    • 可以同時用於 UDP 和 TCP
    • 協議不可知
  • 缺點:
    • 選擇/輪詢管道在 Windows 上不可用,在這種情況下,您應該將其替換為另一個用作通知管道的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,我們應該隔離對象中的異步通知,以便為我們的操作系統選擇正確的實現

首先,我們將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)

在此之后,我們可以通過raise InterruptException替換return ''並且測試再次通過。

准備擴展的版本可以是:

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 版本或處理套接字超時變得非常簡單。

這里的解決辦法是強行關閉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)

要在 python 中正確關閉 tcp 套接字,您必須在調用 socket.close() 之前調用 socket.shutdown(arg)。 參見python socket文檔,關於關機的部分。

如果套接字是 UDP,則不能調用 socket.shutdown(...),它會引發異常。 單獨調用 socket.close() 會像 tcp 一樣,保持阻塞的操作阻塞。 close() 單獨不會打斷他們。

許多建議的解決方案(並非全部)不起作用或被視為麻煩,因為它們涉及 3rd 方庫。 我還沒有測試過 poll() 或 select()。 什么絕對有效,如下:

首先,為運行 socket.recv() 的任何線程創建一個正式的 Thread 對象,並將句柄保存到它。 其次,導入信號。 Signal 是一個官方庫,它可以向進程發送/接收 linux/posix 信號(閱讀其文檔)。 第三,要中斷,假設您的線程的句柄稱為 udpThreadHandle:

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

當然,在實際的線程/循環中進行接收:

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

請注意,KeyboardInterrupt 的異常處理程序(由 SIGINT 生成)在接收循環之外。 這會默默地終止接收循環及其線程。

在服務器和客戶端套接字上實現退出命令。 應該像這樣工作:

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