← 글 목록

notes

MQTT Protocool

MQTT 프로토콜의 토픽·QoS·브로커 구조와 통신 흐름 정리.

IoT·실시간 센서 쪽을 붙이다가 HTTP 요청-응답만으로는 불편해서 MQTT를 다시 봤다. 경량 pub/sub이라 브로커만 가운데 두면 발행자·구독자가 서로의 IP를 몰라도 된다.

  • MQTT는 브로커 하나를 가운데 둔 발행·구독 방식이라 기기끼리 서로의 주소를 몰라도 된다.
  • 토픽은 home/room1/temp처럼 계층으로 짓고, 구독할 때 +(한 단계)와 #(나머지 전부)를 쓴다.
  • QoS는 0·1·2 세 단계이고, 브라우저는 웹소켓 리스너(보통 9001)로 붙는다.

HTTP랑 뭐가 다르냐

HTTP는 클라이언트가 서버에 요청 → 응답. MQTT는 Broker / Publisher / Subscriber.

공간·시간·동기화가 분리된다. 발행자가 오프라인이어도 브로커 정책에 따라 메시지를 들고 있을 수 있고, 구독자는 여러 명이 같은 토픽을 볼 수 있다(1:N).

용어

토픽과 와일드카드

토픽은 대소문자를 구분한다. 구독할 때 +는 딱 한 단계, #는 맨 끝에서만 쓸 수 있고 그 아래 전부를 받는다. 발행할 때는 와일드카드를 못 쓴다. $SYS/로 시작하는 토픽은 브로커가 자기 상태를 올리는 곳이라 #로 구독해도 딱려오지 않는다.

처음엔 토픽을 대충 지었다가 나중에 구독 패턴이 안 맞아서 다 고쳤다. 장소/기기/측정값 순서처럼 넓은 것에서 좁은 것 순으로 정해 두면 +랑 #로 거의 다 풀린다. 맨 앞에 /를 붙이면 빈 레벨이 하나 생겨서 헷갈리니 안 붙인다.

QoS

QoS는 발행자와 브로커 사이, 브로커와 구독자 사이에 각각 적용된다. 발행자가 2로 보내도 구독자가 0으로 구독했으면 그쪽은 0으로 내려간다.

센서 텔레메트리는 0, 명령·결제성 신호는 1~2를 고민한다.

연결 흐름 (짧게)

  • 클라이언트가 브로커 IP/포트로 TCP 연결
  • CONNECT → CONNACK
  • PUBLISH / SUBSCRIBE
  • 브로커가 토픽 구독자에게 배포

패킷은 고정 헤더 + (가변 헤더) + 페이로드. 최소 패킷이 작아서 저전력·느린 망에 잘 맞는다.

프론트에서

브라우저는 TCP 소켓을 직접 못 열어서 1883에는 못 붙는다. 그래서 브로커에 웹소켓 리스너를 하나 더 열거나, 백엔드가 대신 구독해서 SSE나 웹소켓으로 넘겨 준다. 실험할 땐 앞의 방식이 제일 빠르다.

메시지가 초당 수십 개씩 들어오면 파싱만으로도 UI가 버벅인다. 그때는 MQTT 클라이언트를 Web Worker 안에서 돌리고, 화면에 필요한 값만 묶어서 postMessage로 메인 스레드에 넘기는 식으로 Next에 붙였다.

// mqtt.worker.js
import mqtt from 'mqtt';

const client = mqtt.connect('ws://localhost:9001');
client.on('connect', () => client.subscribe('home/+/temp'));
client.on('message', (topic, payload) => {
  postMessage({ topic, value: Number(payload.toString()) });
});

로컬에서는 Mosquitto 이미지를 compose에 넣고 1883과 9001만 열어서 실험했다.

services:
  mosquitto:
    image: eclipse-mosquitto:2
    ports:
      - "1883:1883"
      - "9001:9001"
    volumes:
      - ./mosquitto.conf:/mosquitto/config/mosquitto.conf

Mosquitto 2부터는 설정 파일이 없으면 localhost에서만 받고 익명 접속도 막힌다. 컨테이너 밖에서 붙으려면 리스너를 직접 적어야 한다. 처음엔 이걸 몰라서 포트는 열려 있는데 연결이 안 돼서 한참 헤맸다.

# mosquitto.conf (로컬 실험용)
listener 1883
listener 9001
protocol websockets
allow_anonymous true

실제 서버에 올릴 때는 allow_anonymous false로 바꾸고 password_file을 지정한다. mosquitto_passwd -c /mosquitto/config/passwd 사용자명으로 만들 수 있다.

알아두면 편한 기능: retained, LWT, keepalive
const client = mqtt.connect('mqtt://localhost:1883', {
  keepalive: 30,
  will: { topic: 'devices/esp32-1/status', payload: 'offline', retain: true, qos: 1 },
});
client.on('connect', () => {
  client.publish('devices/esp32-1/status', 'online', { retain: true, qos: 1 });
});

LWT와 retained를 같이 쓰면 상태 토픽을 구독하기만 해도 기기가 지금 살아 있는지 바로 알 수 있다.