Spec-Zone.ru › Web APIs

Написание WebSocket-серверов

WebSocket-сервер — это всего лишь приложение, слушающее любой порт TCP-сервера, который следует определённому протоколу. Создание пользовательского сервера может показаться сложным, если вы никогда этого не делали. Однако на самом деле реализовать базовый WebSocket-сервер на выбранной вами платформе довольно просто.

WebSocket-сервер можно написать на любом серверном языке программирования, способном использовать сокеты Berkeley, такие как C(++), Python, PHP или серверный JavaScript. Это не учебник по какому-либо конкретному языку, а руководство по написанию собственного сервера.

В этой статье предполагается, что вы уже знакомы с принципом работы HTTP и имеете средний уровень опыта программирования. В зависимости от поддержки языка может потребоваться знание сокетов TCP. Цель данного руководства — представить минимальные знания, необходимые для написания WebSocket-сервера.

Примечание: Прочитайте последний официальный спецификация WebSocket, RFC 6455. Разделы 1 и 4-7 особенно интересны для разработчиков серверов. Раздел 10 обсуждает безопасность, и вы определённо должны ознакомиться с ним перед открытием вашего сервера для доступа.

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

Рукопожатие WebSocket

Сначала сервер должен прослушивать входящие подключения сокетов с помощью стандартного TCP-сокета. В зависимости от вашей платформы это может быть выполнено автоматически. Например, предположим, что ваш сервер прослушивает example.com, порт 8000, и ваш сервер сокетов отвечает на GET запросы по адресу example.com/chat.

Предупреждение: Сервер может прослушивать любой порт, но если он выберет любой порт, кроме 80 или 443, у него могут возникнуть проблемы с брандмауэрами и/или прокси. Браузеры обычно требуют безопасное соединение для WebSocket, хотя они могут предложить исключение для локальных устройств.

Рукопожатие — это «Веб» в WebSocket. Это мост между HTTP и WebSocket. При рукопожатии согласовываются детали соединения, и любая сторона может отказаться от завершения, если условия неблагоприятны. Сервер должен тщательно изучить всё, о чём просит клиент, в противном случае могут возникнуть проблемы с безопасностью.

Примечание: Запрос-URI (/chat здесь) не имеет определённого смысла в спецификации. Поэтому многие люди используют его для того, чтобы один сервер обрабатывал несколько приложений WebSocket. Например, example.com/chat может вызывать приложение чата для нескольких пользователей, а /game на том же сервере может вызывать многопользовательскую игру.

Запрос рукопожатия клиента

Несмотря на то, что вы создаёте сервер, клиент всё равно должен инициировать процесс рукопожатия WebSocket, связавшись с сервером и запросив WebSocket-соединение. Поэтому вы должны знать, как интерпретировать запрос клиента. Клиент отправит довольно стандартный HTTP-запрос с заголовками, похожими на этот (версия HTTP должна быть 1.1 или выше, а метод должен быть GET):

GET /chat HTTP/1.1
Host: example.com:8000
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

Здесь клиент может запросить расширения и/или подпротоколы; см. Разное для получения подробностей. Также могут присутствовать общие заголовки, такие как User-Agent, Referer, Cookie или заголовки аутентификации. Делайте с ними всё, что хотите; они напрямую не относятся к WebSocket. Также безопасно их игнорировать. Во многих распространённых конфигурациях обратный прокси уже с ними поработал.

Примечание: Все браузеры отправляют заголовок Origin. Вы можете использовать этот заголовок для безопасности (проверка на соответствие источнику, автоматическое разрешение или запрет и т. д.) и отправлять 403 Запрещено, если вам не нравится то, что вы видите. Это эффективно против Перехвата WebSocket через разные сайты (CSWH). Однако будьте осторожны, что агенты, не являющиеся браузерами, могут отправлять поддельный Origin. Большинство приложений отклоняют запросы без этого заголовка.

