Как сделать сокет

Добавил пользователь Евгений Кузнецов
Обновлено: 10.09.2024

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

Содержание.

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

Что такое сокет и как он создается?

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

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

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

Следует отметить пару вещей: в коде выше использовалась функция socket.gethostname() , чтобы сокет был виден внешнему миру. Если использовать вызов serv_sock.bind(('localhost', 443)) или serv_sock.bind(('127.0.0.1', 443)) , то серверный сокет был бы виден только локальной машине. Вызов serv_sock.bind(('', 443)) указывает, что сокет доступен по любому адресу.

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

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

Существует 3 основных способа, которыми этот цикл может работать:

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

Подробнее об этом позже. Сейчас важно понять, что это все, что делает серверный сокет. Он не отправляет и не получает никаких данных. Он просто воспроизводит/создает клиентские сокеты. Каждый клиентский сокет создается в ответ на то, что какой-то новый клиентский сокет выполняет подключение sock.connect() к серверу на определенный хост и порт. На этот запрос, сервер создает новый клиентский сокет, и как только он это сделает то сразу возвращается к прослушиванию следующих подключений. Два клиента могут свободно общаться, например на каком-нибудь динамически выделенном порту, который будет закрыт после общения.

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

Если необходим быстрый IPC между двумя процессами на одной машине, то следует изучить каналы Pipe() или общую память (объекты Value() и Array() ). Если все же решите использовать socket.AF_INET сокеты, то необходимо привязать серверный сокет к localhost . На большинстве платформ это позволит сократить несколько уровней сетевого кода и будет работать немного быстрее.

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

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

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

Для приема/передачи данных можно использовать методы объекта сокета Socket.send() и Socket.recv() , а можно превратить клиентский сокет в файловый объект и использовать чтение/запись. По поводу использования сокета, как файлового объекта, необходимо сделать предупреждение, что в сокетах, при выполнении записи, нужно использовать вызов file.flush() . Сокеты имеют дело с буферизованными "файлами" и распространенной ошибкой является - записать что-то и не вызвать file.flush() , а затем перейти в режим чтения ответа. При этом можно бесконечно долго ждать ответа, т. к. записанные данные все еще могут оставаться в выходном буфере.

Но если в планах повторно использовать открытый сокет для каких-то задач, то нужно понимать, что в сокетах нет EOT - "end of trensfer" (конец файла). ЕЩЕ РАЗ: если сокет после вызова методов Socket.send() или Socket.recv() возвращает 0 байтов, то соединение было разорвано. Если соединение не было разорвано, то можно вечно ждать получения данных вызовом Socket.recv() , т. к. сокет не может сказать, что читать больше нечего.

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

Прием/передача двоичных данных.

Вполне возможно отправлять двоичные данные через сокет. Основная проблема заключается в том, что не все машины используют одни и те же форматы для двоичных данных. Например, чип Motorola будет представлять 16-битное целое число, например 1 в виде двух шестнадцатеричных байтов - 00 01. Intel и DEC переворачивают байты - то же самое число 1 здесь будет выглядеть как 01 00. Модуль socket имеет функции для преобразования 16 и 32-битных целых чисел - socket.ntohl() , socket.htonl() , socket.ntohs() и socket.htons() , где в названиях первая буква означает n - сеть, а h - хост, а последняя s - короткий, а l - длинный. Там, где сетевой порядок является порядком хоста, функции ничего делать не будут, но там, где машина перевернула байты, они соответствующим образом все поменяют.

Закрытие соединения сокета.

Строго говоря, сначала необходимо использовать вызов объекта сокета shutdown() , прежде чем закрыть его командой Socket.close() . Вызов Socket.shutdown() - это предупреждение для сокета на другом конце. В зависимости от аргумента, который передавать, это может означать "Я больше не буду отправлять, но я все равно буду слушать" или "Я не слушаю, мне по барабану". Разработчики библиотек сокетов настолько привыкли к тому, что программисты пренебрегают этим элементом этикета, что у некоторых, обычный вызов Socket.close() - означает последовательность вызовов: Socket.shutdown() ; Socket.close() . Поэтому в большинстве ситуаций явный вызов Socket.shutdown() не требуется.

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

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

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

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

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

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

