C#按指定端口抓取TCP/UDP数据包:从原理到SharpPcap实战

发布时间:2026/10/11 2:58:45
C#按指定端口抓取TCP/UDP数据包:从原理到SharpPcap实战 简介这是一份基于C#实现的网络数据包抓取工具项目面向网络开发人员、运维工程师及安全分析人员用于在Windows环境下捕获并解析IP、TCP、UDP协议数据包。工具支持指定端口监听可提取源/目标IP、端口号、TCP序号、确认号及UDP长度等关键字段并以可读形式在界面中展示适合网络协议学习和日常抓包排障。压缩包共82个文件大小仅1.12MB。其中包含24个C#源码文件、3个可执行exe、2个解决方案文件以及resx、resources、png、ico等界面资源与图标同时保留pdb调试信息和构建日志便于直接运行或二次开发。该项目已有738人学习下载。通过研读源码可以掌握C#套接字编程、TCP/UDP头部解析、按端口过滤数据包、结果展示与导出等实用技能同时获得一个可直接运行的抓包小工具。进一步还可理解Socket方式与WinPcap/Npcap抓包的异同帮助提升网络调试和安全分析能力。1. C# 抓取网络数据包从“黑匣子”到“看得见”的流量分析很多做网络通信开发的同行都遇到过这种尴尬自己写的 TCP/UDP 程序连上了、发数据了但对方到底收没收到、报文格式对不对、端口通不通全凭猜测。抓包工具用了不少可一旦要按端口过滤、要拿到数据负载做二次处理通用工具就变得不够用了。C# 抓取 IP、TCP、UDP 网络数据包这个资源解决的就是这个问题——用托管代码直接接管网卡流量按指定端口过滤把原始报文解析成你能看懂的结构体甚至能直接集成进你自己的通信调试工具里。适合正在做 Socket 通信、上位机、协议调试或内网监控且不想为了抓包专门切到 C 生态的开发者。它补的正是“能跑但看不见”这块短板让抓包从玄学变成可复现的操作。2. 抓包技术选型为什么偏选 C# 来做网络数据包捕获2.1 托管代码抓包的三种主流路线RAW Socket、SharpPcap、驱动层C# 里要抓网络包摆在面前的无非三条路。外貌上差异不大但实际用起来体验完全不同。第一种是 RAW Socket也就是 Socket 类型设为 Raw加上 AddressFamily.InterNetwork、SocketType.Raw、ProtocolType.IP。这条路的好处是纯托管代码不依赖第三方库部署简单。但坑也很明显Windows 上 RAW Socket 的收包能力受系统限制抓不到非 IP 协议帧而且收到的包少了以太网头某些场景下数据不全只适合快速验证。第二种是 SharpPcap封装了 libpcap/WinPcap 的 C# 库也是目前 C# 抓包事实上的标准选择。它能直接拿到链路层原始帧支持过滤表达式还能抓环回流量。代价是需要随包分发一个驱动Windows 上是 npcap首次部署时用户容易被驱动安装劝退。第三种是直接通过 P/Invoke 调底层驱动接口比如调用 Packet.dll 或 npcap 的 API。这条路定制性最强但代码量大几乎没有团队愿意纯手工做这件事。结论很直接实际项目里大部分人是选择 SharpPcap 做主力RAW Socket 做快速验证。比如这个 C# 抓包资源主干逻辑也是基于 SharpPcap 的封装思路把设备列表、过滤规则、包解析三件事拆成了模块方便直接拿去做二次开发。2.2 指定端口过滤BPF 语法在 C# 里的正确用法抓包不是把网卡上所有流量都捞起来——生产环境流量大全收下来要么磁盘爆掉要么程序卡死。所以按指定端口过滤不是可选项而是刚需。BPFBerkeley Packet Filter语法在 SharpPcap 里直接以字符串形式传给设备常见写法device.Filter tcp port 8080;这行代码表示只捕获 TCP 协议且目的或源端口为 8080 的报文。如果想同时抓 TCP 和 UDP 的 8080 端口写法是device.Filter tcp port 8080 or udp port 8080;逻辑说明BPF 字符串传给底层 libpcap 后会编译成内核级过滤指令在数据包进入应用层之前就完成筛选效率远高于“全抓下来再在 C# 里判断端口”。参数说明port 默认匹配源或目的端口如果想固定方向用 src port 或 dst port如果还要限制协议族前缀加 ip 或 ip6比如 ip src host 192.168.1.100 and tcp dst port 443。这里有一个容易忽略的点BPF 里 “tcp port 8080” 只能匹配标准端口字段无法匹配 TCP 负载里的自定义端口号比如某些协议把真实端口放在负载首部。遇到这种场景要么把 port 过滤去掉、全量捕获后在 C# 层做负载解析要么在过滤器里用 offset 偏移匹配负载字节。后者写法复杂我一般会直接走应用层二次筛选。2.3 抓到包之后从原始字节到协议结构体的解析过程过滤器给你的是“包”但你要的是“数据”。原始字节从网卡拿到后是个 byte[]长度为帧长通常 14 字节以太网头 20 字节 IP 头 协议头 负载。要把“字节”变成“人话”得手动做一次拆包。以最常见场景——UDP 指定端口抓包为例代码骨架// 假定 Ethernet 头已剥离data 是 IP 层起始位置 int ipHeaderLength (data[0] 0x0F) * 4; // IP 头长度是首部长度字段低 4 位乘以 4 bool isUdp data[9] 17; // IPv4 协议字段17 表示 UDP if (isUdp) { int udpHeaderStart ipHeaderLength; ushort srcPort (ushort)((data[udpHeaderStart] 8) | data[udpHeaderStart 1]); ushort dstPort (ushort)((data[udpHeaderStart 2] 8) | data[udpHeaderStart 3]); ushort udpLength (ushort)((data[udpHeaderStart 4] 8) | data[udpHeaderStart 5]); byte[] payload new byte[udpLength - 8]; Array.Copy(data, udpHeaderStart 8, payload, 0, payload.Length); Console.WriteLine(${srcPort} - {dstPort}, payload {payload.Length} bytes); }逻辑说明IP 头里第 0 字节的低 4 位记录头部长度单位是 4 字节所以先算出实际 IP 头长度第 9 字节是上层协议号UDP 是 17、TCP 是 6。UDP 头固定 8 字节前 4 字节拆出源端口和目的端口第 4~5 字节是 UDP 总长度减去 8 字节 UDP 头就得到负载长度。参数注意这里的 udpLength 是整个 UDP 数据报长度不包含 IP 头如果报文分片了这个值拿到的是分片后的长度真实负载长度要以 IP 头 Total Length 减去 IP 头部长度为准。踩过坑的都知道分片报文按 udpLength 去截数组大概率越界。3. 按指定端口抓取 TCP 与 UDP 数据核心实现拆解3.1 SharpPcap 的 SharpPcapDevice 事件驱动模型与回调用法SharpPcap 的抓包模型是事件驱动的把“收到包”这件事暴露成事件你只需要注册回调不用自己管理线程和缓冲区。核心代码using SharpPcap; using PacketDotNet; var devices CaptureDeviceList.Instance; if (devices.Count 0) { Console.WriteLine(未找到网卡设备请检查 npcap 驱动是否安装); return; } var device devices[0]; // 实际项目中建议让用户选择 device.OnPacketArrival OnPacketArrivalHandler; device.Open(new DeviceConfiguration { Mode DeviceModes.Promiscuous, // 混杂模式抓取所有经过网卡的包 ReadTimeout 1000 }); device.Filter tcp port 8080 or udp port 8080; device.StartCapture();回调函数里拿到的是原始报文封装对象需要通过 PacketDotNet 或者手动解析才能得到可读字段。逻辑说明OnPacketArrival 每次事件触发携带一个 PacketCapture 对象其中 Data 属性是字节数组Header 里记录了捕获时间、帧长度等信息。参数说明DeviceModes.Promiscuous 让网卡不过滤目的 MAC抓到所有经过的包这对交换机镜像端口抓包尤其重要ReadTimeout 单位是毫秒直接影响历史缓冲区读取频率——设太短 CPU 空转设太长丢包率上升一般 500~1000 毫秒是折中值。3.2 事件回调里做协议解析TCP 标志位、UDP 负载、IP 分片处理回调只是入口重头戏在解析。TCP 和 UDP 的解析路径不同而且 TCP 要额外看标志位——SYN、ACK、FIN、RST 直接决定连接状态判断的准确性。private static void OnPacketArrivalHandler(object sender, PacketCapture e) { var rawPacket e.GetPacket(); var packet PacketDotNet.Packet.ParsePacket(rawPacket.LinkLayerType, rawPacket.Data); var ipPacket packet.ExtractIPPacket(); if (ipPacket null) return; var tcpPacket packet.ExtractTcpPacket(); if (tcpPacket ! null) { bool syn tcpPacket.Syn; // 握手第一步 bool ack tcpPacket.Ack; // 确认标志位 bool rst tcpPacket.Rst; // 连接重置 bool fin tcpPacket.Fin; // 正常关闭 Console.WriteLine($[TCP] {ipPacket.SourceAddress}:{tcpPacket.SourcePort} - ${ipPacket.DestinationAddress}:{tcpPacket.DestinationPort} $SYN{syn} ACK{ack} RST{rst} FIN{fin}); return; } var udpPacket packet.ExtractUdpPacket(); if (udpPacket ! null) { byte[] payload udpPacket.PayloadData; // 注意这是一个副本可以直接修改不影响原始包对象 Console.WriteLine($[UDP] {ipPacket.SourceAddress}:{udpPacket.SourcePort} - ${ipPacket.DestinationAddress}:{udpPacket.DestinationPort} $payload{payload.Length} bytes); } }逻辑说明PacketDotNet 库帮你省去了手工偏移计算的痛苦Extract 泛型方法直接返回对应协议对象。TCP 的 Syn/Ack/Rst/Fin 都是布尔属性直接从 TCP 头标志位映射。说明这里用 Extract 拿 TCP再拿 UDP因为一个包不可能同时是 TCP 和 UDP所以两个分支是排他的不会重复打印。参数说明ipPacket.SourceAddress 返回的是 IPAddress 对象直接 ToString 就是点分十进制字符串UdpPacket.PayloadData 返回的是原始字节引用还是副本取决于 PacketDotNet 版本老版本是切窗引用新版本多为副本——如果要缓存报文内容做分析建议显式加一行 Array.Copy 做深拷贝避免后续回调修改同一段内存造成脏数据。3.3 多端口监听把“指定端口”从单个扩展成一组端口需求再往前走一步——不只要监听 8080还要监听 443、3306、6379怎么办BPF 语法支持逻辑或组合var portList new[] { 8080, 443, 3306, 6379 }; // 这里把端口列表拼成 BPF 表达式 string bpfFilter string.Join( or , portList.Select(p $tcp port {p})); device.Filter bpfFilter;逻辑说明Select 将每个端口生成一段 “tcp port 数字” 的过滤子句string.Join 用 “ or ” 连接最终得到形如 “tcp port 8080 or tcp port 443 or tcp port 3306 or tcp port 6379” 的完整过滤串。参数说明这种写法只过滤 TCP如需同时抓 UDP把每个元素改成 $(tcp port {p} or udp port {p})并用 or 连接——注意加括号优先级。如果端口是可变的每次修改 Filter 后旧端口无残留但 Filter 属性赋值前建议先 StopCapture改完再 Start否则部分驱动不刷新。这套多端口方案做内网服务巡检特别合适。比如公司内部有十几个服务和若干端口把它们写成配置项从 appsettings.json 读进来动态构造 BPF就能一个进程盯住所有关键服务。我在一个模拟项目X上就是这么干的——用这个方案替代了原先 4 个控制台进程内存占用少了 60%。4. 指定端口抓包的工程化落地从“能抓”到“稳定抓”4.1 数据缓存与批量落盘异步写文件的正确姿势抓包程序跑起来容易跑稳难。最常见的问题抓包回调里直接 File.WriteAllBytes结果 IO 阻塞把抓包线程卡死丢包率直线上升。正确做法是回调只负责任务分发写盘交给独立线程。private ConcurrentQueuebyte[] _packetQueue new ConcurrentQueuebyte[](); // 事件回调只入队不写盘 private void OnPacketArrivalHandler(object sender, PacketCapture e) { var rawPacket e.GetPacket(); _packetQueue.Enqueue(rawPacket.Data); } // 独立写盘线程 private async Task FlushPacketsAsync() { while (!_cancellationRequested) { var batch new Listbyte[](); while (_packetQueue.TryDequeue(out var data)) { batch.Add(data); if (batch.Count 500) break; // 攒够 500 包写一次 } if (batch.Count 0) { await File.AppendAllBytesAsync(_pcapOutputPath, BuildPcapBlock(batch)); } else { await Task.Delay(200); // 队列空的休眠减少空转 } } }逻辑说明ConcurrentQueue 保证多线程入队安全回调线程只做 Enqueue写盘线程批量 Dequeue。参数说明500 这个批量阈值不是拍脑袋——接近默认 TCP 缓冲区大小写出的文件块大小合理且单次 IO 控制在毫秒级200ms 的空转延迟在流量低时节省 CPU流量高时队列不会积压太久。BuildPcapBlock 是自定义封装负责把原始字节按 Pcap 格式拼成可被 Wireshark 识别的块。细节点如果对抓包完整性和顺序有要求可以在 Pcap 块里附带从 e.Header 取出的时间戳用 微秒 精度记录方便事后回放对齐时序。4.2 流量统计仪表盘按分钟统计 TCP/UDP 数据包数量与字节数抓包如果只是落盘查询还得靠 Wireshark不够直接。做一套轻量统计模块按分钟汇总各端口流量走势一眼就能看出来。private class PortStats { public long PacketCount; public long ByteCount; } private ConcurrentDictionaryint, PortStats _statsMap new(); private void UpdateStats(int port, int byteCount) { var stats _statsMap.GetOrAdd(port, _ new PortStats()); Interlocked.Increment(ref stats.PacketCount); Interlocked.Add(ref stats.ByteCount, byteCount); } // 定时 10 秒打印一次当前累计值 private void DumpStats() { foreach (var kv in _statsMap.OrderByDescending(k k.Value.ByteCount).Take(10)) { Console.WriteLine($端口 {kv.Key}: {kv.Value.PacketCount} 包, {kv.Value.ByteCount} 字节); } }逻辑说明GetOrAdd 保证端口第一次出现时创建统计对象Interlocked 系列方法保证多线程环境下计数原子性。打印时按字节数倒序取前 10 名先看到谁最占流量。这段代码的现实价值在于端口级流量排名一出来瞬间就能定位异常会话——比如 443 端口突然从每分钟几百包涨到几万包不用看包内容就知道有问题。4.3 指定端口抓包的四个典型翻车场景与排查路径排错部分如果只看操作不做笔记换台机器十有八九再翻一次车。以下是几个高频问题。现象 1Filter 设了 “tcp port 8080”但一个包都没抓到。原因通常是过滤器字符串语法错误或者驱动的 NPF 服务没启动。解决先不设 Filter 抓几个包确认链路通再逐步加上 Filter 测试Windows 上检查 npcap 服务是否已在系统管理器中运行。现象 2混杂模式打开了但只能抓到本机 IP 的上行包。原因大概率是交换机端口限制了广播域非本机流量根本没到达网卡。解决确认网卡是否接在镜像端口条件下这是一条血泪经验——软件没问题是网络环境的问题。现象 3程序运行 10 分钟内存涨到几百 MB 还不回落。原因是对回调中 PacketDotNet 的 ParsePacket 结果做了缓存但没释放底层字节缓冲区或者入队速度大于写盘速度造成队列积压。解决优先检查入队与落盘速度是否匹配增加批量阈值或调高写盘频率并确保缓存只在处理周期内保留。现象 4开发机器上能抓到回环包拿到服务器上就抓不到了。原因是回环抓包依赖 Npcap Loopback Adapter部分服务器环境只装了基础驱动。解决单独安装 Npcap并在设备列表里确认是否存在 “Npcap Loopback Adapter” 虚拟网卡。若找不到只能通过 Raw Socket 变通抓回环包但 RAW Socket 的解析路径与 SharpPcap 完全不同。5. 避坑专题C# 抓包从开发到投产的九个高频坑位5.1 使用 PacketDotNet 时先剥掉 Ethernet 头分片包长度别信 UDP 头PacketDotNet 的 Extract 方法帮你隐藏了很多底层细节但它并不能消除所有坑——最典型的一个IP 分片。当一个 UDP 数据报超过 MTU通常 1500 字节被拆成多个 IP 分片时第一个分片里有 UDP 头后续分片里没有。PacketDotNet 的 Extract 对分片包的行为在不同版本里不一样有的会返回带 UDP 头的内容有的不会。现象就是你要解析的 UDP 负载有时能取到有时取到的是分片序号直接把 payload 当成真实数据去解析会得到一堆 0。原因不只是 PacketDotNet 的解析逻辑更核心的是 IPv4 协议本身分片的偏移字段Fragment Offset标识了这个分片在原始报文中是第几块。解决在使用 Extract 之前先检查 IPPacket 的 FragmentOffset 字段是否大于 0如果大于 0 则跳过负载解析只记账不解析内容。这在做大流量抓包时尤为重要。var ipPacket packet.ExtractIPPacket(); if (ipPacket null) return; // 检查分片偏移非零表示这是一个分片包负载不可信 if (ipPacket.FragmentOffset 0) { // 只打印基本信息不解析负载 return; }还有一个容易踩的点TCP 分片和 UDP 分片处理逻辑不同——TCP 分段由 MSS 控制通常在协议栈就处理过了应用层很少碰到但 UDP 分片是纯网络层行为谁也绕不开。所以 UDP 抓包一定要把分片判断放在解析负载之前否则你打印出来的 74 字节“真实负载”可能是第二分片的 28 字节数据加上一堆长度填充。5.2 现象 → 原因 → 解决Filter 不生效、抓包线程死锁、丢包率爆表过滤表达式写对了、驱动也正常、代码看起来没毛病但实际一跑结果不对。多总结几条供参考。第一个高频现象是 Filter 不生效——设了端口过滤后事件回调里仍然收到大量非指定端口报文。原因在 Open 之前设置 Filter或者设备已在 StartCapture 状态时修改 Filter 但未重新应用。解决把 Filter 赋值放到 Open 之后、StartCapture 之前如果运行中改端口先 StopCapture、重新赋值、再 StartCapture。顺序一错过滤器就是一个摆设。第二个现象是程序关闭时直接挂起。原因事件回调正拿着设备对象做数据拷贝主线程这边直接 device.Close()导致底层驱动同步阻塞。解决关闭前先 StopCapture然后等待 1~2 秒给回调一个结束周期再 Close。顺序不可颠倒。第三个现象是忙时丢包率爆表高峰丢 40% 以上。原因回调内做了复杂解析和 Console.WriteLine串行阻塞了读取线程缓冲区溢出自动丢包。解决Console 输出改为批量打印每 100 个包打一行摘要或者把输出丢给独立队列异步处理。吞吐优先是抓包程序的第一定律日志和统计全部让路。5.3 RAW Socket 是不是万能的哪些场景必须换回 SharpPcap有些同行用 RAW Socket 抓包觉得不依赖外部驱动、部署简单。RAW Socket 能抓到 IP 层以上的包在 Windows 上协议类型设为 IP 后能收到 TCP/UDP 报文。但限制有三个致命点拿不到以太网头意味着你丢掉了源 MAC 和目的 MAC无法做链路层分析Windows 限制 RAW Socket 收包不可靠部分系统版本下收不到 TCP 包只能收到 ICMP回环流量基本抓不到。如果工作场景只要求“确认某端口有没有流量、负载大致是什么”RAW Socket 可以做快速验证。但如果要做协议分析、重复回放、流量审计、性能统计那就必须 SharpPcap——它提供完整链路层帧、时间戳、BPF且驱动稳定。选型就看一件事你需要的是一双眼睛还是一个完整的记录仪。5.4 大流量下抓包线程和内存双双失控限流与降级要早做流量上来后内存问题浮出水面。常见做法是给队列设上限private const int MaxQueueSize 10000; if (_packetQueue.Count MaxQueueSize) { _packetQueue.Enqueue(data); } else { _droppedPackets; }这个逻辑的意义在于队列满时丢新包保进程存活而不是让内存无限制涨下去直到 OOM。量子化的体现是丢多少包你需要知道但进程不能死。参数说明10000 是经验值——如果每包平均 1KB内存缓冲约 10MB配合批量落盘不会造成明显抖动。适当调实验抓包机器内存充足可以放到 50000嵌入式设备调 2000 都行。这是保命设计不是性能优化。6. 进阶用法把抓包引擎做成服务化组件集成进任意 C# 通信项目做到这一步套路都熟了但离真正的生产级还差一个结构设计——整套抓包逻辑很适合封装成一个独立的抓包服务类PacketCaptureService对外只暴露 Start、Stop、AddPortListener、OnPacketReceived 事件。这样就不必每次新项目都复制粘贴代码而是把这个组件当成一个业务模块随时集成进上位机或网络监控项目。public class PacketCaptureService : IDisposable { private ICaptureDevice _device; public event ActionPacketMeta OnPacketReceived; public void Start(string filter, int deviceIndex 0) { _device CaptureDeviceList.Instance[deviceIndex]; _device.OnPacketArrival (s, e) { var meta ParsePacket(e.GetPacket()); OnPacketReceived?.Invoke(meta); }; _device.Open(new DeviceConfiguration { Mode DeviceModes.Promiscuous, ReadTimeout 500 }); _device.Filter filter; _device.StartCapture(); } public void Stop() { _device?.StopCapture(); _device?.Close(); } public void Dispose() Stop(); }这里 ParsePacket 返回的是自定义的 PacketMeta包含时间戳、源/目的 IP、端口、协议类型、负载副本。外部项目引用后只需要三行代码接入var service new PacketCaptureService(); service.OnPacketReceived meta Console.WriteLine(${meta.TimeStamp:T} {meta.Protocol} {meta.SrcIp}:{meta.SrcPort} - {meta.DstIp}:{meta.DstPort}); service.Start(tcp port 8080 or udp port 8080);把这个组件集成进通信类的调试工具后开发期间能实时看到每个业务报文的到达时间与端口分布上线后又能变成一个被动监控旁路——不占用业务连接不影响性能出了问题随手翻包定位。验证方式上我习惯的做法是开两个本地 Socket 互发数据一个发送一个接收同时在抓包服务里观察端口流量数值和实际收发数量是否对得上。等到对不上的时候查的方向就清晰了要么发送端没发出去要么接收端没读到要么中间链路丢了。这套方法我现在每到一个新项目都会用同样的步骤验证一遍抓包模块的可用性确认数据和业务侧一致了才继续往下走。从那以后我每次调试通信程序都强制自己先跑一遍抓包确认链路层和端口层的数据事实再谈上层逻辑少走了很多弯路希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询