사람들이 채팅 서비스에 모여 채팅방을 만들어 그룹화하고 메시지를 주고 받듯이, MQTT는 인터넷 네트워크에 참여한 다양한 장치들을 그룹화해서 서로 메시지를 주고 받을 수 있도록 해주는 프로토콜입니다.

이번 파트에서는 사물인터넷 구축할 때 사용할 수 있는 강력한 툴 중 하나인 MQTT 를 사용해보겠습니다. 그리고 MQTT 를 이용해서 센서장치, 서버, 모바일이 함께 동작하도록 만들어 보겠습니다. 그 전에, MQTT 의 동작 구조와 몇 가지 특징들을 기억해야 합니다.

MQTT 소개

MQTT (MQ Telemetry Transport)는 센서장치나 라즈베리파이 같은 임베디드 장치, 모바일 장치 사이의 통신을 위한 가벼운 메시징 프로토콜입니다. TCP/IP 기반으로 대역폭이 작은 네트워크에서 동작할 수 있도록 설계된 프로토콜입니다. 쉽게 얘기해서 임베디드 장치들을 위한 트위터라고 볼 수 있습니다.

MQTT 자체는 메시지를 어떻게 보낼 것인지를 정의하는 규약일 뿐입니다. 따라서 실제 “MQTT 트위터”를 동작시키기 위해서는 서버 역할을 해주는 장치와 프로그램이 필요한데 이를 MQTT 브로커(Broker)라고 합니다. MQTT 브로커는 각종 장치들(MQTT Client)이 보내주는 메시지를 수집하고 이걸 다시 필요한 장치들에게 전송해주는 중계서버 역할을 합니다.

MQTT 클라이언트는 브로커에게 메시지를 전달하거나 필요한 메시지를 받기만 하면 되고, MQTT 프로토콜의 구조도 간단하기 때문에 처리능력이 낮은 임베디드 장치에 잘 어울립니다. 메시지는 트위터처럼 140자 제한이 없으므로 긴 메시지나 JSON 포맷 또는 파일도 전송이 가능합니다. 이런 특징들 때문에 MQTT 프로토콜은 센서 네트웍을 구성하는데 유용한 도구로 관심을 받고 있습니다.

MQTT 동작 구조

트위터 서비스의 동작구조를 떠올려보세요. 트위터에서 사용자는 다른 사용자를 follow 할 수 있습니다. 그럼 다른 사용자가 생성하는 메시지(트윗)를 받아볼 수 있죠. 그리고 스스로 메시지를 생성할 수도 있습니다. 그럼 자신을 follow하는 사용자에게 메시지가 전달되죠.

MQTT도 거의 유사한 동작구조를 갖습니다. MQTT 시스템에 참여하는 MQTT 클라이언트는 메시지 발행(publish, 트윗에 해당), 메시지 구독(subscribe, follow에 해당) 두 가지 동작을 할 수 있습니다. MQTT 클라이언트가 메시지를 특정 채널(Topic, 토픽)에 발행하면 이 채널을 구독한 모든 클라이언트에게 메시지가 전달되는 겁니다. 중간에서 메시지를 수집, 재분해 하는 작업은 MQTT 브로커가 담당합니다.

토픽 (Topic)

메시지를 발행/구독(pub/sub) 하는 행위는 채널 단위로 일어납니다. 이를 MQTT에서는 토픽(Topic)이라고 합니다. 토픽은 슬래시(/)로 구분된 계층구조를 갖습니다.

topic_basics

메시지를 구독/발행 할 때 여러개의 토픽을 한번에 지정할 수 있도록 wild card 문자를 지원합니다. [+] 문자는 단 1개의(1 레벨) 토픽을 임의의 토픽으로 대체하는 와일드카드 문자입니다.

topic_wildcard_plus

