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

sockets.md

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

1> **Источник:** https://python-all.ru/3.2/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# теперь подключиться к веб-серверу на порту 80 – обычный порт HTTP38s.connect(("www.python.org", 80))39```4041Когда `connect` завершается, сокет `s` можно использовать для отправки запроса на получение текста страницы. Тот же сокет будет читать ответ, а затем будет уничтожен. Именно так, уничтожен. Клиентские сокеты обычно используются только для одного обмена (или небольшой последовательности обменов).4243То, что происходит на веб-сервере, немного сложнее. Сначала веб-сервер создаёт «серверный» сокет:4445```python46# создать INET-сокет для потока данных (STREAM)47serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)48# привязать сокет к публичному хосту и известному порту49serversocket.bind((socket.gethostname(), 80))50# стать серверным сокетом51serversocket.listen(5)52```5354Несколько моментов, на которые стоит обратить внимание: использовался `socket.gethostname()`, чтобы сокет был виден извне. Если бы использовались `s.bind(('', 80))` или `s.bind(('localhost', 80))` или `s.bind(('127.0.0.1', 80))`, то получился бы «серверный» сокет, но видимый только в пределах той же машины.5556Второе, что стоит отметить: порты с низкими номерами обычно зарезервированы для «хорошо известных» служб (HTTP, SNMP и т.д.). Если вы просто экспериментируете, используйте подходящий высокий номер (4 цифры).5758Наконец, аргумент `listen` сообщает библиотеке сокетов, что мы хотим, чтобы она ставила в очередь до 5 запросов на соединение (обычный максимум), прежде чем отказывать во внешних подключениях. Если остальной код написан правильно, этого должно быть достаточно.5960Теперь, когда у нас есть «серверный» сокет, прослушивающий порт 80, мы можем войти в основной цикл веб-сервера:6162```python63while True:64    # принимать подключения извне65    (clientsocket, address) = serversocket.accept()66    # теперь выполнить действия с clientsocket67    # в данном случае предполагается, что это многопоточный сервер68    ct = client_thread(clientsocket)69    ct.run()70```7172There’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.7374### Межпроцессное взаимодействие7576Если требуется быстрый IPC между двумя процессами на одной машине, стоит рассмотреть каналы или разделяемую память. Если же решено использовать AF\_INET сокеты, привяжите «серверный» сокет к `'localhost'`. На большинстве платформ это позволит обойти несколько уровней сетевого кода и будет значительно быстрее.7778> **См. также**79>80> Модуль [`multiprocessing`](https://python-all.ru/3.2/library/multiprocessing.html#module-multiprocessing) интегрирует кроссплатформенный IPC в высокоуровневый API.8182## Использование сокета8384Первое, что стоит отметить: «клиентский» сокет веб-браузера и «клиентский» сокет веб-сервера – это одинаковые сущности. То есть это «одноранговый» разговор. Или, иными словами, *как разработчик, вы должны решить, каковы правила этикета для разговора*. Обычно сокет, вызывающий `connect`, начинает разговор, отправляя запрос или, возможно, приветствие. Но это дизайнерское решение, а не правило сокетов.8586Теперь есть два набора операций для общения. Можно использовать `send` и `recv`, или превратить клиентский сокет в файлоподобную сущность и использовать `read` и `write`. Последний способ – это то, как Java представляет свои сокеты. Я не буду здесь об этом говорить, кроме предупреждения: нужно использовать `flush` для сокетов. Это буферизованные «файлы», и распространённая ошибка – `write` что-то, а затем `read` в ожидании ответа. Без `flush` можно ждать ответа вечно, потому что запрос может всё ещё находиться в выходном буфере.8788Теперь мы подошли к главному камню преткновения сокетов: `send` и `recv` работают с сетевыми буферами. Они не обязательно обрабатывают все байты, которые вы им передаёте (или ожидаете от них), потому что их основная задача – работа с сетевыми буферами. В общем случае они возвращаются, когда соответствующие сетевые буферы заполнены (`send`) или опустошены (`recv`). Затем они сообщают, сколько байт обработали. *Ваша* ответственность – вызывать их снова, пока сообщение не будет полностью обработано.8990Когда `recv` возвращает 0 байт, это означает, что другая сторона закрыла (или закрывает) соединение. Вы больше не получите никаких данных по этому соединению. Никогда. Возможно, вы сможете успешно отправить данные; об этом позже.9192Протокол вроде HTTP использует сокет только для одной передачи. Клиент отправляет запрос, затем читает ответ. Вот и всё. Сокет отбрасывается. Это означает, что клиент может определить конец ответа, получив 0 байт.9394But 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).9596Если вы не хотите завершать соединение, самое простое решение – сообщение фиксированной длины:9798```python99class mysocket:100    """только демонстрационный класс101      написан для ясности, а не для эффективности102    """103104    def __init__(self, sock=None):105        if sock is None:106            self.sock = socket.socket(107                            socket.AF_INET, socket.SOCK_STREAM)108            else:109                self.sock = sock110111    def connect(self, host, port):112        self.sock.connect((host, port))113114    def mysend(self, msg):115        totalsent = 0116        while totalsent < MSGLEN:117            sent = self.sock.send(msg[totalsent:])118            if sent == 0:119                raise RuntimeError("socket connection broken")120            totalsent = totalsent + sent121122    def myreceive(self):123        msg = ''124        while len(msg) < MSGLEN:125            chunk = self.sock.recv(MSGLEN-len(msg))126            if chunk == '':127                raise RuntimeError("socket connection broken")128            msg = msg + chunk129        return msg130```131132Код отправки здесь подходит почти для любой схемы обмена сообщениями – в Python вы отправляете строки и можете использовать `len()` для определения их длины (даже если в них есть встроенные символы `\0`). В основном сложнее становится код получения. (А в C ненамного хуже, только нельзя использовать `strlen`, если сообщение содержит встроенные `\0`.)133134Самый простой способ улучшения – сделать первый символ сообщения индикатором типа сообщения, а тип определяет длину. Теперь у вас два `recv`: первый – чтобы получить (хотя бы) этот первый символ и узнать длину, а второй – в цикле, чтобы получить остальное. Если вы решите пойти по пути разделителей, вы будете получать данные произвольными порциями (4096 или 8192 часто хорошо соответствуют размеру сетевых буферов) и искать разделитель в полученном.135136Стоит учитывать одно осложнение: если ваш протокол общения допускает отправку нескольких сообщений подряд (без какого-либо ответа), и вы передаёте `recv` произвольный размер порции, вы можете прочитать начало следующего сообщения. Его нужно будет отложить и сохранить, пока оно не понадобится.137138Добавление длины в начало сообщения (например, в виде 5 цифр) усложняет задачу, потому что (как ни странно) вы можете не получить все 5 символов за один `recv`. При тестировании это может сработать, но при высокой сетевой нагрузке ваш код очень быстро сломается, если не использовать два цикла `recv` – первый для определения длины, второй для получения данных сообщения. Неприятно. К тому же вы обнаружите, что `send` не всегда успевает отправить всё за один раз. И несмотря на то, что вы это прочитали, рано или поздно вы на этом обожжётесь!139140В целях экономии места, укрепления вашего характера (и сохранения моих конкурентных позиций) эти улучшения оставлены читателю в качестве упражнения. Перейдём к завершению.141142### Двоичные данные143144Вполне возможно отправлять двоичные данные через сокет. Главная проблема в том, что не все машины используют одинаковые форматы для двоичных данных. Например, процессор Motorola представит 16-битное целое число со значением 1 как два шестнадцатеричных байта 00 01. Intel и DEC, однако, используют обратный порядок байтов – та же единица будет 01 00. Библиотеки сокетов имеют функции для преобразования 16- и 32-битных целых чисел: `ntohl, htonl, ntohs, htons`, где «n» означает *network* (сетевой), а «h» – *host* (хостовый), «s» – *short* (короткое), а «l» – *long* (длинное). Если сетевой порядок совпадает с хостовым, эти функции ничего не делают, но если машина использует обратный порядок байтов, они соответствующим образом меняют байты местами.145146В эпоху 32-битных машин ASCII-представление двоичных данных часто оказывается меньше двоичного. Это потому, что на удивление часто все эти длинные целые имеют значение 0 или, возможно, 1. Строка «0» занимает два байта, а двоичное представление – четыре. Конечно, это плохо сочетается с сообщениями фиксированной длины. Решения, решения…147148## Отключение149150Строго говоря, полагается использовать `shutdown` на сокете перед `close`. `shutdown` – это уведомление сокету на другом конце. В зависимости от переданного аргумента, оно может означать «Я больше не буду отправлять, но буду слушать» или «Я не слушаю, скатертью дорога!». Однако большинство библиотек сокетов так привыкли к тому, что программисты пренебрегают этим правилом этикета, что обычно `close` эквивалентно `shutdown(); close()`. Так что в большинстве ситуаций явный `shutdown` не требуется.151152Один из способов эффективного использования `shutdown` – в обмене, подобном HTTP. Клиент отправляет запрос, а затем выполняет `shutdown(1)`. Это говорит серверу: «Клиент закончил отправку, но всё ещё может принимать». Сервер может обнаружить «EOF» по получению 0 байт. Он может считать, что получил полный запрос. Сервер отправляет ответ. Если `send` завершается успешно, то клиент действительно всё ещё принимал.153154Python идёт ещё дальше в автоматическом закрытии и говорит, что когда сокет собирается сборщиком мусора, он автоматически выполнит `close`, если это необходимо. Но полагаться на это – очень плохая привычка. Если ваш сокет просто исчезнет без выполнения `close`, сокет на другом конце может зависнуть на неопределённое время, думая, что вы просто медлите. *Пожалуйста*, `close` свои сокеты, когда закончите.155156### Когда сокеты умирают157158Пожалуй, худшее в использовании блокирующих сокетов – это то, что происходит, когда другая сторона резко обрывает соединение (без выполнения `close`). Ваш сокет, скорее всего, зависнет. TCP – надёжный протокол, и он будет ждать очень, очень долго, прежде чем откажется от соединения. Если вы используете потоки, весь поток по сути мёртв. С этим мало что можно сделать. Пока вы не делаете глупостей, например, не удерживаете блокировку во время блокирующего чтения, поток не потребляет много ресурсов. *Не* пытайтесь убить поток – отчасти потоки эффективнее процессов именно потому, что избегают накладных расходов, связанных с автоматическим освобождением ресурсов. Другими словами, если вам удастся убить поток, весь процесс, скорее всего, будет испорчен.159160## Неблокирующие сокеты161162Если вы поняли предыдущее, вы уже знаете большую часть того, что нужно знать о механике использования сокетов. Вы по-прежнему будете использовать те же вызовы, во многом теми же способами. Просто если делать всё правильно, ваше приложение будет почти вывернуто наизнанку.163164В Python используется `socket.setblocking(0)`, чтобы сделать сокет неблокирующим. В C это сложнее (например, нужно выбирать между вариантом из BSD `O_NONBLOCK` и почти неотличимым вариантом из Posix `O_NDELAY`, который полностью отличается от `TCP_NODELAY`), но идея та же. Это делается после создания сокета, но до его использования. (На самом деле, если вы безумны, можно переключаться туда и обратно.)165166Основное механическое отличие в том, что `send`, `recv`, `connect` и `accept` могут вернуться, ничего не сделав. У вас есть (конечно) несколько вариантов. Можно проверять коды возврата и ошибки и, в общем, свести себя с ума. Если не верите, попробуйте как-нибудь. Ваше приложение станет большим, багованным и будет жрать процессор. Так что давайте пропустим тупые решения и сделаем правильно.167168Используйте `select`.169170В C программирование `select` довольно сложное. В Python это проще пареной репы, но достаточно близко к версии на C, так что если вы понимаете `select` в Python, у вас будет мало проблем с ним в C:171172```python173ready_to_read, ready_to_write, in_error = \174               select.select(175                  potential_readers,176                  potential_writers,177                  potential_errs,178                  timeout)179```180181В `select` передаются три списка: первый содержит все сокеты, которые вы, возможно, хотите попробовать читать; второй – все сокеты, в которые вы, возможно, хотите писать; и последний (обычно оставляется пустым) – те, в которых нужно проверять ошибки. Стоит отметить, что сокет может находиться более чем в одном списке. Вызов `select` блокирующий, но можно задать тайм-аут. Обычно это разумно – задать достаточно большой тайм-аут (например, минуту), если нет веских причин поступать иначе.182183В ответ вы получите три списка. Они содержат сокеты, которые действительно доступны для чтения, записи или находятся в состоянии ошибки. Каждый из этих списков является подмножеством (возможно, пустым) соответствующего списка, который вы передали.184185Если сокет находится в списке readable на выходе, можно быть настолько уверенным, насколько это вообще возможно в нашем деле, что `recv` на этом сокете вернёт *что-нибудь*. То же самое для списка writable. Вы сможете отправить *что-нибудь*. Возможно, не всё, что хотите, но *что-нибудь* лучше, чем ничего. (На самом деле, любой достаточно здоровый сокет будет отмечен как writable – это просто означает, что доступно место в исходящем сетевом буфере.)186187Если у вас есть «серверный» сокет, поместите его в список potential\_readers. Если он оказывается в списке readable, ваш `accept` (почти наверняка) сработает. Если вы создали новый сокет для `connect` к кому-то другому, поместите его в список potential\_writers. Если он появляется в списке writable, есть хороший шанс, что соединение установлено.188189Одна очень неприятная проблема с `select`: если где-то в этих входных списках сокетов есть сокет, который неожиданно завершился с ошибкой, то `select` завершится сбоем. Тогда необходимо пройтись по каждому сокету во всех этих списках и выполнить `select([sock],[],[],0)`, пока не найдётся проблемный. Тайм-аут 0 означает, что это не займёт много времени, но это коряво.190191На самом деле, `select` может быть полезен даже с блокирующими сокетами. Это один из способов определить, будет ли блокировка: сокет помечается как readable, когда в буферах что-то есть. Однако это всё равно не помогает с проблемой определения, закончила ли другая сторона или просто занята чем-то другим.192193**Предупреждение о переносимости**: В Unix `select` работает как с сокетами, так и с файлами. Не пытайтесь делать это в Windows. В Windows `select` работает только с сокетами. Также обратите внимание, что в C многие более продвинутые опции сокетов в Windows реализованы иначе. На самом деле, в Windows для работы с сокетами обычно используются потоки (они работают очень хорошо). Примите как факт: если нужна производительность, код в Windows будет сильно отличаться от кода в Unix.194195### Производительность196197Несомненно, самый быстрый код для работы с сокетами использует неблокирующие сокеты и select для мультиплексирования. Можно собрать нечто, что насытит LAN-соединение без какой-либо нагрузки на процессор.198199Проблема в том, что приложение, написанное таким образом, не может заниматься почти ничем другим – оно должно быть постоянно готово перемещать байты. Если же приложение должно делать что-то помимо этого, то потоки являются оптимальным решением (и использование неблокирующих сокетов будет быстрее, чем блокирующих).200201Наконец, стоит помнить, что хотя блокирующие сокеты несколько медленнее неблокирующих, во многих случаях они являются «правильным» решением. В конце концов, если приложение управляется данными, которые оно получает через сокет, нет особого смысла усложнять логику, чтобы оно ожидало `select` вместо `recv`.202