Работа с сетью в Python: от сокетов до asyncio

Раздел: Разработка на Python -> Сетевое программирование

Основы работы с сокетами в 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/about

About page

$ curl http://localhost:8080/unknown

404 Not Found

Сервер принимает один запрос за раз (блокирующий). Для реального использования потребуется многопоточность или asyncio.

Сетевое программирование на Python - comments

En
Python network programming (python)