Эту статью опубликовал автор «Брат А Бао». Первоначальное название было «WebSocket, которого вы не знаете», которое было переработано и изменено.
Эта статья начнется с многих аспектов, таких как базовые концепции, технические принципы, типичные ошибки и практическая практика. Это длинная статья из 10 000 слов, которая поможет вам всесторонне изучить технологию WebSocket.
Прочитав эту статью, вы узнаете следующее:
Имя автора в сети:Брат Абао
Личный блог:Весь путь к бессмертию
Автор GitHub:semlinker (Брат А Бао) · GitHub
Раньше многие веб-сайты использовали опросы (также называемые короткими опросами) для реализации технологии push-уведомлений. Опрос означает, что браузер отправляет HTTP-запросы на сервер через регулярные промежутки времени, а затем сервер возвращает клиенту последние данные.
Распространенные методы опроса делятся на опрос и длинный опрос. Их различия показаны на следующем рисунке:
Чтобы более интуитивно почувствовать разницу между опросом и длинным опросом, давайте взглянем на конкретный код:
Эта традиционная модель имеет очевидные недостатки: браузеру необходимо постоянно отправлять запросы на сервер. Однако HTTP-запросы и ответы могут содержать длинные заголовки, а действительные данные могут составлять лишь небольшую часть, поэтому это будет потреблять много ресурсов. ресурсов полосы пропускания.
PS:окороткий опрос、О прошлом и настоящем технологии длинных опросов вы можете подробно прочитать в этих двух статьях: «Вводный пост для новичков: подробное объяснение наиболее полных принципов веб-стороны в истории».、《Webконецтехнология обмена мгновенными сообщениямиинвентарь:короткий опрос、Comet、Websocket、SSE》。
Относительно новая технология опроса — Comet. Хотя этот метод обеспечивает двустороннюю связь, он по-прежнему требует повторных запросов. Более того, длинное HTTP-соединение, обычно используемое в Comet, также потребляет ресурсы сервера.
В этом случае HTML5 определяет протокол WebSocket, который позволяет лучше экономить ресурсы сервера и пропускную способность, а также обеспечивает более эффективное взаимодействие в режиме реального времени.
Веб-сокеты используют универсальный идентификатор ресурса (URI) ws или wss, где wss представляет веб-сокет, использующий TLS.
нравиться:
ws://echo.websocket.org wss://echo.websocket.org
WebSocket использует тот же TCP-порт, что и HTTP и HTTPS, и может обходить большинство ограничений брандмауэра.
По умолчанию:
WebSocket протокол сетевой передачи, который можно использовать в одном TCP полнодуплексная связь по соединению, расположенному по адресу OSI Прикладной уровень модели. Вебсокеты Соглашение 2011 год по IETF нормализовано до RFC 6455,Позже RFC 7936 Дополнительные характеристики.
WebSocket упрощает обмен данными между клиентом и сервером, позволяя серверу активно передавать данные клиенту. В API WebSocket браузеру и серверу нужно только завершить рукопожатие, и между ними может быть создано постоянное соединение для двунаправленной передачи данных.
После ознакомления с содержанием опроса и WebSocket, давайте воспользуемся рисунком, чтобы увидеть разницу между опросом XHR (короткий опрос) и WebSocket.
Разница между опросом XHR и WebSocket показана на рисунке ниже:
Принято считать, что преимущества WebSocket заключаются в следующем:
потому что WebSocket обладает вышеуказанными преимуществами, поэтому он широко используется в системах обмена мгновенными сообщениями/IM.、Аудио и видео в реальном времени、существуют линии образования и игр и другие области.
Разработчикам внешнего интерфейса, если они хотят использовать мощные возможности, предоставляемые WebSocket, сначала необходимо освоить API WebSocket. Давайте рассмотрим API WebSocket.
PS:Если вы хотите быть более откровеннымизWebSocketРуководство по началу работы,Вы можете сначала прочитать эту статью "Быстрый" старт для новичков: краткое руководство по WebSocket", затем вернитесь и продолжите.
Прежде чем представить API WebSocket, давайте сначала разберемся с его совместимостью:
(Изображение взято с сайта: https://caniuse.com/#search=WebSocket)
Как видно из рисунка выше: все современные основные веб-браузеры поддерживают WebSocket, поэтому мы можем с уверенностью использовать его в большинстве проектов.
Чтобы использовать возможности, предоставляемые WebSocket в браузере, мы должны сначала создать объект WebSocket, который предоставляет API для создания и управления соединениями WebSocket, а также для отправки и получения данных через соединение.
Используя конструктор WebSocket, мы можем легко создать объект WebSocket.
Далее мы представим API WebSocket со следующих четырех аспектов:
Далее мы начинаем изучать конструктор WebSocket.
PS:Если вы хотите быть более откровеннымизWebSocketРуководство по началу работы,Вы можете сначала прочитать эту статью "Быстрый" старт для новичков: краткое руководство по WebSocket", затем вернитесь и продолжите.
Синтаксис конструктора WebSocket:
const myWebSocket = newWebSocket(url [, protocols]);
Соответствующие параметры объясняются следующим образом:
По поводу пункта 2):Эти строки используются для указания подпротоколов.,Таким образом, один сервер может реализовать несколько субдоговоров WebSocket.
Сравниватьнравиться:Возможно, вы захотите иметь одинсерверспособен указатьизпротокол(protocol)Обработка разных типовизвзаимодействие。Если не указанопротоколнить,тогда предполагается, что это пустая строка.
Использование веб-сокетов При построении функции, когда пробует порт, который вы пробуете, вы его выкидыте SECURITY_ERR аномальный.
PS:связанныйWebSocketКонструкториз Подробнее,Пожалуйста, обратитесь к официальной документации API.
Объект WebSocket содержит следующие свойства:
Конкретное значение каждого атрибута следующее:
WebSocket имеет два основных метода:
Используйте addEventListener() или назначьте прослушиватель событий свойству oneventname объекта WebSocket для прослушивания следующих событий.
Вот несколько событий:
После знакомства с API WebSocket давайте приведем пример использования WebSocket для отправки обычного текста.
существоватьпример вышесередина:нассуществоватьна страницесоздавать Получил два textarea, соответственно используемый для хранения Данные для отправки и Данные, возвращаемые сервером. После того, как пользователь закончил ввод текста для отправки, нажмите кнопку отправлять кнопка будет Пучоквходитьизтекстотправлятьприезжать Служитьконец,После того, как сервер успешно получит сообщение «Приехать».,Полученное сообщение о прибытии будет возвращено клиенту в неизменном виде.
// const socket = new WebSocket("ws://echo.websocket.org"); // const sendMsgContainer = document.querySelector("#sendMessage"); function send() { const message = sendMsgContainer.value; if(socket.readyState !== WebSocket.OPEN) { console.log("Соединение не установлено и сообщение пока нельзя использовать"); return; } if(message) socket.send(message); }
Конечно, после того, как клиент получит сообщение, возвращенное сервером, он сохранит соответствующее текстовое содержимое в текстовом поле текстовой области, соответствующем полученным данным.
// const socket = new WebSocket("ws://echo.websocket.org"); // const receivedMsgContainer = document.querySelector("#receivedMessage"); socket.addEventListener("message", function(event) { console.log("Message from server ", event.data); receivedMsgContainer.value = event.data; });
Чтобы более интуитивно понять описанный выше процесс взаимодействия данных, мы воспользуемся инструментами разработчика браузера Chrome, чтобы взглянуть на соответствующий процесс.
Как показано на рисунке ниже:
Полный код, соответствующий приведенному выше примеру, выглядит следующим образом:
<!DOCTYPE html> <html> <head> <metacharset="UTF-8"/> <metaname="viewport"content="width=device-width, initial-scale=1.0"/> <title>WebSocket отправлятьобычный текст Пример</title> <style> .block { flex: 1; } </style> </head> <body> <h3>WebSocket отправлятьобычный текст Пример</h3> <divstyle="display: flex;"> <divclass="block"> <p>Вскореотправлятьизданные:<button>отправлять</button></p> <textareaid="sendMessage"rows="5"cols="15"></textarea> </div> <divclass="block"> <p>полученные данные:</p> <textareaid="receivedMessage"rows="5"cols="15"></textarea> </div> </div> <script> const sendMsgContainer = document.querySelector("#sendMessage"); const receivedMsgContainer = document.querySelector("#receivedMessage"); const socket = new WebSocket("ws://echo.websocket.org"); // Прослушивание событий успешного подключения socket.addEventListener("open", function (event) { console.log("Соединение успешно, связь может начаться"); }); // Слушайте сообщения socket.addEventListener("message", function (event) { console.log("Message from server ", event.data); receivedMsgContainer.value = event.data; }); function send() { const message = sendMsgContainer.value; if (socket.readyState !== WebSocket.OPEN) { console.log("Соединение не установлено и сообщение пока нельзя использовать"); return; } if (message) socket.send(message); } </script> </body> </html>
на самом деле WebSocket Помимо поддержки отправки простого текста, он также поддерживает отправку двоичных данных, таких как ArrayBuffer Объект, Блоб объект или ArrayBufferView объект.
Пример кода выглядит следующим образом:
const socket = new WebSocket("ws://echo.websocket.org"); socket.onopen = function() { // отправить кодировку UTF-8 из текстового сообщения socket.send("Hello Echo Server!"); // передать кодировку UTF-8 из данных JSON socket.send(JSON.stringify({ msg: «Я брат Абао»})); // отправлятьдвоичныйArrayBuffer const buffer = newArrayBuffer(128); socket.send(buffer); // отправлятьдвоичныйArrayBufferView const intview = new Uint32Array(buffer); socket.send(intview); // отправлятьдвоичныйBlob const blob = new Blob([buffer]); socket.send(blob); };
После успешного выполнения приведенного выше кода мы сможем увидеть соответствующий процесс взаимодействия данных с помощью инструментов разработчика Chrome.
Как показано на рисунке ниже:
Ниже используется пример отправки Blob верно как для, чтобы показать, как отправлять двоичные данные.
Blob(Binary Large Object) представляет собой двоичный тип «объект.существовать» системы управления базами данных середина, который хранит двоичные данные для одного индивидуума из коллекции. Блоб Обычно это изображение, звук или мультимедийный файл. существовать JavaScript середина Blob Объекты типа представляют собой неизменяемые необработанные данные, такие как файловые объекты.
Друзья, интересующиеся Blobs, могут прочитать статью «Blobs, которых вы не знаете».
существование Вышеприведенный пример середина, два из которых мы создали на нашей странице существования. textarea, соответственно используемый для хранения Данные для отправки и Данные, возвращаемые сервером.
После того, как пользователь закончил вводить текст для отправки, нажмите кнопку отправлять кнопку, мы сначала получим входной текст и обернем его в Blob вернокак тогдаотправлятьприезжать Служитьконец,После того, как сервер успешно получит сообщение «Приехать».,Полученное сообщение о прибытии будет возвращено клиенту в неизменном виде.
Когда браузер получает новое сообщение, если это текстовые данные, он автоматически преобразует его в объект DOMString. Если это двоичные данные или объект Blob, оно будет напрямую передано в приложение, и само приложение ответит соответствующим образом. к возвращаемому типу данных.
Код отправки данных:
// const socket = new WebSocket("ws://echo.websocket.org"); // const sendMsgContainer = document.querySelector("#sendMessage"); function send() { const message = sendMsgContainer.value; if(socket.readyState !== WebSocket.OPEN) { console.log("Соединение не установлено и сообщение пока нельзя использовать"); return; } const blob = newBlob([message], { type: "text/plain"}); if(message) socket.send(blob); console.log(`Количество байт, не отправленных на сервериз: ${socket.bufferedAmount}`); }
Когда клиент получает сообщение, возвращенное сервером, он определяет возвращаемый тип данных, если это так. Blob типа, это будет называться Blob Объект text() метод,получить Blob вернослонсерединадержатьиз UTF-8 отформатируйте содержимое, а затем сохраните соответствующее текстовое содержимое в полученные данные Соответствующий textarea Текстовое поле середина.
Код получения данных:
// const socket = new WebSocket("ws://echo.websocket.org"); // const receivedMsgContainer = document.querySelector("#receivedMessage"); socket.addEventListener("message", async function(event) { console.log("Message from server ", event.data); const receivedData = event.data; if(receivedData instanceofBlob) { receivedMsgContainer.value = await receivedData.text(); } else{ receivedMsgContainer.value = receivedData; } });
Аналогичным образом мы используем инструменты разработчика браузера Chrome, чтобы взглянуть на соответствующий процесс:
На картинке выше мы ясно видим, что приезжать, когда использоватьот править Blob Объект, Данные Информация в поле показывает Binary Сообщение, хотя верно используется в обычном тексте, Данные Поле информации отображается непосредственно из текстового сообщения.
Полный код, соответствующий приведенному выше примеру, выглядит следующим образом:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"/> <meta name="viewport"content="width=device-width, initial-scale=1.0"/> <title>WebSocket отправлятьдвоичные данные Пример</title> <style> .block { flex: 1; } </style> </head> <body> <h3>WebSocket отправлятьдвоичные данные Пример</h3> <div style="display: flex;"> <div class="block"> <p>Данные для отправки:<button>отправлять</button></p> <textarea id="sendMessage"rows="5"cols="15"></textarea> </div> <div class="block"> <p>полученные данные:</p> <textarea id="receivedMessage"rows="5"cols="15"></textarea> </div> </div> <script> const sendMsgContainer = document.querySelector("#sendMessage"); const receivedMsgContainer = document.querySelector("#receivedMessage"); const socket = new WebSocket("ws://echo.websocket.org"); // Прослушивание событий успешного подключения socket.addEventListener("open", function(event) { console.log("Соединение успешно, связь может начаться"); }); // Слушайте сообщения socket.addEventListener("message", async function(event) { console.log("Message from server ", event.data); const receivedData = event.data; if(receivedData instanceofBlob) { receivedMsgContainer.value = await receivedData.text(); } else{ receivedMsgContainer.value = receivedData; } }); functionsend() { const message = sendMsgContainer.value; if(socket.readyState !== WebSocket.OPEN) { console.log("Соединение не установлено и сообщение пока нельзя использовать"); return; } const blob = newBlob([message], { type: "text/plain"}); if(message) socket.send(blob); console.log(`Количество байт, не отправленных на сервериз: ${socket.bufferedAmount}`); } </script> </body> </html>
Может быть, некоторые друзья это понимают WebSocket API После этого я почувствовал, что это недостаточно приятно. Следующее поможет вам реализовать программу, которая поддерживает отправку обычного текста из WebSocket сервер.
Прежде чем рассказать, как написать сервер WebSocket вручную, нам необходимо понять жизненный цикл соединения WebSocket.
Как видно из рисунка выше: перед использованием WebSocket для достижения полнодуплексной связи необходимо выполнить рукопожатие между клиентом и сервером. Только после завершения рукопожатия может начаться двусторонняя передача данных.
Рукопожатие происходит после создания схемы связи, но до начала передачи информации.
Рукопожатие используется для согласования параметров, нравиться:
Рукопожатие помогает соединять устройства различной структуры из системы существующего канала связи середина, не требуя человеческого вмешательства для настройки параметров.
Поскольку рукопожатие является первым шагом в жизненном цикле соединения WebSocket, давайте сначала проанализируем протокол рукопожатия WebSocket.
Протокол WebSocket является протоколом прикладного уровня и основан на протоколе TCP транспортного уровня. WebSocket выполняет рукопожатие через код состояния 101 протокола HTTP/1.1. Чтобы создать соединение WebSocket, необходимо выполнить запрос через браузер, а затем сервер ответит. Этот процесс часто называют «квитированием связи».
Использование HTTP для завершения рукопожатия дает несколько преимуществ:
Давайте возьмем пример изотправления обычного текста из примера, который был продемонстрирован ранее, чтобы подробно проанализировать процесс рукопожатия.
5.2.1) Запрос клиента:
GET ws://echo.websocket.org/ HTTP/1.1 Host: echo.websocket.org Origin: file:// Connection: Upgrade Upgrade: websocket Sec-WebSocket-Version: 13 Sec-WebSocket-Key: Zx8rNEkBE4xnwifpuh8DHQ== Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Примечание:Игнорируемая часть HTTP Заголовок запроса.
Описание полей в приведенном выше запросе на достоверность следующее:
Что касается пункта 4) выше:Пучок “Sec-WebSocket-Key” Добавить специальную строку “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,затем вычислить SHA-1 резюме, продолжить позже Base64 Кодировка, результат будет “Sec-WebSocket-Accept” Значение заголовка возвращается клиенту. Поступая так, вы можете попытаться избежать обычных HTTP Запрос был ошибочно принят за WebSocket протокол.
5.2.2) Ответ сервера:
HTTP/1.1 101 Web Socket Protocol Handshake ① Connection: Upgrade ② Upgrade: websocket ③ Sec-WebSocket-Accept: 52Rg3vW4JQ1yWpkvFlsTsiezlqw= ④
Примечание:Игнорируемая часть HTTP заголовок ответа.
Описания полей для приведенного выше ответа следующие:
Конец введения WebSocket протокол рукопожатия, который будет использоваться далее Node.js развивать наши WebSocket сервер.
Разработать WebSocket Сервер, сначала нам нужно реализовать функцию рукопожатия. Здесь я использую Node.js Встроенный -ин http модуль для создания HTTP сервер.
Конкретный код выглядит следующим образом:
const http = require("http"); const port = 8888; const { generateAcceptValue } = require("./util"); const server = http.createServer((req, res) => { res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8"}); res.end("Привет всем, я брат А Бао. Спасибо, что прочитали "WebSocket, которого вы не знаете""); }); server.on("upgrade", function(req, socket) { if(req.headers["upgrade"] !== "websocket") { socket.end("HTTP/1.1 400 Bad Request"); return; } // Прочтите Sec-WebSocket-Key, предоставленный клиентом. const secWsKey = req.headers["sec-websocket-key"]; // Сгенерируйте Sec-WebSocket-Accept с использованием алгоритма SHA-1. const hash = generateAcceptValue(secWsKey); // Установить заголовки ответа HTTP const responseHeaders = [ "HTTP/1.1 101 Web Socket Protocol Handshake", "Upgrade: WebSocket", "Connection: Upgrade", `Sec-WebSocket-Accept: ${hash}`, ]; // Возвращает информацию ответа на запрос установления связи socket.write(responseHeaders.join("\r\n") + "\r\n\r\n"); }); server.listen(port, () => console.log(`Server running at http://localhost:${port}`) );
существоватьприведенный выше кодсередина:наспервыйпредставил http модуль, а затем вызвав метод этого модуля createServer() метод создает HTTP сервер, затем мы слушаем upgrade событие, которое запускается каждый раз, когда сервер отвечает на запрос обновления event.potom. что Наш изсервер поддерживает только обновления WebSocket протокол, поэтому, если клиент запрашивает обновление для протокола, отличного от WebSocket договоримся, мы вернемся “400 Bad Request”。
Когда сервер получает обновление до WebSocket При отправке запроса на установление связи он сначала будет получен из заголовка запроса середина. “Sec-WebSocket-Key” ценить,Затем Пучок Долженценить Добавить специальную строку «258EAFA5-E914-47DA-95CA-C5AB0DC85B11», затем рассчитайте SHA-1 резюме, продолжить позже Base64 Кодировка, результат будет “Sec-WebSocket-Accept” Значение заголовка возвращается клиенту.
Описанный выше процесс может показаться немного громоздким, но самом делеиспользовать Node.js Встроенный -ин crypto Модуль, это можно сделать всего несколькими строками кода.
Код выглядит следующим образом:
// util.js const crypto = require("crypto"); const MAGIC_KEY = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; function generateAcceptValue(secWsKey) { return crypto .createHash("sha1") .update(secWsKey + MAGIC_KEY, "utf8") .digest("base64"); }
После разработки функции рукопожатия мы можем протестировать ее,используя предыдущий пример. После запуска сервера нам нужно всего лишь “отправлятьобычный текст” Например, сделать простую настройку, то есть заменить предыдущую URL Замените адрес на ws://localhost:8888, вы можете выполнить функциональную проверку.
Заинтересованные ребята могут попробовать. Вот результаты моего локального пробега:
Как видно из картинки выше:насвыполнитьиз Функция рукопожатия уже доступнатолькопостоянный работникделать.Так возможно ли, что рукопожатие не удастся??Ответ: даиз。Сравнивать Например, проблемы с сетью、сервераномальныйили Sec-WebSocket-Accept Значение неверно.
Давайте изменим правила генерации «Sec-WebSocket-Accept», например изменив значение MAGIC_KEY, а затем повторно проверим функцию рукопожатия.
В это время консоль браузера выведет следующую информацию об исключении:
WebSocket connection to 'ws://localhost:8888/'failed: Error during WebSocket handshake: Incorrect 'Sec-WebSocket-Accept'header value
Если ваш сервер WebSocket хочет поддерживать подпротоколы, вы можете обратиться к следующему коду для обработки подпротоколов, и я не буду продолжать их здесь представлять.
// Чтение подпротокола из заголовка запроса середина const protocol = req.headers["sec-websocket-protocol"]; // Если включен подпротокол, проанализировать подпротокол const protocols = !protocol ? [] : protocol.split(",").map((s) => s.trim()); // Для простоты мы определяем только, содержит ли он подпротокол JSON. if(protocols.includes("json")) { responseHeaders.push(`Sec-WebSocket-Protocol: json`); }
ОК, вебсокет соглашение о содержание, связанное с рукопожатием, в основном Конец Далее мы представим некоторые базовые знания, необходимые для разработки функции передачи сообщений.
Протокол WebSocket существует середина, данные — это серия кадров данных для передачи из.
для того, чтобы избежать появления сетевого середина (например, некоторых прокси-серверов перехвата) или некоторых проблем безопасности.,Клиент должен добавить маску ко всем кадрам. После того, как сервер получит сообщение о прибытии без добавления маски из фрейма данных.,Соединение должно быть немедленно закрыто.
5.4.1) Формат кадра данных:
Чтобы реализовать передачу сообщений, мы должны понимать формат кадров данных WebSocket:
Некоторые друзья могут начать чувствовать себя немного сбитыми с толку, увидев приведенный выше контент.
Давайте проанализируем его на основе фактического кадра данных:
существовать Изображение вышесередина:Простой анализ “отправлятьобычный текст” Пример Соответствующий формат кадра данных. здесь Давайте представим это дальше Payload length,Потому что длясуществовать позже была разработана функция анализа данных из,Вам нужно использовать это очко знаний, чтобы приехать.
Длина полезной нагрузки представляет длину «полезных данных» в байтах.
Имеет следующие ситуации:
Примечание:Величины многобайтовой длины, выраженные в сетевом порядке байтов,Длина полезной нагрузки «Расширенные данные» + «Данные приложения» длина.«Расширенные данные» Длина может быть 0, то длина полезной нагрузки равна «Данные приложения» длина.
кроме того:Если не согласовано Расширять,в противном случае «Расширенные данные» Длина 0 байт.существоватьсоглашение о рукопожатиисередина, необходимо указать любое расширение «Расширенные данные» длина, как рассчитывается эта длина и как это расширение использовать. Если сохранить расширение существования, то это «Расширенные данные» Содержит общую длину полезной нагрузки середина.
PS:оданныеформат кадраиз Объясните подробно,Вы можете подробно прочитать следующие статьи:
5.4.2) Алгоритм маски:
32-битную ценить. Значение маски должно быть непредсказуемым. поэтому,Маска должна происходить из сильного источника энтропии (энтропии).,И данная маска не позволяет агенту легко предсказывать последующие кадры. Маскировка непредсказуемости необходима для предотвращения раскрытия вредоносными приложениями соответствующих байтовых данных в Интернете.
Маскирование не влияет на длину полезных данных. Шаги, связанные с маскированием и демаскированием данных, одинаковы.
Операции маскировки и демаскирования используют следующий алгоритм:
j = i MOD 4 transformed-octet-i = original-octet-i XOR masking-key-octet-j
Объяснять:
Чтобы друзья могли лучше понять описанный выше процесс расчета маски, давайте возьмем пример истинносередина «Я брат Абао». Данные замаскированы.
здесь «Я брат Абао» Соответствующий UTF-8 Кодировка следующая:
E6 88 91 E6 98 AF E9 98 BF E5 AE 9D E5 93 A5
и Соответствующий Masking-Key для 0x08f6efb1。
Согласно приведенному выше алгоритму, мы можем выполнить операцию маски следующим образом:
let uint8 = new Uint8Array([0xE6, 0x88, 0x91, 0xE6, 0x98, 0xAF, 0xE9, 0x98,0xBF, 0xE5, 0xAE, 0x9D, 0xE5, 0x93, 0xA5]); let maskingKey = new Uint8Array([0x08, 0xf6, 0xef, 0xb1]); let maskedUint8 = new Uint8Array(uint8.length); for(let i = 0, j = 0; i < uint8.length; i++, j = i % 4) { maskedUint8[i ] = uint8[i ] ^ maskingKey[j]; } console.log(Array.from(maskedUint8).map(num=>Number(num).toString(16)).join(' '));
После успешного выполнения приведенного выше кода консоль выведет следующие результаты:
ee 7e 7e 57 90 59 6 29 b7 13 41 2c ed 65 4a
Приведенные выше результаты согласуются с WireShark серединаиз Masked payload Соответствующийценитьдапоследовательныйиз,специфический Как показано на рисунке ниже:
существовать WebSocket протоколсередина,Функция маскировки данных заключается в повышении безопасности протокола. Но маскирование данных не направлено на защиту самих данных.,Потому что сам алгоритм открыт,Операция тоже не сложная.
Так зачем же вводить маскирование данных? Маскирование данных было введено для предотвращения таких проблем, как атаки на загрязнение кэша прокси-сервера в более ранних версиях протокола.
Понял алгоритм маскировки WebSocket и маску данных. После роли давайте представим концепцию сегментирования данных.
5.4.3) Фрагментация данных:
Каждое сообщение WebSocket можно разделить на несколько кадров данных. Когда получатель WebSocket получает кадр данных, он определяет, был ли получен последний кадр данных сообщения, на основе значения FIN.
используя FIN и Opcode, мы можем отправлять сообщения между кадрами.
Код операции сообщает кадру, что он должен делать:
для Чтобы каждый мог лучше понять изложенное выше, давайте взглянем на статью от MDN Пример:
Client: FIN=1, opcode=0x1, msg="hello" Server: (process complete message immediately) Hi. Client: FIN=0, opcode=0x1, msg="and a" Server: (listening, newmessage 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 newyear to you too!
существоватьпример вышесередина:клиентконец Ксерверотправлять Два сообщения,Нет.одининформациясуществоватьодиночный кадрсерединаотправлять,И Нет.два сообщения охватывают три кадра отправки.
Чтосередина:Нет.одининформациядаодинвесьизинформация(FIN=1 и opcode != 0x0), поэтому сервер может обрабатывать ответ по мере необходимости. И Нет. Эти два сообщения представляют собой текстовые сообщения (код операции = 0x1)и. FIN=0, что указывает на то, что сообщение еще не завершено и еще есть последующий исход Все оставшиеся части сообщения используются в кадрах продолжения (код операции=0x0), а последний кадр сообщения используется в отправке. FIN=1 отметка.
Хорошо, давайте кратко представим соответствующее содержание сегментирования данных. Далее приступим к реализации функции обмена сообщениями.
Автор ставит Реализация функции обмена сообщениями,Разложить анализ сообщения и ответ на сообщение на две подфункции.,Ниже мы расскажем, как реализовать эти две подфункции соответственно.
5.5.1) Анализ сообщений:
использовать Основы обмена Ссылка на сообщения середина знакомит с соответствующими знаниями, я реализовал parseMessage Функция, используемая для анализа данных, передаваемых клиентом. WebSocket кадр данных.
ради простоты,здесь обрабатывает только текстовые фреймы,Конкретный код выглядит следующим образом:
function parseMessage(buffer) { // Первый байт содержит бит FIN, код операции, Биты маски const firstByte = buffer.readUInt8(0); // [FIN, RSV, RSV, RSV, OPCODE, OPCODE, OPCODE, OPCODE]; // Сдвиньте 7 бит вправо, чтобы взять первый бит, 1 бит, указывающий, является ли это последним кадром данных. const isFinalFrame = Boolean((firstByte >>> 7) & 0x01); console.log("isFIN: ", isFinalFrame); // Получите код операции, младшие четыре бита /** * %x0: представляет кадр продолжения. когда Opcode для 0 Когда это означает, что для этой передачи данных используется фрагментация данных, и текущий кадр данных имеет один фрагмент данных; * %x1: указывает, что это текстовый фрейм (текстовый frame); * %x2: указывает, что это двоичный кадр (двоичный frame); * %x3-7: зарезервированный код операции, используемый для последующих определенных неуправляющих кадров; * %x8: указывает, что соединение разорвано; * %x9: указывает, что это запрос контрольного сигнала (ping); * %xA: указывает, что это ответ сердечного ритма (понг); * %xB-F: зарезервированные коды операций для последующих определенных управляющих кадров. */ const opcode = firstByte & 0x0f; if(opcode === 0x08) { // соединение закрыто return; } if(opcode === 0x02) { // двоичный кадр return; } if(opcode === 0x01) { // В настоящее время обрабатываются только текстовые фреймы let offset = 1; const secondByte = buffer.readUInt8(offset); // MASK: 1 бит, указывающий, маскируется ли использование, существование должно быть замаскировано в кадре данных на сервер, и серверу не нужна маска при возврате const useMask = Boolean((secondByte >>> 7) & 0x01); console.log("use MASK: ", useMask); const payloadLen = secondByte & 0x7f; // Младшие 7 бит указывают длину байта полезной нагрузки. offset += 1; // четырехбайтовая маска let MASK = []; // Если значение находится в диапазоне 0–125, следующие 4 байта (32 бита) должны напрямую распознаваться как маска; if(payloadLen <= 0x7d) { // Длина груза менее 125 MASK = buffer.slice(offset, 4 + offset); offset += 4; console.log("payload length: ", payloadLen); } elseif(payloadLen === 0x7e) { // Если это значение равно 126, содержимое следующих двух байтов (16 бит) должно распознаваться как 16-битное двоичное число, указывающее размер содержимого данных; console.log("payload length: ", buffer.readInt16BE(offset)); // Длина 126, Затем следующие два байта используются как полезная нагрузка. длина, 32-битная маска MASK = buffer.slice(offset + 2, offset + 2 + 4); offset += 6; } else{ // Если это значение равно 127, следующие 8 байтов (64 бита) должны распознаваться как 64-битное двоичное число, указывающее размер содержимого данных. MASK = buffer.slice(offset + 8, offset + 8 + 4); offset += 12; } // Начните читать следующую полезную нагрузку и вычислите ее с помощью маски, чтобы получить исходное байтовое содержимое. const newBuffer = []; const dataBuffer = buffer.slice(offset); for(let i = 0, j = 0; i < dataBuffer.length; i++, j = i % 4) { const nextBuf = dataBuffer[i ]; newBuffer.push(nextBuf ^ MASK[j]); } return Buffer.from(newBuffer).toString(); } return ""; }
После создания функции parseMessage обновим созданный ранее сервер WebSocket:
server.on("upgrade", function(req, socket) { socket.on("data", (buffer) => { const message = parseMessage(buffer); if(message) { console.log("Message from client:"+ message); } elseif(message === null) { console.log("WebSocket connection closed by the client."); } }); if(req.headers["upgrade"] !== "websocket") { socket.end("HTTP/1.1 400 Bad Request"); return; } // Опустить существующий код });
После завершения обновления перезапускаем сервер и продолжаем использовать “отправлятьобычный текст” Пример проверки функциональности анализа сообщений.
нижеотправлять «Я брат Абао» После текстового сообщения WebSocket Вывод информации сервером:
Сервер работает по адресу http://localhost:8888. isFIN: правда использовать МАСКУ: правда Длина полезной нагрузки: 15 Сообщение от клиента: Я Брат А Бао
Вебсокет Сервер смог успешно проанализировать кадр данных клиента, содержащий обычный текст. Далее мы реализуем функцию ответа на сообщение.
5.5.2) Ответ на сообщение:
Чтобы вернуть данные клиенту, наш сервер WebSocket также должен инкапсулировать данные в формате фрейма данных WebSocket.
Как и ранее представленная функция parseMessage, я также инкапсулировал функциюstructReply для инкапсуляции возвращаемых данных.
Долженфункцияизспецифический Код выглядит следующим образом:
function constructReply(data) { const json = JSON.stringify(data); const jsonByteLength = Buffer.byteLength(json); // В настоящее время поддерживаются только полезные данные размером менее 65535 байт. const lengthByteCount = jsonByteLength < 126 ? 0 : 2; const payloadLength = lengthByteCount === 0 ? jsonByteLength : 126; const buffer = Buffer.alloc(2 + lengthByteCount + jsonByteLength); // настройки Первый байт фрейма данных, настройкаopcodeдля1, представляет собой текстовый фрейм. buffer.writeUInt8(0b10000001, 0); buffer.writeUInt8(payloadLength, 1); // Если payloadLengthдля126, содержимое следующих двух байтов (16 бит) должно распознаваться как 16-битное двоичное число, указывающее размер содержимого данных. let payloadOffset = 2; if(lengthByteCount > 0) { buffer.writeUInt16BE(jsonByteLength, 2); payloadOffset += lengthByteCount; } // Записать данные JSON в туристический буфер середина. buffer.write(json, payloadOffset); return buffer; }
После создания функцииstructReply давайте обновим созданный ранее сервер WebSocket:
server.on("upgrade", function(req, socket) { socket.on("data", (buffer) => { const message = parseMessage(buffer); if(message) { console.log("Message from client:"+ message); // новыйувеличиватьниже👇код socket.write(constructReply({ message })); } elseif(message === null) { console.log("WebSocket connection closed by the client."); } }); });
приезжатьздесь,насиз WebSocket Сервер разработан. Далее полностью проверим его работоспособность.
На картинке выше мы видим:Вышеописанное развитиеиз Простая версия WebSocket Сервер уже может нормально обрабатывать обычные текстовые сообщения.
Наконец, давайте взглянем на полный код.
Файл custom-websocket-server.js:
const http = require("http"); const port = 8888; const { generateAcceptValue, parseMessage, constructReply } = require("./util"); const server = http.createServer((req, res) => { res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8"}); res.end("Привет всем, я брат А Бао. Спасибо, что прочитали "WebSocket, которого вы не знаете""); }); server.on("upgrade", function(req, socket) { socket.on("data", (buffer) => { const message = parseMessage(buffer); if(message) { console.log("Message from client:"+ message); socket.write(constructReply({ message })); } else if(message === null) { console.log("WebSocket connection closed by the client."); } }); if(req.headers["upgrade"] !== "websocket") { socket.end("HTTP/1.1 400 Bad Request"); return; } // Прочтите Sec-WebSocket-Key, предоставленный клиентом. const secWsKey = req.headers["sec-websocket-key"]; // Сгенерируйте Sec-WebSocket-Accept с использованием алгоритма SHA-1. const hash = generateAcceptValue(secWsKey); // Установить заголовки ответа HTTP const responseHeaders = [ "HTTP/1.1 101 Web Socket Protocol Handshake", "Upgrade: WebSocket", "Connection: Upgrade", `Sec-WebSocket-Accept: ${hash}`, ]; // Возвращает информацию ответа на запрос установления связи socket.write(responseHeaders.join("\r\n") + "\r\n\r\n"); }); server.listen(port, () => console.log(`Server running at http://localhost:${port}`) );
файл util.js:
const crypto = require("crypto"); const MAGIC_KEY = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; function generateAcceptValue(secWsKey) { return crypto .createHash("sha1") .update(secWsKey + MAGIC_KEY, "utf8") .digest("base64"); } function parseMessage(buffer) { // Первый байт содержит бит FIN, код операции, Биты маски const firstByte = buffer.readUInt8(0); // [FIN, RSV, RSV, RSV, OPCODE, OPCODE, OPCODE, OPCODE]; // Сдвиньте 7 бит вправо, чтобы взять первый бит, 1 бит, указывающий, является ли это последним кадром данных. const isFinalFrame = Boolean((firstByte >>> 7) & 0x01); console.log("isFIN: ", isFinalFrame); // Получите код операции, младшие четыре бита /** * %x0: представляет кадр продолжения. когда Opcode для 0 Когда это означает, что для этой передачи данных используется фрагментация данных, и текущий кадр данных имеет один фрагмент данных; * %x1: указывает, что это текстовый фрейм (текстовый frame); * %x2: указывает, что это двоичный кадр (двоичный frame); * %x3-7: зарезервированный код операции, используемый для последующих определенных неуправляющих кадров; * %x8: указывает, что соединение разорвано; * %x9: указывает, что это запрос контрольного сигнала (ping); * %xA: указывает, что это ответ сердечного ритма (понг); * %xB-F: зарезервированные коды операций для последующих определенных управляющих кадров. */ const opcode = firstByte & 0x0f; if(opcode === 0x08) { // соединение закрыто return; } if(opcode === 0x02) { // двоичный кадр return; } if(opcode === 0x01) { // В настоящее время обрабатываются только текстовые фреймы let offset = 1; const secondByte = buffer.readUInt8(offset); // MASK: 1 бит, указывающий, маскируется ли использование, существование должно быть замаскировано в кадре данных на сервер, и серверу не нужна маска при возврате const useMask = Boolean((secondByte >>> 7) & 0x01); console.log("use MASK: ", useMask); const payloadLen = secondByte & 0x7f; // Младшие 7 бит указывают длину байта полезной нагрузки. offset += 1; // четырехбайтовая маска let MASK = []; // Если значение находится в диапазоне 0–125, следующие 4 байта (32 бита) должны напрямую распознаваться как маска; if(payloadLen <= 0x7d) { // Длина груза менее 125 MASK = buffer.slice(offset, 4 + offset); offset += 4; console.log("payload length: ", payloadLen); } else if(payloadLen === 0x7e) { // Если это значение равно 126, содержимое следующих двух байтов (16 бит) должно распознаваться как 16-битное двоичное число, указывающее размер содержимого данных; console.log("payload length: ", buffer.readInt16BE(offset)); // Длина 126, Затем следующие два байта используются как полезная нагрузка. длина, 32-битная маска MASK = buffer.slice(offset + 2, offset + 2 + 4); offset += 6; } else{ // Если это значение равно 127, следующие 8 байтов (64 бита) должны распознаваться как 64-битное двоичное число, указывающее размер содержимого данных. MASK = buffer.slice(offset + 8, offset + 8 + 4); offset += 12; } // Начните читать следующую полезную нагрузку и вычислите ее с помощью маски, чтобы получить исходное байтовое содержимое. const newBuffer = []; const dataBuffer = buffer.slice(offset); for(let i = 0, j = 0; i < dataBuffer.length; i++, j = i % 4) { const nextBuf = dataBuffer[i ]; newBuffer.push(nextBuf ^ MASK[j]); } return Buffer.from(newBuffer).toString(); } return ""; } function constructReply(data) { const json = JSON.stringify(data); const jsonByteLength = Buffer.byteLength(json); // В настоящее время поддерживаются только полезные данные размером менее 65535 байт. const lengthByteCount = jsonByteLength < 126 ? 0 : 2; const payloadLength = lengthByteCount === 0 ? jsonByteLength : 126; const buffer = Buffer.alloc(2 + lengthByteCount + jsonByteLength); // настройки Первый байт фрейма данных, настройкаopcodeдля1, представляет собой текстовый фрейм. buffer.writeUInt8(0b10000001, 0); buffer.writeUInt8(payloadLength, 1); // Если payloadLengthдля126, содержимое следующих двух байтов (16 бит) должно распознаваться как 16-битное двоичное число, указывающее размер содержимого данных. let payloadOffset = 2; if(lengthByteCount > 0) { buffer.writeUInt16BE(jsonByteLength, 2); payloadOffset += lengthByteCount; } // Записать данные JSON в туристический буфер середина. buffer.write(json, payloadOffset); return buffer; } module.exports = { generateAcceptValue, parseMessage, constructReply, };
на самом резервный сервер отправляет информацию в браузер, за исключением использования WebSocket Помимо технологии, вы также можете использовать SSE(Server-Sent События). Это позволяет серверу передавать клиентам потоковые текстовые сообщения, например генерировать сообщения в реальном времени на сервере.
для достижения этой цели, SSE Разработаны два компонента:Браузерсерединаиз EventSource API иновыйиз «поток событий» Формат данных (текст/поток событий). Это середина, EventSource позволяет клиенту DOM Push-уведомления сервера принимаются в виде событий, а для доставки каждого обновления данных используются новые форматы данных.
На самом деле:SSE Обеспечивает эффективный кроссбраузерный XHR Потоковая реализация, доставка сообщений использует только длинный HTTP соединять. Однако, с нашим собственным осознанием XHR Различные потоки, браузер поможет нам управлять соединением, Разберите сообщение, что позволит нам сосредоточиться только на бизнес-логике. Пространство ограничено, около SSE Я не буду вдаваться в подробности. SSE Заинтересованные друзья могут самостоятельно прочитать следующие статьи:
WebSocket это своего рода HTTP Разные изпротокол.Оба расположены в OSI Модель прикладного уровня, и оба зависят от транспортного уровня. TCP протокол.
Хоть они и разные, RFC 6455 середина Регулирование:WebSocket Разработано длясуществовать HTTP 80 и 443 порт работает и поддерживает HTTP Агент исередина представился, тем самым сделав это и HTTP Соглашение совместимо. для Для достижения совместимости WebSocket использование рукопожатия HTTP Upgrade голова, от HTTP Изменения соглашениядля WebSocket протокол.
Теперь, когда это было упомянуто OSI(Open System Interconnection Модель) модель, здесь поделилась очень ярким и ярким описанием. OSI Принципиальная схема модели (показана ниже).
(Изображение цитируется по адресу: https://www.networkingsphere.com/2019/07/what-is-osi-model.html)
Конечно, связь между WebSocket и HTTP не может быть четко сформулирована в этих трех или двух предложениях. Заинтересованные читатели могут подробно прочитать следующие две статьи:
Длинный опрос:клиентконецинициироватьодинпросить,сервер Получает приезжать После отправки клиентом запроса,Конец сервера не будет отвечать напрямую,Вместо этого запрос сначала приостанавливается.,Затем определите, были ли обновлены запрошенные данные. Если есть обновление,тогда ответь,Если нет данных,Затем подождите определенное время, прежде чем вернуться.
Сущность длинного опроса по-прежнему основана на HTTP соглашение, остается вопрос и ответ (просьба — ответ) шаблон. и WebSocket После успешного установления связи происходит полнодуплексный режим. TCP канале данные могут активно передаваться с сервера клиенту.
Чтобы понять WebSocket и длинный опрос из разницы,Требуется глубокое понимание технических принципов длительного опроса.,Следующие 3 статьи рекомендуются для углубленного прочтения о внедрении технологии длинных опросов:
Иотправка данных в сети серединаиз осуществляется с помощью Socket. А вот если этот сокет был отключен, то при получении данных обязательно возникнут проблемы.
Но как определить, можно ли еще использовать эту розетку?Это то, что нужносуществоватьсистемасерединасоздаватьсердцебиениемеханизм。
так называемый "Сердцебиение" Это время для пользовательской структуры (пакета сердцебиения или рамки сердцебиения), чтобы дать знать истинной стороне о себе. «существовать строку», чтобы обеспечить валидность ссылки.
Пакет так называемого изсердцебиения — это просто сообщение, которое клиент регулярно отправляет на конец сервера, чтобы сообщить ему, что я тоже существую. Код предназначен для отправки фиксированного сообщения на сервер каждые несколько минут.,Сервер отвечает фиксированным сообщением после получения прибытия.,Если сервер не получит информацию о клиенте в течение нескольких минут, клиент будет считаться отключенным.
существовать WebSocket протоколсерединаопределенный сердцебиение Ping и сердцебиение Pong Контрольный кадр:
По поводу пункта 2):Если наконецконецполучатьприезжатьодин Ping Рамка, но без оформления Pong рамка для ответа на предыдущее Ping кадры, то терминал может выбрать только самые последние обработанные кадры. Ping рамкаотправлять Pong рамка. Кроме того, вы можете автоматически отправить один Pong Фрейм используется как одностороннее сердцебиение.
PS:здесь Есть статьяWebSocketсердцебиениеаспектизIMПрактическая сводная статья,Если вам интересно, вы можете прочитать «Практические советы по обмену мгновенными сообщениями через Интернет: как ускорить отключение и повторное подключение вашего WebSocket?» 》.
Две программы в сети используют двустороннее соединение для обмена данными. Один конец этого соединения называется сокетом, поэтому для организации сетевых соединений. требуется как минимум пара номеров портов.
Сущность сокета:даверно TCP/IP инкапсуляция стека протоколов, которая обеспечивает TCP или UDP Интерфейс программирования, а не очередной протокол.проходить Розетка, вы можете использовать TCP/IP протокол.
Описание Socket в энциклопедии Baidu выглядит следующим образом:
Socket Исходное значение слова «из» на английском языке — «отверстие» или «гнездо»: как для BSD UNIX Механизм связи процесса принимает последнее значение. Его также часто называют «сокетом», который используется для описания IP-адреса и порта. Это дескриптор цепочки связи, который можно использовать для реализации связи между различными виртуальными машинами и разными компьютерами. существоватьInternet Хосты в Интернете обычно используют несколько сервисных программ и предоставляют несколько сервисов одновременно. Каждая служба открывает сокет и привязывается к порту. Разные порты соответствуют разным службам. Розетка Его первоначальное значение на английском языке — «пористая розетка». Хост похож на комнату, полную различных розеток. Каждая розетка имеет номер, а некоторые розетки его предоставляют. 220 вольт переменного тока, Предоставил 110 вольт переменного тока, есть программы кабельного телевидения. Клиентское программное обеспечение подключает вилки к розеткам с разными номерами для получения разных услуг.
Что касается Socket, можно резюмировать следующие моменты:
На следующей диаграмме показаны отношения клиент/сервер API Sockets для протоколов, ориентированных на соединение:
PS:Хочу сказатьWebSocketиSocketизсвязь,Эта статья «Подробное объяснение WebSocket (6): понимание взаимоотношений между WebSocket и Socket» посвящена подробному обмену информацией.,Рекомендуем к прочтению. Эта статья была опубликована одновременно по адресу: http://www.52im.net/thread-3713-1-1.html.
[1] Быстрый старт для новичков: краткое руководство по WebSocket
[2] От новичка до профессионала в WebSocket достаточно получаса!
[3] Вводный пост для новичков: самое полное и подробное объяснение принципов технологии обмена мгновенными сообщениями через Интернет в истории.
[4] Инвентаризация технологий обмена мгновенными сообщениями на веб-странице: короткий опрос, Comet, Websocket, SSE.
[5] Подробное объяснение технологии SSE: новая технология push-событий сервера HTML5.
[6] Подробное объяснение технологии Comet: технология веб-связи в реальном времени, основанная на длинном HTTP-соединении.
[7] Подробное объяснение WebSocket (4): Разбираемся в сути взаимоотношений между HTTP и WebSocket (Часть 1).
[8] Подробное объяснение WebSocket (5): Разбираемся в сути взаимоотношений между HTTP и WebSocket (Часть 2).
[9] Подробное объяснение WebSocket (6): узнайте больше о взаимосвязи между WebSocket и Socket.
[10] Практические советы по обмену мгновенными сообщениями через Интернет: как ускорить отключение и повторное подключение WebSocket?
[11] Сочетание теории с практикой: понимание принципов связи, форматов протоколов и безопасности WebSocket с нуля.
[12] Подробное введение в WebSocket: 200 строк кода, которые научат вас, как создать сервер WebSocket голыми руками.
[13] Краткое введение в технологию обмена мгновенными сообщениями на веб-странице: короткий опрос, длинный опрос, SSE, WebSocket.
[14] Одной статьи достаточно, чтобы понять современные веб-технологии обмена мгновенными сообщениями: WebSocket, Socket.io, SSE.