Асинхронная связь между браузерами и серверами прошла очень долгий путь за последние 6-7 лет.

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

  • Длительный опрос
  • Флэш-сокеты (да, Adobe Flash)
  • Возможное обновление до WebSockets

Эти хаки обычно использовались как часть очень популярной библиотеки абстракций Socket.IO. Который был разработан, чтобы инициировать соединение с наименьшим общим знаменателем, а затем «обновить» соединение, если клиент поддерживает лучшую функцию.

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

Socket.IO

Со временем Socket.IO стал безумно популярным, набрав более 25000 звезд на Github и 5000+ форков.

Но в проекте всегда казалось, что он пытается сделать слишком много. Это библиотека чата? Это служба присутствия? Это розетка? Это кувалда? (Я бы назвал его Sledgehammer.IO)

В настоящее время Socket IO - это фактически две библиотеки. Engine.io, который поддерживает абстракции сокетов и управление соединениями. И Socket.IO, который, насколько я могу судить, обрабатывает переподключение, отправку событий и размещение имен сообщений (вроде чатов).

Это все лишнее, когда все, что вам действительно нужно или нужно, - это Socket.

Программирование сокетов

Розетки - это основа для всех подключений через Интернет. Они являются конечными частями, с которыми и клиент, и сервер завершают соединение.

Сокеты низкого уровня - это, по сути, поток байтов. Вы пишете в один сокет, и он появляется байт за байтом в буфере чтения другого сокета.

Когда вы делаете HTTP-запрос через Интернет, он обычно открывает сокет-соединение с сервером и отправляет заголовок запроса, за которым следует необязательное тело запроса. Это просто поток байтов, и принимающий сокет должен знать, когда сообщение получено полностью. Для HTTP это обрабатывается путем указания длины полезной нагрузки. Затем клиент может просто получать, пока не будет достигнута длина байтов, затем взять эти байты и проанализировать их как запрос. Чтобы ответить, он затем отправит ответ, используя ту же семантику. Перед закрытием соединения (если не указано в заголовке Keep-Alive).

По сути, это то, что мы называем фрейминг-сообщениями. Подробнее об этом позже.

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

До относительно недавнего времени в Интернете, как и в браузерах, не было эквивалента сокетам. Вы могли делать только асинхронные запросы ресурсов от клиента, но у сервера никогда не было возможности ответить, пока клиент не спрашивает его о чем-то. Примерно в то время, когда появился Node.js, возможность сервера отправлять внеполосные сообщения клиенту считалась невероятно полезной, особенно для асинхронного сервера, поэтому Socket.IO стал настолько популярным.

WebSockets были еще очень молоды, и еще не существовало стандарта протокола, который все браузеры соглашались внедрять. Потребовалось несколько лет, и многие стандарты завершились, прежде чем в 2011 году RFC 6455 стал официальным протоколом WebSocket.

Веб-сокеты

WebSockets - это не настоящие сокеты. Но они чертовски близки и работают замечательно.

Протокол WebSocket представил пару дополнительных атрибутов скромному сокету TCP. Во-первых, чтобы установить соединение, клиент должен запросить «обновление» с HTTP до WebSocket во время процесса установления связи. Это инициируется с помощью стандартного HTTP-запроса к заданной конечной точке WebSocket. Если сервер поддерживает WebSockets, он ответит ответом HTTP / 1.1 101 Switching Protocols, в котором клиенту следует оставить соединение открытым и использовать его в качестве потока.

После обновления соединения вы можете отправлять данные по сокету в обоих направлениях, но здесь есть второе главное отличие: вы не получаете поток байтов. Все сообщения автоматически оформляются сокетом. Таким образом, как только один конец закончит запись, сообщение будет доступно на другом конце, и сокет автоматически определит, сколько байтов принадлежит этому сообщению. Это значительно упрощает программирование WebSockets по сравнению с обычными сокетами.

WebSockets также реализуют свою собственную семантику Ping / Pong, чтобы поддерживать соединение в рабочем состоянии при отсутствии отправляемых данных. Это то, что вам обычно приходится учитывать с сокетами TCP, чтобы гарантировать, что другой конец не ушел.

Еще одна полезная функция возможности установить соединение с использованием HTTP - это возможность использовать семантику HTTP-аутентификации для сокета, такую ​​как заголовок авторизации для базовой аутентификации или аутентификации токена носителя.

Стратегии аутентификации

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

Распространенным шаблоном для этого является требование, чтобы первое сообщение через сокет было сообщением аутентификации, обычно называемым «рукопожатием аутентификации». Здесь две стороны обмениваются информацией друг о друге, и они соглашаются поддерживать соединение.

Обычно клиент обменивается с сервером примерно так:

CLIENT > AUTH: secretpasscodeletmein
SERVER < OK

Сервер, ожидающий сообщения типа AUTH, примет значение и проверит его подлинность. Если значение оказывается правильным, соединение остается открытым, и сервер отвечает каким-то сообщением OK. В противном случае он просто закрывает соединение.

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

Или используя существующий протокол, например STOMP или аналогичный.

Пример использования: чат

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

Реализовать что-то подобное с помощью WebSockets на самом деле довольно просто и гораздо более гибко, чем вы могли ожидать.

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

let client = {
 id: 1,
 isAuthenticated: true,
 protocolVersion: 1,
 connection: <WebSocket Connection instance>
}

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

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

Как установить подписку - решать вам, но это может быть так же просто, как обмен клиентом:

CLIENT > SUB:general
SERVER < OK
CLIENT > UNSUB:general
SERVER < OK

Затем, когда у вас есть сообщение для доставки - которое может исходить от другого клиента, внутреннего или запроса API - вам просто нужно найти всех клиентов, связанных с этим каналом, и отправить сообщение каждому из них. Пример:

const subscriptions = new Map();
subscriptions.set(“general”, new Set())

// Subscribe a client to “general” channel.
subscriptions.get(“general”).add(client.id);

// Send message to a channel
subscriptions.get(“general”).forEach((clientId) => {
  clients.get(clientId).connection.send(message);
});

// Unsubscribe
subscriptions.get(“general”).delete(client.id);

См. Https://www.npmjs.com/package/websocket для хорошей отправной точки для реализации сервера NodeJS, если вы используете Go, https://github.com/gorilla/websocket - это хороший выбор.

Совместимость с другими клиентами

Основным преимуществом внедрения WebSockets перед Socket IO является возможность взаимодействия с множеством различных клиентов. Существуют соответствующие стандартам реализации клиента WebSocket в ObjectiveC, Javascript, Go, Haskell, Erlang, Ruby, Swift и, конечно же, во всех основных браузерах. Так и должно быть. Вам не нужно адаптировать свой код для множества разных транспортов, которые делают одно и то же.

Благодаря Socket IO

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

Заключение

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

✉️ Подпишитесь на рассылку еженедельно Email Blast 🐦 Подпишитесь на Codeburst на Twitter и 🕸️ Изучите полную веб-разработку .