Node.js — это популярный инструмент для построения клиент-серверных приложений. При правильном использовании, Node.js способен обрабатывать большое количество сетевых запросов, используя всего один поток. Несомненно, сетевой ввод — вывод является одной из сильнейших сторон этой платформы. Казалось бы, что используя Node.js для написания серверного кода приложения, активно использующего различные сетевые протоколы, разработчики должны знать, как эти протоколы работают, но зачастую это не так. Виной тому еще одна сильная сторона Node.js, это его пакетный менеджер NPM, в котором можно найти готовое решение практически под любую задачу. Используя готовые пакеты, мы упрощаем себе жизнь, переиспользуем код (и это правильно), но в то же время скрываем от себя, за ширмой библиотек, суть происходящих процессов. В этой статье мы постараемся разобраться в протоколе WebSocket, реализуя часть спецификации, не используя внешних зависимостей. Добро пожаловать под кат. Для начала необходимо разобраться с исторической составляющей, а именно, зачем придумали сетевой протокол WebSocket и что послужило главной мотивацией для его создания. Изначально приложения, которым требовался активный обмен данными с сервером, использовали протокол http, что накладывало много ограничейний, связанных с этим протоколом. Ведь при создании http не предпологалось использовать его как двунаправленный протокол. Http работает по принципу request/reply — клиент отправляет запрос на сервер, а сервер на этот запрос формирует ответ и отправляет его клиенту. Каждый раз при такой схеме происходит установка нового соединения (напомню, что я рассказываю про стародревние времена до http 2.0). Протокол не подразумевает, что сервер сам может инициировать соединение с клиентом и отправить ему сообщение. Поэтому многие клиентские приложения, на подобии чатов, используя проткол http, были вынуждены с определенным интервалом опрашивать сервер на предмет изменений его состояния. Существует спецификация RFC6202, которая описывает лучшие практики относительно того, как серверу передавать сообщения клиенту по своей инициативе. Первая версия стандарта протокола WebSocket появилась в 2008 году, после чего несколько раз перерабатывалась. То, что мы знаем как WebSocket на данный момент появилось в 2011 году в виде 13ой версии протокола и описанной в стандарте RFC6455. Протокол находится на том же уровне сетевой модели OSI что и http и так же работает поверх tcp. WebSocket решает все описанные проблемы присущие http. Протокол WebSocket является двунаправленным, что означает, что после установки соединения, клиент и сервер могут обмениваться асинхронными сообщениями по открытому подключению. Инициировать подключение может как клиент так и сервер. К слову сказать, поддержка протокола WebSocket в браузере появилась в 2009 году и первым браузером, реализовавшем стандарт, был Google Chrome 4й версии. Но от к слов к делу, у нас есть протокол, давайте разберемся с ним и начнем его реализовывать. Работа с WebSocket делится на два больших этапа: Для того, чтобы клиент смог установить соединение с сервером, по протоколу WebSocket, нужно перевести http сервер в этот режим работы. Чтобы это сделать, нужно отправить GET запрос со специальными заголовками. Но чтобы понять, какие заголовки отправляются на сервер из браузера, при попытки установить сокетное соединение, не будем сразу смотреть в спецификацию к протоколу, а начнем писать сервер и увидем эти заголовки в консоли. Для начала напишем http сервер, который будет принимать любой запрос и выводить в консоль заголовки этого запроса. Код я буду писать на typescript и запускать с помощью ts-node. Сервер будет запущен на порту 8080. Теперь откроем консоль разработчика в браузере и напишем следующий код. При создании объекта класса WebSocket, браузер попытается подключиться к серверу. У полученного объекта можно посмотреть текущее состояние подключения с помощью свойства readyState. Это свойство может принимать одно из четырех значений: Если сейчас посмотреть свойство readyState, то оно будет в состоянии 0, но через некоторое время перейдет в состояние 3. Это происходит потому, что мы ничего не ответили на запрос перевода сервера на работу с другим протоколом. Подробнее про WebSocket API в браузере можно почитать тут В консоли с запущенным сервером получим следующее: Для перехода на другой протокол используется стандартный механизм, описанный в стандарте http RFC2616. Происходит http запрос типа GET, в котором передаётся заголовок upgrade с названием протокола, на который клиент хочет переключить сервер. Если сервер поддерживает желаемый протокол, то он должен ответить кодом 101, если нет — вернуть ошибку. В описании протокола WebSocket дополнительно передаётся еще несколько заголовков, часть из которых опциональны: Чтобы клиент понял, что сервер успешно перешел на нужный протокол, сервер должен ответить кодом 101, а в ответе должен быть заголовок sec-websocket-accept, значение которого сервер должен сформировать, используя заголовок sec-websocket-key следующим образом: Так же сервер должен передать в заголовках ответа заголовки Upgrade: WebSocket и Connection: Upgrade. Звучит не сложно, давайте реализуем. Для генерации загловка sec-websocket-key нам потребуется встроеный в node.js модуль crypto. Необходимо в начале импортировать его. А затем изменить конструктор класса SocketServer  У http сервера Node.js есть специальное событие на upgrade соединения, используем его. Перезапустив сокет сервер с этими изменениями и снова попытавшись создать соединение в браузере, мы получим объект сокета, который будет в состоянии 1. Мы успешно создали соединение с нашим сервером и завершили первый этап. Переёдем ко второму. Передача данных по сокетам происходит с помощью фреймов. Каждый фрейм — это единица информации с данными и метаинформацией. Фреймы сокетов никак не соотносятся с делением информации на фреймы или пакеты на более низких уровнях сетевой модели. За счет того, что информация передаётся фреймами, появляется возможность фрагментировать сообщения, т.е. пересылать сообщения частями. За счет фрагметации можно по сокетам передвать сообщения неизвестной длины, например, если нам нужно передать в виде сообщения большой файл, который мы читаем из стороннего источника (так тоже можно). Для того, чтобы разобраться с фреймами, нужно понимать, по каким правилам он формируется. В стандарте приведена следующая структура фрейма.  Визуально структура фрейма, на этой картинки, выглядит достаточно сложно. Поэтому я поделил его на несколько частей. Определив какой размер данных, в полученном нами фрейме, мы можем прочитать его. Но данные с клиента всегда должны быть маскированны. Маска — это случайные 4 байта, которые клиент передаёт во фрейме. Отправляя данные, клиент накладывает эту маску на данные с помощью функции XOR. Для того, чтобы сервер мог расшифровать информацию, нужно повторно наложить маску с помощью функции XOR. Это основные знания, которые потребуются для реализации WebSocket сервера. Что бы в реализации сервера была какая то цель, нужно эту цель придумать. Целью кода данной статьи будет написание WebSocket сервера, который реализует часть протокола сокетов и позволяет переписываться нескольким клиентам из консоли браузера. Для начала нужно реализовать функционал опроса клиента с помощью управляющих фреймов Ping. Нам нужно знать, что клиент еще жив и готов принимать данные с сервера. Фрейм Ping, управляющий фрейм, но он так же может содержать данные. Когда клиент получит такое сообщение по сокету, он должен отправить на сервер фрейм Pong с теми данными, которые были во фрейме Ping. До реализации этого функционала, давайте пропишем в класс сервера необходимые константы Далее реализуем наш метод по формированию фрейма Ping По большому счету, в данном случае, это не требуется. Нам совершенно не обязательно пересылать какие-то данные клиенту вместе с управляющим фреймом Ping. Поэтому этот метод можно удалить, а вместо него в класс добавить еще одну константу. Также для того, чтобы реализовать функционал чата, нам потребуется хранить объекты подключений. Заведем под это отдельную коллекцию в классе сервера. Модицифируем конструктор, добавим отправку фрейма Ping подключившимся клиентам с интервалом в 5 секунд, а также добавляем новых клиентов в коллекцию. Теперь мы можем принимать соединения по сокетам и поддерживать его с помощью пингов. Осталось научить наш сервер маршрутизировать сообщения от клиентов. В спецификации к протоколу написано, что клиенты всегда должны отправлять сообщения на сервер в маскированном виде, а сообщения сервера всегда без маски. Из этого следует, что нам нужно раскодировать сообщение, а для этого нужно понять, что за сообщение пришло на сервер, получить маску, длину сообщения и сами данные. Напишем для этого метод С помощью этой функции мы можем получать всю необходимую информацию для обмена сообщениями между клиентами. Напишем метод, который будет демаскировать данные Демаскирование происходит путем применения функции XOR к каждому байту данных и соответствующему ему байту маски. Длина маски указана в спецификации и составляет 4 байта. Теперь можно написать метод для отправки коротких сообщений по сокету клиенту. Нам осталось финализировать конструктор класса. Добавим туда рассылку полученных сообщений от клиента всем активным клиентам, а также добавим отправку всем клиентам сообщения при подключении нового клиента. Теперь можно запустить сервер. Для проверки работоспособности можно открыть две вкладки браузера и в консоли каждой вклдаки написать следующий код. Затем отправить сообщение в одной из вкладок  Конечно, если в вашем приложении нужны WebSocket'ы, а скорее всего они нужны, не стоит реализовывать протокол самостоятельно без существенной необходимости. Всегда можно выбрать подходящее решение из многообразия библиотек в npm. Лучше переиспользовать уже написанный и протестированный код. Но понимание как это работает "под капотом", всегда даст много больше, чем просто использование чужого кода. Приведенный выше пример доступен на github Программист