Логика построения сетевого приложения и работа с socketserver
Доброго времени суток.
Кратко: Как обеспечить связь между двумя пользователями программы с типом подключения клиент-клиент?
Подробно:
Возникла идея получить и сразу же применить знания в области сетей и многопоточности на Python. Предполагалось написать несложную программу, "исполняемый файл" которой будет одинаковым у обеих пользователей.
Познакомился с socket-ами. Написал (скопировал) примеры с первого сайта. Насколько хватило мозгов, понял, что в сокетах всё делится на "сервер" и "клиент", причём передавать и принимать данные может как первый, так и второй.
Хорошо, сказал я себе. Пускай в приложении будет кнопка "создать сервер" и "подключиться к серверу". В первом случае будет запускаться "сервер", во втором - клиент (логично).
Проблемы начались, когда дошло дело до кода. Ещё на этапе изучения информации, я понял, что всё общение с сетью нужно выносить в отдельные поток. Значит, в программе у меня должны быть "ServerThread" и "ClientThread", так как назначения (ровно как и код) у них разные. До этого момента было всё более-менее хорошо, и даже получилось получить подтверждение, что потоки работают как надо, и программы "слышат" друг друга.
Не очень информативно, но всё же (слева "сервер", справа "клиент"):

И вот самое главное - процесс отправления информации туда-сюда, вызывает у меня судорожное недоумение. Моя идея: так как информация может отсылаться с обоих сторон, значит, обе стороны должны постоянно (с каким-то промежутком) опрашивать сокет (или просто ждать из него информации). Супер. Значит, внутри каждого такого потока должен быть ПОДпоток?! Или как это должно быть устроено?
На данный момент я вижу пару решений:
- Сохранять сокет. Что я имею под этим ввиду: возможно, connection, который возвращает сервер, можно хранить вне потока? И то же самое делать и с socket клиента? Тогда потоков всё-равно остаётся два, но уже других - направленных на получение/отправку информации.
О каких connection и socket я веду речь:
Но можно ли такое делать? Нет, мне не сложно попробовать, но это будет велосипед, ведь правда? - Сделать сервер отдельной программой. Ну, такой вариант будет рассматриваться мной в последнюю очередь. Всё же идею изначальную хочется реализовать именно описанным выше образом.
Суммируя всё вышесказанное: прошу помочь с спагетти-идеями или хотя бы тыкнуть носом туда, где лучше почитать про реализацию подобной штуки.
Спасибо.
По совету @eri слегка переработал идею: оба приложения запускают севера в отдельном потоке на "прослушивание", а отправку делают с помощью UDP без потоков. Таким образом под кнопкой "Создать сервер" теперь находится:
class Programm:
...
def create_server(self):
HOST, PORT = "localhost", 9999
with socketserver.UDPServer((HOST, PORT), MyUDPHandler) as server:
self.server_thread = threading.Thread(target=server.serve_forever)
# self.server_thread.start() # Смотри ниже
, где MyUDPHandler:
class MyUDPHandler(socketserver.BaseRequestHandler):
def handle(self):
data = self.request[0].decode()
print("{} wrote:".format(self.client_address[0]))
print(data)
Производится попытка отправки сообщения на данный сервер из этой же программы:
class Programm:
...
def connect_to_server(self):
# Пока что просто пытаемся отправить data
HOST, PORT = "localhost", 9999
data = "Wow, it's data!"
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.sendto(data.encode(), (HOST, PORT))
print("Sent: {}".format(data))
В идеале я должен видеть в консоли две строки:
(1) Sent: Wow, it's data! # Информация отправленная клиентом
(2) "Wow, it's data!" # Информация принятая сервером
Тем не менее, этого не происходит. Появляется лишь (1), а в метод handle выполнение программы и вовсе не заходит.
Я подумал, может, поток "инициализируется", но при это не запускается? Хорошо, добавил строку self.server_thread.start() в метод create_server (место помещения этой строчки смотри в коде выше). Теперь программа совершенно не хочет запускаться, заявляя следующее:
OSError: [WinError 10038] Сделана попытка выполнить операцию на объекте, не являющемся сокетом
Отладка не дала полезной для меня информации. Где я могу ошибаться? В понимании потоков? В MyUDPHandler?
Проблема скрывалась в методе create_server. Переписанный код:
class Programm:
...
def create_server(self):
HOST, PORT = "localhost", 9999
server = socketserver.UDPServer((HOST, PORT), MyUDPHandler)
self.server_thread = threading.Thread(target=server.serve_forever)
self.server_thread.start()
Ответы (1 шт):
Клиент-клиент соединение сложно в реализации, но возможно при использовании протоколов поверх udp.
Лучше же использовать соединение сервер-сервер с UDP. Ведь сокет сервер датаграм может отсылать сообщения на любой адрес.
Для опроса сервера есть готовые конструкции.
Синхронный сервер. https://docs.python.org/3/library/socketserver.html
. На каждое сообщение будет вызываться handle в котором можно читать сообщения. server_thread = threading.Thread(target=server.serve_forever) и Ваш сервер работает в отдельном потоке.
Асинхронный вариант чуть сложнее и требует запуска asyncio.loop
https://docs.python.org/3/library/asyncio-protocol.html#udp-echo-server
Для графических приложений надо этот сервер запускать в отдельном процессе или запускаться на потоке графики через https://github.com/CabbageDevelopment/qasync или подобное.
Дергать poll/select - вариант, но это усложняет код.
Держи минимальный пример:
import socketserver
import sys
import threading
class MyUDPHandler(socketserver.BaseRequestHandler):
def handle(self):
data = self.request[0].strip()
socket = self.request[1]
peer, peer_port = self.client_address # Узнаешь адрес вызывающей стороны
print("\r{}:{} <".format(peer, peer_port))
print(data.decode())
HOST, PORT = "0.0.0.0", int(sys.argv[1])
print('Serving at {}:{}'.format(HOST, PORT) )
server = socketserver.UDPServer((HOST, PORT), MyUDPHandler)
server_thread = threading.Thread(target=server.serve_forever)
server_thread.start()
sock = server.socket
# Эта часть в графике будет не обязательной на одной стороне.
# Пира можно получить когда придет первый пакет.
# Но в этой версии нужно ввести на обоих концах
peer, peer_port = input('Connect to:').split(':')
peer_port = int(peer_port)
while True: # основной цикл интерфейса
sock.sendto(input('>').encode(), (peer, peer_port))
Запускаешь python3 udpui.py 6667 и python3 udpui.py 6667. Потом вводи адрес обратной стороны и можно чатиться
