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

- 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.

| Số thiết bị | Mỗi 1 giây | Mỗi 10 giây | Mỗi 60 giây |
|---|---|---|---|
| 1 | 0,49 GB/tháng | 0,05 GB/tháng | 0,01 GB/tháng |
| 10 | 4,9 GB/tháng | 0,49 GB/tháng | 0,08 GB/tháng |
| 50 | 24 GB/tháng | 2,4 GB/tháng | 0,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

- Đă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ó.
- 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.
- Chép địa chỉ cụm và cổng TLS 8883.
- 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"
"MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw\n"
"TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh\n"
"cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4\n"
"WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu\n"
"ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY\n"
"MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc\n"
"h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+\n"
"0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U\n"
"A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW\n"
"T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH\n"
"B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC\n"
"B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv\n"
"KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn\n"
"OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn\n"
"jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw\n"
"qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI\n"
"rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV\n"
"HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq\n"
"hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL\n"
"ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ\n"
"3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK\n"
"NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5\n"
"ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur\n"
"TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC\n"
"jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc\n"
"oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq\n"
"4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA\n"
"mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d\n"
"emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=\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");
}
}

- 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ượng | Nguyên nhân | Cá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 5 | Sai tài khoản MQTT, hoặc tài khoản chưa có quyền topic | Tạ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ây | Hai mạch trùng mã client | Dùng mã theo MAC như trong code |
| Máy tính nối được, ESP32 không | Thiếu RAM, chứng chỉ thiếu ký tự xuống dòng | In heap; giữ đúng từng dòng PEM |
| Chạy tốt rồi tự ngắt sau vài chục giây | Không gọi mqtt.loop() đều | Không dùng delay() dài trong loop() |
| Đột nhiên không nối được cả hệ thống | Hết hạn mức hoặc cụm bị dừng | Xem 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.
Xem thêm