Broker MQTT miễn phí trên mạng: HiveMQ Cloud, EMQX Serverless, tính hạn mức và nối ESP32 qua TLS

So sánh broker MQTT miễn phí HiveMQ Cloud, EMQX Serverless và broker công cộng, tính lưu lượng theo chu kỳ gửi, chọn chứng chỉ TLS và chương trình ESP32 có Last Will, tự nối lại.

Muốn ESP32 ở nhà gửi dữ liệu tới điện thoại ở bất kỳ đâu mà không thuê máy chủ, cách nhanh nhất là dùng một broker MQTT miễn phí trên mạng. HiveMQ Cloud và EMQX Serverless đều cho một gói miễn phí có TLS và tài khoản riêng, đủ để làm dự án nhỏ. Bài này so sánh hai dịch vụ với broker công cộng, tính lưu lượng để biết hạn mức có đủ không, và đưa chương trình ESP32 kết nối TLS đúng cách.

1. Chọn broker nào

Bảng so sánh HiveMQ Cloud, EMQX Serverless, broker công cộng và tự cài Mosquitto về tài khoản, bảo mật, hạn mức miễn phí, vượt hạn mức, cổng và trường hợp dùng
Hạn mức có thể đổi, xem lại trên trang giá của hãng trước khi dùng.
  • Broker công cộng như broker.emqx.io, test.mosquitto.org: không cần đăng ký nên rất tiện để thử. Nhưng ai cũng đăng ký được mọi topic, nên tuyệt đối không dùng để điều khiển thiết bị thật hay gửi dữ liệu riêng.
  • HiveMQ Cloud gói Serverless: 100 phiên kết nối, 10 GB mỗi tháng. Hợp với dự án thử nghiệm có nhiều thiết bị.
  • EMQX Serverless: tính theo phiên-phút, mỗi tháng 1 triệu phiên-phút và 1 GB, tối đa 1.000 kết nối cùng lúc. 1 triệu phiên-phút tương đương khoảng 23 thiết bị bật liên tục cả tháng. Để hạn chi tiêu bằng 0 thì khi hết hạn mức, dịch vụ dừng thay vì phát sinh tiền.
  • Tự cài Mosquitto: nếu có máy chạy 24/7 và chỉ dùng trong nhà, cách này tự do nhất. Xem bài cùng cụm về cài Mosquitto trên Windows.

Gói miễn phí không có cam kết chất lượng, và có thể đổi điều kiện. Dự án thương mại, thiết bị của khách hàng thì nên dùng gói trả phí hoặc máy chủ riêng. Khái niệm topic, QoS, retained có ở bài cùng cụm MQTT là gì; cách dùng cơ bản có ở bài dùng MQTT với ESP32.

2. Tính xem hạn mức có đủ không

Một gói PUBLISH QoS 0 gồm 1 byte đầu, 1 byte độ dài, 2 byte độ dài topic, rồi topic và dữ liệu. Topic 30 ký tự, JSON 60 ký tự thành 94 byte. Lưu lượng tính cả hai chiều: một lần vào broker và một lần ra cho mỗi bên nhận.

Đồ thị lưu lượng MQTT mỗi tháng theo chu kỳ gửi cho 10 và 50 thiết bị so với hạn mức 1 GB của EMQX và 10 GB của HiveMQ
Tin 94 byte, một bên nhận, chưa tính phần TLS và TCP.
Số thiết bịMỗi 1 giâyMỗi 10 giâyMỗi 60 giây
10,49 GB/tháng0,05 GB/tháng0,01 GB/tháng
104,9 GB/tháng0,49 GB/tháng0,08 GB/tháng
5024 GB/tháng2,4 GB/tháng0,41 GB/tháng

Thiết bị đo gửi mỗi 10–60 giây thì hạn mức miễn phí dư sức. Bản tính thô trên chưa gồm phần đệm TLS, TCP và gói giữ kết nối; hãy chừa khoảng 50 % để an toàn. Sửa số trong script dưới đây theo dự án của bạn:

# PYWORLD.VN - Tính lưu lượng MQTT mỗi tháng để biết có nằm trong hạn mức miễn phí của broker trên mạng không
# PYWORLD.VN - Đếm byte gói PUBLISH QoS 0: 1 byte đầu, độ dài còn lại, 2 byte độ dài topic, topic, dữ liệu; không tính phần TLS, TCP
# PYWORLD.VN - Chạy: python tinh_luu_luong_mqtt.py          (chỉ dùng thư viện chuẩn; sửa các hằng số theo dự án)
PHUT_MOI_THANG = 60 * 24 * 30


def byte_moi_tin(do_dai_topic: int, do_dai_du_lieu: int) -> int:
    con_lai = 2 + do_dai_topic + do_dai_du_lieu
    so_byte_do_dai = 1 if con_lai < 128 else 2 if con_lai < 16384 else 3
    return 1 + so_byte_do_dai + con_lai


def gb_moi_thang(so_thiet_bi: int, chu_ky_s: float, do_dai_topic: int = 30, do_dai_du_lieu: int = 60, so_nguoi_nhan: int = 1) -> float:
    """Lưu lượng hai chiều: một lần vào broker và so_nguoi_nhan lần đi ra."""
    so_tin = so_thiet_bi * PHUT_MOI_THANG * 60 / chu_ky_s
    return so_tin * byte_moi_tin(do_dai_topic, do_dai_du_lieu) * (1 + so_nguoi_nhan) / 1e9


def phien_phut(so_thiet_bi: int) -> int:
    return so_thiet_bi * PHUT_MOI_THANG


if __name__ == "__main__":
    print("Mot tin:", byte_moi_tin(30, 60), "byte")
    print("Thiet bi chay lien tuc voi 1 trieu phien-phut:", round(1e6 / PHUT_MOI_THANG, 1))
    for so in (1, 10, 50):
        for chu_ky in (1, 10, 60, 300):
            print(f"{so:>3} thiet bi, moi {chu_ky:>3} s: {gb_moi_thang(so, chu_ky):8.3f} GB/thang")

3. Tạo broker

Năm bước: đăng ký tạo cụm miễn phí, tạo tài khoản MQTT, lấy địa chỉ cổng 8883, thử bằng MQTTX hoặc Python, nạp cho ESP32
Thử bằng phần mềm trên máy tính trước khi nạp mạch.
  1. Đăng ký tài khoản, tạo cụm (cluster) gói miễn phí. Chọn khu vực gần Việt Nam nếu có.
  2. Vào phần quản lý truy cập, tạo tài khoản MQTT gồm tên và mật khẩu. Đây không phải tài khoản đăng nhập web.
  3. Chép địa chỉ cụm và cổng TLS 8883.
  4. Thử bằng phần mềm MQTTX hoặc script Python với thư viện paho-mqtt: đăng ký topic pyworld/#, gửi thử một tin.

4. TLS: chứng chỉ nào cho dịch vụ nào

Cổng 8883 mã hóa toàn bộ kết nối. ESP32 cần chứng chỉ gốc để xác minh đúng là broker của bạn, không phải máy giả mạo.

  • HiveMQ Cloud dùng chứng chỉ Let's Encrypt, gốc là ISRG Root X1, có sẵn trong chương trình của bài.
  • EMQX Serverless dùng chứng chỉ gốc khác, tải file CA trong trang tổng quan của cụm, rồi thay nội dung hằng số.

Không nên dùng setInsecure() ngoài lúc thử nhanh: mã hóa vẫn có nhưng không xác minh máy chủ. Lý do và cách kiểm tra có ở bài HTTPS trên ESP32. Một kết nối TLS tốn khoảng 40–50 KB RAM, nên in heap để theo dõi.

5. Chương trình ESP32

Sửa các hằng số ở đầu file. Bài dùng gói esp32 3.3.10 và thư viện PubSubClient. Chương trình gửi một tin JSON mỗi 30 giây, nhận lệnh on, off để bật tắt LED GPIO2.