Чтобы сделать сокет неблокирующим, в Python используют вызов Socket.setblocking(False) . Вызов делается после создания сокета, но перед его использованием. Если программист не очень умный, то скорее всего попытается переключаться туда и обратно (с блокирующего сокета на неблокирующий).

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

Одним из возможных решений является делегирование работы с клиентами отдельным потокам. Однако создание потоков и переключение контекстов между ними на самом деле не является дешевой операцией. Для решения этой проблемы существует так называемый асинхронный способ работы с сокетами. Основная идея состоит в том, чтобы делегировать поддержание состояния сокета операционной системе и позволить ей уведомлять программу, когда есть что-то для чтения из сокета или когда он готов к записи. Для этого можно использовать вызов операционной системы select , подробнее о нем можно посмотреть командой терминала Unix $ man select

В Python такой вызов сделать совсем несложно, для этой цели используйте встроенный модуль select , в частности вызов select.select() :

Здесь передается в select.select() три списка:

  • potential_readers содержит все сокеты, которые нужно прочитать,
  • potential_writers содержит все сокеты, в которые надо что-то записать,
  • potential_errs - которые нужно проверять на наличие ошибок (обычно оставляют пустым).

Следует отметить, что один сокет может входить в разные списки. Вызов select.select() блокируется, но можно задать ему таймаут. Как правило, это разумный поступок - дайте ему длительный таймаут (скажем, минуту), если нет веских причин поступить иначе.

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

Если есть "серверный" сокет, то поместим его в список potential_readers . Если он появится в списке ready_to_read , то вызов .accept почти наверняка сработает. Если сервер создал новый сокет для подключения, то поместим его в список potential_writers . Если он отображается в списке ready_to_write , то есть неплохие шансы, что он подключился.

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

Пример асинхронного сервера с вызовом select.select() .

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

Так работают сокеты на низком уровне. В большинстве случаев нет необходимости реализовывать логику на таком низком уровне. Рекомендуется использовать некоторые абстракции более высокого уровня, такие как | Twisted |, | Tornado | или | ZeroMQ |, в зависимости от ситуации.

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

Принципы сокетов¶

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

Каждый сокет имеет свой адрес. ОС семейства UNIX могут поддерживать много типов адресов, но обязательными являются INET-адрес и UNIX-адрес. Если привязать сокет к UNIX-адресу, то будет создан специальный файл (файл сокета) по заданному пути, через который смогут сообщаться любые локальные процессы путём чтения/записи из него (см. Доменный сокет Unix). Сокеты типа INET доступны из сети и требуют выделения номера порта.

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

Основные функции¶

socket()¶

Создаёт конечную точку соединения и возвращает файловый дескриптор. Принимает три аргумента:

domain указывающий семейство протоколов создаваемого сокета

  • AF_INET для сетевого протокола IPv4
  • AF_INET6 для IPv6
  • AF_UNIX для локальных сокетов (используя файл)

type

  • SOCK_STREAM (надёжная потокоориентированная служба (сервис) или потоковый сокет)
  • SOCK_DGRAM (служба датаграмм или датаграммный сокет)
  • SOCK_RAW (Сырой сокет — сырой протокол поверх сетевого уровня).

protocol

Протоколы обозначаются символьными константами с префиксом IPPROTO_* (например, IPPROTO_TCP или IPPROTO_UDP). Допускается значение protocol=0 (протокол не указан), в этом случае используется значение по умолчанию для данного вида соединений.

Функция возвращает -1 в случае ошибки. Иначе, она возвращает целое число, представляющее присвоенный дескриптор.

Пример на Python

Связывает сокет с конкретным адресом. Когда сокет создается при помощи socket(), он ассоциируется с некоторым семейством адресов, но не с конкретным адресом. До того как сокет сможет принять входящие соединения, он должен быть связан с адресом. bind() принимает три аргумента:

  1. sockfd — дескриптор, представляющий сокет при привязке
  2. serv_addr — указатель на структуру sockaddr, представляющую адрес, к которому привязываем.
  3. addrlen — поле socklen_t, представляющее длину структуры sockaddr.

