
搞嵌入式这么多年网络协议栈这块一直是很多人的心结。裸机时候用个现成TCP/IP库总觉得束手束脚切到带操作系统的平台又觉得协议栈配置复杂文档少坑又多。这次我把FreeRTOSTCP在实战项目里做TCP服务器的完整过程捋了一遍从环境搭建到代码落地再到最后抓包排查问题全程记录给后面要走这条路的兄弟提供个参考。这个实验的核心目标很明确在一块MCU上跑FreeRTOS通过它的TCP协议栈组件建立一个TCP服务器能够接受多个客户端连接并回显数据。它不是跑在Linux开发板上那种“洒洒水”的活儿而是实打实地在资源受限的MCU上做网络通信内存管理、任务调度、缓冲区策略全都要自己心里有数。如果你正准备在STM32、ESP32这类硬件上做联网功能或者是想把项目从裸机网络库迁移到RTOS环境这篇内容应该能帮你少踩不少坑。1. 整体架构设计与核心思路拆解1.1 FreeRTOSTCP为什么值得选聊FreeRTOSTCP之前先说说市面上几个常见的选择。lwIP是开源的轻量级协议栈用的人多资料也相对好找但它的API抽象层比较厚配置项多到让人眼花缭乱而且跟FreeRTOS的集成并不是“原生”的要自己搞定互斥锁、信号量、内存分配这些适配工作。有些商用闭源协议栈倒是开箱即用但授权费用对小项目来说是个负担出了问题还只能干瞪眼。FreeRTOSTCP是FreeRTOS官方出品的协议栈最大的优势就是跟FreeRTOS内核“师出同门”。它直接基于FreeRTOS的任务调度、队列、信号量、软件定时器来实现不需要额外做操作系统的适配层。这个意义非常大意味着你跑在系统里的任务和协议栈内部的任务天然就是协同工作的优先级、时间片、资源竞争这些问题都比较好梳理。官方提供的移植层也很清晰网络接口驱动、时钟基准、内存分配都能对应到具体的移植文件里。如果你的硬件平台是官方支持的前面那些“磨刀”功夫基本能省掉一大半。1.2 实验整体架构与功能拆解这个TCP服务器实验从模块划分上实际上是三层结构第一层是硬件抽象层主要包含以太网PHY芯片驱动、MAC控制器驱动以及跟硬件绑定的DMA描述符和中断处理。这一层做的事情是把物理网线上的数据帧收下来或者把要发送的数据帧送出去。第二层是系统服务层由FreeRTOS内核提供任务调度、时间管理、内存分配FreeRTOSTCP协议栈在这些基础服务之上运行。协议栈内部有自己的任务也有被应用任务调用的API接口。第三层是应用层我们的TCP服务器逻辑就放在这一层。它创建一个socket绑定端口监听连接然后为每个客户端创建一个独立的处理任务实现并发通信。从功能角度看这个实验要覆盖的点包括协议栈初始化、网络接口配置、socket生命周期管理、多客户端并发处理、数据收发缓冲区策略、TCP断开重连机制。在设计方案时我选择了一个比较经典的“一个监听任务 每客户端一个处理任务”的模型。监听任务只负责accept新的连接一旦有客户端连上来就把这个连接交给一个专门的任务去处理。这样做的好处是单个客户端的阻塞式收发不会影响到其他客户端的正常通信。1.3 为什么采用任务分离的设计方式很多刚上手的朋友会问为什么不用一个任务循环去遍历所有连接那样代码看起来更简单啊。当然在只有一个或两个客户端、数据量也不大的场景下这种轮询方式确实能跑但对MCU这种资源有限的嵌入式环境任务分离的方式有更实际的好处。一方面阻塞式API在单任务轮询下会变成“一夫当关”的阻塞点。如果某个客户端连接后不发送数据轮询任务就会卡在接收调用上其他客户端的连接请求就永远得不到响应。而任务分离后每个连接是一个独立任务它卡它的其他连接的数据照常处理监听任务也不会被拖住。另一方面TCP本身是流式协议数据的到达时间是完全不可预测的每个客户端绑一个任务可以用一个简单的阻塞式“收数据——处理——回数据”循环来表达业务逻辑这种代码的可读性非常高后续维护也方便。后续想让服务器支持MQTT、HTTP这些应用层协议直接套在这个模型上扩展就行。2. 开发环境搭建与协议栈集成2.1 硬件平台选择我用的是某款基于Cortex-M7内核的开发板主频跑在400MHz板载了百兆以太网PHY芯片通过RMII接口跟MAC层连接。选这个硬件的原因很简单Cortex-M7有足够性能跑协议栈做数据搬运而百兆以太网接口在大多数工业场景和物联网网关里都够用。如果你手里的是Cortex-M3或M4的板子也不必担心FreeRTOSTCP一个比较友好的地方是它并不挑剔内核只要能跑FreeRTOS理论上都可以移植。只是要注意RAM空间协议栈本身会占用一些内存再加上TCP收发缓冲区如果芯片的RAM只有几十KB跑起来会很吃力。2.2 编译工程搭建与组件获取在工程搭建上我用的是STM32CubeMX生成的基础工程因为CubeMX对FreeRTOS的集成已经非常成熟可以直接在图形界面里勾选FreeRTOS会自动生成带时间片轮转和优先级配置的初始化代码。手动集成的话需要自己处理启动文件、中断向量和堆栈设置那些东西能省就省。FreeRTOSTCP组件的获取方式有几种最简单的是直接去FreeRTOS官网下载嵌入式协议栈的Source包整个源码树里已经包含了核心组件。需要注意的是版本匹配不同版本的FreeRTOS内核跟TCP组件之间有依赖关系最好打包一起升级不要单独只换协议栈的版本。把源文件加入工程后有几个关键配置项需要修改。第一个是FreeRTOSConfig.h中的configUSE_TIMERS要设置为1因为协议栈内部很多超时和重传机制依赖软件定时器。第二个是configTOTAL_HEAP_SIZE这个值决定了FreeRTOS的堆大小我在这块板子上设置为512KB因为TCP收发缓冲区和各种网络控制块都要从这个堆里分配。2.3 协议栈配置的“灵魂”选项FreeRTOSTCP跟lwIP不一样的地方是它没有那么多分散的配置头文件绝大部分选项都集中在FreeRTOSIPConfig.h里。这个文件是你跟协议栈打交道的主要入口。先说网络接口的配置#define ipconfigUSE_DHCP 0 #define ipconfigIP_ADDRESS 192.168.1.100 #define ipconfigNET_MASK 255.255.255.0 #define ipconfigGATEWAY 192.168.1.1我直接静态指定了IP地址没有开DHCP。实验环境下路由器或交换机旁边就是电脑完全可以直接静态配置而且静态IP下问题排查会简单很多。代码里通过FreeRTOS_IPInit函数传入这些参数进行初始化。如果你的使用场景是产品级别的比如设备要接入各种办公网络那还是要老老实实开DHCP对配置为“ipconfigUSE_DHCP 1”并在代码里等待IP分配完成后再启动应用任务。再说内存分配方式#define ipconfigUSE_EPHEMERAL_PORTS 1 #define ipconfigNUM_DEFERRED_TASKS 1 #define ipconfigEVENT_QUEUE_LENGTH 16 #define ipconfigTCP_RX_BUFFER_LENGTH 4096 #define ipconfigTCP_TX_BUFFER_LENGTH 4096协议栈在收发TCP数据时每个socket都会为接收方向和发送方向分别建立缓冲区缓冲区大小就由上面这两个宏控制。我设置为4KB对于回显服务这种小数据量的场景已经绰绰有余。需要留意的是这些缓冲区都来自FreeRTOS堆每个连接都会占用发送和接收两个4KB空间再加上socket控制块和驱动描述符连接数多了内存会被迅速吃掉。有些文档建议用内存池的方式FreeRTOSTCP也提供了对网络缓冲区使用独立内存池的选项好处是可以精确定位到缓冲区分配失败的case不至于把系统堆搞乱。这次实验我用的是默认的堆分配方式因为手上的RAM足够大而独立内存池的调试工作又比较费时间。3. TCP服务器核心代码实现与关键逻辑3.1 服务启动流程代码的核心入口是网络初始化加服务器任务启动void StartNetwork(void *pvParameters) { // 等待网络就绪 while (!FreeRTOS_IPIsNetworkUp()) { vTaskDelay(pdMS_TO_TICKS(100)); } // 启动TCP服务器监听任务 xTaskCreate(Task_TCP_Server, TCPServer, 1024, NULL, tskIDLE_PRIORITY 2, NULL); vTaskDelete(NULL); }FreeRTOS_IPIsNetworkUp这个函数在所有网络初始化完成后会返回pdTRUE。在DHCP模式下这个等待过程可能需要几十秒。静态IP方式基本启动即网络就绪。服务器任务里做的是标准的socket四步曲static void Task_TCP_Server(void *pvParameters) { Socket_t xSocket; struct freertos_sockaddr xServerAddr, xClientAddr; socklen_t xClientAddrLen sizeof(xClientAddr); // 1. 创建socket xSocket FreeRTOS_socket(FREERTOS_AF_INET, FREERTOS_SOCK_STREAM, FREERTOS_IPPROTO_TCP); if (xSocket FREERTOS_INVALID_SOCKET) { vTaskDelete(NULL); } // 2. 绑定端口 xServerAddr.sin_family FREERTOS_AF_INET; xServerAddr.sin_port FreeRTOS_htons(50321); xServerAddr.sin_addr (uint32_t)FreeRTOS_inet_addr(0.0.0.0); FreeRTOS_bind(xSocket, xServerAddr, sizeof(xServerAddr)); // 3. 设置监听 FreeRTOS_listen(xSocket, 4); // 4. 循环接受连接 for (;;) { Socket_t xConnected FreeRTOS_accept(xSocket, xClientAddr, xClientAddrLen); if (xConnected ! FREERTOS_INVALID_SOCKET) { // 为每个客户端创建独立处理任务 xTaskCreate(Task_TCP_ClientHandler, TCPClient, 2048, (void*)xConnected, tskIDLE_PRIORITY 1, NULL); } } }这段代码有几个细节值得说明。FreeRTOS_socket的第三个参数在标准socket编程里对应的是协议类型这里使用FREERTOS_IPPROTO_TCP。返回值是Socket_t类型的句柄类似于文件描述符的概念。FreeRTOS_htons要把端口号从主机字节序转成网络字节序。很多同学第一次用的时候忘了这一步然后发现端口始终对不上抓包一看源端口或目的端口变成了奇怪的值。端口号的选用我避开了80、443这些常用的端口也没有用常见的测试端口。产品开发时建议给每个服务分配一个有辨识度的端口比如模拟项目X用的是50321这种高位端口在局域网内冲突的概率小很多。FreeRTOS_accept是阻塞式的没有客户端连接时任务会一直挂在这里不会消耗CPU时间。有了新连接进来accept返回一个新的socket句柄这个句柄就独立代表这条TCP连接了。原监听socket继续等着下一个连接。3.2 客户端处理任务与回显逻辑客户端连接建立后核心工作就在处理任务里static void Task_TCP_ClientHandler(void *pvParameters) { Socket_t xConnected (Socket_t)pvParameters; uint8_t ucBuffer[1024]; BaseType_t xRxCount; for (;;) { xRxCount FreeRTOS_recv(xConnected, ucBuffer, sizeof(ucBuffer), 0); if (xRxCount 0) { // 原样回显给客户端 FreeRTOS_send(xConnected, ucBuffer, xRxCount, 0); } else if (xRxCount 0) { // 对端关闭连接 break; } else { // 出错或超时 break; } } FreeRTOS_closesocket(xConnected); vTaskDelete(NULL); }这个回显逻辑本身很简单但FreeRTOS_recv和FreeRTOS_send的返回值语义必须搞清楚。FreeRTOS_recv的返回值有三种情况大于0表示实际接收到的字节数等于0表示对端已优雅关闭连接小于0表示错误或超时。代码里不能想当然地认为recv返回0就是出错这个区分在断连处理时非常关键。FreeRTOS_send的返回值是实际发送成功的字节数。TCP是流协议调用一次send并不保证能把全部数据都送出去可能只发送了一部分。严谨的写法应该是检查返回值如果小于缓冲区长度就得循环发送剩余数据。不过对于1KB这种小数据块在发送缓冲区足够大的情况下一次send基本都能全部发出去。任务退出时要把socket关闭然后删除自己。如果不在任务结束前关闭socket连接资源无法及时释放下一次客户端连接时可能因为资源不足而失败。3.3 超时控制与断开检测TCP处理里最容易忽略的是recv的超时设置。默认情况下recv会一直阻塞下去看起来没问题但如果客户端连接后不发送任何数据这个任务就会永久挂起占着一个任务栈和一组socket缓冲区。如果这样的空闲连接多了系统资源迟早被耗尽。解决思路是给recv设置接收超时struct timeval xTimeout; xTimeout.tv_sec 60; xTimeout.tv_usec 0; FreeRTOS_setsockopt(xConnected, 0, FREERTOS_SO_RCVTIMEO, xTimeout, sizeof(xTimeout));设置了超时后如果60秒内没有数据到达FreeRTOS_recv会返回一个负值此时可以主动关闭这个空闲连接释放资源。这在物联网设备上尤其重要因为很多传感器设备连接后可能会长时间不发送数据但不代表它已经断开了服务器是等它发数据还是超时清理需要在设计时想清楚。我这个实验的场景是回显服务采用超时主动断开的策略因为客户端连上来不说话占着资源没有任何意义。如果你的产品需要支持长连接可能需要更精细的心跳检测机制。4. 内核与协议栈的配合机制4.1 任务优先级与内存分配策略任务优先级的设定表面看只是一个数字实际上牵动着整个系统的实时性表现。我把监听任务设为tskIDLE_PRIORITY 2客户端处理任务设为tskIDLE_PRIORITY 1也就是监听任务的优先级高于客户端任务。这样设计基于一个原则新的连接请求应该被快速响应。如果监听任务优先级低了在高负载情况下应用任务和协议栈任务会抢占CPU导致accept得不到调度新的客户端迟迟连接不上表现为“连接超时”。客户端处理任务之间是平级的互相之间不会干扰。如果某个客户端的业务需要更低延迟可以在创建任务时调整优先级或者给关键客户端的数据处理分配一个高优先级任务。内存方面每个任务都有自己的栈空间。监听任务栈是1024字客户端处理任务栈是2048字。栈大小的估算主要看任务里定义的局部变量和函数调用深度。客户端任务里有一个1KB的缓冲区定义在栈上所以栈必须至少大于1KB再算上FreeRTOS_send和FreeRTOS_recv这两个函数的调用帧2048字在多字节对齐的结构里是够用的。如果你的代码里要处理更大的数据块比如DEFLATE解压就必须相应增大任务栈。4.2 协议栈内部任务和事件机制FreeRTOSTCP有自己的内部任务机制通过事件队列跟应用层交互。有一个专门的定时任务负责TCP的超时重传、keep-alive计时等周期性工作。还有一个可选的“延迟处理任务”当协议栈在中断上下文接收到数据帧时可以选择把大部分处理工作推到这个延迟任务里执行从而缩短中断服务函数的执行时间。我建议在中断里尽量少做事情。以太网中断触发后驱动只是把DMA收到的数据帧挂入队列后续的协议栈解析、TCP确认等操作都放在任务里去做。这种“中断快进、任务慢出”的模式是嵌入式网络的最佳实践。事件队列长度ipconfigEVENT_QUEUE_LENGTH设置为16如果网络流量较大队列满了新到的数据帧会被丢弃TCP的重传机制会兜底。这也就是TCP比UDP可靠的原因之一丢帧了协议栈会自动补偿。4.3 缓冲区生命周期与零拷贝考量FreeRTOSTCP有一个特别值得说的特性网络缓冲区可以在驱动和协议栈之间高效流转。驱动收到数据帧后DMA把数据放进缓冲区协议栈处理完后这个缓冲区可以直接交给应用层作为recv的数据源不需要再复制一份。在收发路径上尽量减少数据复制次数这对MCU的CPU负载和内存带宽友好。实际调用FreeRTOS_recv时数据是从网络缓冲区的接收队列拷贝到应用缓冲区里的。这一步拷贝看起来有点浪费但好处是应用层拿到的是一个独立的、稳定的数据副本之后自己怎么解析处理都不会影响协议栈内部状态。如果你有特殊的需求想直接引用协议栈内部缓冲区避免拷贝FreeRTOSTCP也提供了零拷贝接收的API但使用起来约束多一点缓冲区必须及时归还否则协议栈的内存不足。对于我这种回显场景数据拷贝带来的性能开销完全可以忽略代码简单清晰最重要。5. 网络连通性测试与问题排查实录5.1 测试方法与预期结果实验测试环境是一台电脑直接用网线连接开发板没有经过交换机。电脑的网卡配置为静态IP跟开发板同一网段。这样可以最大程度缩小问题排查范围因为网络上只有两个节点如果通了就是通的不通就是代码的问题。测试工具我用了两种。第一是电脑端的telnet工具可以直接连上开发板的50321端口发送数据马上就能看到回显。第二是网络调试助手它有图形界面还能统计收发字节数。两个工具配合着用telnet用来确认基本连通性图形工具用来做多连接并发测试。5.2 实际测试过程记录开发板启动后串口打印了网络初始化日志可以清楚看到MAC地址、IP地址、网关和掩码。串口日志的内容对于判断协议栈是否正常初始化非常有帮助。首次TCP连接测试还算顺利telnet连接到192.168.1.100 50321端口输入“hello freeRTOSTCP”服务器立刻原样回显。这次成功说明socket创建、绑定、监听、accept以及收发数据的整条链路都是通的协议栈和驱动的匹配没有问题。接下来做双客户端并发测试。我用网络调试助手开了两个连接分别发送数据两个连接都能及时收到回显互不干扰。这说明“每客户端一个任务”的设计起到了预期效果阻塞式接收没有变成全局的阻塞点。测试结果汇总测试项目预期结果实际结果结论单客户端连接回显数据原样返回数据原样返回通过双客户端并发两个连接独立回显两个连接独立回显通过客户端强制断开服务端感知并清理recv返回0任务退出通过连续大数据收发无丢包无错乱收发稳定通过5.3 排查过的两个典型问题测试过程中并非一帆风顺有两个问题花了不少时间才定位清楚。第一个问题是板子连上线后电脑端telnet完全连不上显示“无法打开到主机的连接”。排查思路是先看串口日志协议栈初始化正常IP地址也打印出来了。再看板子的网口指示灯连接状态是正常的。于是我用电脑ping开发板的IP发现完全不通。问题范围进一步缩小到驱动链路层或协议栈底层。后来在代码里加了一个简单的调试手段统计网卡驱动收到的帧数和协议栈处理的帧数。对比后发现驱动收到的帧数一直在增加但协议栈处理的帧数始终为零。这就说明数据帧是在驱动和协议栈之间的交接环节丢失的。最终定位到问题根源DMA描述符的内存没有按硬件要求做对齐导致驱动在配置DMA时出错。修复对齐问题后ping立即通了。第二个问题比较隐蔽。双客户端并发测试时发现一个新连接的accept偶尔会失败现象是客户端发送连接请求后要等了好久才成功连接。排查发现监听socket的连接队列满了。FreeRTOS_listen的第二个参数是backlog大小我原本设置为4本身不算小但每次accept后由accept返回的socket句柄是直接作为任务参数传递的如果任务创建不及时或者任务栈不够导致创建失败那个已建立的连接就一直没被处理占着队列位置不释放。后来在创建任务失败时主动关闭这个已连接的socket队列就不会被“死连接”占满。5.4 调试工具与排查小技巧送给各位几个排查网络问题时非常管用的经验。串口日志一定要善用。FreeRTOSTCP本身提供了一些调试输出宏在FreeRTOSIPConfig.h里能打开ipconfigHAS_PRINTF然后通过prvOutputMessage函数输出协议栈的内部状态。调试完记得关掉串口波特率不够高时日志输出本身是会拖慢协议栈的。抓包工具是定位分层问题的利器。开发板的网卡和电脑之间没法直接抓包时可以加一个笨办法让数据包经过一台电脑的共享网络再转发到开发板在这台电脑上启用抓包。正常拓扑下的抓包结果可以直接看到TCP的三次握手有没有完成、有没有重传、RST是谁发的问题出在哪一层一目了然。还要特别提醒一点使用FreeRTOS_recv返回0表示对端关闭连接这一点容易被误判为错误。很多数据长度为零就返回零的协议比如某些自定义协议的心跳帧不能简单用recv0来判断断开需要区分返回值是整数0还是负数。6. 实时性优化与多任务资源权衡6.1 内存资源的精确估算在MCU方案里RAM始终是稀缺资源。实验板RAM虽然大但真实产品里RAM往往只有64KB甚至更少。我把这个项目消耗的内存做个简要梳理如果你要裁剪到更小的芯片上这就是参考依据。TCP服务器依赖的几个内存大头协议栈本身的核心缓存和任务栈这个吃掉了大概十几KB每个TCP连接有两个4KB的收发缓冲区一个连接就是8KB客户端处理任务的任务栈按2048字计算在4字节对齐的32位平台上大约是8KB还有监听任务栈相对较小。如果你想让芯片同时支持8个TCP客户端粗略一算光连接相关的缓冲区就是64KB这还没算协议栈核心和任务栈。所以在资源受限的平台上不一定非要撑那么多并发连接很多场景下4个连接完全够用。连接数上限要用FreeRTOSconfig里的一个配置项来限制FreeRTOSTCP中对同时打开的socket数量有上限防止内存早早耗尽。我的建议是在项目初期就把内存预算表做出来哪部分固定消耗、哪些跟连接数线性增长心里有数。等到联调时发现内存不够用再回头裁剪架构代价就大了。6.2 提升吞吐量的路径如果后续实验涉及大文件传输或高频率数据流有几条性能优化路径值得考虑。DMA描述符和缓冲区尽量放在独立的内存区域很多MCU支持多个RAM总线可以从硬件层面减少访问冲突。把网络DMA缓冲区和协议栈缓冲区放到不同的内存区域对吞吐量提升有明显帮助。调整接收缓冲区大小。如果一次传输的数据块比较大比如1KB那么一个4KB的接收缓冲区就显得捉襟见肘数据可能被TP分段多次recv才能取完。缓冲区太大会占用大量内存需要根据最大的应用数据块尺寸来定。FreeRTOSTCP里有个“复用缓冲”机制收到的数据帧如果是在同一个TCP连接里的连续帧协议栈可以合并后再交付给应用层。把这个特性配合稍微大一点的接收缓冲区使用recv调用次数会减少任务上下文切换的开销也降低了。6.3 低功耗场景下的网络策略如果你的产品是电池供电的物联网设备网络这块的功耗策略需要单独考虑。TCP协议本身有keep-alive机制可以周期性地发送探测包维护连接但保持连接本身就有功耗开销。一种省电思路是客户端有数据要发时才唤醒模组连接服务器数据传完就关闭连接让网络接口进入低功耗状态。这种情况下服务器端的业务逻辑要考虑半开连接和SYN cookie这些场景实现复杂度会上一个台阶。另一种思路是维持长连接但降低keep-alive探测频率。FreeRTOSTCP的keep-alive时间是可以配置的。把空闲探测周期拉长能在长时间无数据通信时节省不少功耗。7. 实验总结与进阶扩展思考做完这个实验我的感受是FreeRTOSTCP的协议栈设计确实比很多商用协议栈要有诚意得多。API风格上既保留了标准socket编程的习惯又针对MCU嵌入式环境做了精简和限制上手成本低。但也不得不说它的文档和示例代码相对内核来说少一些尤其是针对特定硬件平台的移植说明更多还是要靠查看源码去理解设计思路。动手之前建议把FreeRTOSIPConfig.h里的每个配置项都仔细读一遍注释结合官方给出的应用说明文档一起看。紧接着可以做的方向有三个第一个是把HTTP服务器模块加进来在现有TCP服务器基础上解析HTTP请求回传网页数据这个方向对调试工具、数据可视化展示都很有价值第二个是做一个基于TCP自定义协议的数据采集服务一端采集传感器信息打包另一端用上位机下发配置指令这更接近实际产品形态第三个是研究一下FreeRTOSTCP的TLS支持把安全性补上但TLS会占用较多内存和算力需要评估硬件性能是否达标。实验过程中给我印象最深的还是那一次DMA对齐问题引发的“驱动收到帧但协议栈不处理”的现象。这类问题不会在静态代码检查中暴露也不会在仿真中报错只会在真实硬件上跑起来才出现。调试网络问题的时候把串口日志、抓包结果和设备端统计三方面信息结合起来看定位速度会快很多。最后提醒一句在你的项目真正要量产之前一定要做长时间稳定性测试。网络协议栈的很多问题比如内存泄漏、文件描述符耗尽、死锁都是隐藏极深的慢性病跑个几分钟看不出问题连续跑几天可能就会暴露。TCP服务器的可靠性不是靠代码写出来的是靠测出来的。