Delphi流量监控实战:从PDH吞吐量到TCP连接统计与进程流量分析

发布时间:2026/10/12 6:01:55
Delphi流量监控实战:从PDH吞吐量到TCP连接统计与进程流量分析 简介这是一套面向 Delphi 开发者的网络流量监控与吞吐量检测实现资料适合需要为网络应用加入实时带宽统计、网卡数据采集与异常排查功能的系统监控类项目。包体共 218 个文件以 215 个 .pas 源文件为主体内含 JEDI Windows API 库的完整 Pascal 单元另附 WinDefines.inc 定义文件、readme.html 说明页与 txt 文档整体约 1.77MB便于按模块直接查阅引用。资源围绕网卡选择、WinPCap/Npcap 数据包捕获、上传下载字节统计、吞吐量图表显示等核心环节给出可运行的 Delphi 代码思路可帮助开发者快速搭建一套具备实时图表、定时刷新与持久化分析能力的流量监控工具。目前已有 442 人学习使用适合具备一定 Delphi 基础、希望节省 API 封装成本并提升网络监控开发效率的开发者。1. 用 Delphi 做流量监控网络吞吐量是第一个要拿下的指标这次要聊的是用 Delphi 做流量监控核心指标是网络吞吐量。正在做内网带宽看板、客户端上传下载测速、或者上网行为辅助工具的同学应该都遇到过同一个问题怎么在程序里实时读出一块网卡当前的收发速率并且发布后不需要额外装驱动。先给结论抓包方案往往是最后才该选的把 Windows 自带的性能计数器和 TCP 统计接口用好能解决八成需求这是我的血泪经验。整条路线上手难度不高尤其适合维护存量 Delphi 工程、又想快速加一个流量面板的朋友。2. 读网卡吞吐量先走 PDHDelphi 最小实现与三个关键参数2.1 为什么先选 PDH零驱动、免管理员的系统级统计入口Windows 自带一套性能数据体系所有能看到的系统计数器CPU、内存、磁盘、网络都从这里面出。Delphi 程序通过 Pdh 开头的几个 API 就能直接读取不需要管理员权限也不需要安装驱动。相比用 GetIfTable 自己轮询网卡累计字节的老做法PDH 的好处是系统已经帮我们算好了速率我们只需要决定多久取一次。需要说明的是还有人会推荐直接用网卡 API 拿累计字节再算差值这条路也能走通但网卡接口表在拨号、虚拟机、蓝牙适配器一堆的情况下维护成本高还是 PDH 的实例枚举更省事。下面的最小实现就是枚举系统里的网络接口实例逐个添加计数器再定时读值。2.2 最小实现枚举网络接口实例并逐块网卡读取 Bytes Total/secPDH 的读取流程分成四步打开查询句柄、添加计数器、采集快照、格式化取值。真正麻烦的是计数器路径里的网卡名不能写死因为中文系统里显示名可能是“以太网”英文系统是“Ethernet”重名还会追加“#2”这类后缀。所以要先枚举再拼路径。uses Winapi.Windows, Winapi.Pdh; type TCardInfo record Name: string; Handle: TPDH_HCOUNTER; MBps: Double; end; function TNetMonitor.EnumAndAddCounters: Boolean; var ObjBuf: array[0..4095] of WideChar; ItemBuf: array[0..8191] of WideChar; BufSize, ItemBufSize, CountBufSize: DWORD; p, q: PWideChar; ObjName, InstanceName: string; Card: TCardInfo; h: TPDH_HCOUNTER; begin Result : False; BufSize : SizeOf(ObjBuf); // 枚举 PDH 性能对象拿到对象名列表双 null 结尾 if PdhEnumObjectsW(nil, nil, ObjBuf, BufSize, PERF_DETAIL_WIZARD, True) ERROR_SUCCESS then Exit; p : ObjBuf; while p^ #0 do begin ObjName : p; // 本地化后对象名可能不同用 Network 关键片段匹配 if Pos(Network, ObjName) 0 then Break; Inc(p, Length(p) 1); end; if p^ #0 then Exit; ItemBufSize : SizeOf(ItemBuf); CountBufSize : 0; // 枚举该对象下的实例每块网卡是一个实例 if PdhEnumObjectItemsW(nil, nil, PWideChar(ObjName), ItemBuf, ItemBufSize, nil, CountBufSize, PERF_DETAIL_WIZARD, 0) ERROR_SUCCESS then Exit; q : ItemBuf; while q^ #0 do begin InstanceName : q; // 给当前实例添加“总字节数/秒”计数器 if PdhAddCounterW(FQuery, PWideChar(\ ObjName ( InstanceName )\Bytes Total/sec), 0, h) ERROR_SUCCESS then begin Card.Name : InstanceName; Card.Handle : h; Card.MBps : 0; FList.Add(Card); end; Inc(q, Length(q) 1); end; Result : FList.Count 0; end;这段代码先枚举性能对象再枚举网卡实例最后为每块网卡添加一个速率计数器。逻辑上分三步第一遍枚举拿的是对象名第二遍拿的是实例名。注意PdhEnumObjectsW第一个参数传nil表示读本机性能数据最后一个参数True表示强制刷新缓存这样能避免程序启动后等很久才看到新增的网卡。添加计数器时路径里的Bytes Total/sec是英文计数器名在非英文系统上可能需要用枚举出来的本地化名称。稳妥做法是像代码里一样先跑通英文名再退回枚举结果去匹配。实际工程里我常把这一步的返回值打日志一旦 AddCounter 失败日志里能看到完整路径方便快速定位。2.3 三个关键参数刷新粒度、格式类型、查询句柄生命周期PDH 用起来不复杂但几个参数直接决定数据质量。第一个是刷新周期PdhCollectQueryData的调用间隔不要小于 1 秒性能底层数据块的刷新频率决定了你读到的永远是“上一次更新后的快照”间隔再快也只是读到相同的缓存值。第二个是格式化类型读速率计数器用PDH_FMT_DOUBLE不要用PDH_FMT_LONG不然低于 1MB/s 的流量会被截断成 0。第三个是查询句柄生命周期一个查询可以挂多个计数器程序退出前必须PdhCloseQuery否则在频繁启停的监控服务里会持续积累未释放的句柄。function TNetMonitor.RefreshValues: Boolean; var Card: TCardInfo; Value: TPDH_FMT_COUNTERVALUE; begin Result : False; // 每次取值前先采集一次快照确保读到的是最新样本 if PdhCollectQueryData(FQuery) ERROR_SUCCESS then Exit; for Card in FList do begin if PdhGetFormattedCounterValue(Card.Handle, PDH_FMT_DOUBLE, nil, Value) ERROR_SUCCESS then begin Card.MBps : Value.doubleValue / 1024 / 1024; // 字节/秒 转 MB/s UpdateUiValue(Card.Name, Card.MBps); end; end; Result : True; end;取值代码里PDH_FMT_DOUBLE是关键它让系统把原始计数器格式化成浮点数直接就是“每秒字节数”。转成 MB/s 只需要除以两次 1024。UI 层不要直接在这个方法里做控件更新建议把结果写进一个内存变量或原子类型由主界面定时器去取否则采集频率一快界面会卡。提示PDH 的轮询周期不要小于 1 秒。想要更高精度的吞吐量曲线需要下一层的抓包方案后面会讲。3. 从总吞吐量到连接明细GetPerTcpConnectionEStats 把单连接速率算出来3.1 PDH 看不到连接级数据TCP 统计 API 是系统自带的正路网卡总吞吐量有了下一个常遇到的问题就是我想知道每一台远程服务器和我之间的 TCP 连接占了多少带宽。PDH 在这个粒度上是无能为力的它只统计到网卡这一层。Win7 之后系统提供了一个用户态接口可以拿到单个 TCP 连接的各项统计包括累计收发字节数、RTT、重传、拥塞窗口。这个接口就是GetPerTcpConnectionEStats纯 Delphi 可以直接调用不需要驱动。它的用法和直觉不太一样不是传一个句柄而是传连接的四元组。所以常规流程是先用GetTcpTable2拿到本机所有 TCP 连接列表再对感兴趣的连接调用扩展统计接口读回数据结构里的DataBytesOut和DataBytesIn两次采样差值除以间隔就是这条连接的实时吞吐量。3.2 实现流程GetTcpTable2 枚举连接按四元组采样两次求差值procedure TConMonitor.SampleTcpFlows(const TargetPid: Cardinal); var Tbl: PMIB_TCPTABLE2; Size: ULONG; Row: PMIB_TCPROW2; Rw: _TCP_ESTATS_DATA_RW_v0; Rod: _TCP_ESTATS_DATA_ROD_v0; h: ULONG; i: Integer; begin Size : 0; GetTcpTable2(nil, Size, True); // 第一遍只拿长度 GetMem(Tbl, Size); try if GetTcpTable2(Tbl, Size, True) NO_ERROR then Exit; for i : 0 to Integer(Tbl.dwNumEntries) - 1 do begin Row : Tbl.table[i]; // 只关心指定进程建立的连接也可以按端口过滤 if Row.dwOwningPid TargetPid then Continue; ZeroMemory(Rw, SizeOf(Rw)); ZeroMemory(Rod, SizeOf(Rod)); // 读 TCP 数据层的统计Data 类型不需要额外权限 h : GetPerTcpConnectionEStats(Row, TcpConnectionEstatsData, Rw, 0, SizeOf(Rw), nil, 0, 0, Rod, 0, SizeOf(Rod)); if h NO_ERROR then begin // 这里把当前累计字节记入列表下次采样做差值 RecordSample(Row.dwLocalAddr, Row.dwLocalPort, Row.dwRemoteAddr, Row.dwRemotePort, Rod.DataBytesOut, Rod.DataBytesIn); end; end; finally FreeMem(Tbl); end; end;代码里先取 TCP 表然后按进程号过滤。实际监控工具一般不会一次扫全表再过滤而是先拿一次全表和上次的四元组列表做差集这样采样间隔内的“新连接”也不会漏掉。Rod结构体字段很多我这里只取累计字节完整定义按 SDK 头文件补齐即可。调用GetPerTcpConnectionEStats前要把Rw结构清零它是一个请求描述结构表示“我想读哪一类统计”。Rod是结果结构体调用成功后会往里面写值。注意端口字段在 TCP 表里是网络字节序打印或者做匹配前要先ntohs转换不然你用本地端口 8080 去匹配表里0x1F90这种值是永远对不上的。3.3 能拿到的延伸指标RTT、重传数、拥塞窗口以及权限边界连接级统计接口不只是吞吐量。把EstatsType换成TcpConnectionEstatsRtt能拿到平滑 RTT换成TcpConnectionEstatsSndCong能看到发送端的拥塞窗口变化。这些指标对于定位“带宽高但网页还是卡”的问题非常有用比单纯盯着吞吐量更能说明问题。权限上有个容易踩的坑数据类统计累计字节普通权限可读但 RTT、重传这类延伸统计在某些系统上要求管理员权限。程序里要做动态降级能读到 RTT 就展示读不到就只展示字节量不要为了一个可选指标把整个监控面板变成必须提权才能用。判断权限边界很简单调试时用管理员运行一次看同一调用是否返回不同的错误码即可。4. 想按进程查流量抓包聚类是常规解法但要先看清方案边界4.1 需求起点总量有了但不知道谁在占带宽监控看板做到一半业务方一定会提新需求帮我看看是不是某个进程在后台跑更新把出口带宽吃满了。PDH 给不了这个答案因为它是按网卡汇总的。要按进程拆流量常见做法是把底层抓包驱动收到的数据包按五元组聚合成“流”再把流映射到进程对应的连接上。这个方案能拿到协议级明细但代价是需要安装驱动。4.2 常见做法抓包回调里按五元组聚类窗口结束关联 PID完整的抓包方案会涉及抓包循环、协议解析、流表管理、PID 关联四部分。先看最核心的聚合逻辑这一步决定后面统计的正确性。// 抓包回调里的流聚类示意代码以五元组为 key 累加字节数 procedure OnPacketCapture(Data: Pointer; TotalLen: Cardinal); var IpHeader: PIpHeader; TcpHead: PTcpHeader; Key: string; Cur: TFlowEntry; begin // 跳过以太网帧头解析出 IP 头与 TCP 头示意代码 if not ExtractTcpKey(PByte(Data), TotalLen, IpHeader, TcpHead) then Exit; // 五元组源IP、源端口、目标IP、目标端口、协议类型 Key : Format(%s:%d%s:%d/%d, [IpHeader.Src, TcpHead.SrcPort, IpHeader.Dst, TcpHead.DstPort, IpHeader.Protocol]); if FFlows.TryGetValue(Key, Cur) then Cur.Bytes : Cur.Bytes TotalLen // 已有流则累加 else begin Cur.Bytes : TotalLen; // 新流则插入 FFlows.Add(Key, Cur); end; end;这段代码有两个工程上容易忽略的点。第一包长要加上以太网帧头否则统计出的流量比实际值少 14 字节/每包低速时看不出来万兆网卡上一分钟就少了几百 MB。第二抓包回调跑在独立线程上FFlows字典必须加锁不能用主线程的普通 TDictionary 直接写。实际操作中我会把锁粒度放粗抓包回调只负责累加一个临时计数器每 5 秒由后台线程合并一次降低锁竞争。拿到流表之后再用GetTcpTable2采集一次本机连接表里面带dwOwningPid按四元组匹配流表就能把每条流映射到进程最后按进程聚合得到流量排行。4.3 三个方案的边界对比PDH、TCP 统计 API、抓包/WFP把三条路线放在一起对比能帮你在接需求的时候快速判断该选哪条不用一上来就放大招。方案粒度驱动/权限适用场景PDH 性能计数器网卡级总吞吐量无驱动普通权限默认首选做总览面板TCP 统计 APITCP 单连接吞吐量、RTT无驱动部分统计需管理员连接排行、质量分析抓包统计协议级、按进程聚合需要安装抓包驱动明细分析、按进程定位还有一个思路是走系统防火墙框架去拿进程级统计技术上确实能精确到进程和端口但它本质是驱动侧的过滤框架纯 Delphi 很难独立完成通常要拿底层语言写驱动再给 Delphi 套一层壳维护成本明显高出一截。除非你的产品本身就是专业网管工具否则我建议先把前两条路吃透覆盖业务方 90% 的问题。5. 避坑Delphi 流量监控最容易翻车的五个细节5.1 计数器路径写死后取值为 0别手拼字符串先枚举实例现象按网上老代码写\Network Interface(以太网)\Bytes Total/sec返回值永远是 0或者 AddCounter 直接报错。原因网卡实例名在不同语言系统、不同网络环境下是不一样的。家里电脑叫“以太网”公司域环境可能叫“Ethernet0”重名网卡还会出现“以太网#2”。带括号和井号的实例名在拼路径时还必须做转义手拼字符串很容易翻车。解决永远先用PdhEnumObjectItemsW枚举真实的实例名再拼计数器路径。任何写死的网卡名在用户机器上都可能是错的名字。5.2 速率曲线三秒跳一次不是代码慢是计数器底表刷新周期现象自己写的采集线程每 500ms 读一次但曲线是阶梯状半天才跳一下。原因PDH 的性能计数器底层不是每次调用都重新计算系统按自己的周期刷新性能数据块。读得再勤拿到的还是上一次刷新后的旧值纯属白忙。解决把轮询周期放宽到 1 秒以上程序里不要靠提高采集频率来换取“更平滑”的曲线。想要平滑视觉效果应该在前端做滑动平均而不是后端缩短采样间隔。需要毫秒级精度时直接切换到抓包统计。5.3 32 位编译读到的网卡列表和本机不符优先编译成 64 位现象同一台机器任务管理器能看到 6 块网卡自己的 32 位监控程序只枚举出 3 块或者个别网卡速率为 0。原因WOW64 环境下32 位进程访问性能计数器时会经过一层格式转换某些 64 位才支持的大整数计数器在转换中被截断表现就是丢网卡、读数为 0。解决Delphi 工程直接出 64 位版本这也是现代监控工具的最低要求。遗留 32 位程序短期内改不了的话建议退回GetIfTable读累计字节再做差值求速率绕开性能库的位数差异。5.4 GetPerTcpConnectionEStats 拿不到 RTT部分统计是权限敏感数据现象调用返回“拒绝访问”但同一账号跑其他接口都正常换管理员身份运行就好了。原因RTT、重传、拥塞窗口这类统计在不同系统版本上被标记为敏感数据普通权限进程默认拿不到。解决程序 manifest 里声明管理员权限或者做动态降级读不到 RTT 就隐藏该项不要弹出错误框打断用户。数据类统计一般不受影响所以吞吐量排行在普通权限下仍然可用。5.5 虚拟网卡和隧道适配器混进来总量对不上现象机器开了虚拟机PDH 枚举出一堆“vEthernet”“Tunnel”适配器总吞吐量比物理网卡实际值大一截。原因PDH 枚举的是系统里所有网络接口实例虚拟交换机和隧道适配器同样会被统计它们的流量是同一份物理流量在不同层的复制累加时就会重复计算。解决拿到实例名后再用GetAdaptersInfo对应适配器的类型和状态只保留以太网和无线网卡。枚举时也不要放过IfOperStatusUp这个状态判断停用的网卡读出来全是 0放在列表里不仅乱还会误导排序。6. 收尾技巧用交叉验证给吞吐量数据“上保险”给监控程序做验收前我习惯先让数据自己证明自己是可信的不然上线后被拉去对照任务管理器数字对不上就会被打回票。三个办法最省力第一任务管理器“性能”页的以太网曲线或者系统自带“性能监视器”里加同一个计数器和程序并排跑一分钟误差在个位数百分比算正常偶发的毛刺不要慌。第二本机造一个固定速率的流量源用下载工具拉一个大文件记录程序读到的峰值和平均速率和理论值对比。这个方法最有说服力因为它是可控的、可重复的。第三长跑验证连续运行一天把每次采样到的累计值打日志检查增量是不是始终大于等于 0、单日累计和网卡属性里看到的字节数增量是否吻合。我还会做两件容易被忽略的事析构函数里补上一句PdhCloseQuery避免“查询句柄泄漏导致的系统性能库占用”问题采集线程和 UI 线程分离后台线程负责刷新和计算主界面定时器只读快照这样即使弹窗拖动界面采集也不会卡顿。早年间我做过一个带宽看板发布后用户反馈曲线和任务管理器差 20%查到最后是性能监视器里把虚拟网卡也算进去了这个教训我记了很久。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询