ESP8266 与 ESP32 连接 MQTT IPv6 Broker(GlobalLink-Local)的问题分享
MQTT 协议Broker 类型ESP8266ESP32mqtt/tcp需要应用mqtt://[<IPv6地址>%< interface name>]:<端口>mqtt/tcp需要应用mqtt://[<IPv6地址>]:<端口>mqtt/ssl待测试待测试mqtt/sslmqtts://[<IPv6地址>]:<端口>mqtts://[<IPv6地址>]:<端口>
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 设置:

测试日志:

案例三 解决方案
案例三 解决方案一
- 在 esp-idf/components/esp-tls/esp_tls.c 里的 esp_tls_hostname_to_fd 函数里设置下
p->sin6_scope_id

-
mqtt/tcp socket创建函数调用过程:
esp_mqtt_task —> esp_transport_connect(components/tcp_transport/transport.c) —>
tcp_connect(components/tcp_transport/transport_ssl.c) —>
esp_tls_plain_tcp_connect(components/esp-tls/esp_tls.c) —> tcp_connect(components/esp-tls/esp_tls.c) —> esp_tls_hostname_to_fd(components/esp-tls/esp_tls.c) -
测试结果:

案例三 解决方案二
-
需要采用正确的 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_id 是 sockaddr_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 来指明具体接口
例如:
- ESP8266 使用 ESP8266_RTOS_SDK/examples/protocols/mqtt/ssl 例程连接到
public broker
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 。
火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐
所有评论(0)