> **Источник:** https://python-all.ru/3.15/howto/sockets.html
>
> «Документация Python на русском» – неофициальный перевод официальной документации Python: версии от 2.6 до 3.16, полнотекстовый поиск, английский оригинал рядом с переводом. Эта Markdown-версия страницы предназначена для работы с LLM: вставьте её в ChatGPT, Claude или Cursor.

---

# Руководство по программированию сокетов

Аннотация

Сокеты используются практически везде, но при этом являются одной из самых неправильно понимаемых технологий. Это обзор сокетов с высоты птичьего полёта. Это не совсем учебник – вам всё равно придётся приложить усилия, чтобы всё заработало. Он не охватывает тонкости (а их много), но, надеюсь, даст достаточную основу для начала их приличного использования.

## Сокеты

Я расскажу только о сокетах INET (т.е. IPv4), но они составляют как минимум 99% используемых сокетов. И я буду говорить только о потоковых сокетах (т.е. TCP) – если вы не знаете точно, что делаете (в этом случае этот HOWTO не для вас!), то от потокового сокета вы получите лучшие характеристики и производительность, чем от чего-либо другого. Я постараюсь развеять тайну того, что такое сокет, а также дать несколько советов по работе с блокирующими и неблокирующими сокетами. Но начну я с блокирующих сокетов. Вам нужно понять, как они работают, прежде чем переходить к неблокирующим.

Одна из трудностей в понимании этих вещей заключается в том, что слово «сокет» может означать несколько слегка разных вещей в зависимости от контекста. Итак, сначала давайте разграничим «клиентский» сокет – конечную точку беседы, и «серверный» сокет, который больше похож на оператора телефонной станции. Клиентское приложение (например, ваш браузер) использует только «клиентские» сокеты; веб-сервер, с которым оно общается, использует как «серверные», так и «клиентские» сокеты.

### История

Среди различных форм IPC сокеты, безусловно, самые популярные. На любой платформе, вероятно, существуют другие, более быстрые формы IPC, но для межплатформенного взаимодействия сокеты – это практически единственный доступный вариант.

Они были изобретены в Беркли как часть BSD-версии Unix. Они распространились со скоростью лесного пожара вместе с интернетом. И не зря – сочетание сокетов с INET делает связь с любыми машинами по всему миру невероятно простой (по крайней мере, по сравнению с другими подходами).

## Создание сокета

Если говорить приблизительно, то когда вы щёлкнули по ссылке, которая привела вас на эту страницу, ваш браузер сделал примерно следующее:

```python
# создать INET-сокет для потока данных (STREAM)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# теперь подключиться к веб-серверу на порту 80 – обычный порт HTTP
s.connect(("www.python.org", 80))
```

После завершения `connect` сокет `s` можно использовать для отправки запроса на получение текста страницы. Тот же сокет прочитает ответ, а затем будет уничтожен. Да, именно уничтожен. Клиентские сокеты обычно используются только для одного обмена (или небольшого набора последовательных обменов).

То, что происходит на веб-сервере, немного сложнее. Сначала веб-сервер создаёт «серверный» сокет:

```python
# создать INET-сокет для потока данных (STREAM)
serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# привязать сокет к публичному хосту и известному порту
serversocket.bind((socket.gethostname(), 80))
# стать серверным сокетом
serversocket.listen(5)
```

Пара моментов, на которые стоит обратить внимание: мы использовали `socket.gethostname()`, чтобы сокет был виден из внешнего мира. Если бы мы использовали `s.bind(('localhost', 80))` или `s.bind(('127.0.0.1', 80))`, у нас всё равно был бы «серверный» сокет, но доступный только в пределах той же машины. `s.bind(('', 80))` указывает, что сокет доступен по любому адресу, который есть у машины.

Второе, что стоит отметить: порты с низкими номерами обычно зарезервированы для «хорошо известных» служб (HTTP, SNMP и т.д.). Если вы просто экспериментируете, используйте подходящий высокий номер (4 цифры).

Наконец, аргумент `listen` сообщает библиотеке сокетов, что мы хотим, чтобы она ставила в очередь до 5 запросов на подключение (обычный максимум) перед отказом во внешних подключениях. Если остальной код написан правильно, этого должно быть достаточно.

Теперь, когда у нас есть «серверный» сокет, прослушивающий порт 80, мы можем войти в основной цикл веб-сервера:

```python
while True:
    # принимать подключения извне
    (clientsocket, address) = serversocket.accept()
    # теперь выполнить действия с clientsocket
    # в данном случае предполагается, что это многопоточный сервер
    ct = make_client_thread(clientsocket)
    ct.start()
```

