
简介一项面向网络与Java学习者的嗅探器项目资料包围绕基于JavaJnetpcap的网络抓包程序设计与实现适合作为毕设、课程设计或工程实训参考。项目完整实现了网卡选择抓包、五层模型数据包捕获与显示以及从链路层到应用层的逐层包头解析同时支持Ethernet、IP、ARP、ICMP、UDP、TCP、HTTP七种常见协议包的过滤并通过源/目的IP、关键字过滤和基于IPPort的TCP流追踪帮助使用者理解协议栈与流量分析流程。压缩包共72个文件整体约1.85MB包含8个Java源码、24个class编译文件及jnetpcap.jar可直接查看代码结构与运行逻辑14个xml记录项目配置17张jpg图表展示界面与抓包效果另有README.md、txt说明等辅助文档。已有467人学习下载适合需要快速搭建抓包实验、梳理Netpcap调用过程或准备答辩演示的读者。整体目录结构清晰从源码、配置到效果展示一应俱全便于按模块对照学习。1. 网络嗅探器不玄乎由抓包程序讲透 Java 侧协议栈落地网络嗅探器这个题目在 Java 技术栈里属于典型的“看着不难、上手才知深浅”。网上搜一圈抓包 demo 不少但多数要么用旧版 Jpcap 踩兼容性坑要么只做到“能显示十六进制”就没了下文。这次拆的这份基于 JavaJnetpcap 的抓包程序把整条链路做全了网卡选择、混杂模式抓帧、从链路层到应用层的逐层解析、七种常用协议Ethernet、IP、ARP、ICMP、UDP、TCP、HTTP的过滤和字段展示、关键字过滤、基于 IP 与端口组合的 TCP 流追踪、结果保存都覆盖到了。想拿它做毕设打底、课程设计或者工程实训或者想把网络协议栈在代码里完整走一遍的应用型开发者都能在这个框架上直接改。2. 初始化抓包引擎网卡枚举、混杂模式与 BPF 过滤大部分抓包项目翻车翻在初始化这一层的概率比翻在解析高得多。抓包工具的本质是旁路监听它要把网卡设成混杂模式网卡才会把不属于自己的数据帧也一并收上来。而 Java 标准库里的 Socket 只能拿发给本进程的包链路层帧里的 MAC 地址、VLAN 标签、原始以太网类型统统看不到所以纯 Java 写不了真正的嗅探器必须借道 libpcap。2.1 选型逻辑为什么是 Jnetpcap 而不是别的libpcap 是同源的一套底层抓包库Tcpdump、Wireshark 的抓包核心都是基于它实现的。这份资源里的 jnetpcap.jar 就是 libpcap 的 JNI 封装Java 侧负责界面、数据展示和调度底层捕包、过滤器编译、环形缓冲区全交给 C 去处理。这么做的好处是稳你不需要自己处理内核缓冲区丢包、驱动差异这些脏活只要把 Java 侧的回调写好就行。另一个常见做法是直接调 Wireshark 的命令行工具 tshark 生成文本但那属于外部进程调用交互体验差实时过滤也不如把包直接拿进内存里处理来得顺。Jnetpcap 这套方案适合你把抓包逻辑做进自己程序里的场景这也是它适合做毕设和课设的原因——有足够的技术纵深又不会深到半年做不完。还要提醒一句早期日本作者维护过一个 jpcap 库包名是jpcap.*而 JNetPcap 的包名是org.jnetpcap.*。两者都源自 libpcap方法名也像但类名不一致网上搜教程时最容易混。如果发现import jpcap.JpcapCaptor编译不过多半是库用错了对照本项目的 import 路径重新引即可。2.2 第一步拉出设备列表findAllDevs 的返回值不能忽略第一步做网卡发现。Jnetpcap 提供 findAllDevs 方法把所有网卡包括虚拟网卡和环回口列出来供用户在界面上选择。注意这个方法在 Linux 下如果没权限返回的列表会是空的并不是你机器没有网卡。ListPcapIf allDevs new ArrayList(); StringBuilder errbuf new StringBuilder(); int error JpcapCaptor.findAllDevs(allDevs, errbuf); if (error ! 0) { System.out.println(网卡枚举失败 errbuf); return; } for (int i 0; i allDevs.size(); i) { PcapIf dev allDevs.get(i); System.out.println(序号 i 设备名 dev.getName() 描述 dev.getDescription()); }第一个参数是接收结果的集合第二个是错误缓冲区。方法返回 0 表示成功非 0 时错误信息会写进 errbuf。很多初学同学忽略返回值列表为空时还在继续遍历直接抛空指针。练习时建议先只打印不选择确认你能看到无线网卡、有线网卡和 lo 口之后再进下一步。2.3 打开设备混杂模式、snaplen 与 timeout 的取舍选好网卡后调用 openLive这是整个初始化的关键。参数稍有差池后面要么抓不到包要么抓到一堆截断帧。JpcapCaptor captor JpcapCaptor.openLive( allDevs.get(selectIndex).getName(), 65536, // snaplen每帧最多抓多少字节 true, // promisc进入混杂模式 100 // 超时毫秒 ); if (captor null) { System.out.println(打开设备失败请检查权限); return; } captor.setFilter(ip or arp, true);参数逐一说。snaplen 写 65536 表示完整收下整个链路层帧如果偷懒写成 128帧头后面全被截断解析 IP 和 TCP 字段时会看到大量残缺数据。promisc 为 true 才进入混杂模式如果开着程序只看到本机流量多半是这里没设对。timeout 是读取超时100 毫秒对交互式工具比较合适数值太小会频繁空转太大则关闭程序时有明显延迟感。openLive 返回 null 时不只可能是网卡名写错Windows 下未以管理员身份运行、Linux 下普通用户抓包都会走这个分支最好顺手把 errbuf 打出来。setFilter 是 BPF 表达式过滤在驱动层做匹配效率远高于应用层遍历。“ip or arp”的意思是只放行 IPv4 和 ARP 帧进来IPv6、生成树等其它协议直接不进 Java 内存。这个表达式按需写后面 4.1 会专门展开。2.4 抓包循环processPacket 的阻塞本质与退出时机open 完之后进入抓包循环两种常见写法各有场景。nextPacket() 是阻塞读一个包适合写教学片段或“抓满 N 个就停”的统计生产型嗅探器一般用 processPacket它注册一个回调每抓到一个包就执行一次 handler不占主线程的时候特别方便。PcapPacketHandlerString handler (packet, user) - { System.out.println(抓到 packet.size() 字节时间戳 packet.getCaptureHeader().timestampInMillis()); }; captor.processPacket(-1, handler);processPacket 的第一个参数 -1 表示无限抓包直到主动关停如果写 100则是抓到 100 个包后返回。第二个参数是回调接口user 是透传的自定义对象需要给 handler 传上下文时可以用。实际用的时候processPacket 是阻塞式循环要放在独立线程里跑否则界面卡死。程序退出前记得在 finally 里调 captor.close()不释放的话下次再起程序会提示设备被占用。到这里一个能持续接收原始数据帧的引擎就起来了。下一章要做的就是把这些原始帧按协议栈逐层拆开从 14 字节的以太网帧头一直拆到 HTTP 请求行。3. 协议解析链路从 Ethernet 帧头一路拆到 HTTP 文本引擎初始化完成后数据包会以 PcapPacket 的形式不断回调进来。这章拆解七种协议的解析核心思路是逐层剥洋葱链路层先解开拿到类型字段网络层再解开拿到协议号传输层再解开拿到端口最后应用层按文本探测。3.1 以太网帧头14 字节里藏着什么链路层帧头固定 14 字节前 6 字节目的 MAC后 6 字节源 MAC最后 2 字节类型字段EtherType。一般包的 EtherType 是 0x0800IPv4或 0x0806ARP。字段偏移长度说明目的 MAC06 字节接收方物理地址源 MAC66 字节发送方物理地址EtherType122 字节0x0800 IPv40x0806 ARPEthernet eth packet.getHeader(new Ethernet()); if (eth null) return; String dstMac toMac(eth.destination()); String srcMac toMac(eth.source()); System.out.println(目的 MAC: dstMac 源 MAC: srcMac 类型: 0x Integer.toHexString(eth.type()));getHeader 返回的 Ethernet 对象已经把结构体排好不需要自己按偏移量切字节。type() 返回的是网络序转换好的 int直接和 0x0800、0x0806 比较即可。toMac 是你自己封装的方法把 byte[] 的每一位补零、用冒号拼接Jnetpcap 没有直接提供格式化好的 MAC 字符串这步得自己写。3.2 IP 层协议号决定下一层去哪EtherType 为 0x0800 时才存在 Ip4 头。IPv4 首部固定 20 字节里面最重要的几个展示字段是版本号、TTL、协议号和地址。协议号决定了“下一个头是谁”1 是 ICMP6 是 TCP17 是 UDP。这个值在抓包程序里是分流的开关。Ip4 ip packet.getHeader(new Ip4()); if (ip null) return; System.out.println(版本: ip.version()); System.out.println(TTL: ip.ttl()); System.out.println(协议号: ip.protocol()); System.out.println(源 IP: ip.source()); System.out.println(目的 IP: ip.destination());注意一个容易忽略的场景IP 分片。当抓到的包带有分片偏移且不是最后一个分片时上层 TCP/UDP 头并不完整强行解析会得到半截端口号。常见做法是先判断ip.fragmentOffset()不为 0 时只展示 IP 层信息标注“分片包上层未解析”。这样显示逻辑反而更诚实不会把垃圾数据摆到界面上。3.3 TCP/UDP端口、标志位与三次握手的痕迹TCP 头默认 20 字节UDP 头固定 8 字节。Jnetpcap 对这两个头都有现成封装解析代码非常简短。Tcp tcp packet.getHeader(new Tcp()); if (tcp null) return; int srcPort tcp.source(); int dstPort tcp.destination(); int flags tcp.flags(); if ((flags Tcp.TH_SYN) ! 0) System.out.println(SYN 包); if ((flags Tcp.TH_ACK) ! 0) System.out.println(ACK 包);flags 是位掩码Tcp.TH_SYN、Tcp.TH_ACK 这些常量已经定义好按位与判断即可。三次握手的痕迹在这里非常直观第一个包只有 SYN第二个包 SYNACK 同时置位第三个包只有 ACK。做课程设计时可以把握手过程单独拉出来做成状态展示这是评委喜欢看的功能点。UDP 的解析更简单Udp udp packet.getHeader(new Udp())打印 udp.source() 和 udp.destination() 就行。需要记住的是UDP 没有标志位、没有序号所以做 TCP 流追踪时只对 TCP 有意义遇到 UDP 流只能按地址和端口聚合。3.4 HTTP 探测与七协议分流if-else 的顺序不能乱七种协议的分流逻辑看起来是 if-else但顺序有讲究。ARP 一定没有 IP 层所以要先用 EtherType 拦掉ICMP 没有端口所以不能用 TCP/UDP 的解析方法去读。Ethernet eth packet.getHeader(new Ethernet()); if (eth ! null eth.type() 0x0806) { /* ARP 解析分支 */ return; } if (eth ! null eth.type() 0x0800) { Ip4 ip packet.getHeader(new Ip4()); switch (ip.protocol()) { case 1: /* ICMP 分支 */ break; case 6: /* TCP 分支再按端口进入 HTTP 探测 */ break; case 17: /* UDP 分支 */ break; } }HTTP 是文本协议解析方式是在 TCP payload 里做 ASCII 探测。所谓“应用层分析”在抓包程序里其实就是把 payload 转成字符串再读取第一行。byte[] payload packet.getPayload(); if (payload ! null payload.length 0) { String text new String(payload, StandardCharsets.UTF_8); if (text.startsWith(GET ) || text.startsWith(POST ) || text.startsWith(HTTP/)) { System.out.println(text.split(\r\n)[0]); } }这里的边界条件要注意如果抓到的是 TLS 加密流量payload 是一堆二进制直接转 UTF-8 会得到乱码也不会命中 GET/POST 前缀。遇到这种情况合理的处理是标注“加密流量”而不是硬解析。HTTP/3 走的是 UDP 443 端口QUIC光看 TCP 80 端口也会漏后面 5.5 会把这个坑展开说。4. 过滤与 TCP 流追踪关键字命中、四元组分桶与保存抓包程序只“能看”是不够的毕设和工程里更看重的是“能不能在数据里找到想要的东西”。这一章讲过滤、流追踪和保存每一块都有独立的工程决策要做。4.1 两个层面的过滤驱动层 BPF 与应用层二次筛选setFilter 做的是驱动层过滤它在网卡驱动里直接丢弃不匹配的帧不进用户态速度快、不占内存。但它有个特点过滤规则一旦设置没匹配的包永远不会出现在回调里属于“进不来”。应用层过滤则相反所有包都先进内存再按业务条件二次筛选属于“留下来”。过滤方式执行位置优点缺点BPF setFilter网卡驱动层省内存、速度快规则写死难动态改业务层遍历Java 应用层灵活、可交互流量大时开销高常见做法是把两者结合驱动层做粗过滤比如“ip or arp”“tcp port 80 or tcp port 443”把无关协议挡在门外业务层做细过滤比如按源 IP、按关键字、按时间范围。BPF 表达式也不用写太复杂下面几个够覆盖九成场景tcp只放行 TCP 包udp port 53只放行 DNS 流量host 192.168.1.1只放行与指定主机相关的包tcp and not port 22排除 SSH 端口4.2 源 IP/目的 IP/关键字过滤match 函数的边界条件这份资源里实现了“源 IP、目的 IP、及包携带内容的关键字过滤”落到代码上就是一个多条件组合判断函数。写这个函数时最容易漏的是条件为空时的处理逻辑。boolean match(Ip4 ip, byte[] payload, String keyword, String srcIp, String dstIp) { if (srcIp ! null !srcIp.isEmpty() !srcIp.equals(ip.source().getHostAddress())) { return false; } if (dstIp ! null !dstIp.isEmpty() !dstIp.equals(ip.destination().getHostAddress())) { return false; } if (keyword ! null !keyword.isEmpty()) { if (payload null) return false; String body new String(payload, StandardCharsets.UTF_8); if (body.indexOf(keyword) 0) return false; } return true; }逻辑上按顺序做短路判断IP 过滤不匹配直接返回 false不继续做 payload 转换。payload 为空且有关键字条件时直接不通过避免空指针。keyword 用的是 indexOf 而不是 contains是因为旧版本 JDK 的 contains 内部就是 indexOf写起来没有区别但某些环境下 indexOf 重构更直观。性能上有个现实问题每次 match 都 new String 拷贝整个 payload流量大时开销很大。我一般会先做一次 BPF 粗过滤把进入 match 的包控制在每秒钟几百个以内再谈关键字匹配。如果是要做 GB 级流量的离线分析这个函数就不够用了得改成在字节数组上直接做 KMP 或 BM 匹配。4.3 TCP 流追踪四元组归一化与 seq 排序重组TCP 流追踪的核心是“把属于同一次会话的包聚到一起”。一次 TCP 会话的四元组是源 IP、源端口、目的 IP、目的端口。问题在于客户端向服务器发的包和服务器向客户端回的包四元组是镜像的如果不做归一化两个方向的包会分到两个桶里。String flowKey(Tcp tcp, Ip4 ip) { int srcPort tcp.source(); int dstPort tcp.destination(); String src ip.source().getHostAddress(); String dst ip.destination().getHostAddress(); if (srcPort dstPort || (srcPort dstPort src.compareTo(dst) 0)) { return dst : dstPort - src : srcPort; } return src : srcPort - dst : dstPort; }这段代码做了一个方向归一化按端口大小排序端口相同时再按 IP 字符串排序保证两个方向的包计算出的 key 一致。这个技巧在做会话聚合时是通用的不管底层是 Jnetpcap 还是别的库思路都一样。拿到 key 之后用 Map 分桶存储MapString, ListPcapPacket streams new HashMap(); streams.computeIfAbsent(key, k - new ArrayList()).add(packet);这里要提醒两个点。一是无界 Map 会内存泄漏跑上一天几万条流全部攒在内存里程序必挂。常见做法是限制桶数比如超过 5000 条流就丢弃最旧的或者定期清理超过 N 秒没有新包的流。二是存储桶内的包顺序未必是应用层顺序因为 TCP 允许乱序到达。要还原完整会话需要按 TCP 序号排序后再拼接 payload这一步是 TCP 流追踪里真正的“进阶活”6.3 会给思路。4.4 结果落盘文本分析和原始 pcap 两种保存数据保存有两种形态用途完全不同。第一种是保存解析后的分析结果字段用文本或 CSV 写出去方便做报告、画图、交作业。StringBuilder sb new StringBuilder(); for (PcapPacket p : filteredList) { sb.append(p.getCaptureHeader().timestampInMillis()).append(,) .append(srcIp).append(,).append(dstIp).append(\n); } Files.write(Paths.get(result.csv), sb.toString().getBytes(StandardCharsets.UTF_8));第二种是保存原始 pcap好处是之后随时可以用 Wireshark 重新分析不丢现场。Jnetpcap 提供了 JpcapWriter 来做这件事。JpcapWriter writer JpcapWriter.openWriter(captor, export.pcap); writer.writePacket(packet);核心注意点JpcapWriter 必须在捕获器打开之后创建writer 和 captor 的关闭顺序也有讲究先关 writer 再关 captor否则缓冲区里的数据没落盘就没了。文件命名建议带时间戳比如trace-20250101-1530.pcap避免覆盖前一次抓包结果。文本保存则要注意不能每包 flush攒一批写一次IO 开销会小很多。5. 避坑排查Jnetpcap 实战里五个高频翻车点这份资源在调试阶段踩过不少坑我当时把关键问题记成了排错文档。这里挑五个最高频的按“现象、原因、解决”写清楚能让复现的人少走弯路。5.1 抓包一片空白回调收到 0 字节现象程序启动后没有任何输出或者回调触发但包长度为 0数据全空。原因网卡没有真正进入混杂模式或者当前系统权限不够。Windows 下不以管理员身份运行Jnetpcap 打不开网卡Linux 下普通用户没有 raw socket 权限findAllDevs 返回空列表openLive 也会失败。解决Windows 以管理员身份运行 IDE 或终端Linux 用 sudo 启动 Java 进程。更稳妥的方式是给 java 二进制加 capabilities避免每次都要提权。排查时先打印 openLive 失败时的 errbuf它通常会直接告诉你“permission denied”还是设备不存在。5.2 UnsatisfiedLinkErrorjnetpcap native 库没加载现象Java 代码编译通过运行时抛java.lang.UnsatisfiedLinkError: no jnetpcap in java.library.path。原因jnetpcap.jar 只是 Java 侧封装还依赖对应平台的 .dll 或 .so 文件。如果 dll 没被加载或者 32 位 JDK 配了 64 位 dll都会报这个错。Java 的 library path 不会自动包含项目 libs 目录。解决在 IDEA 的运行配置里加 VM options-Djava.library.pathlibs把 dll 所在目录指给 JVM。另一种做法是把 .dll 复制到 JDK 的 bin 目录下省去配置。注意 jar 和 dll 的版本必须配套不能拿 1.x 的 jar 配 2.x 的 dll接口对不上时会在调用处莫名其妙地崩溃。5.3 ICMP 包被当成 TCP/UDP 解析返回 null现象收到 ICMP 包时getHeader(new Tcp()) 返回 null再往下取端口就空指针或者强转之后 ClassCastException。原因ICMP 没有端口号和 seq 字段它的结构是 Type、Code、校验和加 payload。很多同学图省事拿到包先试 TCP 再试 UDP忽略了 ICMP 这个分支。解决解析顺序上先按 EtherType 再按 IP 协议号分流ICMP 单独走一个分支if (ip.protocol() 1) { Icmp icmp packet.getHeader(new Icmp()); System.out.println(ICMP Type: icmp.type() Code: icmp.code()); }养成习惯任何 getHeader 返回 null 都要容忍说明这个包里没有对应协议层而不是程序出错。5.4 端口号显示 20480 而不是 80网络字节序的坑现象明明访问的是 HTTP 80 端口程序却显示目的端口 20480。原因TCP/UDP 端口在网络传输时是大端字节序而 x86 是小端。如果你绕过 Jnetpcap 的封装直接用 ByteBuffer 解析原始字节读 short 时默认小端序0x50 0x00 就会读成 0x0050 即 80 的字节序颠倒变 20480。Jnetpcap 的 Tcp.source()、Udp.destination() 已经在内部做了转换问题只出在手动解析的场景。解决如果是手动解析必须显式指定大端ByteBuffer buf ByteBuffer.wrap(rawBytes); buf.order(ByteOrder.BIG_ENDIAN); int srcPort Short.toUnsignedInt(buf.getShort(0));如果用的是 Jnetpcap 自带对象就直接用 tcp.source()不要再自己翻转一次否则端口会变成 0x5000 和 0x0050 来回折腾这就是典型的“翻车翻在自己的补救代码里”。5.5 HTTP 包总是不全HTTPS 与 HTTP/2 的干扰现象明明开着浏览器访问网页程序里按 HTTP 过滤却只抓到零星几个包而且多为 4xx 或连接重置。原因现代浏览器默认走 HTTPS443 端口流量是 TLS 加密的payload 全是二进制不满足 GET/POST 开头判断HTTP/2 的请求头是二进制帧也不再有明文的 GET 行。只盯 80 端口、只测 ASCII 前几个字节自然抓不全。解决过滤条件放宽到tcp port 80 or tcp port 443对 payload 做内容探测前几个字节是二进制就直接标注加密流量不硬解析。另外 HTTP/3 走 UDP 443QUIC需要单独判断。做课设时不要为了“抓到 HTTP”而只演示一把本地 HTTP 站点这样评委一问现代流量特征就露馅了。把加密流量识别做出来反而能加分。6. 验证方法拿 Wireshark 当裁判顺便把导出格式再升一级写完解析器最大的问题是“怎么证明你的解析是对的”。我的做法是拿 Wireshark 当对照裁判把同样的流量用两边同时看逐字段核对。6.1 用 Wireshark 做五元组对照在一台电脑上同时跑本程序和 Wireshark抓同一条 TCP 流找一个包做细节对照。核对点程序显示Wireshark 显示一致源 IP192.168.1.100192.168.1.100是目的 IP93.184.216.3493.184.216.34是源端口5234152341是目的端口443443是协议TCPTCP是只要有一列对不上说明解析层的偏移量算错了。MAC 地址和 EtherType 也可以加进对照表链路层错的常见表现是源和目的写反因为 14 字节帧头的 dst 在前、src 在后手工解析时顺序容易颠倒。6.2 导出 pcap 后交给 Wireshark 复验更严格的验证是把程序抓到的包用 JpcapWriter 导成 pcap 文件再放进 Wireshark 里打开。Wireshark 能正常识别协议树就说明原始帧完整、链路层字段没有损坏。这个技巧同时也是一个能写进报告的功能点——“本程序支持导出 Wireshark 可读的抓包文件”毕设答辩时是实打实的功能展示。6.3 进阶seq 重组、会话统计与自校验如果按 4.3 的流追踪把包分到了桶里还差最后一步按 TCP seq 排序重组。基本思路是取 tcp.seq() 作为排序键前一个包的 seq 加上 payload 长度等于后一个包的 seq说明序列连续。中间有缺口说明丢包了用 TreeMap 按 key 天然排序拼出来的 payload 才接近真实应用层数据。TreeMapLong, byte[] seqMap new TreeMap(); seqMap.put(tcp.seq(), payload); // 遍历 seqMap 按顺序拼接即可得到完整的会话数据这个重组逻辑做完再配一个简单的流量统计按四元组统计每个 TCP 流的包数和字节数打印一个条形图到控制台这个抓包工具就能从“看包”升级到“分析会话”。从那以后我每次写完抓包工具都强制自己走一遍“五元组对照 pcap 导出复验”的验证路径双向一致才敢说解析没问题。这个习惯帮我省掉了大量排查时间也建议你复现这份资源时把同样的验证流程跑一遍。希望帮到你。本文还有配套的精品资源点击获取