简介:这份资源是面向高校计算机与网络相关专业学生的Java Web课程设计完整项目,主题为跨平台网络流量实时监控与分析软件。项目采用Java完成后台开发,前端以Web客户端形式呈现,可解决无图形界面操作系统或远程目标机难以本地展示数据的问题,通过互联网将采集到的流量数据发送至浏览器进行分析,同时兼顾传输安全性与运行稳定性,适合作为课程设计参考或Java Web综合练习素材。压缩包共77个文件,约11.31MB,以27个java源码、15个js与9个jsx前端脚本为核心,另含gradle构建脚本、jar依赖、html页面、json与properties配置、keystore与crt证书文件及课程设计报告书pdf等,覆盖源码、构建、证书与文档多个层面。目前已有323人学习下载。读者可据此了解流量采集、后台处理与Web展示的整体实现思路,参考证书配置与Gradle构建方式,并借助报告书梳理设计流程与模块划分。
1. 从一次线上卡顿排查说起:Java 做 Web 流量分析到底在分析什么
去年帮一个做企业级 Web 开发的朋友排查线上问题,现象很典型:每天上午十点接口响应从 200ms 涨到 3s,运维查了 CPU、内存、GC 都正常,最后抓包才发现是某个内部服务在疯狂重试一个已经下线的第三方接口,把连接池占满了。这件事让我意识到,很多团队缺的不是监控大盘,而是一个能自己掌控、能按业务维度拆解的 Web 网络流量分析工具。用 Java 来做这件事有天然优势:抓包有 pcap4j、解析有 Netty 的 ByteBuf、统计有 Stream API、Web 展示有 Spring Boot 加前端图表,整条链路都能用一套语言闭环。这篇笔记就围绕「基于 Java 实现的 Web 网络流量分析软件」这个方向,把抓包、解析、会话还原、指标统计、Web 可视化这条落地路径拆开讲清楚,适合有 Java 基础、想做一个能真正跑起来、能看懂自己业务流量的工程师,也适合正在找 Java 课程设计案例源码或企业级 Web 开发练手项目的同学。
2. 抓包与协议解析:Java 侧的三条技术路线怎么选
2.1 先想清楚:你要的是全量镜像还是本机抓包
做流量分析第一步不是写代码,而是确定数据从哪来。常见做法有三种:一是本机网卡抓包,用 pcap4j 或 jNetPcap 直接监听 eth0 或 wlan0,适合开发调试和单机服务分析;二是交换机端口镜像,把核心交换机的流量复制一份到分析服务器,适合企业级 Web 开发场景下分析整个业务集群;三是应用层埋点,在 Spring Boot 的 Filter 或 Interceptor 里记录请求响应,严格说这不算网络层流量,但胜在能拿到业务字段。我一般会先问清楚:你是要分析 TCP 重传、握手延迟这类网络问题,还是要分析接口调用频次、慢请求分布这类业务问题?前者必须走抓包,后者埋点更省事。如果两个都要,那就抓包做底层、埋点做上层,用同一个 traceId 串起来。
选 pcap4j 的理由很实际:它是纯 Java 封装 libpcap,Maven 直接引,跨平台比 jNetPcap 省心,社区文档也够用。jNetPcap 性能略好但 native 库绑定麻烦,新手容易在环境变量配置上翻车。下面这段是最小可运行的抓包骨架。
// pom.xml 依赖:org.pcap4j:pcap4j-core:1.8.2 与 pcap4j-packetfactory-static import org.pcap4j.core.*; import org.pcap4j.packet.Packet; public class CaptureDemo { public static void main(String[] args) throws Exception { // 1. 列出所有网卡,生产环境建议按名称或描述过滤,别用 getDevByAddress 硬编码 for (PcapNetworkInterface nif : Pcaps.findAllDevs()) { System.out.println(nif.getName() + " -> " + nif.getDescription()); } // 2. 选定网卡,snaplen 设 65536 保证不截断,promisc 开启混杂模式 PcapNetworkInterface nif = Pcaps.getDevByName("eth0"); int snapLen = 65536; PcapNetworkInterface.PromiscuousMode mode = PcapNetworkInterface.PromiscuousMode.PROMISCUOUS; int timeout = 10; // 毫秒 PcapHandle handle = nif.openLive(snapLen, mode, timeout); // 3. BPF 过滤:只抓 80/443/8080,减少无关流量,这是性能关键 handle.setFilter("tcp port 80 or tcp port 443 or tcp port 8080", BpfProgram.BpfCompileMode.OPTIMIZE); // 4. 循环抓包,生产环境务必放到独立线程并加背压 PacketListener listener = packet -> { // 这里只做最轻量的入队,解析交给下游线程池 System.out.println(packet); }; handle.loop(-1, listener); // -1 表示无限循环 } }逻辑说明:findAllDevs列出网卡是为了让你确认抓哪块,别上来就写死。openLive的 snaplen 设 65536 是避免大包被截断导致后续解析失败,promisc 模式在镜像口场景下必须开。setFilter用 BPF 语法在 kernel 层过滤,比抓上来再丢效率高一个数量级。loop是阻塞的,实际项目里我会把它丢进单独线程,回调里只做入队,解析和统计放到消费线程,否则抓包线程一慢就丢包。
参数怎么调:timeout 设 10ms 是平衡延迟和 CPU 的常用值,高流量场景可以降到 1ms;snaplen 如果只关心头部可以设 128,但要做 HTTP body 分析就必须 65536;BPF 过滤表达式建议按业务端口收窄,别图省事抓全部。
2.2 解析到哪一层:以太网、IP、TCP、HTTP 的拆包顺序
抓到包只是字节数组,真正有价值的是解析出五元组和协议字段。pcap4j 的 Packet 对象是嵌套结构,packet.get(TcpPacket.class)能直接拿到 TCP 层,但要注意它内部是逐层解析的,遇到分片包或异常包会返回 null,必须判空。常见做法是先判IpV4Packet,再判TcpPacket,最后看 payload 是不是 HTTP。
import org.pcap4j.packet.*; public class PacketParser { public static void parse(Packet packet) { IpV4Packet ip = packet.get(IpV4Packet.class); if (ip == null) return; // 非 IPv4,可能是 ARP 或 IPv6,按需处理 TcpPacket tcp = packet.get(TcpPacket.class); if (tcp == null) return; String srcIp = ip.getHeader().getSrcAddr().getHostAddress(); String dstIp = ip.getHeader().getDstAddr().getHostAddress(); int srcPort = tcp.getHeader().getSrcPort().valueAsInt(); int dstPort = tcp.getHeader().getDstPort().valueAsInt(); // 五元组作为会话 key,注意方向:双向流量要归一化 String sessionKey = normalize(srcIp, srcPort, dstIp, dstPort); byte[] payload = tcp.getPayload() == null ? new byte[0] : tcp.getPayload().getRawData(); if (payload.length > 0 && (dstPort == 80 || srcPort == 80)) { // HTTP 明文才可直接解析,443 是 TLS 密文,需要另做处理 String httpText = new String(payload, java.nio.charset.StandardCharsets.ISO_8859_1); if (httpText.startsWith("GET") || httpText.startsWith("POST")) { System.out.println(sessionKey + " -> " + httpText.split("\r\n")[0]); } } } // 归一化:让 A->B 和 B->A 落到同一个 key,方便统计双向流量 private static String normalize(String sIp, int sPort, String dIp, int dPort) { if (sIp.compareTo(dIp) < 0 || (sIp.equals(dIp) && sPort <= dPort)) { return sIp + ":" + sPort + "-" + dIp + ":" + dPort; } return dIp + ":" + dPort + "-" + sIp + ":" + sPort; } }逻辑说明:get(IpV4Packet.class)是 pcap4j 的便捷方法,内部会逐层匹配,比手动 instanceof 清爽。五元组归一化是会话统计的基础,不做归一化会把一次请求拆成两条单向记录,统计出来的连接数直接翻倍。payload 用 ISO_8859_1 解码是为了不破坏二进制字节,HTTP 头本身是 ASCII,body 再按 Content-Type 二次解码。
参数说明:HTTP 解析只对 80 端口明文有效,443 端口拿到的是 TLS 记录层,需要先做 TLS 握手解析或直接放弃 body 只统计流量。如果业务用了非标准端口,BPF 和解析逻辑都要同步改。分片包在 pcap4j 里默认不重组,遇到大响应体可能解析不全,需要自己实现 IP 分片重组或引入更上层的库。
3. 会话还原与指标统计:把字节流变成能看的数字
3.1 用 ConcurrentHashMap 做会话表,注意内存和过期
解析出五元组之后,下一步是把同一个会话的所有包聚到一起。最直接的做法是ConcurrentHashMap<String, Session>,key 用归一化五元组,value 里累计上下行字节数、包数、首包时间、末包时间、TCP 标志位计数。这里有个血泪经验:如果不做过期清理,跑一天内存就爆了,因为短连接会不断产生新 key。
import java.util.concurrent.*; public class SessionTracker { // 会话表,key 为归一化五元组 private final ConcurrentHashMap<String, Session> sessions = new ConcurrentHashMap<>(); // 定时清理:每 30 秒扫一次,超过 120 秒没更新的会话归档并移除 private final ScheduledExecutorService cleaner = Executors.newSingleThreadScheduledExecutor(); public SessionTracker() { cleaner.scheduleAtFixedRate(this::evict, 30, 30, TimeUnit.SECONDS); } public void onPacket(String key, int payloadLen, boolean isUpstream, long ts, boolean syn, boolean fin) { Session s = sessions.computeIfAbsent(key, k -> new Session(ts)); s.packetCount.incrementAndGet(); if (isUpstream) s.upBytes.addAndGet(payloadLen); else s.downBytes.addAndGet(payloadLen); s.lastSeen = ts; if (syn) s.synCount.incrementAndGet(); if (fin) s.finCount.incrementAndGet(); } private void evict() { long now = System.currentTimeMillis(); sessions.entrySet().removeIf(e -> { if (now - e.getValue().lastSeen > 120_000) { archive(e.getValue()); // 归档到统计库或日志 return true; } return false; }); } private void archive(Session s) { /* 写入时序库或聚合统计 */ } static class Session { final long firstSeen; volatile long lastSeen; final java.util.concurrent.atomic.LongAdder upBytes = new java.util.concurrent.atomic.LongAdder(); final java.util.concurrent.atomic.LongAdder downBytes = new java.util.concurrent.atomic.LongAdder(); final java.util.concurrent.atomic.LongAdder packetCount = new java.util.concurrent.atomic.LongAdder(); final java.util.concurrent.atomic.LongAdder synCount = new java.util.concurrent.atomic.LongAdder(); final java.util.concurrent.atomic.LongAdder finCount = new java.util.concurrent.atomic.LongAdder(); Session(long ts) { this.firstSeen = ts; this.lastSeen = ts; } } }逻辑说明:用LongAdder而不是AtomicLong是因为高并发下 LongAdder 的写性能更好,代价是读取时不是强一致,对统计场景完全够用。computeIfAbsent保证会话只创建一次。evict用removeIf原子移除,避免遍历时并发修改。归档这一步别省,否则你只能看到当前活跃会话,历史趋势就丢了。
参数说明:清理周期 30 秒、过期阈值 120 秒是经验值,短连接多的业务可以把阈值降到 60 秒,长连接多的(比如 WebSocket)要单独识别并延长。lastSeen用 volatile 保证可见性,但更新不是原子的,极端情况下可能少算几毫秒,不影响统计。
3.2 从会话表里能算出哪些真正有用的指标
会话表建好之后,指标就是聚合查询的事。我一般会算这几类:一是流量类,上下行总字节、包数、平均包大小;二是连接类,新建连接数、并发连接数、SYN 重传数;三是质量类,握手延迟(SYN 到 SYN-ACK 的时间差)、首包延迟、TCP 重传率;四是业务类,如果解析了 HTTP,还能算 URL 频次、状态码分布、慢请求 TOP N。这些指标用 Java Stream 对会话表做一次遍历就能出来。
// 假设 sessions 是当前活跃会话集合 var stats = sessions.values().stream().collect(java.util.stream.Collectors.teeing( java.util.stream.Collectors.summingLong(s -> s.upBytes.sum() + s.downBytes.sum()), java.util.stream.Collectors.averagingLong(s -> s.packetCount.sum()), (totalBytes, avgPackets) -> new Object() { long bytes = totalBytes; double avgPkt = avgPackets; } ));逻辑说明:teeing是 Java 12 之后的双收集器,一次遍历同时算总量和均值,比遍历两遍优雅。实际项目里指标更多,建议封装成MetricsCollector类,每个指标一个方法,方便单测。TCP 重传率需要额外记录序列号,pcap4j 的 TcpPacket 能拿到getSequenceNumber,发现同一序列号重复出现就计一次重传。
参数说明:平均包大小能反映业务类型,小包多说明是交互型,大包多说明是传输型。握手延迟超过 100ms 就要警惕网络质量。这些阈值没有绝对标准,建议先跑一周基线,再用基线做告警。
4. Web 可视化与实时推送:让数据在浏览器里动起来
4.1 Spring Boot 后端接口怎么设计才不拖后腿
分析结果要给人看,最省事的方案是 Spring Boot 提供 REST 接口,前端定时轮询。但流量数据是持续产生的,轮询延迟高、无效请求多,更好的做法是 WebSocket 或 SSE 推送。我一般用 SSE,因为它是单向推送、基于 HTTP、实现简单,浏览器原生EventSource就能接。
import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.util.concurrent.*; @RestController @RequestMapping("/api/traffic") public class TrafficController { private final CopyOnWriteArrayList<SseEmitter> emitters = new CopyOnWriteArrayList<>(); private final SessionTracker tracker; public TrafficController(SessionTracker tracker) { this.tracker = tracker; } @GetMapping("/stream") public SseEmitter stream() { SseEmitter emitter = new SseEmitter(0L); // 0 表示不超时 emitters.add(emitter); emitter.onCompletion(() -> emitters.remove(emitter)); emitter.onTimeout(() -> emitters.remove(emitter)); return emitter; } // 由定时任务每秒调用,推送聚合指标 public void pushMetrics() { var snapshot = tracker.snapshot(); // 返回当前指标快照 for (SseEmitter e : emitters) { try { e.send(SseEmitter.event().name("metrics").data(snapshot)); } catch (Exception ex) { emitters.remove(e); // 推送失败说明连接已断,移除 } } } }逻辑说明:SseEmitter(0L)表示不主动超时,适合长连接推送。CopyOnWriteArrayList适合读多写少的场景,推送时遍历不会加锁。推送失败必须移除 emitter,否则会积累大量死连接。pushMetrics由@Scheduled(fixedRate = 1000)驱动,每秒推一次,前端拿到就更新图表。
参数说明:推送频率 1 秒是平衡实时性和带宽的常用值,高精度场景可以到 200ms,但要注意前端渲染压力。如果客户端很多,建议先做聚合再推送,别每个连接单独算。
4.2 前端图表选型和几个容易忽略的细节
前端我一般用 ECharts 或 Chart.js,ECharts 对时序数据的dataZoom和large模式支持更好,适合流量曲线。关键细节有三个:一是时间轴要用服务端时间戳,别用浏览器本地时间,否则多客户端对不齐;二是数据点要做滑动窗口,只保留最近 5 分钟,否则内存涨得比后端还快;三是 WebSocket/SSE 断线要自动重连,EventSource自带重连但间隔是固定的,可以自己封装指数退避。
// 前端 SSE 接入与滑动窗口 const MAX_POINTS = 300; // 5 分钟,每秒一个点 const series = { up: [], down: [], time: [] }; const es = new EventSource('/api/traffic/stream'); es.addEventListener('metrics', (e) => { const d = JSON.parse(e.data); series.time.push(d.ts); series.up.push(d.upBytes); series.down.push(d.downBytes); if (series.time.length > MAX_POINTS) { series.time.shift(); series.up.shift(); series.down.shift(); } chart.setOption({ xAxis: { data: series.time }, series: [ { data: series.up }, { data: series.down } ]}); }); es.onerror = () => { /* EventSource 会自动重连,这里可加提示 */ };逻辑说明:滑动窗口用shift移除最老的点,保证数组长度恒定。setOption每次全量更新在 300 点以内性能没问题,超过就要用appendData或增量更新。onerror里不要手动close,否则自动重连就失效了。
参数说明:MAX_POINTS根据展示时长和推送频率算,5 分钟乘 1Hz 就是 300。如果推送频率是 200ms,同样时长就是 1500 点,建议改用降采样或分页加载。
5. 避坑与排查:那些让我加班到凌晨的细节
5.1 抓不到包,先查权限和网卡模式
现象:代码跑起来没报错,但一个包都抓不到。原因:Linux 下普通用户没有 raw socket 权限,或者选错了网卡。解决:用setcap cap_net_raw,cap_net_admin=eip /path/to/java给 Java 授能,或者临时用 root 跑;网卡用ip link确认名称,容器里要确认是不是eth0,K8s 环境可能是cali开头的虚拟网卡。
5.2 解析 HTTP 全是乱码,检查端口和编码
现象:payload 解出来是乱码,或者startsWith("GET")永远 false。原因:抓的是 443 端口的 TLS 密文,或者 payload 被 snaplen 截断了。解决:确认 BPF 过滤的是 80 端口;snaplen 设 65536;如果业务强制 HTTPS,要么在应用层埋点,要么只统计流量不解析内容。
5.3 内存持续上涨,会话表没清理
现象:跑几小时后 OOM,堆 dump 里全是 Session 对象。原因:短连接产生大量 key,evict没生效或阈值太大。解决:检查evict是否真的被调度,阈值按业务连接时长调整;用jmap -histo确认对象数量;必要时给会话表设上限,超过就按 LRU 淘汰。
5.4 统计的连接数翻倍,五元组没归一化
现象:同一对 IP 之间的连接被算成两条。原因:A->B 和 B->A 的 key 不同。解决:用前面normalize方法做字典序归一化;如果业务有 NAT,还要考虑 NAT 前后的映射,必要时用应用层 traceId 关联。
5.5 SSE 推送延迟高,emitter 列表里有死连接
现象:前端图表更新越来越慢。原因:断开的连接没从emitters移除,每次推送都在做无用功。解决:onCompletion和onTimeout都要移除;推送异常时也移除;定期打印emitters.size()观察是否异常增长。
6. 进阶:把分析结果落到告警和容量规划上
做到可视化只是第一步,真正让这个工具产生价值的是把它接到告警和容量规划里。我一般会设三类告警:一是突增类,某 IP 的连接数 1 分钟内涨 10 倍,可能是爬虫或攻击;二是质量类,握手延迟 P99 超过 200ms,说明网络或对端有问题;三是容量类,并发连接数持续超过历史峰值 80%,该扩容了。告警规则别拍脑袋,先跑两周基线,用基线加 3 倍标准差做阈值,误报会少很多。
验证方法上,我会用tc命令人为注入延迟和丢包,看分析工具能不能准确反映。比如tc qdisc add dev eth0 root netem delay 100ms loss 5%,然后观察握手延迟和重传率指标是否同步上升。这个法子比等线上出问题再验证靠谱得多。
| 指标 | 采集方式 | 建议阈值 | 告警动作 |
|---|---|---|---|
| 新建连接数 | 会话表按秒聚合 | 基线 3 倍 | 通知值班 |
| 握手延迟 P99 | SYN 到 SYN-ACK 差值 | 200ms | 检查网络 |
| TCP 重传率 | 序列号重复计数 | 1% | 检查链路 |
| 并发连接数 | 活跃会话数 | 峰值 80% | 容量评估 |
最后说个我自己的习惯:这个工具我从来不在生产环境直接跑全量抓包,而是先在测试环境用镜像流量验证解析逻辑,确认指标对得上再上生产,并且生产环境一定加采样,比如 10% 采样率,既能看到趋势又不至于把分析机压垮。流量分析这件事,准比全重要,能长期稳定跑比一次抓得全更有价值。希望帮到你。
本文还有配套的精品资源,点击获取