Эта статья представляет собой китайскую документацию по написанию серверов WebSocket, переведенную с MDN.Writing WebSocket servers. Длина немного длинная, и неизбежно будут ошибки из-за ограниченных личных возможностей.
Суть сервера веб-сокетов
Сервер WebSocket — это просто приложение TCP, которое прослушивает любой порт сервера по специальному протоколу. Задача создания собственного сервера часто пугает. Однако, основываясь на реализации простого сервера Websocket, это не так уж и сложно.
Сервер WebSocket может быть реализован с использованием любого языка программирования на стороне сервера, который реализует базовые сокеты Berkeley. Например, c(++), Python, PHP, серверный JavaScript (node.js). Нижеследующее представляет собой не учебник по конкретному языку, а руководство, которое поможет нам создать собственный сервер.
Нам нужно понимать, как работает http, и иметь умеренный опыт программирования. Также необходимо знание сокетов TCP на основе языковой поддержки. Целью этого руководства является введение минимальных знаний, необходимых для разработки сервера WebSocket.
В этой статье мы объясним сервер WebSocket с точки зрения очень низкого уровня. Серверы WebSocket обычно представляют собой отдельные выделенные серверы (для балансировки нагрузки и по другим причинам), поэтому обратный прокси-сервер (например, стандартный HTTP-сервер) обычно используется для обнаружения протокола рукопожатия WebSocket, их предварительной обработки и отправки клиентских сообщений на реальный сервер WebSocket. Это означает, что сервер WebSocket не должен быть переполнен обработчиками файлов cookie и подписи. Это может быть полностью обработано в прокси.
правила рукопожатия через веб-сокет
Во-первых, сервер должен использовать стандартный сокет TCP для прослушивания входящих сокетных соединений. Основываясь на нашей платформе, они, вероятно, обрабатываются нами (зрелые серверные языки предоставляют эти интерфейсы, избавляя нас от необходимости начинать с нуля). Например, предположим, что наш сервер прослушивает порт 8000 сайта example.com, а сервер сокетов отвечает на запросы GET для /chat.
Предупреждение. Сервер может прослушивать любой порт, но если он находится за пределами 80 или 443, вы можете столкнуться с проблемами брандмауэра или прокси-сервера. Порт 443 подходит в большинстве случаев, но, конечно, требуется безопасное соединение (TLS/SSL). Кроме того, имейте в виду, что большинство браузеров не позволяют подключаться с защищенных страниц к незащищенным серверам Websocket.
Рукопожатие в WebSockets — это сеть, которая является мостом между HTTP и WS. Через рукопожатие оцениваются детали соединения, и каждая часть может быть прекращена, если условия не будут выполнены до завершения. Сервер должен внимательно анализировать всю информацию, запрошенную клиентом, иначе возникнут проблемы с безопасностью.
Запрос рукопожатия клиента
Хотя мы разрабатываем сервер, клиенту по-прежнему необходимо инициировать процесс рукопожатия 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
Здесь клиент может инициировать расширения или подпротоколы вMiscellaneousПодробнее см. Кроме того, можно включить общедоступные заголовки, такие как User-Agent, Referer, Cookie или аутентификация и т. д. Делайте то, что хотите, одним предложением. Они не имеют прямого отношения к WebSocket, и их можно игнорировать.Во многих распространенных настройках есть прокси-сервер, который обрабатывает эту информацию.
Если некоторые заголовки не распознаны или имеют недопустимые значения, серверу СЛЕДУЕТ отправить «400 Bad Request» и немедленно закрыть сокет.Причина сбоя рукопожатия обычно также указывается в теле возврата HTTP, но эта информация может не отображаться ( потому что браузеры их не отображают). Если сервер не распознает версию WebSockets, он ДОЛЖЕН вернуть заголовок Sec-WebSocket-Version, указывающий приемлемую версию (предпочтительно V13 и самую последнюю). Давайте взглянем на самый загадочный заголовок сообщения Sec-WebSocket-Key.
намекать:
- Все браузеры будут отправлять заголовок Origin, мы можем использовать этот заголовок для ограничений безопасности (проверьте, совпадает ли источник) и возвращать 403 Forbidden, если источник не ожидается. Затем обратите внимание, что клиенты без браузера могут отправлять поддельные источники, и многие приложения будут отклонять запросы без этого заголовка.
- Локатор ресурса запроса (здесь /chat) четко не определен в спецификации, поэтому многие люди используют его с умом, чтобы позволить серверу обрабатывать несколько приложений WebSocket. Например, example.com/chat может указывать на приложение для многопользовательского чата, а /game на том же сервере указывает на многопользовательскую игру. которыйПути под одним и тем же доменным именем могут указывать на разные приложения.
- Канонический HTTP-код можно использовать только перед рукопожатием. После успешного рукопожатия следует использовать другой набор кодов. См. раздел 7.4 спецификации.
Квитирование сервера возвращается
Когда сервер получает запрос, он должен отправить довольно странный ответ, который выглядит примерно так, но соответствует спецификации HTTP. Обратите внимание, что каждый заголовок заканчивается на \r\n и добавляет дополнительный \r\n после последнего.
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Кроме того, здесь сервер может принять решение о расширении или субпротокольных запросах. Для получения более подробной информации, пожалуйста, проверьтеMiscellaneous. Часть Sec-WebSocket-Accept очень интересна, сервер должен получить ее на основе Sec-WebSocket-Key, запрошенного клиентом, и конкретный метод заключается в следующем: связать Sec-WebSocket-Key с «258EAFA5-E914- 47DA-95CA-C5AB0DC85B11", через хэш SHA-1, чтобы получить результат, а затем вернуть кодировку base64 этого результата.
###намекать
Поскольку этот, казалось бы, сложный процесс существует, клиенту не нужно заботиться о том, поддерживает ли сервер веб-сокет. Кроме того, важность этого процесса — безопасность.Если сервер разбирает Websocket-соединение как http-запрос, мелких проблем не будет.
Таким образом, если ключ "dGhlIHNhbXBsZSBub25jZQ==", Accept будет "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=", и как только сервер отправит эти заголовки, протокол рукопожатия будет завершен.
Сервер МОЖЕТ отправить другие заголовки, такие как Set-Cookie, Request Signature, Redirect и т. д., прежде чем ответить на рукопожатие.
отслеживать клиента
Хотя он и не имеет прямого отношения к протоколу Websocket, он заслуживает нашего внимания. Сервер будет отслеживать клиентские сокеты, поэтому нам не нужно выполнять еще одно рукопожатие с клиентом, который уже выполнил протокол рукопожатия. Один и тот же IP-адрес клиента может пытаться подключиться несколько раз (но сервер может отказаться, если он попытается подключиться несколько раз, чтобы сохранить свою трассировку отказа в обслуживании)
Обмен данными FramesEdit
И клиент, и сервер могут отправлять сообщения в любое время, и в этом магия веб-сокетов. Однако процесс извлечения информации из фрейма данных менее волшебен. Хотя все кадры имеют один и тот же формат, данные, отправляемые клиентом на сервер, обрабатываются с помощью XOR-шифрования (с использованием 32-битного ключа), подробно описанного в главе 5 спецификации.
Формат
Каждый фрейм данных, отправляемый клиентом на сервер, имеет следующий формат:
帧格式:
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 ... |
+---------------------------------------------------------------+
MASK (Маска: строка двоичных кодов, которая выполняет побитовую операцию И над целевым полем, маскируя текущие входные биты.) Этот бит указывает только на то, было ли сообщение замаскировано. Сообщение от клиента должно быть обработано, поэтому мы должны установить его равным 1 (на самом деле в Разделе 5.1 говорится, что если клиент отправляет сообщение без маски, сервер должен отключиться). При отправке кадра клиенту не обрабатывать данные и не устанавливайте бит маски. Причина будет объяснена ниже. Примечание. Мы должны обработать сообщение, используя безопасный сокет. RSV1-3 можно игнорировать, это бит, который нужно расширить.
Поле кода операции определяет, как анализировать действительные данные:
- 0x0 продолжить обработку
- Текст 0x1 (должен быть в кодировке UTF-8)
- Двоичные данные 0x2 и другие данные, называемые управляющими кодами.
- 0x3-0x7 0xB-0xF Эта версия WebSockets бессмысленна
FIN указывает, является ли это последним сообщением набора данных, если оно равно 0, сервер продолжает отслеживать сообщение, ожидая оставшуюся часть сообщения. В противном случае сервер считает, что сообщение было полностью отправлено.
Эффективная длина закодированных данных
Чтобы проанализировать действительные закодированные данные, мы должны знать, когда они заканчиваются. Здесь важно знать правильную длину данных. К сожалению, есть некоторые сложности. Давайте рассмотрим это шаг за шагом.
- Прочитайте биты 9-15 и интерпретируйте их как целое число без знака, если оно меньше или равно 125, это длина данных. Если это 126, перейдите к шагу 2, если это 127, пожалуйста, прочитайте шаг 3
- Прочитайте следующие 16 бит и интерпретируйте как целое число без знака, конец
- Прочитайте следующие 64 бита и интерпретируйте как целое число без знака, конец
Данные считывания и антимаски
Если установлен бит MASK (конечно, он должен быть установлен для сообщения от клиента к серверу), прочитайте последние 4 байта (т.е. 32 бита) зашифрованного ключа. Как только длина данных и ключ шифрования расшифрованы, мы можем читать пакеты байтов непосредственно из сокета. Получите закодированные данные и ключ маски, декодируйте их, переберите зашифрованные байты (октеты, единицы текстовых данных) и добавьте их к (i%4)-му байту битовой маски (т.е. разделите i на 4 и возьмите остаток), чтобы выполнить операцию XOR.Если использовать js, то будет так (это правило только правило шифрования и дешифрования, вникать в него не надо, все умеют им пользоваться).
var DECODED = "";
for (var i = 0; i < ENCODED.length; i++) {
DECODED[i] = ENCODED[i] ^ MASK[i % 4];
}
Теперь мы можем знать конкретное значение декодированных данных в нашем приложении.
сегментация сообщения
Поля FIN и код операции работают вместе, чтобы разложить сообщение на отдельные кадры.Этот процесс называется сегментацией сообщения и доступен только тогда, когда коды операций равны 0x0-0x2 (как упоминалось ранее, другие значения в текущей версии не имеют смысла).
Вспомните, что код операции указывает, что будет делать кадр, и если он равен 0x1, данные являются текстовыми. Если это 0x2, стих представляет собой двоичные данные. Однако, когда он равен 0x0, кадр является кадром продолжения, что указывает на то, что сервер должен связать действительные данные кадра с последним кадром, полученным сервером. Вот скетч, который показывает, как сервер реагирует, когда клиент отправляет текстовое сообщение, первое сообщение отправляется в одном фрейме, а второе сообщение состоит из трех фреймов. Детали FIN и кода операции отображаются только для клиента. Это должно быть легче понять, взглянув на пример ниже.
Client: FIN=1, opcode=0x1, msg="hello"
Server: (消息传输过程完成) Hi.
Client: FIN=0, opcode=0x1, msg="and a"
Server: (监听,新的消息包含开始的文本)
Client: FIN=0, opcode=0x0, msg="happy new"
Server: (监听,有效数据与上面的消息拼接)
Client: FIN=1, opcode=0x0, msg="year!"
Server: (消息传输完成) Happy new year to you too!
Примечание. Первый фрейм включает в себя полное сообщение (FIN=1 и код операции != 0x0), поэтому он может вернуться, когда сервер найдет конец. Действительные данные второго фрейма — это текст (код операции = 0x1), но полное сообщение не пришло (FIN = 0). Все остальные части сообщения отправляются через кадр продолжения (код операции = 0x0) и, наконец, идентифицируются кадром через FIN = 1.
Сердцебиение для WebSockets: пинг и понг
В любой момент после принятия рукопожатия либо клиент, либо сервер могут выбрать отправку пинга другому. Когда получен пинг, получатель ДОЛЖЕН вернуть понг, когда это возможно. Мы можем использовать этот метод, чтобы убедиться, что соединение все еще действительно.
Пинг или понг — это просто обычный кадр, но управляющий кадр, код операции Pings — 0x9, а pong — 0xA. Когда мы получим пинг, верните понг с точно такими же действительными данными. (Для ping и pong максимальная допустимая длина данных составляет 125). Мы можем получить pong без отправки ping. Пожалуйста, игнорируйте этот случай.
Перед отправкой понга, если мы получаем более одного пинга, просто отвечаем одним понгом.
закрыть соединение
Чтобы закрыть соединение между клиентом и сервером, мы можем отправить кадр управления, содержащий данные для определенной очереди управления, чтобы запустить протокол закрытого рукопожатия. Когда кадр получен, другая сторона отправляет в ответ кадр закрытия. Затем первый закроет соединение. Все данные, полученные после закрытия соединения, будут удалены.
Более
Расширения и подпротоколы WebSocket согласовываются через заголовки в процессе рукопожатия. Иногда расширения и подпротоколы настолько похожи, что их невозможно различить. Самое основное отличие заключается в том, что расширение управляет фреймом веб-сокета и изменяет полезную нагрузку. Однако подпротокол представляет собой полезную нагрузку веб-сокета и никогда ничего не изменяет. Расширения необязательны, а подпротоколы обязательно ограничены.
расширять
Думайте о расширении как о сжатии файла перед его отправкой, независимо от того, как вы это делаете, вы будете отправлять одни и те же данные только в разных кадрах. Получатель в конечном итоге получит те же данные, что и ваша локальная копия, но отправленные другим способом. Это то, что делают расширения. веб-сокеты определяют протокол и основной способ отправки данных, однако такие расширения, как сжатие, могут доставлять те же данные в более коротких кадрах.
подпротокол
Думайте о подпротоколах как о пользовательских таблицах xml или спецификациях типа документа. Вы используете XML и его синтаксис, но вы ограничены структурой, с которой соглашаетесь. Так обстоит дело с подпротоколом WebSocket. Они не вводят какие-то другие причудливые вещи, просто строят структуру, например, тип документа и таблицу, обе части (клиент и сервер) согласовывают протокол, в отличие от типа документа и таблицы, подпротокол реализуется сервером и клиентом. не могу Внешние ссылки.
Клиент должен запросить определенный подпротокол, и для этого в рамках исходного рукопожатия будет отправлено что-то вроде следующего.
GET /chat HTTP/1.1
...
Sec-WebSocket-Protocol: soap, wamp
или эквивалентно
...
Sec-WebSocket-Protocol: soap
Sec-WebSocket-Protocol: wamp
Теперь сервер должен выбрать один из протоколов, предложенных и поддерживаемых клиентом. Если их несколько, отправьте первый, отправленный клиентом. Представьте, что наш сервер может использовать мыло и вамп, и тогда в возвращенном рукопожатии будет отправлена следующая форма.
Sec-WebSocket-Protocol: soap
Сервер НЕ ДОЛЖЕН отправлять более одного заголовка Sec-Websocket-Protocol.Если сервер не хочет использовать какой-либо из подпротоколов, он НЕ ДОЛЖЕН отправлять заголовок Sec-WebSocket-Protocol. Отправка пустого заголовка является ошибкой. Клиент МОЖЕТ закрыть соединение, если ожидаемый подпротокол не может быть получен.
Если мы хотим, чтобы наш сервер придерживался определенного подпротокола, естественно, на нашем сервере требуется дополнительный код. Представьте, что мы используем подпротокол json, на основе этого подпротокола все данные будут передаваться как JSON, если клиент запрашивает подпротокол и сервер хочет его использовать, сервис, который вам нужен, чтобы иметь парсер JSON . По правде говоря, будет библиотека инструментов, но серверу также нужно будет передавать данные.
Во избежание конфликтов имен рекомендуется использовать часть домена в качестве имени подпротокола. Если мы разрабатываем приложение чата, использующее определенный формат, мы можем использовать такое имя: Sec-WebSocket-Protocol: chat.example.com Обратите внимание, что это не обязательно. Просто необязательное соглашение, мы можем использовать любой символ, какой захотим.
заключительные замечания
Первоначальная цель перевода этого документа состоит в том, чтобы увидеть, что большинство китайцев о веб-сокетах связаны с клиентской стороной, а меня интересует реализация серверной части.Я не нашел подходящей информации, поэтому я должен прочитать английский самостоятельно, чтобы улучшить свои собственные. Цель состоит в том, чтобы перевести его, надеясь помочь другим учащимся,Посмотреть исходный текст. Пожалуйста, с нетерпением ждите практики реализации сервера веб-сокетов узлом позже.
исходный документ
Переведено с MDNWriting WebSocket servers