写这篇东西的起因很简单:我手头有个自己写的代理客户端,每次一跑起来,流量哗哗往上走,但任务管理器里只能看到一个总带宽曲线,根本分不清是哪个模块在偷跑。后来项目里又要评估一个第三方 exe 的网络行为,我不得不想办法把这个 exe 产生的流量单独算出来。折腾了一圈下来,最后落地的方案就是 C++ 调 pcap 抓包,再配合进程的网络端点表把包的归属权追到具体 PID 上。
这条路线的核心思路不复杂:pcap 负责把网卡上的包完整拿到手,然后我们自己维护一份“这个 exe 建立了哪些连接、绑定了哪些端口”的映射,两者一对照,每个包该算到谁的头上就清清楚楚了。这篇博文就是把我从选型、写代码到填坑的完整过程记录下来,适合正在做进程级流量统计、客户端网络诊断或者 exe 行为审计的朋友参考。
1. 方案选型:为什么绕了一圈还是回到 pcap
1.1 任务管理器类方案的天然短板
最先想到的肯定是系统自带的那套。任务管理器“应用历史记录”里确实有按进程统计的网络使用量,但它只给你一个汇总数字,没有任何包级别的信息。你想分析这个 exe 在什么时候连了哪些 IP、走的是什么协议、上传和下载各占多少,它统统做不到。Windows 资源监视器倒是能按进程列出 TCP 连接,但它的更新频率很低,很多短连接压根就来不及显示就消失了。资源监视器也基本只有“看着多、抓不住细节”的能力。
性能计数器 Performance Counter 也能读到“某进程发送/接收的字节数”,问题在于这些计数器的数据源和统计口径不完全透明,对于异步 I/O 完成的网络请求,计数经常滞后。我试过一次,拿它算一个做视频上传的客户端,结果跟实际抓包统计出来的数值差了将近 20%。所以系统自带的方案只能算“能用”,谈不上“够用”。
1.2 三条正经技术路线的对比
要做进程级的精确流量统计,业内真正值得考虑的无非是三条路:
- 性能计数器(PerfMon + Network Interface / Process 对象),优点是接入简单,缺点是无法关联到远程地址,也无法感知非 TCP 流量的细节;
- WFP(Windows Filtering Platform),在系统的网络栈里挂一层过滤驱动,优点是能拿到真正的进程 ID,而且性能和精确度都很好,缺点是开发门槛高,一个 WFP 驱动项目光是环境配置就够折腾半天,而且签名和部署都麻烦;
- pcap 抓包 + 连接表关联,应用层开抓,代码量小、可控性强,任何类型的包(TCP、UDP、ICMP)都能拿到,缺点是需要自己把“包”映射到“进程”。
最后我选了第三条。核心原因很实际:我不想为了查一个 exe 的流量去写驱动,也不想被系统计数器把判断逻辑绑死。pcap 抓包拿到的原始数据包是全网卡级别的,我可以自己定义“什么算下载、什么算上传”,也可以随时加过滤条件,实时看某个 IP 的往来情况。至于进程关联的问题,Windows 自带 iphlpapi,一行 API 就能把 TCP 连接表连同进程 ID 一起读出来,完全可以弥补 pcap 的短板。
这条路对绝大多数做工具类软件、网络审计脚本、以及想搞清楚自家客户端网络行为的人来说,是性价比最高的。
2. 环境准备:Npcap 和 C++ 项目的最小搭法
2.1 为什么必须装 Npcap 而不是 WinPcap
WinPcap 已经太久没更新了,在 Windows 10 和 Windows 11 上安装时经常会被提示驱动不兼容,而且它不支持 Npcap 新增的许多现代网卡特性和回环接口捕获。所以在 2025 年这个时间点,不要有任何犹豫,直接选 Npcap。Npcap 安装时有两个选项值得注意。
第一个是“支持 802.1Q VLAN 标签”,如果你的网络环境里有 VLAN,建议勾上,否则抓到的包可能在链路层解析时出现偏移,解析 IP 头就会出错。第二个是“限制 Npcap 在管理员模式下使用”,为了开发调试方便,这个选项可以不勾。不过即便如此,实际抓包时程序还是必须用管理员权限运行,这一点后面排查问题的时候再细说。
开发包方面,Npcap 安装目录下自带 SDK,路径一般是 C:\Program Files\Npcap\SDK。里面包括了头文件和静态库,主要有 wpcap.lib 和 packet.lib。我习惯把 SDK 里的 Include 和 Lib 目录直接配置进 Visual Studio,比手动拷贝 dll 省事。另外,pcap 的很多 API 依赖 Winsock 的类型定义,所以工程里需要提前包含 Winsock2.h,并且链接 ws2_32.lib。
2.2 第一个能跑的抓包骨架
写一个最小抓包逻辑只需要三步:罗列设备、打开设备、循环取包。
第一步用 pcap_findalldevs 枚举所有网卡接口。返回的是一个 pcap_if_t 链表,每个节点就是一个网卡。我要做的只是找到第一个有 IPv4 地址、且不是回环接口的设备,然后把它打印出来。代码长这样:
#include <pcap.h> #include <winsock2.h> #include <iphlpapi.h> #pragma comment(lib, "wpcap.lib") #pragma comment(lib, "ws2_32.lib") #pragma comment(lib, "iphlpapi.lib") int main() { pcap_if_t* alldevs = nullptr; char errbuf[PCAP_ERRBUF_SIZE]; if (pcap_findalldevs_ex(PCAP_SRC_IF_STRING, nullptr, &alldevs, errbuf) == -1) { printf("枚举设备失败: %s\n", errbuf); return 1; } for (pcap_if_t* dev = alldevs; dev; dev = dev->next) { if (dev->addresses && dev->addresses->addr) { printf("设备名: %s\n", dev->name); printf("描述: %s\n", dev->description ? dev->description : "(无)"); } } pcap_freealldevs(alldevs); return 0; }第二步,选择设备后进行 pcap_open_live。这里有个参数需要解释一下:snapshot 长度、混杂模式、超时时间、缓冲区大小。snapshot 长度建议设为 65536,保证不会把大包截断;混杂模式设置为 1,这样能看到不是发给本机 MAC 地址的包,但如果你只关注本机进程,设不设混杂模式其实影响不大;超时时间设为 100 毫秒,这个值影响 pcap_next_ex 的阻塞行为——设置太短会频繁空转,太长则会让退出响应变慢,100 毫秒是很平衡的选择。
第三步就是 pcap_next_ex 循环。但直接这么写只能看到一片字节,毫无可读性。真正到抓包这一步,我们先要把协议解析摆上桌面。
3. 核心难点:把一个包准确算到某个 exe 头上
3.1 为什么 pcap 本身不知道“包属于哪个进程”
这是我认为整件事里最需要想明白的一个点。pcap 抓到的只是网卡驱动从链路上复制的原始帧,帧头里只有 MAC 地址、IP 地址、端口号,完全没有进程信息。操作系统里的 TCP/IP 协议栈知道这个连接是哪个 PID 建立的,但 pcap 在协议栈下方工作,它拿不到这个信息。
所以我们必须绕过一层:先问操作系统“当前有哪些 TCP/UDP 连接,每个连接属于哪个 PID”,然后拿连接信息去对照抓到的包。比如某个包的四元组是 192.168.1.5:52340 → 8.8.8.8:443,而系统的连接表里恰好有一条记录,本地地址是 192.168.1.5:52340,远端地址是 8.8.8.8:443,PID 是 1234,那这个包就理直气壮地算到 1234 这个 exe 头上。
3.2 用 GetExtendedTcpTable 读连接表
Windows 提供的接口是 GetExtendedTcpTable,通过 iphlpapi.h 调用。我用的方式是先拿表的大小,再分配缓冲区,最后再调用一次拿到真正的数据。这样比一次性分配一个固定大小的大缓冲区要稳妥。
获取 TCP 连接表的代码:
std::map<std::pair<in_addr, uint16_t>, DWORD> g_tcpOwner; void RefreshTcpTable() { ULONG size = 0; GetExtendedTcpTable(nullptr, &size, FALSE, AF_INET, TCP_TABLE_OWNER_PID_ALL, 0); std::unique_ptr<BYTE[]> buffer(new BYTE[size]); PMIB_TCPTABLE_OWNER_PID table = reinterpret_cast<PMIB_TCPTABLE_OWNER_PID>(buffer.get()); if (GetExtendedTcpTable(buffer.get(), &size, FALSE, AF_INET, TCP_TABLE_OWNER_PID_ALL, 0) != NO_ERROR) { return; } g_tcpOwner.clear(); for (DWORD i = 0; i < table->dwNumEntries; ++i) { MIB_TCPROW_OWNER_PID& row = table->table[i]; std::pair<in_addr, uint16_t> key; key.first.S_un.S_addr = row.dwRemoteAddr; key.second = ntohs((uint16_t)row.dwRemotePort); g_tcpOwner[key] = row.dwOwningPid; } }这里有几个容易被坑的细节。第一个是 MIB_TCPROW_OWNER_PID 里的端口字段是网络字节序,直接用会得到一个大得离谱的端口号,需要 ntohs 转换。第二个是 dwRemoteAddr 也是网络字节序,但如果你要打印的话,用 inet_ntoa 是没问题的,因为它同样期望网络字节序。第三个是这张表的更新频率和覆盖范围有限制——一个 TCP 连接在 close 之后会从表里消失,而我们捕获的数据包可能还残余一些最后发送或重传的包,这些包会因为映射缺失而无法归属。
实际上这种“连接消失导致包找不到进程”的情况非常常见。因为 pcap 抓包本身就是持续性的,而连接表是我们定时去取的快照,两者天然有时间差。我的解决办法是把快照刷新周期缩短到 300 到 500 毫秒,并且给“找不到进程的包”单独加一个归类和统计,这样至少能知道有多少流量是漏掉的,不至于把不完整的数字当成最终结果。
3.3 UDP 流量的归属问题
这一节是很多教程都不会讲的。TCP 连接表非常容易拿到进程,因为 TCP 是面向连接的,状态管理明确。但 UDP 进程级归属要麻烦得多。
Windows 的 GetExtendedUdpTable 返回的是 MIB_UDPTABLE_OWNER_PID,这里面只有本地地址和本地端口,没有远程地址和远程端口。哪怕你抓到一条 UDP 包去向是 1.1.1.1:53,你也无法从这张表里直接知道它是哪个进程发出的,因为表里只有“本机 UDP 端口 → PID”的映射。
所以,如果你的 exe 主要走 UDP 通信(比如某些游戏、VoIP 软件、QUIC 类的下载工具),只靠 GetExtendedUdpTable 是兜不住的。我在项目里对 UDP 的归属做了兜底处理:用“本地端口 → PID”映射来匹配,只要一个 UDP 包的源端口或目的端口和表中某个本地端口吻合,就把它算到那个进程头上,算不上的单独归到 unknown。这个方案在单进程、单端口场景下相当准,但如果你开了好几个都依赖 UDP 的软件,就会混。这是一个必须接受现实的限制。
如果你对 UDP 进程归属的精度有硬性要求,那就不该停留在 pcap 这一层了,更合适的是走 WFP 或者 ETW,那样能直接拿到每个 UDP 发送操作的进程 ID。关于这部分我会在文章后面再介绍一个轻量替代方案。
4. 过滤条件与流量计算:从裸数据包到“这个 exe 用了多少流量”
4.1 写 BPF 过滤表达式
拿到目标 exe 的 IP 和端口之后,我们就可以在抓包时直接把无关流量过滤掉。pcap_compile 和 pcap_setfilter 组合使用:
struct bpf_program fp; const char* filter = "tcp or udp"; if (pcap_compile(handle, &fp, filter, 1, PCAP_NETMASK_UNKNOWN) == -1) { printf("BPF 编译失败: %s\n", pcap_geterr(handle)); return -1; } pcap_setfilter(handle, &fp);这里的优化不止一处。如果你已经知道目标 exe 固定连哪几个服务器,可以把过滤表达式改成 host 1.2.3.4 or host 5.6.7.8,这样抓包数量会骤减,CPU 占用也会明显下降。但如果是想统计“这个 exe 所有流量”,那你不能只过滤目标主机的 IP,因为你不知道它会连哪些 IP——所以更合适的做法是不过滤远端 IP,而是只过滤端口关联:tcp port 443 or tcp port 80 or udp port 53。或者干脆先不过滤,把所有包都抓进来,在用户态做归属判定。
这里我多说一句 pcap 的过滤机制。BPF 过滤是在内核驱动里完成的,能极高地降低用户态接收数据的数量级。如果你的程序声明了要统计整个系统所有进程,就不要急着在驱动层过滤,因为这样会把本应统计的数据挡在外面。最稳妥的是在驱动层只过滤“跟本机 IP 相关的包”,也就是 ip host 192.168.1.5,然后在用户态再根据连接表区分进程。这个组合是我实测在千兆网卡满速抓包时还能跑得动的原因——用户态收到的包数量比全量抓包少了至少一个数量级。
4.2 如何判定方向:上传还是下载
判断一个包是上传还是下载,理论上有两种做法。
第一种是最直观的:看源 IP 是不是本机 IP。如果源 IP 是本机,说明这是本机发出的包,算上传;反之算下载。这种方法在绝大多数情况下都正确,但有一个例外:本机可能有多个网卡,比如有线网卡和虚拟网卡,流量走了别的接口,导致源 IP 判断错误。
第二种做法是配合连接表:找到匹配的 PID 之后,再看这条连接的“本地地址和本地端口”到底是包里的源还是目的。
bool IsUpload(const ip_header* ip, const tcp_header* tcp, const std::set<std::pair<in_addr, uint16_t>>& localSet) { std::pair<in_addr, uint16_t> src = {ip->src_addr, tcp->src_port}; return localSet.count(src) > 0; }第二种在开发中更可靠。因为 GetExtendedTcpTable 返回的 row 里有 dwLocalAddr 和 dwLocalPort 字段,这两个组合就是“这 exe 建立的本地端点”。我们把所有要统计进程的本地端点收集到 set 里,以后每个包只要匹配源端点在这个集合里,就一定是上传,否则是下载。采用这套逻辑还能顺带解决多网卡的问题。
4.3 字节数怎么算才准确
抓到包以后,最直接的字节数可以用 pcap_pkthdr.len,也就是“线上传输的长度”。但如果要统计应用层产生的流量,最好从网络层去算。以太网帧有 14 字节头,是链路层的头。IP 头则有可选字段,长度不固定,所以要把 IP 头的真实长度取出来。
size_t GetIpPayloadLength(const uint8_t* pkt, size_t len) { if (len < sizeof(ether_header)) return 0; const ip_header* ip = (const ip_header*)(pkt + sizeof(ether_header)); size_t ipHeaderLen = ip->ver_ihl & 0x0F; ipHeaderLen *= 4; size_t ipTotalLen = ntohs(ip->total_length); if (ipTotalLen > len - sizeof(ether_header)) { ipTotalLen = len - sizeof(ether_header); } return ipTotalLen - ipHeaderLen; }这里有个容易忽略的点。IP header 的 total_length 字段包含 IP 头本身,而我们要算的是“这个包对这个进程流量数据量的贡献”。有两种口径:
- 口径一:算 IP 层总长度,包括 IP 头。好处是跟大多数路由器统计口径一致。
- 口径二:算 IP 载荷长度,即 TCP/UDP 段及其载荷的总长。好处是减去了网络层协议开销。
我在实践中用的是口径一,也就是把 IP 头也算进流量,理由很直接:运营商和路由器计算流量时本来就是算 IP 总长,overhead 也是真实流量的一部分。如果做的是应用行为分析,你可以用口径二,代码只需要一行改动。无论用哪种口径,一旦选定就全程统一,不要混用,否则统计结果会无法横向对比。
5. 完整实现:一个能直接跑起来的进程流量统计工具
5.1 程序总体结构
一个最小的程序,拆成三个模块就够了:
- 模块一:网络设备枚举和 pcap 打开;
- 模块二:定时刷新 TCP/UDP 连接表,把“远端 IP:端口 → PID”和“本地端点 → PID”的映射整理好;
- 模块三:pcap 抓包循环,每抓到一个包,先根据四元组查连接表,命中就累加计数,并区分上传和下载。
我在实际项目中使用一个 std::thread 专门跑抓包循环,另一个 std::thread 负责每 300 毫秒刷新连接表。由于抓包回调运行在主抓包线程,映射表又是跨线程访问,所以必须加锁。用 std::shared_mutex 比较好:连接表刷新的线程很少写,抓包线程频繁读,读写锁能减少锁竞争。
</完整代码太长,我拆成关键片段给你看。这是抓包累加的核心:
std::atomic<uint64_t> g_uploadBytes{0}; std::atomic<uint64_t> g_downloadBytes{0}; std::map<std::pair<in_addr, uint16_t>, DWORD> g_ownerMap; std::shared_mutex g_mapMutex; void OnPacket(u_char* user, const pcap_pkthdr* hdr, const u_char* data) { size_t ipLen = GetIpPayloadLength(data, hdr->len); auto* ip = (const ip_header*)(data + 14); auto* tcp = (const tcp_header*)(data + 14 + (ip->ver_ihl & 0x0F) * 4); std::pair<in_addr, uint16_t> remoteKey; remoteKey.first.S_un.S_addr = ip->src_addr; remoteKey.second = ntohs(tcp->src_port); std::shared_lock lock(g_mapMutex); auto it = g_ownerMap.find(remoteKey); if (it == g_ownerMap.end()) return; if (it->second != g_targetPid) return; // 判断方向:看本地端口是源还是目的 ... }5.2 测试一个真实 exe 的效果
我用自己做的一个网络测试小工具试验了一下,它启动后会连接一个 HTTP 服务器,下载一个 10 MB 的文件,然后上传一个小 JSON。程序输出是这样:
[12:00:01] 目标 PID: 8124 [12:00:01] 打开设备: \Device\NPF_{...} [12:00:02] 已过滤设备下的流量 [12:00:03] 上传: 2.4 KB 下载: 1.02 MB 未归属: 1.2 KB [12:00:04] 上传: 2.4 KB 下载: 3.89 MB 未归属: 1.2 KB ... [12:00:06] 统计完成 上传总量: 8.9 KB 下载总量: 10.2 MB 未归属流量: 4.3 KB下载数字比 10 MB 略大,这是正常现象——TCP 连接建立本身有三次握手包和 ACK 确认包,加上可能存在的 HTTP 头部和 TLS 握手,实际线路上传的字节一定大于应用层文件大小。而“未归属流量”往往是连接刚关闭、映射还来不及刷新的尾巴流量,以及少量发到同一目标但端口查不到的 UDP 包。
5.3 如何把结果导出成“exe 流量使用报告”
统计完成后,输出不能只有汇总数字。我建议至少输出以下内容:
- 目标进程的 PID 和进程名;
- 上传/下载总字节数;
- TCP/UDP 分别的占比;
- 按远端 IP 汇总的流量分布;
- 未归属流量的字节数。
按远端 IP 汇总特别有用。它能直观告诉你,这个 exe 到底在跟哪些服务器通信。我当时统计一个第三方 exe 时发现它有大量流量指向一个本不该出现的云存储 IP,这就是一次典型的“偷跑流量”审计。实现很简单:在 OnPacket 里再维护一个 std::map<in_addr, uint64_t>,每次累加时顺便按远端 IP 计数,最后输出时用 inet_ntoa 转成点分十进制的字符串即可。
6. 连接表快照之外:一个更省事的备选方案
如果你不想每一步都自己维护连接表,Windows 上其实有一个读写更省事的数据来源:etsys。严格来说是 ETW 中的 Microsoft-Windows-Kernel-Network 事件,它能在每个 TCP/UDP 收发事件上直接带出进程 ID。但这个路线的接入成本比 pcap 高,需要解析 ETW 事件流,而且不同 Windows 版本的事件布局天差地别。
相比之下,还有一个折中的办法:直接用 netstat 命令行工具的外部输出,在脚本里定时跑 netstat -ano,然后把结果喂给抓包程序。我早期原型就是这么跑的,代码不复杂,也能快速验证方案。netstat 的缺点就是文本解析有点丑陋,而且每次启动 netstat 都有几十毫秒的额外开销,频繁调用会明显增加 CPU。但它的最大好处是零代码试水,你可以在动手写大工程之前先用它验证思路是否可行。
如果目标 exe 的流量不复杂,比如只是简单的短连接 HTTP 请求,我建议你先用 netstat 方案验证一天,看看能不能满足需求。如果发现连接建立得太快总是抓不到,再切换到 GetExtendedTcpTable。不要一上来就写一个几百行的 C++ 程序。
7. 实测中的难点与避坑记录
7.1 必须用管理员权限运行
这是新手最容易踩的坑。我第一版程序抓不到任何包,找了半天才发现是权限问题。pcap_open_live 在普通权限下能打开设备,但设置过滤条件后收到的包数量几乎为零,Npcap 默认在非管理员模式下会限制数据包进入用户态。所以写完之后,务必在调试配置里把“强制以管理员身份运行”打开,或者直接右键管理员运行。
7.2 抓包缓冲区大小和丢包
pcap_open_live 的最后一个参数是缓冲区大小,默认我用 1 MB。如果某些网卡流量较大,用户态处理不过来,驱动就会把缓冲区的旧包丢掉,导致统计结果偏小。Npcap 默认支持最高 256 MB 的内核缓冲区,在 pcap_open 时可以用 pcap_set_buffer_size 设置。我实测在千兆网卡下,把缓冲区加到 64 MB 之后,丢包率就从千分之几降到了零。代价是内存占用上升,但这在现代 PC 上完全不是问题。
具体的设置方式是打开设备之后先调用 pcap_set_buffer_size(handle, 64 * 1024 * 1024),再调用 pcap_setfilter。因为缓冲区的设置在激活阶段生效,如果设备已经激活了,再设置就无效了。
7.3 本地回环流量抓不到
Npcap 默认支持抓回环接口的流量,你需要专门选 loopback 设备。但这里有个坑:Npcap 的回环包跟在真实网卡上抓到的包不同,它们是以以太网帧形式模拟出来的。如果你的程序只针对真实网卡写死了 14 字节的链路层偏移,抓回环时会解析错位。解决方案是用数据链路层的类型来判断偏移。当前 Npcap 的回环设备多数情况下返回 DLT_EN10MB,和有线网卡一样;但 Windows 的老版本可以用 DLT_NULL。稳妥的做法是对每个设备读取 pcap_datalink,然后根据返回值决定解析函数。如果你的 exe 的通信对象是对本机回环地址的服务器,而这个地址又被系统回环驱动接管了,那么网卡设备是抓不到的;你需要打开 Npcap Loopback Adapter,并针对 DLT_EN10MB 做特殊判断。
7.4 网卡顺序变了导致设备写死失效
pcap_findalldevs 返回的设备名类似于 \Device\NPF_{GUID},这个 GUID 在重启后未改变,但如果你在代码里按索引顺序选设备,可能今天选到有线网卡,明天就变成虚拟网卡。我在一个项目里吃过这个亏,后来改成遍历设备列表,用设备描述里的关键词匹配来选网卡,比如“Ethernet”或“Wi-Fi”,匹配不到时再按索引兜底。如果你的运行环境相对固定,也可以让程序把网卡名写到配置文件中,启动时直接读取,这样不会选错。
7.5 统计结果的偏差来源
最后必须提醒的是,任何 pcap 方案都存在统计偏差。一是可能丢包,导致统计偏低;二是 IP 分片包处理不当,导致某个分片没算进数据量;三是 TCP 重传包会被多次统计,因为我们计算的是“线路上看到的包”,而不是应用层实际产生的数据。如果你要的是相对精确的应用层流量,重传是个干扰因素,现实中的应用层流量统计总是比理论值略高。实际应用时要明白这一点,并跟需求方约定好统计口径。
7.6 IPv6 流量暂时放弃
我上面的代码全部基于 AF_INET 的 IPv4。如果你的目标 exe 大量使用 IPv6 连接,pcap 抓包时依然能抓到 IPv6 的包,但连接表获取就得换成 AF_INET6 版本的 GetExtendedTcpTable,字段也变成了 16 字节的 in6_addr。解析和打印都会复杂不少。我个人的经验是优先支持 IPv4,再根据实际需求决定是否补上 IPv6。如果你抓的包里面 IPv6 占比超过 10%,建议尽早把 IPv6 解析加进去,否则统计结果会少一大块。
8. 一个更轻量的补充:不抓包也能估算“某个 exe 的流量”
如果你只是需要一个“大致可信”的流量数字,不要求看到每个包的细节,Windows 自带的 PerfMon 性能计数器有一条捷径:进程对象的 “IO Read Bytes/sec” 和 “IO Write Bytes/sec” 其实涵盖所有文件与网络 IO,混在一起;但网络相关的更准确应为 Network Interface 不再按进程。所以这一层要拿到进程粒度网络 IO,得借助 WFP 或者 Sysmon。Sysmon 的 Network connection 事件可以被 ETW 订阅,里面有完整 PID、目标 IP、端口和传输层协议。
如果只是应急快速获得粗粒度数据,我试过用 PowerShell 周期读取 Get-NetTCPConnection,把 LocalAddress、LocalPort、OwningProcess 以及对应的 TCP 字节统计拼起来。但问题依旧是短连接抓不到,而且 UDP 的远程地址信息缺失。用它做长期监控,数据缺口会让你头疼。
所以我给的建议很明确:你的目标是“精确到进程、且能看清每个连接对象”,就直接上 pcap 方案;目标只是“大概知道这个进程一天跑了几百 MB 上下”,用系统自带工具就够了。两条路线的成本差了十倍,不要为了一个粗需求投入精致的工具链。
最后再分享一个我在项目里常用的调试技巧:抓包程序里加一个“详情模式”,把每条命中目标进程的包打印出来,格式是“远端 IP:端口 方向 字节数 时间戳”。排查问题时开着它,跑几十秒,几乎一眼就能看出这个 exe 的网络行为模式。等到确认过滤规则没问题,再把详情模式关掉,改成只统计总量。这个小开关帮我节省了大量验证时间,也算是一路踩坑下来的副产品。