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

sockets.md

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

1> **Источник:** https://python-all.ru/2.6/howto/sockets.html2>3> «Документация Python на русском» – неофициальный перевод официальной документации Python: версии от 2.6 до 3.16, полнотекстовый поиск, английский оригинал рядом с переводом. Эта Markdown-версия страницы предназначена для работы с LLM: вставьте её в ChatGPT, Claude или Cursor.45---67# Руководство по программированию сокетов89| Автор: | Gordon McMillan |10| --- | --- |1112Аннотация1314Сокеты используются практически везде, но при этом являются одной из самых неправильно понимаемых технологий. Это обзор сокетов с высоты птичьего полёта. Это не совсем учебник – вам всё равно придётся приложить усилия, чтобы всё заработало. Он не охватывает тонкости (а их много), но, надеюсь, даст достаточную основу для начала их приличного использования.1516## Сокеты1718Сокеты используются практически повсеместно, но при этом являются одной из наиболее неправильно понимаемых технологий. Это общий обзор сокетов с высоты 10 000 футов. Это не совсем учебник – вам всё равно придётся поработать, чтобы всё заработало. Он не охватывает все тонкости (а их много), но, надеюсь, даст достаточно знаний, чтобы начать их прилично использовать.1920Далее речь пойдёт только о сокетах INET, но они составляют не менее 99% используемых сокетов. И только о потоковых сокетах – если вы действительно не знаете, что делаете (в таком случае этот HOWTO не для вас!), вы получите лучшую производительность от потокового сокета, чем от чего-либо ещё. Будет предпринята попытка прояснить, что такое сокет, а также дать некоторые рекомендации по работе с блокирующими и неблокирующими сокетами. Но начать стоит с блокирующих сокетов. Необходимо понять, как они работают, прежде чем переходить к неблокирующим.2122Одна из трудностей в понимании этих вещей заключается в том, что слово «сокет» может означать несколько слегка разных вещей в зависимости от контекста. Итак, сначала давайте разграничим «клиентский» сокет – конечную точку беседы, и «серверный» сокет, который больше похож на оператора телефонной станции. Клиентское приложение (например, ваш браузер) использует только «клиентские» сокеты; веб-сервер, с которым оно общается, использует как «серверные», так и «клиентские» сокеты.2324### История2526Среди различных форм IPC (*межпроцессного взаимодействия*) сокеты являются наиболее популярными. На любой платформе, вероятно, существуют другие формы IPC, которые быстрее, но для кроссплатформенной коммуникации сокеты – практически единственный вариант.2728Они были изобретены в Беркли как часть BSD-версии Unix. С распространением Интернета они распространились как лесной пожар. И не зря – сочетание сокетов с INET позволяет невероятно легко общаться с произвольными машинами по всему миру (по крайней мере, по сравнению с другими подходами).2930## Создание сокета3132Если говорить приблизительно, то когда вы щёлкнули по ссылке, которая привела вас на эту страницу, ваш браузер сделал примерно следующее:3334```python35#создать INET-сокет для потока данных (STREAM)36s = socket.socket(37    socket.AF_INET, socket.SOCK_STREAM)38#теперь подключаемся к веб-серверу на порту 8039# - обычный HTTP-порт40s.connect(("www.mcmillan-inc.com", 80))41```4243Когда `connect` завершается, сокет `s` теперь можно использовать для отправки запроса на получение текста этой страницы. Тот же сокет прочитает ответ, а затем будет уничтожен. Именно так – уничтожен. Клиентские сокеты обычно используются только для одного обмена (или небольшой последовательности обменов).4445Что происходит в веб-сервере, немного сложнее. Сначала веб-сервер создаёт «серверный сокет».4647```python48#создать INET-сокет для потока данных (STREAM)49serversocket = socket.socket(50    socket.AF_INET, socket.SOCK_STREAM)51#привязать сокет к публичному хосту,52# и широко известному порту53serversocket.bind((socket.gethostname(), 80))54#стать серверным сокетом55serversocket.listen(5)56```5758Несколько моментов, на которые стоит обратить внимание: использовался `socket.gethostname()`, чтобы сокет был виден извне. Если бы использовались `s.bind(('', 80))` или `s.bind(('localhost', 80))` или `s.bind(('127.0.0.1', 80))`, то получился бы «серверный» сокет, но видимый только в пределах той же машины.5960Второе, что стоит отметить: порты с низкими номерами обычно зарезервированы для «хорошо известных» служб (HTTP, SNMP и т.д.). Если вы просто экспериментируете, используйте подходящий высокий номер (4 цифры).6162Наконец, аргумент `listen` сообщает библиотеке сокетов, что мы хотим, чтобы она ставила в очередь до 5 запросов на соединение (обычный максимум), прежде чем отказывать во внешних подключениях. Если остальной код написан правильно, этого должно быть достаточно.6364Итак, теперь у нас есть «серверный» сокет, слушающий порт 80. Теперь мы входим в главный цикл веб-сервера:6566```python67while 1:68    #принимать подключения извне69    (clientsocket, address) = serversocket.accept()70    #теперь выполнить действия с clientsocket71    #в данном случае предполагается, что это многопоточный сервер72    ct = client_thread(clientsocket)73    ct.run()74```7576There’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.7778### Межпроцессное взаимодействие7980Если требуется быстрый межпроцессный обмен (IPC) между двумя процессами на одной машине, стоит изучить любую форму разделяемой памяти, доступную на платформе. Простой протокол на основе разделяемой памяти и блокировок или семафоров – самый быстрый подход.8182Если вы всё же решили использовать сокеты, привяжите «серверный» сокет к `'localhost'`. На большинстве платформ это позволит обойти несколько уровней сетевого кода и будет работать заметно быстрее.8384## Использование сокета8586Первое, что стоит отметить: «клиентский» сокет веб-браузера и «клиентский» сокет веб-сервера – это одинаковые сущности. То есть это «одноранговый» разговор. Или, иными словами, *как разработчик, вы должны решить, каковы правила этикета для разговора*. Обычно сокет, вызывающий `connect`, начинает разговор, отправляя запрос или, возможно, приветствие. Но это дизайнерское решение, а не правило сокетов.8788Теперь есть два набора операций для связи. Можно использовать `send` и `recv`, или преобразовать клиентский сокет в файлоподобную сущность и использовать `read` и `write`. Последний способ – это то, как Java представляет свои сокеты. Я не буду здесь о нём говорить, кроме предупреждения: необходимо использовать `flush` для сокетов. Это буферизированные «файлы», и распространённая ошибка – `write` что-то, а затем `read` в ожидании ответа. Без `flush` можно ждать ответа вечно, потому что запрос всё ещё может находиться в выходном буфере.8990Теперь мы подошли к главному камню преткновения в сокетах – `send` и `recv` работают с сетевыми буферами. Они не обязательно обрабатывают все байты, которые им передают (или ожидают получить), поскольку их основная задача – управление сетевыми буферами. В общем случае они возвращают управление, когда соответствующие сетевые буферы заполнены (`send`) или опустошены (`recv`). Затем они сообщают, сколько байт было обработано. *Ваша* задача – вызывать их снова, пока сообщение не будет полностью обработано.9192Когда `recv` возвращает 0 байт, это означает, что другая сторона закрыла (или закрывает) соединение. Вы больше не получите никаких данных по этому соединению. Никогда. Возможно, вы сможете успешно отправлять данные; об этом я расскажу на следующей странице.9394Протокол вроде HTTP использует сокет только для одной передачи. Клиент отправляет запрос, затем читает ответ. И всё. Сокет отбрасывается. Это означает, что клиент может определить конец ответа, получив 0 байт.9596Но если вы планируете повторно использовать сокет для дальнейших передач, нужно осознать, что *на сокете нет «EOT» (конца передачи).* Повторяю: если сокет `send` или `recv` возвращает 0 обработанных байт, соединение разорвано. Если соединение *не* разорвано, можно ждать на `recv` вечно, потому что сокет *не* сообщит, что больше нечего читать (пока). Теперь, если немного подумать, можно осознать фундаментальную истину о сокетах: *сообщения должны либо быть фиксированной длины* (фу), *либо иметь разделители* (пожимание плечами), *либо указывать свою длину* (гораздо лучше), *либо завершаться закрытием соединения*. Выбор полностью за вами (но некоторые способы правильнее других).9798Если вы не хотите завершать соединение, самое простое решение – сообщение фиксированной длины:99100```python101class mysocket:102    '''только демонстрационный класс103      написан для ясности, а не для эффективности104    '''105106    def __init__(self, sock=None):107        if sock is None:108            self.sock = socket.socket(109                socket.AF_INET, socket.SOCK_STREAM)110        else:111            self.sock = sock112113    def connect(self, host, port):114        self.sock.connect((host, port))115116    def mysend(self, msg):117        totalsent = 0118        while totalsent < MSGLEN:119            sent = self.sock.send(msg[totalsent:])120            if sent == 0:121                raise RuntimeError("socket connection broken")122            totalsent = totalsent + sent123124    def myreceive(self):125        msg = ''126        while len(msg) < MSGLEN:127            chunk = self.sock.recv(MSGLEN-len(msg))128            if chunk == '':129                raise RuntimeError("socket connection broken")130            msg = msg + chunk131        return msg132```133134Код отправки здесь подходит почти для любой схемы обмена сообщениями – в Python вы отправляете строки и можете использовать `len()` для определения их длины (даже если в них есть встроенные символы `\0`). В основном сложнее становится код получения. (А в C ненамного хуже, только нельзя использовать `strlen`, если сообщение содержит встроенные `\0`.)135136Самый простой способ улучшения – сделать первый символ сообщения индикатором типа сообщения, а тип определяет длину. Теперь у вас два `recv`: первый – чтобы получить (хотя бы) этот первый символ и узнать длину, а второй – в цикле, чтобы получить остальное. Если вы решите пойти по пути разделителей, вы будете получать данные произвольными порциями (4096 или 8192 часто хорошо соответствуют размеру сетевых буферов) и искать разделитель в полученном.137138Стоит учитывать одно осложнение: если ваш протокол общения допускает отправку нескольких сообщений подряд (без какого-либо ответа), и вы передаёте `recv` произвольный размер порции, вы можете прочитать начало следующего сообщения. Его нужно будет отложить и сохранить, пока оно не понадобится.139140Добавление длины в начало сообщения (например, в виде 5 цифр) усложняет задачу, потому что (как ни странно) вы можете не получить все 5 символов за один `recv`. При тестировании это может сработать, но при высокой сетевой нагрузке ваш код очень быстро сломается, если не использовать два цикла `recv` – первый для определения длины, второй для получения данных сообщения. Неприятно. К тому же вы обнаружите, что `send` не всегда успевает отправить всё за один раз. И несмотря на то, что вы это прочитали, рано или поздно вы на этом обожжётесь!141142В целях экономии места, укрепления вашего характера (и сохранения моих конкурентных позиций) эти улучшения оставлены читателю в качестве упражнения. Перейдём к завершению.143144### Двоичные данные145146Вполне возможно отправлять двоичные данные через сокет. Главная проблема в том, что не все машины используют одинаковые форматы для двоичных данных. Например, процессор Motorola представит 16-битное целое число со значением 1 как два шестнадцатеричных байта 00 01. Intel и DEC, однако, используют обратный порядок байтов – та же единица будет 01 00. Библиотеки сокетов имеют функции для преобразования 16- и 32-битных целых чисел: `ntohl, htonl, ntohs, htons`, где «n» означает *network* (сетевой), а «h» – *host* (хостовый), «s» – *short* (короткое), а «l» – *long* (длинное). Если сетевой порядок совпадает с хостовым, эти функции ничего не делают, но если машина использует обратный порядок байтов, они соответствующим образом меняют байты местами.147148В эпоху 32-битных машин ASCII-представление двоичных данных часто оказывается меньше двоичного. Это потому, что на удивление часто все эти длинные целые имеют значение 0 или, возможно, 1. Строка «0» занимает два байта, а двоичное представление – четыре. Конечно, это плохо сочетается с сообщениями фиксированной длины. Решения, решения…149150## Отключение151152Строго говоря, полагается использовать `shutdown` на сокете перед `close`. `shutdown` – это уведомление сокету на другом конце. В зависимости от переданного аргумента, оно может означать «Я больше не буду отправлять, но буду слушать» или «Я не слушаю, скатертью дорога!». Однако большинство библиотек сокетов так привыкли к тому, что программисты пренебрегают этим правилом этикета, что обычно `close` эквивалентно `shutdown(); close()`. Так что в большинстве ситуаций явный `shutdown` не требуется.153154Один из способов эффективного использования `shutdown` – в обмене, подобном HTTP. Клиент отправляет запрос, а затем выполняет `shutdown(1)`. Это говорит серверу: «Клиент закончил отправку, но всё ещё может принимать». Сервер может обнаружить «EOF» по получению 0 байт. Он может считать, что получил полный запрос. Сервер отправляет ответ. Если `send` завершается успешно, то клиент действительно всё ещё принимал.155156Python идёт ещё дальше в автоматическом закрытии и говорит, что когда сокет собирается сборщиком мусора, он автоматически выполнит `close`, если это необходимо. Но полагаться на это – очень плохая привычка. Если ваш сокет просто исчезнет без выполнения `close`, сокет на другом конце может зависнуть на неопределённое время, думая, что вы просто медлите. *Пожалуйста*, `close` свои сокеты, когда закончите.157158### Когда сокеты умирают159160Пожалуй, самая неприятная особенность использования блокирующих сокетов – это ситуация, когда другая сторона неожиданно отключается (без вызова `close`). Ваш сокет, скорее всего, зависнет. SOCKSTREAM – надежный протокол, и он будет очень долго ждать, прежде чем откажется от соединения. Если вы используете потоки, весь поток по сути мертв. Здесь мало что можно сделать. Пока вы не делаете ничего глупого, например, не удерживаете блокировку во время блокирующего чтения, поток практически не потребляет ресурсов. *Не* пытайтесь завершить поток – отчасти потоки эффективнее процессов именно потому, что избегают накладных расходов на автоматическое освобождение ресурсов. Другими словами, если вам удастся завершить поток, весь процесс, скорее всего, выйдет из строя.161162## Неблокирующие сокеты163164Если вы поняли предыдущее, вы уже знаете большую часть того, что нужно знать о механике использования сокетов. Вы по-прежнему будете использовать те же вызовы, почти теми же способами. Просто, если всё сделать правильно, ваше приложение будет почти вывернуто наизнанку.165166В Python используется `socket.setblocking(0)`, чтобы сделать сокет неблокирующим. В C это сложнее (например, нужно выбирать между вариантом из BSD `O_NONBLOCK` и почти неотличимым вариантом из Posix `O_NDELAY`, который полностью отличается от `TCP_NODELAY`), но идея та же. Это делается после создания сокета, но до его использования. (На самом деле, если вы безумны, можно переключаться туда и обратно.)167168Основное механическое отличие в том, что `send`, `recv`, `connect` и `accept` могут вернуться, ничего не сделав. У вас есть (конечно) несколько вариантов. Можно проверять коды возврата и ошибки и, в общем, свести себя с ума. Если не верите, попробуйте как-нибудь. Ваше приложение станет большим, багованным и будет жрать процессор. Так что давайте пропустим тупые решения и сделаем правильно.169170Используйте `select`.171172На C написание `select` довольно сложно. В Python это проще простого, но реализация достаточно близка к версии на C, так что если вы понимаете `select` в Python, у вас будет мало проблем с ним на C.173174```python175ready_to_read, ready_to_write, in_error = \176               select.select(177                  potential_readers,178                  potential_writers,179                  potential_errs,180                  timeout)181```182183В `select` передаются три списка: первый содержит все сокеты, которые вы, возможно, хотите попробовать читать; второй – все сокеты, в которые вы, возможно, хотите писать; и последний (обычно оставляется пустым) – те, в которых нужно проверять ошибки. Стоит отметить, что сокет может находиться более чем в одном списке. Вызов `select` блокирующий, но можно задать тайм-аут. Обычно это разумно – задать достаточно большой тайм-аут (например, минуту), если нет веских причин поступать иначе.184185В ответ вы получите три списка. Они содержат сокеты, которые фактически доступны для чтения, записи и с ошибками. Каждый из этих списков является подмножеством (возможно пустым) соответствующего списка, который вы передали. И если вы поместили сокет в несколько входных списков, он окажется не более чем в одном выходном списке.186187Если сокет находится в списке readable на выходе, можно быть настолько уверенным, насколько это вообще возможно в нашем деле, что `recv` на этом сокете вернёт *что-нибудь*. То же самое для списка writable. Вы сможете отправить *что-нибудь*. Возможно, не всё, что хотите, но *что-нибудь* лучше, чем ничего. (На самом деле, любой достаточно здоровый сокет будет отмечен как writable – это просто означает, что доступно место в исходящем сетевом буфере.)188189Если у вас есть «серверный» сокет, поместите его в список potential\_readers. Если он оказывается в списке readable, ваш `accept` (почти наверняка) сработает. Если вы создали новый сокет для `connect` к кому-то другому, поместите его в список potential\_writers. Если он появляется в списке writable, есть хороший шанс, что соединение установлено.190191Одна очень неприятная проблема с `select`: если где-то в этих входных списках сокетов есть сокет, который неожиданно завершился с ошибкой, то `select` завершится сбоем. Тогда необходимо пройтись по каждому сокету во всех этих списках и выполнить `select([sock],[],[],0)`, пока не найдётся проблемный. Тайм-аут 0 означает, что это не займёт много времени, но это коряво.192193На самом деле, `select` может быть полезен даже с блокирующими сокетами. Это один из способов определить, будет ли блокировка: сокет помечается как readable, когда в буферах что-то есть. Однако это всё равно не помогает с проблемой определения, закончила ли другая сторона или просто занята чем-то другим.194195**Предупреждение о переносимости**: В Unix `select` работает как с сокетами, так и с файлами. Не пытайтесь делать это в Windows. В Windows `select` работает только с сокетами. Также обратите внимание, что в C многие более продвинутые опции сокетов в Windows реализованы иначе. На самом деле, в Windows для работы с сокетами обычно используются потоки (они работают очень хорошо). Примите как факт: если нужна производительность, код в Windows будет сильно отличаться от кода в Unix.196197### Производительность198199Несомненно, самый быстрый код для сокетов использует неблокирующие сокеты и select для их мультиплексирования. Можно создать нечто, что насытит LAN-соединение без какой-либо нагрузки на процессор. Проблема в том, что приложение, написанное таким образом, не может делать ничего другого – оно должно быть постоянно готово пересылать байты.200201Если ваше приложение на самом деле должно делать что-то большее, то потоки – оптимальное решение (и использование неблокирующих сокетов будет быстрее, чем блокирующих). К сожалению, поддержка потоков в Unix-системах различается и по API, и по качеству. Поэтому обычное решение в Unix – создать подпроцесс для каждого соединения. Накладные расходы на это значительны (не стоит делать этого в Windows – накладные расходы на создание процесса там огромны). Это также означает, что если подпроцессы не полностью независимы, потребуется другая форма межпроцессного взаимодействия, например канал (pipe), разделяемая память и семафоры, для обмена данными между родительским и дочерними процессами.202203Наконец, стоит помнить, что хотя блокирующие сокеты несколько медленнее неблокирующих, во многих случаях они являются «правильным» решением. В конце концов, если приложение управляется данными, которые оно получает через сокет, нет особого смысла усложнять логику, чтобы оно ожидало `select` вместо `recv`.204