Spec-Zone.ru › Python 3.8

Программирование сокетов HOWTO

Автор

Гордон МакМиллан

Аннотация

Сокеты используются практически везде, но являются одной из самых неправильно понятых технологий. Это обзор сокетов на высоком уровне. Это не учебник — вам все равно придется поработать, чтобы заставить их работать. Он не охватывает все тонкости (а их много), но, я надеюсь, он даст вам достаточно информации, чтобы начать использовать их должным образом.

Сокеты

Я буду говорить только о сокетах INET (т.е. IPv4), так как они составляют не менее 99% всех используемых сокетов. И я буду говорить только о сокетах STREAM (т.е. TCP) — если вы не знаете, что делаете (в этом случае это HOWTO не для вас!), вы получите более качественное поведение и производительность от сокета STREAM, чем от любого другого типа. Я постараюсь прояснить загадку того, что такое сокет, а также дам некоторые советы по работе с блокирующими и неблокирующими сокетами. Но начну с блокирующих сокетов. Вам нужно знать, как они работают, прежде чем работать с неблокирующими сокетами.

Часть проблемы с пониманием этих вещей заключается в том, что «сокет» может означать несколько совершенно разных вещей в зависимости от контекста. Итак, сначала давайте разберемся с разницей между сокетом «клиента» — конечной точкой разговора, и сокетом «сервера», который больше похож на оператора коммутационной станции. Клиентское приложение (например, ваш браузер) использует исключительно сокеты «клиента»; веб-сервер, с которым оно взаимодействует, использует и сокеты «сервера», и сокеты «клиента».

История

Из различных форм Взаимодействия процессов сокеты являются наиболее популярными. На любой платформе, скорее всего, существуют другие формы Взаимодействия процессов, которые быстрее, но для кроссплатформенного взаимодействия сокеты — практически единственный вариант.

Они были изобретены в Беркли как часть BSD-варианта Unix. Они распространились как лесной пожар с появлением Интернета. По понятным причинам — сочетание сокетов с INET делает общение с произвольными машинами по всему миру невероятно простым (по крайней мере, по сравнению с другими схемами).

Создание сокета

Грубо говоря, когда вы щелкнули по ссылке, которая привела вас на эту страницу, ваш браузер сделал что-то вроде следующего:

# create an INET, STREAMing socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# now connect to the web server on port 80 - the normal http port
s.connect(("www.python.org", 80))

После того, как connect завершится, сокет s может быть использован для отправки запроса на получение текста страницы. Тот же сокет прочитает ответ и затем будет уничтожен. Да, уничтожен. Клиентские сокеты обычно используются только для одного обмена (или небольшого набора последовательных обменов).

Что происходит на веб-сервере, немного сложнее. Сначала веб-сервер создает «сокет сервера»:

# create an INET, STREAMing socket
serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# bind the socket to a public host, and a well-known port
serversocket.bind((socket.gethostname(), 80))
# become a server socket
serversocket.listen(5)

Несколько замечаний: мы использовали socket.gethostname() для того, чтобы сокет был доступен извне. Если бы мы использовали s.bind(('localhost', 80)) или s.bind(('127.0.0.1', 80)), у нас все равно был бы «сокет сервера», но доступный только внутри одной машины. s.bind(('', 80)) указывает, что к сокету можно получить доступ с любого адреса машины.

Еще один момент: порты с низкими номерами обычно зарезервированы для «известных» сервисов (HTTP, SNMP и т. д.). Если вы экспериментируете, используйте большой номер (4 цифры).

Наконец, аргумент функции listen сообщает библиотеке сокетов, что мы хотим, чтобы она создавала очередь из максимального количества запросов подключения (обычно максимально возможного) перед отказом от внешних подключений. При правильном написании остального кода этого должно хватить.

Теперь, когда у нас есть «сокет сервера», прослушивающий порт 80, мы можем войти в основной цикл веб-сервера:

while True:
    # accept connections from outside
    (clientsocket, address) = serversocket.accept()
    # now do something with the clientsocket
    # in this case, we'll pretend this is a threaded server
    ct = client_thread(clientsocket)
    ct.run()

