Документация Python неофициальный перевод

sockets.md

205 строк · 33.8 КБ · обычная страница · сырой текст · скачать

1> **Источник:** https://python-all.ru/2.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, но они составляют не менее 99% используемых сокетов. И только о потоковых сокетах – если вы действительно не знаете, что делаете (в таком случае этот HOWTO не для вас!), вы получите лучшую производительность от потокового сокета, чем от чего-либо ещё. Будет предпринята попытка прояснить, что такое сокет, а также дать некоторые рекомендации по работе с блокирующими и неблокирующими сокетами. Но начать стоит с блокирующих сокетов. Необходимо понять, как они работают, прежде чем переходить к неблокирующим.2021Одна из трудностей в понимании этих вещей заключается в том, что слово «сокет» может означать несколько слегка разных вещей в зависимости от контекста. Итак, сначала давайте разграничим «клиентский» сокет – конечную точку беседы, и «серверный» сокет, который больше похож на оператора телефонной станции. Клиентское приложение (например, ваш браузер) использует только «клиентские» сокеты; веб-сервер, с которым оно общается, использует как «серверные», так и «клиентские» сокеты.2223### История2425Среди различных форм IPC сокеты, безусловно, самые популярные. На любой платформе, вероятно, существуют другие, более быстрые формы IPC, но для межплатформенного взаимодействия сокеты – это практически единственный доступный вариант.2627Они были изобретены в Беркли как часть BSD-версии Unix. С распространением Интернета они распространились как лесной пожар. И не зря – сочетание сокетов с INET позволяет невероятно легко общаться с произвольными машинами по всему миру (по крайней мере, по сравнению с другими подходами).2829## Создание сокета3031Если говорить приблизительно, то когда вы щёлкнули по ссылке, которая привела вас на эту страницу, ваш браузер сделал примерно следующее:3233```python34#создать INET-сокет для потока данных (STREAM)35s = socket.socket(36    socket.AF_INET, socket.SOCK_STREAM)37#теперь подключаемся к веб-серверу на порту 8038# - обычный HTTP-порт39s.connect(("www.mcmillan-inc.com", 80))40```4142После завершения `connect` сокет `s` можно использовать для отправки запроса на получение текста страницы. Тот же сокет прочитает ответ, а затем будет уничтожен. Да, именно уничтожен. Клиентские сокеты обычно используются только для одного обмена (или небольшого набора последовательных обменов).4344То, что происходит на веб-сервере, немного сложнее. Сначала веб-сервер создаёт «серверный» сокет:4546```python47#создать INET-сокет для потока данных (STREAM)48serversocket = socket.socket(49    socket.AF_INET, socket.SOCK_STREAM)50#привязать сокет к публичному хосту,51# и широко известному порту52serversocket.bind((socket.gethostname(), 80))53#стать серверным сокетом54serversocket.listen(5)55```5657Пара моментов, на которые стоит обратить внимание: мы использовали `socket.gethostname()`, чтобы сокет был виден из внешнего мира. Если бы мы использовали `s.bind(('localhost', 80))` или `s.bind(('127.0.0.1', 80))`, у нас всё равно был бы «серверный» сокет, но доступный только в пределах той же машины. `s.bind(('', 80))` указывает, что сокет доступен по любому адресу, который есть у машины.5859Второе, что стоит отметить: порты с низкими номерами обычно зарезервированы для «хорошо известных» служб (HTTP, SNMP и т.д.). Если вы просто экспериментируете, используйте подходящий высокий номер (4 цифры).6061Наконец, аргумент `listen` сообщает библиотеке сокетов, что мы хотим, чтобы она ставила в очередь до 5 запросов на подключение (обычный максимум) перед отказом во внешних подключениях. Если остальной код написан правильно, этого должно быть достаточно.6263Теперь, когда у нас есть «серверный» сокет, прослушивающий порт 80, мы можем войти в основной цикл веб-сервера:6465```python66while 1:67    #принимать подключения извне68    (clientsocket, address) = serversocket.accept()69    #теперь выполнить действия с clientsocket70    #в данном случае предполагается, что это многопоточный сервер71    ct = client_thread(clientsocket)72    ct.run()73```7475На самом деле есть три общих способа организации этого цикла: породить поток для обработки `clientsocket`, создать новый процесс для обработки `clientsocket`, или перестроить приложение для использования неблокирующих сокетов и мультиплексировать между нашим «серверным» сокетом и любыми активными `clientsocket` с помощью `select`. Об этом позже. Важно понять сейчас: *это и есть всё*, что делает «серверный» сокет. Он не отправляет данные. Он не принимает данные. Он просто создаёт «клиентские» сокеты. Каждый `clientsocket` создаётся в ответ на то, что какой-то *другой* «клиентский» сокет выполняет `connect()` к хосту и порту, к которому мы привязаны. Как только мы создали этот `clientsocket`, мы возвращаемся к ожиданию новых подключений. Два «клиента» могут свободно общаться – они используют некоторый динамически выделенный порт, который будет переиспользован после завершения разговора.7677### Межпроцессное взаимодействие7879Если требуется быстрый межпроцессный обмен (IPC) между двумя процессами на одной машине, стоит изучить любую форму разделяемой памяти, доступную на платформе. Простой протокол на основе разделяемой памяти и блокировок или семафоров – самый быстрый подход.8081Если вы всё же решите использовать сокеты, привяжите «серверный» сокет к `'localhost'`. На большинстве платформ это позволит обойти несколько уровней сетевого кода и будет заметно быстрее.8283## Использование сокета8485Первое, что следует отметить: «клиентский» сокет веб-браузера и «клиентский» сокет веб-сервера – это одинаковые звери. То есть это диалог «равный с равным». Или, другими словами, *как разработчик, вы должны решить, каковы правила этикета для этого диалога*. Обычно сокет `connect` начинает разговор, отправляя запрос или, возможно, приветствие. Но это проектное решение – а не правило работы сокетов.8687Теперь есть два набора функций для общения. Вы можете использовать `send` и `recv`, или преобразовать клиентский сокет в файлоподобную сущность и использовать `read` и `write`. Последний способ – это то, как Java представляет свои сокеты. Я не буду здесь об этом говорить, за исключением предупреждения: вам нужно использовать `flush` на сокетах. Это буферизованные «файлы», и распространенная ошибка – `write` что-то, а затем `read` ответа. Без `flush` в этом случае вы можете ждать ответа вечно, потому что запрос все еще может находиться в выходном буфере.8889Теперь мы подошли к главному камню преткновения сокетов – `send` и `recv` работают напрямую с сетевыми буферами. Они не обязательно обрабатывают все байты, которые вы им передаете (или ожидаете от них), потому что их основная задача – работа с сетевыми буферами. В общем случае они возвращают управление, когда соответствующие сетевые буферы заполнены (`send`) или опустошены (`recv`). Затем они сообщают, сколько байтов было обработано. Это *ваша* задача – вызывать их снова, пока ваше сообщение не будет полностью обработано.9091Когда `recv` возвращает 0 байт, это означает, что другая сторона закрыла (или закрывает) соединение. Вы больше не получите никаких данных по этому соединению. Никогда. Возможно, вы сможете успешно отправлять данные; позже я расскажу об этом подробнее.9293Протокол вроде HTTP использует сокет только для одной передачи. Клиент отправляет запрос, затем читает ответ. Вот и всё. Сокет отбрасывается. Это означает, что клиент может определить конец ответа, получив 0 байт.9495Но если вы планируете повторно использовать сокет для дальнейших передач, нужно понимать, что *не существует* EOT *на сокете.* Повторяю: если `send` или `recv` возвращает управление после обработки 0 байт, соединение разорвано. Если соединение *не* разорвано, вы можете ждать на `recv` вечно, потому что сокет *не* сообщит вам, что больше нечего читать (пока). Теперь, если немного подумать, можно прийти к фундаментальной истине сокетов: *сообщения должны быть фиксированной длины* (фу), *или быть разделёнными ограничителями* (пожимание плечами), *или указывать свою длину* (гораздо лучше), *или завершаться закрытием соединения*. Выбор полностью за вами (но некоторые способы более правильные, чем другие).9697Если вы не хотите завершать соединение, самое простое решение – сообщение фиксированной длины:9899```python100class mysocket:101    '''только демонстрационный класс102      написан для ясности, а не для эффективности103    '''104105    def __init__(self, sock=None):106        if sock is None:107            self.sock = socket.socket(108                socket.AF_INET, socket.SOCK_STREAM)109        else:110            self.sock = sock111112    def connect(self, host, port):113        self.sock.connect((host, port))114115    def mysend(self, msg):116        totalsent = 0117        while totalsent < MSGLEN:118            sent = self.sock.send(msg[totalsent:])119            if sent == 0:120                raise RuntimeError("socket connection broken")121            totalsent = totalsent + sent122123    def myreceive(self):124        chunks = []125        bytes_recd = 0126        while bytes_recd < MSGLEN:127            chunk = self.sock.recv(min(MSGLEN - bytes_recd, 2048))128            if chunk == '':129                raise RuntimeError("socket connection broken")130            chunks.append(chunk)131            bytes_recd = bytes_recd + len(chunk)132        return ''.join(chunks)133```134135Приведённый здесь код отправки подходит почти для любой схемы передачи сообщений – в Python вы отправляете строки и можете использовать `len()` для определения их длины (даже если они содержат встроенные символы `\0`). В основном сложнее становится код получения. (А в C ненамного хуже, за исключением того, что нельзя использовать `strlen`, если сообщение содержит встроенные `\0`.)136137Самое простое улучшение – сделать первый символ сообщения индикатором типа сообщения, и пусть тип определяет длину. Теперь у вас есть два `recv`: первый – чтобы получить (как минимум) этот первый символ, чтобы узнать длину, и второй – в цикле для получения остального. Если вы решите пойти по пути разделителей, вы будете получать данные произвольными порциями (4096 или 8192 часто хорошо соответствуют размерам сетевых буферов) и сканировать полученное на предмет разделителя.138139Одно усложнение, о котором нужно знать: если ваш протокол общения допускает отправку нескольких сообщений подряд (без какого-либо ответа) и вы передаёте `recv` произвольный размер порции, вы можете прочитать начало следующего сообщения. Вам придётся отложить его в сторону и хранить, пока оно не понадобится.140141Префикс сообщения с его длиной (скажем, 5 числовых символов) усложняет задачу, потому что (верите или нет) вы можете не получить все 5 символов за один `recv`. При экспериментах это может сойти с рук, но при высокой сетевой нагрузке ваш код очень быстро сломается, если вы не используете два цикла `recv`: первый для определения длины, второй для получения данных сообщения. Отвратительно. И именно тогда вы обнаружите, что `send` не всегда удаляет всё за один проход. И несмотря на то, что вы это прочитали, вы всё равно на этом обожжётесь!142143В целях экономии места, укрепления вашего характера (и сохранения моих конкурентных позиций) эти улучшения оставлены читателю в качестве упражнения. Перейдём к завершению.144145### Двоичные данные146147It 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.148149В эпоху 32-битных машин ASCII-представление двоичных данных часто оказывается меньше двоичного. Это потому, что на удивление часто все эти длинные целые имеют значение 0 или, возможно, 1. Строка «0» занимает два байта, а двоичное представление – четыре. Конечно, это плохо сочетается с сообщениями фиксированной длины. Решения, решения…150151## Отключение152153Строго говоря, полагается использовать `shutdown` на сокете перед тем, как `close` его. `shutdown` – это уведомление для сокета на другом конце. В зависимости от переданного аргумента оно может означать: «Я больше не буду отправлять, но продолжу слушать» или «Я не слушаю, скатертью дорога!». Однако большинство библиотек сокетов настолько привыкли к тому, что программисты пренебрегают этим правилом этикета, что обычно `close` эквивалентно `shutdown(); close()`. Поэтому в большинстве ситуаций явный вызов `shutdown` не требуется.154155Один из способов эффективного использования `shutdown` – в обмене, подобном HTTP. Клиент отправляет запрос, а затем выполняет `shutdown(1)`. Это говорит серверу: «Клиент закончил отправку, но всё ещё может принимать». Сервер может обнаружить «конец файла» по получению 0 байт. Он может считать, что получил полный запрос. Сервер отправляет ответ. Если `send` завершается успешно, значит, клиент действительно всё ещё принимал.156157Python идёт ещё дальше в автоматическом завершении и говорит, что когда сокет собирается сборщиком мусора, он автоматически выполнит `close`, если это необходимо. Но полагаться на это – очень плохая привычка. Если ваш сокет просто исчезнет без выполнения `close`, сокет на другом конце может зависнуть на неопределённый срок, думая, что вы просто медлите. *Пожалуйста* `close` свои сокеты, когда закончите.158159### Когда сокеты умирают160161Вероятно, самое неприятное в использовании блокирующих сокетов – это то, что происходит, когда другая сторона резко отключается (без вызова `close`). Ваш сокет, скорее всего, зависнет. SOCKSTREAM – это надежный протокол, и он будет ждать очень долго, прежде чем откажется от соединения. Если вы используете потоки, весь поток по сути мертв. С этим мало что можно поделать. Пока вы не делаете глупостей, например, не удерживаете блокировку во время блокирующего чтения, поток не потребляет много ресурсов. *Не* пытайтесь убить поток – отчасти причина, по которой потоки эффективнее процессов, заключается в том, что они избегают накладных расходов, связанных с автоматическим освобождением ресурсов. Другими словами, если вам удастся убить поток, весь процесс, скорее всего, будет испорчен.162163## Неблокирующие сокеты164165Если вы поняли предыдущее, вы уже знаете большую часть того, что нужно знать о механике использования сокетов. Вы по-прежнему будете использовать те же вызовы, во многом теми же способами. Просто если делать всё правильно, ваше приложение будет почти вывернуто наизнанку.166167В Python для неблокирующего режима используется `socket.setblocking(0)`. В C всё сложнее (нужно выбрать между BSD-версией `O_NONBLOCK` и почти неотличимой версией Posix `O_NDELAY`, которая полностью отличается от `TCP_NODELAY`), но идея та же. Это делается после создания сокета, но до его использования. (Впрочем, если есть особая необходимость, можно переключаться туда и обратно.)168169Главное механическое отличие в том, что `send`, `recv`, `connect` и `accept` могут вернуться, ничего не сделав. У вас (конечно) есть несколько вариантов. Вы можете проверять коды возврата и коды ошибок и в целом свести себя с ума. Если не верите, попробуйте как-нибудь. Ваше приложение разрастётся, станет глючным и будет пожирать процессор. Так что давайте пропустим тупые решения и сделаем правильно.170171Используйте `select`.172173В C программирование `select` довольно сложно. В Python это раз плюнуть, но оно достаточно близко к версии на C, так что если вы понимаете `select` в Python, у вас будет мало проблем с ним в C:174175```python176ready_to_read, ready_to_write, in_error = \177               select.select(178                  potential_readers,179                  potential_writers,180                  potential_errs,181                  timeout)182```183184Вы передаёте `select` три списка: первый содержит все сокеты, которые вы, возможно, захотите читать; второй – все сокеты, в которые вы, возможно, захотите писать; и последний (обычно оставляемый пустым) – те, которые нужно проверить на ошибки. Следует отметить, что один сокет может находиться в нескольких списках. Вызов `select` блокирующий, но вы можете задать тайм-аут. Обычно это разумно – установить хороший длинный тайм-аут (скажем, минуту), если нет веской причины поступать иначе.185186В ответ вы получите три списка. Они содержат сокеты, которые действительно доступны для чтения, записи или находятся в состоянии ошибки. Каждый из этих списков является подмножеством (возможно, пустым) соответствующего списка, который вы передали.187188Если сокет находится в списке readable на выходе, вы можете быть настолько уверены, насколько это вообще возможно в нашем деле, что `recv` на этом сокете вернёт *что-то*. Та же идея для списка writable. Вы сможете отправить *что-то*. Возможно, не всё, что хотели, но *что-то* лучше, чем ничего. (На самом деле, любой достаточно здоровый сокет будет возвращаться как writable – это просто означает, что доступно место в исходящем сетевом буфере.)189190Если у вас есть «серверный» сокет, поместите его в список potential\_readers. Если он оказывается в списке readable, ваш `accept` (почти наверняка) сработает. Если вы создали новый сокет для `connect` к кому-то другому, поместите его в список potential\_writers. Если он появляется в списке writable, есть хороший шанс, что соединение установлено.191192Одна очень неприятная проблема с `select`: если где-то в этих входных списках сокетов есть сокет, который умер неприятной смертью, то `select` завершится ошибкой. Затем вам нужно перебрать каждый проклятый сокет во всех этих списках и выполнить `select([sock],[],[],0)`, пока не найдёте проблемный. Тайм-аут 0 означает, что это не займёт много времени, но это уродливо.193194На самом деле, `select` может быть полезен даже с блокирующими сокетами. Это один из способов определить, произойдёт ли блокировка: сокет становится readable, когда в буферах что-то есть. Однако это всё равно не решает проблему определения, закончил ли другой конец работу или просто занят чем-то ещё.195196**Предупреждение о переносимости**: В Unix `select` работает как с сокетами, так и с файлами. Не пытайтесь делать это в Windows. В Windows `select` работает только с сокетами . Также обратите внимание, что в C многие продвинутые опции сокетов реализованы по-другому в Windows. Фактически, в Windows я обычно использую потоки (которые работают очень, очень хорошо) с моими сокетами. Смиритесь: если вам нужна хоть какая-то производительность, ваш код будет выглядеть совершенно по-разному в Windows и Unix.197198### Производительность199200Несомненно, самый быстрый код для сокетов использует неблокирующие сокеты и select для их мультиплексирования. Можно создать нечто, что насытит LAN-соединение без какой-либо нагрузки на процессор. Проблема в том, что приложение, написанное таким образом, не может делать ничего другого – оно должно быть постоянно готово пересылать байты.201202Если ваше приложение на самом деле должно делать что-то большее, то потоки – оптимальное решение (и использование неблокирующих сокетов будет быстрее, чем блокирующих). К сожалению, поддержка потоков в Unix-системах различается и по API, и по качеству. Поэтому обычное решение в Unix – создать подпроцесс для каждого соединения. Накладные расходы на это значительны (не стоит делать этого в Windows – накладные расходы на создание процесса там огромны). Это также означает, что если подпроцессы не полностью независимы, потребуется другая форма межпроцессного взаимодействия, например канал (pipe), разделяемая память и семафоры, для обмена данными между родительским и дочерними процессами.203204Наконец, помните, что хотя блокирующие сокеты несколько медленнее неблокирующих, во многих случаях они являются «правильным» решением. В конце концов, если ваше приложение управляется данными, которые оно получает через сокет, нет большого смысла в усложнении логики только для того, чтобы ваше приложение ожидало `select` вместо `recv`.205