简介这是一份基于.NET框架、使用C#编写的网络数据包嗅探分析器源码面向对网络协议与底层通信感兴趣的C#初学者及进阶开发者可用于网络诊断、安全审计与性能测试等场景的学习实践。项目围绕数据包捕获与解析展开核心模块涵盖TCP、UDP、IP、ICMP、IGMP等协议的报文处理并借助原生API完成底层抓包代码结构清晰、便于阅读与二次修改。压缩包共84个文件约594KB以48个cs源码文件为主体辅以csproj工程文件、dll与exe可执行程序、resx资源及txt说明文档另含sln解决方案方便直接编译运行。目前已有204人学习下载。通过研读源码读者可掌握C#中数据包捕获、协议解析与信息展示的实现思路理解网络通信细节对网络管理员、安全从业者及希望深入网络编程的开发者均有参考价值。1. 网络数据包嗅探分析器到底在抓什么从一次“丢包悬案”说起线上服务偶发超时日志里干干净净应用层指标也正常可用户就是偶尔卡那么一下。这种问题最折磨人因为你看的地方根本没错——错的是你压根没看到那一层。网络数据包嗅探分析器要解决的就是把这层“看不见的流量”摊开给你看谁在什么时候、往哪个地址、发了多大的包、TCP 握手有没有重传、DNS 解析是不是慢在了半路。用 .NET C# 来做这件事好处是你能把抓包、解析、统计、可视化全塞进一个自己可控的进程里不用在多个工具之间来回倒腾。这篇面向的是已经会写 C#、想自己动手做流量观测工具的开发者尤其是做后端排障、内网监控、协议调试的那批人。下面从原理到能跑起来的代码一层层拆。2. 抓包原理与 .NET 技术选型为什么不能只用 Socket2.1 从网卡到用户态数据包到底经过了几道手要抓包先得搞清楚一个包从网线进来之后走了哪条路。网卡收到帧触发中断驱动把数据写进内核的环形缓冲区协议栈再逐层解析。普通 Socket 只能拿到“属于本进程某个连接”的、已经被内核处理完的载荷TCP 的重传、乱序、重复包这些信息在 Socket 层已经被抹平了。而嗅探要的是链路层原始帧这就必须绕过协议栈直接跟驱动打交道。在 Windows 上这条路径常见有三条一是 WinPcap 的继任者 Npcap它通过 NDIS 轻量过滤驱动把帧复制到用户态二是原始套接字Raw Socket绑定到具体网卡后能收到 IP 层及以上的包但拿不到以太网头且对回环流量支持差三是 Windows Filtering PlatformWFP的 callout 驱动能力最强但开发成本高。做分析器绝大多数场景选 Npcap 就够了它成熟、文档多、.NET 封装也现成。在 Linux 上则是 AF_PACKET 原始套接字配合 PACKET_MMAP 做零拷贝环形缓冲性能比 Windows 那套还猛。跨平台库一般会同时封装这两条路径你在代码里只面对统一的接口。2.2 SharpPcap 与 PacketDotNet一对必须认识的组合.NET 生态里做抓包绕不开两个库SharpPcap 负责“抓”PacketDotNet 负责“解”。SharpPcap 是对 libpcap/Npcap 的托管封装提供设备枚举、打开设备、设置过滤、收包回调PacketDotNet 把收到的原始字节解析成 EthernetPacket、IPv4Packet、TcpPacket 这样的对象树你直接读属性就行不用自己按偏移量抠字节。选型上有个常见误区有人觉得“我自己解析字节更可控”。真做过就知道协议头长度可变、选项字段、VLAN 标签嵌套、IPv6 扩展头这些边界情况能把你拖垮。用成熟解析库把精力放在业务逻辑上才是正路。当然如果你要做的是超高性能的专用解析器那另说但那是另一个量级的工程。安装这两个库用 NuGet 即可dotnet add package SharpPcap dotnet add package PacketDotNet注意 Npcap 本身要单独装安装时勾选“WinPcap API 兼容模式”否则 SharpPcap 找不到设备。这一步翻车的人特别多装完库跑起来报“no devices found”八成就是驱动没装或没勾兼容。2.3 最小可运行抓包程序20 行拿到第一个包先别急着做分析器把“能收到包”这件事跑通。下面这段代码枚举网卡、打开第一个可用设备、抓 10 个包并打印概要using SharpPcap; using PacketDotNet; // 枚举所有可抓包的网卡设备 var devices CaptureDeviceList.Instance; if (devices.Count 0) { Console.WriteLine(没有找到可用设备检查 Npcap 是否安装); return; } // 取第一个设备实际项目里应该让用户选或按名称匹配 var device devices[0]; Console.WriteLine($使用设备: {device.Name} - {device.Description}); // 打开设备ReadTimeout 设为 1000ms避免无限阻塞 device.Open(new DeviceConfiguration { Mode DeviceModes.Promiscuous, // 混杂模式抓经过网卡的所有帧 ReadTimeout 1000 }); // 注册收包回调 device.OnPacketArrival (sender, capture) { var rawPacket Packet.ParsePacket(capture.GetPacket().LinkLayerType, capture.GetPacket().Data); var ip rawPacket.ExtractIPv4Packet(); var tcp rawPacket.ExtractTcpPacket(); if (ip ! null tcp ! null) { Console.WriteLine(${ip.SourceAddress}:{tcp.SourcePort} - ${ip.DestinationAddress}:{tcp.DestinationPort} $len{ip.TotalLength}); } }; device.StartCapture(); Console.WriteLine(抓包中按回车停止...); Console.ReadLine(); device.StopCapture(); device.Close();逻辑说明DeviceModes.Promiscuous让网卡接收所有经过的帧而不只是发给本机的ReadTimeout控制底层读取的超时设太小会频繁空转设太大停止响应慢。Packet.ParsePacket的第一个参数是链路层类型必须从抓到的包对象里取不能写死成 Ethernet否则在回环或某些虚拟网卡上会解析失败。ExtractT是递归查找即使包里有 VLAN 标签也能找到内层 IP。参数上混杂模式在交换网络里其实抓不到别人的流量除非你接在镜像口或集线器上这点后面避坑章节会细说。3. 协议解析与流量统计把原始字节变成能看的指标3.1 解析到传输层之后该提取哪些字段抓到包只是原料分析器真正的价值在于把包变成指标。我一般会按连接维度聚合每条连接记录这些字段五元组源 IP、源端口、目的 IP、目的端口、协议、包数量、字节数、首包时间、末包时间、TCP 标志位统计SYN/FIN/RST 各多少、重传次数。这些字段足够支撑大部分排障场景。提取时有个细节TCP 的载荷长度不等于 IP 总长度。IP 总长度减去 IP 头长度再减去 TCP 头长度才是真正的应用层数据量。很多人统计流量时直接用ip.TotalLength结果把协议头也算进去了做带宽分析时偏差能到 5% 以上。正确做法是var ip packet.ExtractIPv4Packet(); var tcp packet.ExtractTcpPacket(); if (ip ! null tcp ! null) { // IP 头长度以 4 字节为单位字段值需乘 4 int ipHeaderLen ip.HeaderLength; int tcpHeaderLen tcp.DataOffset * 4; int payloadLen ip.TotalLength - ipHeaderLen - tcpHeaderLen; // 标志位判断用于识别连接建立与拆除 bool isSyn tcp.Synchronize; bool isFin tcp.Finished; bool isRst tcp.Reset; // 累加到连接统计对象伪代码实际用字典按五元组索引 // stats.AddBytes(srcIp, srcPort, dstIp, dstPort, payloadLen); }DataOffset是 TCP 头长度字段单位也是 4 字节忘了乘 4 是新手最常见的错算出来的载荷长度会莫名其妙偏大。3.2 用字典做连接聚合并发下的坑聚合连接最自然的做法是ConcurrentDictionary键用五元组拼成的字符串或自定义结构。但这里有个性能陷阱如果每个包都拼字符串当键高流量下 GC 压力会非常大。我一般用一个实现了IEquatable的 struct 做键避免装箱和字符串分配。public readonly struct FlowKey : IEquatableFlowKey { public readonly uint SrcIp, DstIp; public readonly ushort SrcPort, DstPort; public readonly byte Protocol; public FlowKey(uint sIp, uint dIp, ushort sPort, ushort dPort, byte proto) { SrcIp sIp; DstIp dIp; SrcPort sPort; DstPort dPort; Protocol proto; } public bool Equals(FlowKey other) SrcIp other.SrcIp DstIp other.DstIp SrcPort other.SrcPort DstPort other.DstPort Protocol other.Protocol; public override int GetHashCode() HashCode.Combine(SrcIp, DstIp, SrcPort, DstPort, Protocol); }用 struct 做键字典查找时不会产生堆分配。HashCode.Combine在 .NET Core 之后是标准做法比手写异或分布更均匀。注意 IP 地址要转成 uint 存储别存字符串否则每包一次字符串比较吞吐直接掉一个数量级。3.3 统计指标怎么算才有参考价值光有总量没用得算比率。我常看的几个指标重传率重传包数 / 总包数、RST 比例RST 包数 / 连接数、平均包大小、连接持续时间分布。重传率超过 1% 基本说明链路有质量问题RST 比例高往往是服务端主动拒绝或端口没监听。重传的判定比较讲究。严格来说要看 TCP 序列号是否重复且载荷非空但简化做法是看同一个连接里是否有相同序列号的包再次出现。下面是个简化判定// 记录每个连接已见过的序列号范围判断是否重传 // 实际实现建议用滑动窗口这里只示意逻辑 bool IsRetransmission(FlowKey key, uint seq, int payloadLen) { if (payloadLen 0) return false; // 纯 ACK 不算重传 if (_seenSeq.TryGetValue(key, out var lastSeq)) { // 序列号回退且带载荷大概率是重传 if (seq lastSeq) return true; } _seenSeq[key] seq (uint)payloadLen; return false; }这个判定不完美遇到乱序会误判但对排障够用。要做精确判定得维护接收窗口和选择性确认SACK信息复杂度上一个台阶除非你做的是专业级分析工具否则没必要。4. 避坑与常见问题排查那些让我熬夜的瞬间4.1 抓不到任何包设备列表还是空的现象程序启动后CaptureDeviceList.Instance.Count为 0或者打开设备直接抛异常。原因Npcap 没装或者装了但没勾选 WinPcap 兼容模式SharpPcap 的底层调用找不到入口。解决重装 Npcap安装向导里明确勾选“Install Npcap in WinPcap API-compatible Mode”。另外程序必须以管理员权限运行否则打不开网卡。这个坑我踩过不止一次明明代码没问题就是权限不够。4.2 混杂模式开了却抓不到别人的流量现象设了DeviceModes.Promiscuous但只能看到本机进出的包。原因现在几乎全是交换机网络交换机会根据 MAC 地址表把帧只转发给目标端口混杂模式在交换机面前无效。解决要么在交换机上配置端口镜像把目标流量复制到你接的端口要么在目标主机本机抓。指望软件层面“偷看”整个局域网在交换网络里是不成立的这是物理层的事代码解决不了。4.3 高流量下丢包严重统计数字对不上现象千兆流量跑满时抓到的包数明显少于实际重传率算出来偏高。原因收包回调里做了太多事比如每包都写日志、拼字符串、做复杂计算导致内核缓冲区溢出。解决回调里只做最轻量的入队操作把解析和统计放到独立线程消费。用BlockingCollection或Channel做生产者消费者模型// 回调只入队不做解析 var queue new BlockingCollectionRawCapture(boundedCapacity: 10000); device.OnPacketArrival (s, e) { if (!queue.TryAdd(e.GetPacket(), 0)) // 满了直接丢避免阻塞回调 { Interlocked.Increment(ref _dropped); } }; // 独立线程消费 Task.Run(() { foreach (var raw in queue.GetConsumingEnumerable()) { // 在这里做解析和统计 } });boundedCapacity要设不设的话内存会被打爆。满了就丢并计数丢包数本身也是个重要指标说明你的处理能力跟不上流量该优化或上多线程了。4.4 解析 IPv6 或带 VLAN 的包时崩溃现象遇到某些包ExtractIPv4Packet()返回 null或者解析出的端口是乱码。原因包里带了 VLAN 标签802.1Q或者根本就是 IPv6。ExtractIPv4Packet在 VLAN 场景下有时找不到内层需要先剥 VLAN。解决用ExtractVlanPacket()判断或者直接用Packet.ParsePacket后逐层判断类型。更稳妥的做法是先判断ip.Version分别处理 IPv4 和 IPv6别假设全是 IPv4。4.5 程序跑久了内存一直涨现象运行几小时后内存占用从几十兆涨到几个 G。原因连接统计字典只增不减长连接和短连接混在一起短连接结束后条目没清理。解决给每个连接条目加最后活跃时间起一个后台任务定期清理超过阈值比如 5 分钟没更新的条目。别指望 GC 自动回收字典持有引用GC 动不了。// 定期清理过期连接 var now DateTime.UtcNow; var expired _flows.Where(kv (now - kv.Value.LastSeen).TotalMinutes 5) .Select(kv kv.Key).ToList(); foreach (var key in expired) _flows.TryRemove(key, out _);清理间隔和过期阈值要按你的场景调内网短连接多就设短点长连接监控就设长点。5. 进阶技巧把分析器做成能长期用的工具做到能抓、能统计只是及格线。真正让一个嗅探分析器有价值的是“能持续跑、能定位问题、能验证结论”。分享几个我实际用下来最管用的技巧。第一加一个“慢连接”检测。记录每个 TCP 连接从 SYN 到首个数据包的时间超过阈值比如 500ms就标记出来。很多“服务慢”的问题其实是 TCP 握手阶段就慢了应用层日志根本看不到。这个指标一加定位效率翻倍。第二把统计结果定期落盘成时序数据而不是只打印到控制台。用简单的 CSV 追加就行字段包括时间戳、总包数、总字节数、重传数、活跃连接数。跑一天下来用表格软件画个图流量规律一目了然。别小看这个很多偶发问题就是靠长期趋势看出来的。第三验证你的分析器本身准不准。方法很简单用ping发 100 个包看你的程序统计到的是不是 100 个用curl下载一个已知大小的文件看统计的字节数和文件大小是否吻合考虑协议头开销。如果对不上先怀疑自己的解析逻辑别急着怀疑网络。我见过有人统计出来的流量比实际大 20%查了半天发现是把以太网帧的填充字节也算进去了。第四过滤器要用起来。SharpPcap 支持 BPF 过滤表达式在打开设备时设置能让内核层就过滤掉不关心的流量大幅降低用户态压力// 只抓 80 端口的 TCP 流量BPF 语法 device.Filter tcp port 80; // 或者排除本机回环噪音 device.Filter not host 127.0.0.1;BPF 过滤在内核执行比抓到用户态再判断高效得多。表达式语法和 tcpdump 一致会 tcpdump 就会这个。最后说个习惯每次改完解析逻辑我都会拿一个固定的、已知内容的抓包文件pcap 格式回放一遍对比输出是否和预期一致。SharpPcap 支持从文件读取CaptureFileReaderDevice就是干这个的。有了回归测试才敢放心改代码不然每次改动都像开盲盒。这个习惯帮我省了无数个排查“为什么昨天还好好的”的夜晚。希望帮到你。本文还有配套的精品资源点击获取