На самом деле, есть 3 основных способа работы этого цикла: запуск потока для обработки clientsocket, создание нового процесса для обработки clientsocket, или перестройка приложения для использования неблокирующих сокетов и мультиплексирования между нашим «сокетом сервера» и активными clientsocket с помощью select. Подробнее об этом позже. Важно понять следующее: это всё, что делает «сокет сервера». Он не отправляет данные. Он не получает данные. Он просто создает «сокеты клиентов». Каждый clientsocket создается в ответ на некоторое другое действие сокета «клиента» — подключение к хосту и порту, к которому мы привязаны. Как только мы создали этот clientsocket, мы возвращаемся к прослушиванию новых подключений. Два «клиента» свободно общаются — они используют динамически выделенный порт, который будет повторно использован при завершении разговора.

Взаимодействие процессов

Если вам нужно быстрое Взаимодействие процессов между двумя процессами на одной машине, вам следует использовать каналы или общую память. Если вы все же решите использовать сокеты AF_INET, привяжите «сокет сервера» к 'localhost'. На большинстве платформ это позволит обойти несколько уровней сетевого кода и будет значительно быстрее.

См. также

Библиотека multiprocessing интегрирует кроссплатформенное Взаимодействие процессов в API более высокого уровня.

Использование сокета

Первое, что следует отметить, — сокет «клиента» веб-браузера и сокет «клиента» веб-сервера идентичны. То есть, это «peer-to-peer» общение. Или, другими словами, как разработчик, вы должны будете определить правила этикета для разговора. Обычно, сокет connect начинает разговор, отправляя запрос или, возможно, сообщение о входа. Но это решение дизайна — это не правило сокетов.

Теперь есть два набора глаголов для коммуникации. Вы можете использовать send и recv, или вы можете преобразовать свой сокет клиента в подобный файлу объект и использовать read и write. Последний способ используется Java для представления сокетов. Я не буду об этом говорить, за исключением предупреждения о том, что вам необходимо использовать flush на сокетах. Это буферизованные «файлы», и распространённая ошибка — write что-то и затем read ответ. Без flush в этом процессе, вы можете ждать ответа вечно, потому что запрос может всё ещё находиться в буфере вывода.

Теперь мы переходим к главному камню преткновения сокетов — send и recv работают с сетевыми буферами. Они не обязательно обрабатывают все байты, которые вы им передаёте (или ожидаете от них), потому что их основная задача — работа с сетевыми буферами. В общем случае, они возвращают значение, когда связанные сетевые буферы заполнены (send) или опустошены (recv). Затем они сообщают вам, сколько байтов они обработались. Вам необходимо вызывать их повторно, пока ваше сообщение не будет полностью обработано.

Когда функция recv возвращает 0 байтов, это означает, что другая сторона закрыла (или в процессе закрытия) соединение. Вы больше не получите данных по этому соединению. Никогда. Вы можете успешно отправить данные; я расскажу об этом позже.

Протокол, подобный HTTP, использует сокет только для одной передачи. Клиент отправляет запрос, затем считывает ответ. Всё. Сокет удаляется. Это означает, что клиент может определить конец ответа, получив 0 байтов.

Но если вы планируете повторно использовать сокет для дальнейших переводов, вам необходимо осознать, что нет EOT на сокете. Повторяю: если сокет send или recv возвращает значение после обработки 0 байтов, соединение прервано. Если соединение не прервано, вы можете ждать на recv вечно, потому что сокет не скажет вам, что больше ничего нет для чтения (на данный момент). Теперь, если вы немного об этом подумаете, вы поймёте фундаментальную истину сокетов: сообщения должны иметь фиксированную длину (фу!), или быть ограничены разделителями (пожимаем плечами), или указывать, какой длины они (намного лучше), или заканчиваться закрытием соединения. Выбор полностью за вами, (но некоторые способы лучше других).

Предполагая, что вы не хотите прерывать соединение, простейшее решение — сообщение фиксированной длины:

class MySocket:
    """demonstration class only
      - coded for clarity, not efficiency
    """

    def __init__(self, sock=None):
        if sock is None:
            self.sock = socket.socket(
                            socket.AF_INET, socket.SOCK_STREAM)
        else:
            self.sock = sock

    def connect(self, host, port):
        self.sock.connect((host, port))

    def mysend(self, msg):
        totalsent = 0
        while totalsent < MSGLEN:
            sent = self.sock.send(msg[totalsent:])
            if sent == 0:
                raise RuntimeError("socket connection broken")
            totalsent = totalsent + sent

    def myreceive(self):
        chunks = []
        bytes_recd = 0
        while bytes_recd < MSGLEN:
            chunk = self.sock.recv(min(MSGLEN - bytes_recd, 2048))
            if chunk == b'':
                raise RuntimeError("socket connection broken")
            chunks.append(chunk)
            bytes_recd = bytes_recd + len(chunk)
        return b''.join(chunks)

