
简介jnetpcap-src-1.4.r1425-1.zip 是面向Java网络封包捕获与分析开发者的jnetpcap 1.4.r1425-1源码包适合需要理解libpcap封装、JNI调用、本地库构建及依赖兼容排查的中高级开发者也可供网络监控、协议解析、安全研究与教学演示参考。压缩包共1256个文件、约9.4MB内含285个java源码、62个h头文件、27个cpp实现、26个o中间文件与2个so动态库并配有383个class、419个html文档、5个jar以及properties和xml构建配置另有16个pcap、3个cap示例抓包。已有526人学习资源较完整地保留Java层与本地库实现便于研究libjnetpcap、libjnetpcap-pcap100的组成与接口组织HTML文档和样例数据可用于建立接口说明与实际数据的对应关系帮助读者理解抓包分析流程、项目目录结构及源码编译所需依赖。对于希望从源码层面掌握数据包捕获、过滤与解析机制的读者这份包提供了可检索、可对照的完整材料与实践样例。1. jnetpcap 源码包解开之后真正要先搞明白的是 jar 与原生库分居两层多数人拿到jnetpcap-src-1.4.r1425-1.zip第一反应是解压、丢进 IDE、写个main跑一下然后被UnsatisfiedLinkError拦在原地。这并不奇怪jnetpcap 不是纯 Java 库它是 libpcap 的 JNI 封装Java 层只负责 API 和协议对象真正的抓包、过滤、时间戳全在 C 层。这个源码包的价值在于你能自己编出与目标机器架构、JDK 版本、libpcap 版本三者对齐的libjnetpcap.so而不是赌官方二进制包恰好能加载。适合三类人要在 JVM 服务里做报文级解析的、要拿离线 pcap 做回归测试的、以及需要自定义协议头扩展的。下面按编译、开卡、拆包、避坑的顺序走一遍。2. 编译链打通libpcap 开发包、JDK 版本与 ant 目标的三处咬合源码包能不能编过取决于三个东西是否同时到位C 编译器和 libpcap 的头文件、一个和这份老代码兼容的 JDK、以及一份没被改坏的构建脚本。缺任何一个报错信息都会指向别处让人误判。2.1 解压后先看目录java 层、c 层、resources 层各管什么不要急着敲构建命令先花两分钟看清目录职责。1.4 这一代的源码树大致是这样分的不同打包方式可能少一两层但主干不会变路径内容是否需要动src/main/javaorg.jnetpcap.Pcap、PcapPacket、protocol.*等 Java 类一般不动扩展协议头时才加src/main/cJNI 桥接代码、jnetpcap_*.c、平台相关实现移植或改抓包行为时才动src/main/resources打包进 jar 的本机库、协议描述等资源极少动build.xmlant 构建脚本含 native 编译目标换编译器/改 source 版本时会动pom.xml部分打包版本带Maven 描述通常只覆盖 Java 部分有就参考别指望它编 C先用命令确认手里这份的实际布局别照着别人的目录结构硬套unzip -q jnetpcap-src-1.4.r1425-1.zip -d /opt/build cd /opt/build # 看清顶层目录名后面所有路径都以它为准 ls -1 # 只列三层快速判断是 maven 布局还是老式 src/ 布局 find . -maxdepth 3 -type d | sort | head -40逻辑说明-d指定解压目录避免污染当前目录。find -maxdepth 3只探三层是为了不把上千个源码文件刷满终端。看目录的目的只有一个——确认 C 代码在不在src/main/c如果在就必须走 native 编译这条路只编 Java 是拿不到能用的产物的。2.2 依赖装对libpcap 头文件缺失是大部分编译失败的起点Java 侧编译只要 JDKC 侧编译要的是pcap.h和链接期的-lpcap。很多人装了运行期的 libpcap 库却没装开发包于是编译到一半报pcap.h: No such file or directory。# 编译工具链 JDK 8 ant libpcap 开发包 # JDK 8 不是随便选的见 2.3 的说明 apt-get install -y build-essential openjdk-8-jdk ant libpcap-dev # 验证头文件和链接库都在位 ls /usr/include/pcap.h ls /usr/lib/x86_64-linux-gnu/libpcap.so # 确认 javac 版本后面 ant 用的是 JAVA_HOME 而不是 PATH java -version echo $JAVA_HOME参数说明libpcap-dev提供头文件和.so软链二者缺一链接阶段会报cannot find -lpcap。JAVA_HOME必须显式导出ant 找javac走的是这个变量PATH里指向 JDK 8 但JAVA_HOME还是 17 的情况非常常见报错却表现为“source 版本不支持”。多个 JDK 共存时养成先export JAVA_HOME$(dirname $(dirname $(readlink -f $(which javac))))再构建的习惯。2.3 用 ant -p 探目标再分两步产出 jar 与 libjnetpcap.so不要凭记忆敲 ant 目标名。先让脚本自己把可用目标列出来这一步能省掉大量试错cd /opt/build/jnetpcap-1.4.r1425 # 列出构建脚本里声明的所有 target 及描述 ant -p看到目标列表后典型做法是先编 Java、再编 native最后打包。目标名各家可能叫jar/compile-native/dist按-p的输出替换# 先只编 Java确认源码本身没问题把问题隔离在纯 Java 层 ant jar # 再编 native这一步才会真正调用 gcc 和 libpcap ant compile-native # 如果脚本有 dist/package 目标用它一次性产出可直接分发的产物 ant dist编完不要急着写业务代码先确认产物形态# 找出 jar 和本机库产物目录名可能是 target/ 也可能是 build/ find . -name jnetpcap*.jar -o -name libjnetpcap.so -o -name jnetpcap.dll # 确认本机库的架构与当前 JVM 一致不一致会直接加载失败 file ./libjnetpcap.so 2/dev/null || file $(find . -name libjnetpcap.so | head -1)逻辑说明先编 Java 再编 native是为了把两类错误分开——Java 编译错误通常是 JDK 版本问题native 编译错误通常是头文件或编译器问题。混在一起编日志会交错定位成本翻倍。file的输出要能看到64-bit且架构与 JVM 匹配在 x86 机器上编出 aarch64 的.so加载时报的错和“库不存在”几乎一样非常容易误判。关于 JDK 版本血泪经验是这一代代码里大量使用老语法构建脚本里的 source/target 常年停在 1.5/1.6新 JDK 直接拒绝编译。要么用 JDK 8 跑构建要么把脚本里的 source/target 改到 8 并在编译参数里加-Xlint:-options压掉告警。改脚本时注意别只改一处Java 和 native 的头文件生成两处都可能带版本参数。2.4 产物校验一个二十行的设备枚举程序在写抓包逻辑之前先跑通“能列出网卡”这件事。这一步不涉及抓包权限也不涉及过滤器是验证 jar 和.so是否配对成功的最小用例import org.jnetpcap.Pcap; import org.jnetpcap.PcapIf; import java.util.ArrayList; import java.util.List; public class ListDevices { public static void main(String[] args) { StringBuilder errbuf new StringBuilder(); ListPcapIf devs new ArrayListPcapIf(); // 返回 0 表示成功非 0 时错误信息在 errbuf 里 int rc Pcap.findAllDevs(devs, errbuf); if (rc ! 0 || devs.isEmpty()) { System.err.println(enum failed rc rc msg errbuf); return; } for (PcapIf dev : devs) { // name 才是 openLive 要的入参description 只是给人看的 System.out.println(name dev.getName() desc dev.getDescription() flags dev.getFlags()); } } }逻辑说明findAllDevs只做枚举不打开设备因此不需要特殊权限是排查链路问题的理想探针。返回码非 0 时先看errbuf但要注意 —— 有些实现路径返回非 0 却忘了写errbuf这时错误的成因往往在别处比如库版本和头文件版本不一致。参数说明dev.getName()是系统设备标识openLive只认它getDescription()是可读描述拿它去开卡必失败。getFlags()里能看到设备是否 up、是否支持混杂模式后续排查“抓不到包”时用得上。运行的时候要显式指定库路径不要依赖 jar 内自动解压javac -cp jnetpcap.jar ListDevices.java java -Djava.library.path/opt/build/jnetpcap-1.4.r1425 \ -cp .:jnetpcap.jar ListDevices容器化部署时这条命令会被写进启动脚本java.library.path指向只读挂载的目录即可。在新版本 JDK 上临时目录自动解压可能被安全策略拦住显式指定路径能绕开这一整类问题。3. 打开网卡之前的参数决策snaplen、混杂模式与超时怎么定openLive那四个参数看起来都是整数实际上每一个都直接决定后面解析代码怎么写、内存占多少、丢包率多少。定错了不会立刻报错只会在压测或者上线之后以一个奇怪的现象暴露出来。3.1 openLive 的四个入参逐条拆参数常用取值影响面定错的后果devicedev.getName()抓哪块卡传成描述字符串返回 nullsnaplen65536 全包96~128 只抓头每包拷贝字节数、解析器可用深度设太小应用层载荷被截断解析出空内容flags混杂模式开启/关闭能否看到非本机地址的流量交换机环境里设了也不生效误判为代码问题timeout10~100毫秒批量返回的等待上限设 0 在部分平台上变成“等到满为止”采集延迟陡增snaplen 是最容易拍脑袋的一个。如果只做五元组统计和协议分布96 字节足够覆盖以太头、IP 头、TCP 头如果要解析 HTTP 请求行或者 TLS 握手就得给到 65536。代价是每个包在 Java 侧的直接内存占用随 snaplen 线性上升高pps场景下这个数字会被放大到很可观。flags 的坑在于预期管理。混杂模式只在共享介质或者被镜像的端口上有意义在普通交换环境里网卡本来就只能收到发给自己和广播的帧开不开混杂都一样。很多人调了半天代码最后发现是采集点选错了。真要做全流量分析得从交换机镜像口或者主机上做流量复制这不是 Java 层能解决的。timeout 决定的是内核缓冲区积攒多久后批量交付。设得太小系统调用次数暴增CPU 花在上下文切换上设得太大实时性下降做告警类业务会明显感觉迟钝。我一般的起点是 10 毫秒压测时再根据丢包统计往上调。3.2 BPF 过滤表达式先在内核里筛再回到 Java 里解析能写 BPF 就一定写 BPF。这不是风格问题是数量级问题过滤发生在内核不匹配的包根本不会拷贝到用户态而在 handler 里用 Java 判断包已经完成了一次跨态拷贝和对象封装白白消耗。常见做法是把粗筛全部下沉到 BPFJava 侧只做 BPF 表达不了的那部分。表达式本身是 libpcap 的语法写错了会返回非 0错误信息同样进errbuf。几个实操中好用的写法# 只要某个服务的双向流量排除广播噪声 tcp port 8080 and not broadcast and not multicast # 只关心某个网段的入向 SYN减少后续解析量 tcp[tcpflags] tcp-syn ! 0 and src net 10.0.0.0/8 # 排除本机回环之外的所有 UDP用于排查特定协议 udp and not port 53 and not port 123需要强调的是方向限定。不加src/dst的port 8080会同时命中两个方向量直接翻倍在只关心请求侧的场景里这一条能省掉一半的处理成本。另外 BPF 里的host写成域名会触发一次解析启动阶段做可以热更新过滤器时最好用 IP避免解析阻塞采集线程。3.3 一个能跑的采集循环Pcap.loop 与 PcapPacketHandler把上面这些拼起来最小可用的采集器长这样import org.jnetpcap.Pcap; import org.jnetpcap.PcapIf; import org.jnetpcap.packet.PcapPacket; import org.jnetpcap.packet.PcapPacketHandler; import java.util.ArrayList; import java.util.List; public class CaptureDemo { public static void main(String[] args) { StringBuilder errbuf new StringBuilder(); ListPcapIf devs new ArrayListPcapIf(); Pcap.findAllDevs(devs, errbuf); PcapIf dev null; for (PcapIf d : devs) { // 按名字精确匹配不要用下标枚举顺序不保证 if (d.getName().equals(args[0])) { dev d; break; } } if (dev null) { System.err.println(device not found); return; } int snaplen 65536; int flags Pcap.PCAP_PROMISC; // 常量名以手上源码的 Pcap 类为准 int timeout 10; Pcap pcap Pcap.openLive(dev.getName(), snaplen, flags, timeout, errbuf); if (pcap null) { System.err.println(open failed: errbuf); return; } // optimizetrue 让 BPF 做一次编译期优化netmask 未知时传 0 String bpf tcp port 8080 and not broadcast; if (Pcap.setFilter(pcap, bpf, true, 0) ! 0) { System.err.println(bpf rejected: errbuf); pcap.close(); return; } PcapPacketHandlerString handler new PcapPacketHandlerString() { private long count 0; Override public void nextPacket(PcapPacket packet, String user) { // 只取头部元信息不持有 packet 引用 long ts packet.getCaptureHeader().timestampInMicros(); int caplen packet.getCaptureHeader().caplen(); int wirelen packet.getCaptureHeader().wirelen(); if ((count % 10000) 0) { System.out.println(n count ts ts caplen caplen wirelen wirelen); } } }; // -1 表示不限期直到 breakloop返回值为收到的包数 int rc Pcap.loop(pcap, -1, handler, demo); System.out.println(loop exit rc rc); pcap.close(); } }逻辑说明Pcap.setFilter在openLive之后、loop之前调用顺序反了过滤器不生效。loop内部是 C 层循环回调 Java-1表示不限包数靠Pcap.breakloop(pcap)从其他线程打断这也是优雅停机的标准做法。参数说明optimizetrue会让 libpcap 对 BPF 做一次重排优化绝大多数场景应当开启netmask只在表达式里用到broadcast这类需要网段信息的关键字时才重要传 0 会让部分表达式退化成保守匹配此时最好从设备地址列表里取出真实掩码传进去。getCaptureHeader()提供的是链路层元信息caplen是实际抓到的字节数wirelen是线上原始长度两者不等就说明 snaplen 截断了。如果你的版本里这个方法名不一样直接翻src/main/java/org/jnetpcap/packet/PcapPacket.java源码就在手边比查任何文档都快。4. 协议拆包从 JBuffer 内存视图到 Ethernet/Ip4/Tcp 的逐层取头解析这一层是 jnetpcap 相对裸 JNI 的价值所在它在直接内存上维护了一个包对象协议头按需映射不用自己维护偏移。但也正因为如此它的对象生命周期和普通 Java 对象完全不同。4.1 为什么 packet 不是 byte[]直接内存与生命周期PcapPacket底下是一块 native 内存回调结束后这块内存会被复用。这意味着两件事一是在 handler 里把packet塞进队列、缓存到 Map、丢给线程池拿到的都是已被覆写的数据二是任何“先收下来慢慢解析”的设计都必须显式拷贝。常见做法是在 handler 里当场把需要的字段抽成普通 POJO然后投递 POJO绝不投递 packet。拷贝的粒度也要控制无脑packet.getByteArray(0, packet.size())会带来大量小数组分配GC 压力比想象中大。按字段抽取一般够用// 在 handler 内部完成抽取只把结果带出去 byte[] srcIp new byte[4]; byte[] dstIp new byte[4]; packet.getByteArray(26, srcIp); // 以太 14 IP 源地址偏移 12 packet.getByteArray(30, dstIp); // 以太 14 IP 目的地址偏移 16 int srcPort packet.getUShort(34); int dstPort packet.getUShort(36);这段是手工偏移能跑但很脆——一旦遇到 VLAN 标签或者 IP 选项偏移全错。它适合做性能对照不适合进生产。参数说明getUShort返回的是无符号短整型端口号正好两字节getByteArray(offset, dst)从直接内存拷到堆上这一步才是真正“脱离生命周期”的操作。4.2 内置协议头类的用法与 hasHeader 的代价正规写法是用内置头类让扫描器自己算偏移import org.jnetpcap.packet.JPacketHandler; import org.jnetpcap.packet.PcapPacket; import org.jnetpcap.protocol.lan.Ethernet; import org.jnetpcap.protocol.network.Ip4; import org.jnetpcap.protocol.tcpip.Tcp; // 头对象建议作为成员复用不要在每次回调里 new private final Ethernet eth new Ethernet(); private final Ip4 ip new Ip4(); private final Tcp tcp new Tcp(); PcapPacketHandlerString handler new PcapPacketHandlerString() { Override public void nextPacket(PcapPacket packet, String user) { // hasHeader 会触发协议扫描可以用对象重载省掉一次查找 if (!packet.hasHeader(eth)) return; if (!packet.hasHeader(ip)) return; // 非 IPv4 直接丢 if (!packet.hasHeader(tcp)) return; // 非 TCP 直接丢 byte[] dst ip.destination(); int sport tcp.source(); int dport tcp.destination(); // 处理业务 } };逻辑说明hasHeader(header)这个重载在命中的同时把头对象绑定到当前包后续ip.destination()直接读不需要第二次查找。如果写成hasHeader(Ip4.ID)再getHeader(ip)等于扫了两次在高 pps 下这是实打实的开销。参数说明ip.destination()返回 4 字节数组注意它可能指向包内存别直接存起来tcp.source()/destination()返回int不需要再转无符号。层级校验要一层层做跳过eth直接问ip在多数情况下也能工作但遇到非以太链路比如 raw IP就会静默失败。想在链路层做兼容先判断链路类型再决定偏移// 链路类型决定以太头是否存在DLT_EN10MB 有 14 字节DLT_RAW 直接是 IP int dlt Pcap.datalink(pcap); boolean hasEther (dlt 1); // 1 对应以太网这一步在容器、隧道、回环口上尤其重要。我见过不止一次“代码没问题但一个包都解析不出来”最后发现采集的是回环口压根没有以太头。4.3 落盘与回放dumpOpen 与 openOffline 做离线复现线上抓到的包能不能复现决定了排查效率。采集器里加一条落盘分支成本很低import org.jnetpcap.PcapDumper; // 与 pcap 句柄绑定写入标准 pcap 格式 PcapDumper dumper Pcap.dumpOpen(pcap, /data/cap/trace.pcap); if (dumper null) { System.err.println(dump open failed); } // 在 handler 内按条件落盘全量落盘会很快吃满磁盘 if (shouldDump) { Pcap.dump(pcap, dumper, packet); } // 退出前必须 flush 再 close否则最后一批包丢失 Pcap.dumpFlush(pcap, dumper); Pcap.dumpClose(dumper);逻辑说明dumpOpen返回的 dumper 内部有缓冲区进程被强杀时缓冲区里的包会丢所以停机钩子里必须有dumpFlush加dumpClose这一对。落盘一定要有条件比如只落特定端口或者只落前 N 个包不然一次故障就能写出几十 GB。回放侧用openOffline打开文件之后的loop、handler、解析逻辑完全复用线上那一套Pcap offline Pcap.openOffline(/data/cap/trace.pcap, errbuf); if (offline null) { System.err.println(errbuf); return; } // 回放不限速做压测时可以在 handler 里 sleep 模拟真实速率 Pcap.loop(offline, -1, handler, replay); offline.close();参数说明openOffline的errbuf在文件损坏时能给出具体偏移比抛异常直观。回放不限速这一点要注意离线文件的包是全速灌进来的如果 handler 里有同步 IO很容易把回放变成一次压力测试——这既是缺点也是优点做吞吐验证时正好用得上。5. 避坑与排查编译、加载、权限、丢包、内存的五次翻车记录下面这五条都是我自己踩过或者帮别人定位过的现象和成因往往对不上号值得照着对照一遍。5.1 现象ant 一开始就把 Java 层编译打断报源选项不支持现象ant jar刚跑几秒就失败提示类似“source option 5 is no longer supported”C 代码根本没轮到编译。原因构建脚本里写死的老 source/target 版本被新 JDK 移除同时这份代码本身也是老语法在高版本 JDK 上还会撞上模块访问限制。这不是代码写错是工具链错配。解决优先用 JDK 8 完成整个构建构建机和应用机保持同一大版本。实在要用高版本把脚本里的 source/target 统一改到 8编译参数补-Xlint:-options并逐一确认 native 头文件生成那一段也同步改了只改一处会得到“Java 编过、JNI 头文件对不上”的二次故障。5.2 现象IDE 里一运行就 UnsatisfiedLinkError说找不到 jnetpcap现象编译期一切正常跑main立刻抛UnsatisfiedLinkError: no jnetpcap in java.library.path。原因三种可能叠在一起——只加了 jar 没给.so.so的位数或架构与 JVM 不一致依赖 jar 内自动解压但临时目录被安全策略限制或挂载成noexec。解决先用file libjnetpcap.so确认64-bit和架构再显式加-Djava.library.path指向库所在目录日志里打开-XshowSettings:properties看java.library.path的实际值。容器里把库放只读目录并关掉自动解压是最省心的做法别让运行时去写临时目录。5.3 现象openLive 返回 null但 errbuf 是空的现象开卡失败打印errbuf.toString()什么都没有只能靠猜。原因最常见的是把设备描述当成了设备名传进去其次是权限不足而某些失败路径没有把错误写进缓冲区。以 root 身份跑一遍能立刻区分这两类。解决先用 2.4 那段枚举程序把name打出来代码里只传name。权限问题不要用chmod 777这种粗暴办法给可执行文件加CAP_NET_RAW和CAP_NET_ADMIN能力更干净也便于审计。定位阶段可以在openLive前后各打一行日志把入参原样输出。5.4 现象过滤器写好了计数一直是零或者全是本机流量现象loop正常跑着计数器不涨或者一开就是自己发的包。原因采集点选错在交换环境里指望混杂模式看到别人的流量回环口没有以太头按以太偏移解析全部错位BPF 没限定方向或者关键字里带了域名触发解析失败。解决先用datalink判断链路类型再决定偏移回环和隧道场景下走 raw IP 分支。表达式一律加方向限定src/dst明确写出来。判断是否真的没流量可以在同一台机器上用命令行抓包工具起一个对照两边计数差一个数量级以上问题就在采集点而不是代码。5.5 现象跑几小时后堆外内存一路上涨最后卡死现象Java 堆看着正常进程 RSS 一直涨loop后来像卡住一样不再回调。原因handler 里把packet或它内部的字节数组存进了队列直接内存被长期持有或者停机时没调用close句柄和缓冲区没有释放还有一种隐蔽情况是 handler 里做了阻塞 IO回调变慢导致内核缓冲区溢出包被丢掉看起来像“卡住”。解决handler 里只做字段抽取和投递投递目标是有界队列满了就计数丢弃绝不无界堆积。落盘、上报全部移到独立线程。停机走breakloop加close并在关机钩子里再兜一次。压测时把丢弃计数打出来这个数字比 CPU 占用更能说明问题。6. 进阶验证把采集器放进长跑环境前必做的三项对照编译通过、能抓到包离能上线还有一段距离。我的习惯是在正式接入前跑三项对照每项都能在两小时内出结论但能挡掉后面一大半事故。第一项是丢包对照。同一块网卡、同一段时间用命令行抓包工具和你自己写的采集器并行跑比较两边的包数。差距超过千分之一就要回头看 snaplen 是不是太大、handler 里是不是有同步操作、队列是不是太浅。这个对照的价值在于它把“代码问题”和“采集点问题”一次性分开了。第二项是回放一致性。把线上落下来的 pcap 用openOffline灌一遍解析结果和实时采集时的统计做比对。差异通常来自两处实时路径上被 snaplen 截断的包在回放里一样截断但统计口径可能没对齐以及回放不限速触发了 handler 里平时跑不到的并发分支。第三项是长跑泄漏检查。连续跑六小时以上每十分钟采一次 RSS 和直接内存使用量画成曲线看斜率。观测项采集方式判定标准进程 RSS外部定时采样六小时后斜率趋近于零无台阶式上涨直接内存JVM 内存池指标与在途包数正相关回落时同步下降回调丢弃计数代码内自增稳态下为零突发时可容忍但要有告警句柄数系统级统计启动后恒定不随重连次数增长三项都过再谈接入。有一次我图快跳过了第三项结果一个把PcapPacket塞进缓存的小改动把进程在第四个小时撑爆了事后回看那条 RSS 曲线在第一个小时就已经抬头只是没人看。从那以后我每次编完这份源码包都会先跑设备枚举、再跑一分钟定时采集、最后跑一次离线回放三步都绿了才动业务代码。这个习惯看起来笨但它把问题锁在了它该出现的那一层。希望帮到你。本文还有配套的精品资源点击获取