Возвращает 0 при успехе и -1 при возникновении ошибки.

Пример на Python

Автоматическое получение имени хоста.

listen()¶

Подготавливает привязываемый сокет к принятию входящих соединений. Данная функция применима только к типам сокетов SOCK_STREAM и SOCK_SEQPACKET. Принимает два аргумента:

  1. sockfd — корректный дескриптор сокета.
  2. backlog — целое число, означающее число установленных соединений, которые могут быть обработаны в любой момент времени. Операционная система обычно ставит его равным максимальному значению.

После принятия соединения оно выводится из очереди. В случае успеха возвращается 0, в случае возникновения ошибки возвращается -1.

Пример на Python

accept()¶

Используется для принятия запроса на установление соединения от удаленного хоста. Принимает следующие аргументы:

  1. sockfd — дескриптор слушающего сокета на принятие соединения.
  2. cliaddr — указатель на структуру sockaddr, для принятия информации об адресе клиента.
  3. addrlen — указатель на socklen_t, определяющее размер структуры, содержащей клиентский адрес и переданной в accept(). Когда accept() возвращает некоторое значение, socklen_t указывает сколько байт структуры cliaddr использовано в данный момент.

Функция возвращает дескриптор сокета, связанный с принятым соединением, или -1 в случае возникновения ошибки.

Пример на Python

connect()¶

Устанавливает соединение с сервером.

Некоторые типы сокетов работают без установления соединения, это в основном касается UDP-сокетов. Для них соединение приобретает особое значение: цель по умолчанию для посылки и получения данных присваивается переданному адресу, позволяя использовать такие функции как send() и recv() на сокетах без установления соединения.

Загруженный сервер может отвергнуть попытку соединения, поэтому в некоторых видах программ необходимо предусмотреть повторные попытки соединения.

Возвращает целое число, представляющее код ошибки: 0 означает успешное выполнение, а -1 свидетельствует об ошибке.

Пример на Python

Передача данных¶

Для передачи данных можно пользоваться стандартными функциями чтения/записи файлов read и write, но есть специальные функции для передачи данных через сокеты:

Именованные каналы пригодны для организации межпроцессного взаимодействия как в случае процессов, выполняющихся на одной и той же системе, так и в случае процессов, выполняющихся на компьютерах, связанных друг с другом локальной или глобальной сетью. Эти возможности были продемонстрированы на примере клиент-серверной системы, разработанной в главе 11, начиная с программы 11.2.

Однако как именованные каналы, так и почтовые ящики (в отношении которых для простоты мы будем использовать далее общий термин — "именованные каналы", если различия между ними не будут играть существенной роли) обладают тем недостатком, что они не являются промышленным стандартом. Это обстоятельство усложняет перенос программ наподобие тех, которые рассматривались в главе 11, в системы, не принадлежащие семейству Windows, хотя именованные каналы не зависят от протоколов и могут выполняться поверх многих стандартных промышленных протоколов, например TCP/IP.

Возможность взаимодействия с другими системами обеспечивается в Windows поддержкой сокетов (sockets) Windows Sockets — совместимого и почти точного аналога сокетов Berkeley Sockets, де-факто играющих роль промышленного стандарта. В этой главе использование API Windows Sockets (или "Winsock") показано на примере модифицированной клиент-серверной системы из главы 11. Результирующая система способна функционировать в глобальных сетях, использующих протокол TCP/IP, что, например, позволяет серверу принимать запросы от клиентов UNIX или каких-либо других, отличных от Windows систем.

В данной главе указанная клиент-серверная система используется в качестве механизма демонстрации интерфейса Winsock, и в процессе того, как сервер будет модифицироваться, в него будут добавляться новые интересные возможности. В частности, нами будут впервые использованы точки входа DLL (глава 5) и внутрипроцессные серверы DLL. (Эти новые средства можно было включить уже в первоначальную версию программы в главе 11, однако это отвлекло бы ваше внимание от разработки основной архитектуры системы.) Наконец, дополнительные примеры покажут вам, как создаются безопасные реентерабельные многопоточные библиотеки.

