简介:这是一份面向C#开发者、网络管理员与安全分析人员的网络抓包工具源码,聚焦IP、TCP、UDP数据包的捕获与解析,可指定网卡与端口进行监听,适合用于网络调试、协议学习与安全排查等场景。压缩包共82个文件,约1.12MB,以24个cs源码文件为核心,配合14个png界面截图、8个resx与4个resources资源文件、2个sln解决方案及2个csproj工程文件,另有exe、dll、ico等可执行与图标资源,整体结构完整,可直接编译运行。项目围绕Socket套接字编程展开,涉及本地端点绑定、ReceiveFrom持续接收、IP/TCP/UDP头部字段解析,以及时间戳、源目标地址、协议类型、端口号、数据大小等信息的格式化展示,并配有过滤选项与主界面窗体。目前已有735人学习下载,读者可借此理解网络通信底层机制,掌握数据包捕获、解析与显示的实现思路,提升网络调试与协议分析能力。
1. 从一块网卡说起:C# 抓包到底能拿到什么
很多人第一次做 C# 抓包,是因为现场设备通信出了问题:上位机发出去的指令对方没回,或者回了一堆看不懂的字节,日志里只有「超时」两个字。这时候你需要的不是加打印,而是把经过网卡的真实数据包原样捞出来看。C# 抓取 IP、TCP、UDP 网络数据包,本质上是借助 WinPcap/Npcap 这类底层驱动,把网卡收到的原始帧交给托管代码解析,再按协议栈逐层剥开——以太网头、IP 头、TCP/UDP 头、应用层载荷。它能解决的核心问题就三个:确认数据到底有没有发出去、确认对方到底回了什么、确认端口和协议有没有配对。适合做上位机、工控网关、协议对接、网络调试的从业者,尤其是需要按指定端口过滤、又不想装 Wireshark 整套环境的场景。下面这份资源就是围绕这条链路拆的,我按「能跑起来 → 看得懂 → 避得开坑」的顺序讲。
2. 环境与选型:SharpPcap 为什么是 C# 抓包的首选
2.1 托管抓包库的三种路线对比
C# 想抓包,绕不开底层驱动。常见做法有三条路:一是直接 P/Invoke 调 WinPcap 的 wpcap.dll,灵活但全是裸指针,稍不留神就内存泄漏;二是用 SharpPcap,它对 wpcap 做了完整托管封装,设备枚举、过滤器、收包事件都是 C# 风格;三是用 .NET 自带的 Socket 走 Raw Socket,但 Windows 对 Raw Socket 限制多,收不到非本机流量,抓 TCP 握手都费劲。我一般直接上 SharpPcap,配合 PacketDotNet 做协议解析,这两个库版本要配套,SharpPcap 负责「拿到字节」,PacketDotNet 负责「翻译字节」。
选型时要注意运行环境:SharpPcap 依赖 Npcap(WinPcap 已停止维护),安装 Npcap 时必须勾选「WinPcap API-compatible Mode」,否则 SharpPcap 枚举不到网卡。这一步是新手翻车重灾区,装完没勾,代码里CaptureDeviceList.Instance返回空列表,还以为是代码写错了。
2.2 用 NuGet 把依赖装齐
新建一个 .NET 控制台或 WinForms 项目,包管理器里执行:
# 安装抓包核心库与协议解析库,版本号按项目框架选 dotnet add package SharpPcap --version 6.2.5 dotnet add package PacketDotNet --version 1.4.7装完后确认输出目录里有PacketDotNet.dll和SharpPcap.dll。如果是 .NET Framework 老项目,用Install-Package命令效果一样。这里版本不是随便写的:SharpPcap 6.x 对应 Npcap 1.x,PacketDotNet 1.4.x 的TcpPacket、UdpPacket属性名和 6.x 的 SharpPcap 事件模型能对上,混用旧版会出现Packet类型不匹配的编译错误。
2.3 枚举网卡并确认抓包权限
抓包前先列出所有可用设备,确认目标网卡在列表里:
using SharpPcap; using SharpPcap.LibPcap; // 获取所有可抓包的网络接口 var devices = CaptureDeviceList.Instance; if (devices.Count == 0) { Console.WriteLine("未发现网卡,检查 Npcap 是否勾选兼容模式"); return; } // 打印每块网卡的名字和描述,方便定位 for (int i = 0; i < devices.Count; i++) { Console.WriteLine($"[{i}] {devices[i].Name} - {devices[i].Description}"); }CaptureDeviceList.Instance是静态属性,第一次访问会触发驱动加载。如果返回 0,九成是 Npcap 没装好或没勾兼容模式。Name是\Device\NPF_{GUID}格式,Description才是人类可读的网卡名。生产环境里别用索引硬编码,用 Description 关键字匹配更稳,比如找含「Realtek」或「Intel」的那块。
3. 抓包核心:按指定端口过滤 TCP 与 UDP
3.1 BPF 过滤器语法与端口表达式
SharpPcap 的过滤器直接透传给底层 BPF 引擎,语法和 tcpdump 一致。要抓指定端口,核心表达式是tcp port 502或udp port 47808。多个端口用or连接,比如tcp port 502 or tcp port 503。想同时抓 TCP 和 UDP 的某个端口,写port 8080即可,BPF 会自动匹配两种协议。这里有个容易忽略的点:过滤器在驱动层生效,不匹配的包根本不会进托管内存,所以过滤条件写得越精确,CPU 占用越低。
常见做法是先宽后窄:调试阶段用ip host 192.168.1.100先确认流量方向,再逐步加端口条件。如果一上来就写死端口,结果发现包根本没到网卡,排查方向就窄了。
3.2 打开设备并注册收包回调
下面这段是完整的抓包主流程,按指定端口过滤并解析:
using SharpPcap; using PacketDotNet; // 假设已选中 devices[0],打开设备准备抓包 var device = devices[0]; device.Open(new DeviceConfiguration { Mode = DeviceModes.Promiscuous, // 混杂模式,抓经过网卡的所有帧 ReadTimeout = 1000 // 读超时 1 秒,避免线程卡死 }); // 设置 BPF 过滤器:只抓 TCP 502 端口和 UDP 47808 端口 device.Filter = "tcp port 502 or udp port 47808"; // 注册收包事件,每来一个包触发一次 device.OnPacketArrival += (sender, e) => { // 从原始字节解析出以太网帧 var rawPacket = Packet.ParsePacket(e.Packet.LinkLayerType, e.Packet.Data); var ipPacket = rawPacket.Extract<IPPacket>(); if (ipPacket == null) return; // 按协议类型分别处理 if (ipPacket.Protocol == ProtocolType.Tcp) { var tcp = ipPacket.Extract<TcpPacket>(); Console.WriteLine($"TCP {ipPacket.SourceAddress}:{tcp.SourcePort} -> " + $"{ipPacket.DestinationAddress}:{tcp.DestinationPort} " + $"载荷 {tcp.PayloadData?.Length ?? 0} 字节"); } else if (ipPacket.Protocol == ProtocolType.Udp) { var udp = ipPacket.Extract<UdpPacket>(); Console.WriteLine($"UDP {ipPacket.SourceAddress}:{udp.SourcePort} -> " + $"{ipPacket.DestinationAddress}:{udp.DestinationPort} " + $"载荷 {udp.PayloadData?.Length ?? 0} 字节"); } }; // 开始抓包,-1 表示持续抓直到手动停止 device.StartCapture(); Console.WriteLine("抓包中,按回车停止..."); Console.ReadLine(); device.StopCapture(); device.Close();逻辑上分四步:打开设备、设过滤器、挂回调、启停控制。DeviceModes.Promiscuous是混杂模式,能抓到目的地址不是本机的帧,做网关分析时必须开;如果只关心本机通信,用DeviceModes.None更省资源。ReadTimeout设 1000 毫秒是经验值,太小会导致频繁空轮询,太大则停止响应慢。Packet.ParsePacket的第一个参数是链路层类型,以太网就是LinkLayerType.Ethernet,别硬编码,用e.Packet.LinkLayerType最稳。
3.3 解析 IP 头与端口的关键字段
IPPacket里几个字段值得盯:SourceAddress、DestinationAddress是System.Net.IPAddress类型,直接 ToString 就是点分十进制;Protocol是枚举,判断 TCP/UDP 用它;TotalLength是 IP 包总长,包含头。TCP 层里SourcePort、DestinationPort是 ushort,PayloadData是 byte 数组,应用层数据全在里面。UDP 层同理,但PayloadData可能为 null,取值前判空。
如果要抓的是 Modbus TCP 这类应用协议,端口过滤到 502 后,tcp.PayloadData的前 7 个字节就是 MBAP 头,第 8 字节开始是 PDU。这一步是「抓包」到「懂包」的分界线,很多人卡在拿到字节不知道怎么切。
4. 避坑与排查:抓不到包时先看这五条
4.1 现象:设备列表为空,代码报索引越界
原因:Npcap 未安装,或安装时没勾选 WinPcap 兼容模式,SharpPcap 找不到 wpcap.dll。解决:重装 Npcap,安装向导里明确勾选「Install Npcap in WinPcap API-compatible Mode」,装完重启一次系统再跑。
4.2 现象:过滤器设了端口,但一个包都收不到
原因:BPF 表达式写错,比如把tcp port 502写成tcp port = 502,或者端口号超出 65535。解决:先用device.Filter = "ip"确认能收到任意 IP 包,再逐步加端口条件;表达式里不要加等号,端口直接跟数字。
4.3 现象:能抓到包但 PayloadData 全是 null
原因:抓到的包是纯 ACK 或握手包,本身没有应用层载荷;或者链路层类型判断错,导致解析偏移。解决:先打印ipPacket.Protocol和tcp.PayloadData == null判断,握手包无载荷是正常的;确认LinkLayerType用的是e.Packet.LinkLayerType而非硬编码。
4.4 现象:抓包程序 CPU 占用飙到 100%
原因:ReadTimeout设得太小,或者回调里做了耗时操作(比如写文件、发网络请求)。解决:ReadTimeout调到 500~1000 毫秒;回调里只做轻量解析,把数据丢进ConcurrentQueue,另起线程消费。
4.5 现象:混杂模式下抓到大量无关广播包
原因:混杂模式会收所有经过网卡的帧,包括 ARP、DHCP 广播。解决:过滤器里加and not broadcast and not multicast,或者用ip host 目标IP限定通信对端,把噪声挡在驱动层。
5. 进阶:把抓到的包落盘并做端口级统计
5.1 用 Pcap 格式存盘,方便二次分析
抓到的包如果只打印就浪费了,SharpPcap 支持直接写 pcap 文件,Wireshark 能打开:
// 打开一个 pcap 写入器,格式为标准 pcap var writer = new CaptureFileWriterDevice("capture.pcap"); device.OnPacketArrival += (sender, e) => { writer.Write(e.Packet); // 原始帧直接落盘 };CaptureFileWriterDevice的构造参数是文件路径,写入的是原始链路层帧,所以 Wireshark 打开后协议解析完全正常。注意 writer 要在 device 打开之后再创建,关闭顺序是先停抓包、再关 writer、最后关 device,顺序反了会丢最后几个包。
5.2 按端口做收发字节统计
现场排查经常要回答「这个端口到底跑了多少数据」,用字典累加即可:
// 用线程安全字典按端口累计收发字节 var stats = new ConcurrentDictionary<int, long>(); device.OnPacketArrival += (sender, e) => { var raw = Packet.ParsePacket(e.Packet.LinkLayerType, e.Packet.Data); var ip = raw.Extract<IPPacket>(); if (ip == null) return; var tcp = ip.Extract<TcpPacket>(); if (tcp != null) { // 按目的端口累加载荷长度 stats.AddOrUpdate(tcp.DestinationPort, tcp.PayloadData?.Length ?? 0, (k, v) => v + (tcp.PayloadData?.Length ?? 0)); } };ConcurrentDictionary的AddOrUpdate保证多线程回调下计数不丢。统计的是载荷长度而非帧长,因为排查应用层流量时载荷更有意义。跑一段时间后遍历stats就能看到哪个端口最忙,配合时间窗口还能算吞吐。
5.3 一个我常犯的错
早期我总在回调里直接Console.WriteLine,抓高速流量时控制台成了瓶颈,包丢得比抓的多。后来改成回调只入队、单独线程格式化输出,丢包率立刻降下来。从那以后我每次写抓包工具,都强制把「收包」和「处理」拆成两个线程,这个习惯帮我省了无数次现场排查的返工。希望帮到你。
本文还有配套的精品资源,点击获取