Если какой-либо заголовок не понятен или имеет неправильное значение, сервер должен отправить ответ 400 («Неправильный запрос») и немедленно закрыть сокет. Как обычно, он также может указать причину сбоя рукопожатия в теле HTTP-ответа, но сообщение может никогда не быть отображено (браузеры его не отображают). Если сервер не понимает ту версию WebSocket, он должен отправить заголовок Sec-WebSocket-Version обратно, содержащий версию(и), которую он понимает. В приведённом примере указана версия 13 протокола WebSocket.

Самый интересный заголовок здесь — Sec-WebSocket-Key. Давайте рассмотрим его дальше.

Примечание: Стандартные коды состояния HTTP могут использоваться только до рукопожатия. После успешного рукопожатия вам нужно использовать другой набор кодов (определённых в разделе 7.4 спецификации).

Ответ сервера на рукопожатие

Когда сервер получает запрос на рукопожатие, он должен отправить специальный ответ, указывающий на то, что протокол будет изменён с HTTP на WebSocket. Этот заголовок выглядит примерно так (помните, что каждая строка заголовка заканчивается \r\n и добавьте дополнительный \r\n после последнего, чтобы указать конец заголовка):

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

Кроме того, сервер может принять решения по запросам расширений/подпротоколов здесь; см. Разное для получения подробностей. Заголовок Sec-WebSocket-Accept важен тем, что сервер должен получить его из Sec-WebSocket-Key, отправленного клиентом. Для этого нужно объединить Sec-WebSocket-Key клиента и строку "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" (это «магическая строка»), вычислить хеш SHA-1 результата и вернуть кодировку base64 этого хеша.

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

Итак, если Ключ был "dGhlIHNhbXBsZSBub25jZQ==", значение заголовка Sec-WebSocket-Accept составляет "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=". После отправки этих заголовков сервером рукопожатие завершено, и вы можете начать обмен данными!

Примечание: Сервер может отправить другие заголовки, такие как Set-Cookie, или запросить аутентификацию или перенаправление с помощью других кодов состояния, прежде чем отправить ответ на рукопожатие.

Отслеживание клиентов

Это не напрямую относится к протоколу WebSocket, но стоит упомянуть здесь: ваш сервер должен отслеживать сокеты клиентов, чтобы не выполнять рукопожатие снова с клиентами, которые уже его выполнили. Один и тот же IP-адрес клиента может пытаться подключиться несколько раз. Однако сервер может запретить им, если они предпримут слишком много попыток подключения, чтобы защитить себя от атак типа «отказ в обслуживании».

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

Обмен кадрами данных

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

Формат

Каждый кадр данных (от клиента к серверу или наоборот) следует этому же формату:

Frame format:

      0                   1                   2                   3
      0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
     +-+-+-+-+-------+-+-------------+-------------------------------+
     |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
     |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
     |N|V|V|V|       |S|             |   (if payload len==126/127)   |
     | |1|2|3|       |K|             |                               |
     +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
     |     Extended payload length continued, if payload len == 127  |
     + - - - - - - - - - - - - - - - +-------------------------------+
     |                               |Masking-key, if MASK set to 1  |
     +-------------------------------+-------------------------------+
     | Masking-key (continued)       |          Payload Data         |
     +-------------------------------- - - - - - - - - - - - - - - - +
     :                     Payload Data continued ...                :
     + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
     |                     Payload Data continued ...                |
     +---------------------------------------------------------------+

Это означает, что кадр содержит следующие байты:

  • Первый байт:
    • бит 0: FIN
    • бит 1: RSV1
    • бит 2: RSV2
    • бит 3: RSV3
    • биты 4-7: OPCODE
  • Байты 2-10: длина полезной нагрузки (см. Декодирование длины полезной нагрузки)
  • Если используется маскирование, следующие 4 байта содержат ключ маскирования (см. Чтение и снятие маски с данных)
  • Все последующие байты — полезная нагрузка