Отправляющий код здесь пригоден для почти любой схемы обмена сообщениями — в Python вы отправляете строки, и вы можете использовать len() для определения их длины (даже если в них есть встроенные \0 символы). В основном код получения становится сложнее. (И в C это ненамного хуже, кроме того, что вы не можете использовать strlen, если в сообщении есть встроенные \0 символы.)

Самое простое улучшение — сделать первый символ сообщения индикатором типа сообщения и определить длину по типу. Теперь у вас есть два recv — первый, чтобы получить (по крайней мере) первый символ, чтобы узнать длину, и второй в цикле, чтобы получить остальную часть. Если вы решили использовать разделители, вы будете получать данные в произвольном блоке размера, (4096 или 8192 часто хорошо подходят для размеров сетевых буферов), и проверять полученные данные на наличие разделителей.

Один нюанс: если ваш протокол общения допускает отправку нескольких сообщений друг за другом (без каких-либо ответов), и вы передаёте recv произвольный размер блока, вы можете получить часть следующего сообщения. Вам нужно будет отложить его и сохранить, пока он не понадобится.

Префикс сообщения его длиной (например, 5 числовых символов) усложняется, потому что (поверьте или нет), вы можете не получить все 5 символов в одном recv. В простых тестах вы справитесь, но при высокой сетевой нагрузке ваш код очень быстро сломается, если вы не будете использовать два цикла recv — первый, чтобы определить длину, второй, чтобы получить данные сообщения. Плохо. Это также момент, когда вы обнаружите, что send не всегда удаётся избавиться от всего за один проход. И, несмотря на прочтение этого, вы рано или поздно об этом споткнётесь!

В интересах экономии места, построения вашего персонажа (и сохранения моего конкурентного положения), эти улучшения оставлены в качестве упражнения для читателя. Перейдём к очистке.

Двоичные данные

Совершенно возможно отправлять двоичные данные через сокет. Основная проблема заключается в том, что не все машины используют одинаковые форматы для двоичных данных. Например, процессор Motorola будет представлять 16-битное целое число со значением 1 как два шестнадцатеричных байта 00 01. Intel и DEC, однако, используют обратный порядок байтов — то же самое 1 будет 01 00. Библиотеки сокетов имеют функции для преобразования 16- и 32-битных целых чисел — ntohl, htonl, ntohs, htons где «n» означает сетевой, а «h» — хостовый, «s» — короткий, а «l» — длинный. Если сетевой порядок совпадает с хостовым, эти функции ничего не делают, но если машина использует обратный порядок байтов, они меняют байты соответствующим образом.

В наши дни 32-битных машин ASCII представление двоичных данных часто меньше, чем двоичное представление. Это потому что удивительно часто все эти значения long равны 0 или, может быть, 1. Строка "0" будет занимать два байта, а двоичное значение — четыре. Конечно, это не очень хорошо сочетается с сообщениями фиксированной длины. Решения, решения.

Отключение

Строго говоря, вы должны использовать shutdown на сокете перед тем, как close его. shutdown — это рекомендация для сокета на другом конце. В зависимости от переданного аргумента, это может означать «Я больше не буду отправлять, но буду всё ещё принимать», или «Я не буду принимать, до свидания!». Однако большинство библиотек сокетов настолько привыкли к тому, что программисты пренебрегают этим правилом этикета, что обычно close эквивалентно shutdown(); close(). Поэтому в большинстве случаев явное shutdown не требуется.

Один из способов эффективного использования shutdown — в обмене данных, подобном HTTP. Клиент отправляет запрос, а затем выполняет shutdown(1). Это сообщает серверу «Клиент закончил отправку, но может всё ещё принимать». Сервер может определить «EOF» по приёму 0 байтов. Он может предположить, что получил весь запрос. Сервер отправляет ответ. Если send завершится успешно, значит, клиент всё ещё принимал.

Python продвигает автоматическое закрытие ещё дальше и заявляет, что при сборке мусора сокета он автоматически выполнит close если это необходимо. Но полагаться на это — очень плохая привычка. Если ваш сокет просто исчезает, не выполнив close, сокет на другом конце может зависнуть на неопределённое время, полагая, что вы просто медлителен. Пожалуйста close ваши сокеты, когда закончите.

Когда сокеты умирают