Поскольку интерфейс Winsock должен соответствовать промышленным стандартам, принятые в нем соглашения о правилах присвоения имен и стилях программирования несколько отличаются от тех, с которыми мы сталкивались в процессе работы с описанными ранее функциями Windows. Строго говоря, Winsock API не является частью Win32/64. Кроме того, Winsock предоставляет дополнительные функции, не подчиняющиеся стандартам; эти функции используются лишь в случае крайней необходимости. Среди других преимуществ, обеспечиваемых Winsock, следует отметить улучшенную переносимость результирующих программ на другие системы.

Winsock API разрабатывался как расширение Berkley Sockets API для среды Windows и поэтому поддерживается всеми системами Windows. К преимуществам Winsock можно отнести следующее:

• Перенос уже имеющегося кода, написанного для Berkeley Sockets API, осуществляется непосредственно.

• Системы Windows легко встраиваются в сети, использующие как версию IPv4 протокола TCP/IP, так и постепенно распространяющуюся версию IPv6. Помимо всего остального, версия IPv6 допускает использование более длинных IP-адресов, преодолевая существующий 4-байтовый адресный барьер версии IPv4.

• Сокеты могут использоваться совместно с перекрывающимся вводом/выводом Windows (глава 14), что, помимо всего прочего, обеспечивает возможность масштабирования серверов при увеличении количества активных клиентов.

• Сокеты можно рассматривать как дескрипторы (типа HANDLE) файлов при использовании функций ReadFile и WriteFile и, с некоторыми ограничениями, при использовании других функций, точно так же, как в качестве дескрипторов файлов сокеты применяются в UNIX. Эта возможность оказывается удобной в тех случаях, когда требуется использование асинхронного ввода/вывода и портов завершения ввода/вывода.

• Существуют также дополнительные, непереносимые расширения.

Winsock API поддерживается библиотекой DLL (WS2_32.DLL), для получения доступа к которой следует подключить к программе библиотеку WS_232.LIB. Эту DLL следует инициализировать с помощью нестандартной, специфической для Winsock функции WSAStartup, которая должна быть первой из функций Winsock, вызываемых программой. Когда необходимость в использовании функциональных возможностей Winsock отпадает, следует вызывать функцию WSACleanup. Примечание. Префикс WSA означает "Windows Sockets asynchronous …" ("Асинхронный Windows Sockets …"). Средства асинхронного режима Winsock нами здесь не используются, поскольку при возникновении необходимости в выполнении асинхронных операций мы можем и будем использовать потоки.

int WSAStartup(WORD wVersionRequired, LPWSADATA ipWSAData);

Параметры

wVersionRequired — указывает старший номер версии библиотеки DLL, который вам требуется и который вы можете использовать. Как правило, версии 1.1 вполне достаточно для того, чтобы обеспечить любое взаимодействие с другими системами, в котором у вас может возникнуть необходимость. Тем не менее, во всех системах Windows, включая Windows 9x, доступна версия Winsock 2.0, которая и используется в приведенных ниже примерах. Версия 1.1 считается устаревшей и постепенно выходит из употребления.

Функция возвращает ненулевое значение, если запрошенная вами версия данной DLL не поддерживается.

Младший байт параметра wVersionRequired указывает основной номер версии, а старший байт — дополнительный. Обычно используют макрос MAKEWORD; таким образом, выражение MAKEWORD (2,0) представляет версию 2.0.

ipWSAData — указатель на структуру WSADATA, которая возвращает информацию о конфигурации DLL, включая старший доступный номер версии. О том, как интерпретировать ее содержимое, вы можете прочитать в материалах оперативной справки Visual Studio.

Чтобы получить более подробную информацию об ошибках, можно воспользоваться функцией WSAGetLastError, но для этой цели подходит также функция GetLastError, а также функция ReportError, разработанная в главе 2.

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