// PYWORLD.VN - ESP32 gửi dữ liệu lên broker MQTT miễn phí trên mạng (HiveMQ Cloud) qua TLS cổng 8883, có kiểm tra chứng chỉ
// PYWORLD.VN - Có Last Will báo mất kết nối, mã client lấy theo địa chỉ MAC, nhận lệnh bật tắt LED, tự nối lại giãn dần
// PYWORLD.VN - Thay TEN_WIFI, MAT_KHAU_WIFI, MAY_CHU, TAI_KHOAN, MAT_KHAU_MQTT. EMQX Serverless dùng chứng chỉ gốc khác: xem bài

#include <PubSubClient.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>

const char *TEN_MANG = "TEN_WIFI";
const char *MAT_KHAU_MANG = "MAT_KHAU_WIFI";
const char *MAY_CHU = "TEN_CUM.s1.eu.hivemq.cloud";   // địa chỉ cụm lấy trong trang quản trị
const uint16_t CONG_TLS = 8883;
const char *TAI_KHOAN = "esp32";
const char *MAT_KHAU_MQTT = "MAT_KHAU_MQTT";
const int CHAN_LED = 2;
const uint32_t CHU_KY_GUI_MS = 30000;

// Chứng chỉ gốc ISRG Root X1 (Let's Encrypt), dữ liệu công khai, không phải bí mật.
const char *ISRG_ROOT_X1 =
  "-----BEGIN CERTIFICATE-----\n"
  "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\n"
  "-----END CERTIFICATE-----\n";

WiFiClientSecure mangTls;
PubSubClient mqtt(mangTls);
char maClient[24], topicDuLieu[48], topicLenh[48], topicTrangThai[48];
uint32_t lucGui = 0, lucThuLai = 0, choMs = 2000, soTin = 0;

void khiCoTin(char *topic, byte *duLieu, unsigned int doDai) {
  Serial.printf("Lenh [%s]: %.*s\n", topic, (int)doDai, (const char *)duLieu);
  if (doDai == 2 && memcmp(duLieu, "on", 2) == 0) {
    digitalWrite(CHAN_LED, HIGH);
  } else if (doDai == 3 && memcmp(duLieu, "off", 3) == 0) {
    digitalWrite(CHAN_LED, LOW);
  }
}

bool ketNoiBroker() {
  Serial.print("Ket noi broker... ");
  // Last Will: broker tự đăng "offline" lên topicTrangThai nếu mạch mất kết nối bất thường.
  if (!mqtt.connect(maClient, TAI_KHOAN, MAT_KHAU_MQTT, topicTrangThai, 1, true, "offline")) {
    char loi[100];
    Serial.printf("loi %d\n", mqtt.state());
    if (mangTls.lastError(loi, sizeof(loi)) != 0) {
      Serial.printf("  TLS: %s\n", loi);
    }
    return false;
  }
  Serial.println("xong");
  mqtt.publish(topicTrangThai, "online", true);
  mqtt.subscribe(topicLenh, 1);
  return true;
}

void setup() {
  Serial.begin(115200);
  pinMode(CHAN_LED, OUTPUT);
  WiFi.mode(WIFI_STA);
  WiFi.begin(TEN_MANG, MAT_KHAU_MANG);
  while (WiFi.status() != WL_CONNECTED) {
    delay(300);
  }
  Serial.printf("Wi-Fi: %s\n", WiFi.localIP().toString().c_str());

  const uint64_t mac = ESP.getEfuseMac();
  snprintf(maClient, sizeof(maClient), "esp32-%06llx", mac >> 24);   // mỗi mạch một mã, trùng mã là bị đá nhau
  snprintf(topicDuLieu, sizeof(topicDuLieu), "pyworld/%s/du-lieu", maClient);
  snprintf(topicLenh, sizeof(topicLenh), "pyworld/%s/lenh", maClient);
  snprintf(topicTrangThai, sizeof(topicTrangThai), "pyworld/%s/trang-thai", maClient);
  Serial.printf("Ma client: %s\n", maClient);

  mangTls.setCACert(ISRG_ROOT_X1);
  mqtt.setServer(MAY_CHU, CONG_TLS);
  mqtt.setCallback(khiCoTin);
  mqtt.setKeepAlive(30);
  mqtt.setSocketTimeout(10);             // bắt tay TLS mất vài giây
  mqtt.setBufferSize(256);
}

