在工业物联网和智能家居场景中,我们经常会遇到这样的问题:同一局域网内的两个设备(比如逆变器和电表)想要通信,但彼此不知道对方的 IP 地址。手动配置 IP 既繁琐又容易出错,而传统的 DNS 服务又依赖于服务器,无法满足零配置的需求。
这时候,**mDNS(Multicast Domain Name System,多播域名系统)** 就成了我们的最佳选择。它是一种零配置网络协议,能让设备在局域网内自动发现彼此,无需手动配置 IP 或依赖 DNS 服务器。
本文将结合实际项目场景,详细介绍 mDNS 的原理、实现方式,以及如何在 ESP32 平台上进行代码开发,最终实现逆变器自动发现并连接电表的功能。
一、场景与问题
我们的场景非常典型:
- 电表:作为数据采集端,需要将电力数据(电压、电流、功率等)上报。
- 逆变器:作为控制端,需要从电表获取数据进行逻辑处理。
- 网络环境:两者连接到同一个 WiFi 局域网。
核心痛点:
- 逆变器不知道电表的 IP 地址,无法建立连接。
- 手动配置 IP 地址在大规模部署时效率低下,且容易出错。
- 希望实现 "上电即通" 的零配置体验。
二、mDNS 是什么?
mDNS,全称多播域名系统,是一种零配置网络协议。它的核心作用是在局域网内实现设备的自动发现和域名解析。
简单来说,mDNS 让设备可以:
- 给自己起个名字 :比如
meter.local,并向局域网内的所有设备广播这个名字和对应的 IP 地址。 - 通过名字找人:当其他设备(如逆变器)需要和它通信时,只需要喊出这个名字,就能自动获取到它的 IP 地址。
mDNS 的工作流程分为两个阶段:
1. 注册阶段(服务端 / 电表侧)
- 设备(电表)启动时,向局域网的 mDNS 多播地址(
224.0.0.251:5353)发送一个 "声明报文",宣布自己的主机名(如meter.local)和 IP 地址。 - 如果需要发布服务(如自动发现),设备还会注册服务类型(如
_meter._tcp),并在报文中包含服务的端口、IP 等信息。 - 局域网内的其他设备会缓存这些信息,以便后续快速解析。
2. 发现阶段(客户端 / 逆变器侧)
- 当客户端(逆变器)需要访问
meter.local时,会向局域网多播发送一个 "查询报文":"谁是meter.local?请告诉我你的 IP 地址。" - 拥有
meter.local主机名的设备(电表)收到查询后,会单播响应自己的 IP 地址。 - 如果客户端查询的是服务类型(如
_meter._tcp),电表会响应服务的详细信息(IP、端口、设备名称等),客户端即可自动连接。
三、基础实现:注册主机名,实现 "ping 通域名"
这是最基础的 mDNS 应用,也是我们项目中 "顾永盛" 提到的 "没有做服务,就是单独的域名" 的场景。
1. 电表端代码(注册名字)
电表启动后,先连接 WiFi,然后注册 mDNS 主机名 meter.local。这样,局域网内的任何设备都可以通过 ping meter.local 来获取它的 IP 地址。
c
运行
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "mdns.h"
// WiFi名称和密码(和逆变器连同一个)
#define WIFI_SSID "你的WiFi名称"
#define WIFI_PASS "你的WiFi密码"
static const char *TAG = "电表-基础版";
// 初始化mDNS:给电表起固定名字 meter.local
static void start_mdns_service(void)
{
// 初始化mDNS
mdns_init();
// 核心:设置主机名 → 域名就是 meter.local
mdns_hostname_set("meter");
ESP_LOGI(TAG, "电表mDNS启动完成,域名:meter.local");
}
// WiFi连接(标准代码)
static void wifi_event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) {
if (event_id == WIFI_EVENT_STA_START) esp_wifi_connect();
if (event_id == WIFI_EVENT_STA_CONNECTED) ESP_LOGI(TAG, "WiFi连接成功");
}
static void wifi_init(void) {
ESP_ERROR_CHECK(nvs_flash_init());
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_create_default_wifi_sta();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, wifi_event_handler, NULL, NULL));
wifi_config_t wifi_cfg = {
.sta = {.ssid = WIFI_SSID, .password = WIFI_PASS}
};
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_cfg));
ESP_ERROR_CHECK(esp_wifi_start());
}
void app_main(void)
{
// 初始化NVS
nvs_flash_init();
// 连接WiFi
wifi_init();
// 等待WiFi连接稳定
vTaskDelay(pdMS_TO_TICKS(3000));
// 启动mDNS(关键代码)
start_mdns_service();
// 电表后续:启动TCP服务器,等待逆变器连接
// tcp_server_start();
}
2. 逆变器端代码(解析名字)
逆变器启动后,连接到同一个 WiFi,然后通过 mDNS 查询 meter.local,自动获取电表的 IP 地址。
c
运行
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_wifi.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "mdns.h"
#include "lwip/sockets.h"
#define WIFI_SSID "你的WiFi名称"
#define WIFI_PASS "你的WiFi密码"
static const char *TAG = "逆变器-基础版";
// 核心函数:通过域名 meter.local 获取电表IP
static void get_meter_ip(void)
{
esp_ip4_addr_t ip;
// 关键:解析 mDNS 域名 → 自动得到IP
esp_err_t err = mdns_query_a("meter.local", 2000, &ip);
if (err == ESP_OK) {
// 成功!打印电表IP
char ip_str[16];
sprintf(ip_str, IPSTR, IP2STR(&ip));
ESP_LOGI(TAG, "✅ 成功获取电表IP:%s", ip_str);
// 逆变器拿到IP后,直接连接电表的TCP端口(比如10241)
// tcp_connect(ip, 10241);
} else {
ESP_LOGE(TAG, "❌ 未找到电表");
}
}
void app_main(void)
{
nvs_flash_init();
wifi_init(); // 连同一个WiFi
vTaskDelay(pdMS_TO_TICKS(3000));
// 自动获取电表IP
get_meter_ip();
}
效果验证:
- 电脑连接到同一个 WiFi,打开命令行输入
ping meter.local,如果能 ping 通,说明 mDNS 基础功能正常。 - 逆变器的串口日志会打印出电表的 IP 地址。
四、进阶实现:注册服务广播,实现 "零配置自动发现"
基础实现虽然解决了 IP 获取的问题,但逆变器仍然需要知道电表的域名 meter.local。在更复杂的场景中,我们希望逆变器连域名都不用记,上电后自动扫描局域网内的电表服务,然后自动连接。
这就是 "鑫" 提到的 "广播服务",我们可以通过注册 mDNS 服务类型来实现。
1. 电表端代码(广播服务)
电表不仅注册主机名,还额外注册一个服务类型 _meter._tcp,并广播自己的 TCP 端口(如 10241)。
c
运行
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "mdns.h"
// ==================== 配置项 ====================
#define WIFI_SSID "你的WiFi名称"
#define WIFI_PASS "你的WiFi密码"
#define METER_TCP_PORT 10241 // 电表TCP端口
static const char *TAG = "电表-服务端";
// ==================== mDNS服务注册(核心) ====================
static void mdns_service_start(void) {
// 1. 初始化mDNS
ESP_ERROR_CHECK(mdns_init());
// 2. 设置主机名(可选,兼容ping)
mdns_hostname_set("meter");
// 3. 【进阶核心】注册电表服务:_meter._tcp 端口10241
ESP_ERROR_CHECK(
mdns_service_add(
"智能电表", // 服务名称(自定义)
"_meter", // 服务类型(固定,逆变器按这个搜索)
"_tcp", // 协议(TCP)
METER_TCP_PORT, // 服务端口
NULL, 0 // 无额外参数
)
);
ESP_LOGI(TAG, "✅ 电表服务广播成功:_meter._tcp.local,端口:%d", METER_TCP_PORT);
}
// ==================== WiFi连接(标准代码) ====================
static void wifi_event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) {
if (event_id == WIFI_EVENT_STA_START) esp_wifi_connect();
if (event_id == WIFI_EVENT_STA_CONNECTED) ESP_LOGI(TAG, "WiFi连接成功");
}
static void wifi_init(void) {
ESP_ERROR_CHECK(nvs_flash_init());
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_create_default_wifi_sta();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, wifi_event_handler, NULL, NULL));
wifi_config_t wifi_cfg = {
.sta = {.ssid = WIFI_SSID, .password = WIFI_PASS}
};
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_cfg));
ESP_ERROR_CHECK(esp_wifi_start());
}
// ==================== 主函数 ====================
void app_main(void) {
wifi_init();
vTaskDelay(pdMS_TO_TICKS(3000));
mdns_service_start(); // 启动mDNS广播
// 后续:启动TCP服务器,等待逆变器连接
// tcp_server_init(METER_TCP_PORT);
}
2. 逆变器端代码(自动发现服务)
逆变器启动后,不再查询特定的域名,而是直接扫描局域网内的 _meter._tcp 服务,自动获取电表的 IP 和端口。
c
运行
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_wifi.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "mdns.h"
#include "lwip/sockets.h"
// ==================== 配置项 ====================
#define WIFI_SSID "你的WiFi名称"
#define WIFI_PASS "你的WiFi密码"
static const char *TAG = "逆变器-客户端";
// ==================== 自动发现电表服务(核心) ====================
static void find_meter_service(void) {
mdns_result_t *results = NULL;
// 搜索局域网内的 _meter._tcp 服务(和电表注册的一致)
esp_err_t err = mdns_query_ptr("_meter", "_tcp", 2000, 1, &results);
if (err != ESP_OK || !results) {
ESP_LOGE(TAG, "❌ 未搜索到电表服务");
return;
}
// 自动获取 电表IP + 端口
char ip_str[16];
sprintf(ip_str, IPSTR, IP2STR(&results->addr->u_addr.ip4.addr));
uint16_t port = results->port;
ESP_LOGI(TAG, "==================================");
ESP_LOGI(TAG, "✅ 发现电表服务!");
ESP_LOGI(TAG, "电表IP:%s", ip_str);
ESP_LOGI(TAG, "电表端口:%d", port);
ESP_LOGI(TAG, "==================================");
// ==================== 直接连接TCP(零配置) ====================
// tcp_connect(ip_str, port);
mdns_query_results_free(results); // 释放资源
}
// ==================== WiFi连接(和电表一致) ====================
static void wifi_init(void) {
// 代码和电表端完全相同,直接复制
ESP_ERROR_CHECK(nvs_flash_init());
esp_netif_init();
esp_event_loop_create_default();
esp_netif_create_default_wifi_sta();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
esp_wifi_init(&cfg);
wifi_config_t wifi_cfg = {.sta = {.ssid = WIFI_SSID, .password = WIFI_PASS}};
esp_wifi_set_mode(WIFI_MODE_STA);
esp_wifi_set_config(WIFI_IF_STA, &wifi_cfg);
esp_wifi_start();
vTaskDelay(pdMS_TO_TICKS(3000));
}
// ==================== 主函数 ====================
void app_main(void) {
wifi_init();
find_meter_service(); // 自动搜电表
}
运行效果:逆变器的串口日志会直接打印出电表的 IP 和端口,实现了真正的零配置连接。
五、TCP vs UDP:一对一还是一对多?
在我们的项目中,TCP 用于可靠的功率数据传输,而 UDP 用于 mDNS 设备发现。理解它们的 "一对一" 和 "一对多" 特性至关重要。
表格
| 协议 | 单个连接 / 收发 | 服务端支持 | 典型场景 | 你的项目用法 |
|---|---|---|---|---|
| TCP | 一对一 | 一对多(多独立连接) | 可靠传输、功率采集 | EMS 服务端 ↔ 多台电表(一对一连接,一对多通信) |
| UDP | 一对一 / 一对多 / 多对多 | 天然一对多 | 广播、发现、实时传输 | mDNS 设备发现(一对多广播) |
1. TCP:像打电话
- 单个连接是严格一对一:就像两个人打电话,一条通话链路只能有两个人。
- 服务端可以一对多:就像客服热线,一个服务端可以同时接多个客户端的来电,每通电话都是独立的一对一连接。
在我们的项目中,EMS 作为 TCP 服务端,可以同时对接多台电表,每台电表与 EMS 之间都是独立的 TCP 连接,保证了数据传输的可靠性。
2. UDP:像发传单 / 广播
- 天生无连接,灵活多变:UDP 没有 "建立连接" 的步骤,发数据包就像扔传单或广播喊话。
- 单播(一对一):逆变器单独给某台电表发消息。
- 广播 / 组播(一对多):1 台设备发消息,同一个局域网内所有设备都能收到。这就是 mDNS 使用 UDP 的原因,电表广播自己的信息,所有逆变器都能收到。
六、总结
通过 mDNS 技术,我们成功解决了局域网内设备自动发现的问题,实现了逆变器与电表的零配置通信。
- 基础版:通过注册 mDNS 主机名,实现了通过域名获取 IP 地址的功能。
- 进阶版:通过注册 mDNS 服务类型,实现了完全自动的服务发现,逆变器无需任何配置即可连接到电表。
- 协议选择:TCP 用于可靠的功率数据传输,UDP 用于高效的设备发现,两者各司其职,完美配合。