Инициализировав Winsock DLL, вы можете использовать стандартные (Berkeley Sockets) функции для создания сокетов и соединений, обеспечивающих взаимодействие серверов с клиентами или взаимодействие равноправных узлов сети между собой.

Используемый в Winsock тип данных SOCKET аналогичен типу данных HANDLE в Windows, и его даже можно применять совместно с функцией ReadFile и другими функциями Windows, требующими использования дескрипторов типа HANDLE. Для создания (или открытия) сокета служит функция socket.

SOCKET socket(int af, int type, int protocol);

Параметры

Тип данных SOCKET фактически определяется как тип данных int, потому код UNIX остается переносимым, не требуя привлечения типов данных Windows.

af — обозначает семейство адресов, или протокол; для указания протокола IP (компонент протокола TCP/IP, отвечающий за протокол Internet) следует использовать значение PF_INET (или AF_INET, которое имеет то же самое числовое значение, но обычно используется при вызове функции bind).

type — указывает тип взаимодействия: ориентированное на установку соединения (connection-oriented communication), или потоковое (SOCK_STREAM), и дейтаграммное (datagram communication) (SOCK_DGRAM), что в определенной степени сопоставимо соответственно с именованными каналами и почтовыми ящиками.

protocol — является излишним, если параметр af установлен равным AF_INET; используйте значение 0.

В случае неудачного завершения функция socket возвращает значение INVALID_SOCKET.

Winsock можно использовать совместно с протоколами, отличными от TCP/IP, указывая различные значения параметра protocol; мы же будем использовать только протокол TCP/IP.

Как и в случае всех остальных стандартных функций, имя функции socket не должно содержать прописных букв. Это является отходом от соглашений, принятых в Windows, и продиктовано необходимостью соблюдения промышленных стандартов.

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

Если не оговорено иное, типом сокетов в наших примерах всегда будет SOCK_STREAM. Сокеты типа SOCK_DGRAM рассматривается далее в этой главе.

Следующий шаг заключается в привязке сокета к его адресу и конечной точке (endpoint) (направление канала связи от приложения к службе). Вызов socket, за которым следует вызов bind, аналогичен созданию именованного канала. Однако не существует имен, используя которые можно было бы различать сокеты данного компьютера. Вместо этого в качестве конечной точки службы используется номер порта (port number). Любой заданный сервер может иметь несколько конечных точек. Прототип функции bind приводится ниже.

int bind(SOCKET s, const struct sockaddr *saddr, int namelen);

Параметры

s — несвязанный сокет, возвращенный функцией socket.

saddr — заполняется перед вызовом и задает протокол и специфическую для протокола информацию, как описано ниже. Кроме всего прочего, в этой структуре содержится номер порта.

namelen — присвойте значение sizeof (sockaddr).

В случае успешного выполнения функция возвращает значение 0, иначе SOCKET_ERROR. Структура sockaddr определяется следующим образом:

Первый член этой структуры, sa_family, обозначает протокол. Второй член, sa_data, зависит от протокола. Internet-версией структуры sa_data является структура sockaddr_in:

Обратите внимание на использование типа данных short integer для номера порта. Кроме того, номер порта и иная информация должны храниться с соблюдением подходящего порядка следования байтов, при котором старший байт помещается в крайней позиции справа (big-endian), чтобы обеспечивалась двоичная совместимость с другими системами. В структуре sin_addr содержится подструктура s_addr, заполняемая уже знакомым нам 4-байтовым IP-адресом, например 127.0.0.1, указывающим систему, чей запрос на образование соединения должен быть принят. Обычно удовлетворяются запросы любых систем, в связи с чем следует использовать значение INADDR_ANY, хотя этот символический параметр должен быть преобразован к корректному формату, как показано в приведенном ниже фрагменте кода.

Для преобразования текстовой строки с IP-адресом к требуемому формату можно использовать функцию inet_addr, поэтому член sin_addr.s_addr переменной sockaddr_in инициализируется следующим образом:

О связанном сокете, для которого определены протокол, номер порта и IP-адрес, иногда говорят как об именованном сокете (named socket).

Функция listen делает сервер доступным для образования соединения с клиентом. Аналогичной функции для именованных каналов не существует.

