Клиент-серверная модель в Python и сокеты: рабочий эхо-сервер и клиент

Клиент-серверная модель в Python и сокеты: рабочий эхо-сервер и клиент Полезное

Клиент-серверная модель — это схема сетевого взаимодействия, в которой одна программа (сервер) ждет входящих подключений и отвечает на запросы, а другая (клиент) сама инициирует соединение и эти запросы шлет. В Python обе роли пишутся через встроенный модуль socket — отдельные пакеты для этого не нужны. Ниже разберем, чем сокет отличается от соединения, когда брать TCP, а когда UDP, и соберем рабочий эхо-сервер с клиентом, который прогоним на loopback (127.0.0.1) и увидим реальный вывод консоли.

Код проверен на CPython 3.12 под macOS 20 сентября 2026 года. Стандартный модуль socket одинаков на Linux, macOS и Windows, различия оговорены отдельно.

Как устроено взаимодействие клиент-сервер

Роли в этой модели закреплены не за железом, а за поведением программы. Сервер стартует первым, «садится» на известный адрес и порт и переходит в режим ожидания. Клиент знает этот адрес заранее и подключается к нему.

Важно развести три близких понятия, которые часто путают:

  • сокет — объект-конечная точка канала связи (создается вызовом socket.socket());
  • слушающий сокет — сокет сервера, который принимает новые подключения после listen();
  • сокет соединения — отдельный сокет, который accept() возвращает под каждого подключившегося клиента; именно через него идет обмен данными.

То есть у работающего TCP-сервера сокетов минимум два: один слушает новые подключения, второй (или несколько) обслуживает уже установленные. Это разделение и позволяет серверу принимать новых клиентов, не разрывая текущие.

Обмен подчиняется протоколу — своду правил о том, как адресовать, упаковывать и доставлять данные. На транспортном уровне в сетевых программах на Python чаще всего используют два протокола: TCP и UDP.

TCP и UDP: чем отличаются

TCP устанавливает логическое соединение и гарантирует, что байты дойдут в том же порядке, в каком отправлены, а потерянные сегменты будут переотправлены. Цена — накладные расходы на подтверждения и установку соединения.

UDP соединение не устанавливает и доставку не подтверждает: датаграмма может потеряться, задублироваться или прийти не по порядку, и протокол об этом не сообщит. Зато он проще и без задержек на подтверждения, что важно для голоса, видео и игр, где устаревший пакет уже не нужен.

Свойство TCP UDP
Соединение устанавливается (рукопожатие) не устанавливается
Доставка и порядок гарантируются, переотправка потерь без гарантий
Модель данных поток байтов отдельные датаграммы
Накладные расходы выше ниже
Тип сокета в Python SOCK_STREAM SOCK_DGRAM
Типичное применение HTTP, почта, чаты, БД DNS, стриминг, онлайн-игры

Дальше собираем TCP-пример: для чата и обмена сообщениями важна надежная доставка в правильном порядке.

Модуль socket: создаем сокет

Сокет создается одним вызовом. Для TCP через IPv4 это выглядит так:

import socket

# AF_INET - адресация IPv4, SOCK_STREAM - протокол TCP
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

Основные аргументы:

  • family — семейство адресов: AF_INET (IPv4) или AF_INET6 (IPv6);
  • type — тип сокета: SOCK_STREAM (TCP) или SOCK_DGRAM (UDP);
  • proto — обычно не задают, по умолчанию 0 (протокол выбирается по типу).

Адрес для IPv4 задается парой (host, port). Порты 0-1023 — системные (HTTP, SSH и прочие), для своих экспериментов берут порт выше 1023, например 5050.

Методы делятся на серверные и клиентские. У сервера: bind() привязывает сокет к адресу и порту, listen() включает прием подключений, accept() блокируется в ожидании клиента и возвращает пару (сокет_соединения, адрес). У клиента: connect() подключается к серверу. Общие методы обмена: sendall() шлет байты, recv(n) читает до n байт, close() закрывает сокет.

Рабочий пример: эхо-сервер

Эхо-сервер принимает от клиента данные и возвращает их обратно без изменений — это минимальная, но полная клиент-серверная программа. Сохраните код в файл server.py:

import socket

HOST = "127.0.0.1"  # loopback: сервер доступен только на этой машине
PORT = 5050         # порт выше 1023

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
    # SO_REUSEADDR - чтобы повторный запуск не падал с "address already in use"
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server.bind((HOST, PORT))
    server.listen(1)
    print(f"Сервер слушает {HOST}:{PORT}")

    conn, addr = server.accept()   # блокируется, пока не подключится клиент
    with conn:
        print(f"Подключился клиент: {addr}")
        while True:
            data = conn.recv(1024)     # читаем до 1024 байт
            if not data:               # пустые байты = клиент закрыл соединение
                print("Клиент отключился")
                break
            text = data.decode("utf-8")
            print(f"Получено: {text}")
            conn.sendall(data)         # отправляем те же байты обратно

