1. 项目缘起:一次失败的嗅探器自定义尝试
最近在折腾一个物联网项目,需要基于ESP8266 RTOS SDK开发一个Wi-Fi数据包嗅探器。我的需求很明确:不仅要能抓包,还要能根据特定的协议字段(比如我自定义的一个应用层协议头)来过滤和解析数据,而不是简单地抓取所有802.11帧。听起来是个挺常见的需求,对吧?我一开始也是这么想的,觉得乐鑫的SDK功能这么全,搞个自定义的Sniffer应该手到擒来。于是,我兴冲冲地打开了esp_wifi.h和esp_wifi_types.h,准备大干一场。
结果,现实给了我当头一棒。我按照文档和社区里一些零散的教程,尝试去修改和扩展Sniffer的回调函数,希望能注入我自己的过滤和解析逻辑。但过程极其不顺利,要么编译不过,要么运行起来直接崩溃,要么就是抓到的数据和我预想的完全对不上。最让人头疼的是,错误信息非常模糊,很多时候就是一句“内存错误”或者“无效参数”,排查起来像在走迷宫。这次“自定义失败”的经历,让我不得不停下来,重新审视ESP8266 RTOS SDK中Sniffer机制的设计。我决定把这次踩坑的全过程,以及后续我如何理解其底层限制并找到替代方案的思路,完整地记录下来。如果你也正打算在ESP8266上做深度数据包处理,希望我的这些教训能帮你省下几天甚至几周的折腾时间。
2. ESP8266 RTOS SDK Sniffer 机制深度拆解
要理解为什么自定义会失败,首先得彻底搞清楚ESP8266 RTOS SDK提供的Sniffer到底是个什么工作模式,它的能力边界在哪里。很多人,包括最初的我,都误以为它像libpcap或者Wireshark那样,提供了一个可以任意编程的数据包处理管道。实际上,远非如此。
2.1 核心API与工作流程
在ESP8266 RTOS SDK中,开启Sniffer模式的核心是esp_wifi_set_promiscuous_rx_cb这个函数。你传入一个回调函数,当Wi-Fi芯片处于混杂模式时,所有监听到的802.11帧(包括信标帧、数据帧、管理帧等)都会通过这个回调递交给你的应用程序。
typedef void (*wifi_promiscuous_cb_t)(void *buf, wifi_promiscuous_pkt_type_t type);这个回调函数签名非常简洁,也暴露了其设计的局限性。参数buf指向一个wifi_promiscuous_pkt_t结构体,里面包含了帧的元信息(如RSSI、信道、时间戳)和原始的帧数据。参数type指明帧的类型。这里的关键在于,这个回调函数是在一个高优先级的Wi-Fi任务上下文中被调用的。
注意:这个上下文环境极其敏感且资源受限。你不能在这个回调函数里做任何耗时的操作,比如复杂的解析、动态内存分配(
malloc)、打印大量日志(printf)等,否则极易导致看门狗复位或系统不稳定。这是第一个大坑,但还不是最深的。
2.2 数据结构的固有限制
我们来看看SDK定义的数据结构,这是理解其能力边界的关键:
typedef struct { wifi_pkt_rx_ctrl_t rx_ctrl; /**< 接收控制信息,如RSSI、信道等 */ uint8_t payload[0]; /**< 指向实际数据包负载的指针 */ } wifi_promiscuous_pkt_t; typedef struct { signed rssi: 8; /**< 信号强度 */ unsigned rate: 4; /**< 数据速率 */ unsigned is_group: 1; /**< 是否为组播/广播 */ unsigned : 1; /**< 保留 */ unsigned sig_mode: 2; /**< 信号模式,如11b/g/n */ unsigned legacy_length: 12; /**< 传统帧长度 */ unsigned damatch0: 1; /**< 硬件匹配标志 */ unsigned damatch1: 1; /**< 硬件匹配标志 */ unsigned bssidmatch0: 1; /**< BSSID匹配标志 */ unsigned bssidmatch1: 1; /**< BSSID匹配标志 */ unsigned mcs: 7; /**< MCS索引(用于HT/VHT) */ unsigned cwb: 1; /**< 信道带宽 */ unsigned : 16; /**< 保留 */ unsigned smoothing: 1; /**< 保留 */ unsigned not_sounding: 1; /**< 保留 */ unsigned : 1; /**< 保留 */ unsigned aggregation: 1; /**< 是否为聚合帧 */ unsigned stbc: 2; /**< 空时分组码 */ unsigned fec_coding: 1; /**< 前向纠错编码 */ unsigned sgi: 1; /**< 短保护间隔 */ signed noise_floor: 8; /**< 噪声基底 */ unsigned ampdu_cnt: 8; /**< A-MPDU计数 */ unsigned channel: 4; /**< 主信道号 */ unsigned secondary_channel: 4; /**< 次信道号 */ unsigned : 8; /**< 保留 */ unsigned timestamp: 32; /**< 时间戳 */ unsigned : 32; /**< 保留 */ unsigned : 32; /**< 保留 */ unsigned ant: 8; /**< 天线号 */ unsigned sig_len: 11; /**< 信号长度 */ unsigned rx_state: 8; /**< 接收状态 */ } wifi_pkt_rx_ctrl_t;payload[0]是一个柔性数组,它指向的是原始的802.11 MAC帧数据。这里没有任何关于上层协议(如IP、TCP、UDP)的解析信息。SDK提供的Sniffer停留在MAC层。如果你想分析HTTP、MQTT或者你自己的自定义协议,你必须从payload指针开始,手动解析802.11帧头、LLC/SNAP头(如果有)、IP头、TCP/UDP头,最后才能拿到你的应用层数据。这个过程不仅复杂,而且非常消耗CPU资源,在ESP8266这种单片机上,在回调函数里做这件事几乎是不可行的。
2.3 硬件过滤能力的真相
我最初设想“自定义”的一个主要方向,是希望利用ESP8266硬件可能提供的过滤功能,比如只抓取目标MAC地址或特定类型的帧,以减轻软件处理压力。然而,经过查阅大量底层文档和实验,我发现ESP8266的硬件过滤能力非常基础。
你可以通过esp_wifi_set_promiscuous_filter设置过滤,但它只支持两种非常粗粒度的模式:
WIFI_PROMIS_FILTER_MASK_ALL: 接收所有帧。WIFI_PROMIS_FILTER_MASK_MGMT: 仅接收管理帧(如信标帧、探针请求/响应)。WIFI_PROMIS_FILTER_MASK_DATA: 仅接收数据帧。WIFI_PROMIS_FILTER_MASK_MGMT | WIFI_PROMIS_FILTER_MASK_DATA: 接收管理和数据帧(这是最常用的组合)。
它不支持基于MAC地址、BSSID、或更高级的规则进行硬件过滤。这意味着,即使你只关心发给某个特定设备的数据,硬件也会把空中所有数据帧都抓下来,丢给你的回调函数。软件过滤是唯一途径,而这又回到了性能瓶颈的问题上。
3. 自定义失败的具体场景与根因分析
理解了机制,我们再回头看我失败的那些尝试,就能清晰地看到问题所在了。
3.1 尝试一:在回调函数内进行复杂协议解析
我的第一个方案是在wifi_promiscuous_cb_t回调函数里,直接解析payload,找到IP包,再找到UDP包,最后匹配我自定义的协议魔数(Magic Number)。我写了一个简单的解析函数,看起来没问题。
失败现象:系统运行几分钟后,必定会触发看门狗超时(WDT Reset),或者出现内存碎片化导致的分配失败。
根因分析:
- 耗时操作:解析一个数据包,即使优化得很好,也需要几十到几百微秒。在Wi-Fi密集的环境下,每秒可能有上百个包。回调函数执行时间过长,严重阻塞了高优先级的Wi-Fi任务,导致看门狗无法及时喂食。
- 栈溢出风险:回调函数在Wi-Fi任务的栈上执行,这个栈空间并不富裕。我的解析函数使用了局部数组和递归调用(用于解析嵌套的协议头),很容易导致栈溢出,破坏内存。
- 实时性要求:Wi-Fi驱动需要快速处理完这个包,以便接收下一个。任何延迟都可能导致丢包。
3.2 尝试二:使用队列将数据包抛到独立任务处理
既然回调里不能处理,那我就在回调里只做最简单的事:把数据包拷贝出来,然后通过一个队列(xQueueSend)发送给一个专门的数据处理任务(Data Processing Task)。这个任务拥有更大的栈空间,可以安心地进行解析。
失败现象:初期似乎可行,但在高流量下(例如进行Wi-Fi扫描或附近有大量设备时),系统仍然会崩溃,或者队列很快被填满,导致后续数据包丢失。
根因分析:
- 内存拷贝开销:在回调中,即使只是用
memcpy把payload(可能长达1500字节+)拷贝到一个临时缓冲区,这个操作本身在每秒上百个包的场景下就是巨大的开销。ESP8266的CPU主频只有80/160MHz,大量内存拷贝会迅速耗尽CPU时间。 - 动态内存压力:我为每个包都在堆上分配内存(
malloc)来创建缓冲区。高频率的分配和释放,在ESP8266有限的内存和简单的内存管理机制下,极易造成碎片化,最终导致分配失败。即使使用静态内存池,管理起来也很复杂。 - 队列阻塞:如果数据处理任务因为解析较慢而跟不上抓包速度,队列会满。
xQueueSend在队列满时的行为(阻塞或立即返回错误)需要仔细处理。在Wi-Fi回调中阻塞是绝对禁止的,这会导致系统死锁。
3.3 尝试三:修改SDK底层驱动以注入过滤逻辑
这是最激进的做法。我尝试去修改esp_wifi组件内部的驱动代码,想在硬件数据上报到软件回调之前的某个环节,插入我自己的过滤函数。
失败现象:编译系统复杂,经常出现头文件依赖和链接错误。即使偶尔编译成功,烧录后设备根本无法启动,或Wi-Fi功能完全失效。
根因分析:
- SDK封装与耦合度:ESP8266 RTOS SDK的Wi-Fi驱动(
esp_phy,esp_wifi)是闭源的二进制库(.a文件)或高度封装、深度耦合的代码。我们只能通过乐鑫提供的有限API进行交互,无法触及核心的数据流处理逻辑。强行修改只会破坏其内部状态机。 - 兼容性与维护性:即使某次你 hack 成功了,SDK一升级,你的修改很可能全部失效,且难以调试。这种方式完全不可持续。
4. 可行的替代方案与架构设计
经历了上述失败,我放弃了“魔改”SDK自带Sniffer的想法,转而设计了一套更务实、基于现有API能力边界的方案。核心思想是:在回调中做最少的事,把重活转移到可控的、低优先级的上下文中,并坦然接受性能瓶颈,通过策略优化来规避。
4.1 方案一:轻量级回调 + 高优先级处理任务
这个方案是“尝试二”的精细化改进版,核心在于极致减少回调内的操作。
架构设计:
- 使用静态内存池:在程序初始化时,预先分配一个固定大小的缓冲区池(比如20个,每个1536字节)。在回调函数中,从池中获取一个空闲缓冲区,而不是动态分配。
#define PKT_POOL_SIZE 20 #define PKT_BUF_SIZE 1536 static uint8_t pkt_pool[PKT_POOL_SIZE][PKT_BUF_SIZE]; static bool pkt_pool_used[PKT_POOL_SIZE] = {0}; - 回调函数只做拷贝和发送:回调函数中,检查缓冲区池,找到空闲块,使用
memcpy拷贝必要的元数据(rx_ctrl)和payload(仅拷贝有效长度,通过rx_ctrl.sig_len计算)。然后通过xQueueSendToBackFromISR(注意是FromISR版本)发送缓冲区的索引到队列。这里必须使用非阻塞方式发送,如果队列满,则直接丢弃这个包并释放缓冲区。丢包是可接受的,总比系统崩溃好。 - 专用高优先级处理任务:创建一个优先级高于普通应用任务但低于Wi-Fi任务的任务。它从队列中取出缓冲区索引,进行完整的协议解析、过滤和业务处理。处理完毕后,标记该缓冲区为空闲。
- 流量控制:根据
rx_ctrl.rssi等信息,在回调中实现简单的软件过滤。例如,只处理信号强度大于-70dBm的包,直接丢弃弱信号包,从源头减少压力。
优缺点:
- 优点:相对稳定,避免了动态内存管理,通过队列实现了生产-消费者模型,解耦了抓包和处理。
- 缺点:内存占用固定且较大,在高流量环境下丢包率可能较高。拷贝操作依然是性能瓶颈。
4.2 方案二:基于esp_wifi_80211_tx的“镜像”嗅探
如果你的目标不是抓取空中所有流量,而只是监控本设备(ESP8266本身)发出和接收的数据,那么有一个更优雅的替代方案:使用esp_wifi_80211_tx和esp_wifi_set_rx_cb。
工作原理:
- 发送数据监控:原本你使用
esp_wifi_80211_tx发送原始的802.11帧。你可以包装这个函数,在调用真正的发送函数之前,先把要发送的帧数据复制一份给你的监控模块处理。这完全在应用层控制,没有任何性能风险。 - 接收数据监控:通过
esp_wifi_set_rx_cb可以设置一个接收回调。这个回调在网络栈上层(比如在IP层之后)被调用,它接收到的已经是去除了802.11 MAC帧头、可能已经解密的数据(例如通过esp_wifi_sta_get_ap_info连接后接收的数据)。这对于监控本STA与AP之间的应用层通信非常有用,而且数据结构更友好。
代码示例(发送监控):
// 自定义的发送函数,增加了镜像功能 esp_err_t my_wifi_send_raw(const void *buffer, int len) { // 1. 镜像:将buffer数据交给你的分析模块 my_packet_analyzer_mirror(buffer, len, DIRECTION_TX); // 2. 调用原始SDK函数发送 return esp_wifi_80211_tx(WIFI_IF_STA, buffer, len, false); } // 在你的应用代码中,调用 my_wifi_send_raw 而不是 esp_wifi_80211_tx优缺点:
- 优点:零性能开销,稳定性极高,可以直接获取到应用层数据,无需解析复杂的MAC帧。
- 缺点:功能受限,只能监控本设备收发的数据,无法抓取网络中的其他设备(如旁路监听)的通信。无法捕获管理帧(如信标帧)。
4.3 方案三:分阶段处理与采样嗅探
如果方案一在高流量下仍不稳定,或者你不需要100%的包捕获率,可以采用“采样”策略。这是很多专业网络分析工具在资源受限时的做法。
实施步骤:
- 定时开启/关闭嗅探:不要一直开着嗅探。可以设置一个定时器,每秒钟只开启嗅探100毫秒,然后关闭900毫秒。这直接减少了90%的数据流。
void timer_callback(TimerHandle_t xTimer) { static bool sniffer_active = false; if (sniffer_active) { esp_wifi_set_promiscuous(false); sniffer_active = false; // 启动处理阶段 xTaskNotifyGive(data_task_handle); } else { esp_wifi_set_promiscuous(true); sniffer_active = true; } } - 在开启阶段使用方案一:在嗅探开启的100毫秒内,使用方案一的轻量级回调+队列机制。
- 在关闭阶段集中处理:在嗅探关闭的900毫秒内,数据处理任务可以安心地、慢慢地处理队列中积累的包。
优缺点:
- 优点:极大降低了系统的平均负载,非常适合周期性监控或只需要抓取流量样本的场景。
- 缺点:会丢失大量数据包,不适合需要完整会话分析(如抓取登录过程)的场景。
5. 实战:实现一个稳定的简易信道扫描器
为了将上述理论付诸实践,我们来实现一个基于方案一(轻量级回调+任务)的简易Wi-Fi信道扫描器。它的目标是稳定运行,收集周围AP的信标帧,并统计其信号强度(RSSI),而不是进行深度的协议解析。
5.1 硬件与软件环境准备
- 硬件:任意一款ESP8266开发板(如NodeMCU、Wemos D1 Mini)。
- 开发框架:ESP8266 RTOS SDK (v3.4+)。使用VS Code + PlatformIO 或乐鑫官方的Eclipse IDE均可。
- 关键配置(
sdkconfig):确保Wi-Fi任务有足够的栈空间和优先级。在menuconfig中:Component config -> Wi-Fi -> WiFi RX IRAM speed optimization可以关闭以节省IRAM,但可能影响性能,根据情况选择。- 确保
FreeRTOS -> TIMER task stack size和Wi-Fi task stack size不是太小(建议至少4096)。
5.2 代码实现详解
我们创建三个主要部分:内存池与队列管理、轻量级嗅探回调、数据处理任务。
第一部分:全局资源定义
#include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/queue.h" #include "esp_wifi.h" #include "esp_log.h" static const char *TAG = "SNIFFER"; #define PKT_POOL_SIZE 10 // 缓冲区数量,根据内存调整 #define PKT_BUF_SIZE 256 // 只存储信标帧的头部,无需完整1500字节 #define QUEUE_LEN 5 // 队列深度 typedef struct { int pool_index; // 缓冲区在池中的索引 wifi_pkt_rx_ctrl_t rx_ctrl; // 元数据 uint16_t length; // 实际数据长度 } pkt_queue_item_t; // 静态内存池和队列 static uint8_t pkt_pool[PKT_POOL_SIZE][PKT_BUF_SIZE]; static bool pkt_pool_used[PKT_POOL_SIZE] = {0}; static QueueHandle_t pkt_queue = NULL; static TaskHandle_t process_task_handle = NULL;第二部分:轻量级嗅探回调这个回调只抓取管理帧(信标帧),并且只拷贝帧的前256字节(其中包含了SSID、BSSID等关键信息)。
static void wifi_sniffer_packet_handler(void *buff, wifi_promiscuous_pkt_type_t type) { if (type != WIFI_PKT_MGMT) { return; // 只处理管理帧 } const wifi_promiscuous_pkt_t *ppkt = (wifi_promiscuous_pkt_t *)buff; const wifi_ieee80211_packet_t *ipkt = (wifi_ieee80211_packet_t *)ppkt->payload; const wifi_ieee80211_mac_hdr_t *hdr = &ipkt->hdr; // 进一步过滤:只处理信标帧 (子类型 0x08) if ((hdr->frame_ctrl[0] & 0x0F) != 0x08) { return; } // 查找空闲缓冲区 int free_index = -1; for (int i = 0; i < PKT_POOL_SIZE; i++) { if (!pkt_pool_used[i]) { free_index = i; pkt_pool_used[i] = true; break; } } if (free_index == -1) { // 没有空闲缓冲区,丢弃此包 return; } // 计算实际需要拷贝的长度 uint16_t copy_len = ppkt->rx_ctrl.sig_len; if (copy_len > PKT_BUF_SIZE) { copy_len = PKT_BUF_SIZE; // 只拷贝缓冲区能容纳的部分 } // 拷贝数据 memcpy(pkt_pool[free_index], ppkt->payload, copy_len); // 准备队列项 pkt_queue_item_t item = { .pool_index = free_index, .rx_ctrl = ppkt->rx_ctrl, .length = copy_len }; // 非阻塞方式发送到队列 BaseType_t xHigherPriorityTaskWoken = pdFALSE; BaseType_t res = xQueueSendToBackFromISR(pkt_queue, &item, &xHigherPriorityTaskWoken); if (res != pdTRUE) { // 队列已满,释放缓冲区 pkt_pool_used[free_index] = false; // 可以在这里增加一个丢包计数器 } if (xHigherPriorityTaskWoken) { portYIELD_FROM_ISR(); } }第三部分:数据处理任务这个任务负责解析信标帧,提取并打印AP信息。
static void packet_process_task(void *pvParameter) { pkt_queue_item_t item; while (1) { // 阻塞等待数据包 if (xQueueReceive(pkt_queue, &item, portMAX_DELAY) == pdTRUE) { uint8_t *payload = pkt_pool[item.pool_index]; // 简单的信标帧解析:跳过MAC头部,寻找SSID字段 // 信标帧体开始于固定的管理帧头部之后(通常为24字节MAC头 + 12字节固定参数) // 这是一个简化示例,实际解析需要处理变长的信息元素(IE) int offset = 36; // 一个常见的起始偏移量 if (offset + 1 < item.length) { if (payload[offset] == 0x00) { // SSID IE的ID通常是0 uint8_t ssid_len = payload[offset + 1]; if (ssid_len > 0 && (offset + 2 + ssid_len) <= item.length) { char ssid[33] = {0}; memcpy(ssid, &payload[offset + 2], ssid_len); ssid[ssid_len] = '\0'; ESP_LOGI(TAG, "AP Found: SSID=%s, RSSI=%d, Channel=%d", ssid, item.rx_ctrl.rssi, item.rx_ctrl.channel); } } } // 处理完毕,释放缓冲区 pkt_pool_used[item.pool_index] = false; } } }第四部分:主函数初始化
void app_main() { // 1. 初始化队列 pkt_queue = xQueueCreate(QUEUE_LEN, sizeof(pkt_queue_item_t)); if (pkt_queue == NULL) { ESP_LOGE(TAG, "Failed to create queue"); return; } // 2. 初始化Wi-Fi wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); ESP_ERROR_CHECK(esp_wifi_init(&cfg)); ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM)); ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_NULL)); // 设置为NULL模式,仅用于嗅探 ESP_ERROR_CHECK(esp_wifi_start()); // 3. 设置混杂模式回调 ESP_ERROR_CHECK(esp_wifi_set_promiscuous_rx_cb(&wifi_sniffer_packet_handler)); ESP_ERROR_CHECK(esp_wifi_set_promiscuous_filter(&s_wifi_promiscuous_filter)); // 设置为只抓管理帧 ESP_ERROR_CHECK(esp_wifi_set_promiscuous(true)); // 4. 创建数据处理任务 xTaskCreate(packet_process_task, "pkt_proc", 4096, NULL, 5, &process_task_handle); // 优先级5 // 5. 可以在这里添加信道切换逻辑,循环扫描1-13信道 for (int channel = 1; channel <= 13; ++channel) { ESP_ERROR_CHECK(esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE)); vTaskDelay(pdMS_TO_TICKS(500)); // 在每个信道停留500ms } }5.3 实测结果与稳定性优化
将上述代码编译烧录后,设备开始循环扫描信道。通过串口日志,可以看到它稳定地输出周围Wi-Fi AP的SSID、RSSI和信道信息,长时间运行(超过1小时)未出现复位或崩溃。
关键的稳定性优化点:
- 缓冲区大小:本例中
PKT_BUF_SIZE设为256,对于只解析信标帧头部足够了,极大减少了内存拷贝压力。如果你需要抓取数据帧,这个值需要增大,但务必测试内存是否够用。 - 队列深度与任务优先级:
QUEUE_LEN设为5,process_task优先级设为5(高于默认任务)。这确保了即使短时间内有多个包,队列也能缓冲一下,处理任务有足够的CPU时间。如果发现丢包严重,可以适当增加队列深度,但要以牺牲内存为代价。 - 信道停留时间:每个信道500ms,这是一个权衡。时间太短,可能抓不到信标帧(信标帧通常每100ms发送一次);时间太长,扫描周期变慢。可以根据实际需求调整。
- 过滤前置:在回调函数最开始就通过
type和帧控制字段进行过滤,避免了无效数据进入后续流程,这是提升性能最有效的手段。
6. 总结:在资源受限环境下做取舍的艺术
回顾整个“自定义失败”到“找到可行方案”的过程,其核心教训在于:在ESP8266这类资源高度受限的MCU上,不能把PC或高端嵌入式设备上的软件设计思路直接套用过来。SDK提供的Sniffer接口是一个底层、高效的钩子,但它不是一个通用的、可任意扩展的数据包处理框架。
成功的自定义,不是去对抗它的限制,而是理解和尊重这些限制,并在其划定的边界内跳舞。这意味着你需要:
- 明确需求底线:你到底需要多细粒度的数据?必须抓取所有包吗?能接受丢包吗?延迟要求多高?回答这些问题比写代码更重要。
- 进行系统级权衡:内存、CPU时间、功耗、实时性,这些资源是相互冲突的。增加缓冲区可以减少丢包,但会增加内存开销和拷贝时间。更复杂的过滤算法可以提高信息质量,但会消耗更多CPU。你需要找到一个满足你最低需求的平衡点。
- 拥抱不完美:在MCU上做网络嗅探,尤其是想达到“自定义解析”的程度,注定无法完美。接受采样、接受丢包、接受有限的协议支持,把有限的计算资源用在最关键的逻辑上。
最终,我放弃了最初那个“全功能自定义嗅探器”的幻想,转而采用“轻量回调+静态池+专用任务”的架构,实现了项目需要的核心功能——稳定地收集特定网络信息。这个方案不够酷,但足够用,而这在嵌入式开发中,往往就是最好的方案。