简介:这是一份面向C#开发者与网络编程学习者的网络抓包实践资源,基于开源SharpPcap库实现数据包捕获与解析,可用于网络诊断、流量监控与安全分析等场景,适合希望掌握套接字编程与网络协议解析的中级学习者。压缩包共17个文件,约2.24MB,包含png程序截图、exe可执行程序、dll动态库、xml文档、config配置及docx参考资料等,兼顾直接运行与源码研读两种使用方式。目前已有307人学习下载。资源同时提供程序源码与编译好的可执行文件,读者既能直接运行工具完成抓包操作,也能通过阅读源码理解SharpPcap与PacketDotNet的调用方式、数据包解析流程及WinPcap环境配置思路;配套截图与说明文档可辅助快速上手,是提升C#网络编程能力的实用案例。
1. 从一次抓包需求说起:这个 C# 项目能帮你省下多少折腾
有次排查一个上位机与采集板之间的通信问题,日志里全是超时,但设备厂商坚持说数据发出去了。当时手边没有现成的抓包工具能嵌进自己的诊断程序里,临时装个通用抓包软件又没法把结果直接喂给业务代码做二次判断。后来翻到一个用 C# 写的 SharpPcap 抓包程序源码包,才意识到这类需求其实完全可以自己掌控——它就是一个基于 SharpPcap 库的 C# 网络抓包程序,附带完整源码,能直接编译运行,也能把抓包能力嵌进你自己的 .NET 项目里。
这个项目适合两类人:一类是想学网络协议分析、需要看得到每一层报文结构的开发者;另一类是手里有 C# 上位机、工控软件或运维工具,想加一个「抓包诊断」按钮却不想引入重型依赖的人。它不追求替代通用抓包软件,而是给你一份能读、能改、能集成的源码底子。下面按「能干什么 → 怎么跑 → 怎么改」的顺序拆开讲。
2. 项目定位与技术选型:为什么是 C# 加 SharpPcap
2.1 这个抓包程序到底解决什么问题
从标题和关键词能看出,这是一个 C# 网络抓包程序,核心依赖是 SharpPcap。SharpPcap 是 .NET 平台对 libpcap/WinPcap/Npcap 的封装,也就是说它本身不实现抓包驱动,而是调用系统底层抓包库。这个定位决定了它的能力边界:能抓到经过网卡的原始帧,能按 BPF 过滤表达式筛选,能把包解析成以太网、IP、TCP/UDP 等层次结构,但它不是协议解析全家桶,应用层协议要自己往上叠。
项目正文为空,说明没有额外的功能描述,所以功能范围就以「SharpPcap 抓包程序」这个核心来推。常见做法是:枚举网卡、选择设备、设置过滤规则、开始捕获、回调里解析并展示、支持停止和保存。这套流程在 SharpPcap 的示例里是标准骨架,这个源码包大概率也是围绕它展开的。对读者来说,价值不在于它多复杂,而在于它把「抓包」这件事从命令行工具变成了可嵌入的 C# 代码。
关键词里「c# 网络 网络 软件/插件」也印证了这一点:它既可以当独立软件跑,也可以当插件式模块塞进更大的网络诊断工具里。如果你正在做网络监控、协议调试、工控通信排查,这份源码能省掉你从零封装 SharpPcap 的时间。
2.2 技术栈拆解:SharpPcap 在 .NET 里的位置
SharpPcap 的工作方式分两层:上层是 C# 的 API,下层是系统抓包库。在 Windows 上通常依赖 Npcap(WinPcap 的后续),在 Linux 上依赖 libpcap。这意味着程序要跑起来,目标机器上必须装好对应的底层驱动或库,否则 SharpPcap 初始化时会直接报找不到设备。
技术栈大致是:
| 层次 | 组件 | 作用 |
|---|---|---|
| 语言运行时 | .NET / .NET Framework | 承载 C# 代码 |
| 抓包封装 | SharpPcap | 提供设备枚举、捕获、过滤、包解析 API |
| 底层驱动 | Npcap / libpcap | 真正从网卡取数据 |
| 包解析 | PacketDotNet(常见搭配) | 把原始字节解析成协议对象 |
PacketDotNet 不是标题里写的,但 SharpPcap 的官方示例和绝大多数实战项目都会搭配它来做协议解析,否则你拿到的只是一堆字节数组。我一般会把它一起引入,这样解析 IP、TCP、UDP 头字段时不用自己移位运算。
提示:SharpPcap 和 PacketDotNet 的版本要匹配,版本错位时常见现象是编译通过但运行时解析方法找不到,或者包类型转换失败。
2.3 目录结构预判与关键文件
源码包没有给出目录清单,但按这类项目的常见组织方式,结构通常如下(以实际解压为准):
SharpPcapCapture/ ├── SharpPcapCapture.sln # 解决方案文件 ├── SharpPcapCapture/ │ ├── Program.cs # 入口,控制台或窗体启动 │ ├── CaptureManager.cs # 抓包核心逻辑封装 │ ├── PacketParser.cs # 包解析与展示 │ ├── DeviceSelector.cs # 网卡枚举与选择 │ ├── app.config / *.csproj # 依赖与配置 │ └── Properties/ └── packages/ 或 NuGet 引用 # SharpPcap、PacketDotNet如果你拿到的是 WinForms 版本,还会多出 MainForm.cs、MainForm.Designer.cs 这类界面文件。判断方式很简单:看入口是 Console.ReadLine 还是 Application.Run。不管哪种,抓包逻辑都会集中在一个管理类里,改的时候优先找那个类。
3. 跑起来:环境准备与第一个可捕获的包
3.1 环境准备:Npcap 与项目依赖
在 Windows 上跑这个项目,第一步不是打开 Visual Studio,而是确认底层抓包库装好了。SharpPcap 本身是托管库,但它调用的 wpcap.dll 来自 Npcap。安装 Npcap 时有一个选项叫「Install Npcap in WinPcap API-compatible Mode」,建议勾上,否则部分老代码调用 WinPcap 接口会失败。
安装完成后,用命令行验证一下:
# 查看 Npcap 是否安装成功,以及 wpcap.dll 是否存在 where wpcap.dll # 正常应输出类似 C:\Windows\System32\Npcap\wpcap.dll如果这条命令没有输出,说明底层库没装好,后面代码里CaptureDeviceList.Instance会返回空列表,程序看起来「没报错但抓不到任何网卡」。这是最常见的翻车点之一。
项目依赖通过 NuGet 还原,常见包是 SharpPcap 和 PacketDotNet。用命令行还原:
# 在解决方案目录下执行,还原所有 NuGet 包 dotnet restore SharpPcapCapture.sln # 如果项目是旧式 .NET Framework,用 nuget restore nuget restore SharpPcapCapture.sln参数说明:restore会根据 csproj 或 packages.config 里的引用去拉包,网络不通时会卡在这里,表现为「找不到 SharpPcap 命名空间」。解决方式是配置可用的 NuGet 源,或者手动把包放进 packages 目录。
3.2 枚举网卡并选择捕获设备
抓包程序的第一步永远是列出网卡。SharpPcap 用CaptureDeviceList.Instance拿到所有设备,每个设备有 Name、Description、Addresses 等属性。下面这段是常见写法:
using SharpPcap; using SharpPcap.LibPcap; // 获取本机所有可抓包的网卡设备 var devices = CaptureDeviceList.Instance; if (devices.Count == 0) { Console.WriteLine("没有找到任何网卡,请检查 Npcap/libpcap 是否安装。"); return; } // 打印网卡列表,让用户选择 for (int i = 0; i < devices.Count; i++) { var dev = devices[i]; // Description 在 Windows 上通常带网卡型号,Linux 上可能是接口名 Console.WriteLine($"[{i}] {dev.Name} - {dev.Description}"); } Console.Write("请输入要抓包的网卡编号: "); int index = int.Parse(Console.ReadLine()); ILiveDevice selectedDevice = devices[index];逻辑说明:CaptureDeviceList.Instance是静态属性,每次访问会重新枚举,设备热插拔后建议重新获取。ILiveDevice是 SharpPcap 较新版本的接口类型,老版本里是ICaptureDevice,如果你编译时报「找不到 ILiveDevice」,说明包版本较旧,换成ICaptureDevice即可。参数方面,dev.Name是系统内部标识,dev.Description是给人看的,选择时用编号最稳妥。
注意:如果列表里出现多个相似网卡,优先选有 IP 地址的那个,否则可能抓到的是虚拟网卡或未连接的口,什么都抓不到。
3.3 打开设备、设置过滤器并开始捕获
选好设备后,打开设备并挂上 BPF 过滤表达式,能大幅减少无关包。下面这段是核心捕获循环:
// 打开设备,ReadTimeout 表示读超时毫秒数,超时后会触发回调 selectedDevice.Open(new DeviceConfiguration { Mode = DeviceModes.Promiscuous, // 混杂模式,能抓到经过网卡的所有帧 ReadTimeout = 1000 // 1000ms 超时,避免回调一直不触发 }); // 只抓 TCP 80 端口的包,BPF 语法和通用抓包工具一致 selectedDevice.Filter = "tcp port 80"; // 注册包到达回调 selectedDevice.OnPacketArrival += (sender, e) => { var rawPacket = e.GetPacket(); // 这里先打印长度,解析放到下一节 Console.WriteLine($"抓到包,长度 {rawPacket.Data.Length} 字节"); }; selectedDevice.StartCapture(); Console.WriteLine("开始抓包,按回车停止..."); Console.ReadLine(); selectedDevice.StopCapture(); selectedDevice.Close();逻辑说明:Open的DeviceConfiguration里Mode设为混杂模式后,网卡会接收目的地址不是本机的帧,这在排查广播、镜像口场景时有用,但在交换网络里不一定能看到别人的单播流量。Filter是 BPF 表达式,写错会抛异常,常见错误是端口写成port = 80,正确写法是tcp port 80。OnPacketArrival是事件回调,回调里不要做耗时操作,否则会丢包,解析和写盘建议放到队列里异步处理。
参数怎么改:ReadTimeout调小会让回调更频繁但 CPU 占用上升;Filter留空表示抓所有包,流量大时容易把内存打满。我一般会先设一个窄过滤,确认链路通了再放宽。
4. 核心模块拆解:从原始字节到可读协议字段
4.1 包解析:用 PacketDotNet 拆开每一层
抓到原始包只是第一步,真正有用的是把字节解析成协议字段。PacketDotNet 提供Packet.ParsePacket,传入链路层类型和原始字节,返回一个 Packet 对象,然后逐层取 EthernetPacket、IpPacket、TcpPacket 等。
using PacketDotNet; // 在 OnPacketArrival 回调里解析 var rawPacket = e.GetPacket(); // LinkLayerType 来自设备,常见是以太网 Packet packet = Packet.ParsePacket(rawPacket.LinkLayerType, rawPacket.Data); // 逐层提取,注意每层都可能为 null var ethernet = packet.Extract<EthernetPacket>(); var ip = packet.Extract<IPPacket>(); var tcp = packet.Extract<TcpPacket>(); if (ip != null) { Console.WriteLine($"源IP {ip.SourceAddress} -> 目的IP {ip.DestinationAddress}"); } if (tcp != null) { Console.WriteLine($"源端口 {tcp.SourcePort} -> 目的端口 {tcp.DestinationPort}"); Console.WriteLine($"标志位 {tcp.Flags}"); }逻辑说明:Extract<T>会从当前包及其负载里找指定类型的层,找不到返回 null,所以每层都要判空。ip.SourceAddress返回的是System.Net.IPAddress,直接 ToString 就是点分十进制。tcp.Flags是枚举,能看出 SYN、ACK、FIN 等,排查连接建立失败时特别有用。
参数说明:rawPacket.LinkLayerType决定解析起点,如果是以太网就是 EthernetPacket,如果是 Raw IP 则没有以太网头。抓 Loopback 接口时链路类型可能不同,解析代码要兼容,否则Extract<EthernetPacket>()返回 null,后面全空。
4.2 过滤表达式:BPF 语法与常见写法
BPF 过滤器是抓包效率的关键。写得好,回调里只处理你关心的包;写得差,等于把整条链路流量都拉进托管内存。常见表达式如下:
| 需求 | BPF 表达式 |
|---|---|
| 抓指定 IP | host 192.168.1.10 |
| 抓指定端口 | tcp port 8080 |
| 抓某网段 | net 192.168.1.0/24 |
| 排除某端口 | not port 22 |
| 组合条件 | tcp and host 192.168.1.10 and port 443 |
设置方式就是selectedDevice.Filter = "...",必须在StartCapture之前设置。如果表达式语法错误,SharpPcap 会抛PcapException,消息里通常带「syntax error」。排查时先把表达式简化到tcp,确认能抓到再逐步加条件。
提示:过滤表达式里的 IP 和端口是网络字节序层面的匹配,不涉及应用层内容。想按 HTTP 内容过滤,BPF 做不到,得在回调里自己判断负载字符串。
4.3 捕获结果的展示与保存
抓到并解析后,通常要展示或落盘。展示可以走控制台,也可以绑到 DataGridView。保存成 pcap 文件是常见需求,SharpPcap 提供CaptureFileWriterDevice:
// 创建 pcap 写入设备,路径和链路类型要对 var writer = new CaptureFileWriterDevice("capture.pcap", selectedDevice.LinkType); selectedDevice.OnPacketArrival += (sender, e) => { var rawPacket = e.GetPacket(); // 原样写入,保证文件能被通用抓包工具打开 writer.Write(rawPacket); }; selectedDevice.StartCapture();逻辑说明:CaptureFileWriterDevice的第二个参数是链路类型,必须和抓包设备一致,否则写出的文件在通用工具里打开会提示格式错误。writer.Write接收 RawCapture,直接写原始字节,不做解析,这样性能最好。停止抓包后要调用writer.Close(),否则文件头可能不完整。
参数说明:文件路径建议用绝对路径,相对路径在服务或计划任务里容易写到意外目录。如果抓包量大,写盘会成为瓶颈,常见做法是加一个BlockingCollection做生产者消费者,回调只入队,单独线程写文件。
5. 避坑与常见问题:抓不到包时先查这几处
5.1 设备列表为空
现象:程序启动后CaptureDeviceList.Instance.Count为 0,或者打印「没有找到任何网卡」。
原因:底层抓包库没装或没装对。Windows 上缺 Npcap,Linux 上缺 libpcap,或者装了但当前用户没有权限访问。
解决:Windows 重新安装 Npcap 并勾选兼容模式;Linux 用apt install libpcap-dev安装开发库,运行时如果提示权限不足,用setcap cap_net_raw,cap_net_admin=eip 你的程序授权,而不是简单用 root 跑。
5.2 能抓到包但解析全为 null
现象:回调触发了,长度也打印了,但Extract<IPPacket>()返回 null。
原因:链路层类型判断错,或者过滤到了非 IP 包(比如 ARP)。也可能是Packet.ParsePacket的第一个参数传错。
解决:先打印rawPacket.LinkLayerType确认类型,以太网用 EthernetPacket 解析。对 ARP 包做判空跳过。如果抓的是 Loopback,链路类型可能是 Raw,需要换解析入口。
5.3 回调里做耗时操作导致丢包
现象:抓包数量明显少于预期,或者程序越跑越卡。
原因:OnPacketArrival在抓包线程上同步执行,回调里做字符串拼接、写数据库、更新界面都会阻塞抓包。
解决:回调里只做入队,用ConcurrentQueue或BlockingCollection把 RawCapture 交给后台线程处理。界面更新用Invoke切回 UI 线程,且做批量刷新,不要每包一次。
5.4 过滤器设置后一个包都抓不到
现象:不设过滤器能抓到,设了之后回调完全不触发。
原因:BPF 表达式写错,或者过滤条件与当前流量不符。比如在只有 UDP 流量的链路上写tcp port 80。
解决:先把过滤器清空确认链路通,再逐步加条件。用通用抓包工具验证同一表达式是否能抓到包,排除表达式本身的问题。注意port 80和tcp port 80的区别,前者会匹配 UDP 80。
5.5 编译报找不到 SharpPcap 命名空间
现象:using SharpPcap;报错,或者ILiveDevice类型不存在。
原因:NuGet 包没还原,或者包版本与代码不匹配。
解决:先dotnet restore,确认 packages 目录或全局缓存里有 SharpPcap。如果代码用的是新接口而包是旧版,要么升级包,要么把ILiveDevice改回ICaptureDevice。版本升级时注意 PacketDotNet 也要同步升,两者 API 有联动。
6. 二次开发:把抓包能力嵌进自己的工具里
6.1 封装成可复用的抓包服务
直接改 Program.cs 只能跑一个独立程序,想嵌进现有项目,建议把抓包逻辑抽成一个服务类,对外暴露开始、停止、事件三个口子。我一般会这样组织:
public class CaptureService { private ILiveDevice _device; private CancellationTokenSource _cts; // 包到达事件,外部订阅后自行处理,避免服务类耦合界面 public event Action<RawCapture> PacketArrived; public void Start(string deviceName, string filter) { _device = CaptureDeviceList.Instance .FirstOrDefault(d => d.Name == deviceName); if (_device == null) throw new ArgumentException("设备不存在"); _device.Open(new DeviceConfiguration { ReadTimeout = 500 }); if (!string.IsNullOrEmpty(filter)) _device.Filter = filter; _device.OnPacketArrival += (s, e) => PacketArrived?.Invoke(e.GetPacket()); _device.StartCapture(); } public void Stop() { _device?.StopCapture(); _device?.Close(); } }逻辑说明:把设备查找、打开、过滤、事件转发收在一个类里,外部只关心PacketArrived。这样无论是控制台、WinForms 还是 ASP.NET 后台服务,都能复用同一套抓包逻辑。PacketArrived用 Action 而不是直接暴露 SharpPcap 事件,是为了隔离依赖,将来换库时改动面小。
参数说明:deviceName用设备 Name 而不是编号,避免界面刷新后编号错位。filter允许为空,空表示抓全部。ReadTimeout设 500ms 是为了让停止操作更跟手,太大时 StopCapture 会等当前读超时。
6.2 加一个环形缓冲,避免内存无限增长
抓包程序跑久了内存飙升,多半是包对象没释放。SharpPcap 的 RawCapture 本身不大,但如果解析后把 Packet 对象存进 List,很快就会撑爆。常见做法是环形缓冲,只保留最近 N 条:
// 固定容量队列,超出后自动丢弃最旧的包 private readonly ConcurrentQueue<PacketInfo> _recent = new(); private const int MaxKeep = 5000; public void OnPacket(RawCapture raw) { var info = ParseToInfo(raw); // 解析成轻量结构,别存 Packet 对象 _recent.Enqueue(info); while (_recent.Count > MaxKeep) _recent.TryDequeue(out _); }逻辑说明:PacketInfo是自己定义的轻量类,只存时间、源、目的、端口、长度等字段,不持有 PacketDotNet 的对象引用。MaxKeep按内存和界面需求调,5000 条对诊断够用。这样即使抓一整天,内存也稳定在一个水位。
6.3 验证改造是否生效的笨办法
改完抓包服务后,别急着接业务,先用一个最小验证:抓本机回环上的一个已知请求。比如本地起一个 HTTP 服务,用tcp port 你的端口过滤,发一次请求,看回调是否触发、解析出的源目的端口是否对得上。这个办法能同时验证设备选择、过滤器、解析链路三处,比直接上生产环境排查快得多。
我踩过的坑是:改完过滤表达式后忘了重新Open设备,结果新过滤器没生效,还以为是解析代码写错了,查了半天。从那以后我每次改过滤器都强制走一遍「停止 → 重新打开 → 设置过滤 → 开始」的完整流程,不图省事直接赋值。希望这份源码和上面的拆解能帮到你,拿到包后先跑通最小链路,再往上叠自己的业务逻辑。
本文还有配套的精品资源,点击获取