반면 [#] 문자는 여러 레벨의 토픽을 대체할 수 있습니다. [myhome/#] 처럼 사용하면 myhome 의 하위 토픽들 모두를 지칭하게 됩니다. [#] 문자는 토픽 문자열 끝에만 사용할 수 있으며 하위 토픽 모두를 지칭합니다. (2단계 이상 하위 토픽도 포함)

topic_wildcard_hash

[$] 문자로 시작하는 토픽은 시스템에 의해 사용되는 특수한 토픽을 의미합니다. 이 토픽들은 [#] 문자열로 지정해도 포함되지 않는 토픽이 됩니다. 주로 [$SYS/] 로 시작하는 토픽들이 브로커의 내부 메시지를 위해 사용됩니다. (이에 대한 명확한 표준은 없는 상태인듯)

토픽 구조를 구성할 때 몇 가지 주의할 점이 있습니다.

  • 최상위 토픽이 [/] 문자로 시작하지 않도록 합니다. [/home/sensor/humid] 이렇게 사용할 수는 있지만 최상위 토픽이 이름이 없는 토픽이 되므로 사용하지 않는 것을 권장합니다.
  • 토픽 이름에 공백을 사용하지 않습니다.
  • 토픽 이름은 ASCII 문자만 사용합니다. (임베디드 장치와의 호환성을 위해 주의)
  • [#] 를 이용해서 토픽 전체를 구독하지 않도록 합니다. 오버헤드가 심할 경우 브로커/클라이언트 프로세스가 중단될 수 있습니다.

QoS (Quality of Service)

MQTT는 시스템에 참여하는 장치들의 처리 능력, 네트워크 대역폭, 메시지 오버헤드 등 주변상황에 맞게 시스템이 동작할 수 있도록 3단계 QoS(Quality of Service) 를 제공합니다.

  • 0 : 메시지는 한번만 전달하며, 전달여부를 확인하지 않는다. Fire and Forget 타입이다.
  • 1 : 메시지는 반드시 한번 이상 전달된다. 하지만 메시지의 핸드셰이킹 과정을 엄밀하게 추적하지 않기 때문에, 중복전송될 수도 있다.
  • 2 : 메시지는 한번만 전달된다. 메시지의 핸드셰이킹 과정을 추적한다. 높은 품질을 보장하지만 성능의 희생이 따른다.

0에 가까울수록 메시지 처리에 대한 부하가 적은 대신 메시지 손실 위험이 높아집니다. 2에 가까울수록 메시지 손실 위험은 줄어들지만 메시지 처리 부하가 급격히 늘어납니다.

(보통 0~1 정도의 QoS를 사용하면서 메시지 손실등의 위험은 상위 어플리케이션 차원에서 관리하도록 하는듯 합니다.)

MQTT 브로커 설치

MQTT 브로커는 시스템의 핵심 서버 역할을 하며,  여기에 메시지가 수집되고 다시 재분배 됩니다. 다양한 MQTT 브로커 프로그램들이 개발되어 있는데 ActiveMQ, Apollo, IBM Message SightJoramMQMosquittoRabbitMQSolace Message Routers 등이 자주 사용됩니다. 아래 링크에 MQTT 브로커의 특징을 비교한 자료가 있습니다.

여기서는 Mosquitto 브로커를 사용할 것입니다. Mosquitto는 MQTT의 기본 기능을 충실히 지원하는 가벼운 MQTT 브로커 프로그램입니다. Mosquitto 클라이언트 프로그램도 있으므로 여러대의 PC를 이용해서 테스트를 할 수 있습니다.

MQTT 동작 테스트를 위해 라즈베리파이에 Mosquitto MQTT 브로커를 설치하고 PC 및 모바일 폰에는 MQTT 클라이언트를 설치 할 것입니다.

라즈베리파이에 Mosquitto 브로커를 설치하는 작업부터 시작합니다. Mosquitto 는 apt-get 명령어를 통해 간단히 설치할 수도 있지만 업데이트가 되지 않는 문제점이 보고되고 있습니다. 아래 순서로 설치하길 권장합니다. (최신 소스코드를 다운로드 받아 설치하실땐 링크를 참고하세요.)

  • wget http://repo.mosquitto.org/debian/mosquitto-repo.gpg.key
  • sudo apt-key add mosquitto-repo.gpg.key
  • cd /etc/apt/sources.list.d/
  • sudo wget http://repo.mosquitto.org/debian/mosquitto-wheezy.list
  • sudo apt-get install mosquitto

설치가 완료되면 브로커는 1883 포트를 사용하게 됩니다.

이제 PC와 모바일 폰에 MQTT 클라이언트를 설치해서 연동을 해보겠습니다. PC(Windows, Linux, OS X)에서는 MQTT.fx 클라이언트를 설치하면 됩니다. 아래 링크에서 프로그램을 받을 수 있습니다.

MQTT.fx 클라이언트는 윈도우 64비트만 지원합니다. 이 문제 때문에 설치가 안된다면 크롬 브라우저의 확장 앱으로 설치해서 사용하는 방법도 있습니다. 크롬 브라우저에서 아래 링크를 통해 설치할 수 있습니다.

모바일 폰에서는 앱 스토어에서 MQTT로 검색하시면 됩니다. 다양한 클라이언트가 등록되어 있는데 대부분 비슷하므로 아무거나 사용하셔도 됩니다. 유명한 MQTT 클라이언트들은 링크에서 확인하실 수 있습니다.

설정 방법은 간단합니다. 일단 MQTT 브로커 연결을 위해 아래 항목들만 정확히 설정해 주면 됩니다.

  • MQTT 브로커 URL : MQTT 브로커를 설치한 서버의 URL 입니다.
    • 같은 공유기에 PC, 모바일 폰, 라즈베리파이가 같이 물려있다면 라즈베리파이의 내부 IP 주소(192.168.x.x)를 사용하면 됩니다.
  • Port : 앞서 설치한 Mosquitto 브로커는 기본 1883 포트를 사용합니다.
  • Username / Password : 지금은 설정하지 않아도 됩니다.

그리고 우리가 메시지를 받을 Topic 을 지정해주면 이후 해당 Topic 으로 들어오는 메시지를 볼 수 있습니다. MQTT 클라이언트에서 subscribe 버튼을 누르고 토픽에 messagebox를 입력합니다. 그럼 messagebox 토픽이 생성되고 구독이 됩니다. PC, 모바일 등 모든 장치에서 같은 작업을 해줍니다.

이제 PC, 모바일에서 publish 를 합니다. 이때 토픽은 messagebox로 선택합니다. 그럼 입력한 메시지가 브로커로 전달되고 messagebox 토픽을 구독하는 다른 장치들에도 전달되어야 합니다. 제대로 동작하는지 확인해보세요.

MQTT 브로커를 설치한 라즈베리파이에 MQTT 클라이언트를 설치해서 확인할 수도 있습니다.

  • apt-get install mosquitto-clients

아래 명령으로 메시지 발행(pub)을 할 수 있습니다. 다른 기기에도 메시지가 보여야겠죠.

  • mosquitto_pub -d -t messagebox -m “sent from RPi server”

특정 토픽을 구독(sub) 할 수 있습니다. 이 경우 특정 토픽에 메시지가 도착 할 때마다 표시됩니다. Ctrl+c 를 눌러 멈추기 전까지는 메시지 표시하는 상태로 유지됩니다.

  • mosquitto_sub -d -t messagebox

센서장치와 MQTT

이제 센서장치에 MQTT client 기능을 넣어서 데이터를 뿌리면 PC – 서버 – 모바일 폰 등 다양한 장치들이 메시지를 수신할 수 있겠죠?

센서장치에 MQTT 기능을 넣기 위해서는 라이브러리 설치가 필요합니다. 아두이노 개발환경을 실행해서 [메뉴 – 스케치 – 라이브러리 포함하기 – 라이브러리 관리] 를 순서대로 실행합니다. 그리고 PubSubClient 로 검색하세요. 발견된 라이브러리 중 PubSubClient 라이브러리를 설치하면 됩니다.

ESP32 모듈이 MQTT 를 이용해 데이터를 송신, 수신할 수 있도록 코드를 올려보겠습니다. 아래 스케치를 받아 ESP32에 올려주세요.

소스코드에서 아래 부분들을 수정해줘야 합니다.

const char* ssid = "your_ssid";
const char* password =  "your_pass";
const char* mqttServer = "192.168.1.9";
const int mqttPort = 1883;
const char* mqttUser = "yourMQTTuser";
const char* mqttPassword = "yourMQTTpassword";
const char* topic_pub = "messagebox";
const char* topic_sub = "messagebox2";

ssid, password 는 자신의 공유기 환경에 맞게 수정하세요. mqttServer 는 MQTT 브로커가 설치된 라즈베리파이 주소입니다. 같은 공유기 안에 있다면 내부 IP 주소를 넣어주면 됩니다. mqttUser, mqttPassword 는 사용하지 않습니다. topic_pub 가 메시지를 전송할 토픽 이름입니다. topic_sub 는 센서장치로 들어오는 메시지 수신을 위해 새로 만든 토픽입니다. (별도로 토픽을 만드는 작업은 필요 없습니다. 다른 장치에서 해당 토픽으로 보내기만 하면 됩니다.)

이제 스케치를 업로드하고, 시리얼 모니터를 켜세요. 그리고 PC, 모바일 폰 등에 설치된 MQTT client를 유심히 보세요.

messagebox 토픽으로 센서장치가 생성하는 랜덤한 숫자가 5초 간격으로 보일겁니다. 그리고 MQTT client 에서 messagebox2 토픽으로 메시지를 보내면, ESP32 가 받아서 시리얼 모니터에 표시해 줄겁니다.

이처럼 MQTT 를 활용하면 큰 힘 들이지 않고도 장치들을 자유자재로 연결할 수 있습니다.

ESP32 에 올린 소스코드를 보겠습니다. 초기화 함수인 setup() 함수부터 보죠.

void setup() {
  ......
  mqttClient.setServer(mqttServer, mqttPort);
  mqttClient.setCallback(callback);
 
  while (!mqttClient.connected()) {
    Serial.println("Connecting to MQTT...");
    
    if (mqttClient.connect("ESP32Client", mqttUser, mqttPassword )) {
      Serial.println("connected");
    } else {
      Serial.print("failed with state ");
      Serial.print(mqttClient.state());
      delay(2000);
    }
  }

  mqttClient.subscribe(topic_sub);
  mqttClient.publish(topic_pub, "ESP32 logged in");
  prevUpdateTime = millis();
}

공유기에 연결된 이후로 mqttClient.setServer() 함수를 호출해서 MQTT 서버 연결 정보를 등록합니다. 그리고 mqttClient.setCallback() 을 호출해서 subscribe 한 토픽에서 메시지를 받았을 때 호출될 함수를 지정해줍니다.  콜백함수에는 호출될 때 받은 메시지를 출력하는 코드만 넣어뒀습니다.

void callback(char* topic, byte* payload, unsigned int length) {
  Serial.print("Message arrived in topic: ");
  Serial.println(topic_sub);
 
  Serial.print("Message: ");
  for (int i = 0; i < length; i++) {
    Serial.print((char)payload[i]);
  }
 
  Serial.println();
  Serial.println("-----------------------");
}

이제 mqttClient.connect() 함수를 이용해 MQTT 브로커에 연결을 시도합니다. 연결이 완료되면 subscribe()  함수를 이용해 메시지 구독할 토픽을 지정해줍니다. public() 함수를 이용하면 특정 토픽에 메시지를 전송할 수 있습니다.

loop() 함수에서는 주기적으로 랜덤한 숫자를 생성해 messagebox 토픽으로 전송합니다.

void loop() {
  mqttClient.loop();

  unsigned long currentTime = millis();
  if(currentTime > prevUpdateTime + UPDATE_INTERVAL) {
    int i = random(100);
    std::stringstream st;
    st<<i;
    mqttClient.publish(topic_pub, st.str().c_str());
    prevUpdateTime = currentTime;
  }
}

서버에서 MQTT 처리하기

우리가 서버로 사용하는 라즈베리파이 홈 서버에는 MQTT 브로커가 설치되어 있습니다. 하지만 MQTT 브로커는 메시지를 중계할 뿐, 메시지를 받아 특정한 작업을 처리하지는 않습니다.

라즈베리파이 서버도 MQTT 메시지를 받으면 특정한 작업을 처리하도록 파이썬으로 코드를 만들어 보겠습니다. 먼저 python 에서 MQTT client 모듈을 쓸 수 있도록 설치작업을 해야합니다.

  • sudo pip3 install paho-mqtt

이제 아래에서 소스코드를 받아 라즈베리파이 적당한 곳에 올려두세요.

코드는 굉장히 간단합니다.

import paho.mqtt.client as mqtt
import time

mqttc = mqtt.Client('RaspberryPI')
mqttc.connect('test.mostquitto.org', 1883)
while mqttc.loop() == 0:
     mqttc.publish('rfd', 'Publish')
     time.sleep(10)

여기서 mqttc.connet() 함수 호출할 때 넘기는 파라미터를 수정해주세요. 라즈베리파이 자신에 MQTT 브로커가 설치되어 있으니 자신의 IP를 넣어주면 됩니다. 그리고 mqttc.publish() 함수를 이용해 메시지를 전송할 때 사용하는 topic 이 ‘messagebox’ 로 되어있는지 확인하세요.

수정이 끝났으면 이 파이썬 코드를 실행해보죠.

  • python3 mqtt-publish.py

실행시키면 터미널 창에는 아무 반응이 없을겁니다. 하지만 주기적으로 MQTT 로 메시지를 보내도록 되어 있습니다. PC/모바일 폰에서 MQTT client 로 확인해보세요. 아래처럼 “Message from RPi” 가 주기적으로 찍히면 정상입니다.

이번에는 반대로 MQTT 메시지를 받는 코드를 만들어 보겠습니다.

마찬가지로 소스를 받은 후 몇 가지 수정해줘야합니다.

import socket
import fcntl
import struct
import paho.mqtt.client as mqtt

def on_connect(client, userdata, flags, rc):
    print("Connected with result code "+str(rc))
    client.subscribe("hello/world")

def on_message(client, userdata, msg):
    print( msg.topic+" "+str(msg.payload))

def get_ipaddress(network):
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    return socket.inet_ntoa(fcntl.ioctl(
        s.fileno(),
        0x8915,  # SIOCGIFADDR
        struct.pack('256s', network[:15].encode('utf-8'))
    )[20:24])


port = 1883

client = mqtt.Client('RaspberryPI')
client.on_connect = on_connect
client.on_message = on_message

client.connect(get_ipaddress('eth0'), port, 60)
client.loop_forever()

MQTT 브로커에 연결되면 호출되는 콜백함수가 on_connect() 입니다. 여기서 client.subscribe() 함수를 이용해 특정 토픽을 구독하도록 짜여져있습니다. 따라서 구독할 topic 이름을 센서장치가 메시지를 보내주는 messagebox 로 수정해줘야 합니다.

def on_connect(client, userdata, flags, rc):
    print("Connected with result code "+str(rc))
    client.subscribe("hello/world")

나머지 코드는 수정할 부분이 없습니다. 왜냐면 라즈베리파이 자신이 IP 주소를 가져와서 사용하도록 코드가 짜여져 있거든요.

코드 중 on_message 함수는 구독한 채널로 메시지가 들어올 때 호출되는 콜백함수입니다. 센서장치의 데이터를 수신했을 때 호출되겠지요. 수신된 데이터를 출력만 합니다.

def on_message(client, userdata, msg):
    print( msg.topic+" "+str(msg.payload))

파일을 실행했을 때 진행되는 코드는 아래와 같습니다.

port = 1883

client = mqtt.Client('RaspberryPI')
client.on_connect = on_connect
client.on_message = on_message

client.connect(get_ipaddress('eth0'), port, 60)
client.loop_forever()

MQTT 브로커에 연결되었을 때, 구독한 topic 으로 메시지가 들어왔을 때 실행할 콜백 함수를 등록해 줬습니다. 그리고 client.connect() 함수를 이용해 MQTT 브로커에 연결했습니다. client_loop_forever() 함수를 호출하면 MQTT 관련 작업이 백그라운드에서 계속 실행됩니다. 그리고 코드 진행은 여기서 멈춥니다. (사용자가 Ctrl+C 를 눌러 강제로 멈추기 전까지는요.)

이상의 mqtt.py 파일을 실행하면 아래와 같이 센서장치의 데이터를 받아 출력해 줄겁니다.

  • python3 mqtt.py

이 코드에서 살을 붙여나가면 서버에서 각종 MQTT 메시지를 받아 처리할 수 있겠죠!! 물론 MQTT 메시지는 파이썬 외에 Node.js 등 다른 플랫폼에서도 라이브러리를 통해 같은 방식의 제어가 가능합니다.

모바일에서 MQTT 처리하기

안드로이드나 iOS 같은 모바일 장치에 MQTT client 를 설치했다면 센서장치가 전송하는 메시지를 수실할 수 있을겁니다. 모바일 폰에 이런 기능을 직접 구현하고 싶다면 Paho 라이브러리를 사용하길 추천합니다. Paho 프로젝트는 Java 뿐 아니라 C/C++, JavaScript, Android, Python, Go, Rust, C# 등 다양한 언어에서 MQTT client 를 구현할 수 있도록 라이브러리를 제공하는 프로젝트입니다.

Paho 홈페이지에서 안드로이드 라이브러리를 이용하면 MQTT client 를 안드로이드 앱 안에 넣을 수 있습니다.

다만 페이지 업데이트가 느리고 라이브러리 코드가 약간 복잡해서 사용이 어려울 수도 있을겁니다. 안드로이드용 라이브러리를 이용한 안드로이드 앱 샘플을 제작해서 올려뒀으니 아래 코드를 다운로드 받아 분석하고 실행해보세요.

앱을 다운로드 받은 후 직접 빌드하고, APK 파일을 안드로이드 폰에 올려 설치해 보시기 바랍니다.

MQTT를 응용해서 음성으로 센서장치 제어가 가능하도록 만든 안드로이드 앱도 코드를 공유하고 있습니다. 아래 링크를 참고하세요.

안드로이드는 코드 양이 많고 라이브러리 사용법이 꽤 복잡해서 여기서 다루지는 않습니다. 하지만 MQTT 의 동작구조를 숙지하고, Java 언어와 Android 플랫폼에 대한 이해가 있다면 직접 분석하실 수 있을겁니다.

활용

사물인터넷에 참가하는 센서나 PC, 모바일 폰 등… node에 해당하는 장치와 사용자가 많을수록, 서버에 전달된 메시지를 적재적소에 직접 분배하기는 어려워집니다. MQTT 를 사용하면 Topic 기반한 채널 관리로 이런 수고를 상당 부분 줄일 수 있습니다. 실제 페이스북이나 우아한 형제들에서도 MQTT 를 서비스에 활용하는 것으로 알려져 있습니다.

당장 사용하지는 않더라도 MQTT 의 존재와 활용 가능성은 염두에 두세요. 구현하는 서비스의 규모가 점점 커질수록 MQTT 에 대한 향수가 점점 커질지도 모릅니다.

참고자료

주의!!! [사물 인터넷 네트워크와 서비스 구축 강좌] 시리즈 관련 문서들은 무단으로 내용의 일부 또는 전체를 게시하여서는 안됩니다. 계속 내용이 업데이트 되는 문서이며, 문서에 인용된 자료의 경우 원작자의 라이센스 문제가 있을 수 있습니다.

.

.

강좌 전체보기

.