ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java网络流量分析实战:基于pcap4j的抓包、解析与会话聚合

Java网络流量分析实战:基于pcap4j的抓包、解析与会话聚合 简介这是一份基于Java实现的跨平台网络流量实时监控与分析课程设计资源面向计算机网络相关专业学生及需要远程部署流量采集场景的Java开发者。方案采用Java后端处理数据、Web前端展示分析的架构在无图形界面系统或远程目标机环境下可将流量数据安全传至浏览器端进行可视化分析并兼顾传输加密与运行稳定性。包内共76个文件核心为27个Java源码文件另含15个JS与9个JSX前端逻辑、HTML/CSS页面、Gradle构建配置、可直接运行的jar产物及密钥证书文件整体压缩包仅11.32MB目录划分清晰便于按需检索与二次开发。随包还附有课程设计报告PDF、任务说明文档和常用配置参考能够帮助读者快速复现项目环境理解数据采集、传输与展示的完整链路。目前已有462人学习下载作为课程设计参考或网络流量分析入门实践均有较高参考价值。1. 用 Java 做网络流量分析软件先确认它在解决什么问题用 Java 做网络流量分析软件放在五年前会被人反问“为什么不用 C”现在这个问题基本可以正面回答了。pcap4j 通过 JNA 把 libpcap/Npcap 的能力搬进 JVM抓包、BPF 过滤、离线 pcap 回放都能在 Java 里完成。标题里那个【100010394】是仓库项目编号源码怎么组织先不管核心链路逃不开这几段找网卡、开句柄、回调收包、解析协议、聚合会话、输出指标。典型的落地场景是运维半夜被告警吵醒一台内网主机反复外连日志看不出名堂只能从网卡上拿原始帧才能定位测试要统计一条链路上的协议占比安全基线要记录每个会话的字节数。这套方案适合三类人查网络问题的运维、做基线的测试以及把流量分析当内部工具或毕业设计来做的 Java 工程师。有人喜欢拿 Python 写抓包脚本但做成要长期维护、要并发、要打包分发的软件Java 的线程模型和类库生态更合我口味。2. 抓包引擎怎么选pcap4j 与 jnetpcap 的差距以及依赖和 native 环境2.1 先对比再动手jnetpcap、jpcap 与 pcap4j 的选型逻辑Java 圈能做抓包的库掰着手指头数就三个最早是 jpcap后来 jnetpcap 在它基础上加了更多 libpcap 结构的映射再后来才是 pcap4j。很多人教程看多了一上来就抄 jnetpcap 的样例代码结果在 64 位 JDK 上编不过去或者换了新版本 Npcap 之后句柄打不开。这不是你的代码问题是选型问题。jnetpcap 的最后一个活跃版本停留在很多年前它对 Npcap 的适配靠社区补丁和你安装时的“WinPcap 兼容模式”来兜底64 位环境下经常要自己再编译一次 dll。pcap4j 不一样它是纯 Java 项目通过 JNA 在运行时动态加载系统里的 libpcap 或 wpcap.dll不依赖预编译的 JNI 二进制跨平台和版本适配都要省心得多。从维护节奏、issue 回复速度和文档完整度看新项目没有理由再选 jnetpcap。对比项jnetpcappcap4j维护状态基本停更持续活跃native 加载方式自带 JNI dll平台强绑定JNA 动态加载系统 pcap 库64 位支持需要自己构建原生支持离线 pcap 重放支持但接口较原始Pcaps.openOffline 直接可用结构化协议解析需要自己手工拼字节内置 Ethernet/IP/TCP/UDP 等包对象学习曲线老教程多但坑多文档齐全坑有迹可循如果你手上恰好有个基于 jnetpcap 的老项目能跑就继续跑不要把线上正在用的东西冲动重写但如果是新起一个工具我一般直接上 pcap4j。实际写的时候你会发现pcap4j 把解析结果封装成一层层对象调试起来比对着原始字节猜要舒服得多。2.2 用 Maven 把 pcap4j 拉进来最小 pom 与版本选择pcap4j 不是单包核心分成两个 artifactpcap4j-core 提供抓包句柄和设备枚举pcap4j-packetfactory-static 提供现成的包对象工厂。之间有个 packetfactory 是因为库本身也允许你自定义工厂但 99% 的场景用 static 就够了。下面这个 pom 是我常用的最小配置properties !-- 版本号以 Maven 中央仓库最新稳定版为准 -- pcap4j.version1.7.7/pcap4j.version /properties dependencies dependency groupIdorg.pcap4j/groupId artifactIdpcap4j-core/artifactId version${pcap4j.version}/version /dependency dependency groupIdorg.pcap4j/groupId artifactIdpcap4j-packetfactory-static/artifactId version${pcap4j.version}/version /dependency dependency groupIdorg.slf4j/groupId artifactIdslf4j-simple/artifactId version1.7.32/version !-- pcap4j 内部用 slf4j 打日志给一个实现不然全是告警 -- /dependency /dependencies版本号这里给的是 1.7.7 作为示例真正写工程时把 pcap4j.version 换成仓库里的最新版本。pcap4j 2.x 之后包结构调整过artifactId 可能合并或改名用 Maven 拉依赖时注意看中央仓库的实际发布列表不要死抄老工程的坐标。slf4j-simple 这个依赖不是必须的但 pcap4j 内部用 slf4j 记录 JNA 加载和设备枚举的过程不绑一个实现的话你排查问题时少了一半日志线索。2.3 环境变量与 native 库Windows 装 Npcap、Linux 装 libpcap再跑通设备枚举pcap4j 本身不携带抓包能力它只是把系统里的 libpcap/Npcap 通过 JNA 包装给你。所以环境准备是第一步也是最多人翻车的一步。Windows 上要先装 Npcap安装向导里有一项 “Install Npcap in WinPcap API-compatible Mode”pcap4j 依赖它来加载 wpcap.dll这个选项一定要勾。装完之后 wpcap.dll 会出现在 System32 下JNA 从系统目录自动加载不需要你把 JAVA_HOME 指向 Npcap 目录网上有些教程把环境变量配置讲得过于玄乎这里其实没那么复杂。Linux 上更直接Debian/Ubuntu 系装 libpcap0.8运行时和 libpcap-dev编译和 tshark 对照时用然后确认当前用户有没有打开原始套接字的权限后面避坑章节会专门讲。装完先别急着写抓包逻辑先用一段极简代码确认库加载正常import org.pcap4j.core.PcapNetworkInterface; import org.pcap4j.core.Pcaps; // 列出所有网卡验证 native 环境和 JNA 加载 ListPcapNetworkInterface devices Pcaps.findAllDevs(); if (devices null || devices.isEmpty()) { // 到这里说明 wpcap.dll/libpcap 没加载成功先别往下写解析代码 throw new IllegalStateException(no network interface found, check pcap install); } for (PcapNetworkInterface device : devices) { System.out.println(device.getName() : device.getDescription()); device.getAddresses().forEach(a - System.out.println( address a.getAddress())); } System.out.println(total devices devices.size());这段代码的逻辑很简单Pcaps.findAllDevs 返回系统识别的网卡列表包括虚拟网卡如 VMware 的 VMnet。如果这里抛 PcapNativeException 或者列表为空说明 native 层有问题后面所有代码都跑不动。device.getName 是类似 “\Device\NPF_{GUID}” 或 “eth0” 这样的内核名device.getAddresses 会带出该网卡绑定的 IP这一步输出的信息在后续按 IP 选择网卡时会直接用上。注意虚拟网卡也会出现在列表里生产环境抓包时先搞清楚你要的是物理网卡还是虚拟网卡否则抓半天全是 VM 内网流量。3. 从网卡到协议解析一个能直接跑的最小抓包链路3.1 打开 PcapHandlesnaplen、promiscuous 和 timeoutMillis 三个参数怎么定设备拿得到之后核心对象是 PcapHandle它对应 libpcap 里一个抓包会话。pcap4j 推荐用 Builder 来配置参数比裸调 openLive 可读性好// 按 IP 挑网卡避免在多网卡机器上拿 device 列表第一个 PcapNetworkInterface nif devices.stream() .filter(d - d.getAddresses().stream() .anyMatch(a - a.getAddress() instanceof Inet4Address a.getAddress().getHostAddress().equals(192.168.1.10))) .findFirst() .orElseThrow(() - new IllegalStateException(网卡 192.168.1.10 不存在)); // 抓包句柄snaplen65535 抓完整帧混杂模式10ms 超时 PcapHandle handle new PcapHandle.Builder(nif.getName()) .snaplen(65535) .promiscuousMode(PcapNetworkInterface.PromiscuousMode.PROMISCUOUS) .timeoutMillis(10) .bufferSize(2 * 1024 * 1024) // 内核缓冲区设 2MB降低重负载丢包 .build(); // BPF 过滤表达式只放行 TCP 和 UDP丢弃 ARP/ICMP 等 handle.setFilter(tcp or udp, BpfProgram.BpfCompileMode.OPTIMIZE);三个参数各有讲究。snaplen 表示每个包最多截多少字节65535 能覆盖以太网帧上限如果你的分析只关心包头设 128 或 256 能省不少内存代价是拿不到应用层 Payload。promiscuous 混杂模式让网卡把不是发给本机的包也收上来这是“旁路分析”的前提关掉它就只能看到本机进出的流量。timeoutMillis 在 Windows 上特别关键设成 0 的话WinPcap/Npcap 的线程模型会让你等到内核缓冲区攒满才返回一批包实时性很差设 10ms 是常见的折中Linux 上也适用。bufferSize 是很多人忽略的默认值偏小压测场景下内核缓冲区一满pcap 直接丢包统计结果就对不上。3.2 解析以太网帧和 IP/TCP 头结构化 API 与手动字节解析对照pcap4j 的包对象是分层的packet 是最外层调用 get(EthernetPacket.class) 拿到以太网头再往下能取到 IP 头和 TCP 头。类型不匹配时返回 null所以每次取层都要判空// 结构化 API适合开发期快速验证和后期维护 EthernetPacket eth packet.get(EthernetPacket.class); IpV4Packet ip packet.get(IpV4Packet.class); TcpPacket tcp packet.get(TcpPacket.class); if (tcp null) { return; // 非 TCP 包UDP/ICMP或解析失败直接跳过 } IpV4Header ipHeader ip.getHeader(); TcpHeader tcpHeader tcp.getHeader(); System.out.printf(%s:%d - %s:%d proto%d bytes%d%n, ipHeader.getSrcAddr().getHostAddress(), tcpHeader.getSrcPort().valueAsInt(), ipHeader.getDstAddr().getHostAddress(), tcpHeader.getDstPort().valueAsInt(), ipHeader.getProtocol().value(), packet.getRawData().length);另一条路是手动解析原始字节。结构化 API 方便但每个包都要构建一堆对象纯统计场景下 JVM 压力不小。手动解析只要拿到 rawData 后按偏移取值省掉对象分配byte[] raw packet.getRawData(); if (raw null || raw.length 34) { return; // 14 字节以太网 20 字节 IP 头是最低要求不够说明包不完整 } // 以太网头固定 14 字节偏移 12-13 是 EtherType0x0800 表示 IPv4 if ((raw[12] 0xFF) ! 0x08 || (raw[13] 0xFF) ! 0x00) { return; // 丢弃 ARP、VLAN 标签包VLAN 会整体偏移 4 字节这里先不处理 } int ipOff 14; int ihl (raw[ipOff] 0x0F) * 4; // IP 头长度单位是 4 字节 int totalLen ((raw[ipOff 2] 0xFF) 8) | (raw[ipOff 3] 0xFF); // 大端拼接总长度 int protocol raw[ipOff 9] 0xFF; // 6TCP, 17UDP String srcIp String.format(%d.%d.%d.%d, raw[ipOff 12] 0xFF, raw[ipOff 13] 0xFF, raw[ipOff 14] 0xFF, raw[ipOff 15] 0xFF); if (protocol ! 6 || totalLen ihl 20) { return; // 非 TCP或者 IP 头之后不足 20 字节 TCP 头 } int tcpOff ipOff ihl; int srcPort ((raw[tcpOff] 0xFF) 8) | (raw[tcpOff 1] 0xFF); int dstPort ((raw[tcpOff 2] 0xFF) 8) | (raw[tcpOff 3] 0xFF); // TCP 头的第 14 个字节低 6 位分别是 URG/ACK/PSH/RST/SYN/FIN int flags raw[tcpOff 13] 0x3F; boolean syn (flags 0x02) ! 0; boolean fin (flags 0x01) ! 0;这里最容易出错的是符号扩展Java 的 byte 是有符号的0x80 以上的字节直接 shift 会带出符号位所以每个字节都要 0xFF转成 0~255 再拼。ihl 的算法是因为 IP 头长度字段的单位是 4 字节取低 4 位后乘 4 才是真实字节数跳过去才是 TCP 头的起点。如果包是 IPv6EtherType 0x86DD偏移完全不一样这套解析会错乱所以入口的 EtherType 判断很重要。3.3 回调只入队、后台线程做解析避免抓包线程成为瓶颈PcapHandle.loop 的监听器在一个抓包线程里串行执行回调里一旦出现耗时操作比如解析全部字段、打印日志、写数据库内核缓冲区很快被占满pcap 就开始丢包。这个坑几乎每个初写抓包程序的人都会踩一次。我的做法是回调里只做一件最轻的事把包放进有界队列然后由消费者线程池去解析import java.util.concurrent.*; // 有界队列容量 5 万防止消费者跟不上时无限制堆积内存 BlockingQueuePacket queue new LinkedBlockingQueue(50_000); AtomicLong dropped new AtomicLong(); // 记录因队列满而丢弃的包数 // 抓包线程只入队不做任何解析 handle.loop(-1, packet - { if (!queue.offer(packet)) { dropped.incrementAndGet(); } }); // 消费者线程池4 个线程做解析和聚合速度跟不上就排队 ExecutorService workers Executors.newFixedThreadPool(4); for (int i 0; i 4; i) { workers.submit(() - { while (!Thread.currentThread().isInterrupted()) { Packet packet queue.take(); // 这里再调用前面的结构化解析或手动解析逻辑 } }); }loop 的第一个参数 -1 表示无限抓下去传一个正整数就只抓指定数量的包然后自动返回这个语义在做“只抓 1 万个包做抽样统计”时很好用。队列的 offer 方法在满时会立刻返回 false而不是阻塞抓包线程所以用 AtomicLong 把丢弃数记下来——在流量分析里丢包率本身也是一个需要监控的指标。消费者线程数不用太多解析本身是 CPU 密集任务开 4~8 个跟核数匹配就行开多了反而在锁竞争上浪费时间。程序退出时记得 handle.close()它底层释放的是 native 层的 pcap_t 句柄不关闭的话在 Windows 上会残留抓包会话下次打开同一张网卡可能失败。4. 协议识别与会话聚合把包变成可统计的业务指标4.1 端口、特征码、行为三招识别 HTTP/DNS/TLS拿到一条连接记录后第一个问题通常是“这是什么协议”。纯端口判断是基础53 大概率是 DNS80/8080 是 HTTP443 是 TLS。但端口可以被复用内网里把服务跑在非标准端口上的情况比比皆是所以我在端口判断之外加了一层 Payload 特征码验证。HTTP 的请求行和响应行特征非常明显DNS 的头部结构固定TLS 的握手记录首字节固定为 0x16// 协议识别先看端口再看 Payload 特征返回协议标识 static String classify(int srcPort, int dstPort, byte[] payload) { // 端口 53 基本可以断定 DNSUDP 上尤其可靠 if (srcPort 53 || dstPort 53) { return DNS; } // 80/8080 先标记为 HTTP但要用特征码二次确认 boolean isHttpPort srcPort 80 || dstPort 80 || srcPort 8080 || dstPort 8080; if (isHttpPort looksLikeHttp(payload)) { return HTTP; } // 443 上大概率是 TLSStartTLS 或非标准端口靠 ClientHello 特征识别 if (srcPort 443 || dstPort 443 || looksLikeTls(payload)) { return TLS; } return OTHER; } // 检查 Payload 前 16 字节是否像 HTTP static boolean looksLikeHttp(byte[] payload) { if (payload null || payload.length 4) { return false; } String head new String(payload, 0, Math.min(16, payload.length), StandardCharsets.ISO_8859_1); return head.startsWith(GET ) || head.startsWith(POST ) || head.startsWith(PUT ) || head.startsWith(DELETE ) || head.startsWith(HEAD ) || head.startsWith(HTTP/); } // TLS 记录头0x16 表示握手第 6 个字节是握手类型 0x01 表示 ClientHello static boolean looksLikeTls(byte[] payload) { return payload ! null payload.length 6 (payload[0] 0xFF) 0x16 (payload[5] 0xFF) 0x01; }DNS 的判断其实还能再细一点DNS 头部前 12 字节是 ID(2)、标志(2)、QDCOUNT(2)……把 flags 的 bit15 取出来能区分请求和响应QDCOUNT 大于 0 的通常是请求。这套特征识别不是百分之百内网有人把 SSH 挪到 443 端口上跑TLS 特征识别不出来但它已经能覆盖绝大多数正常业务流量。识别结果会直接影响后面的协议占比统计所以要给“OTHER”留一个可见的档位不要什么都吞进“未知”里否则统计报表做出来没法解释。4.2 五元组会话聚合FlowKey 设计与定时回收流量分析的第二件事是把逐包记录聚合成会话。会话的天然主键是五元组源 IP、源端口、目标 IP、目标端口、协议。但这里有个细节TCP 客户端端口是随机高位端口如果不做方向归一化同一个 TCP 连接的来回流量会被拆成两条流A→B 一条B→A 一条。我一般做法是把五元组按字典序归并成一个方向无关的 key双向字节合在一起统计// 会话 key五元组但方向归一化双向流量合并到同一条流 public final class FlowKey { private final String ipA; private final int portA; private final String ipB; private final int portB; private final int protocol; // 工厂方法把 src/dst 按字典序归并避免双向拆成两条流 public static FlowKey of(String ip1, int p1, String ip2, int p2, int proto) { int cmp ip1.compareTo(ip2); if (cmp 0 || (cmp 0 p1 p2)) { return new FlowKey(ip1, p1, ip2, p2, proto); } return new FlowKey(ip2, p2, ip1, p1, proto); } // equals、hashCode 按五个字段生成这里省略 } // 聚合表ConcurrentHashMap 保证多消费者线程写入安全 ConcurrentHashMapFlowKey, FlowStats flows new ConcurrentHashMap(); // 每个包到达时更新对应会话的统计 FlowKey key FlowKey.of(srcIp, srcPort, dstIp, dstPort, protocol); FlowStats stats flows.computeIfAbsent(key, k - new FlowStats(System.currentTimeMillis())); stats.packets; stats.bytes packetLength; stats.lastSeen System.currentTimeMillis();为什么用 computeIfAbsent 而不是先 get 再 put多消费者线程同时处理不同包时check-then-act 会产生竞态同一个新会话可能被两个线程各建一条记录后面的流量就会被分流到两条流上统计彻底失真。computeIfAbsent 在 ConcurrentHashMap 上是原子的能保证同一个 key 只会创建一个 FlowStats 实例。如果你还要区分请求和响应方向来分析“谁先发起连接”就在 FlowStats 里加两个方向独立的计数器而不是改 key 结构——改了 key 结构就回到两条流的老问题上了。会话不能无限存活。TCP 的正常关闭有 FIN 标志但一半以上的流量靠超时消失比如移动端断网、服务端直接 RST。我会用一条定时任务清扫空闲会话ScheduledExecutorService scheduler Executors.newSingleThreadScheduledExecutor(); // TCP 空闲 30 秒、UDP 空闲 10 秒后回收 scheduler.scheduleWithFixedDelay(() - { long now System.currentTimeMillis(); flows.entrySet().removeIf(e - { FlowStats s e.getValue(); long idleLimit e.getKey().protocol 6 ? 30_000 : 10_000; return now - s.lastSeen idleLimit; }); }, 30, 30, TimeUnit.SECONDS);定时任务不一定非要引 Quartzjava.util.concurrent 自带的 ScheduledExecutorService 在这个场景足够了。清扫周期取 30 秒太频繁会白耗 CPU太懒则表里的幽灵会话太多。注意 removeIf 在 ConcurrentHashMap 上会逐个加锁如果表里有几百万条流一次清扫可能耗时几百毫秒这是可以接受的但不要把清扫和抓包线程放在同一个池子里否则业务线程会被抢走。4.3 指标设计和内存边界吞吐量、TopN 与快照落盘会话表只是中间态最终要给人的是几个能说明问题的指标。我做流量分析最少会出这几项链路吞吐量、协议占比、包长分布、TopN 会话。吞吐量用滑动窗口算维护最近 60 个秒级计数器每秒清空当前桶60 秒窗口求和再除以时长就是平均吞吐。包长分布把长度分桶0-64、65-128、129-512、513-1024、1024扫描和爆破流量的特征都在小包区域。指标计算方式用途吞吐量60 秒滑动窗口字节和 / 时长发现突发流量和基线偏离包长分布按长度分桶累加计数识别扫描、小包攻击协议占比分类计数占总包数比例业务流量构成基线TopN 会话按累计字节排序取前 N定位流量大头和异常外连内存边界的控制是一开始就要想好的不然后面必 OOM。两个硬性上限队列容量已经有界会话表也要有界。我一般给会话表设最大值比如 100 万条达到上限后按 lastSeen 淘汰最旧的。淘汰逻辑放在定时清扫里一起做把removeIf的条件改成“超时或超容量”这样不会增加额外的遍历开销。每 60 秒把当前会话表和 TopN 快照写一次日志或时序库然后清空统计桶会话表保留但不重置——会话表的生命周期就是会话本身的生命周期这是它和临时统计桶最大的区别。5. 避坑清单Java 流量分析最常见的五个翻车现场5.1 Windows 上打不开句柄先查 Npcap 的 WinPcap 兼容模式现象Pcaps.findAllDevs 返回空列表或者 openLive 抛 PcapNativeException代码跟教程一模一样但就是跑不通。换个同事的机器又正常。原因本机装了 Npcap但安装时没勾选 “Install Npcap in WinPcap API-compatible Mode”。pcap4j 的 JNA 映射依赖 wpcap.dll 提供 WinPcap 兼容层的函数入口不勾装出来的 Npcap 缺少这些入口加载就会失败。另外 JDK 是 64 位就装 64 位 Npcap位数混了会出现加载成功但一调用就崩溃的怪象。解决重装 Npcap安装向导里把兼容模式勾上装完重启终端和 IDE。然后回到 2.3 节的设备枚举代码确认列表能打印出来再往下走。5.2 Linux 下普通用户收不到包capability 与路径绑定现象程序不报错handle 也打开了但 loop 一直拿不到包。sudo 跑立刻正常用普通用户跑就是黑匣子一样没反应。原因打开 pcap 句柄需要 CAP_NET_RAW 和 CAP_NET_ADMIN 两个 capability普通用户默认没有openLive 在某些内核和 libpcap 版本下不会立刻报权限错误而是直接把抓包静默失效。解决要么开发时直接 sudo 跑要么给 Java 二进制附加 capability# 给 java 可执行文件附加网络抓包权限注意路径要跟你的 JDK 实际路径一致 sudo setcap cap_net_raw,cap_net_admineip /usr/lib/jvm/java-17-openjdk-amd64/bin/java getcap /usr/lib/jvm/java-17-openjdk-amd64/bin/javasetcap 之后用 getcap 确认输出里能看到 cap_net_raw,cap_net_admin 就说明加上了。坑在于如果你用 sdkman、jenv 或者 IDE 内置 JDK 切换版本capability 是加在具体二进制路径上的一切换路径就丢表现为“昨天还能抓包今天突然不行”。我后来统一把抓包程序打成可执行 jar用固定路径的 JDK 启动脚本去跑才彻底躲开这个玄学问题。5.3 重负载丢包回调里的耗时操作是隐形杀手现象空载时一切正常一上压测包数就少了一大截。在回调里加了 JSON 序列化或者日志输出之后丢包更明显。原因handle.loop 的监听回调在抓包线程里串行执行回调耗时长内核 pcap 缓冲区很快写满新到达的包被内核直接丢弃。这个丢包发生在 native 层JVM 里看不到任何异常只有拿 tshark 同网卡对照才会发现数量对不上。解决回调里只入队解析放到消费者线程池。队列用有界队列满了记 dropped 数而不是无限阻塞。真到了连入队都跟不上的极端场景宁可丢包也要保住抓包线程不崩再把丢包率作为监控项暴露出来。5.4 端口和包长解析出来是天文数字字节序与符号扩展现象在实际设备上抓包解析出来的源端口是 13568明明访问的是 53 端口。包长字段出现 65535 之类的怪值偶尔还抛 ArrayIndexOutOfBoundsException程序崩掉。原因两手罪都犯了。第一Java 的 byte 是有符号类型0x80 以上的字节直接 8会带符号扩展拼出来的数完全不对第二网络字节序是大端x86 内存是小端数值拼接必须按大端顺序手工移位。至于数组越界是没做长度校验就取了 raw[20]遇到超短包直接访问越界。解决所有字节取值统一写成(raw[i] 0xFF)再用大端方式组合或者用ByteBuffer.wrap(raw).order(ByteOrder.BIG_ENDIAN)统一读取。每次解析前先判长度以太网加 IP 头加 TCP 头至少要 54 字节不足就直接跳过。这一步是血泪经验错一两个字节在本地可能看不出来上了生产流量就原形毕露。5.5 长时间运行 OOM会话表无限增长和队列积压现象程序跑了几个小时突然 OOM重启后又复发。GC 日志显示老年代持续上涨Full GC 越来越频繁最后抓包线程卡死。原因网络上有扫描器或异常程序在产生大量五元组会话表无限膨胀回调队列如果设计成无界队列消费者线程一慢队列也能吃掉全部堆内存。解决会话表加容量上限达到上限按 lastSeen 淘汰最旧会话队列全部改有界入队失败只计数不阻塞另外把-Xmx设成一个可控的值而不是放任默认比如-Xmx2g。定期快照落盘后主动调用System.gc()并不解决问题真正有效的是把每个集合的上限都卡死让内存用量和流量大小解耦。6. 上线前的三个进阶动作离线重放、性能验证与数据一致性6.1 把抓包来源抽象成接口pcap 离线重放先行新需求到手我一般先把“包从哪来”抽象出来。LiveSource 和 OfflineSource 都实现同一个 PacketSource 接口解析层只管拿包不关心包是网卡来的还是文件来的。pcap4j 的离线读取只需要一行差异// 离线模式读 pcap 文件同一套解析和聚合逻辑直接复用 PcapHandle offline Pcaps.openOffline(capture.pcap); offline.loop(-1, packet - queue.offer(packet)); // 进同一个队列离线重放的价值太大了。出问题时先用 Wireshark 在真机上抓一份 pcap回到测试环境重放问题就能稳定复现不用在生产网卡上反复折腾。我在 OfflineSource 里还会加一个限速参数控制每秒吐多少包模拟慢速和高压两种场景做回归。6.2 用 tshark 对照验证统计结果再抠两个性能点解析和聚合写完第一件事是验证结果对不对而不是继续加功能。抓一份固定流量的 pcap然后用 tshark 出会话统计做对照# 对比 TCP 会话数和字节量检验聚合逻辑是否准确 tshark -r capture.pcap -q -z conv,tcptshark 的 conv 表会列出每个会话的双向包数和字节数拿它和你的 TopN 输出比数量级误差在几 KB 以内就说明解析和聚合链路没问题。如果差很多多半是方向归并或超时回收的口径不一致先对齐口径再谈性能。性能上值得抠的点有两个一是纯统计场景少用包对象pcap4j 的 Packet 对象分层构建开销不小只关心包头就手动解析 rawData二是 bufferSize 在压测环境调到 4~8MB减少内核丢包。数据一致性主要靠 ConcurrentHashMap 的原子方法和 AtomicLong 计数器遇到“统计值时大时小”的问题先怀疑是不是多线程下用了普通 HashMap。说句实在的我以前做抓包也爱直接在回调里一把梭直到被线上丢包教育过一次。现在的习惯是任何抓包需求都先落一份 pcap离线重放跑通再上生产网卡。有一次半夜线上异常外连就是靠离线重放把解析逻辑调对上线后半小时就定位到是台测试机的定时任务在扫外网端口。这项目做下来最大的体会是抓包不难难的是让解析逻辑在真实流量下不翻车、不 OOM边界和上限在一开始就定好。希望帮到你。本文还有配套的精品资源点击获取
返回列表