大家好,我是专注于嵌入式与网络通信开发的博主。在物联网、工业控制和车载网络项目中,以太网(Ethernet)作为最基础、最可靠的物理层和数据链路层技术,其设计与实现是每个开发者绕不开的课题。无论是使用STM32连接W5500模块,还是调试车载以太网协议栈,亦或是分析Wireshark抓包,一个清晰、稳健的以太网设计都是项目成功的基石。
然而,在实际开发中,我们常常面临一系列挑战:如何选择合适的物理接口(如RJ45、光口)?如何配置复杂的网络协议栈?如何确保通信的稳定性和抗干扰能力(如金属外壳接地)?以及如何排查那些令人头疼的“无法设置移动热点”或“网络连接失败”等问题。网上的资料往往零散,专注于某个芯片或某个工具,缺乏从硬件选型、协议理解到软件驱动、应用调试的全链路视角。
本文旨在为你提供一份系统性的“以太网设计”实战指南。我们将从核心概念出发,逐步深入到硬件电路设计、驱动移植、协议栈配置、数据收发实战以及高级调试技巧。无论你是正在学习STM32以太网开发的学生,还是需要为产品集成车载以太网的工程师,亦或是被网络配置问题困扰的开发者,都能从本文中找到可复用的代码、清晰的配置步骤和高效的排错思路。学完本文,你将能够独立完成一个中小型嵌入式设备的以太网通信功能设计与调试。
1. 以太网核心概念与设计考量
在动手写代码和画电路图之前,我们必须理解以太网到底是什么,以及在现代嵌入式系统中设计以太网功能时需要权衡哪些关键因素。
1.1 什么是以太网?
用最通俗的话讲,以太网是一套让多台设备通过网线(或光纤)相互连接并通信的“交通规则”。它定义了设备如何接入网络、数据包(帧)长什么样、如何避免数据碰撞、以及物理信号的电平标准等。
从技术分层来看,以太网主要涵盖了OSI模型中的物理层(PHY)和数据链路层(MAC):
- 物理层(PHY):负责处理实际的电信号或光信号。它定义了接口类型(如RJ45、SFP)、传输介质(双绞线、光纤)、信号编码(如曼彻斯特编码)、传输速率(10M/100M/1000M)以及电气特性(如电平、阻抗)。我们常说的“以太网接口”主要指这一层。
- 数据链路层(MAC):负责将上层交下来的数据封装成“帧”,并添加源/目的MAC地址、帧类型以及至关重要的帧校验序列(FCS)。MAC层还负责帧的发送、接收和简单的错误检测。很多微控制器(如STM32)内部集成了MAC控制器。
为什么需要帧校验和(FCS)?这是以太网可靠性的基石。发送方根据帧内容计算出一个校验值(CRC32),填入FCS字段。接收方收到后重新计算校验值,如果与收到的FCS不匹配,则直接丢弃该帧,从而避免处理错误数据。网络上热门的“以太网帧校验和计算器”工具,就是用来手动计算或验证这个值的。
1.2 常见以太网类型与应用场景
根据速率、介质和场景,以太网衍生出多种类型,设计时需要根据需求选择:
- 标准以太网(10/100/1000BASE-T):最常见的形式,使用RJ45接口和双绞线。适用于绝大多数工业控制、智能家居和通用嵌入式设备。STM32系列常外接LAN8720A、DP83848等PHY芯片实现百兆以太网。
- 千兆及以上以太网(10GbE, 25GbE):用于服务器、数据中心和高性能计算。物理接口可能是电口(RJ45)或光口(如SFP+)。10G以太网光口常与Aurora(一种用于FPGA间高速串行通信的协议)结合讨论,因为它们都涉及高速SerDes(串行器/解串器)技术,但Aurora是私有协议,而10GbE是标准以太网。
- 车载以太网(Automotive Ethernet):汽车电子领域的核心网络技术。它使用非屏蔽双绞线,并通过车载以太网协议栈(如SOME/IP、DoIP)满足汽车对低延迟、高带宽和确定性的要求。汽车以太网刷写(ECU软件更新)就是其重要应用之一。
- 以太网供电(PoE):通过网线同时传输数据和电力,简化了网络摄像头、AP等设备的部署。
1.3 嵌入式以太网设计关键决策点
开始设计前,请思考以下问题,它们将直接影响硬件选型和软件架构:
- 速率要求:10M、100M还是1000M?更高的速率意味着更高的硬件成本(PHY芯片、PCB布线要求)和软件开销。
- 接口类型:RJ45电口还是光口?电口成本低、通用性好;光口抗干扰能力强、传输距离远,适用于工业环境。
- 集成度:
- MCU内置MAC + 外置PHY:这是STM32等MCU的经典方案。MCU处理MAC层协议,外置PHY芯片处理物理信号转换。需要设计RMII或MII接口连接两者。
- 集成MAC+PHY的芯片:如W5500、ENC28J60。这类芯片通过SPI接口与MCU通信,极大简化了硬件和驱动设计,适合资源有限的系统,但性能通常低于方案一。
- 纯软件协议栈(如SLIP):通过串口模拟网络,仅用于极低速率或调试,非标准以太网。
- 协议栈选择:是否需要完整的TCP/IP协议栈?使用LWIP(轻量级IP)、FreeRTOS+TCP还是芯片厂商提供的库(如STM32的HAL库驱动)?
- 特殊要求:是否需要以太网供电(PoE)?是否需要考虑严苛环境的金属外壳接地以防止静电和干扰?
2. 环境准备与硬件设计要点
本部分我们将以最常见的“STM32 + 外置PHY”方案为例,讲解硬件设计和软件开发环境的搭建。
2.1 硬件环境与物料清单
- 主控MCU:STM32F407/F429/H7等系列(需内置以太网MAC控制器)。本文示例以STM32F407为例。
- PHY芯片:LAN8720A(性价比高,常用)或DP83848(稳定性好)。两者都支持RMII接口。
- 网络变压器:集成在RJ45插座内(如HR911105A),或单独使用变压器模块。它是隔离信号、防雷击的关键元件,必不可少。
- 晶振:为PHY芯片提供50MHz参考时钟(对于LAN8720A)或25MHz(对于DP83848,内部倍频)。
- PCB设计:
- RMII接口布线:RX/TX数据线、CRS_DV、REF_CLK等信号线应等长、紧耦合布线,减少信号完整性问题和时钟偏移。
- 电源与滤波:为PHY芯片的模拟和数字电源提供良好的去耦(100nF + 10uF电容)。
- 接地:模拟地(AGND)和数字地(DGND)通常通过磁珠或0欧电阻单点连接。金属外壳接地应通过高压电容(如1nF/2KV)或气体放电管连接到机壳地(Chassis GND),而非信号地,以实现静电释放和屏蔽。
- 软件IDE:STM32CubeIDE 或 Keil MDK。
- 软件框架:STM32CubeMX + HAL库。
- 协议栈:LWIP(Lightweight IP)开源协议栈。
2.2 使用STM32CubeMX进行基础配置
STM32CubeMX可以图形化配置引脚和时钟,并生成LWIP的初始化代码,极大提高效率。
- 选择MCU型号:在CubeMX中选择你的STM32型号。
- 使能以太网MAC:在
Pinout & Configuration标签页的Connectivity下,选择ETH。模式通常选择RMII。 - 配置引脚:CubeMX会自动分配RMII所需的引脚(如PC1/PC4/PC5用于RMII,PA1/PA2用于MDIO/MDC)。检查并确认这些引脚没有被其他功能占用。
- 配置时钟树:
- 确保系统时钟(HCLK)满足要求。
- 最关键的一步:为ETH提供50MHz的时钟。对于STM32F4,这通常来自PLL的输出。你需要配置PLL,使得
PLLQ输出50MHz,并连接到ETH_RMII_REF_CLK。如果使用LAN8720A,它需要外部输入50MHz时钟(由MCU提供或外部晶振提供),并在CubeMX中正确选择时钟源。
- 配置LWIP:在
Middleware下找到LWIP并启用它。在Platform Settings中,根据你的PHY芯片型号,设置正确的地址(LAN8720A地址通常为0或1)和复位引脚(如果使用)。 - 生成代码:设置好项目名称、路径和IDE后,点击
Generate Code。
3. 以太网驱动与LWIP协议栈深度解析
生成的代码提供了骨架,但深入理解其工作原理才能应对复杂问题。
3.1 PHY芯片驱动与链路状态检测
PHY芯片需要通过MDIO(管理数据输入输出)接口进行配置和状态查询。HAL库提供了相关函数,但我们需要理解其流程。
// 文件路径:Src/ethernetif.c (由CubeMX生成,需要用户完善) // 这是一个简化的PHY状态查询函数示例 uint32_t ETH_PHY_GetLinkState(ETH_HandleTypeDef *heth) { uint32_t phyreg = 0; uint32_t linkState = 0; /* 读取PHY芯片的基本状态寄存器(BMCR)或状态寄存器(BSR) */ if(HAL_ETH_ReadPHYRegister(heth, PHY_ADDRESS, PHY_BSR, &phyreg) != HAL_OK) { return ETH_LINK_DOWN; } /* 检查“链路建立”位 */ if((phyreg & PHY_LINKED_STATUS_BIT) != 0) { linkState = ETH_LINK_UP; /* 可以进一步检查速度和双工模式 */ if(HAL_ETH_ReadPHYRegister(heth, PHY_ADDRESS, PHY_SR, &phyreg) == HAL_OK) { if((phyreg & PHY_SPEED_STATUS_BIT) != 0) { // 100Mbps } else { // 10Mbps } if((phyreg & PHY_DUPLEX_STATUS_BIT) != 0) { // 全双工 } } } else { linkState = ETH_LINK_DOWN; } return linkState; }为什么需要这个函数?系统需要周期性地(例如每秒一次)调用此函数来检测网线是否插好、链路是否连通。这是解决“电脑未建立以太网连接”问题的第一步。如果链路未建立,协议栈将无法工作。
3.2 LWIP协议栈初始化与内存管理
LWIP是一个模块化的协议栈,其初始化过程涉及内存池(mem_init)、网络接口(netif_add)的添加和启动。
// 文件路径:Src/app_ethernet.c (用户应用文件) void ETH_Init(void) { ip_addr_t ipaddr, netmask, gw; /* 初始化LWIP内核 */ tcpip_init(NULL, NULL); /* 设置静态IP地址(也可配置为DHCP) */ IP4_ADDR(&ipaddr, 192, 168, 1, 100); IP4_ADDR(&netmask, 255, 255, 255, 0); IP4_ADDR(&gw, 192, 168, 1, 1); /* 添加网络接口 netif_add(struct netif *netif, ...) * 这个函数会调用我们在ethernetif.c中实现的 low_level_init */ netif_add(&gnetif, &ipaddr, &netmask, &gw, NULL, ðernetif_init, &tcpip_input); /* 注册默认网络接口并启动它 */ netif_set_default(&gnetif); netif_set_up(&gnetif); /* 如果你使用DHCP,需要启动DHCP客户端 */ // dhcp_start(&gnetif); }内存管理注意事项: LWIP的性能和稳定性很大程度上取决于内存配置。在lwipopts.h(用户配置文件)中,你需要根据应用调整:
MEM_SIZE:堆内存总大小。如果同时需要TCP和UDP,且有多个连接,需要设置足够大(如10KB+)。PBUF_POOL_SIZE和PBUF_POOL_BUFSIZE:pbuf内存池。用于存储网络数据包。更大的池可以处理更高的并发流量,但消耗更多内存。TCP_WND、TCP_MSS:TCP窗口和最大报文段长度。影响TCP传输速度。
4. 完整实战:构建一个TCP Echo服务器
现在,我们将利用配置好的环境,创建一个简单的TCP Echo服务器。客户端发送任何数据,服务器都将原样返回。
4.1 项目结构概览
YourProject/ ├── Inc/ │ ├── main.h │ ├── app_ethernet.h // 以太网应用头文件 │ └── lwipopts.h // LWIP配置头文件 ├── Src/ │ ├── main.c │ ├── app_ethernet.c // 以太网应用主文件 │ ├── ethernetif.c // CubeMX生成的底层接口(需修改) │ └── syscalls.c ├── Drivers/ └── LWIP/ // LWIP协议栈源码4.2 实现TCP服务器回调函数
TCP通信在LWIP中通过回调函数(Callback)机制实现。我们需要创建一个TCP控制块(PCB)并绑定回调。
// 文件路径:Src/app_ethernet.c static struct tcp_pcb *echo_pcb = NULL; // 接收数据回调函数 static err_t tcp_echo_recv(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err) { if (p == NULL) { // 连接关闭 tcp_close(tpcb); echo_pcb = NULL; return ERR_OK; } if (err != ERR_OK) { pbuf_free(p); return err; } // 立即回显数据 (Echo) tcp_write(tpcb, p->payload, p->len, TCP_WRITE_FLAG_COPY); tcp_recved(tpcb, p->len); // 告知TCP内核已处理数据 pbuf_free(p); // 释放pbuf return ERR_OK; } // 接受新连接回调函数 static err_t tcp_echo_accept(void *arg, struct tcp_pcb *newpcb, err_t err) { if (err != ERR_OK || newpcb == NULL) { return ERR_VAL; } // 为新连接设置接收回调函数 tcp_recv(newpcb, tcp_echo_recv); return ERR_OK; } // 创建并启动TCP Echo服务器 void tcp_echo_server_init(void) { err_t err; // 创建新的TCP协议控制块 echo_pcb = tcp_new(); if (echo_pcb == NULL) { printf("Failed to create PCB.\r\n"); return; } // 绑定到本地IP和端口 (端口号7是标准的Echo端口,也可用其他如8080) err = tcp_bind(echo_pcb, IP_ADDR_ANY, 7); if (err != ERR_OK) { printf("Failed to bind. Error: %d\r\n", err); tcp_close(echo_pcb); return; } // 进入监听状态,指定最大待处理连接数 echo_pcb = tcp_listen(echo_pcb); if (echo_pcb == NULL) { printf("Failed to listen.\r\n"); return; } // 设置连接接受回调函数 tcp_accept(echo_pcb, tcp_echo_accept); printf("TCP Echo Server started on port 7.\r\n"); }4.3 在主循环中集成并运行
// 文件路径:Src/main.c #include "app_ethernet.h" int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_ETH_Init(); // CubeMX生成的ETH初始化 MX_LWIP_Init(); // CubeMX生成的LWIP初始化 // 初始化我们的以太网应用(包含协议栈初始化和服务器创建) ETH_Init(); tcp_echo_server_init(); while (1) { // LWIP内核需要被周期性处理 sys_check_timeouts(); // 或者使用更通用的:MX_LWIP_Process(); // 这个函数内部会调用sys_check_timeouts()并处理接收到的数据包 // 它应该在主循环中尽可能频繁地被调用 // 其他应用任务... HAL_Delay(1); // 短暂延时,避免CPU占用率100% } }4.4 测试与验证
- 硬件连接:将开发板的RJ45接口通过网线连接到路由器或直接连接到电脑(需要使用交叉线或连接交换机)。
- 编译下载:将程序编译并下载到STM32开发板。
- 查看IP:如果配置了静态IP(如192.168.1.100),确保电脑IP在同一网段(如192.168.1.10)。如果使用DHCP,需要通过串口打印或其它方式查看获取到的IP。
- 使用网络调试工具测试:
- 打开网络调试助手(如TCP/UDP调试工具)。
- 创建TCP客户端,连接服务器地址
192.168.1.100,端口7。 - 发送字符串“Hello Ethernet”,你应该立即收到相同的“Hello Ethernet”回复。
成功现象:数据被正确回显,表明从物理层到应用层的整个以太网通信链路畅通。
5. 常见问题与深度排查指南
以太网开发中遇到的问题五花八门,以下是一个系统性的排查清单。
5.1 硬件与链路层问题
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 网口指示灯不亮 | 1. 电源未接通或电压不对。 2. PHY芯片复位失败或晶振不起振。 3. RJ45插座或网络变压器损坏。 4. PCB布线问题导致RMII信号异常。 | 1. 测量PHY芯片电源引脚电压。 2. 用示波器检查晶振波形(50MHz/25MHz)。 3. 检查复位引脚电平,确保上电后释放。 4. 检查RMII的REF_CLK是否有50MHz时钟输出(LAN8720A方案中由MCU或外部提供)。 |
| 指示灯亮但闪烁异常(常亮/不闪) | 1. 链路未成功建立(网线问题、对端设备问题)。 2. 自协商失败,强制速率/双工模式不匹配。 | 1. 更换网线,连接至已知正常的设备(如路由器)。 2. 在代码中读取PHY状态寄存器,检查链路状态、速率和双工模式。尝试在PHY初始化时强制设置模式。 |
| “无法设置移动热点,因为电脑未建立以太网连接” | 此问题通常出现在Windows系统共享网络时。根本原因是操作系统认为以太网适配器“未连接”(即链路层未UP)。 | 1.检查物理连接:网线是否插紧?设备是否上电? 2.检查设备管理器:以太网控制器是否有感叹号?驱动是否正常? 3.检查嵌入式设备:确认你的设备以太网驱动已正确初始化,PHY链路已建立( ETH_LINK_UP)。4.禁用再启用网络适配器:在Windows网络连接中操作。 |
5.2 协议栈与软件问题
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| Ping不通设备IP | 1. IP地址、子网掩码、网关设置错误。 2. 防火墙或安全软件阻止。 3. LWIP协议栈未正确启动或网络接口未启用。 4. ARP协议问题。 | 1. 确认设备IP与电脑IP在同一子网。 2. 暂时关闭电脑防火墙。 3. 在代码中确认 netif_set_up()已被调用。使用printf打印IP信息。4. 在电脑上执行 arp -a,查看是否能学习到设备的MAC地址。 |
| 能Ping通,但TCP连接失败 | 1. 服务器程序未监听指定端口。 2. 路由器/交换机端口限制。 3. LWIP内存不足,无法创建新的PCB。 | 1. 使用netstat -an(电脑)或类似命令检查端口监听状态。确保服务器已调用tcp_listen。2. 尝试直连排除网络设备问题。 3. 增大 lwipopts.h中的MEM_SIZE和TCP_WND。 |
| 通信不稳定,时断时续 | 1. 网络负载过大,内存池耗尽。 2. 中断处理不当,丢失数据包。 3. 软件看门狗复位导致网络重启。 | 1. 监控LWIP内存使用情况。优化pbuf和mem的配置。2. 确保以太网接收中断(RX)优先级合理,且中断服务函数快速退出,将处理交给线程。 3. 在以太网任务中定期喂狗,或将网络任务优先级提高。 |
| Wireshark抓包显示“The capture file appears to have been cut short” | 抓包文件不完整或损坏。在嵌入式场景中,可能是: 1. 存储抓包数据的缓冲区溢出或磁盘已满。 2. 抓包过程被意外终止(如设备复位)。 | 1. 检查存储空间。 2. 确保抓包会话正常停止。对于嵌入式设备,如果通过日志输出抓包,需保证输出过程不被中断。 3. 尝试重新抓包。 |
5.3 高级调试技巧:使用Wireshark进行协议分析
Wireshark是定位网络问题的终极利器。
- 连接与抓包:将电脑和设备接入同一个交换机,或在电脑上设置网络桥接,在Wireshark中选择正确的网卡开始抓包。
- 过滤分析:
eth.addr == xx:xx:xx:xx:xx:xx:过滤特定MAC地址。ip.addr == 192.168.1.100:过滤特定IP地址。tcp.port == 7:过滤特定端口。
- 关键帧分析:
- ARP包:查看设备是否正确发出ARP请求/应答。没有ARP,则无法进行IP通信。
- TCP三次握手:查看
[SYN],[SYN, ACK],[ACK]序列。握手失败可能是防火墙或服务未监听。 - 数据包校验和:Wireshark会验证以太网帧校验和、IP校验和、TCP/UDP校验和。如果显示校验和错误(黑色背景),可能是网卡硬件校验和卸载(Checksum Offload)功能导致,可在Wireshark的
Edit -> Preferences -> Protocols -> IPv4中关闭“Validate the IPv4 checksum if possible”来避免误判,但真实环境中的校验和错误必须重视。
6. 进阶主题与最佳工程实践
掌握了基础通信后,这些进阶实践能让你的以太网设计更健壮、更专业。
6.1 动态主机配置协议(DHCP)集成
静态IP适合固定网络,而DHCP能自动获取IP,提升设备部署灵活性。
// 在ETH_Init()中,使用DHCP替代静态IP配置 // IP4_ADDR(&ipaddr, 0, 0, 0, 0); // IP4_ADDR(&netmask, 0, 0, 0, 0); // IP4_ADDR(&gw, 0, 0, 0, 0); // netif_add(&gnetif, &ipaddr, &netmask, &gw, ...); // dhcp_start(&gnetif); // 需要周期性调用 dhcp_fine_tmr() 和 dhcp_coarse_tmr() // 通常在 sys_check_timeouts() 中会自动处理最佳实践:实现超时回退机制。如果DHCP获取失败(例如超时),应自动切换到一个预配置的静态链路本地地址(如169.254.x.x)或用户指定的静态IP,保证设备始终可访问。
6.2 网络连接状态管理
一个健壮的系统需要持续监控网络状态。
// 创建一个状态监控任务或定时器回调 void Network_Monitor_Task(void const *argument) { uint32_t prevLinkState = ETH_LINK_DOWN; for(;;) { uint32_t currentLinkState = ETH_PHY_GetLinkState(&heth); if(currentLinkState != prevLinkState) { prevLinkState = currentLinkState; if(currentLinkState == ETH_LINK_UP) { printf("Ethernet Link UP.\r\n"); netif_set_link_up(&gnetif); // 通知LWIP链路已就绪 // 如果是DHCP,可以触发重新申请 // dhcp_renew(&gnetif); } else { printf("Ethernet Link DOWN.\r\n"); netif_set_link_down(&gnetif); // 通知LWIP链路断开 // 清理相关网络连接 } } osDelay(1000); // 每秒检查一次 } }6.3 性能优化与内存管理
- 使用Zero-Copy:在
ethernetif.c的low_level_input函数中,尽量将DMA接收到的数据缓冲区直接包装成pbuf,而不是复制数据。这能极大提升吞吐量。 - 调整LWIP参数:根据应用调整
lwipopts.h。TCP_SND_BUF/TCP_WND:增大可提升TCP吞吐量。TCP_MSS:设置为以太网MTU(1500)减去IP和TCP头(通常为1460)。LWIP_STATS和LWIP_DBG:在开发阶段启用统计和调试信息,便于优化。
- 中断与任务优先级:确保以太网RX中断有足够高的优先级,及时响应数据包。将LWIP的
tcpip_thread或你的网络处理任务设置为较高的OS任务优先级。
6.4 安全性考量
- 输入验证:在TCP/UDP应用层,对所有接收到的数据进行长度和格式验证,防止缓冲区溢出。
- 连接数限制:在
tcp_listen中设置合理的最大未决连接数(backlog)。实现自定义的tcp_accept回调,可以基于源IP地址进行过滤或限制总连接数。 - 防火墙规则:如果设备功能复杂,考虑实现简单的包过滤规则,例如只允许特定IP段访问管理端口。
6.5 生产环境部署检查清单
在设备量产前,请对照此清单进行检查:
- [ ]硬件:PCB的RMII信号线是否等长?电源滤波电容是否齐全?网络变压器型号是否正确?金属外壳是否良好接地(通过电容)?
- [ ]链路:在不同长度、类型的网线下,百兆/十兆自协商是否都成功?长距离传输(接近100米)是否稳定?
- [ ]协议:DHCP获取IP是否稳定?DNS解析是否正常?持续Ping 24小时,有无丢包或延迟突变?
- [ ]压力:进行大数据量(如持续TCP吞吐测试)、多连接并发测试,观察内存是否泄漏、系统是否死机。
- [ ]异常处理:模拟网线热插拔、路由器重启、网关变更等情况,设备网络功能是否能自动恢复?
- [ ]配置:IP、MAC地址等参数是否支持通过串口、网页或其他方式配置并永久保存?
从理解以太网帧结构中的校验和,到为STM32选配合适的PHY芯片;从用CubeMX快速搭建LWIP工程,到亲手实现一个稳定的TCP服务器;从解决“链路灯不亮”的硬件故障,到用Wireshark洞察协议交互的每一个细节——以太网设计是一个融合了硬件知识、驱动编写、协议理解和调试艺术的综合工程。
希望这份近万字的指南,能为你扫清从概念到实战的障碍。真正的掌握源于动手,建议你立即找一个开发板,从点亮一个链路指示灯开始,逐步完成ping通、TCP通信,直到实现你自己的应用协议。过程中遇到的每一个坑,都会让你对“以太网”这三个字有更深的理解。如果在实践中遇到新的问题,欢迎在评论区交流讨论,共同攻克技术难关。