int listen(SOCKET s, int nQueueSize);

Параметр nQueueSize указывает число запросов на соединение, которые вы намерены помещать в очередь сокета. В версии Winsock 2.0 значение этого параметра не имеет ограничения сверху, но в версии 1.1 оно ограничено предельным значением SOMAXCON (равным 5).

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

Функция accept блокируется до тех пор, пока от клиента не поступит запрос соединения, после чего она возвращает новый сокет ввода/вывода. Хотя рассмотрение этого и выходит за рамки данной книги, возможно создание неблокирующихся сокетов, а в сервере (программа 12.2) для приема запроса используется отдельный поток, что позволяет создавать также неблокирующиеся серверы.

SOCKET accept(SOCKET s, LPSOCKADDR lpAddr, LPINT lpAddrLen);

Параметры

s — прослушивающий сокет. Чтобы перевести сокет в состояние прослушивания, необходимо предварительно вызвать функции socket, bind и listen.

lpAddr — указатель на структуру sockaddr_in, предоставляющую адрес клиентской системы.

lpAddrLen — указатель на переменную, которая будет содержать размер возвращенной структуры sockaddr_in. Перед вызовом функции accept эта переменная должна быть инициализирована значением sizeof(struct sockaddr_in).

Когда работа с сокетом закончена, его следует закрыть, вызвав функцию closesocket(SOCKET s). Сначала сервер закрывает сокет, созданный функцией accept, а не прослушивающий сокет, созданный с помощью функции socket. Сервер должен закрывать прослушивающий сокет только тогда, когда завершает работу или прекращает принимать клиентские запросы соединения. Даже если вы работаете с сокетом как с дескриптором типа HANDLE и используете функции ReadFile и WriteFile, уничтожить сокет одним только вызовом функции CloseHandle вам не удастся; для этого следует использовать функцию closesocket.

Ниже приводится фрагмент кода, показывающий, как создать сокет и организовать прием клиентских запросов соединения.

В этом примере используются две стандартные функции: htons ("host to network short" — "ближняя связь") и htonl ("host to network long" — "дальняя связь"), которые преобразуют целые числа к форме с обратным порядком байтов, требуемой протоколом IP.

Номером порта сервера может быть любое число из диапазона, допустимого для целых чисел типа short integer, но для определенных пользователем служб обычно используются числа в диапазоне 1025—5000. Порты с меньшими номерами зарезервированы для таких известных служб, как telnet или ftp, в то время как порты с большими номерами предполагаются для использования других стандартных служб.


Для работы с веб-сокетами требуется Python версии 3.6.1 и выше.

API в Python легко установить следующей командой:

Я не буду затрагивать тему безопасности, и весь код будет на Python. Этого достаточно для понимания всеми, кто немного знаком с языком или с программированием в целом. Таким образом, в дальнейшем вам будет проще написать consumer, producer или даже сервер на другом языке или для фронтенд приложения.

Я надеюсь, что приведенные примеры будут вам полезны, и призываю разработчиков попробовать веб-сокеты хотя бы раз в своей карьере — они потрясающие. Это больше, чем REST, знаете ли!


Три самые важные строки я объясню детально, но, если вас не интересует синтаксис, можете смело пропустить эту часть.

async/await — это просто специальный синтаксис для комфортной работы с промисами. Промис — не более чем объект, представляющий собой возможную передачу или сбой асинхронной операции.

Вместо передачи обратных вызовов в функцию вы можете присоединить обратные вызовы к этому возвращенному объекту.

В Python async гарантирует, что функция вернет промис и обернет в него не-промисы. В процессе вызова await может выполняться другой, не имеющий отношения к процессу, код.

Имейте в виду: иногда я использую аббревиатуру для веб-сокетов (ws), чтобы сделать код более читаемым в статье, но всегда пишу имя полностью в рабочем коде (Например, ws можно прочесть как веб-сайт или веб-сервер. Этого следует избегать хорошему разработчику. В конце концов код должен читаться как хорошая книга).

async for — это что-то вроде синхронного цикла for, позволяющего асинхронное восприятие.


