188. Сеть приложения: HTTP, WebSocket и MQTT
Кабель и BLE достают до прибора, который рядом. Всё остальное идёт через сеть, и выбор способа определяет, каким получится приложение: будет ли оно опрашивать, ждать или подписываться.
Разбираем три способа на одном и том же приборе. Ничего не выдумано: HTTP и WebSocket поднимаются прямо в проверке, MQTT — настоящий брокер Mosquitto в контейнере (тот же, что в разделе 87).
Как это называется в документации
Оригинал Перевод request-response запрос-ответ polling опрос: приложение само спрашивает push доставка по инициативе источника WebSocket upgrade переход HTTP-соединения в двусторонний режим broker, topic брокер, тема publish / subscribe публикация и подписка retained message сохранённое сообщение: последнее значение темы last will and testament (LWT) завещание: что брокер опубликует при обрыве QoS 0/1/2 уровень доставки keepalive период проверки живости
188.1 Три способа и чем они отличаются
| Способ | Кто начинает обмен | Чем хорош | Чем плох |
|---|---|---|---|
| HTTP | приложение | просто, проходит везде | новое значение узнаём только опросом |
| WebSocket | обе стороны | мгновенная доставка в обе стороны | соединение надо держать и поднимать |
| MQTT | обе стороны через брокера | много приборов и много слушателей, есть «последнее значение» | нужен брокер и его обслуживание |
Правило выбора простое. Настройки и команды — HTTP: редко, по делу, с понятным кодом ответа. Живые показания на экране — WebSocket. Парк приборов, который смотрят несколько приложений сразу, — MQTT.
188.2 HTTP: запрос-ответ с таймаутом и понятным отказом
Шлюз отдаёт последнее измерение в JSON, приложение его читает:
final HttpClientRequest request = await http.getUrl(url).timeout(timeout);
final HttpClientResponse response = await request.close().timeout(timeout);
if (response.statusCode != HttpStatus.ok) {
await response.drain<void>();
throw NetError('шлюз ответил отказом', code: response.statusCode);
}
Три вещи, без которых это не работает в поле:
- Срок ожидания на каждом шаге — соединение, ответ, чтение тела.
Молчащий шлюз даёт
TimeoutExceptionза 100 мс, а не вечное ожидание. - Код ответа — часть протокола. Отказ 503 превращается в ошибку с кодом внутри, и пользователь видит «связь: шлюз ответил отказом (код 503)», а не пустой экран.
- Тело дочитывается даже при отказе — иначе соединение остаётся висеть.
В проверке сервер поднимается в том же процессе (HttpServer.bind), и
все три случая — нормальный ответ, отказ 503 и молчание — проверяются
без единого настоящего сервера.
188.3 WebSocket: те же кадры, только по сети
WebSocket сохраняет границы сообщений: что шлюз отправил одним сообщением, то и придёт. Но шлюз обычно пересылает байты порта такими кусками, какими получил, и один кадр оказывается в двух сообщениях, а два — в одном. Поэтому поверх него работает тот же разборщик, что и в порту:
Stream<FrameEvent> wsFrames(Stream<dynamic> socket) =>
frames(socket.where((dynamic m) => m is List<int>).cast<List<int>>());
Кадры шлют только двоичными сообщениями: текст прошёл бы через UTF-8, и каждый
байт от 0x80 превратился бы в пару — 0xC0 стал бы C3 80, и CRC не сошлась бы.
Текстовое сообщение на таком канале — ошибка протокола, и разборщик его
отбрасывает.
В проверке сервер шлёт кадр двумя сообщениями (первые два байта и
остаток), приложение собирает его в один FrameReady и отвечает кадром
команды — сервер получает ровно encodeFrame(<int>[0x01, 0x01]).
Отсюда следствие, которое экономит много работы: весь код разбора, таймаутов и переподключения из раздела 186 переносится на сеть без изменений. Меняется только источник байт.
188.4 MQTT: брокер, сохранённое значение и завещание
MQTT переворачивает схему: прибор публикует в тему, приложение
подписывается, и они друг о друге не знают. Проверка работает с
настоящим eclipse-mosquitto:2 в контейнере.
Сохранённое значение (retain). Прибор публикует показание с флагом
retain, брокер его запоминает. Приложение, подключившееся через час,
получает последнее значение сразу при подписке — до того, как прибор
пришлёт следующее:
| Что проверено | Значение |
|---|---|
опубликовано с retain в meter/last |
3.356 |
| получено новым подписчиком сразу после подписки | 3.356 |
Это решает вечную проблему «экран пустой, пока прибор не пришлёт следующий кадр».
Завещание (LWT). Прибор при подключении говорит брокеру: если я
пропаду — опубликуй в meter/state слово offline. Проверка делает
это байтами, ровно как прошивка:
final body = <int>[
...str('MQTT'), 0x04, // уровень 3.1.1
0x06, // чистая сессия + завещание
0x00, 0x3C, // keepalive 60 с
...str(id), ...str(willTopic), ...str(willMessage),
];
После обрыва соединения (сокет просто уничтожается, без прощания)
подписчик получает offline — от брокера, а не от прибора. При прямом
WebSocket обрыв видно по закрытию соединения и по молчанию на ping, а
завещание решает другую задачу — сообщает об обрыве всем, кто с прибором
напрямую не связан.
Флаги 0x06 — чистая сессия и завещание, но без сохранения.
Подписчику, который уже ждёт, этого хватает, а приложение, открытое после
обрыва, завещания не увидит: в meter/state для него нет ничего — или
старое online. Поэтому в приборе завещание ставят с флагом сохранения —
0x26 (бит 0x20), а сразу после подключения прибор сам публикует в ту
же тему online с retain. Опыт с тем же брокером: подписчик, пришедший
после обрыва, при 0x06 получает online, при 0x26 — offline.
Когда брокер публикует завещание: сразу, если соединение закрылось без
DISCONNECT, и только через полтора keepalive — 90 с при 60 с, — если
прибор просто замолчал, например ушёл из зоны Wi-Fi (в опыте с keepalive
2 с — через 3–4 с). Keepalive выбирают по тому, как быстро нужно узнать
об обрыве.
188.5 Что выбрать под задачу
| Задача | Способ | Почему |
|---|---|---|
| прочитать настройки, записать калибровку | HTTP | редкие запросы, понятные коды ответа |
| живой график на экране | WebSocket | доставка без опроса, обе стороны |
| десять приборов и три приложения | MQTT | брокер разводит всех, retain даёт стартовое значение |
| «прибор пропал» на экране | MQTT + LWT | сообщение приходит от брокера |
| обновление прошивки | HTTP | файл, докачка, код ответа |
Смешивать нормально: команды по HTTP, показания по MQTT — обычная раскладка для парка приборов.
188.6 Грабли
Опрос вместо подписки. Приложение спрашивает раз в секунду, сеть и батарея расходуются, а показание всё равно опаздывает.
Нет срока ожидания. Пропавший шлюз подвешивает экран, и это единственное, что видит пользователь.
Код ответа проглочен. 503 и 200 с пустым телом для приложения одинаковы — и никто не знает, что сломалось.
Тело ответа не дочитано. Соединения копятся, через час шлюз отвечает отказом всем.
MQTT без retain. Экран пустой до первого сообщения прибора, то есть иногда минутами.
MQTT без завещания. Приложение показывает последнее значение и уверяет, что всё хорошо, хотя прибор отключён час назад.
Завещание без сохранения. Приложение, открытое после обрыва, его не
видит: нужен бит 0x20 в CONNECT и своё online с retain.
Разбор кадров переписан под WebSocket. Сообщения режутся не так, как байты в порту, и второй разборщик ловит свои ошибки — тот же код работает и там, и там.
188.7 Итоги
- HTTP — для редких запросов: срок ожидания на каждом шаге, код ответа как часть протокола, тело дочитывается всегда.
- WebSocket — для живых показаний: сообщения приходят кусками, поэтому разбор берётся тот же, что и в порту.
- MQTT — для парка приборов:
retainдаёт новому подписчику последнее значение сразу, завещание сообщает об обрыве от имени брокера. - Проверки не требуют ни прибора, ни интернета: HTTP и WebSocket поднимаются в том же процессе, брокер — контейнер рядом.
Дальше: раздел 189 «BLE и USB-serial: транспорт до прибора, MTU, очередь записей, разрешения».