
简介这份资源是面向高校计算机与网络相关专业学生的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 封装 libpcapMaven 直接引跨平台比 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 的常用值高流量场景可以降到 1mssnaplen 如果只关心头部可以设 128但要做 HTTP body 分析就必须 65536BPF 过滤表达式建议按业务端口收窄别图省事抓全部。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 头本身是 ASCIIbody 再按 Content-Type 二次解码。参数说明HTTP 解析只对 80 端口明文有效443 端口拿到的是 TLS 记录层需要先做 TLS 握手解析或直接放弃 body 只统计流量。如果业务用了非标准端口BPF 和解析逻辑都要同步改。分片包在 pcap4j 里默认不重组遇到大响应体可能解析不全需要自己实现 IP 分片重组或引入更上层的库。3. 会话还原与指标统计把字节流变成能看的数字3.1 用 ConcurrentHashMap 做会话表注意内存和过期解析出五元组之后下一步是把同一个会话的所有包聚到一起。最直接的做法是ConcurrentHashMapString, Sessionkey 用归一化五元组value 里累计上下行字节数、包数、首包时间、末包时间、TCP 标志位计数。这里有个血泪经验如果不做过期清理跑一天内存就爆了因为短连接会不断产生新 key。import java.util.concurrent.*; public class SessionTracker { // 会话表key 为归一化五元组 private final ConcurrentHashMapString, 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 CopyOnWriteArrayListSseEmitter 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.jsECharts 对时序数据的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_admineip /path/to/java给 Java 授能或者临时用 root 跑网卡用ip link确认名称容器里要确认是不是eth0K8s 环境可能是cali开头的虚拟网卡。5.2 解析 HTTP 全是乱码检查端口和编码现象payload 解出来是乱码或者startsWith(GET)永远 false。原因抓的是 443 端口的 TLS 密文或者 payload 被 snaplen 截断了。解决确认 BPF 过滤的是 80 端口snaplen 设 65536如果业务强制 HTTPS要么在应用层埋点要么只统计流量不解析内容。5.3 内存持续上涨会话表没清理现象跑几小时后 OOM堆 dump 里全是 Session 对象。原因短连接产生大量 keyevict没生效或阈值太大。解决检查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 倍通知值班握手延迟 P99SYN 到 SYN-ACK 差值200ms检查网络TCP 重传率序列号重复计数1%检查链路并发连接数活跃会话数峰值 80%容量评估最后说个我自己的习惯这个工具我从来不在生产环境直接跑全量抓包而是先在测试环境用镜像流量验证解析逻辑确认指标对得上再上生产并且生产环境一定加采样比如 10% 采样率既能看到趋势又不至于把分析机压垮。流量分析这件事准比全重要能长期稳定跑比一次抓得全更有价值。希望帮到你。本文还有配套的精品资源点击获取