void loop() {
  if (!mqtt.connected()) {
    if (millis() - lucThuLai >= choMs) {
      lucThuLai = millis();
      if (ketNoiBroker()) {
        choMs = 2000;
      } else {
        choMs = min(choMs * 2, (uint32_t)60000);  // giãn dần, không dội broker khi mạng lỗi
      }
    }
    return;
  }
  mqtt.loop();
  if (millis() - lucGui >= CHU_KY_GUI_MS) {
    lucGui = millis();
    char json[80];
    snprintf(json, sizeof(json), "{\"tin\":%u,\"rssi\":%d,\"heap\":%u}", (unsigned)++soTin, WiFi.RSSI(), (unsigned)ESP.getFreeHeap());
    Serial.printf("Gui: %s %s\n", json, mqtt.publish(topicDuLieu, json) ? "ok" : "LOI");
  }
}
Serial Monitor in Wi-Fi, mã client, kết nối broker xong, gửi JSON ok và nhận lệnh on trên topic pyworld
Hình minh họa kết quả mong đợi.
  • Mã client lấy theo MAC: broker chỉ cho một kết nối với mỗi mã, hai mạch trùng mã sẽ đá nhau liên tục.
  • Last Will dạng retained: mất điện, rớt mạng thì broker tự đăng offline, bên nhận biết ngay mạch đã mất.
  • setSocketTimeout(10): bắt tay TLS lâu hơn kết nối thường. Để quá ngắn sẽ báo lỗi -2 giả.
  • Nối lại giãn dần từ 2 giây tới 60 giây, để không dội broker và không khóa chương trình trong vòng while.
  • Muốn chắc số gửi đi không mất khi rớt mạng thì cần đệm trên mạch, xem bài cùng cụm về MQTT mất kết nối.

6. Xử lý lỗi thường gặp

Hiện tượngNguyên nhânCách xử lý
mqtt.state() là -2, TLS báo lỗi chứng chỉSai chứng chỉ gốc, đồng hồ sai, sai địa chỉChép đúng địa chỉ cụm, dùng đúng file CA của dịch vụ
state() là 4 hoặc 5Sai tài khoản MQTT, hoặc tài khoản chưa có quyền topicTạo lại tài khoản, xem phần quyền topic
Rớt rồi nối lại mỗi vài giâyHai mạch trùng mã clientDùng mã theo MAC như trong code
Máy tính nối được, ESP32 khôngThiếu RAM, chứng chỉ thiếu ký tự xuống dòngIn heap; giữ đúng từng dòng PEM
Chạy tốt rồi tự ngắt sau vài chục giâyKhông gọi mqtt.loop() đềuKhông dùng delay() dài trong loop()
Đột nhiên không nối được cả hệ thốngHết hạn mức hoặc cụm bị dừngXem bảng theo dõi trên trang quản trị

7. Xem thêm

Muốn có giao diện đẹp mà không tự làm, xem Blynk với ESP32. Đưa thiết bị vào nhà thông minh: Home Assistant qua MQTT.

Sản phẩm dùng trong bài: ESP32 DevKit V1 30 chân.

Bài cùng cụm chủ đề: MQTT là gì; cài Mosquitto trên Windows; MQTT mất kết nối.


Tin tức liên quan

MicroPython cho học sinh: dòng lệnh Python đầu tiên trên ESP32
MicroPython cho học sinh: dòng lệnh Python đầu tiên trên ESP32

Hướng dẫn học sinh viết những dòng MicroPython đầu tiên trên ESP32 với Thonny: gõ lệnh trong Shell, bật tắt đèn, nháy đèn, chạm tay bật đèn, trò đoán số, lưu main.py.