Бит MASK указывает, закодировано ли сообщение. Сообщения от клиента должны быть замаскированы, поэтому ваш сервер должен ожидать, что он будет равен 1. (На самом деле, раздел 5.1 спецификации гласит, что ваш сервер должен разорвать соединение с клиентом, если этот клиент отправит незамаскированное сообщение.) При отправке кадра обратно клиенту не маскируйте его и не устанавливайте бит маски. Мы объясним маскирование позже. Примечание: Вы должны маскировать сообщения даже при использовании защищенного сокета. RSV1-3 можно игнорировать, они предназначены для расширений.

Поле OPCODE определяет, как интерпретировать данные полезной нагрузки: 0x0 для продолжения, 0x1 для текста (который всегда закодирован в UTF-8), 0x2 для бинарных данных и другие так называемые «коды управления», которые будут обсуждаться позже. В этой версии WebSockets, 0x3 до 0x7 и 0xB до 0xF не имеют смысла.

Бит FIN указывает, является ли это последним сообщением в серии. Если это 0, сервер продолжает ожидать дополнительные части сообщения; в противном случае сервер должен считать сообщение доставленным. Подробнее об этом позже.

Декодирование длины полезной нагрузки

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

  1. Прочитайте биты 9-15 (включительно) и интерпретируйте их как целое без знака. Если это 125 или меньше, то это и есть длина; вы закончили. Если это 126, перейдите к шагу 2. Если это 127, перейдите к шагу 3.
  2. Прочитайте следующие 16 бит и интерпретируйте их как целое без знака. Вы закончили.
  3. Прочитайте следующие 64 бита и интерпретируйте их как целое без знака. (Самый старший бит должен быть 0.) Вы закончили.

Чтение и снятие маски с данных

Если бит MASK был установлен (а он должен быть установлен для сообщений клиент-сервер), прочитайте следующие 4 октета (32 бита); это ключ маскирования. После декодирования длины полезной нагрузки и ключа маскирования вы можете прочитать это количество байтов из сокета. Назовем данные ENCODED, а ключ MASK. Чтобы получить DECODED, переберите октеты (байты, т.е. символы для текстовых данных) ENCODED и выполните XOR с (i по модулю 4)-м октетом MASK. В псевдокоде (который оказывается допустимым JavaScript):

const MASK = [1, 2, 3, 4]; // 4-byte mask
const ENCODED = [105, 103, 111, 104, 110]; // encoded string "hello"

// Create the byte Array of decoded payload
const DECODED = Uint8Array.from(ENCODED, (elt, i) => elt ^ MASK[i % 4]); // Perform an XOR on the mask

Теперь вы можете определить, что означает DECODED в зависимости от вашей программы.

Разбиение сообщения

Биты FIN и OPCODE работают вместе для отправки сообщения, разбитого на отдельные кадры. Это называется разбиением сообщения. Разбиение доступно только для OPCODE от 0x0 до 0x2.

Напомним, что OPCODE указывает, что должен сделать кадр. Если это 0x1, полезная нагрузка — это текст. Если это 0x2, полезная нагрузка — это двоичные данные. Однако, если это 0x0, кадр — это кадр продолжения; это означает, что сервер должен объединить полезную нагрузку кадра с последним кадром, полученным от этого клиента. Вот набросок, в котором сервер реагирует на отправку клиентом текстовых сообщений. Первое сообщение отправляется в одном кадре, а второе — через три кадра. Подробности FIN и OPCODE показаны только для клиента:

Client: FIN=1, opcode=0x1, msg="hello"
Server: (process complete message immediately) Hi.
Client: FIN=0, opcode=0x1, msg="and a"
Server: (listening, new message containing text started)
Client: FIN=0, opcode=0x0, msg="happy new"
Server: (listening, payload concatenated to previous message)
Client: FIN=1, opcode=0x0, msg="year!"
Server: (process complete message) Happy new year to you too!

Обратите внимание, что первый кадр содержит все сообщение (имеет FIN=1 и opcode!=0x0), поэтому сервер может обработать или ответить, как считает нужным. Второй кадр, отправленный клиентом, содержит текстовую полезную нагрузку (opcode=0x1), но все сообщение еще не пришло (FIN=0). Все оставшиеся части этого сообщения отправляются с помощью кадров продолжения (opcode=0x0), а заключительный кадр сообщения отмечается FIN=1. Раздел 5.4 спецификации описывает фрагментацию сообщений.