Здесь server — слушающий сокет, а conn — сокет соединения с конкретным клиентом. Привязка к 127.0.0.1 намеренная: сервер виден только на локальной машине. Чтобы принимать подключения из сети, адрес меняют на 0.0.0.0, но тогда порт становится доступен извне — открывать его стоит осознанно и за фаерволом.

Клиент

Клиент подключается к тому же адресу и порту, шлет строку и печатает ответ. Файл client.py:

import socket

HOST = "127.0.0.1"
PORT = 5050

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client:
    client.connect((HOST, PORT))
    message = "привет, сервер"
    client.sendall(message.encode("utf-8"))   # строку кодируем в байты
    reply = client.recv(1024)
    print(f"Ответ сервера: {reply.decode('utf-8')}")
# выход из with закрывает сокет -> сервер получит пустой recv

Сокеты работают с байтами, а не со строками, поэтому текст кодируем через encode("utf-8") перед отправкой и декодируем через decode("utf-8") после приема.

Прогон и вывод

Запускаем в двух терминалах: сначала сервер, потом в отдельном окне клиент.

# терминал 1
python3 server.py
# терминал 2 (после старта сервера)
python3 client.py

Вывод сервера в первом терминале (адрес и порт клиента подставляются случайные):

Сервер слушает 127.0.0.1:5050
Подключился клиент: ('127.0.0.1', 63935)
Получено: привет, сервер
Клиент отключился

Вывод клиента во втором терминале:

Ответ сервера: привет, сервер

Сервер получил строку, вернул ее клиенту и, когда клиент вышел из блока with и закрыл сокет, увидел пустой recv — это признак разрыва соединения, по нему цикл и завершается.

Обработка нескольких клиентов

Пример выше обслуживает одного клиента и завершается. Есть важная тонкость: TCP — это поток байтов, а не сообщения. Один recv(1024) может вернуть меньше данных, чем отправил sendall(), или склеить два отправления в одно. Для настоящего протокола поверх TCP нужен разделитель сообщений или их длина в заголовке — на это опираются реальные чаты и HTTP.

Чтобы принимать много клиентов, accept() заворачивают в цикл, а каждое соединение обрабатывают параллельно — через потоки (threading), процессы или асинхронно (asyncio). Простейший вариант — на каждое принятое соединение запускать отдельный поток, который крутит цикл recv/sendall независимо от других.

Выводы

  • Клиент-серверная модель в Python строится на встроенном модуле socket: сервер делает bind -> listen -> accept, клиент — connect.
  • Различайте слушающий сокет (принимает подключения) и сокет соединения, который возвращает accept под каждого клиента.
  • TCP дает надежную упорядоченную доставку (SOCK_STREAM), UDP — быструю без гарантий (SOCK_DGRAM); для чата берут TCP.
  • Сокеты обмениваются байтами, поэтому строки кодируют и декодируют через encode/decode; TCP — поток, границы сообщений задает ваш протокол.
  • Пустой результат recv означает, что вторая сторона закрыла соединение.

Где применяется / связь с практикой

Сокеты — фундамент почти всего сетевого кода: веб-серверы, мессенджеры, игровые бэкенды, обмен между микросервисами и IoT-устройствами в основе используют те же вызовы socket. Понимание клиент-серверной модели помогает читать логи, отлаживать таймауты и осознанно выбирать между TCP и UDP.

Освойте тему на практике

Разложить это по полочкам и дойти до практики удобно на системном курсе. Основы Python, работу с модулями стандартной библиотеки и сетевое взаимодействие разбирают на курсе Python Basic. Посмотреть формат и темы вживую можно на открытых уроках Otus — они бесплатные и проходят регулярно.

FAQ

Почему нужно кодировать строку в байты перед отправкой?
Методы send/sendall и recv работают только с байтами, а не со строками. Поэтому текст переводят в байты через encode("utf-8") при отправке и обратно через decode("utf-8") при приеме.

Что означает ошибка «Address already in use» при запуске сервера?
Порт еще занят предыдущим запуском, который не успел освободить его в ОС. Помогает опция setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) до bind, а также выбор другого свободного порта выше 1023.

Можно ли на одном компьютере запускать и клиент, и сервер?
Да, для этого и нужен адрес loopback 127.0.0.1: сервер и клиент общаются в пределах одной машины без реальной сети. Так удобно проверять код перед выкладыванием в сеть.

OTUS Журнал
Скидка 5% 12-19 октября на курсы (popup)
Бесплатные открытые уроки (поп-ап)