Timer phần cứng trên ESP32: ngắt định kỳ chính xác tới micro giây, API mới của gói esp32 3.x
Timer phần cứng trên ESP32: ngắt định kỳ chính xác tới micro giây, API mới của gói esp32 3.x

Timer phần cứng ESP32 hoạt động thế nào, API timerBegin, timerAlarm của gói esp32 3.x, quy tắc viết hàm ngắt, thí nghiệm so ngắt timer 500 µs với vTaskDelay(1) khi CPU bận.

Hàn linh kiện dán SMD bằng tay: cỡ 0805, SOT-23, SOIC, dụng cụ, các bước và lỗi hay gặp
Hàn linh kiện dán SMD bằng tay: cỡ 0805, SOT-23, SOIC, dụng cụ, các bước và lỗi hay gặp

Các cỡ linh kiện dán 1206, 0805, 0603, 0402, SOT-23, SOIC; dụng cụ cần có; bốn bước hàn điện trở, tụ dán; hàn IC bằng kéo thiếc; nhận ra mối hàn tốt và cách sửa lỗi.

Cảm biến gia tốc: cấu tạo MEMS răng lược, áp điện, cách chế tạo và đo góc nghiêng bằng ESP32
Cảm biến gia tốc: cấu tạo MEMS răng lược, áp điện, cách chế tạo và đo góc nghiêng bằng ESP32

Cảm biến gia tốc hoạt động thế nào: mô hình khối lượng – lò xo, MEMS răng lược điện dung vi sai, chế tạo vi cơ bề mặt, áp điện, servo, đo góc nghiêng và đọc MPU-6050 bằng ESP32.

Đo dao động công trình: chọn gia tốc kế theo mức nhiễu, dải đo, tần số lấy mẫu và độ dài bản ghi
Đo dao động công trình: chọn gia tốc kế theo mức nhiễu, dải đo, tần số lấy mẫu và độ dài bản ghi

Đo dao động công trình nhà, sàn, cầu, móng máy: dải tần và biên độ điển hình, chọn gia tốc kế theo mật độ nhiễu µg/√Hz, MEMS hay IEPE, tần số lấy mẫu 2,56 lần, độ dài bản ghi, quy trình đo.

Thực hành hiệu chuẩn tĩnh gia tốc kế MEMS bằng trọng trường: sáu vị trí, độ lệch, độ nhạy chéo
Thực hành hiệu chuẩn tĩnh gia tốc kế MEMS bằng trọng trường: sáu vị trí, độ lệch, độ nhạy chéo

Hiệu chuẩn tĩnh gia tốc kế MPU-6050 bằng trọng trường với ESP32: sáu vị trí ±1 g, tính độ lệch không, độ nhạy, độ nhạy chéo dạng ma trận, lưu hệ số và kiểm tra độ lớn gia tốc ở tư thế bất kỳ.

Vali chuyên dụng là gì? Các loại vali chống sốc đựng thiết bị và cách chọn
Vali chuyên dụng là gì? Các loại vali chống sốc đựng thiết bị và cách chọn

Vali chuyên dụng là vali vỏ cứng có xốp giữ chặt thiết bị. Bài giải thích cấu tạo, bốn nhóm vali thường gặp (nhựa PP, chống nước IP67, vali nhôm, hộp sẵn xốp) và cách chọn nhanh theo nhu cầu.

Cảm biến lực (load cell): cấu tạo, nguyên lý, cách chế tạo và tự đọc HX711 bằng ESP32
Cảm biến lực (load cell): cấu tạo, nguyên lý, cách chế tạo và tự đọc HX711 bằng ESP32

Load cell hoạt động thế nào: các dạng thân, bốn strain gauge nối cầu Wheatstone, quy trình chế tạo, FSR và cảm biến áp điện, giao thức HX711 và cách tự đọc HX711 bằng ESP32.


Bình luận
Đã thêm vào giỏ hàng