Надёжные сетевые приложения с помощью asyncio, socket и TLS
Способы создания сетевых служб в Python 3
В Python 3 для работы с сетью доступны модули socket, socketserver, selectors, asyncio и ssl. Сокеты открывают низкоуровневый доступ к TCP и UDP. Модуль asyncio организует конкурентное выполнение, а ssl добавляет защищённый канал. Выбор API влияет на количество обслуживаемых подключений и сложность кода.
Какой способ обслуживания многих TCP-подключений подходит для высоконагруженных сервисов?
Библиотека asyncio создана для событийной обработки сети. В одном процессе работает цикл событий, а каждое соединение представлено парой reader и writer. Такой сервер не простаивает, пока клиент не отправил данные, и не тратит ресурсы на лишние потоки.
import asyncio
async def echo_handler(reader: asyncio.StreamReader,
writer: asyncio.StreamWriter) -> None:
peer = writer.get_extra_info('peername')
print('Клиент подключён:', peer)
try:
while True:
data = await reader.read(1024)
if not data:
break
writer.write(data.upper())
await writer.drain()
except (ConnectionResetError, BrokenPipeError):
print('Соединение потеряно:', peer)
finally:
writer.close()
await writer.wait_closed()
async def start_server() -> None:
server = await asyncio.start_server(echo_handler, '127.0.0.1', 8888)
async with server:
await server.serve_forever()
if __name__ == '__main__':
asyncio.run(start_server())Python ipaddress ip network (модуль ipaddress в python)
Клиент для проверки отправляет байтовую строку и ждёт ответ.
import socket
with socket.create_connection(('127.0.0.1', 8888)) as client:
client.sendall(b'hello')
answer = client.recv(1024)
print(answer.decode())Python 3 сеть (сетевые возможности python 3)
Ответ сервера становится строкой HELLO. Метод read возвращает пустую последовательность при закрытии соединения. Вызов drain передаёт данные из буфера записи без блокировки всего цикла событий.
Что изменить в обычном сокетном сервере, чтобы каждый клиент обрабатывался параллельно?
Поток на соединение остаётся простым и понятным способом распараллеливания. Этот вариант подходит для локальных сервисов и небольшого числа абонентов.
import socket
from threading import Thread
def handle_tcp(connection, address):
print('Новое подключение:', address)
with connection:
while True:
data = connection.recv(1024)
if not data:
break
connection.sendall(data.upper())
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(('127.0.0.1', 9000))
server.listen()
print('TCP сервер слушает порт 9000')
while True:
client_socket, client_address = server.accept()
Thread(target=handle_tcp, args=(client_socket, client_address), daemon=True).start()Python client py (клиент на python)
После accept создаётся поток, в котором выполняются чтение и запись. Основной цикл сразу возвращается к приёму новых подключений. Такой подход удобен, когда клиентов десятки, а не тысячи.
Как обработать несколько сокетов в одном потоке без ожидания на каждом из них?
Модуль selectors передаёт управление операционной системе, которая сообщает о готовности сокета к чтению или записи. Это событийная альтернатива потокам.
import selectors
import socket
sel = selectors.DefaultSelector()
def accept_client(server_socket):
connection, address = server_socket.accept()
connection.setblocking(False)
print('Новый клиент:', address)
sel.register(connection, selectors.EVENT_READ, read_client)
def read_client(connection):
data = connection.recv(1024)
if data:
connection.sendall(data.upper())
else:
print('Клиент отключился:', connection.getpeername())
sel.unregister(connection)
connection.close()
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind(('127.0.0.1', 9100))
server_socket.listen()
server_socket.setblocking(False)
sel.register(server_socket, selectors.EVENT_READ, accept_client)
while True:
events = sel.select()
for key, _ in events:
key.data(key.fileobj)Python socket (сокеты в python (socket))
Функция key.data содержит обработчик, который вызывается для готового файлового объекта. Сервер способен держать открытыми тысячи сокетов без создания потоков.
Каким образом передаются короткие сообщения по UDP без установления соединения?
UDP подходит для быстрых дейтаграмм, когда допустима потеря пакетов. Сервер читает адрес клиента и отправляет ответ по этому же адресу.
import socket
udp_server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp_server.bind(('127.0.0.1', 9500))
print('Сервер ожидает данные на порту 9500')
while True:
message, address = udp_server.recvfrom(1024)
print('Сообщение:', message.decode(), 'от', address)
udp_server.sendto(message.upper(), address)Python network programming (сетевое программирование на python)
Клиент отправляет дейтаграмму и ожидает ответа с таймаутом.
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
client.settimeout(2)
client.sendto(b'hello protocol', ('127.0.0.1', 9500))
try:
response, _ = client.recvfrom(1024)
print('Ответ:', response.decode())
except socket.timeout:
print('Ответ не пришёл')Отправка происходит без логического соединения, поэтому отсутствует фаза подтверждения. Использование таймаута защищает клиент от вечного ожидания при потере пакета.
Сетевые сценарии для повторного использования
Приведённые примеры расширяют базовую работу с сокетами и демонстрируют совмещение разных модулей Python 3.
Как организовать локальный обмен данными через Unix domain socket?
Unix socket привязан к файлу в файловой системе. Это удобно для связи контейнеров и процессов на одном хосте.
import os
import socket
path = '/tmp/py_net_service.sock'
if os.path.exists(path):
os.remove(path)
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
server.bind(path)
server.listen(1)
print('Ожидание клиента')
connection, _ = server.accept()
with connection:
request = connection.recv(1024)
print('Запрос:', request.decode())
connection.sendall(b'ответ локального сервера')
server.close()Клиент подключается к пути без указания port.
import socket
client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
client.connect('/tmp/py_net_service.sock')
client.sendall(b'hello from client')
response = client.recv(1024)
print('Ответ:', response.decode())
client.close()Сервер выводит запрос и отправляет ответ. Дополнительный пример вывода для сервера:
Ожидание клиента Запрос: hello from client
Для клиента результат будет таким.
Ответ: ответ локального сервера
Как проверить порты с таймаутом с помощью asyncio?
Функция open_connection пытается установить TCP-соединение. Обёртка wait_for ограничивает время ожидания.
import asyncio
async def check_port(host, port, timeout=1.5):
try:
reader, writer = await asyncio.wait_for(
asyncio.open_connection(host, port), timeout)
except (asyncio.TimeoutError, OSError):
return False
writer.close()
await writer.wait_closed()
return True
async def scan(host, ports):
tasks = [asyncio.create_task(check_port(host, p)) for p in ports]
results = await asyncio.gather(*tasks)
return [port for port, is_open in zip(ports, results) if is_open]
if __name__ == '__main__':
open_ports = asyncio.run(scan('127.0.0.1', [22, 80, 443, 8000, 8888]))
print('Открытые порты:', open_ports)Когда хост недоступен, возникает OSError, и функция возвращает False. Вывод может выглядеть так.
Открытые порты: [8000, 8888]
Как отправить защищённый HTTP-запрос с проверкой сертификата?
Модуль ssl создаёт контекст с сертификатами доверенных центров. Класс HTTPSConnection применяет этот контекст при установке TLS-сессии.
import http.client
import ssl
context = ssl.create_default_context()
connection = http.client.HTTPSConnection('example.com', timeout=5, context=context)
connection.request('GET', '/')
response = connection.getresponse()
body = response.read(100)
print(response.status, response.reason)
print(body.decode('utf-8', errors='replace').strip())
connection.close()Программа выполнит TLS-руккопожатие и проверит имя хоста в сертификате. Возможный фрагмент вывода.
200 OK
Example Domain Как реализовать UDP-приёмник на основе асинхронного протокола?
DatagramProtocol подходит для неблокирующего UDP в приложениях asyncio.
import asyncio
class UdpEchoProtocol(asyncio.DatagramProtocol):
def connection_made(self, transport):
self.transport = transport
print('UDP транспорт запущен')
def datagram_received(self, data, addr):
message = data.decode()
print(f'Пакет от {addr[0]}:{addr[1]}: {message}')
self.transport.sendto(f'эхо: {message}'.encode(), addr)
def error_received(self, exception):
print('Ошибка сокета:', exception)
async def run_udp():
loop = asyncio.get_running_loop()
transport, _ = await loop.create_datagram_endpoint(
UdpEchoProtocol,
local_addr=('127.0.0.1', 8889)
)
try:
await asyncio.Event().wait()
finally:
transport.close()
if __name__ == '__main__':
try:
asyncio.run(run_udp())
except KeyboardInterrupt:
passСинхронный клиент отправляет дейтаграмму на порт 8889.
import socket
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as client:
client.sendto(b'hello async udp', ('127.0.0.1', 8889))
client.settimeout(2)
try:
response, _ = client.recvfrom(1024)
print(response.decode())
except socket.timeout:
print('Таймаут')
Вывод сервера.
UDP транспорт запущен Пакет от 127.0.0.1:60001: hello async udp
Вывод клиента.
эхо: hello async udp