MQTT 连接使用 IPv6 地址的 MQTT Broker,URI 需遵循格式: mqtt://[<IPv6地址>]:<端口> ,比如:

mqtt://[fe80::e526:e9ce:10b0:e571]:1883

案例一:

使用 v3.4 版本的 ESP8266_RTOS_SDK
,基于 mqtt/tcp 示例测试,无法连接到笔记本上开启的本地 MQTT 服务器。其中:

  • MQTT URI 已使用 mqtt://[<IPv6地址>]:<端口> 格式进行设置
  • 笔记本可以 ping 通 ESP8266 并获取到的链路本地 IPV6 地址
  • 笔记本已关闭防火墙
  • 且在另一台笔记本上使用 mqttfx 可以连接到这个本地 MQTT 服务器

工程配置如下:

在这里插入图片描述
在这里插入图片描述

ESP8266 设备输出日志如下:

在这里插入图片描述

问题复现:

  • Windows 上使用 mosquitto 开启本地 mqtt broker

在这里插入图片描述

【注意】

Windows 下使用 mosquitto 开启 mqtt broker 时,不要直接使用 mosquitto -v ,需要使用 -c 指定配置文件,否则 mosquitto 会采用内置的默认配置,会默认启动在 “仅本地模式”(local only mode)下,仅允许本地客户端连接。

C:\Users\mali>mosquitto -v
1749090375: mosquitto version 2.0.18 starting
1749090375: Using default config.
1749090375: Starting in local only mode. Connections will only be possible
from clients running on this machine.
1749090375: Create a configuration file which defines a listener to allow
remote access.
1749090375: For more details see
https://mosquitto.org/documentation/authentication-methods/
1749090375: Opening ipv4 listen socket on port 1883.
1749090375: Opening ipv6 listen socket on port 1883.
1749090375: mosquitto version 2.0.18 running
1749092123: mosquitto version 2.0.18 terminating

安装 msoquitto 时,若未自定义安装路径,默认会安装在 C:\Program
Files\mosquitto\mosquitto.conf,默认配置如下:

# listener port-number [ip address/host name/unix socket path]
# allow_anonymous false

内容可修改为:

listener 1883
allow_anonymous true
  • listener 1883 : 表示让 Mosquitto 在端口 1883 上监听客户端连接。这是 MQTT 协议的默认端口(非加密)。
  • 没有指定 IP 地址时,Mosquitto 会默认监听所有网络接口(0.0.0.0 和 ::)
  • 如果你想控制监听的具体地址,例如只监听本地 IPv4,可以改写为:
listener 1883 127.0.0.1
  • 只监听本地 IPv6 地址,可以改写为
listener 1883 ::1
  • allow_anonymous true:表示允许“匿名客户端”连接,也就是说客户端连接时不需要提供用户名和密码。
    • 如果为 false,则需要配置密码文件(password_file)和可能的用户认证机制。

使用 mosquitto -c "C:\Program Files\mosquitto\mosquitto.conf" -v 启动。
也可以在任意目录下新建一个 mosquitto.conf 文件,例如 D:\test\mosquitto.conf,使用上面的
配置内容,然后用命令 mosquitto -c D:\test\mosquitto.conf -v 启动

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

案例一 解决方案:

对于此问题,需要应用如下 transport_tcp.patch 文件,即可成功连接本地 MQTT IPv6 服务器

  • transport_tcp.patch 源代码:
diff --git a/components/tcp_transport/transport_tcp.c b/components/tcp_transport/transport_tcp.c
index 5bfb99dd..de9ced79 100644
--- a/components/tcp_transport/transport_tcp.c
+++ b/components/tcp_transport/transport_tcp.c
@@ -25,67 +25,90 @@
 
 #include "esp_transport_utils.h"
 #include "esp_transport.h"
-
+#include "tcpip_adapter.h"
 static const char *TAG = "TRANS_TCP";
 
 typedef struct {
     int sock;
 } transport_tcp_t;
 
