
简介这份 C# 网络抓包工具包面向希望学习网络编程、开展网络诊断与流量分析的高校学生和开发者核心围绕 SharpPcap 库实现数据包捕获并借助 PacketDotNet 完成协议解析。压缩包共 17 个文件约 2.24MB包含可直接运行的 exe 程序、dll 依赖与配置文件、pdb 调试符号、png 操作截图、docx 参考资料以及源码压缩包既方便快速试用也便于拆解研究。已有 307 人学习作为入门到进阶的实践案例具有一定参考价值。读者可通过现成程序观察抓包界面与结果也能阅读 C# 源码理解设备选择、过滤规则设置、数据包解析和异常处理思路还可结合截图与参考文档完成 WinPcap 环境配置及常见问题排查。对于想把套接字、协议解析与 SharpPcap 实际结合起来的学习者这份材料提供了较完整的练手路径。1. 拿到一个 C# 抓包程序先分清它解决的是哪一半问题凌晨被叫起来查一条接口偶发超时的链路Wireshark 一开几万个包刷屏真正想看的那几次 TCP 重传埋在中间——这种场景下缺的不是更熟练的过滤器语法而是一个能挂进自己程序、只抓该抓的、抓完能落盘复盘的 C# 网络抓包组件。这份 C# SharpPcap 网络抓包程序及源码.zip 干的就是这件事一个基于 SharpPcap 的 Windows 抓包程序带动网卡枚举、BPF 过滤、报文解析和 pcap 落盘源码可直接编译很多同行会把它当插件塞进自己的网络监控或协议调试软件里。它替代不了 Wireshark 的全能协议分析但适合需要在自己 C# 工具里内嵌抓包能力的开发者也适合照着源码把 SharpPcap 这套 API 摸一遍的新手。2. SharpPcap 的底层依赖与选型为什么不是 Raw Socket 而是驱动2.1 托管代码抓包绕不开一层驱动的原因.NET 里想抓包最直接的想法是Socket开一个原始套接字Windows 上再走SIO_RCVALL把网卡设成混杂。这条路能跑通 demo但一进真实环境就露馅原因有五条条条都是硬伤。第一是权限原始套接字必须管理员身份一个要发给现场运维用的工具每次右键「以管理员身份运行」都是投诉来源。第二是层次原始套接字拿到的数据从 IP 头开始以太网帧头、ARP、VLAN tag 全部看不到遇到要定位二层问题比如交换机端口错包就彻底哑火。第三是过滤原始套接字没有内核态过滤器所有包全量拷贝到用户态再由你的代码丢弃千兆打满时 CPU 直接顶死。第四是混杂模式SIO_RCVALL的行为在各版本系统上并不一致开关也不好控制。第五是可移植性Linux 上这套逻辑又得重写一遍。驱动层方案把这些一次性解决掉。Windows 上的路径是 NDIS 轻量过滤驱动LWF把帧截出来向上暴露一个wpcap.dll的 C 接口Linux 上则是 libpcap 走 AF_PACKET。SharpPcap 就是这层 C 接口的托管封装它本身不做抓包只负责把「打开设备、设过滤器、收包、写文件」翻译成 C# 对象。所以你在代码里写一行device.Filter tcp port 502实际发生的事是这个字符串被送到驱动里编译成 BPF 指令序列跑在内核态的虚拟机上只有命中的包才会被拷贝进你的进程。这一层差别在流量小的时候完全感觉不到流量一大就是生与死的区别。提示BPF 过滤器在驱动层生效这点很关键。很多人写抓包程序习惯「全抓下来再在 C# 里if判断」几百兆的抓包任务里这么写丢包率能到两位数。2.2 SharpPcap 与 PacketDotNet 的分工边界这套源码里其实是两个库在干活边界要分清楚否则后面改代码会一直别扭。SharpPcap 只负责「拿到原始字节」。它提供设备枚举CaptureDeviceList、实时设备LibPcapLiveDevice这一类、离线文件读取CaptureFileReaderDevice、文件写入CaptureFileWriterDevice以及统计信息Statistics。它给你的是RawCapture里面有Data字节数组、Timeval驱动打的时间戳、LinkLayerType链路类型。它不知道什么是 TCP也不关心。PacketDotNet 负责「把字节翻译成协议字段」。Packet.ParsePacket(LinkLayers.Ethernet, data)给你一个EthernetPacket再ExtractIPPacket()、ExtractTcpPacket()逐层剥。这一步是纯 CPU 计算跟抓包驱动没有关系。这个边界带来的第一个工程约束是解析绝对不要放进抓包回调里。回调是驱动线程上跑的你在里面做字符串拼接、写日志、写文件回调一慢驱动环形缓冲就填满填满就丢包。后面第 4 章会把这个拆开讲。还有一个容易忽略的点离线回放和在线抓包可以用同一套解析代码因为两者产出的都是RawCapture。这一点在做回归验证时非常值钱——线上抓到的一段异常流量存成 pcap拿回本地反复跑自己的解析逻辑不用再连网。2.3 解压后先做的依赖体检源码包能不能一次编译通过八成取决于环境而不是代码。按下面顺序过一遍能省掉大量「明明代码没问题却跑不起来」的时间。检查项看哪里常见问题SharpPcap 引用版本工程文件里的 PackageReference 或 packages 目录不同大版本 API 命名有差异Open()的重载、设备类名都变过抓包驱动是否安装设备管理器 / 系统服务没装驱动时CaptureDeviceList返回空集合不报错进程位数工程平台设置32 位进程加载不到 64 位驱动 DLL反之亦然目标框架工程属性老工程可能是 .NET Framework新 SDK 风格工程默认 .NET 6/8是否勾选兼容模式驱动安装向导部分老代码按旧接口名调用需要安装时勾上兼容项驱动这块补充一句SharpPcap 依赖的是系统里那份wpcap.dll它由抓包驱动安装包提供不在 NuGet 包里。所以「源码编译成功」和「程序能抓到包」是两件事前者只证明引用解析对了。我第一次跑这类工程时就栽在这——编译零警告运行起来设备列表空空如也排查了半小时才想起机器是全新的。注意装机时把安装向导里那个「兼容旧接口」的选项勾上能挡掉一批历史代码的兼容问题代价很小。3. 从零跑通网卡枚举、过滤器与抓包循环的落地步骤3.1 环境准备与工程结构先确认三件事再动手驱动装好、工程平台明确建议 x64 固定下来别留 AnyCPU、目标框架和源码包一致。做完之后新建一个控制台工程把源码里的抓包核心类挪进去或者直接编译原工程跑起来。工程结构上我一般按三层拆设备层负责枚举和打开网卡、采集层收包、过滤、入队、处理层解析、落盘、统计。源码包如果没这么分也没关系自己拆一遍后面加功能会舒服很多。第一步是设备枚举。这段代码很短但它决定了后面所有事情——网卡选错抓一小时也是白抓。using SharpPcap; // 枚举本机所有驱动认可的设备没装驱动时这里就是空集合 var devices CaptureDeviceList.Instance; if (devices.Count 0) { Console.WriteLine(设备列表为空先确认抓包驱动已安装且进程位数与驱动一致); return; } foreach (var dev in devices) { // Name 形如 \Device\NPF_{GUID}是 Open() 要用的句柄名人眼不可读 Console.WriteLine($NAME: {dev.Name}); // Description 里带网卡型号和地址信息选网卡看这个比看 Name 靠谱 Console.WriteLine($DESC: {dev.Description}); // 顺带打印 MAC 和 MTU排查抓不到包时这两项很有用 if (dev.MacAddress ! null) Console.WriteLine($MAC : {dev.MacAddress}); }逻辑说明CaptureDeviceList.Instance每次访问都会重新枚举不要放在循环里反复取。Name是给 API 用的Description是给人看的生产代码里最好两者都记日志——现场排查时你就知道当时选的是哪块卡。参数说明MacAddress在某些虚拟网卡上可能为空判空后再用。Description的文案随系统语言变化用它做匹配比如Contains(以太网)很脆中文系统换成英文系统就失效稳妥做法是让用户从列表里选或者按Name里的 GUID 配置。3.2 打开设备与 BPF 过滤器写法选中设备之后是打开。这里每个参数都影响后面的抓包质量值得逐个说清楚。using SharpPcap; using SharpPcap.LibPcap; // 按描述匹配只是演示生产环境建议改成配置项里存 Name var device CaptureDeviceList.Instance .OfTypeLibPcapLiveDevice() .FirstOrDefault(d d.Description ! null d.Description.Contains(以太网)); if (device null) throw new InvalidOperationException(没找到目标网卡检查设备名配置); device.Open(new DeviceConfiguration { Mode DeviceModes.Promiscuous, // 混杂模式收下所有经过网卡的帧能不能看到别人的流量取决于接入方式 ReadTimeout 1000, // 毫秒驱动缓冲没数据时多久返回一次影响停止响应的快慢 BufferSize 4 * 1024 * 1024, // 内核环形缓冲大小大流量场景的关键参数 Immediate false // false 允许驱动攒批再上交减少系统调用true 延迟更低但开销大 }); // BPF 表达式在驱动里编译执行不匹配的包根本不进用户态 device.Filter tcp port 502 or udp port 502;逻辑说明Open之后Filter才有效顺序不能反。过滤器语法是标准 BPFhost、net、port、portrange、tcp[tcpflags]、vlan这些都能用组合靠and/or/not和括号。写完过滤器建议马上用一个已知协议验证——比如先抓icmpping 一下看有没有包确认链路通了再换成业务过滤器。参数说明ReadTimeout设太小比如 10ms会让回调被频繁唤醒CPU 白烧设太大比如 5000ms会让StopCapture响应变慢用户点了停止要等好几秒。1000 是我用得比较顺手的值。BufferSize在千兆以上链路建议 4MB 起步内存不紧张就给到 16MB。Immediate这个参数名在大版本之间变过如果你手上的源码包里没有它说明那份 SharpPcap 版本较老删掉即可不影响其他逻辑。3.3 抓包循环回调里只做搬运第三步是把循环跑起来。这里有个原则回调里只做「拷贝字节 入队」两件事其他全部挪走。using System.Threading.Channels; // 有界队列容量到达上限时丢最老的包避免内存无限涨 var queue Channel.CreateBoundedbyte[](new BoundedChannelOptions(20000) { FullMode BoundedChannelFullMode.DropOldest, SingleReader true }); // 驱动线程上执行预算只有毫秒级绝不要在这里解析或写文件 device.OnPacketArrival (sender, e) { var raw e.GetPacket(); queue.Writer.TryWrite(raw.Data); // Data 是驱动缓冲的视图入队等于复制一份 }; device.StartCapture(); Console.WriteLine(抓包中回车停止...); Console.ReadLine(); device.StopCapture(); // 阻塞式停止等驱动把在途的包交完 device.Close();逻辑说明StartCapture之后收包在驱动线程上跑主线程可以做别的事。StopCapture是同步的会等当前批次处理完所以停止按钮不要直接绑在主 UI 线程上做长等待用Task.Run包一层。e.GetPacket()拿到的Data指向驱动缓冲如果不复制就丢进队列下一轮收包会把它覆盖掉——这个坑非常隐蔽表现是「解析出来的包内容随机错乱」很多人会怀疑到解析库头上。参数说明队列容量20000是经验值按平均包长 200 字节算大约 4MB 内存可以按实际场景调。DropOldest意味着高负载时丢的是旧包如果你更关心最近发生的异常大多数排障场景是这样这个策略最合适如果要做全量审计就得换成Wait并接受背压。注意队列一定要有界。无界队列在流量尖峰时会把内存吃光进程被系统杀掉比丢包严重得多。4. 报文解析与落盘从原始字节到可复盘的 pcap4.1 拆包到五元组拿到字节之后交给 PacketDotNet 逐层剥。这段代码本身不难难在边界判断要写全不然线上一定会在某个畸形包上抛异常。using PacketDotNet; public static string Describe(byte[] data, LinkLayers link) { // 链路类型必须和抓包网卡一致以太网卡就是 Ethernet var packet Packet.ParsePacket(link, data); var ip packet.ExtractIPPacket(); if (ip null) return 非 IP 包; // ARP、LLDP 之类走这里 var tcp ip.ExtractTcpPacket(); var udp ip.ExtractUdpPacket(); // 端口从传输层取没有传输层如 ICMP就是 0 var sport tcp?.SourcePort ?? udp?.SourcePort ?? 0; var dport tcp?.DestinationPort ?? udp?.DestinationPort ?? 0; // PayloadData 只算应用层负载不含各层头部做流量统计时用它更准 var appLen tcp?.PayloadData?.Length ?? udp?.PayloadData?.Length ?? 0; // Flags 里能直接看到 SYN/ACK/RST排查连接问题时比数包快 var flags tcp ! null ? tcp.Flags.ToString() : -; return ${ip.SourceAddress}:{sport} - {ip.DestinationAddress}:{dport} $ttl{ip.TimeToLive} app{appLen}B flags{flags}; }逻辑说明ParsePacket只解析到链路层后面的ExtractT是懒解析你调了什么它才解析什么所以只关心五元组时没必要把每一层都Extract一遍能省不少 CPU。ip.TotalLength是 IP 层总长含 IP 头PayloadData.Length是应用层负载两个口径不一样做带宽统计前先想清楚要哪个否则不同模块的数字对不上排查起来很费劲。参数说明link这个参数在离线回放时不要硬写成Ethernet要从 pcap 文件头里读实际值否则在某些采集卡上抓的包链路类型不是以太网会解析错位。ExtractT返回 null 是正常情况判空不是防御性编程是必须的。4.2 写 pcap时间戳比内容更容易出问题落盘这件事看起来简单但「抓到的包和存下来的包时间对不上」是高频投诉原因通常是自己造了时间戳。using SharpPcap; using SharpPcap.LibPcap; // 文件头magic、版本、链路类型、时间精度由这个类写不要手拼 var writer new CaptureFileWriterDevice(capture_20240101.pcap); // 打开时指定链路类型和快照长度和抓包设备保持一致 writer.Open(device.LinkType, 65535); // 抓包回调里入队的原始对象原样写盘时间戳由驱动给出 writer.Write(rawCapture); writer.Close();逻辑说明Write接收的是RawCapture里面带着驱动时间戳直接写就能保证「网络上的时刻」和「文件里的时刻」一致。如果你自己new RawCapture(DateTime.Now, ...)那存下来的是你的处理时间在丢包、排队、批处理场景下会偏几毫秒到几十毫秒做时序分析时这个偏差足以让你得出错误结论。参数说明第二参数是快照长度snaplen65535 表示整包全存。只想看头部时设成 96 或 128文件体积能小一个数量级代价是应用层内容被截断。这个值必须 ≥ 你关心的最大报文长度改小了之后没法回头补。4.3 用离线回放做回归解析和统计逻辑改完之后怎么证明没改坏答案是把线上抓的 pcap 当测试用例跑一遍。using SharpPcap; // 离线设备读出来的同样是 RawCapture和在线抓包共用一套处理逻辑 var reader new CaptureFileReaderDevice(capture_20240101.pcap); reader.Open(); int total 0; reader.OnPacketArrival (s, e) { var raw e.GetPacket(); total; // 这里调用和在线模式完全相同的解析函数 if (total % 10000 0) Console.WriteLine($已处理 {total} 包); }; reader.Capture(); // 阻塞直到文件读完比 StartCapture 更适合同步处理 reader.Close(); Console.WriteLine($回放完成共 {total} 包);逻辑说明Capture()和StartCapture()的区别在于前者同步阻塞到结束脚本化测试用Capture()更省心。同一份 pcap 反复跑输出的统计数字应该完全稳定任何一次数量上的抖动都说明你的解析逻辑里有依赖时间或随机性的东西那属于设计问题。参数说明回放速度默认是全速做压力测试时不需要额外限速但如果你要测「低速链路下的行为」得在回调里手动Thread.Sleep模拟别指望库给你限速。5. 避坑与排查五条从现场带回来的记录5.1 驱动与权限设备列表为空、打开就抛异常现象程序启动后设备列表是空的或者列表有内容但Open抛异常改到管理员身份运行就好了。原因三种情况混在一起。一是驱动没装wpcap.dll不存在二是进程位数不匹配32 位进程找不到 64 位驱动三是权限不足普通用户对网卡设备没有读权限。解决先单独跑一个十行的枚举程序把CaptureDeviceList.Instance.Count打出来非零说明驱动 OK问题在权限或位数为零就先装驱动再谈。位数问题在工程属性里把平台固定成 x64别留 AnyCPU。权限这块正式发布的工具要么走安装时提权要么在启动时检测并给出明确提示别让用户面对一个空白列表猜。5.2 过滤器写错一个逗号让整个抓包静默现象抓包跑起来了一个包都没有也没有任何报错。原因BPF 表达式语法错误或者语义写反了。常见的有tcp port 80 443少了or、tcp port 80 or尾随运算符、把and写成。另一个高频错误是端口号写的是字符串形式的服务名在不认识该服务的机器上直接匹配不到。解决设置Filter时用 try/catch 包住并打印错误信息别让它静默失败。调试顺序建议从宽到窄——先设ip确认能收到包再加tcp再加port最后加host哪一步变了就知道问题在哪一层。写完过滤器用ping或一次已知请求验证十秒钟的事能省掉半小时的自我怀疑。5.3 丢包统计数字对不上包少了现象抓包程序显示收了一万个包pcap 文件里只有八千个或者device.Statistics里的Received和Dropped差距明显。原因处理速度跟不上收包速度。要么回调里做了重活要么队列满了丢包要么内核缓冲太小导致驱动层就丢了。还有一种少见但真实的情况网卡自身在硬件层就丢了尤其是虚拟网卡和 USB 网卡。解决按顺序查。先看Statistics的Dropped非零说明是内核层丢加大BufferSize再看自己的队列丢弃计数非零说明处理端跟不上把解析和落盘移到独立线程最后看Immediate是不是设成了 true改回批处理模式。这三步走完还丢那就是链路速率超出单线程处理能力了得考虑多队列或降采样。5.4 混杂模式收不到别人的包现象开了混杂模式抓到的还是只有本机收发的流量看不到同网段其他机器的通信。原因这是对混杂模式的误解。混杂模式只让网卡「不丢弃目的 MAC 不是自己的帧」但帧能不能到你网卡上取决于接入设备。接在交换机的一个普通端口上交换机只会把该端口该收到的帧发过来混杂模式在这里没有发挥空间。解决先确认接入方式。需要看全段流量就得在能镜像流量的位置抓或者把抓包点放到网关侧。这一点最好在工具说明里写清楚否则每次交付都要解释一遍「为什么抓不到别人的包」。功能上也可以加一个提示检测到混杂模式开启但没有收到非本机流量时给出「当前接入点可能不支持」的提示。5.5 UI 卡死与内存暴涨现象抓包跑几分钟之后界面点不动任务管理器里内存一路上涨。原因两条常见路径。一是把包列表直接绑到控件上每来一个包就新增一行控件撑不住二是把RawCapture对象或者整包字节塞进列表长期持有内存只增不减。解决界面只展示汇总统计和最近 N 条比如 500 条滚动窗口全量数据交给后台队列和文件。控件更新做节流比如 200ms 合并刷新一次别一个包一次刷新。内存这块任何「存进集合」的动作都要问自己一句这个集合有上限吗6. 进阶用法把缓冲和统计口径定死程序才敢跑通宵一个抓包程序能不能交付分水岭不在能不能抓到包在于连续跑 24 小时之后数字还对不对得上。我踩过的坑基本都集中在这里分享几个收口的做法。第一件事是把统计口径固定下来并且四个数字同时打点。它们的差值能直接告诉你瓶颈在哪一层。指标来源正常表现异常含义驱动收包数device.Statistics.ReceivedPackets随流量稳定增长不涨说明过滤器或网卡有问题驱动丢包数device.Statistics.DroppedPackets0 或极小非零优先加大内核缓冲入队成功数回调里的自增计数与收包数基本相等差值大说明队列满了落盘条数写文件后的自增计数与入队数基本相等差值大说明落盘线程慢第二件事是环形缓冲的正确用法。我一般用Channel有界队列容量按「处理端 200ms 的处理量」来估而不是拍脑袋写个十万。估法很简单压测跑一遍量出每秒处理能力乘 0.2 就是安全容量。// 按处理能力反推容量每秒处理 5 万包留 200ms 余量 1 万 var capacity 10000; var queue Channel.CreateBoundedRawCapture(new BoundedChannelOptions(capacity) { FullMode BoundedChannelFullMode.DropOldest, SingleReader true, // 只有一个消费者线程时打开内部会走更快的路径 SingleWriter false // 多个网卡同时抓包时保持 false }); // 消费端批量取减少唤醒次数 var batch new ListRawCapture(256); await foreach (var item in queue.Reader.ReadAllAsync(ct)) { batch.Add(item); while (batch.Count 256 queue.Reader.TryRead(out var more)) batch.Add(more); WriteBatch(batch); // 一次写盘把 syscall 摊薄 batch.Clear(); }第三件事是滚动文件而不是一个大文件。按大小比如 200MB或时长比如 10 分钟切分文件名带上序号和时间好处是磁盘写满时只损失最后一段也方便事后按时间段定位。代价是回放时要合并写个几十行的合并脚本就行。最后是一个校验习惯每次发布前用同一份 pcap 离线回放把四个统计数字和上一版对比差值超过千分之一就回去看代码改了什么。这个动作花不了十分钟但它抓到过好几次「优化了性能但悄悄少算了包」的回归。从那以后我每次动抓包核心逻辑都强制走一遍「离线回放 四数对比」不管改动多小。抓包这东西的玄学在于错的地方往往不报错只是数字慢慢偏。希望帮到你。本文还有配套的精品资源点击获取