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

Автор:Gordon McMillan

Аннотация

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

Сокеты

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

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

История

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

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

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

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

# создание INET, ПОТОЧНОГО сокета
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# теперь подключимся к веб-серверу через порт 80 - обычный http порт
s.connect(("www.python.org", 80))

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

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

# создание INET, ПОТОЧНОГО сокета
serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# привязать сокет к общедоступному хосту и общеизвестному порту
serversocket.bind((socket.gethostname(), 80))
# стать сокетом сервера
serversocket.listen(5)

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

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

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

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

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

Есть на самом деле 3 общих пути, которыми эта петля могла работать - посылка нити, чтобы обращаться с clientsocket, создать новый процесс, чтобы обращаться с clientsocket или реструктурировать это приложение, чтобы использовать сокета неблокирования и мультиплекс между нашим «сервером» сокет и любым активным clientsockets, используя select. Больше об этом позже. Важная вещь понять теперь является этим: это - весь «сервер», который делает сокет. Это не посылает данных. Это не получает данных. Это просто производит «клиента» сокеты. Каждый clientsocket создан в ответ на некоторого «клиента» другой сокет, делающий connect() хозяину и порту, с которым мы связаны. Как только мы создали это clientsocket, мы возвращаемся к прислушиванию к большему количеству связей. Эти два «клиента» свободны флиртовать с ним - они используют некоторый динамично ассигнованный порт, который будет переработан, когда разговор закончится.

IPC

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

См.также

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

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

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

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

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

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

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

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

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

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):
        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, если сообщение включило \0s.)

Самое простое расширение - сделать первый символ выходного документа индикатором вида выходного документа и определить длину вида. Теперь у вас есть два recvs - первый, чтобы получить (по крайней мере), тот первый символ, таким образом, вы можете искать длину и второе в петле, чтобы получить остальных. Если вы решите пойти разграниченным путем, то вы будете получать в некотором произвольном размере чанк, (4096 или 8192 часто является хорошим совпадением для размеров сетевого буфера) и сканирование полученных данных для разделителя.

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

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

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

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

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

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

Разъединение

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

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

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

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

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

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

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

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

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

Используйте 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 на том сокет возвратит что-то. Та же идея для перезаписываемого списка. Вы будете в состоянии послать что-то. Возможно, не все вы хотите, но что-то лучше чем ничего. (На самом деле любой довольно здоровый сокет возвратится как перезаписываемый - это просто означает, что сетевое буферное место за границу свободное.)

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

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

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