首页 / 资讯中心 / 文章详情

Delphi UDP广播实现局域网设备发现与心跳超时机制

Delphi UDP广播实现局域网设备发现与心跳超时机制 ★ FEATURED ARTICLE
简介面向Delphi开发者与网络通信初学者的UDP广播通讯示例代码包聚焦无连接数据报传输中的广播与单播两种模式可用于理解低延迟、高效率的UDP通信机制及组件化实现方式。压缩包共111个文件约7.47MB以pas源码、dfm窗体、dcu/obj编译单元、exe与dll运行库为主辅以txt说明、cfg配置和项目文件便于按源码、可执行文件与依赖模块分层查阅。示例围绕TUDPClient、TUDPServer展开涉及Broadcast属性、端口与主机设置、SendData发送、OnUDPRead接收事件以及Memo日志记录和Try…Except异常处理能帮助读者梳理广播包发送、单播定向接收与调试排错的完整思路。已有1105人学习适合作为Delphi网络编程入门与课堂示例的参考素材。1. 一个局域网设备发现需求为什么这套示例值得照着做一套内网设备管理工具最容易被低估的模块不是界面而是设备怎么被找到。某实验室做过一版巡检程序最初用 TCP 逐个 IP 轮询254 个地址扫一遍要十几秒交换机上还堆着一批半开连接后来改成 UDP 广播发一帧不到 1 毫秒在线设备自己报名字。改动的关键不在速度而在于把「谁在线」这个问题从「我去挨个问」换成了「你主动报」。这份 Delphi 开发的 UDP 广播通讯示例解决的就是这类需求局域网内的设备发现、状态公告、配置下发、通知推送。它适合正在做网络通讯课程设计、毕设或者要在工控内网工具里加一段轻量发现协议的人——代码量不大但收发、编解码、网卡绑定、超时判定这几条链路是完整的。2. 技术选型与协议设计Indy 组件、报文结构和广播寻址的取舍动手之前得先把三件事定下来用什么库收发、报文长什么样、往哪个地址发。这三件事任何一件定错后面调代码的时间会成倍增加。比较常见的失败情形是代码跑起来了本机收发正常换一台机器就收不到——查了半天是广播地址选了全局广播中间隔着三层设备或者报文用竖线分隔字符串设备名里恰好带了个竖线解析直接错位。选型和格式在写第一行代码之前就该固定下面的取舍来自几个实际交付过的内网项目。2.1 Indy 的 UDP 组件与原生 Winsock 的取舍Delphi 做 UDP 有两条路一是 Indy 自带的TIdUDPClient/TIdUDPServer二是直接调 Winsock 的WSAStartup、socket、bind、sendto、recvfrom。原生 Winsock 控制力最强能精确控制 socket 选项、缓冲区大小、阻塞模式但代价是每个项目都要重写一遍生命周期管理而且select或完成端口的线程模型要自己搭。Indy 把这些包了一层阻塞调用放在内部线程里对外暴露事件回调几十行就能跑通收发。对比项Indy 组件原生 Winsock上手成本低属性赋值即可收发中需管理 socket 生命周期线程模型组件内置监听线程自建线程或 select 循环多网卡绑定通过 Bindings 集合逐条添加每个 socket 单独 bind广播开关TIdSocketHandle.Broadcastsetsockopt(SO_BROADCAST)可控性中部分细节被封装高所有选项可调调试难度低事件里能直接看数据中需要自己加日志我一般选 Indy理由是这套示例的定位是「可复用的最小完整链路」不是网络底层研究。Indy 在 Windows 和 Linux 上的行为基本一致同一份通讯单元可以直接编进控制台程序和 FMX 界面程序。要注意的地方有三个TIdUDPServer的BufferSize默认只有 1024 字节超过这个长度的报文会被截断必须显式调大OnUDPRead回调运行在 Indy 的接收线程里里面绝对不能碰 VCL 控件Bindings集合可以一次绑多张网卡但广播发送时如果只绑了0.0.0.0实际只会从默认路由那张网卡出去。这三点在后文的避坑章节还会展开。2.2 自定义报文格式魔数、版本、命令字与校验位的字段设计很多人第一版会用设备名|IP|状态这种分隔符字符串调试时肉眼看很方便上线就出事设备名里带分隔符、字段增减版本不兼容、半包截断后仍然当成合法数据解析。UDP 本身有报文边界一次recvfrom就是一条完整报文但长度仍要靠自己校验否则遇到其他程序发到同一端口的杂包会直接崩在解析函数里。我这套示例用的是定长头加变长载荷的二进制结构头部 12 字节后面跟载荷末尾 2 字节 CRC16偏移长度字段说明04Magic固定值用于快速识别是否为本协议报文41Version协议版本升级时据此兼容旧客户端51Cmd命令字1 公告、2 心跳、3 配置下发、4 查询62Seq序号用于请求响应配对和去重82PayloadLen载荷字节数不含头尾102Reserved保留后续扩展字段位12NPayload载荷统一 UTF-8 编码12N2Crc16对头部除 CRC 外全部字段加载荷做校验魔数的作用相当于给报文上一个黑匣子标签收到杂包时第一道判断就把它挡掉能省掉大量莫名其妙的解析异常。版本字段看着多余但只要设备固件和上位机不是同一天升级就一定用得上。PayloadLen是双保险既校验报文是否被截断也校验是否被追加了脏数据。载荷统一用 UTF-8而不是依赖系统 ANSI 代码页跨机器、跨系统才不会出现中文设备名变问号的情况。type // 协议常量改动时只需动这里 TUdpConst class public const Magic: Cardinal $4C414E31; // LAN1 const Ver: Byte 1; const HeaderSize 12; const CrcSize 2; const MaxPayload 1400; // 留足 MTU 余量避免 IP 分片 end; // 解析后的内存结构业务层只和它打交道 TUdpPacket record Cmd: Byte; Seq: Word; Payload: TBytes; PeerIP: string; PeerPort: Integer; end; function DecodePacket(const AData: TIdBytes; const APeerIP: string; const APeerPort: Integer; out APacket: TUdpPacket): Boolean; var Len, PayloadLen: Integer; Magic: Cardinal; CalcCrc: Word; begin Result : False; Len : Length(AData); if Len TUdpConst.HeaderSize TUdpConst.CrcSize then Exit; // 太短丢弃 Move(AData[0], Magic, SizeOf(Magic)); if Magic TUdpConst.Magic then Exit; // 非本协议报文 if AData[4] TUdpConst.Ver then Exit; // 版本不匹配 Move(AData[8], PayloadLen, SizeOf(PayloadLen)); if PayloadLen TUdpConst.MaxPayload then Exit; // 超长防越界 if Len TUdpConst.HeaderSize PayloadLen TUdpConst.CrcSize then Exit; Move(AData[Len - 2], CalcCrc, SizeOf(CalcCrc)); if CalcCrc Crc16(AData[0], Len - 2) then Exit; // 校验失败 APacket.Cmd : AData[5]; Move(AData[6], APacket.Seq, SizeOf(APacket.Seq)); SetLength(APacket.Payload, PayloadLen); if PayloadLen 0 then Move(AData[TUdpConst.HeaderSize], APacket.Payload[0], PayloadLen); APacket.PeerIP : APeerIP; APacket.PeerPort : APeerPort; Result : True; end;这段解析函数把所有拒绝路径都放在最前面只要有一项不满足就返回False业务层永远拿不到半成品数据。MaxPayload限制在 1400 字节以内是为了避开 IP 分片UDP 报文一旦分片任意一片丢失整包就废内网环境下这个限制几乎不影响功能却能显著降低丢包后的排查难度。Seq字段在这个示例里只做简单自增主要用途是让「查询—应答」能对得上号顺带做重复报文过滤。2.3 广播地址、端口与网段全局广播和定向广播的适用边界广播地址有两种写法。255.255.255.255是受限广播只在本地链路内传播不经过路由器192.168.1.255这种是定向广播由子网掩码算出来理论上可以跨路由器但绝大多数三层设备默认不转发。内网设备发现场景两者都能用区别在于多网卡机器上的行为。一台机器同时插了办公网和设备网两张网卡时往255.255.255.255发报文只会从系统默认路由那张网卡出去另一张网卡上的设备就永远收不到。正确做法是遍历所有活动网卡按各自的 IP 和掩码算出定向广播地址逐个发送。这套示例里封装了一个EnumBroadcastAddrs函数返回TStringList每项格式为网卡IP|广播地址发送时按条目绑定源 IP 再发。端口选择上避开 1900SSDP、5353mDNS、137/138NetBIOS这些被系统服务占用的知名端口。示例用的是 39001落在动态端口区间内冲突概率低。接收端绑定0.0.0.0:39001监听所有网卡发送端源端口填 0 让系统分配这样同一台机器上开多个实例也不会互相抢端口。// 遍历活动网卡算出每个网段对应的定向广播地址 function EnumBroadcastAddrs: TStringList; var AddrList: TStringList; I: Integer; NicIP, Mask, Bcast: string; begin Result : TStringList.Create; AddrList : TStringList.Create; try GetIpAddrList(AddrList); // 封装好的本机 IPv4 列表跳过 127.0.0.1 for I : 0 to AddrList.Count - 1 do begin NicIP : AddrList.Names[I]; Mask : AddrList.ValueFromIndex[I]; // 形如 255.255.255.0 if IsLoopback(NicIP) then Continue; Bcast : CalcBroadcast(NicIP, Mask); // 按位或取反得到 x.x.x.255 Result.Add(NicIP | Bcast); end; finally AddrList.Free; end; end;算出定向广播地址之后发送时用TIdSocketHandle把源 IP 绑到对应网卡就能保证报文从正确的物理网口出去。这一步多做几行能省掉现场部署时「为什么这台机器只能发现一半设备」的整轮排查。3. 总体架构与模块划分四层结构与设备表的职责边界广播通讯代码写到最后最容易失控的不是协议本身而是「谁在什么时候改设备列表」。收到报文在接收线程里界面刷新在主线程里定时器在另一个线程里三边同时读写同一个TList跑一天不出问题跑一周必崩。架构设计要解决的核心就是这个把通讯、解析、状态维护、界面呈现切开让每个模块只做一件事跨模块的交互只走一条明确的通道。3.1 分层结构界面层、通讯引擎、协议编解码、设备注册表这套示例分成四层层与层之间单向依赖下层不知道上层的存在。界面层负责显示设备列表、发起查询、下发配置。它不直接调用任何 socket API只调用通讯引擎暴露的几个方法Start、Stop、SendAnnounce、SendConfig以及订阅一个OnDeviceChanged事件。通讯引擎是唯一持有TIdUDPServer的地方负责绑定网卡、收发报文、把原始字节丢给编解码层。编解码层是纯函数集合输入字节数组输出TUdpPacket没有任何状态可以单独写单元测试。设备注册表负责维护设备记录、心跳超时、状态迁移是唯一有权修改设备数据的地方。这样切的好处很直接想换掉 Indy 改用原生 Winsock只动通讯引擎一个单元想改协议字段只动编解码层界面想换成 FMX 或者做成服务其他三层原样复用。我在另一个项目里就是因为当初没分层后来要从 VCL 迁到跨平台通讯代码和窗体代码缠在一起等于重写了一遍。3.2 设备表的数据结构与状态机设备表用TDictionarystring, TDeviceInfo键是设备唯一 ID不是 IP。原因是 DHCP 环境下 IP 会变用 IP 做主键会出现同一台设备在列表里出现两条记录的情况。设备 ID 由设备自己生成写死在固件里或者取网卡 MAC 加盐哈希报文里带上来。type TDeviceState (dsOnline, dsSuspect, dsOffline); TDeviceInfo record DevID: string; // 设备唯一标识来自报文载荷 DevName: string; // 展示名称 IP: string; // 最近一次报文来源 IP Port: Integer; LastSeen: TDateTime; // 最后一次收到心跳的时刻 FirstSeen: TDateTime; State: TDeviceState; MissCount: Integer; // 连续错过的心跳次数 end;状态机的规则是这样的收到任意报文MissCount归零状态置dsOnline更新LastSeen。定时扫描每秒跑一次如果Now - LastSeen超过 3 个心跳周期置dsSuspect界面上把该行标灰但仍然保留超过 6 个周期置dsOffline从活动列表移出但记录保留一段时间用于统计。用三个状态而不是简单的在线/离线是因为内网无线设备偶尔丢一两个包很正常直接判离线会导致界面上设备反复闪烁用「疑似」档缓冲一下体验稳得多。设备表的读写必须加锁。示例里用了一个TCriticalSection包住所有公开方法读操作也走锁因为TDictionary在遍历过程中被另一线程插入会直接抛异常。锁的粒度控制在「一次方法调用」级别不在锁内做任何 IO 或界面刷新。3.3 线程模型接收线程、发送队列与主线程的交互线程模型定成这样三条线第一条是 Indy 的接收线程OnUDPRead回调在这里执行。它只做两件事把AData、ABinding.PeerIP、ABinding.PeerPort打包成一个记录塞进线程安全队列然后立刻返回。回调里做任何耗时操作都会拖慢后续报文的接收队列满了 Indy 会丢包。第二条是解析线程从队列里取记录调DecodePacket合法的交给设备注册表更新非法的写日志。单独开一条线程是为了把解析开销从接收线程里剥离出去尤其是 CRC 校验这种纯计算放在接收线程里在报文密集时会影响吞吐。第三条是主线程靠TThread.Queue或者一个 100 毫秒的TTimer把「设备表变了」这个信号转成界面刷新。绝不在解析线程里直接操作TListView。// 接收回调只入队不做任何业务处理 procedure TCommEngine.DoUDPRead(Sender: TObject; AData: TIdBytes; ABinding: TIdSocketHandle); var Item: PRecvItem; begin New(Item); Item.Data : Copy(AData, 0, Length(AData)); Item.PeerIP : ABinding.PeerIP; Item.PeerPort : ABinding.PeerPort; if not FRecvQueue.Push(Item) then // 队列有界满了就丢弃并计数 begin Dispose(Item); InterlockedIncrement(FDropCount); end; end;队列用的是有界环形队列容量 1024 条满了直接丢弃并累加计数器。宁可丢包也不能让内存无限增长这是内网长时间运行的底线。丢包计数在调试面板上显示出来现场排查时一眼就能看出是处理不过来还是根本没收到。4. 关键实现广播发送、接收解析与心跳维护的完整代码路径前面定了结构和协议这一章落到具体代码。四条链路发得出去、收得进来、解得正确、判得准确。每条链路我都给出可以直接编译的片段并说明参数为什么这么取、改的时候要注意什么。4.1 广播发送绑定网卡、开启 SO_BROADCAST 与发送节流发送用TIdSocketHandle手工构造而不是图省事用TIdUDPClient原因是需要在bind之后、发送之前设置Broadcast属性手工构造的时序最清楚。// 向指定网卡的定向广播地址发送一帧 procedure TCommEngine.SendFrame(const ANicIP, ABcastAddr: string; const AFrame: TBytes); var Handle: TIdSocketHandle; Buf: TIdBytes; begin Handle : TIdSocketHandle.Create(nil); try Handle.IP : ANicIP; // 绑定源网卡决定报文从哪个网口出去 Handle.Port : 0; // 源端口交给系统分配 Handle.AllocateSocket(Id_SOCK_DGRAM, Id_IPV4); Handle.Broadcast : True; // 关键不开这个发广播会报错 SetLength(Buf, Length(AFrame)); if Length(AFrame) 0 then Move(AFrame[0], Buf[0], Length(AFrame)); Handle.SendTo(ABcastAddr, cBroadcastPort, Buf); finally Handle.Free; end; end;Handle.IP这一行是整个函数里最重要的一行。填0.0.0.0时由系统选路多网卡机器上会走默认路由那张卡填具体网卡 IP 才能锁定出口。AllocateSocket的第一个参数指定数据报类型第二个指定 IPv4两个都不能省。Broadcast : True对应底层的SO_BROADCAST选项不设置时向广播地址发送会直接抛 socket 异常错误码通常是 10013。发送节流方面公告报文按需发送心跳由设备侧主动上报上位机只在启动时发一轮查询。如果业务需要周期性广播比如每秒一次的服务公告用一个TTimer控制节奏不要在循环里连续SendTo内网交换机对广播风暴的抑制阈值比想象中低几十台设备同时每秒广播就可能触发端口限速。示例里的做法是公告只在启动和手动刷新时发一次后续完全靠设备心跳维持广播报文量压到最低。4.2 接收与解析从字节流到设备记录的完整链路接收端的初始化要显式清空Bindings再逐条添加因为设计期如果放过绑定运行期会重复绑定导致端口占用。procedure TCommEngine.Start; begin FUdpServer : TIdUDPServer.Create(nil); try FUdpServer.BufferSize : 8192; // 默认 1024必须调大 FUdpServer.Bindings.Clear; with FUdpServer.Bindings.Add do begin IP : 0.0.0.0; // 监听所有网卡 Port : cBroadcastPort; end; FUdpServer.OnUDPRead : DoUDPRead; FUdpServer.Active : True; except on E: Exception do LogError(UDP 监听启动失败: E.Message); // 多半是端口被占用 end; end;BufferSize必须调大这一条踩过不止一次。默认 1024 时如果载荷里带了较长的配置内容报文会被静默截断表现是解析函数一直返回False日志里看不到任何异常只有抓包才能发现收到的长度比发出去的短。设成 8192 对内存几乎没影响却能把这类问题一次挡掉。解析线程取出记录后调DecodePacket成功则交给设备注册表procedure TParseThread.Execute; var Item: PRecvItem; Packet: TUdpPacket; begin while not Terminated do begin if FQueue.Pop(Item, 200) then // 200ms 超时便于响应 Terminate begin try if DecodePacket(Item.Data, Item.PeerIP, Item.PeerPort, Packet) then FRegistry.UpdateFromPacket(Packet) // 内部加锁线程安全 else LogDebug(丢弃非法报文来源 Item.PeerIP); finally Dispose(Item); end; end; end; end;UpdateFromPacket里按命令字分派公告报文解析出设备 ID 和名称插入或更新记录心跳报文只更新LastSeen和IP查询报文触发一次应答配置下发报文走确认回执。命令字分派用case而不是一堆if新增命令时只加一个分支不容易漏。4.3 心跳、超时与设备离线判定心跳周期定 2 秒由设备侧定时上报。上位机这边每秒扫一次表判断逻辑放在设备注册表里不放在界面层。procedure TDeviceRegistry.SweepTimeouts; var Pair: TPairstring, TDeviceInfo; Elapsed: TDateTime; begin FLock.Enter; try for Pair in FDevices do begin Elapsed : Now - Pair.Value.LastSeen; if Elapsed cOfflineThreshold then // 12 秒6 个周期 begin if Pair.Value.State dsOffline then begin Pair.Value.State : dsOffline; FChanged : True; end; end else if Elapsed cSuspectThreshold then // 6 秒3 个周期 begin if Pair.Value.State dsSuspect then begin Pair.Value.State : dsSuspect; FChanged : True; end; end else if Pair.Value.State dsOnline then begin Pair.Value.State : dsOnline; FChanged : True; end; end; finally FLock.Leave; end; if FChanged then NotifyChanged; // 通过 TThread.Queue 抛到主线程 end;阈值取 3 个和 6 个周期不是拍脑袋。3 个周期约 6 秒足够覆盖内网偶发的两连丢6 个周期 12 秒是「基本可以认为掉线」的经验值。如果设备是通过无线接入的可以把这个值再放宽到 4 和 8 个周期代价是界面刷新滞后几秒换来的是不会因为一次漫游切换就误报掉线。NotifyChanged用TThread.Queue而不是Synchronize前者不阻塞扫描线程后者在界面卡顿时会拖住整个超时判定。4.4 日志与调试开关让现场问题可复现日志分三级错误、信息、调试。错误级只记录启动失败、端口占用、异常信息级记录设备上下线、配置下发结果调试级记录每一条收发的原始报文十六进制和长度。调试级默认关闭通过配置文件或者一个隐藏的界面开关打开。收发两侧都在入口处打日志格式统一成时间 | 方向 | 来源或目标 | 长度 | 前 32 字节十六进制。这个格式的好处是抓包文件和日志能对得上现场只要拿到日志和一份抓包基本能还原整条链路。日志文件按天切分单文件超过 10MB 滚动避免长期运行把磁盘写满。调试开关做成运行时可切换不要做成编译期条件。现场排查时让运维人员在界面菜单里点一下就能开比让客户重新编译一版程序现实得多。5. 避坑与排查多网卡、防火墙、粘包这些最容易翻车的地方这一章列的五个问题是我在几个内网项目里真实遇到过、并且每次都有人重新踩一遍的。写法统一按「现象 → 原因 → 解决」组织现象描述尽量还原现场看到的样子方便对照。5.1 现象本机自己收发正常同网段其他机器一条都收不到原因通常有三层。最外层是 Windows 防火墙程序第一次运行时会弹一次网络访问询问如果当时点了取消或者被策略静默拦截之后就一直收不到入站报文而本机回环不受影响于是表现为「自测通过」。第二层是监听地址如果接收端绑定的是具体网卡 IP 而不是0.0.0.0换一台机器网卡地址不同自然收不到。第三层是发送侧只往255.255.255.255发多网卡时走错网口。解决顺序先在目标机器上netstat -ano | findstr 39001确认端口处于监听状态再临时关闭防火墙复测一次能通就说明是规则问题给程序加了入站规则即可如果仍不通用抓包工具在目标机器上看有没有收到 UDP 报文收到但解析失败说明是协议层问题完全收不到才是网络层问题。发送侧一律改成按网卡 IP 算定向广播地址逐个发送。5.2 现象发送时报 socket 异常错误码 10013原因是没有开启SO_BROADCAST。部分系统上向广播地址SendTo时如果 socket 没有设置广播权限会直接拒绝错误信息通常是「访问权限不允许」。另一个容易混淆的点是有的人开了广播权限但绑定的源 IP 是127.0.0.1也会失败。解决就是在AllocateSocket之后、SendTo之前设置Handle.Broadcast : True顺序不能反。设置完可以再检查一次Handle.Broadcast的读值。如果用了封装过的组件确认组件内部没有在Active : True之后重置 socket 选项。5.3 现象报文长度对不上解析函数稳定返回 False原因基本锁定在接收缓冲区。Indy 的TIdUDPServer.BufferSize默认 1024超过部分被截断日志里看到的长度比发送端记录的短。这个问题的隐蔽之处在于不抛异常、不报错只是解析失败很容易被误判成协议设计有问题。解决是把BufferSize调到 8192 甚至 16384同时在协议层把单帧载荷上限卡在 1400 字节超过就分片成多帧发送每帧带同一个序号和分片索引。这样即使某天有人往载荷里塞了大内容也不会依赖 socket 缓冲区大小。提示调完缓冲区后务必用一条接近上限的长报文做一次回归别只测短报文。5.4 现象程序跑一段时间后界面卡死或者直接崩溃原因是在OnUDPRead回调里直接更新了 VCL 控件或者直接操作了没有加锁的设备列表。Indy 的回调在接收线程里执行VCL 的TListView、TLabel都不是线程安全的跨线程访问轻则界面错乱重则访问违例。设备列表同理一边遍历一边被插入TDictionary会抛异常。解决是把回调压缩成「入队」一个动作业务处理和界面刷新都挪到各自的线程跨线程通知统一走TThread.Queue。设备表所有公开方法加临界区锁内不做 IO。改完之后用压力测试跑一遍模拟 50 台设备每秒各发一帧连续跑两小时看内存和句柄数是否平稳。5.5 现象中文设备名显示成乱码或者干脆解析失败原因是编码不统一。发送端用系统 ANSI 代码页转字节接收端按 UTF-8 解中文必然花屏更糟的情况是长度字段按字符数算而不是字节数导致整个报文错位。另一种情况是有人用WideString直接Move进缓冲区忽略了长度翻倍。解决是协议层强制规定载荷统一 UTF-8发送前用TEncoding.UTF8.GetBytes转接收后用TEncoding.UTF8.GetString还原长度字段一律按字节数填写并在编解码层做一次断言校验。测试用例里专门放一条带中文和特殊符号的设备名每次改动协议都跑一遍。6. 验收与答辩从抓包到压力测试的完整清单验收这件事我一般按「能看见的证据」来组织而不是口头说明功能正常。下面这张表是这套示例交付时用的验收清单答辩时也可以照着讲。验收项测试方法通过标准单机收发本机开两个实例一个发公告一个收接收端 1 秒内出现设备记录跨机发现两台同网段机器A 发 B 收B 的设备列表出现 AIP 正确多网卡三网卡机器向两个网段各发一轮两个网段设备均被发现非法报文用工具向端口发随机字节程序不崩溃日志记录丢弃截断报文手工发一条少了尾部的帧解析返回 False不产生脏数据心跳超时拔掉一台设备的网线6 秒内标为疑似12 秒内标为离线恢复上线重新插回网线一帧心跳后恢复在线压力测试模拟 50 台设备每秒一帧持续 2 小时CPU 平稳无内存增长无崩溃抓包对照抓包工具过滤目标端口收发帧数与日志条数一致抓包这一步不要省。答辩时如果能把一份抓包截图和程序日志并排放在一页上说明「这一帧从 A 发出、B 收到、解析成哪条设备记录」比讲十分钟设计思路都有说服力。过滤条件就是udp.port 39001选中一帧看十六进制对照协议字段表逐个指出来前四字节是魔数第五字节是版本第六字节是命令字。压力测试要提前跑不要等到答辩前一天。我吃过一次亏功能测试全过现场十几台设备一接上来界面每秒刷新几十次直接卡成幻灯片。后来把刷新改成 200 毫秒合并一次问题才解决。所以验收清单里我固定加一条「模拟 N 台设备的持续上报」N 取实际规模的 1.5 倍。答辩时最容易被问的三个问题提前准备好答案为什么不用 TCP——设备发现阶段还不知道对方 IP 和端口TCP 无从连接且轮询开销大广播丢包怎么办——发现阶段丢一帧只是晚一轮发现心跳机制会补上关键配置下发则用应答加三次重发保证跨网段怎么办——广播不跨三层跨网段场景要么部署中继要么改用组播加三层转发配置这套示例的定位是单网段内网不硬撑。从那以后我每次做这类通讯模块都强制走一遍「抓包—日志—压力」三件套哪怕需求再简单也不跳过因为翻车永远发生在没测的那条路径上。这套示例的完整工程和说明放在下载里协议字段和端口按自己的场景改一改就能用希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站