Инструменты Node.js разработчика. Работа с веб-сокетами по протоколу mqtt

в 23:30, , рубрики: javascript, mqtt, node.js, nodejs, WebSocket

Работа с веб-сокетми позволяет в веб-приложении или в мобильном приложении организовывать отправку сообщений с сервера на клиент, что невозможно сделать средствами REST-API. Для работы с веб-сокетами часто используется библиотека socket.io, или же разработчики работают с нативными объектами веб-сокет браузеров. В этом сообщении я попытаюсь показать, что оба пути не решают всех проблем, и гораздо лучше использовать для работы с веб-сокетами специализированные серверы, например mqtt-сервер (раньше его назвали mqtt-брокер).

Справедливости ради, и чтобы избежать ненужных споров, сразу замечу, что кроме mqtt-сервера может быть использован еще целый ряд серверов, например rabbitmq.

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

Приходится на уровне приложения организовывать подтверждение получения сообщений и повторную их доставку. Я хотел бы особо обратить внимание на фразу «на уровне приложения» (а хотелось бы чтобы это было на уровне протокола).

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

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

Более эффективным был бы путь вынесения этой логики на уровень протокола. И такой протокол существует — это mqtt. Первая версия протокола mqtt была разработана Энди Стэнфорд-Кларком (IBM) и Арленом Ниппером (Arcom) в 1999. Спецификация MQTT 3.1.1 была стандартизирована консорциумом OASIS в 2014 году.

В протоколе mqtt есть параметр «качество сервиса» (qos), которое может принимать значения:
0 — сообщение доставляется если есть возможность;
1 — сообщение доставляется гарантированно, но могут быть дубликаты;
2 — сообщение доставляется гарантированно и гарантированно однократно.

То есть, проблему с гарантированной доставкой сообщений протокол mqtt решает, и это вопрос снимается с повестки дня. Но не только этот вопрос.

Производительность и масштабирование.

При работе с веб-сокетами все подключенные девайсы оставляют открытыми соединения с сервером, даже если нет реального обмена сообщениями. Эта нагрузка по своему характеру отличается от нагрузки по количеству запросов в REST-API, и ее сложно сэмулировать при тестировании приложения. Если мы передаем всю работу с веб-сокетами на специализированный сервер mqtt, то наше приложение на nodejs открывает всего лишь одно соединение по веб-сокету (или по tcp, т.к. mqtt поддерживает оба протокола), и мы можем масштабировать наше приложение, подключая к серверу mqtt несколько экземпяров nodejs.

Если исчерпаются ресурсы одного mqtt-сервера, можно организовать кластер серверов mqtt, при этом не затрагивая приложения на nodejs.

Теперь перейдем к примеру.

mqtt-сервер или брокер, как его именовали в прежних спецификациях, работает по модели отправки сообщений/подписки на сообщения. Сообщения отправляются в топик. Получатель подписывается на топик сообщений. И отправитель и получатель имеют два идентификатора: clientId (идентификатор устройства) и userName (имя пользователя).

Для работы с протоколом mqtt на стороне клиента используется библиотека github.com/eclipse/paho.mqtt.javascript. Реализаций серверов несколько, в том числе есть бесплатные. Мы в этом примере будем использовать сервер emqx, который запускается через docker-compose (см. github.com/apapacy/tut-mqtt).

Для тестирования создадим документ, в котором будем задавать clientId, userName и текст сообщения:

<script src="/paho-mqtt.js"></script>
<script src="/messages.js"></script>
<form name="sender" onsubmit="return false">
  <input type="text" name="user">
  <input type="text" name="client">
  <input type="text" name="message">
  <input type="button" onclick="connect()" value="connect">
  <input type="button" onclick="send()" value="send">
</form>

Отправка сообщений реализована в файле message.js:

var client;

var connectOptions = {
  timeout: 30,
  reconnect: true,
  cleanSession: false,
  mqttVersion: 4,
  keepAliveInterval: 10,
  onSuccess: onConnect,
  onFailure: onFailure
}


function connect() {
  try {
    client = new Paho.Client('localhost', 8083, '/mqtt', document.forms.sender.client.value);
    connectOptions.userName = document.forms.sender.user.value;
    client.connect(connectOptions);
  } catch (ex) {
    console.log(ex);
  }
}

function onConnect() {
  console.log('on connect');
  client.onMessageArrived = function(message) {
    console.log("onMessageArrived: " + message.payloadString);
  }
  client.subscribe("test", { qos: 2 });
}

function onFailure(err) {
  console.log('on failure', JSON.stringify(err));
}

function send() {
   var message = new Paho.Message(document.forms.sender.message.value);
   message.destinationName = "test";
   message.qos = 2;
   client.send(message);
}

Для проверки сначала откройте в браузере файл index.html, задайте clientId, userName, текст сообщения и отправьте несколько сообщений (их можно прочитать в консоли, т.к. клиент отправляет сообщения в топик test, и сам же подписан на этот топик).

Теперь откройте другой браузер или другую вкладку браузера, и присоединитесь с другими (это важно) clientId и userName. Отправьте еще несколько сообщений из первого браузера и убедитесь что они приходят обоим клиентам, т.к. у них различаются clientId и они оба подписаны на топик test.

Теперь закройте второй браузер (или вторую вкладку браузера) и отправьте еще несколько сообщений. После этого вновь откройте второй браузер и присоединитесь с теми же clientId и userName. Убедитесь в логах консоли что к Вам пришли все сообщения, которые отправлялись при закрытом браузере. Так произошло потому что:

  • При отправке сообщения задан уровень качества qos=2;
  • Вы уже присоединялись раньше к этому же топику с этими же clientId и userName, задав qos=2;
  • В опциях соединения задан параметр cleanSession: false.

Код примера можно скачать в репозитории.

apapacy@gmail.com
29 сентября 2019 года

Автор: apapacy

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js