-static int resolve_dns(const char *host, struct sockaddr_in *ip) {
-
-    struct hostent *he;
-    struct in_addr **addr_list;
-    he = gethostbyname(host);
-    if (he == NULL) {
-        return ESP_FAIL;
+static esp_err_t resolve_host_name(const char *host, size_t hostlen, struct addrinfo **address_info)
+{
+    struct addrinfo hints;
+    memset(&hints, 0, sizeof(hints));
+    hints.ai_family = AF_UNSPEC;
+    hints.ai_socktype = SOCK_STREAM;
+
+    char *use_host = strndup(host, hostlen);
+    if (!use_host) {
+        return ESP_ERR_NO_MEM;
     }
-    addr_list = (struct in_addr **)he->h_addr_list;
-    if (addr_list[0] == NULL) {
+
+    ESP_LOGI(TAG, "host:%s: strlen %lu", use_host, (unsigned long)hostlen);
+    if (getaddrinfo(use_host, NULL, &hints, address_info)) {
+        ESP_LOGE(TAG, "couldn't get hostname for :%s:", use_host);
+        free(use_host);
         return ESP_FAIL;
     }
-    ip->sin_family = AF_INET;
-    memcpy(&ip->sin_addr, addr_list[0], sizeof(ip->sin_addr));
+    free(use_host);
     return ESP_OK;
 }
 
 static int tcp_connect(esp_transport_handle_t t, const char *host, int port, int timeout_ms)
 {
-    struct sockaddr_in remote_ip;
-    struct timeval tv = { 0 };
     transport_tcp_t *tcp = esp_transport_get_context_data(t);
+    struct addrinfo *addrinfo;
+    esp_err_t ret;
 
-    bzero(&remote_ip, sizeof(struct sockaddr_in));
-
-    //if stream_host is not ip address, resolve it AF_INET,servername,&serveraddr.sin_addr
-    if (inet_pton(AF_INET, host, &remote_ip.sin_addr) != 1) {
-        if (resolve_dns(host, &remote_ip) < 0) {
-            return -1;
-        }
+    if ((ret = resolve_host_name(host, strlen(host), &addrinfo)) != ESP_OK) {
+        return -1;
     }
 
-    tcp->sock = socket(PF_INET, SOCK_STREAM, 0);
-
+    tcp->sock = socket(addrinfo->ai_family, addrinfo->ai_socktype, addrinfo->ai_protocol);
     if (tcp->sock < 0) {
         ESP_LOGE(TAG, "Error create socket");
+        freeaddrinfo(addrinfo);
         return -1;
     }
 
-    remote_ip.sin_family = AF_INET;
-    remote_ip.sin_port = htons(port);
-
-    esp_transport_utils_ms_to_timeval(timeout_ms, &tv); // if timeout=-1, tv is unchanged, 0, i.e. waits forever
+    if (timeout_ms >= 0) {
+        struct timeval tv;
+        esp_transport_utils_ms_to_timeval(timeout_ms, &tv);
+        setsockopt(tcp->sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
+        setsockopt(tcp->sock, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
+    }
 
-    setsockopt(tcp->sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
-    setsockopt(tcp->sock, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
+    void *addr_ptr;
+    if (addrinfo->ai_family == AF_INET) {
+        struct sockaddr_in *p = (struct sockaddr_in *)addrinfo->ai_addr;
+        p->sin_port = htons(port);
+        addr_ptr = p;
+    }
+#if LWIP_IPV6
+    else if (addrinfo->ai_family == AF_INET6) {
+        struct sockaddr_in6 *p = (struct sockaddr_in6 *)addrinfo->ai_addr;
+        p->sin6_port = htons(port);
+        p->sin6_family = AF_INET6;
+        p->sin6_scope_id = tcpip_adapter_get_netif_index(TCPIP_ADAPTER_IF_STA);
+        addr_ptr = p;
+    }
+#endif
+    else {
+        ESP_LOGE(TAG, "Unsupported protocol family %d", addrinfo->ai_family);
+        close(tcp->sock);
+        freeaddrinfo(addrinfo);
+        tcp->sock = -1;
+        return -1;
+    }
 
-    ESP_LOGD(TAG, "[sock=%d],connecting to server IP:%s,Port:%d...",
-             tcp->sock, ipaddr_ntoa((const ip_addr_t*)&remote_ip.sin_addr.s_addr), port);
-    if (connect(tcp->sock, (struct sockaddr *)(&remote_ip), sizeof(struct sockaddr)) != 0) {
+    if (connect(tcp->sock, addr_ptr, addrinfo->ai_addrlen) != 0) {
         close(tcp->sock);
+        freeaddrinfo(addrinfo);
         tcp->sock = -1;
         return -1;
     }
+
+    freeaddrinfo(addrinfo);
     return tcp->sock;
 }
 
  • 切到 ESP8266_RTOS_SDK 下,执行如下指令:
git apply <path to patch> 
  • 针对文件
    components/tcp_transport/transport_tcp.c 进行修改)应用 patch 即可。

  • 应用完成后,可通过 git status 指令查看文件是否有被改动。
    在这里插入图片描述

  • 对例程重新编译测试,结果如下:

在这里插入图片描述


案例二:

ESP8266 基于 mqtt/tcp 例程,连接 public IPv6 broker 失败。测试时:

  • menuconfig 中需将 Preferred IPv6 Type 设置为 Global Address

  • 也需要开启 CONFIG_LWIP_IPV6_AUTOCONFIG(默认开启)设置

    CONFIG_LWIP_IPV6_AUTOCONFIG=y
    CONFIG_EXAMPLE_CONNECT_IPV6=y
    # CONFIG_EXAMPLE_CONNECT_IPV6_PREF_LOCAL_LINK is not set
    CONFIG_EXAMPLE_CONNECT_IPV6_PREF_GLOBAL=y
    
  • URI 使用 test.mosquitto.org 的 IPv6 地址,可通过 ping 获取到。

    D:\>ping -6 test.mosquitto.org
    正在 Ping test.mosquitto.org [2001:41d0:a:6f1c::1] 具有 32 字节的数据:
    来自 2001:41d0:a:6f1c::1 的回复: 时间=307ms
    来自 2001:41d0:a:6f1c::1 的回复: 时间=313ms
    来自 2001:41d0:a:6f1c::1 的回复: 时间=367ms
    来自 2001:41d0:a:6f1c::1 的回复: 时间=302ms
    2001:41d0:a:6f1c::1 的 Ping 统计信息:
    数据包: 已发送 = 4,已接收 = 4,丢失 = 0 (0% 丢失),
    往返行程的估计时间(以毫秒为单位):
    最短 = 302ms,最长 = 367ms,平均 = 322ms
    
  • 手机可以通过运营商拿到 global IPv6 地址,可以让设备连接到手机创建的 wifi 热点获取 global IPv6 地址。

【注意】

  • 测试时需要将手机 wifi 关闭
  • 若测试时手机有连接到别的热点,则设备无法获取到 global IPv6 地址

软件设置

在这里插入图片描述

测试日志

在这里插入图片描述

案例二 解决方案

当应用 patch transport_tcp.patch 后可连接成功。日志如下:
在这里插入图片描述


案例三:

使用 ESP32 基于 esp-idf/examples/protocols/mqtt/tcp 例程测试连接本地 ipv6 broker 失败

  • 软件使用如下配置:
CONFIG_EXAMPLE_CONNECT_IPV4=y
CONFIG_EXAMPLE_CONNECT_IPV6=y
CONFIG_EXAMPLE_CONNECT_IPV6_PREF_LOCAL_LINK=y
  • MQTT URI 设置:
    在这里插入图片描述

测试日志:

在这里插入图片描述

案例三 解决方案

案例三 解决方案一

在这里插入图片描述

案例三 解决方案二

  • 需要采用正确的 URI 格式,在 % 后附加接口名称,例如 “fe80::fb2:692a:ea4a:7e65%eth0”

  • 可使用以下代码测试:

esp_mqtt_client_config_t mqtt_cfg =
{ .broker.address.uri = "mqtt://[fe80::fb2:692a:ea4a:7e65%st1]:1883", }
;
    • st1是默认 station 接口名称
    • 可以使用ESP_ERROR_CHECK(esp_netif_get_netif_impl_name(EXAMPLE_INTERFACE, name)); 读取该名称)

IPv6 sin6_scope_id 的作用

在 IPv6 中, sin6_scope_idsockaddr_in6 结构体中的一个字段,它的作用是指定地址的作用域(Scope)或接口索引,主要用于 链路本地地址(Link-local Address)。

为什么需要 sin6_scope_id ?

IPv6 中有一种地址叫 链路本地地址(Link-local address),它的格式是 fe80::/10 。这种地址只能在本地链路(本地网络段)中使用,不能跨路由器通信。

由于一台设备可能有多个网络接口(如 eth0、wlan0),每个接口上都可能有一个 link-local 地址,所以:

  • 如果你只给出一个 link-local 地址,如 fe80::1 ,系统就不知道你是想通过哪个接口发送
  • 此时就必须通过 sin6_scope_id 来指明具体接口

例如:

const esp_mqtt_client_config_t mqtt_cfg = {
.uri = "mqtts://[2001:41d0:a:6f1c::1]:8883",
.cert_pem = (const char *)mqtt_eclipse_org_pem_start,
.skip_cert_common_name_check = true,
};
  • 证书替换为 test.mosquitto.org 的证书 mosquitto.org.crt , 可直接连接成功.

总结

MQTT 协议 Broker 类型 ESP8266 ESP32
mqtt/tcp local broker 需要应用 transport_tcp.patch mqtt://[<IPv6地址>%< interface name>]:<端口>
mqtt/tcp public broker 需要应用 transport_tcp.patch mqtt://[<IPv6地址>]:<端口>
mqtt/ssl local broker 待测试 待测试
mqtt/ssl public broker mqtts://[<IPv6地址>]:<端口> mqtts://[<IPv6地址>]:<端口>

FAQ :

如果一个 mqtt broker 域名同时具有 IPv4 和 IPv6 地址,设备也有同时获取到 IPv4 和 IPv6 地址,那么默认是通过 IPv4 连接到 broker 对吗?如果打算通过 IPv6 连接,要怎么做呢?

  • 默认是走 IPv4,如果想走 IPv6,那就传入 IPv6 的 url,目前 idf esp-tls 没有实现可以选择 IPv6,只能应用层传入 IPv6 地址,然后让底层走 IPv6 。
Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