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

---

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

| Автор: | Gordon McMillan |
| --- | --- |

Аннотация

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

## Сокеты

Я расскажу только о сокетах 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 = client_thread(clientsocket)
    ct.run()
```

There’s actually 3 general ways in which this loop could work - dispatching a thread to handle `clientsocket`, create a new process to handle `clientsocket`, or restructure this app to use non-blocking sockets, and mulitplex between our “server” socket and any active `clientsocket`s using `select`. More about that later. The important thing to understand now is this: this is *all* a “server” socket does. It doesn’t send any data. It doesn’t receive any data. It just produces “client” sockets. Each `clientsocket` is created in response to some *other* “client” socket doing a `connect()` to the host and port we’re bound to. As soon as we’ve created that `clientsocket`, we go back to listening for more connections. The two “clients” are free to chat it up - they are using some dynamically allocated port which will be recycled when the conversation ends.

### Межпроцессное взаимодействие

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

> **См. также**
>
> Модуль [`multiprocessing`](https://python-all.ru/3.3/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 байт.

But if you plan to reuse your socket for further transfers, you need to realize that *there is no* EOT *on a socket.* I repeat: if a socket `send` or `recv` returns after handling 0 bytes, the connection has been broken. If the connection has *not* been broken, you may wait on a `recv` forever, because the socket will *not* tell you that there’s nothing more to read (for now). Now if you think about that a bit, you’ll come to realize a fundamental truth of sockets: *messages must either be fixed length* (yuck), *or be delimited* (shrug), *or indicate how long they are* (much better), *or end by shutting down the connection*. The choice is entirely yours, (but some ways are righter than others).

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

```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):
        msg = b''
        while len(msg) < MSGLEN:
            chunk = self.sock.recv(MSGLEN-len(msg))
            if chunk == b'':
                raise RuntimeError("socket connection broken")
            msg = msg + chunk
        return msg
```

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

В Python используется `socket.setblocking(0)`, чтобы сделать сокет неблокирующим. В 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 я обычно использую потоки (которые работают очень, очень хорошо) со своими сокетами.