Ping и Pong: Сердцебиение WebSocket

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

Пинг или понг — это просто обычный кадр, но это кадр управления. Пинги имеют OPCODE 0x9, а понги — 0xA. При получении пинга отправьте понг с точной копией данных полезной нагрузки пинга (для пингов и понгов максимальная длина полезной нагрузки составляет 125). Вы также можете получить понг, никогда не отправляя пинг; проигнорируйте это, если это произойдет.

Примечание: Если вы получили более одного пинга, прежде чем смогли отправить понг, отправляется только один понг.

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

Для закрытия соединения клиент или сервер может отправить кадр управления с данными, содержащими определенную последовательность управления, чтобы начать процесс рукопожатия закрытия (подробнее в разделе 5.5.1). При получении такого кадра другая сторона отправляет ответный кадр Закрытия. Затем первая сторона закрывает соединение. Любые дальнейшие данные, полученные после закрытия соединения, игнорируются.

Разное

Примечание: Коды WebSocket, расширения, подпротоколы и т. д. зарегистрированы в реестре протоколов WebSocket IANA.

Расширения и подпротоколы WebSocket согласовываются через заголовки во время установления рукопожатия. Иногда расширения и подпротоколы очень похожи, но есть четкое различие. Расширения управляют кадром WebSocket и изменяют полезную нагрузку, а подпротоколы структурируют полезную нагрузку WebSocket и никогда ничего не изменяют. Расширения являются необязательными и обобщенными (например, сжатие); подпротоколы являются обязательными и локальными (например, для чата и для MMORPG игр).

Расширения

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

Примечание: Расширения описаны в разделах 5.8, 9, 11.3.2 и 11.4 спецификации.

Подпротоколы

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

Примечание: Подпротоколы описаны в разделах 1.9, 4.2, 11.3.4 и 11.5 спецификации.

Клиент должен запросить определенный подпротокол. Для этого он отправит что-то вроде этого как часть исходного рукопожатия:

GET /chat HTTP/1.1
...
Sec-WebSocket-Protocol: soap, wamp

или, что эквивалентно:

...
Sec-WebSocket-Protocol: soap
Sec-WebSocket-Protocol: wamp

Теперь сервер должен выбрать один из предложенных клиентом подпротоколов, который он поддерживает. Если их несколько, отправляется первый, предложенный клиентом. Представьте, что наш сервер может использовать как soap так и wamp. Тогда в ответном рукопожатии он отправит:

Sec-WebSocket-Protocol: soap

Предупреждение: Сервер не может отправить более одного заголовка Sec-WebSocket-Protocol. Если сервер не хочет использовать какой-либо подпротокол, он не должен отправлять заголовок Sec-WebSocket-Protocol. Отправка пустого заголовка неверна. Клиент может закрыть соединение, если не получит нужный подпротокол.

Если вы хотите, чтобы ваш сервер поддерживал определенные подпротоколы, вам потребуется дополнительный код на сервере. Предположим, мы используем подпротокол json. В этом подпротоколе все данные передаются в формате JSON. Если клиент запрашивает этот протокол, а сервер хочет его использовать, серверу нужен парсер JSON. Практически, это будет частью библиотеки, но серверу нужно передавать данные.

Примечание: Чтобы избежать конфликта имен, рекомендуется делать имя вашего подпротокола частью строки домена. Если вы создаете собственное приложение чата, использующее собственный формат, исключительный для Example Inc., вы можете использовать следующее: Sec-WebSocket-Protocol: chat.example.com. Обратите внимание, что это не обязательно, это просто необязательная соглашение, и вы можете использовать любую строку, которую хотите.

Связанное

  • Разработка приложений-клиентов WebSocket
  • Учебник: WebSocket-сервер на C#
  • Учебник: WebSocket-сервер на Java

© 2005–2024 MDN contributors.
Licensed under the Creative Commons Attribution-ShareAlike License v2.5 or later.
https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers

Spec-Zone.ru

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