Клиент-серверная модель — это схема сетевого взаимодействия, в которой одна программа (сервер) ждет входящих подключений и отвечает на запросы, а другая (клиент) сама инициирует соединение и эти запросы шлет. В 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: сервер и клиент общаются в пределах одной машины без реальной сети. Так удобно проверять код перед выкладыванием в сеть.



