Работа с сетью в Python: от сокетов до asyncio
Основы работы с сокетами в Python
Как создать базовый TCP сервер?
Эффективное решение с использованием модуля socket
Создание простого TCP сервера, который принимает одно соединение и возвращает ответ. Этот подход подходит для обучения и простых приложений, где не требуется высокая производительность.
import socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind(('localhost', 8888))
server_socket.listen(1)
print('Сервер запущен на порту 8888')
client_socket, addr = server_socket.accept()
print(f'Подключен клиент {addr}')
data = client_socket.recv(1024)
print(f'Получено: {data.decode()}')
client_socket.sendall(b'Hello, client!')
client_socket.close()
server_socket.close()
Python client py (клиент на python)
Пояснение: создается сокет AF_INET (IPv4) и SOCK_STREAM (TCP). Метод bind назначает адрес и порт. listen указывает размер очереди. accept блокируется до подключения. recv получает данные, sendall отправляет.
Вариант с менеджером контекста
Использование with гарантирует закрытие сокета даже при ошибках.
import socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(('localhost', 8888))
s.listen()
conn, addr = s.accept()
with conn:
print(f'Подключен {addr}')
data = conn.recv(1024)
conn.sendall(b'OK')
Python socket (сокеты в python (socket))
Типичные проблемы
- Ошибка
Address already in use– порт занят. Решение: использоватьs.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)перед bind. - Блокирующий accept останавливает выполнение программы. Для обработки нескольких клиентов требуется многопоточность или асинхронность.
Как написать TCP клиент?
Базовый клиент для передачи данных
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('localhost', 8888))
client.sendall(b'Привет, сервер!')
response = client.recv(1024)
print(f'Ответ сервера: {response.decode()}')
client.close()
Python ipaddress ip network (модуль ipaddress в python)
После connect отправляются данные, затем ожидается ответ. recv может получить неполные данные, поэтому в реальных приложениях используют протоколы с фиксированной длиной или разделителями.
Клиент с таймаутом и повторными попытками
import socket
import time
s = socket.socket()
s.settimeout(5)
try:
s.connect(('localhost', 8888))
s.sendall(b'ping')
data = s.recv(1024)
except (socket.timeout, ConnectionRefusedError):
print('Не удалось соединиться, повтор через 2 секунды')
time.sleep(2)
Python сети (сетевые возможности python)
Частая ошибка: неполный прием данных
recv(1024) может вернуть только часть сообщения. Рекомендуется организовывать цикл приема с проверкой длины или использовать буферизацию. Например, отправлять перед данными длину сообщения.
Как обрабатывать несколько клиентов одновременно?
Многопоточный сервер на threading
Каждое подключение обрабатывается в отдельном потоке. Это простое решение для небольшого количества соединений.
import socket
import threading
def handle_client(conn, addr):
print(f'Новый поток для {addr}')
with conn:
while True:
data = conn.recv(1024)
if not data:
break
conn.sendall(data.upper())
print(f'Соединение с {addr} закрыто')
server = socket.socket()
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(('', 9090))
server.listen(5)
while True:
conn, addr = server.accept()
thread = threading.Thread(target=handle_client, args=(conn, addr))
thread.start()
Python network programming (сетевое программирование на python)
Многопроцессорный вариант с multiprocessing
from multiprocessing import Process
import socket
def worker(conn):
with conn:
data = conn.recv(1024)
conn.sendall(b'processed')
if __name__ == '__main__':
s = socket.socket()
s.bind(('', 9091))
s.listen(3)
while True:
conn, _ = s.accept()
p = Process(target=worker, args=(conn,))
p.start()
conn.close() # родительский процесс не использует conn
Ip network python (работа с ip-сетями в python)
Цель
Использование процессов позволяет обойти GIL и задействовать несколько ядер CPU для тяжелых вычислений. Однако создание процесса дороже потока, и требуется осторожность с общими ресурсами.
Проблемы многопоточности
GIL в CPython ограничивает параллельное выполнение Python-кода в потоках. Для операций ввода-вывода (как в нашем случае) это не критично, но для смешанных нагрузок лучше использовать asyncio или multiprocessing. Также нужно синхронизировать доступ к общим данным (например, через threading.Lock).
Как использовать асинхронность в сетевом программировании?
Асинхронный сервер на asyncio
Модуль asyncio позволяет обрабатывать тысячи соединений в одном потоке без блокировок.
import asyncio
async def handle_client(reader, writer):
addr = writer.get_extra_info('peername')
print(f'Подключен {addr}')
while True:
data = await reader.read(1024)
if not data:
break
writer.write(data.upper())
await writer.drain()
writer.close()
async def main():
server = await asyncio.start_server(handle_client, '127.0.0.1', 8889)
async with server:
await server.serve_forever()
asyncio.run(main())
Функция start_server принимает корутину handle_client для каждого подключения. await приостанавливает выполнение до готовности данных, не блокируя другие соединения.
Комбинирование с asyncio.wait для нескольких клиентов
import asyncio
async def client_async():
reader, writer = await asyncio.open_connection('127.0.0.1', 8889)
writer.write(b'hello')
await writer.drain()
data = await reader.read(100)
print(f'Получено: {data.decode()}')
writer.close()
async def main():
tasks = [client_async() for _ in range(5)]
await asyncio.wait(tasks)
asyncio.run(main())
Сложность отладки и платформозависимость
Асинхронный код сложнее отлаживать из-за переключения контекста. На Windows некоторые методы asyncio могут требовать установки ProactorEventLoop. Рекомендуется использовать новые версии Python (3.8+) для стабильной работы.
Как работать с UDP?
UDP сервер и клиент
UDP не требует установки соединения, подходит для потоковых данных, где допустима потеря пакетов.
import socket
# Сервер
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(('localhost', 9999))
data, addr = s.recvfrom(1024)
print(f'От {addr}: {data.decode()}')
s.sendto(b'UDP echo', addr)
# Клиент
c = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
c.sendto(b'hello', ('localhost', 9999))
data, _ = c.recvfrom(1024)
print(data.decode())
Широковещательная рассылка
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
s.sendto(b'discover', ('255.255.255.255', 12345))
Потеря пакетов и отсутствие порядка доставки
UDP не гарантирует доставку. Для повышения надежности можно реализовать собственные подтверждения (ACK) и повторные отправки, либо использовать библиотеку Twisted или QUIC.
Как использовать неблокирующие сокеты с select?
Мультиплексирование с помощью select
Позволяет одному потоку обслуживать несколько сокетов, проверяя их готовность к чтению/записи.
import socket
import select
server = socket.socket()
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(('', 9000))
server.listen(5)
server.setblocking(False)
inputs = [server]
while True:
readable, _, exceptional = select.select(inputs, [], inputs)
for s in readable:
if s is server:
conn, addr = s.accept()
conn.setblocking(False)
inputs.append(conn)
else:
data = s.recv(1024)
if not data:
s.close()
inputs.remove(s)
else:
s.sendall(b'OK')
Использование epoll в Linux
import socket
import select
server = socket.socket()
server.bind(('', 9001))
server.listen(5)
server.setblocking(False)
epoll = select.epoll()
epoll.register(server.fileno(), select.EPOLLIN)
try:
connections = {}
while True:
events = epoll.poll(1)
for fd, event in events:
if fd == server.fileno():
conn, _ = server.accept()
conn.setblocking(False)
epoll.register(conn.fileno(), select.EPOLLIN)
connections[conn.fileno()] = conn
elif event & select.EPOLLIN:
data = connections[fd].recv(1024)
if not data:
epoll.unregister(fd)
connections[fd].close()
del connections[fd]
finally:
epoll.close()
Цель
epoll масштабируется лучше select при тысячах соединений, но работает только в Linux. Для кроссплатформенности можно использовать библиотеки типа asyncore (устарела) или curio.
Платформозависимость и сложность
select поддерживается везде, но имеет лимит на количество дескрипторов (обычно 1024). epoll и kqueue (FreeBSD) не доступны в Windows. Рекомендуется использовать asyncio, который абстрагирует эти детали.
Расширенный пример: асинхронный чат сервер на asyncio
Реализация многопользовательского чата с поддержкой комнат
Данный сервер позволяет клиентам подключаться, отправлять сообщения всем участникам в комнате. Используется asyncio для управления множеством соединений.
import asyncio
class ChatServer:
def __init__(self):
self.rooms = {} # room_name -> set of writers
async def handler(self, reader, writer):
addr = writer.get_extra_info('peername')
print(f'Подключен {addr}')
# Запрос ника и комнаты
writer.write(b'Введите ваш ник: ')
await writer.drain()
nickname = (await reader.readline()).decode().strip()
writer.write(b'Введите имя комнаты: ')
await writer.drain()
room = (await reader.readline()).decode().strip()
if room not in self.rooms:
self.rooms[room] = set()
self.rooms[room].add(writer)
welcome = f'{nickname} присоединился к комнате {room}\n'
for w in self.rooms[room]:
if w is not writer:
w.write(welcome.encode())
await w.drain()
try:
while True:
data = await reader.readline()
if not data:
break
msg = f'{nickname}: {data.decode().strip()}'
for w in self.rooms[room]:
if w is not writer:
w.write(f'{msg}\n'.encode())
await w.drain()
finally:
self.rooms[room].discard(writer)
leave_msg = f'{nickname} покинул комнату\n'
for w in self.rooms[room]:
w.write(leave_msg.encode())
await w.drain()
if not self.rooms[room]:
del self.rooms[room]
writer.close()
async def main():
server = await asyncio.start_server(ChatServer().handler, '0.0.0.0', 7777)
print('Чат сервер запущен на порту 7777')
async with server:
await server.serve_forever()
if __name__ == '__main__':
asyncio.run(main())
Результат работы (вывод сервера в консоль)
Чат сервер запущен на порту 7777
Подключен ('127.0.0.1', 54321)
Подключен ('127.0.0.1', 54322)
Два клиента подключаются, вводят ники и комнату. Сообщения одного клиента отображаются у другого. Сервер корректно обрабатывает отключение.
Пример реализации простого HTTP сервера на сокетах
HTTP сервер, возвращающий статические страницы
import socket
import os
HTTP_RESPONSE = """HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: {length}
{body}"""
def handle_request(client):
request = client.recv(1024).decode()
path = request.split(' ')[1] if ' ' in request else '/'
if path == '/':
body = 'Hello World
'
elif path == '/about':
body = 'About page
'
else:
body = '404 Not Found
'
client.sendall(b'HTTP/1.1 404 Not Found\r\n\r\n' + body.encode())
client.close()
return
response = HTTP_RESPONSE.format(length=len(body), body=body)
client.sendall(response.encode())
client.close()
server = socket.socket()
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(('', 8080))
server.listen(5)
print('HTTP сервер на порту 8080')
while True:
client, _ = server.accept()
handle_request(client)
Проверка через браузер или curl
$ curl http://localhost:8080/aboutAbout page
$ curl http://localhost:8080/unknown404 Not Found
Сервер принимает один запрос за раз (блокирующий). Для реального использования потребуется многопоточность или asyncio.