Как прервать сокет.recvfrom () из другого потока в python?
Это похоже на дубликат Как прервать исполнение.recv () из другого потока в Python, но это не так, так как я хочу прервать recvfrom() в потоке, который является UDP, а не TCP.
можно ли это решить с помощью poll () или select.select ()?
3 ответов
Если вы хотите разблокировать чтение UDP из другого потока, отправьте ему датаграмму!
Rgds, Мартин!--1-->
хорошим способом справиться с таким асинхронным прерыванием является старый трюк с трубой. Вы можете создать трубу и использовать select
/poll
на сокете и трубе: теперь, когда вы хотите прерывать приемник, вы можете просто отправить символ в трубу.
- плюсы:
- может работать как для UDP, так и для TCP
- протокол агностик
- минусы:
- select / poll на трубах недоступны в 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()
эволюция
теперь этот код является только отправной точкой. Чтобы сделать его более общим, я вижу, что мы должны исправить следующие проблемы:
- интерфейс: вернуть пустое сообщение в случай прерывания не очень хороший, лучше использовать исключение для его обработки
-
обобщение: мы должны иметь только функцию для вызова перед
socket.recv()
, расширить прерывание для другихrecv
методы становятся очень простыми - мобильность: чтобы сделать простой порт для 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)
после этого мы можем заменить return ''
by 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 или заботиться о тайм-аутах сокета становится очень просто.
реализуйте команду quit на сервере и клиентских сокетах. Должно работать что-то вроде этого:
Thread1:
status: listening
handler: quit
Thread2: client
exec: socket.send "quit" ---> Thread1.socket @ host:port
Thread1:
status: socket closed()