На самом деле есть три общих способа организации этого цикла: породить поток для обработки `clientsocket`, создать новый процесс для обработки `clientsocket`, или перестроить приложение для использования неблокирующих сокетов и мультиплексировать между нашим «серверным» сокетом и любыми активными `clientsocket` с помощью `select`. Об этом позже. Важно понять сейчас: *это и есть всё*, что делает «серверный» сокет. Он не отправляет данные. Он не принимает данные. Он просто создаёт «клиентские» сокеты. Каждый `clientsocket` создаётся в ответ на то, что какой-то *другой* «клиентский» сокет выполняет `connect()` к хосту и порту, к которому мы привязаны. Как только мы создали этот `clientsocket`, мы возвращаемся к ожиданию новых подключений. Два «клиента» могут свободно общаться – они используют некоторый динамически выделенный порт, который будет переиспользован после завершения разговора.

### IPC

Если вам нужен быстрый IPC между двумя процессами на одной машине, рассмотрите каналы (pipes) или разделяемую память. Если вы всё же решите использовать сокеты AF\_INET, привяжите «серверный» сокет к `'localhost'`. На большинстве платформ это позволит сократить путь, обойдя несколько уровней сетевого кода, и будет значительно быстрее.

> **См. также**
>
> [`multiprocessing`](https://python-all.ru/3.15/library/multiprocessing.html#module-multiprocessing) интегрирует межплатформенный IPC в API более высокого уровня.

## Использование сокета

Первое, что следует отметить: «клиентский» сокет веб-браузера и «клиентский» сокет веб-сервера – это одинаковые звери. То есть это диалог «равный с равным». Или, другими словами, *как разработчик, вы должны решить, каковы правила этикета для этого диалога*. Обычно сокет `connect` начинает разговор, отправляя запрос или, возможно, приветствие. Но это проектное решение – а не правило работы сокетов.

Теперь есть два набора функций для общения. Вы можете использовать `send` и `recv`, или преобразовать клиентский сокет в файлоподобную сущность и использовать `read` и `write`. Последний способ – это то, как Java представляет свои сокеты. Я не буду здесь об этом говорить, за исключением предупреждения: вам нужно использовать `flush` на сокетах. Это буферизованные «файлы», и распространенная ошибка – `write` что-то, а затем `read` ответа. Без `flush` в этом случае вы можете ждать ответа вечно, потому что запрос все еще может находиться в выходном буфере.

Теперь мы подошли к главному камню преткновения сокетов – `send` и `recv` работают напрямую с сетевыми буферами. Они не обязательно обрабатывают все байты, которые вы им передаете (или ожидаете от них), потому что их основная задача – работа с сетевыми буферами. В общем случае они возвращают управление, когда соответствующие сетевые буферы заполнены (`send`) или опустошены (`recv`). Затем они сообщают, сколько байтов было обработано. Это *ваша* задача – вызывать их снова, пока ваше сообщение не будет полностью обработано.

Когда `recv` возвращает 0 байт, это означает, что другая сторона закрыла (или закрывает) соединение. Вы больше не получите никаких данных по этому соединению. Никогда. Возможно, вы сможете успешно отправлять данные; позже я расскажу об этом подробнее.

Протокол вроде HTTP использует сокет только для одной передачи. Клиент отправляет запрос, затем читает ответ. Вот и всё. Сокет отбрасывается. Это означает, что клиент может определить конец ответа, получив 0 байт.

Но если вы планируете повторно использовать сокет для дальнейших передач, нужно понимать, что *не существует* EOT *на сокете.* Повторяю: если `send` или `recv` возвращает управление после обработки 0 байт, соединение разорвано. Если соединение *не* разорвано, вы можете ждать на `recv` вечно, потому что сокет *не* сообщит вам, что больше нечего читать (пока). Теперь, если немного подумать, можно прийти к фундаментальной истине сокетов: *сообщения должны быть фиксированной длины* (фу), *или быть разделёнными ограничителями* (пожимание плечами), *или указывать свою длину* (гораздо лучше), *или завершаться закрытием соединения*. Выбор полностью за вами (но некоторые способы более правильные, чем другие).

Если вы не хотите завершать соединение, самое простое решение – сообщение фиксированной длины:

```python
class MySocket:
    """только демонстрационный класс
      написан для ясности, а не для эффективности
    """

    def __init__(self, sock=None):
        if sock is None:
            self.sock = socket.socket(
                            socket.AF_INET, socket.SOCK_STREAM)
        else:
            self.sock = sock

    def connect(self, host, port):
        self.sock.connect((host, port))

    def mysend(self, msg):
        totalsent = 0
        while totalsent < MSGLEN:
            sent = self.sock.send(msg[totalsent:])
            if sent == 0:
                raise RuntimeError("socket connection broken")
            totalsent = totalsent + sent

    def myreceive(self):
        chunks = []
        bytes_recd = 0
        while bytes_recd < MSGLEN:
            chunk = self.sock.recv(min(MSGLEN - bytes_recd, 2048))
            if chunk == b'':
                raise RuntimeError("socket connection broken")
            chunks.append(chunk)
            bytes_recd = bytes_recd + len(chunk)
        return b''.join(chunks)
```

Приведённый здесь код отправки подходит почти для любой схемы передачи сообщений – в Python вы отправляете строки и можете использовать `len()` для определения их длины (даже если они содержат встроенные символы `\0`). В основном сложнее становится код получения. (А в C ненамного хуже, за исключением того, что нельзя использовать `strlen`, если сообщение содержит встроенные `\0`.)

Самое простое улучшение – сделать первый символ сообщения индикатором типа сообщения, и пусть тип определяет длину. Теперь у вас есть два `recv`: первый – чтобы получить (как минимум) этот первый символ, чтобы узнать длину, и второй – в цикле для получения остального. Если вы решите пойти по пути разделителей, вы будете получать данные произвольными порциями (4096 или 8192 часто хорошо соответствуют размерам сетевых буферов) и сканировать полученное на предмет разделителя.

Одно усложнение, о котором нужно знать: если ваш протокол общения допускает отправку нескольких сообщений подряд (без какого-либо ответа) и вы передаёте `recv` произвольный размер порции, вы можете прочитать начало следующего сообщения. Вам придётся отложить его в сторону и хранить, пока оно не понадобится.

Префикс сообщения с его длиной (скажем, 5 числовых символов) усложняет задачу, потому что (верите или нет) вы можете не получить все 5 символов за один `recv`. При экспериментах это может сойти с рук, но при высокой сетевой нагрузке ваш код очень быстро сломается, если вы не используете два цикла `recv`: первый для определения длины, второй для получения данных сообщения. Отвратительно. И именно тогда вы обнаружите, что `send` не всегда удаляет всё за один проход. И несмотря на то, что вы это прочитали, вы всё равно на этом обожжётесь!

В целях экономии места, укрепления вашего характера (и сохранения моих конкурентных позиций) эти улучшения оставлены читателю в качестве упражнения. Перейдём к завершению.

### Двоичные данные

Вполне возможно отправлять двоичные данные через сокет. Основная проблема в том, что не все машины используют одинаковые форматы двоичных данных. Например, [сетевой порядок байтов](https://python-all.ru/3.15/howto/sockets.html) – big-endian, со старшим байтом первым, поэтому 16-битное целое со значением `1` будет представлено двумя шестнадцатеричными байтами `00 01`. Однако большинство распространённых процессоров (x86/AMD64, ARM, RISC-V) используют little-endian, с младшим байтом первым – то же самое `1` будет равно `01 00`.

Библиотеки сокетов содержат функции для преобразования 16- и 32-битных целых чисел – `ntohl, htonl, ntohs, htons`, где «n» означает *network* (сетевой), «h» – *host* (хостовый), «s» – *short* (короткий), «l» – *long* (длинный). Если сетевой порядок совпадает с хостовым, эти функции ничего не делают, но если порядок байтов машины обратный, они соответствующим образом переставляют байты.

В наши дни 64-битных машин ASCII-представление двоичных данных часто оказывается меньше, чем двоичное представление. Это потому, что удивительно часто многие целые числа равны 0 или, может быть, 1. Строка `"0"` будет занимать два байта, в то время как полное 64-битное целое – 8. Конечно, это плохо сочетается с сообщениями фиксированной длины. Решения, решения.

## Отключение

Строго говоря, полагается использовать `shutdown` на сокете перед тем, как `close` его. `shutdown` – это уведомление для сокета на другом конце. В зависимости от переданного аргумента оно может означать: «Я больше не буду отправлять, но продолжу слушать» или «Я не слушаю, скатертью дорога!». Однако большинство библиотек сокетов настолько привыкли к тому, что программисты пренебрегают этим правилом этикета, что обычно `close` эквивалентно `shutdown(); close()`. Поэтому в большинстве ситуаций явный вызов `shutdown` не требуется.

Один из способов эффективного использования `shutdown` – в обмене, подобном HTTP. Клиент отправляет запрос, а затем выполняет `shutdown(1)`. Это говорит серверу: «Клиент закончил отправку, но всё ещё может принимать». Сервер может обнаружить «конец файла» по получению 0 байт. Он может считать, что получил полный запрос. Сервер отправляет ответ. Если `send` завершается успешно, значит, клиент действительно всё ещё принимал.

Python идёт ещё дальше в автоматическом завершении и говорит, что когда сокет собирается сборщиком мусора, он автоматически выполнит `close`, если это необходимо. Но полагаться на это – очень плохая привычка. Если ваш сокет просто исчезнет без выполнения `close`, сокет на другом конце может зависнуть на неопределённый срок, думая, что вы просто медлите. *Пожалуйста* `close` свои сокеты, когда закончите.

### Когда сокеты умирают

Пожалуй, самое худшее в использовании блокирующих сокетов – это то, что происходит, когда другая сторона внезапно отключается (без выполнения `close`). Ваш сокет, скорее всего, зависнет. TCP – надёжный протокол, и он будет ждать очень, очень долго, прежде чем отказаться от соединения. Если вы используете потоки, весь поток по сути мёртв. С этим мало что можно сделать. Пока вы не делаете ничего глупого, например, не удерживаете блокировку во время блокирующего чтения, поток не потребляет много ресурсов. *Не* пытайтесь убить поток – частично причина, по которой потоки эффективнее процессов, заключается в том, что они избегают накладных расходов, связанных с автоматическим освобождением ресурсов. Другими словами, если вы всё же убьёте поток, весь ваш процесс, скорее всего, будет испорчен.

## Неблокирующие сокеты

Если вы поняли предыдущее, вы уже знаете большую часть того, что нужно знать о механике использования сокетов. Вы по-прежнему будете использовать те же вызовы, во многом теми же способами. Просто если делать всё правильно, ваше приложение будет почти вывернуто наизнанку.

В Python вы используете `socket.setblocking(False)`, чтобы сделать сокет неблокирующим. В C это сложнее (например, нужно выбирать между BSD-версией `O_NONBLOCK` и практически неотличимой POSIX-версией `O_NDELAY`, которая полностью отличается от `TCP_NODELAY`), но идея та же. Это делается после создания сокета, но перед его использованием. (На самом деле, если вы ненормальный, можете переключаться туда-сюда.)

Главное механическое отличие в том, что `send`, `recv`, `connect` и `accept` могут вернуться, ничего не сделав. У вас (конечно) есть несколько вариантов. Вы можете проверять коды возврата и коды ошибок и в целом свести себя с ума. Если не верите, попробуйте как-нибудь. Ваше приложение разрастётся, станет глючным и будет пожирать процессор. Так что давайте пропустим тупые решения и сделаем правильно.

Используйте `select`.

В C программирование `select` довольно сложно. В Python это раз плюнуть, но оно достаточно близко к версии на C, так что если вы понимаете `select` в Python, у вас будет мало проблем с ним в C:

```python
ready_to_read, ready_to_write, in_error = \
               select.select(
                  potential_readers,
                  potential_writers,
                  potential_errs,
                  timeout)
```

Вы передаёте `select` три списка: первый содержит все сокеты, которые вы, возможно, захотите читать; второй – все сокеты, в которые вы, возможно, захотите писать; и последний (обычно оставляемый пустым) – те, которые нужно проверить на ошибки. Следует отметить, что один сокет может находиться в нескольких списках. Вызов `select` блокирующий, но вы можете задать тайм-аут. Обычно это разумно – установить хороший длинный тайм-аут (скажем, минуту), если нет веской причины поступать иначе.

В ответ вы получите три списка. Они содержат сокеты, которые действительно доступны для чтения, записи или находятся в состоянии ошибки. Каждый из этих списков является подмножеством (возможно, пустым) соответствующего списка, который вы передали.

Если сокет находится в списке readable на выходе, вы можете быть настолько уверены, насколько это вообще возможно в нашем деле, что `recv` на этом сокете вернёт *что-то*. Та же идея для списка writable. Вы сможете отправить *что-то*. Возможно, не всё, что хотели, но *что-то* лучше, чем ничего. (На самом деле, любой достаточно здоровый сокет будет возвращаться как writable – это просто означает, что доступно место в исходящем сетевом буфере.)

Если у вас есть «серверный» сокет, поместите его в список potential\_readers. Если он оказывается в списке readable, ваш `accept` (почти наверняка) сработает. Если вы создали новый сокет для `connect` к кому-то другому, поместите его в список potential\_writers. Если он появляется в списке writable, есть хороший шанс, что соединение установлено.

На самом деле, `select` может быть полезен даже с блокирующими сокетами. Это один из способов определить, произойдёт ли блокировка: сокет становится readable, когда в буферах что-то есть. Однако это всё равно не решает проблему определения, закончил ли другой конец работу или просто занят чем-то ещё.

**Предупреждение о переносимости**: В Unix `select` работает как с сокетами, так и с файлами. Не пытайтесь делать это в Windows. В Windows `select` работает только с сокетами. Также обратите внимание, что в C многие более продвинутые опции сокетов в Windows реализованы иначе. На самом деле, в Windows я обычно использую потоки (которые работают очень и очень хорошо) со своими сокетами.