Для запуска этого простого consumer’а просто укажите имя хоста и порт, он запустится в постоянном режиме. Предельно просто. Не беспокойтесь, если нет цикла событий. asyncio создаст новый цикл и установит в качестве текущего.

Приведу пример producer’а, выдающего только одно значение. И он даже проще, чем comsumer:


Теперь нам нужен только способ выполнить эту сопрограмму-отправитель только один раз.

Конечно, у Python есть решение. Мы можем просто использовать цикл обработки событий так же, как мы делали это с consumer. Единственное отличие будет в том, что он будет запущен, пока мы не получим ответ от сервера. После получения ответа задача завершится.

В Python 3.7 стало еще лучше — теперь можно использовать функцию run для выполнения сопрограмм.

Сервер создается и определяет сопрограмму обработчика веб-сокета. Функция веб-сокета serve — это обёртка вокруг метода create_server() цикла обработки событий. Он создает и запускает сервер с create_server() и принимает обработчик веб-сокета в качестве аргумента.

Когда бы ни подключался клиент, сервер принимает соединение, создает WebSocketServerProtocol , осуществляет открывающее “рукопожатие” и передает обработчику соединения, определенному обработчиком веб-сокета. Как только обработчик заканчивает работу, нормально или с исключением, сервер выполняет закрывающее “рукопожатие” и закрывает соединение.


Последняя часть кода — самая длинная, оставайтесь с нами, мы почти закончили.



Сетевой сокет — это эндпоинт межпроцессного взаимодействия в компьютерной сети. В Python Standard Library есть модуль socket, предоставляющий низкоуровневый сетевой интерфейс. Этот интерфейс является общим для разных языков программирования, поскольку он использует системные вызовы на уровне операционной системы.

Для создания сокета существует функция, называемая socket . Она принимает аргументы family , type и proto (подробнее см. в документации). Чтобы создать TCP-сокет, нужно использовать socket.AF_INET или socket.AF_INET6 для family и socket.SOCK_STREAM для type .

Пример Python socket:

Функция возвращает объект сокета, который имеет следующие основные методы:

  • bind()
  • listen()
  • accept()
  • connect()
  • send()
  • recv()

Методы bind() , listen() и accept() специфичны для серверных сокетов, а метод connect() — для клиентских. send() и recv() являются общими для обоих типов сокетов. Приведем пример Echo-сервера, взятый из документации:

Здесь мы создаем серверный сокет, привязываем его к localhost и 50000-му порту и начинаем прослушивать входящие соединения.

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

Затем он в бесконечном цикле считывает данные из сокета партиями по 1024 байта, используя метод recv() , пока не вернет пустую строку. После этого он отправляет все входящие данные обратно, используя метод sendall() , который в свою очередь многократно вызывает метод send() . И после этого сервер просто закрывает клиентское соединение. Данный пример может обрабатывать только одно входящее соединение, потому что он не вызывает accept() в цикле.

Код на стороне клиента выглядит проще:

Вместо методов bind() и listen() он вызывает только метод connect() и сразу же отправляет данные на сервер. Затем он получает обратно 1024 байта, закрывает сокет и выводит полученные данные.

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

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

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

Существует множество интерфейсов для разных операционных систем:

Все они примерно одинаковы, поэтому давайте создадим сервер с помощью Python select. Пример Python select :

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

Создание серверного сокета происходит так же, кроме одной строки: server.setblocking(0) . Это нужно для того, чтобы сокет не блокировался. Такой сервер более продвинутый, поскольку он может обслуживать более одного клиента. Главная причина заключается в сокетах selecting :

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

Этот вызов (если не передан аргумент timeout ) блокирует программу до тех пор, пока какие-либо из переданных сокетов не будут готовы. В этот момент вызов вернет три списка сокетов для указанных операций.

Так работают сокеты на низком уровне. Однако в большинстве случаев нет необходимости реализовывать настолько низкоуровневую логику. Рекомендуется использовать более высокоуровневые абстракции, такие как Twisted, Tornado или ZeroMQ, в зависимости от ситуации.

Читайте также: