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

sockets.md

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

1> **Источник:** https://python-all.ru/3.1/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(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(('', 80))` или `s.bind(('localhost', 80))` или `s.bind(('127.0.0.1', 80))`, то получился бы «серверный» сокет, но видимый только в пределах той же машины.5859Второе, что стоит отметить: порты с низкими номерами обычно зарезервированы для «хорошо известных» служб (HTTP, SNMP и т.д.). Если вы просто экспериментируете, используйте подходящий высокий номер (4 цифры).6061Наконец, аргумент `listen` сообщает библиотеке сокетов, что мы хотим, чтобы она ставила в очередь до 5 запросов на соединение (обычный максимум), прежде чем отказывать во внешних подключениях. Если остальной код написан правильно, этого должно быть достаточно.6263Теперь, когда у нас есть «серверный» сокет, прослушивающий порт 80, мы можем войти в основной цикл веб-сервера:6465```python66while True:67    #принимать подключения извне68    (clientsocket, address) = serversocket.accept()69    #теперь выполнить действия с clientsocket70    #в данном случае предполагается, что это многопоточный сервер71    ct = client_thread(clientsocket)72    ct.run()73```7475There’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.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 байт.9495But 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).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        msg = ''125        while len(msg) < MSGLEN:126            chunk = self.sock.recv(MSGLEN-len(msg))127            if chunk == '':128                raise RuntimeError("socket connection broken")129            msg = msg + chunk130        return msg131```132133Код отправки здесь подходит почти для любой схемы обмена сообщениями – в Python вы отправляете строки и можете использовать `len()` для определения их длины (даже если в них есть встроенные символы `\0`). В основном сложнее становится код получения. (А в C ненамного хуже, только нельзя использовать `strlen`, если сообщение содержит встроенные `\0`.)134135Самый простой способ улучшения – сделать первый символ сообщения индикатором типа сообщения, а тип определяет длину. Теперь у вас два `recv`: первый – чтобы получить (хотя бы) этот первый символ и узнать длину, а второй – в цикле, чтобы получить остальное. Если вы решите пойти по пути разделителей, вы будете получать данные произвольными порциями (4096 или 8192 часто хорошо соответствуют размеру сетевых буферов) и искать разделитель в полученном.136137Стоит учитывать одно осложнение: если ваш протокол общения допускает отправку нескольких сообщений подряд (без какого-либо ответа), и вы передаёте `recv` произвольный размер порции, вы можете прочитать начало следующего сообщения. Его нужно будет отложить и сохранить, пока оно не понадобится.138139Добавление длины в начало сообщения (например, в виде 5 цифр) усложняет задачу, потому что (как ни странно) вы можете не получить все 5 символов за один `recv`. При тестировании это может сработать, но при высокой сетевой нагрузке ваш код очень быстро сломается, если не использовать два цикла `recv` – первый для определения длины, второй для получения данных сообщения. Неприятно. К тому же вы обнаружите, что `send` не всегда успевает отправить всё за один раз. И несмотря на то, что вы это прочитали, рано или поздно вы на этом обожжётесь!140141В целях экономии места, укрепления вашего характера (и сохранения моих конкурентных позиций) эти улучшения оставлены читателю в качестве упражнения. Перейдём к завершению.142143### Двоичные данные144145Вполне возможно отправлять двоичные данные через сокет. Главная проблема в том, что не все машины используют одинаковые форматы для двоичных данных. Например, процессор Motorola представит 16-битное целое число со значением 1 как два шестнадцатеричных байта 00 01. Intel и DEC, однако, используют обратный порядок байтов – та же единица будет 01 00. Библиотеки сокетов имеют функции для преобразования 16- и 32-битных целых чисел: `ntohl, htonl, ntohs, htons`, где «n» означает *network* (сетевой), а «h» – *host* (хостовый), «s» – *short* (короткое), а «l» – *long* (длинное). Если сетевой порядок совпадает с хостовым, эти функции ничего не делают, но если машина использует обратный порядок байтов, они соответствующим образом меняют байты местами.146147В эпоху 32-битных машин ASCII-представление двоичных данных часто оказывается меньше двоичного. Это потому, что на удивление часто все эти длинные целые имеют значение 0 или, возможно, 1. Строка «0» занимает два байта, а двоичное представление – четыре. Конечно, это плохо сочетается с сообщениями фиксированной длины. Решения, решения…148149## Отключение150151Строго говоря, полагается использовать `shutdown` на сокете перед `close`. `shutdown` – это уведомление сокету на другом конце. В зависимости от переданного аргумента, оно может означать «Я больше не буду отправлять, но буду слушать» или «Я не слушаю, скатертью дорога!». Однако большинство библиотек сокетов так привыкли к тому, что программисты пренебрегают этим правилом этикета, что обычно `close` эквивалентно `shutdown(); close()`. Так что в большинстве ситуаций явный `shutdown` не требуется.152153Один из способов эффективного использования `shutdown` – в обмене, подобном HTTP. Клиент отправляет запрос, а затем выполняет `shutdown(1)`. Это говорит серверу: «Клиент закончил отправку, но всё ещё может принимать». Сервер может обнаружить «EOF» по получению 0 байт. Он может считать, что получил полный запрос. Сервер отправляет ответ. Если `send` завершается успешно, то клиент действительно всё ещё принимал.154155Python идёт ещё дальше в автоматическом закрытии и говорит, что когда сокет собирается сборщиком мусора, он автоматически выполнит `close`, если это необходимо. Но полагаться на это – очень плохая привычка. Если ваш сокет просто исчезнет без выполнения `close`, сокет на другом конце может зависнуть на неопределённое время, думая, что вы просто медлите. *Пожалуйста*, `close` свои сокеты, когда закончите.156157### Когда сокеты умирают158159Пожалуй, самая неприятная особенность использования блокирующих сокетов – это ситуация, когда другая сторона неожиданно отключается (без вызова `close`). Ваш сокет, скорее всего, зависнет. SOCKSTREAM – надежный протокол, и он будет очень долго ждать, прежде чем откажется от соединения. Если вы используете потоки, весь поток по сути мертв. Здесь мало что можно сделать. Пока вы не делаете ничего глупого, например, не удерживаете блокировку во время блокирующего чтения, поток практически не потребляет ресурсов. *Не* пытайтесь завершить поток – отчасти потоки эффективнее процессов именно потому, что избегают накладных расходов на автоматическое освобождение ресурсов. Другими словами, если вам удастся завершить поток, весь процесс, скорее всего, выйдет из строя.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`: если где-то в этих входных списках сокетов есть сокет, который неожиданно завершился с ошибкой, то `select` завершится сбоем. Тогда необходимо пройтись по каждому сокету во всех этих списках и выполнить `select([sock],[],[],0)`, пока не найдётся проблемный. Тайм-аут 0 означает, что это не займёт много времени, но это коряво.191192На самом деле, `select` может быть полезен даже с блокирующими сокетами. Это один из способов определить, будет ли блокировка: сокет помечается как readable, когда в буферах что-то есть. Однако это всё равно не помогает с проблемой определения, закончила ли другая сторона или просто занята чем-то другим.193194**Предупреждение о переносимости**: В Unix `select` работает как с сокетами, так и с файлами. Не пытайтесь делать это в Windows. В Windows `select` работает только с сокетами. Также обратите внимание, что в C многие более продвинутые опции сокетов в Windows реализованы иначе. На самом деле, в Windows для работы с сокетами обычно используются потоки (они работают очень хорошо). Примите как факт: если нужна производительность, код в Windows будет сильно отличаться от кода в Unix.195196### Производительность197198Несомненно, самый быстрый код для сокетов использует неблокирующие сокеты и select для их мультиплексирования. Можно создать нечто, что насытит LAN-соединение без какой-либо нагрузки на процессор. Проблема в том, что приложение, написанное таким образом, не может делать ничего другого – оно должно быть постоянно готово пересылать байты.199200Если ваше приложение на самом деле должно делать что-то большее, то потоки – оптимальное решение (и использование неблокирующих сокетов будет быстрее, чем блокирующих). К сожалению, поддержка потоков в Unix-системах различается и по API, и по качеству. Поэтому обычное решение в Unix – создать подпроцесс для каждого соединения. Накладные расходы на это значительны (не стоит делать этого в Windows – накладные расходы на создание процесса там огромны). Это также означает, что если подпроцессы не полностью независимы, потребуется другая форма межпроцессного взаимодействия, например канал (pipe), разделяемая память и семафоры, для обмена данными между родительским и дочерними процессами.201202Наконец, стоит помнить, что хотя блокирующие сокеты несколько медленнее неблокирующих, во многих случаях они являются «правильным» решением. В конце концов, если приложение управляется данными, которые оно получает через сокет, нет особого смысла усложнять логику, чтобы оно ожидало `select` вместо `recv`.203