
简介基于Java实现网络流量实时监控与分析课程设计项目面向计算机网络课程设计、Java Web开发及网络运维人员主要解决跨平台流量采集、远程Web可视化与数据传输安全等实际问题。项目采用Java后台完成数据抓取与分析前端使用Web客户端展示可运行在无图形界面的远程主机上。压缩包共76个文件含27个Java源文件、15个JS脚本、9个JSX组件、HTML/CSS页面、Gradle构建配置、可执行JAR包、证书密钥及课程设计报告PDF等整体大小11.32MB。已有462人学习浏览。借助完整源码、构建脚本与配置说明可快速搭建实验环境理解流量分析模块划分、前后端交互流程也可扩展功能或用于课程答辩。1. 基于Java的网络流量分析软件到底在解决什么问题凌晨两点IDC 出口带宽告警登录交换机一看某个内网 IP 正以每秒几千个连接的速度扫描外网 445 端口。业务系统的 access log 里全是正常请求ELK 里查不到任何异常——因为异常流量不经过应用端口。这时候最直接的办法是抓一份 pcap把五元组聚合出来看看“谁在什么时候、跟哪些目标 IP、建立了多少连接、传了多少字节”。这套工作完全可以交给 Java 来做抓包交给 Pcap4J 接 libpcap协议解析和会话聚合用纯 Java 字节操作完成最后输出一份 Top N 会话报表。它不追求替代 Wireshark 的交互式体验而是解决“定位问题、留证、产出可查记录”这类自动化需求。本文把这套结构完整拆开讲适合做安全运维、网络排障以及想用 Java 写网络工具的工程师。2. 先立模型流量分析里的核心对象与抓包选型2.1 抓包层选型为什么是 Pcap4J 而不是直接调 libpcapJava 语言本身没有能力直接从网卡驱动拿二层帧必须依赖 JNI 封装的操作系统抓包接口。常见选择有三个Jpcap、JNetPcap、Pcap4J。Jpcap 是很多老教程的首选但项目已停止维护在 JDK 8 之后的模块化环境下会遇到反射访问受限的问题。JNetPcap 性能不错但文档少上手成本高。Pcap4J 是目前维护最活跃的封装库对 libpcap 的 API 覆盖完整支持 Windows 的 Npcap、Linux 的 libpcap并且提供了Packet对象模型拿到原始字节的同时也能直接取到 Ethernet、Ipv4、Tcp 等协议对象。封装库维护状态跨平台Java 版本兼容上手成本适合场景Jpcap基本停止一般JDK 8 以下友好低教学示例JNetPcap活跃度低较好中高性能敏感型自定义工具Pcap4J活跃Linux/Windows/macOSJDK 8低生产级分析工具需要明确一个边界Pcap4J 只负责把网卡上的数据包交到你的 Java 代码里它不负责“分析”。分析逻辑——协议字段解析、流表聚合、超时回收、异常检测——全部由我们自己写。这个边界很重要因为很多方案把分析逻辑堆在抓包线程里导致丢包率飙升。我的建议是抓包线程只做一件事把原始数据包放入队列解析工作交给独立消费者线程。2.2 五个核心模型Packet、Session、Endpoint、ProtoStats、Report写流量分析代码之前先把领域对象定清楚。我在做这类工具时习惯定义五个核心类PacketFrame一帧原始数据保留rawData字节数组和时间戳。Endpoint一个通信端点包含 IP、端口、协议类型。Session一条双向连接记录包含两端 Endpoint、正反向的包数、字节数、SYN/FIN/RST 计数以及最近活跃时间。ProtoStats按协议维度的统计信息比如 TCP、UDP、ICMP、DNS、HTTP 各自的包数和字节数。TrafficReport最终产出的报表对象承载 Top N 会话和协议分布。public class Session { private final Endpoint src; private final Endpoint dst; private long upBytes; private long downBytes; private int upPackets; private int downPackets; private int synCount; private int rstCount; private volatile long lastActiveAt; public void record(boolean isUpstream, int packetLen, int flags) { if (isUpstream) { upBytes packetLen; upPackets; } else { downBytes packetLen; downPackets; } if ((flags 0x02) ! 0) synCount; if ((flags 0x04) ! 0) rstCount; lastActiveAt System.currentTimeMillis(); } }这段代码里record方法被设计成每次来一个包就调用一次。参数isUpstream表示方向flags是 TCP 标志位字节。把统计字段直接内聚在 Session 中避免后续在汇总阶段再遍历所有包内存开销会小很多。2.3 用生产者-消费者模型把抓包和解析解耦libpcap 的回调线程是阻塞式的如果回调里直接做协议解析、哈希表更新一个慢操作就会拖慢整个抓包过程内核缓冲区很快被打满然后开始丢包。解决办法是引入一个有界队列BlockingQueuebyte[] packetQueue new ArrayBlockingQueue(65536); long dropped 0; // 生产者抓包回调里只做入队 PacketListener listener packet - { byte[] raw packet.getRawData(); if (raw null) return; if (!packetQueue.offer(raw)) { dropped; // 队列满时计数而不是阻塞抓包线程 } }; // 消费者独立线程批量处理 ExecutorService exec Executors.newSingleThreadExecutor(); exec.submit(() - { while (!Thread.currentThread().isInterrupted()) { byte[] raw packetQueue.poll(100, TimeUnit.MILLISECONDS); if (raw ! null) { PacketFrame frame Parser.parse(raw); SessionTable.update(frame); } } });这里有两个参数值得注意。队列容量 65536 是经验值按 MTU 1500 算满队列大约占 98MB 内存如果机器内存紧张可以下调到 16384。offer失败时宁可丢包并计数也不能让抓包线程卡在put上。dropped计数一定要保留后续分析结果里如果 dropped 比例超过 5%说明消费者处理速度跟不上需要增加消费者线程数。3. 用 Pcap4J 写一个可用的采集与协议解析模块3.1 引入依赖与准备 libpcap 运行环境项目使用 Maven 构建Pcap4J 的核心依赖是两个包dependency groupIdorg.pcap4j/groupId artifactIdpcap4j-core/artifactId version1.8.2/version /dependency dependency groupIdorg.pcap4j/groupId artifactIdpcap4j-packetfactory-static/artifactId version1.8.2/version /dependency运行时环境方面Linux 需要安装 libpcap 开发库Debian/Ubuntu 执行apt install libpcap-devCentOS/RHEL 执行yum install libpcap-devel。Windows 需要安装 Npcap安装时勾选“WinPcap API-compatible Mode”否则 Pcap4J 会发现设备但无法打开。提示启动 JVM 时建议加-Djava.library.path指向 libpcap 所在目录避免部分系统上 JNI 找不到本地库。3.2 打开网卡并抓取第一个数据包抓包前先枚举网卡选择需要的接口。下面是完整的最小抓包代码import org.pcap4j.core.*; import org.pcap4j.packet.Packet; public class Sniffer { public static void main(String[] args) throws Exception { // 枚举所有网卡 PcapNetworkInterface nif null; for (PcapNetworkInterface dev : PcapNetworkInterface.getDevList()) { if (eth0.equals(dev.getName())) { // 换成实际网卡名 nif dev; break; } } if (nif null) throw new IllegalStateException(未找到目标网卡); // 打开设备snaplen65535混杂模式读超时10ms PcapHandle handle nif.openLive(65535, PcapNetworkInterface.PromiscuousMode.PROMISCUOUS, 10_000); // 抓包回调这里先打印包长度 PacketListener listener packet - System.out.println(captured: packet.length() bytes); // 0 表示无限抓包 handle.loop(0, listener); handle.close(); } }openLive的第二个参数决定是否开启混杂模式。在服务器上做排障时如果只关注本机收发流量可以不开启混杂模式如果交换机端口做了镜像必须开启。第三参数 10 是 read timeout单位毫秒设为 0 表示一直阻塞等待包到达但某些网卡驱动在低流量时会导致回调延迟设 10ms 比较稳妥。loop(0, listener)中的 0 代表无限抓包改为正整数则抓完指定数量后自动停止。3.3 手工从字节数组解析 IPv4 与 TCP 头Pcap4J 自带的Packet对象虽然能直接取协议头字段但它的对象模型在高速场景下会产生大量中间对象。所以我更习惯直接从getRawData()里手工解析自己关心的字段。以太网帧头固定 14 字节第 12、13 字节是 EtherType0x0800 表示 IPv4。IPv4 头从偏移 14 开始。public class PacketParser { // 只解析以太网 IPv4 TCP public static ParsedPacket parseTcp(byte[] frame) { int offset 14; int etherType ((frame[12] 0xFF) 8) | (frame[13] 0xFF); if (etherType ! 0x0800) return null; int ipHeaderLen (frame[offset] 0x0F) * 4; int protocol frame[offset 9] 0xFF; if (protocol ! 6) return null; // TCP only String srcIp parseIp(frame, offset 12); String dstIp parseIp(frame, offset 16); int tcpOffset offset ipHeaderLen; int srcPort readShort(frame, tcpOffset); int dstPort readShort(frame, tcpOffset 2); int flags frame[tcpOffset 13] 0xFF; int tcpHeaderLen ((frame[tcpOffset 12] 4) 0x0F) * 4; int payloadLen frame.length - (tcpOffset tcpHeaderLen); return new ParsedPacket(srcIp, dstIp, srcPort, dstPort, flags, payloadLen); } private static String parseIp(byte[] frame, int offset) { return (frame[offset] 0xFF) . (frame[offset 1] 0xFF) . (frame[offset 2] 0xFF) . (frame[offset 3] 0xFF); } private static int readShort(byte[] b, int offset) { return ((b[offset] 0xFF) 8) | (b[offset 1] 0xFF); } }字节序是这里最常见的坑。网络字节序是大端所以读端口必须把第一个字节左移 8 位再与第二字节按位或。IPv4 头长度字段在第一个字节的低四位单位是 4 字节所以乘 4 得到实际长度。TCP 头的长度字段同理。payloadLen需要减掉 IP 头和 TCP 头的长度这个值后续计算上下行带宽时会用到。3.4 校验和检查与 VLAN 帧处理TCP/IP 协议栈对校验和的计算和验证有一套固定流程但在线抓包场景下我建议关闭校验和验证。原因有两个一是部分网卡开启 TSO/GRO 卸载后驱动层会修改数据包校验和字段在用户态看已经失效强行验证会误报二是校验和计算本身消耗 CPU在每秒十万包的压力下不值得。public static int checksum(byte[] data, int offset, int length) { long sum 0; int i offset; int end offset length - 1; while (i end) { sum ((data[i] 0xFF) 8) | (data[i 1] 0xFF); i 2; } if (i end) { sum (data[i] 0xFF) 8; } while ((sum 16) ! 0) { sum (sum 0xFFFF) (sum 16); } return (~sum) 0xFFFF; }实际项目中这段代码只在离线 pcap 回放时启动用于验证自己写的解析器是否算对偏移。另一个高频坑是 VLAN带 802.1Q 标签的帧在以太网头之后多了 4 字节EtherType 位置后移。遇到带 VLAN 的交换机网络需要先读 14 字节处的 EtherType如果是 0x8100则偏移加 4 再继续解析。很多工具在 VLAN 环境下解析出来的 IP 全是乱的原因就在这。4. 会话聚合与流量统计把数据包变成连接记录4.1 五元组会话 Key 的构造位压缩与哈希实时流量分析里数据包级别是无意义的必须聚合到连接级。标准做法是五元组源 IP、源端口、目的 IP、目的端口、协议号。最朴素的实现是用字符串拼接但每个包都做一次字符串拼接会产生大量临时对象影响 GC。我一般用位压缩的方式把五元组塞进两个 long 字段再包一层对象作为 HashMap 的 key。public final class SessionKey { public final long local; // 高32位: 本地IP, 低16位: 本地端口 public final long remote; // 高32位: 远端IP, 低24位: 远端端口协议 public final int hash; public SessionKey(int srcIp, int srcPort, int dstIp, int dstPort, int proto) { this.local ((long) srcIp 16) | (srcPort 0xFFFFL); this.remote ((long) dstIp 24) | ((dstPort 0xFFFFL) 8) | (proto 0xFFL); this.hash (int) (local ^ (local 32)) * 31 (int) (remote ^ (remote 32)); } Override public boolean equals(Object obj) { if (!(obj instanceof SessionKey)) return false; SessionKey k (SessionKey) obj; return local k.local remote k.remote; } Override public int hashCode() { return hash; } }local 字段用 48 位存本地 IP 和端口remote 字段用 56 位存远端 IP、远端端口和协议号两个 long 正好覆盖完整五元组。这个设计的关键在于方向归一化建立连接时主动发起方算 local被动方算 remote。如果 SYN 包从 A 到 B后续响应包从 B 到 A需要交换方向再查找会话表否则同一条连接会被拆成两个半连接。4.2 会话表设计、超时回收与最大容量保护会话表的核心就是一个HashMapSessionKey, Session。流量分析场景的特点是连接数量大但单连接存活时间短所以必须有超时回收机制。我的做法是定时任务周期性扫描过期会话清理的同时把会话记录写入下游存储。public class SessionTable { private final MapSessionKey, Session sessions new HashMap(); private static final long TCP_IDLE_TIMEOUT 120_000; private static final long UDP_IDLE_TIMEOUT 60_000; private static final long MAX_SESSIONS 1_000_000; public void update(ParsedPacket pkt) { SessionKey key buildKey(pkt.srcIp, pkt.srcPort, pkt.dstIp, pkt.dstPort, 6); Session session sessions.computeIfAbsent(key, k - new Session()); session.record(true, pkt.payloadLen, pkt.flags); } public void evictExpired(long now) { IteratorMap.EntrySessionKey, Session it sessions.entrySet().iterator(); while (it.hasNext()) { Session s it.next().getValue(); long idle now - s.lastActiveAt; if (idle TCP_IDLE_TIMEOUT) { flushToStorage(s); // 落库或写日志 it.remove(); } } } }超时参数不是拍脑袋定的需要根据业务调整场景TCP idle 超时UDP idle 超时强制老化时间常规 IDC 出口监控120 秒60 秒8 小时公网入口安全分析300 秒120 秒24 小时内网横向移动检测60 秒30 秒2 小时MAX_SESSIONS是保护网。当表容量超过百万量级时HashMap 的重哈希开销会变得明显GC 压力也大。此时优先清理最老的 10% 会话而不是继续扩容。4.3 协议识别端口映射与特征匹配混用会话聚合解决了“谁和谁通信”的问题但还回答不了“跑了什么业务”。协议识别常见有两种手段端口映射和载荷特征匹配。端口映射效率高但不可靠因为很多业务使用非标准端口特征匹配准确但需要窥探 payload。实践里我把两者混用先看端口再看包负载前几个字节。协议端口特征载荷特征检查位置HTTP80, 8080, 8000GET,POST,HTTP/1TCP payload 起始字节TLS4430x16 0x03 0x01ClientHelloTCP payload 起始字节DNS53无固定前序依赖长度与类型UDP payloadSSH22SSH-2.0TCP payload 起始字节特征匹配只需在每个会话的第一个包上做一次命中后打上协议标签后续包直接走统计逻辑。注意 DN 的识别如果一条会话在三次握手后的首个数据包不是GET也不是0x16 0x03不要立即标记为 Unknown部分应用会先发 TLS 握手外的协议协商数据建议等待该方向前 3 个包再下结论。4.4 聚合结果输出按会话归并上下行指标会话表更新了一段时间后需要产出一份可读的结果。最常见的是 Top 会话排序sessions.values().stream() .sorted(Comparator.comparingLong(Session::totalBytes).reversed()) .limit(50) .forEach(s - System.out.printf( %s:%d - %s:%d up%dKB down%dKB packets%d syn%d%n, s.srcIp, s.srcPort, s.dstIp, s.dstPort, s.upBytes / 1024, s.downBytes / 1024, s.upPackets s.downPackets, s.synCount));在百万会话量级下全量排序的代价不可忽略。如果目的是快速定位热点可以采用分桶策略先把会话按 5 分钟时间窗切片每个窗内只保留各方向 Top 1000最终报表从这些窗口里合并。这套方式比全量排序内存友好得多。totalBytes的排序结果中如果一个会话的 SYN 数量远大于正常值通常会出现在上游——这正是去查攻击溯源报告的第一步线索。5. 验证与进阶pcap 回放、流重组与异常线索捕捉5.1 用离线 pcap 回放回归解析逻辑抓包环境不可控改了解析代码后不能总跑到线上抓一次包验证。我习惯把 tcpdump 抓下来的 pcap 文件保存起来用同一套代码离线回放作为回归测试的样本。Pcap4J 读取 pcap 文件的标准姿势PcapHandle offline Pcaps.openOffline(/tmp/capture.pcap); Packet packet; int count 0; while ((packet offline.getNextPacket()) ! null) { ParsedPacket parsed PacketParser.parseTcp(packet.getRawData()); if (parsed ! null) { SessionTable.update(parsed); count; } } offline.close(); System.out.println(replayed packets: count);openOffline不需要 root 权限这是离线回放优于在线抓包的地方。建议把之前踩过坑的 pcap 文件归档比如带 VLAN 标签的帧、畸形 TCP 头、IPv6 混合流量每次改完解析器先跑一遍这几个文件确保能读出不抛异常。5.2 TCP 流重组序列号与方向缓存会话统计只知道“传了多少字节”不知道“内容是什么”。要深入分析 HTTP 请求或检测恶意文件下载必须做 TCP 流重组。重组的基本结构是按方向维护两个有序缓存块列表以 TCP sequence number 为偏移依据TreeMapInteger, byte[] reassembled new TreeMap(); // 包到达时按 seq 与 payload 长度计算写入位置 // 若已存在重叠区域只写入新增部分 reassembled.put(seq, payload);收到乱序包时TreeMap 能让数据按 seq 排序后续按连续区间读取即可还原完整应用层数据。注意处理重复 ACK 和重传同一 seq 段若已存在则直接丢弃。这块逻辑是 Java 流量分析工具里最花时间的部分建议先只支持 HTTP 明文按连接重组TLS 流量解密不在初版范围内。5.3 从流量数字里找异常特征三个可落地的检测规则聚合出会话数据后可以叠加几个简单的检测规则来找攻击线索。这些规则不依赖复杂算法纯统计即可规则判定字段参考阈值说明SYN 扫描单源 IP 的 synCount / 目的 IP 数1 分钟内目的 IP 数 100未收到 ACK 的比例极高DNS 隧道查询域名长度 / 查询频率平均域名长度 40 或单 IP 查询频率 50/min通常伴随 TXT 记录异常重传单个会话 rstCount / 总包数比例 30%配合上下行字节数判断这三条规则的实现都不复杂在SessionTable的定时扫描逻辑里加上对应字段的判断即可。把 pcap 回放和这些规则接进同一个会话处理链路后这个 Java 分析器就完成了从“流量统计工具”到“线索发现工具”的蜕变——每次告警附带的回放包里能自动输出可疑连接列表和协议分布省去手动用 Wireshark 翻包的步骤。本文还有配套的精品资源点击获取