
简介这份工程示例面向监控系统集成开发者与安防设备对接初学者围绕海康网络摄像机IPC的自动探测展开借助UDP组播与ONVIF设备搜索机制解决局域网内IPC快速发现、响应解析及后续管理对接问题。压缩包共39个文件、约95KB以16个.h头文件和14个.cpp源文件为主体配合.sln解决方案与.vcxproj工程配置另有资源文件、说明文档和配置文件构成可直接编译学习的完整示例工程。已有2319人学习下载。读者可从中了解组播套接字创建、加入组播组、发送SOAP发现请求、解析设备IP与型号固件信息以及超时重试等丢包处理思路代码按网络通信、线程、XML解析和界面模块分层组织便于对照理解ONVIF搜索流程并迁移到自有设备发现与集成项目中。1. 为什么一个 UDP 组播包就能把网段里的摄像机“喊”出来机柜上电四十台海康 IPC 挂在同一台 PoE 交换机下网段是 192.168.1.0/24可没人知道哪台是 .64、哪台是 .65。拆下来看机身标签太慢翻路由器 ARP 表又经常缺项。打开厂商的搜索工具点一下刷新几秒钟整张表就出来了IP、MAC、序列号、固件版本、激活状态一应俱全。这事儿没有黑匣子一个 UDP 组播包打到 239.255.255.250 的 37020 端口在线设备收到后各自单播回一份 XML 身份卡。这套机制就是利用 UDP 组播实现海康网络摄像机IPC的自动探测功能的核心也是批量激活、批量改 IP、上线巡检工具的底座。做安防集成、NVR 对接、产线老化测试和机房巡检的人迟早要自己写一遍。2. 海康 SADP 的报文结构与组播收发时序先搞懂一问一答动手之前得先把这套协议的一问一答想明白否则后面收到的包长什么样全靠猜排错就变成了玄学。SADPSearch Active Device Protocol的设计非常朴素客户端发一条 XML 查询设备收到后返回一条 XML 应答。查询走组播因为客户端不知道设备在哪应答走单播因为设备知道查询是从哪个 IP、哪个端口发出来的。整个交互只依赖 UDP没有握手、没有重传、没有会话状态设备掉线了也不会有任何通知全靠客户端自己等超时。这种设计的取舍很清楚。优点是不需要预先知道设备地址插上网线就能被发现产线上几百台设备同时上电也不会因为并发连接把服务打挂。缺点是丢一个包就少一台设备所以实际工具里都必须做重发和超时兜底。理解这一点后面调参数时就不会纠结“为什么我要发三次”。2.1 Probe 报文里真正必要的只有两个字段一条探测请求的 XML 大致长这样不同固件版本可能增加可选字段但下面这两项是不能少的?xml version1.0 encodingutf-8? Probe Uuid6f1c2b8e-3a4d-4c9b-9f11-0a2b3c4d5e6f/Uuid Typesinquiry/Types /ProbeUuid是客户端的临时标识每次探测重新生成一个就行设备会原样带回用来区分“这条应答是回给哪次请求的”。多线程并发探测时这个字段很有用单线程一次一发的话它其实没什么存在感。Types固定填inquiry表示这是一次查询而不是配置下发。有实现会在里面带上 MAC 或序列号做定向查询那是为了让设备少回一点普通发现场景用不上。设备返回的ProbeMatch字段就丰富多了常见的包括DeviceSN、DeviceType、DeviceDescription、IPv4Address、IPv4SubnetMask、IPv4Gateway、MAC、HttpPort、CommandPort、SoftwareVersion、Activated、DhcpEnabled等。注意这里是“常见”不是“一定”——同一厂商不同产品线、不同固件字段会增减有的精简固件甚至只回 IP 和 MAC。所以解析代码必须按“字段可能不存在”来写不能对着某一份报文写死。提示字段名大小写敏感IPv4Address和ipv4address在 XML 里是两个东西。动手前先抓一次真实回包把设备实际返回的标签抄下来比自己猜快得多。2.2 组播发出去、单播回来的时序一次完整的探测时序是这样的客户端创建 UDP socket 并绑定本地端口把 Probe 报文sendto到组播地址 239.255.255.250 的 37020 端口交换机和网卡把这份数据复制给该组播组内的所有端口在线的 IPC 收到后解析Types确认是查询请求于是把ProbeMatch单播回客户端的源 IP 和源端口客户端在超时窗口内循环recvfrom把每一条应答收下来。这里有几个容易忽略的细节。第一客户端绑定本地端口时绑 37020 最省事因为设备回包的目标端口就是请求的源端口如果绑成随机端口理论上也能收到但部分老固件的实现里会硬编码回 37020绑随机端口反而收不到。第二组播 TTL 决定了探测范围设成 1 只在本网段内传播设成 2 以上才会跨三层但前提是网关设备上开了组播路由否则包出去就没了。第三如果客户端有多张网卡不指定出口网卡的话组播会走默认路由那一张经常出现“本机能搜到、服务器搜不到”的翻车现场。把这三个点记住第二章剩下的工作就是验证。2.3 用抓包确认一次完整往返别猜字段名写解析代码之前先抓一次包。这一步花五分钟能省掉后面两小时的“为什么解析出来是空的”。Linux 下用 tcpdump# 同时抓组播探测包和单播回包-A 直接打印 XML 便于肉眼核对字段 sudo tcpdump -i eth0 -n -s 0 -A udp port 37020 # 只看发出去的组播探测目的地址是组播地址 sudo tcpdump -i eth0 -n dst host 239.255.255.250 and udp port 37020 # 只看某台设备单播回来的应答 sudo tcpdump -i eth0 -n -A src host 192.168.1.64 and udp port 37020Windows 上装个图形化抓包工具过滤条件写udp.port 37020即可效果一样。抓到之后重点看三件事源端口是不是 37020回包里的字段名和你代码里写的是否一字不差回包时间相对探测发出时间延迟多少。一般局域网内延迟在几十毫秒如果观察到接近一秒说明设备 CPU 忙或者网络里有人占带宽超时时间就得往上调。另外留意有没有收到自己发的包——组播回环开着的时候recvfrom第一条拿到的往往是自己的 Probe如果代码没做过滤就会把它当设备解析然后得到一个空字典这种坑在日志里表现得非常隐蔽。注意抓包时如果一条 37020 的包都看不到先别怀疑代码检查本机防火墙有没有拦出站 UDP以及网卡是否真的在这个网段上。3. 用 Python 写出可用的 SADP 探测器从组播发探测到解析回包协议清楚了落到代码上其实很短。这一章按“能跑通 → 能解析 → 能上生产 → 能交付”四步走每步给一段可以直接抄的代码并说明每个参数为什么这么设。3.1 最小可运行版本建组播 socket、发 Probe、收第一个回包下面这段不到四十行是整套工具的内核# sadp_probe.py —— 最小可运行的 SADP 组播探测器 import socket import time import uuid MCAST_GRP 239.255.255.250 # SADP 约定的组播地址 MCAST_PORT 37020 # SADP 约定的 UDP 端口 def build_probe() - bytes: 构造一条 Probe 查询报文Uuid 每次探测重新生成 return ( ?xml version1.0 encodingutf-8?\n Probe\n f Uuid{uuid.uuid4()}/Uuid\n Typesinquiry/Types\n /Probe\n ).encode(utf-8) def discover(timeout: float 3.0): sock socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) # 端口复用避免本机已装厂商搜索工具时 bind 失败 sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 关闭组播回环防止收到自己发出去的 Probe sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, 0) # TTL1 表示不出本网段跨网段另说 sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 1) # 绑定 37020设备回包的目的端口就是这里的源端口 sock.bind((0.0.0.0, MCAST_PORT)) sock.settimeout(timeout) sock.sendto(build_probe(), (MCAST_GRP, MCAST_PORT)) replies [] deadline time.time() timeout while time.time() deadline: try: data, addr sock.recvfrom(65535) except socket.timeout: break replies.append((addr[0], data)) sock.close() return replies if __name__ __main__: for ip, payload in discover(): print(ip, payload[:160])逻辑上分四段。第一段建 socketSOCK_DGRAM加IPPROTO_UDP是显式写法比默认参数更不容易出错。第二段设三个选项SO_REUSEADDR解决端口占用IP_MULTICAST_LOOP0关掉回环IP_MULTICAST_TTL1把探测限制在本网段。第三段bind到 37020 再sendto组播地址注意只发一次重发逻辑放到 3.3 讲。第四段用绝对截止时间循环收包而不是靠settimeout(0)轮询这样即使中途收到很多包也不会把总等待时间拖长。参数上只有两个值得调timeout默认给 3 秒五十台以内的现场够用设备数量上到几百台时回包会明显错峰建议加到 5 到 8 秒否则最后一批设备永远收不到。IP_MULTICAST_TTL保持 1除非你确认网关开了组播转发贸然调大只会让包出去得更多、回来越少。3.2 把 ProbeMatch 解析成结构化设备字典收到的 payload 是 XML 字节串直接ET.fromstring解析有一定概率翻车原因通常是 BOM、前导空字节或者尾部垃圾字符。稳妥的写法是先做一次清洗再解析import xml.etree.ElementTree as ET # 只取我们真正要用的字段字段缺失时留空字符串 WANT_FIELDS [ DeviceSN, DeviceType, DeviceDescription, IPv4Address, IPv4SubnetMask, IPv4Gateway, MAC, HttpPort, CommandPort, SoftwareVersion, Activated, DhcpEnabled, ] def parse_probe_match(payload: bytes) - dict: # utf-8-sig 会自动吃掉 BOMerrorsignore 兜住个别固件的脏字节 text payload.decode(utf-8-sig, errorsignore).strip() # 截到第一个 处理前面夹杂空字节或调试信息的情况 start text.find() if start 0: text text[start:] if not text: return {} try: root ET.fromstring(text) except ET.ParseError: return {} # 只认 ProbeMatch顺手过滤掉自己发的 Probe 回环 if root.tag ! ProbeMatch: return {} dev {} for key in WANT_FIELDS: node root.find(key) dev[key] (node.text or ).strip() if node is not None else return dev这段代码的核心思想是“宽进严出”。入口处尽量宽容编码不对、前缀脏、字段缺失都不抛异常一律降级成空字典或空字符串出口处保持结构固定每个设备字典都有相同的键后面写 CSV 或者入库时不用再判断键是否存在。root.tag ! ProbeMatch这一行是必须的它同时解决了两个问题过滤掉非 SADP 的杂包以及过滤掉自己发出去的 Probe。Activated这个字段值得单独说一句。它的值是true或false代表设备是否已经激活设置过密码。批量部署时最怕的就是把未激活设备当成可用设备登记进去结果调试时连不上。有经验的写法是在输出表里把这一列标红或者单独筛出来先把未激活的那批处理掉。3.3 多网卡绑定、超时重发与跨网段的三组参数单网卡笔记本上跑得很顺的代码搬到机房服务器上经常一台设备都搜不到九成是网卡选错了。服务器一般有管理口、业务口、带外口至少三张网卡组播默认从系统路由表里优先级最高的那张出去而摄像机在另一张卡上。解决办法是显式指定出口def bind_interface(sock: socket.socket, local_ip: str) - None: # 指定组播出口网卡不指定的情况下走系统默认路由 sock.setsockopt( socket.IPPROTO_IP, socket.IP_MULTICAST_IF, socket.inet_aton(local_ip), )local_ip填的是本机在该网段的地址比如 192.168.1.10不是摄像机的地址。多个网段都要扫的话就按网卡起多个 socket各自绑定各自的地址收完再合并。这种写法比一张卡一张卡切网线靠谱得多。重发逻辑建议单独包一层不要塞进discover里def probe_with_retry(sock, retries: int 3, gap: float 0.3) - None: # 同一份 Uuid 连发多次设备对重复探测是幂等的 for _ in range(retries): sock.sendto(build_probe(), (MCAST_GRP, MCAST_PORT)) time.sleep(gap)设备端对重复的 Probe 是幂等的收到几次就回几次客户端在 3.4 里去重即可。重发三次、间隔 300 毫秒这个组合能吃掉绝大多数偶发丢包又不会把网络打得太热闹。跨网段是另一个话题。组播默认不跨三层原因不是协议限制而是路由器默认不转发 239.0.0.0/8。可行的路只有三条在网关上开组播路由PIM 之类在每个网段各部署一个探测进程或者干脆放弃组播改用逐 IP 单播探测。前两条是正路第三条是兜底扫描一个 /24 网段大概几十秒精度反而不差因为单播探测能覆盖到组播被禁的设备。把上面这些参数收拢成一张表参数建议值说明IP_MULTICAST_TTL1本网段探测跨网段需网关支持组播转发本地绑定端口37020部分老固件硬编码回此端口绑随机端口可能收不到探测超时3 s≤50 台/ 58 s数百台设备多时回包错峰超时太短会漏尾重发次数23 次间隔 300 ms吃掉偶发丢包IP_MULTICAST_LOOP0关闭回环避免把自己发的 Probe 当应答出口网卡显式指定多网卡服务器必设否则走默认路由3.4 批量场景下的去重与输出重发机制带来一个问题同一台设备会被收到两三次不去重的话导出的表格里全是重复行。去重键选DeviceSN最可靠它是设备唯一标识DeviceSN为空时退化为MAC两者都空再用IPv4Address兜底。这个优先级顺序不能反因为 DHCP 环境下 IP 会变MAC 在某些虚拟化场景下会重复只有序列号是真正唯一的。import csv def dedup_and_export(replies, out_path: str ipc_scan.csv) - list: devices {} for ip, payload in replies: dev parse_probe_match(payload) if not dev: continue key dev.get(DeviceSN) or dev.get(MAC) or dev.get(IPv4Address) or ip # 后到的覆盖先到的保留最新一次应答 devices[key] dev rows list(devices.values()) rows.sort(keylambda d: tuple(int(x) for x in d[IPv4Address].split(.)) if d[IPv4Address] else (0, 0, 0, 0)) with open(out_path, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnamesWANT_FIELDS) writer.writeheader() writer.writerows(rows) return rows导出时用utf-8-sig编码是为了让表格软件打开 CSV 时不出现中文乱码。按 IP 排序而不是按发现顺序排是为了让多次扫描的结果能直接肉眼比对——顺序一致新出现的设备一眼就能看出来。这一段看着琐碎但真正做巡检工具时输出格式的可比性比功能本身更重要否则每次扫描结果都要人工对半天。4. ONVIF WS-Discovery 作为第二条腿什么时候该换协议SADP 只覆盖自家设备混合品牌的项目里就不够用了。ONVIF 的 WS-Discovery 是同类思路的另一套实现同样靠组播发现但报文格式、端口、返回内容都不一样。两条腿走路覆盖率才够。4.1 WS-Discovery 的 Probe 报文与 SADP 的差异WS-Discovery 走的是标准 Web 服务那套组播地址同样是 239.255.255.250端口换成 3702载荷是 SOAP 信封。它和 SADP 的差别可以列成一张表维度海康 SADPONVIF WS-Discovery组播地址239.255.255.250239.255.255.250端口37020/UDP3702/UDP载荷格式自定义 XMLSOAP Envelope类型过滤TypesinquiryTypesdn:NetworkVideoTransmitter回包关键字段IP、MAC、序列号、固件、激活状态XAddrs 服务地址、Scopes 设备信息设备覆盖主要自家设备支持 ONVIF 的多家设备能否改配置可走命令端口下发只能发现配置要另调接口发现后能做什么直接改 IP、激活、重启拿到服务地址后走 ONVIF 接口从表里能看出一条清晰的分工要批量改 IP、批量激活、看固件版本必须走 SADP要跨品牌盘点“这个网段里到底有几台摄像机”WS-Discovery 更合适。实际项目里两个都跑结果合并比死磕一个协议省事。4.2 用 Python 发 WS-Discovery Probe 并解析 XAddrsSOAP 报文比 SADP 长不少但结构固定拼一次就够了import socket import uuid import xml.etree.ElementTree as ET WS_GRP 239.255.255.250 WS_PORT 3702 NS { e: http://www.w3.org/2003/05/soap-envelope, w: http://schemas.xmlsoap.org/ws/2004/08/addressing, d: http://schemas.xmlsoap.org/ws/2005/04/discovery, dn: http://www.onvif.org/ver10/network/wsdl, } def build_ws_probe() - bytes: msg_id fuuid:{uuid.uuid4()} return f?xml version1.0 encodingUTF-8? e:Envelope xmlns:e{NS[e]} xmlns:w{NS[w]} xmlns:d{NS[d]} xmlns:dn{NS[dn]} e:Header w:MessageID{msg_id}/w:MessageID w:Tourn:schemas-xmlsoap-org:ws:2005:04:discovery/w:To w:Actionhttp://schemas.xmlsoap.org/ws/2005/04/discovery/Probe/w:Action /e:Header e:Body d:Probe d:Typesdn:NetworkVideoTransmitter/d:Types /d:Probe /e:Body /e:Envelope.encode(utf-8) def ws_discover(timeout: float 4.0): sock socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 1) sock.bind((0.0.0.0, WS_PORT)) sock.settimeout(timeout) sock.sendto(build_ws_probe(), (WS_GRP, WS_PORT)) found [] while True: try: data, addr sock.recvfrom(65535) except socket.timeout: break try: root ET.fromstring(data.decode(utf-8-sig, errorsignore)) except ET.ParseError: continue for m in root.iter({%s}ProbeMatch % NS[d]): xaddrs m.findtext({%s}XAddrs % NS[d], ).strip() scopes m.findtext({%s}Scopes % NS[d], ).strip() found.append({ip: addr[0], xaddrs: xaddrs, scopes: scopes}) sock.close() return foundTypes填dn:NetworkVideoTransmitter是关键它把查询限定在“网络视频发送端”也就是摄像机。填错或者留空会收到一堆打印机、路由器、智能电视的应答把结果表撑得没法看。MessageID每次重新生成用于区分不同请求抓包时对日志很有用。解析时用root.iter加命名空间匹配而不是写死前缀。因为不同厂商返回的 XML 里前缀可能叫d、wsd、a只有命名空间 URI 是固定的。XAddrs里是设备服务地址通常形如http://192.168.1.64/onvif/device_service从这个地址里能反解出 IP但更省事的做法是直接用recvfrom拿到的addr[0]。Scopes里是一串 URI包含设备名称、硬件型号、位置等需要按空格切开再逐段解析这里为了简洁先原样保留。4.3 两套协议的结果怎么合并去重合并的难点不在代码在判断哪条记录更可信。原则很简单SADP 的记录优先因为它字段更全、还能标识激活状态WS-Discovery 的记录用于补充 SADP 没覆盖到的品牌。按 IP 做合并键SADP 有的字段用 SADP 的SADP 缺的用 WS 的补。def merge_results(sadp_devices, ws_devices) - list: # 以 IP 为键SADP 优先WS 记录仅补空缺 merged {} for d in sadp_devices: ip d.get(IPv4Address) if ip: merged[ip] dict(d, SourceSADP) for w in ws_devices: ip w.get(ip) if not ip: continue if ip in merged: merged[ip][Source] SADPWS else: merged[ip] { IPv4Address: ip, DeviceSN: , DeviceType: , MAC: , CommandPort: , HttpPort: , SoftwareVersion: , Activated: , DhcpEnabled: , DeviceDescription: w.get(scopes, ), Source: WS, } return list(merged.values())多出来的Source列看着不起眼实际排障时非常好用。看到某个 IP 只有 WS 没有 SADP基本能判断这不是自家设备或者是固件里 SADP 服务被关掉了看到SADPWS说明两条路都通设备状态最健康。这种信息比多扫出几台设备更有价值。5. 现场排查组播探测收不到回包的 5 类问题协议和代码都通了剩下的全是现场问题。下面这五条是我在不同项目里反复遇到的按“现象 → 原因 → 解决”记下来遇到时对号入座就行。5.1 本机跑得通服务器上一台都搜不到现象同一份代码笔记本插网线能扫出全部设备部署到机房服务器后结果为空。原因服务器多网卡组播默认从系统路由表里优先级最高的网卡出去而那多半是业务口或带外口摄像机在管理口那一边。探测包发出去了但走错了网卡自然收不到回包。解决用IP_MULTICAST_IF显式指定出口网卡的本地地址多网段就起多个 socket 分别绑定。指定之后仍然为空的话用ip route get 239.255.255.250之类的命令确认系统实际选了哪张卡两边对不上就说明绑定没生效。5.2 收到的第一条是自己的 Probe设备一台没回现象日志里打印出一条“设备”但所有字段都是空的root.tag是Probe而不是ProbeMatch。原因组播回环开着本机会收到自己发出去的包。如果解析代码没做root.tag判断就会把它当设备处理而且它会排在所有真实应答前面日志里看起来就像“只收到一台空设备”。解决setsockopt里把IP_MULTICAST_LOOP设为 0同时在解析函数里加root.tag ! ProbeMatch的判断。两道保险都上因为有些虚拟化环境里回环设置不一定生效。5.3 部分设备收不到但网页能正常打开现象同一网段里二十台设备只扫出十五台剩下五台浏览器登录一切正常。原因这五台的 SADP 服务被关掉了或者被上层平台占用。有些项目为了“安全加固”会禁用设备发现协议另一些场景里平台软件常驻占用 37020 端口第三方探测请求会被忽略。还有一种可能是设备固件精简版根本不带这个服务。解决先用 WS-Discovery 兜底ONVIF 服务通常不会被一起关掉能扫到就说明设备在线只是 SADP 不通。两套都扫不到的话退回逐 IP 单播探测端口 80 和 8000能连上就登记进表标注“发现方式端口探测”。覆盖率比协议完整性更重要。5.4 跨网段探测包发出去了但没回应现象探测 192.168.1.0/24 正常探测 10.10.20.0/24 一条回包都没有抓包显示探测包已经离开本机网卡。原因组播不跨三层是默认行为路由器不会转发 239.0.0.0/8 的数据探测包到了网关就被丢掉。TTL 调到 4 也没用问题不在 TTL在转发策略。解决三条路选一条。网关设备上开组播路由这是最干净的在目标网段各部署一个探测进程用消息队列把结果汇总这是最省事的实在动不了网络设备就退化成逐 IP 单播探测扫一个 /24 网段几十秒精度够用。5.5 解析出来字段全是空字符串现象包收到了parse_probe_match也返回了字典但所有字段都是空串。原因字段名对不上。不同固件里可能有IPv4Address和DeviceAddress混用的情况也有把MAC写成MacAddress的另一种情况是编码问题回包前面带了一段调试信息或者空字节ET.fromstring解析失败被吞掉了。解决抓一次包把真实回包的 XML 原样打印出来对着标签一个一个核对。解析函数里不要用try/except一把兜住然后返回空字典——那样会把“解析失败”和“设备没回”混成同一种现象排查时根本分不清。至少打一行日志区分这两种情况。6. 把探测器做成可交付的小工具几个提效技巧与验证方法代码跑通只是起点真正交付出去的东西还得解决“怎么证明它扫得全”这个问题。最实用的技巧是加一层基线比对。第一次扫描的结果存成基线文件之后每次扫描都和基线 diff新出现的 IP 标成新增消失的标成离线。这一层逻辑不到二十行但把“扫描工具”变成了“巡检工具”价值完全不同。产线上用它抓私接设备机房里用它抓意外下线的摄像机都比人工核对表格靠谱。验证覆盖率的方法有两个。一是交叉验证把 SADP、WS-Discovery、ARP 表三份结果放一起比对三者应该高度重合缺的那部分就是需要单独排查的设备。二是注入验证故意在一台已知设备上拔网线再插回去看扫描结果里它是否如预期地消失和重现这个动作能验证超时参数设得合不合适。参数上有一个容易被忽略的点探测频率。有人图省事写个while True每秒扫一次跑一晚上把设备的 ARP 缓存和 CPU 都拖垮了第二天现场反馈“摄像机变卡”。巡检类工具的合理间隔是分钟级五分钟一次完全够用。这个教训我是真踩过——某次在实验室连续高频探测了两小时最后是设备端日志里一堆异常请求把自己暴露了现场排查了半天才定位到是自己的脚本。定时任务里加个 sleep比什么优化都管用。提示交付前把探测日志和 CSV 一起归档出问题时能直接回溯“当时扫到了什么”比事后复现有用得多。这套东西从头写一遍大概两三天但之后每次上新项目都能直接复用改改网卡地址和输出路径就行。希望帮到你。本文还有配套的精品资源点击获取