sockets.md
1> **Источник:** https://python-all.ru/3.7/howto/sockets.html2>3> «Документация Python на русском» – неофициальный перевод официальной документации Python: версии от 2.6 до 3.16, полнотекстовый поиск, английский оригинал рядом с переводом. Эта Markdown-версия страницы предназначена для работы с LLM: вставьте её в ChatGPT, Claude или Cursor.45---67# Руководство по программированию сокетов89**Автор**1011Gordon McMillan1213Аннотация1415Сокеты используются практически везде, но при этом являются одной из самых неправильно понимаемых технологий. Это обзор сокетов с высоты птичьего полёта. Это не совсем учебник – вам всё равно придётся приложить усилия, чтобы всё заработало. Он не охватывает тонкости (а их много), но, надеюсь, даст достаточную основу для начала их приличного использования.1617## Сокеты1819Я расскажу только о сокетах INET (т.е. IPv4), но они составляют как минимум 99% используемых сокетов. И я буду говорить только о потоковых сокетах (т.е. TCP) – если вы не знаете точно, что делаете (в этом случае этот HOWTO не для вас!), то от потокового сокета вы получите лучшие характеристики и производительность, чем от чего-либо другого. Я постараюсь развеять тайну того, что такое сокет, а также дать несколько советов по работе с блокирующими и неблокирующими сокетами. Но начну я с блокирующих сокетов. Вам нужно понять, как они работают, прежде чем переходить к неблокирующим.2021Одна из трудностей в понимании этих вещей заключается в том, что слово «сокет» может означать несколько слегка разных вещей в зависимости от контекста. Итак, сначала давайте разграничим «клиентский» сокет – конечную точку беседы, и «серверный» сокет, который больше похож на оператора телефонной станции. Клиентское приложение (например, ваш браузер) использует только «клиентские» сокеты; веб-сервер, с которым оно общается, использует как «серверные», так и «клиентские» сокеты.2223### История2425Среди различных форм IPC сокеты, безусловно, самые популярные. На любой платформе, вероятно, существуют другие, более быстрые формы IPC, но для межплатформенного взаимодействия сокеты – это практически единственный доступный вариант.2627Они были изобретены в Беркли как часть BSD-версии Unix. С распространением Интернета они распространились как лесной пожар. И не зря – сочетание сокетов с INET позволяет невероятно легко общаться с произвольными машинами по всему миру (по крайней мере, по сравнению с другими подходами).2829## Создание сокета3031Если говорить приблизительно, то когда вы щёлкнули по ссылке, которая привела вас на эту страницу, ваш браузер сделал примерно следующее:3233```python34# создать INET-сокет для потока данных (STREAM)35s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)36# теперь подключиться к веб-серверу на порту 80 – обычный порт HTTP37s.connect(("www.python.org", 80))38```3940После завершения `connect` сокет `s` можно использовать для отправки запроса на получение текста страницы. Тот же сокет прочитает ответ, а затем будет уничтожен. Да, именно уничтожен. Клиентские сокеты обычно используются только для одного обмена (или небольшого набора последовательных обменов).4142То, что происходит на веб-сервере, немного сложнее. Сначала веб-сервер создаёт «серверный» сокет:4344```python45# создать INET-сокет для потока данных (STREAM)46serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)47# привязать сокет к публичному хосту и известному порту48serversocket.bind((socket.gethostname(), 80))49# стать серверным сокетом50serversocket.listen(5)51```5253Пара моментов, на которые стоит обратить внимание: мы использовали `socket.gethostname()`, чтобы сокет был виден из внешнего мира. Если бы мы использовали `s.bind(('localhost', 80))` или `s.bind(('127.0.0.1', 80))`, у нас всё равно был бы «серверный» сокет, но доступный только в пределах той же машины. `s.bind(('', 80))` указывает, что сокет доступен по любому адресу, который есть у машины.5455Второе, что стоит отметить: порты с низкими номерами обычно зарезервированы для «хорошо известных» служб (HTTP, SNMP и т.д.). Если вы просто экспериментируете, используйте подходящий высокий номер (4 цифры).5657Наконец, аргумент `listen` сообщает библиотеке сокетов, что мы хотим, чтобы она ставила в очередь до 5 запросов на подключение (обычный максимум) перед отказом во внешних подключениях. Если остальной код написан правильно, этого должно быть достаточно.5859Теперь, когда у нас есть «серверный» сокет, прослушивающий порт 80, мы можем войти в основной цикл веб-сервера:6061```python62while True:63 # принимать подключения извне64 (clientsocket, address) = serversocket.accept()65 # теперь выполнить действия с clientsocket66 # в данном случае предполагается, что это многопоточный сервер67 ct = client_thread(clientsocket)68 ct.run()69```7071На самом деле есть три общих способа организации этого цикла: породить поток для обработки `clientsocket`, создать новый процесс для обработки `clientsocket`, или перестроить приложение для использования неблокирующих сокетов и мультиплексировать между нашим «серверным» сокетом и любыми активными `clientsocket` с помощью `select`. Об этом позже. Важно понять сейчас: *это и есть всё*, что делает «серверный» сокет. Он не отправляет данные. Он не принимает данные. Он просто создаёт «клиентские» сокеты. Каждый `clientsocket` создаётся в ответ на то, что какой-то *другой* «клиентский» сокет выполняет `connect()` к хосту и порту, к которому мы привязаны. Как только мы создали этот `clientsocket`, мы возвращаемся к ожиданию новых подключений. Два «клиента» могут свободно общаться – они используют некоторый динамически выделенный порт, который будет переиспользован после завершения разговора.7273### Межпроцессное взаимодействие7475Если вам нужен быстрый IPC между двумя процессами на одной машине, рассмотрите каналы (pipes) или разделяемую память. Если вы всё же решите использовать сокеты AF\_INET, привяжите «серверный» сокет к `'localhost'`. На большинстве платформ это позволит сократить путь, обойдя несколько уровней сетевого кода, и будет значительно быстрее.7677> **См. также**78>79> [`multiprocessing`](https://python-all.ru/3.7/library/multiprocessing.html#module-multiprocessing) интегрирует межплатформенный IPC в API более высокого уровня.8081## Использование сокета8283Первое, что следует отметить: «клиентский» сокет веб-браузера и «клиентский» сокет веб-сервера – это одинаковые звери. То есть это диалог «равный с равным». Или, другими словами, *как разработчик, вы должны решить, каковы правила этикета для этого диалога*. Обычно сокет `connect` начинает разговор, отправляя запрос или, возможно, приветствие. Но это проектное решение – а не правило работы сокетов.8485Теперь есть два набора функций для общения. Вы можете использовать `send` и `recv`, или преобразовать клиентский сокет в файлоподобную сущность и использовать `read` и `write`. Последний способ – это то, как Java представляет свои сокеты. Я не буду здесь об этом говорить, за исключением предупреждения: вам нужно использовать `flush` на сокетах. Это буферизованные «файлы», и распространенная ошибка – `write` что-то, а затем `read` ответа. Без `flush` в этом случае вы можете ждать ответа вечно, потому что запрос все еще может находиться в выходном буфере.8687Теперь мы подошли к главному камню преткновения сокетов – `send` и `recv` работают напрямую с сетевыми буферами. Они не обязательно обрабатывают все байты, которые вы им передаете (или ожидаете от них), потому что их основная задача – работа с сетевыми буферами. В общем случае они возвращают управление, когда соответствующие сетевые буферы заполнены (`send`) или опустошены (`recv`). Затем они сообщают, сколько байтов было обработано. Это *ваша* задача – вызывать их снова, пока ваше сообщение не будет полностью обработано.8889Когда `recv` возвращает 0 байт, это означает, что другая сторона закрыла (или закрывает) соединение. Вы больше не получите никаких данных по этому соединению. Никогда. Возможно, вы сможете успешно отправлять данные; позже я расскажу об этом подробнее.9091Протокол вроде HTTP использует сокет только для одной передачи. Клиент отправляет запрос, затем читает ответ. Вот и всё. Сокет отбрасывается. Это означает, что клиент может определить конец ответа, получив 0 байт.9293Но если вы планируете повторно использовать сокет для дальнейших передач, нужно понимать, что *не существует* EOT *на сокете.* Повторяю: если `send` или `recv` возвращает управление после обработки 0 байт, соединение разорвано. Если соединение *не* разорвано, вы можете ждать на `recv` вечно, потому что сокет *не* сообщит вам, что больше нечего читать (пока). Теперь, если немного подумать, можно прийти к фундаментальной истине сокетов: *сообщения должны быть фиксированной длины* (фу), *или быть разделёнными ограничителями* (пожимание плечами), *или указывать свою длину* (гораздо лучше), *или завершаться закрытием соединения*. Выбор полностью за вами (но некоторые способы более правильные, чем другие).9495Если вы не хотите завершать соединение, самое простое решение – сообщение фиксированной длины:9697```python98class MySocket:99 """только демонстрационный класс100 написан для ясности, а не для эффективности101 """102103 def __init__(self, sock=None):104 if sock is None:105 self.sock = socket.socket(106 socket.AF_INET, socket.SOCK_STREAM)107 else:108 self.sock = sock109110 def connect(self, host, port):111 self.sock.connect((host, port))112113 def mysend(self, msg):114 totalsent = 0115 while totalsent < MSGLEN:116 sent = self.sock.send(msg[totalsent:])117 if sent == 0:118 raise RuntimeError("socket connection broken")119 totalsent = totalsent + sent120121 def myreceive(self):122 chunks = []123 bytes_recd = 0124 while bytes_recd < MSGLEN:125 chunk = self.sock.recv(min(MSGLEN - bytes_recd, 2048))126 if chunk == b'':127 raise RuntimeError("socket connection broken")128 chunks.append(chunk)129 bytes_recd = bytes_recd + len(chunk)130 return b''.join(chunks)131```132133Приведённый здесь код отправки подходит почти для любой схемы передачи сообщений – в Python вы отправляете строки и можете использовать `len()` для определения их длины (даже если они содержат встроенные символы `\0`). В основном сложнее становится код получения. (А в C ненамного хуже, за исключением того, что нельзя использовать `strlen`, если сообщение содержит встроенные `\0`.)134135Самое простое улучшение – сделать первый символ сообщения индикатором типа сообщения, и пусть тип определяет длину. Теперь у вас есть два `recv`: первый – чтобы получить (как минимум) этот первый символ, чтобы узнать длину, и второй – в цикле для получения остального. Если вы решите пойти по пути разделителей, вы будете получать данные произвольными порциями (4096 или 8192 часто хорошо соответствуют размерам сетевых буферов) и сканировать полученное на предмет разделителя.136137Одно усложнение, о котором нужно знать: если ваш протокол общения допускает отправку нескольких сообщений подряд (без какого-либо ответа) и вы передаёте `recv` произвольный размер порции, вы можете прочитать начало следующего сообщения. Вам придётся отложить его в сторону и хранить, пока оно не понадобится.138139Префикс сообщения с его длиной (скажем, 5 числовых символов) усложняет задачу, потому что (верите или нет) вы можете не получить все 5 символов за один `recv`. При экспериментах это может сойти с рук, но при высокой сетевой нагрузке ваш код очень быстро сломается, если вы не используете два цикла `recv`: первый для определения длины, второй для получения данных сообщения. Отвратительно. И именно тогда вы обнаружите, что `send` не всегда удаляет всё за один проход. И несмотря на то, что вы это прочитали, вы всё равно на этом обожжётесь!140141В целях экономии места, укрепления вашего характера (и сохранения моих конкурентных позиций) эти улучшения оставлены читателю в качестве упражнения. Перейдём к завершению.142143### Двоичные данные144145It is perfectly possible to send binary data over a socket. The major problem is that not all machines use the same formats for binary data. For example, a Motorola chip will represent a 16 bit integer with the value 1 as the two hex bytes 00 01. Intel and DEC, however, are byte-reversed - that same 1 is 01 00. Socket libraries have calls for converting 16 and 32 bit integers - `ntohl, htonl, ntohs, htons` where “n” means *network* and “h” means *host*, “s” means *short* and “l” means *long*. Where network order is host order, these do nothing, but where the machine is byte-reversed, these swap the bytes around appropriately.146147В эпоху 32-битных машин ASCII-представление двоичных данных часто оказывается меньше двоичного. Это потому, что на удивление часто все эти длинные целые имеют значение 0 или, возможно, 1. Строка «0» занимает два байта, а двоичное представление – четыре. Конечно, это плохо сочетается с сообщениями фиксированной длины. Решения, решения…148149## Отключение150151Строго говоря, полагается использовать `shutdown` на сокете перед тем, как `close` его. `shutdown` – это уведомление для сокета на другом конце. В зависимости от переданного аргумента оно может означать: «Я больше не буду отправлять, но продолжу слушать» или «Я не слушаю, скатертью дорога!». Однако большинство библиотек сокетов настолько привыкли к тому, что программисты пренебрегают этим правилом этикета, что обычно `close` эквивалентно `shutdown(); close()`. Поэтому в большинстве ситуаций явный вызов `shutdown` не требуется.152153Один из способов эффективного использования `shutdown` – в обмене, подобном HTTP. Клиент отправляет запрос, а затем выполняет `shutdown(1)`. Это говорит серверу: «Клиент закончил отправку, но всё ещё может принимать». Сервер может обнаружить «конец файла» по получению 0 байт. Он может считать, что получил полный запрос. Сервер отправляет ответ. Если `send` завершается успешно, значит, клиент действительно всё ещё принимал.154155Python идёт ещё дальше в автоматическом завершении и говорит, что когда сокет собирается сборщиком мусора, он автоматически выполнит `close`, если это необходимо. Но полагаться на это – очень плохая привычка. Если ваш сокет просто исчезнет без выполнения `close`, сокет на другом конце может зависнуть на неопределённый срок, думая, что вы просто медлите. *Пожалуйста* `close` свои сокеты, когда закончите.156157### Когда сокеты умирают158159Пожалуй, самое худшее в использовании блокирующих сокетов – это то, что происходит, когда другая сторона внезапно отключается (без выполнения `close`). Ваш сокет, скорее всего, зависнет. TCP – надёжный протокол, и он будет ждать очень, очень долго, прежде чем отказаться от соединения. Если вы используете потоки, весь поток по сути мёртв. С этим мало что можно сделать. Пока вы не делаете ничего глупого, например, не удерживаете блокировку во время блокирующего чтения, поток не потребляет много ресурсов. *Не* пытайтесь убить поток – частично причина, по которой потоки эффективнее процессов, заключается в том, что они избегают накладных расходов, связанных с автоматическим освобождением ресурсов. Другими словами, если вы всё же убьёте поток, весь ваш процесс, скорее всего, будет испорчен.160161## Неблокирующие сокеты162163Если вы поняли предыдущее, вы уже знаете большую часть того, что нужно знать о механике использования сокетов. Вы по-прежнему будете использовать те же вызовы, во многом теми же способами. Просто если делать всё правильно, ваше приложение будет почти вывернуто наизнанку.164165В Python вы используете `socket.setblocking(0)`, чтобы сделать сокет неблокирующим. В C это сложнее (например, нужно выбирать между BSD-версией `O_NONBLOCK` и практически неотличимой POSIX-версией `O_NDELAY`, которая полностью отличается от `TCP_NODELAY`), но идея та же. Это делается после создания сокета, но перед его использованием. (На самом деле, если вы ненормальный, можете переключаться туда-сюда.)166167Главное механическое отличие в том, что `send`, `recv`, `connect` и `accept` могут вернуться, ничего не сделав. У вас (конечно) есть несколько вариантов. Вы можете проверять коды возврата и коды ошибок и в целом свести себя с ума. Если не верите, попробуйте как-нибудь. Ваше приложение разрастётся, станет глючным и будет пожирать процессор. Так что давайте пропустим тупые решения и сделаем правильно.168169Используйте `select`.170171В C программирование `select` довольно сложно. В Python это раз плюнуть, но оно достаточно близко к версии на C, так что если вы понимаете `select` в Python, у вас будет мало проблем с ним в C:172173```python174ready_to_read, ready_to_write, in_error = \175 select.select(176 potential_readers,177 potential_writers,178 potential_errs,179 timeout)180```181182Вы передаёте `select` три списка: первый содержит все сокеты, которые вы, возможно, захотите читать; второй – все сокеты, в которые вы, возможно, захотите писать; и последний (обычно оставляемый пустым) – те, которые нужно проверить на ошибки. Следует отметить, что один сокет может находиться в нескольких списках. Вызов `select` блокирующий, но вы можете задать тайм-аут. Обычно это разумно – установить хороший длинный тайм-аут (скажем, минуту), если нет веской причины поступать иначе.183184В ответ вы получите три списка. Они содержат сокеты, которые действительно доступны для чтения, записи или находятся в состоянии ошибки. Каждый из этих списков является подмножеством (возможно, пустым) соответствующего списка, который вы передали.185186Если сокет находится в списке readable на выходе, вы можете быть настолько уверены, насколько это вообще возможно в нашем деле, что `recv` на этом сокете вернёт *что-то*. Та же идея для списка writable. Вы сможете отправить *что-то*. Возможно, не всё, что хотели, но *что-то* лучше, чем ничего. (На самом деле, любой достаточно здоровый сокет будет возвращаться как writable – это просто означает, что доступно место в исходящем сетевом буфере.)187188Если у вас есть «серверный» сокет, поместите его в список potential\_readers. Если он оказывается в списке readable, ваш `accept` (почти наверняка) сработает. Если вы создали новый сокет для `connect` к кому-то другому, поместите его в список potential\_writers. Если он появляется в списке writable, есть хороший шанс, что соединение установлено.189190На самом деле, `select` может быть полезен даже с блокирующими сокетами. Это один из способов определить, произойдёт ли блокировка: сокет становится readable, когда в буферах что-то есть. Однако это всё равно не решает проблему определения, закончил ли другой конец работу или просто занят чем-то ещё.191192**Предупреждение о переносимости**: В Unix `select` работает как с сокетами, так и с файлами. Не пытайтесь делать это в Windows. В Windows `select` работает только с сокетами. Также обратите внимание, что в C многие более продвинутые опции сокетов в Windows реализованы иначе. На самом деле, в Windows я обычно использую потоки (которые работают очень и очень хорошо) со своими сокетами.193