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

Руководство по программированию сокетовSocket Programming HOWTO

Автор:Gordon McMillan

Аннотация

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

СокетыSockets

Сокеты используются практически повсеместно, но при этом являются одной из наиболее неправильно понимаемых технологий. Это общий обзор сокетов с высоты 10 000 футов. Это не совсем учебник – вам всё равно придётся поработать, чтобы всё заработало. Он не охватывает все тонкости (а их много), но, надеюсь, даст достаточно знаний, чтобы начать их прилично использовать.

Далее речь пойдёт только о сокетах INET, но они составляют не менее 99% используемых сокетов. И только о потоковых сокетах – если вы действительно не знаете, что делаете (в таком случае этот HOWTO не для вас!), вы получите лучшую производительность от потокового сокета, чем от чего-либо ещё. Будет предпринята попытка прояснить, что такое сокет, а также дать некоторые рекомендации по работе с блокирующими и неблокирующими сокетами. Но начать стоит с блокирующих сокетов. Необходимо понять, как они работают, прежде чем переходить к неблокирующим.

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

ИсторияHistory

Среди различных форм IPC сокеты, безусловно, самые популярные. На любой платформе, вероятно, существуют другие, более быстрые формы IPC, но для межплатформенного взаимодействия сокеты – это практически единственный доступный вариант.

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

Создание сокетаCreating a Socket

Если говорить приблизительно, то когда вы щёлкнули по ссылке, которая привела вас на эту страницу, ваш браузер сделал примерно следующее:

# создать INET-сокет для потока данных (STREAM)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# теперь подключиться к веб-серверу на порту 80 – обычный порт HTTP
s.connect(("www.python.org", 80))

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

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

# создать INET-сокет для потока данных (STREAM)
serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# привязать сокет к публичному хосту и известному порту
serversocket.bind((socket.gethostname(), 80))
# стать серверным сокетом
serversocket.listen(5)

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

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

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

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

while True:
    # принимать подключения извне
    (clientsocket, address) = serversocket.accept()
    # теперь выполнить действия с clientsocket
    # в данном случае предполагается, что это многопоточный сервер
    ct = client_thread(clientsocket)
    ct.run()

There’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 clientsockets 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.

Межпроцессное взаимодействиеIPC

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

См. также

Модуль multiprocessing интегрирует кроссплатформенный IPC в высокоуровневый API.

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

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

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

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

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

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

But 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).

Если вы не хотите завершать соединение, самое простое решение – сообщение фиксированной длины:

class mysocket:
    """только демонстрационный класс
      написан для ясности, а не для эффективности
    """

    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):
        msg = ''
        while len(msg) < MSGLEN:
            chunk = self.sock.recv(MSGLEN-len(msg))
            if chunk == '':
                raise RuntimeError("socket connection broken")
            msg = msg + chunk
        return msg

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

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

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

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

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

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

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

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

Отключение Disconnecting

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

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

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

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

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

Неблокирующие сокеты Non-blocking Sockets

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

В 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 блокирующий, но можно задать тайм-аут. Обычно это разумно – задать достаточно большой тайм-аут (например, минуту), если нет веских причин поступать иначе.

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

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

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

Одна очень неприятная проблема с select: если где-то в этих входных списках сокетов есть сокет, который неожиданно завершился с ошибкой, то select завершится сбоем. Тогда необходимо пройтись по каждому сокету во всех этих списках и выполнить select([sock],[],[],0), пока не найдётся проблемный. Тайм-аут 0 означает, что это не займёт много времени, но это коряво.

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

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

ПроизводительностьPerformance

Несомненно, самый быстрый код для работы с сокетами использует неблокирующие сокеты и select для мультиплексирования. Можно собрать нечто, что насытит LAN-соединение без какой-либо нагрузки на процессор.

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

Наконец, стоит помнить, что хотя блокирующие сокеты несколько медленнее неблокирующих, во многих случаях они являются «правильным» решением. В конце концов, если приложение управляется данными, которые оно получает через сокет, нет особого смысла усложнять логику, чтобы оно ожидало select вместо recv.