Вероятно, худшее, что может произойти при использовании блокирующих сокетов, — это то, что происходит, когда другая сторона резко отключается (без выполнения close). Ваш сокет, скорее всего, зависнет. TCP — надёжный протокол, и он будет долго ждать, прежде чем откажется от соединения. Если вы используете потоки, весь поток фактически мёртв. Вы мало что можете сделать. Пока вы не делаете что-то глупое, например, удерживаете блокировку во время блокирующего чтения, поток не потребляет много ресурсов. Не пытайтесь убить поток — одна из причин, по которой потоки эффективнее процессов, заключается в том, что они избегают накладных расходов, связанных с автоматической переработкой ресурсов. Другими словами, если вы всё же попытаетесь убить поток, весь ваш процесс, вероятно, сломается.

Неблокирующие сокеты

Если вы поняли предыдущее, то вы уже знаете большую часть того, что нужно знать о механике использования сокетов. Вы по-прежнему будете использовать те же самые вызовы, практически теми же самыми способами. Просто, если вы сделаете всё правильно, ваше приложение будет почти перевёрнутым наизнанку.

В Python вы используете socket.setblocking(0) для того, чтобы сделать его неблокирующим. В C это более сложно (во-первых, вам нужно выбрать между вариантом BSD O_NONBLOCK и почти неотличимым вариантом POSIX O_NDELAY, который полностью отличается от TCP_NODELAY), но это точно та же самая идея. Вы делаете это после создания сокета, но до его использования. (На самом деле, если вы сумасшедшие, вы можете переключаться туда-сюда.)

Основное механическое различие заключается в том, что send, recv, connect и accept могут вернуть значение без выполнения каких-либо действий. У вас есть (конечно) ряд вариантов. Вы можете проверять код возврата и коды ошибок и вообще сводить себя с ума. Если вы мне не верите, попробуйте когда-нибудь. Ваше приложение станет большим, глючным и будет забирать много ЦП. Поэтому давайте пропустим решения, убивающие мозг, и сделаем всё правильно.

Используйте select.

В C, кодирование select довольно сложно. В Python это проще простого, но это достаточно близко к версии C, что если вы понимаете select в Python, вам будет легко с этим в C:

ready_to_read, ready_to_write, in_error = \
               select.select(
                  potential_readers,
                  potential_writers,
                  potential_errs,
                  timeout)

Вы передаёте select три списка: первый содержит все сокеты, которые вы могли бы захотеть попробовать прочитать; второй — все сокеты, которые вы могли бы захотеть попробовать написать к ним, а последний (обычно оставленный пустым) — сокеты, для которых вы хотите проверить ошибки. Обратите внимание, что сокет может находиться в более чем одном списке. Вызов select блокирующий, но вы можете указать таймаут. Это, как правило, разумно — установить большой таймаут (например, минуту), если нет веских причин поступить иначе.

В ответ вы получите три списка. Они содержат сокеты, которые фактически читабельны, записываемы и содержат ошибки. Каждый из этих списков является подмножеством (возможно, пустым) соответствующего списка, который вы передали.

Если сокет находится в выходном списке читаемых, вы можете быть практически уверены, что вызов recv для этого сокета вернёт что-то. Та же идея для списка записываемых. Вы сможете отправить что-то. Возможно, не всё, что вы хотите, но что-то лучше, чем ничего. (На самом деле, любой относительно здоровый сокет вернёт значение как записываемый — это просто означает, что доступно пространство буфера для исходящих сетевых данных.)

Если у вас есть сокет «сервера», поместите его в список потенциальных читателей. Если он появится в списке читаемых, ваш вызов accept (почти наверняка) сработает. Если вы создали новый сокет для connect с кем-то другим, поместите его в список потенциальных писателей. Если он окажется в списке записываемых, у вас есть неплохие шансы на то, что он подключился.

На самом деле, select может быть полезным даже с блокирующими сокетами. Это один из способов определить, будете ли вы блокироваться — сокет возвращается как читаемый, когда в буферах есть что-то. Однако это всё ещё не помогает с проблемой определения того, закончил ли другой конец или просто занят чем-то ещё.

Предупреждение о переносимости: В Unix select работает как с сокетами, так и с файлами. Не пробуйте это в Windows. В Windows select работает только с сокетами. Также обратите внимание, что в C многие из более продвинутых параметров сокета обрабатываются по-разному в Windows. На самом деле, в Windows я обычно использую потоки (которые работают очень, очень хорошо) с моими сокетами.

© 2001–2022 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.8/howto/sockets.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API