[i=s] 本帖最后由 WildboarG 于 2025-12-21 22:38 编辑 [/i]
🏷️引言
如果你用过小米的智能家居产品,大概都经历过这样的过程:
买回一个米家传感器、智能插座或者智能灯,接通电源,打开米家 App,点击“添加设备”,很快 App 就会提示——发现新设备。
整个过程几乎不需要你关心网络细节:
设备的 IP 地址是什么?
它在局域网里的哪个位置?
这些问题,用户从来不需要知道。
但从技术角度看,这件事其实并不“理所当然”。在一个普通的家庭网络中,路由器只负责分配 IP 和转发数据,并不会主动告诉手机“现在多了一台智能设备”。那么,米家 App 是如何在众多设备中,第一时间找到这台刚刚上电的智能硬件的?
答案之一,就是 局域网自动发现技术,而 mDNS 正是其中最常见、也最重要的一种。
🔋什么是mDNS?
mDNS(Multicast DNS)是一种在局域网内实现域名解析的协议,它允许设备在没有中心 DNS 服务器的情况下互相发现和访问。
传统 DNS 依赖一个服务器来将域名解析为 IP 地址,而 mDNS 则通过组播(multicast)方式在局域网内广播查询与响应,实现设备的零配置自动发现。
简单来说,mDNS 能让局域网中的设备像这样互相找到对方:
my-device.local → 192.168.1.50
而不需要手动查 IP、配置 hosts 文件或依赖路由器提供的 DNS。
mDNS 的主要特点:
- 零配置:设备上电即可被发现,无需手动配置 IP 或 DNS
- 局域网内有效:只在本地链路广播,不会穿过路由器
- 兼容 DNS 格式:使用标准 DNS 报文,但通过组播发送
- 支持服务发现:配合 DNS-SD,可以发现 HTTP、SSH、打印机等服务
简单理解:
传统 DNS 是互联网“电话簿”,mDNS 是局域网里的“邻居广播”。
简答理解组播与广播:
组播:一个订阅小群🎧
广播: 大喇叭通知📢
当你的设备订阅了组播 224.0.0.251 ,它会向群内广播一条关于 我是谁 我在哪 我能做什么 的记录。 群内所有人都会接收到这条消息。而大部分的智能电子设备也就是这么干的,不过各自添加了厂商的私有协议以便认证。比如苹果的Bonjour,允许 macOS、iOS 设备在局域网内自动发现打印机、共享文件夹、AirPlay 设备等服务。
mDNS 并没有重新发明一套协议,而是建立在现有网络机制之上:
| 项目 |
mDNS 取值 |
| 传输层协议 |
UDP |
| 端口号 |
5353 |
| IPv4 组播地址 |
224.0.0.251 |
| IPv6 组播地址 |
ff02::fb |
| 作用范围 |
本地链路(Link-Local) |
🔎使用场景
我用wb2做了一个台灯,需要smartconfig配网,灯可以通过http进行开关和状态查询? 但是我需要知道ip地址
一种简单的方式:我去路由器后台查看新加入的设备IP
http://192.168.0.xxx:80/light
正常的控制我的设备
如果我多个卧室内放了多个台灯,我需要知道多个灯的ip地址.
于是我就引入了Mdns
我们可以像访问域名一样访问设备。
通过mdns将我的灯的ip自定义域名指向本地ip
http://台灯1.local --> 192.168.0.100
这样就可智能配网后,我不需要知道设备的ip地址,通过域名就可以访问。
当有多个设备
http://台灯1.local
http://卧室台灯.local
http://客厅吸顶灯.local
http://卧室空调.local
http://卧室加湿器.local
当然 并不局限于http协议,几乎常见的网络服务都可以这么做。
📦服务发现的核心
mDNS 中的“记录”是什么?
mDNS 使用的仍然是标准 DNS Resource Record(RR),常见的有:
| 记录类型 |
作用 |
| A |
主机名 → IPv4 |
| AAAA |
主机名 → IPv6 |
| PTR |
服务类型枚举 |
| SRV |
服务所在主机与端口 |
| TXT |
服务附加信息 |
| 例如最简单的一条主机记录: |
|
my-device.local. A 192.168.1.50
假设局域网中有多台设备:
dev1.local
dev2.local
dev3.local
它们可能分别提供:
- HTTP
- SSH
- MQTT
- 打印服务
如果只有 A 记录,你只能做到:
“我知道这个名字对应哪个 IP”
但你不知道:
- 哪些设备提供 HTTP?
- 服务监听在哪个端口?
- 一台设备是否有多个同类服务?
PTR 和 SRV 正是为了解决这些问题而存在的。
| 记录 |
责任 |
| PTR |
发现(有哪些) |
| SRV |
定位(在哪 + 端口) |
| TXT |
描述(属性能力) |
| A/AAAA |
地址解析 |
|
|
如果你要发布的不只是主机名,而是一个服务(比如 HTTP):
【示例目标】
发布一个 HTTP 服务:
my-device.local:80
需要发布的记录(成组出现)
mDNS 服务发布通常包含 四类记录:
- PTR(服务类型 → 实例名)
_http._tcp.local. → My Web Server._http._tcp.local.
- SRV(实例名 → 主机名 + 端口)
My Web Server._http._tcp.local. SRV my-device.local:80
- TXT(服务属性)
My Web Server._http._tcp.local. TXT "path=/"
- A / AAAA(主机名 → IP)
my-device.local. A 192.168.1.50
mDNS 要求这些记录一起发布,否则服务发现会不完整。
🔧用WB2发布一个http服务
使用AI-WB2模拟一个http请求的台灯,并发布这个服务。
快速开始:
在sdk中打开例程protocols/http_server
在例程上快速修改;
- 删除原来的http_server.c的index
改成灯控:
#include "http_server.h"
#include <string.h>
#include <stdio.h>
#define SERVER_PORT 80
/* 模拟灯的状态 */
static int light_state = 0; // 0=OFF, 1=ON
/* HTTP 响应头 */
static const char http_txt_hdr[] =
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/plain\r\n"
"Connection: close\r\n"
"\r\n";
/* 404 响应 */
static const char http_404[] =
"HTTP/1.1 404 Not Found\r\n"
"Content-Type: text/plain\r\n"
"Connection: close\r\n"
"\r\n"
"Not Found\r\n";
static void web_http_server(struct netconn *conn)
{
struct netbuf *inputbuf;
char *buf;
u16_t buflen;
err_t err;
err = netconn_recv(conn, &inputbuf);
if (err != ERR_OK)
return;
netbuf_data(inputbuf, (void **)&buf, &buflen);
printf("HTTP Request:\n%.*s\n", buflen, buf);
/* 只处理 GET */
if (buflen >= 5 && strncmp(buf, "GET /", 5) == 0)
{
netconn_write(conn, http_txt_hdr, sizeof(http_txt_hdr) - 1, NETCONN_NOCOPY);
/* GET /light/on */
if (strncmp(buf + 5, "light/on", 8) == 0)
{
light_state = 1;
netconn_write(conn, "LIGHT ON\r\n", 10, NETCONN_NOCOPY);
printf("Light turned ON\n");
}
/* GET /light/off */
else if (strncmp(buf + 5, "light/off", 9) == 0)
{
light_state = 0;
netconn_write(conn, "LIGHT OFF\r\n", 11, NETCONN_NOCOPY);
printf("Light turned OFF\n");
}
/* GET /light/status */
else if (strncmp(buf + 5, "light/status", 12) == 0)
{
if (light_state)
netconn_write(conn, "LIGHT STATUS: ON\r\n", 18, NETCONN_NOCOPY);
else
netconn_write(conn, "LIGHT STATUS: OFF\r\n", 19, NETCONN_NOCOPY);
}
else
{
netconn_write(conn, "UNKNOWN CMD\r\n", 13, NETCONN_NOCOPY);
}
}
else
{
netconn_write(conn, http_404, sizeof(http_404) - 1, NETCONN_NOCOPY);
}
netconn_close(conn);
netbuf_delete(inputbuf);
}
void http_server_start(void *pvParameters)
{
struct netconn *conn, *newconn;
err_t err;
conn = netconn_new(NETCONN_TCP);
netconn_bind(conn, NULL, SERVER_PORT);
netconn_listen(conn);
printf("HTTP server started on port %d\n", SERVER_PORT);
while (1)
{
err = netconn_accept(conn, &newconn);
if (err == ERR_OK)
{
web_http_server(newconn);
netconn_delete(newconn);
}
}
}
从代码中可以看出我们添加了三个get请求
light/on --> 开灯
light/off ——> 关灯
light/status -->查询灯的状态
- 在wifi_execute.c中添加设备发现的逻辑
- 当wifi连接成功获取ip
- 新线程运行http服务
- 发布我的http灯控服务
#include "wifi_execute.h"
#include <lwip/init.h>
#include <lwip/netif.h>
#define STA_SSID "OpenWrt"
#define STA_PASSWORD "timeless"
enum mdns_sd_proto {
DNSSD_PROTO_UDP = 0,
DNSSD_PROTO_TCP = 1
};
#include <mdns_server.h>
static wifi_conf_t conf = {
.country_code = "CN",
};
/**
* @brief 向mdns服务添加TXT记录
* @param service 服务结构体
* @param txt_userdata 要添加的txt描述信息
**/
static void srv_txt(struct mdns_service *service, void *txt_userdata)
{
err_enum_t res;
/* Web 根路径 */
res = mdns_resp_add_service_txtitem(service, "path=/", 6);
/* 灯控 API 描述 */
res |= mdns_resp_add_service_txtitem(service, "api=/light", 10);
res |= mdns_resp_add_service_txtitem(service, "on=/light/on", 13);
res |= mdns_resp_add_service_txtitem(service, "off=/light/off", 15);
res |= mdns_resp_add_service_txtitem(service, "status=/light/status", 21);
if (res != ERR_OK) {
// printf("mDNS TXT add failed\n");
}
}
/**
* @brief 启动 MDNS 响应器并注册 HTTP 服务
* @param netif 要注册的网络接口
* @return 服务的槽位号(成功)或 -1(失败)
*/
int mdns_responder_starts(struct netif *netif)
{
int ret, slot = -1;
if (netif == NULL) {
printf("netif is NULL\r\n");
return -1;
}
// 1. 初始化 MDNS 响应器:打开 UDP 5353 端口 [4]
mdns_resp_init();
// 2. 将网络接口添加到 MDNS,设置主机名为 "mdns.local"
// TTL 设置为 3600 秒
ret = mdns_resp_add_netif(netif, "mywb2light", 3600);
if (ret != ERR_OK) {
mdns_resp_deinit();
printf("add netif failed:%d\r\n", ret);
return -1;
}
// 3. 添加 HTTP 服务 (_http._tcp)
// 服务名称: "mdns"
// 服务类型: "_http"
// 协议: TCP (DNSSD_PROTO_TCP)
// 端口: 80
// TTL: 3600
// TXT 回调: srv_txt
slot = mdns_resp_add_service(netif, "mdns", "_http", DNSSD_PROTO_TCP, 80, 3600, srv_txt, NULL);
if (slot < 0) {
mdns_resp_remove_netif(netif);
mdns_resp_deinit();
printf("add server failed:%d\r\n", slot);
return -1;
}
printf("mDNS HTTP Service registered successfully on slot %d.\r\n", slot);
return slot;
}
country_code_type country_code = WIFI_COUNTRY_CODE_CN;
static wifi_interface_t g_wifi_sta_interface = NULL;
static int g_wifi_sta_is_connected = 0;
wifi_sta_reconnect_t sta_reconn_params = {15, 10}; // set connection interval = 15, reconntction times = 10
void wifi_background_init(country_code_type country_code)
{
char *country_code_string[WIFI_COUNTRY_CODE_MAX] = {"CN", "JP", "US", "EU"};
/* init wifi background*/
strcpy(conf.country_code, country_code_string[country_code]);
wifi_mgmr_start_background(&conf);
/* enable scan hide ssid */
wifi_mgmr_scan_filter_hidden_ssid(0);
}
int wifi_sta_connect(void)
{
g_wifi_sta_interface = wifi_mgmr_sta_enable();
if (g_wifi_sta_is_connected == 1)
{
printf("sta has connect\n");
return 0;
}
else
{
wifi_mgmr_sta_autoconnect_enable();
printf("connect to wifi %s\n", STA_SSID);
return wifi_mgmr_sta_connect(g_wifi_sta_interface, STA_SSID, STA_PASSWORD, NULL, NULL, 0, 0);
}
}
static void wifi_event_cb(input_event_t *event, void *private_data)
{
static char *ssid;
static char *password;
printf("[APP] [EVT] event->code %d\r\n", event->code);
printf("[SYS] Memory left is %d Bytes\r\n", xPortGetFreeHeapSize());
switch (event->code)
{
case CODE_WIFI_ON_AP_STARTED:
{
printf("[APP] [EVT] AP INIT DONE %lld\r\n", aos_now_ms());
}
break;
case CODE_WIFI_ON_AP_STOPPED:
{
printf("[APP] [EVT] AP STOP DONE %lld\r\n", aos_now_ms());
}
break;
case CODE_WIFI_ON_INIT_DONE:
{
printf("[APP] [EVT] INIT DONE %lld\r\n", aos_now_ms());
wifi_mgmr_start_background(&conf);
wifi_sta_connect();
}
break;
case CODE_WIFI_ON_MGMR_DONE:
{
printf("[APP] [EVT] MGMR DONE %lld\r\n", aos_now_ms());
}
break;
case CODE_WIFI_ON_SCAN_DONE:
{
printf("[APP] [EVT] SCAN Done %lld\r\n", aos_now_ms());
// wifi_mgmr_cli_scanlist();
}
break;
case CODE_WIFI_ON_DISCONNECT:
{
g_wifi_sta_is_connected = 0;
printf("wifi sta disconnected\n");
printf("[APP] [EVT] disconnect %lld\r\n", aos_now_ms());
}
break;
case CODE_WIFI_ON_CONNECTING:
{
printf("[APP] [EVT] Connecting %lld\r\n", aos_now_ms());
}
break;
case CODE_WIFI_CMD_RECONNECT:
{
printf("[APP] [EVT] Reconnect %lld\r\n", aos_now_ms());
}
break;
case CODE_WIFI_ON_CONNECTED:
{
printf("wifi sta connected\n");
printf("[APP] [EVT] connected %lld\r\n", aos_now_ms());
}
break;
case CODE_WIFI_ON_PRE_GOT_IP:
{
printf("[APP] [EVT] connected %lld\r\n", aos_now_ms());
}
break;
case CODE_WIFI_ON_GOT_IP:
{
printf("WIFI STA GOT IP\n");
printf("[APP] [EVT] GOT IP %lld\r\n", aos_now_ms());
/* create http server task */
xTaskCreate(http_server_start, (char *)"http server", 1024 * 4, NULL, 15, NULL);
g_wifi_sta_is_connected = 1;
struct netif * netif = wifi_mgmr_sta_netif_get(); // 依赖于您的 SDK 实现
if (netif) {
if (mdns_responder_starts(netif) >= 0) {
printf("mDNS HTTP Service Discovery started.\r\n");
} else {
printf("mDNS startup failed.\r\n");
}
} else {
printf("Error: Could not retrieve active netif.\r\n");
}
}
break;
case CODE_WIFI_ON_PROV_SSID:
{
printf("[APP] [EVT] [PROV] [SSID] %lld: %s\r\n",
aos_now_ms(),
event->value ? (const char *)event->value : "UNKNOWN");
if (ssid)
{
vPortFree(ssid);
ssid = NULL;
}
ssid = (char *)event->value;
}
break;
case CODE_WIFI_ON_PROV_BSSID:
{
printf("[APP] [EVT] [PROV] [BSSID] %lld: %s\r\n",
aos_now_ms(),
event->value ? (const char *)event->value : "UNKNOWN");
if (event->value)
{
vPortFree((void *)event->value);
}
}
break;
case CODE_WIFI_ON_PROV_PASSWD:
{
printf("[APP] [EVT] [PROV] [PASSWD] %lld: %s\r\n", aos_now_ms(),
event->value ? (const char *)event->value : "UNKNOWN");
if (password)
{
vPortFree(password);
password = NULL;
}
password = (char *)event->value;
}
break;
case CODE_WIFI_ON_PROV_CONNECT:
{
printf("connecting to %s:%s...\r\n", ssid, password);
}
break;
case CODE_WIFI_ON_PROV_DISCONNECT:
{
printf("[APP] [EVT] [PROV] [DISCONNECT] %lld\r\n", aos_now_ms());
}
break;
default:
{
printf("[APP] [EVT] Unknown code %u, %lld\r\n", event->code, aos_now_ms());
/*nothing*/
}
}
}
void wifi_execute(void *pvParameters)
{
aos_register_event_filter(EV_WIFI, wifi_event_cb, NULL);
static uint8_t stack_wifi_init = 0;
if (1 == stack_wifi_init)
{
printf("Wi-Fi Stack Started already!!!\r\n");
return;
}
stack_wifi_init = 1;
printf("Wi-Fi init successful\r\n");
hal_wifi_start_firmware_task();
/*Trigger to start Wi-Fi*/
aos_post_event(EV_WIFI, CODE_WIFI_ON_INIT_DONE, 0);
vTaskDelete(NULL);
}
可以在代码中看到,我只是添加了两个函数,一个启动 MDNS 响应器并注册 HTTP 服务,一个添加TXT解析信息。
在头文件中引入:
#include <lwip/init.h>
#include <lwip/netif.h>
还要再引入mdns_server.h之前定义
enum mdns_sd_proto {
DNSSD_PROTO_UDP = 0,
DNSSD_PROTO_TCP = 1
};
#include mdns_server.h
我们定义了灯的本地域名为 mywb2light.local
烧录验证

通过串口工具看到服务已经正常启动,分配ip:192.168.0.109 然后启动了mDNS http server

- hostname = [mywb2light.local]
- adress = 192.168.0.109
- prot = 80
ip和域名对上了,而且txt记录里存放我的控制台灯的api


⛏️拓展
现在可以用自定义内网域名访问wb2提供的服务了,不需要每次都要查看ip地址,多设备也ok。
这样做有什么用?
- 多设备可以快速区分;
- 设备可以能被home-Assistant自动发现注册
当然对于设备不多的小伙伴,没必要使用home-Assistant ,
可以用ai-wv01-32s小安智能助手自动识别我内网下的服务自动注册为工具,通过语音唤醒就可以调用各个设备。
我们把设备如何使用的API发布在TXT描述信息了方便小安知道如何控制设备。
设备上电后,在组播中告诉内网设备 【我是谁】【我在哪】【我能干嘛】,小安AI 也在监听这个组播地址,当识别到新设备自动注册为自己的内部工具,用户就可以语音控制。