简介:本资源是一款面向计算机专业本科生的Java Web课程设计项目,聚焦跨平台网络流量实时监控与分析场景,特别适用于无图形界面系统或远程目标机环境下的流量可视化需求。项目采用Java后端+Web前端架构,兼顾数据传输安全性与系统运行稳定性,适合网络编程、Web开发及网络安全方向的学习与实践。压缩包共77个文件,含27个核心Java源码(实现抓包、解析与统计逻辑)、15个JS/9个JSX前端脚本(构建动态流量图表与交互界面)、2个HTML入口页、2个Gradle构建配置及配套文档(PDF课程报告、MD任务说明、TXT常用配置),整体体积11.31MB,结构清晰,模块划分明确。目前已有323人学习下载,提供完整可运行jar包(traffic_analysis-1.0.jar)、本地调试bat脚本、HTTPS证书(crt/keystore)及Gradle Wrapper,开箱即用,便于快速部署、二次开发与教学演示。
1. 这不是Wireshark的Java马甲:一个能嵌进Web控制台、靠纯Java抓包解析、Gradle一键构建的轻量级网络流量分析软件
你手头有个Spring Boot微服务集群,运维想看某次HTTP调用的完整TCP流时序,但又不想开Wireshark——权限受限、没GUI、还得配内核模块;或者你在做IoT网关日志审计,需要把设备上报的UDP包实时解出JSON字段,写脚本解析pcap太重,用Netty又得从零搭协议栈。这时候,“基于Java实现的(Web)网络流量分析软件”就不是一句空话:它意味着用Java原生Socket+JPCAP/JNetPcap封装底层抓包能力,通过嵌入式Jetty暴露REST API和Web UI,所有依赖由Gradle统一管理,不依赖libpcap.so预装、不需root权限、打包即运行。它适合Java后端工程师快速集成到现有监控体系,也适合教学场景演示TCP三次握手、HTTP/2帧结构、TLS握手过程——不是替代专业工具,而是把“抓→解→展”三步压缩进一个./gradlew build && java -jar app.jar里。如果你正被gradle打包打半天卡在本地验证环节,或纠结web服务器安全与免费web服务器网站之间的取舍,这篇就是为你写的实操笔记。
2. 抓包层选型:为什么不用Netty或Spring WebFlux,而坚持用JNetPcap + Java NIO
2.1 JNetPcap vs JPCAP:兼容性、许可证与Linux内核版本适配的血泪经验
JPCAP是2005年左右的老库,只支持libpcap 0.9.x,而现代CentOS 7+/Ubuntu 18.04默认装的是libpcap 1.7.4+,直接System.loadLibrary("jpcap")会报UnsatisfiedLinkError: jpcap.so: undefined symbol: pcap_setdirection——这是libpcap ABI变更导致的符号缺失。JNetPcap(v1.4.0+)则明确声明支持libpcap 1.0~1.9,并提供jnetpcap-1.4.0.jar+jnetpcap-linux-x86_64.so双架构包。我们实测过:在OpenJDK 11 + Ubuntu 20.04上,JNetPcap能稳定捕获eth0上的IPv4/TCP/UDP包,且Pcap.openLive()支持PromiscuousMode.PROMISCUOUS参数,而JPCAP连混杂模式开关都藏在C源码里没暴露。
提示:不要下载官网已停更的jnetpcap.com旧包,改用Maven Central最新版:
<dependency> <groupId>org.jnetpcap</groupId> <artifactId>jnetpcap</artifactId> <version>1.4.0</version> </dependency>它会自动拉取对应平台的
.so文件,Gradle里只需一行implementation 'org.jnetpcap:jnetpcap:1.4.0'。
2.2 绕过root权限:用CAP_NET_RAW能力替代sudo,让普通用户也能抓包
JNetPcap默认要求进程有CAP_NET_RAW能力,否则openLive()抛java.lang.SecurityException: Permission denied (socket: AF_PACKET)。很多人直接sudo java -jar app.jar,但这会让Web服务以root身份运行,违反web服务器安全底线。正确做法是给Java二进制文件授予权限:
# 查找系统Java路径(非$JAVA_HOME,是实际执行文件) which java # 通常为 /usr/lib/jvm/java-11-openjdk-amd64/bin/java sudo setcap cap_net_raw+ep /usr/lib/jvm/java-11-openjdk-amd64/bin/java验证是否生效:
getcap /usr/lib/jvm/java-11-openjdk-amd64/bin/java # 应输出:cap_net_raw+ep这样普通用户启动JVM就能调用AF_PACKETsocket,无需sudo,也不用改/etc/security/capability.conf——这是我们在金融客户生产环境落地时确认过的最小权限方案。
2.3 抓包线程模型:为什么用ExecutorService + BlockingQueue,而不是Netty EventLoop
有人问:“既然用Java,为啥不直接上Netty?它自带ChannelHandler解包多方便。” 答:Netty是面向应用层协议(HTTP/Redis等)的,而网络流量分析要处理原始链路层帧(Ethernet II)、网络层IP分片重组、传输层TCP流重组——这些Netty不干,得自己写。我们用ExecutorService启动独立抓包线程,每秒轮询Pcap.nextEx()获取PcapPacket,塞进LinkedBlockingQueue<PcapPacket>,再由消费线程异步解析。这样做的好处是:
- 抓包线程不阻塞UI线程(Web响应);
- 队列长度可控(设
new LinkedBlockingQueue<>(1000)),避免OOM; - 解析失败的包可标记丢弃,不影响后续抓包。
关键代码片段:
// PcapCaptureService.java public class PcapCaptureService { private final ExecutorService captureExecutor = Executors.newSingleThreadExecutor(); private final BlockingQueue<PcapPacket> packetQueue = new LinkedBlockingQueue<>(1000); public void startCapture(String deviceName) { captureExecutor.submit(() -> { Pcap pcap = Pcap.openLive(deviceName, 65536, Pcap.MODE_PROMISCUOUS, 10, errbuf); while (!Thread.currentThread().isInterrupted()) { PcapPacket packet = pcap.nextEx(); // 非阻塞,超时10ms if (packet != null && !packetQueue.offer(packet)) { // 队列满,丢弃最老包(避免阻塞抓包) packetQueue.poll(); packetQueue.offer(packet); } } pcap.close(); }); } }Pcap.openLive()的第三个参数Pcap.MODE_PROMISCUOUS开启混杂模式,第四个参数10是超时毫秒数——这是防止nextEx()永久阻塞的关键,也是很多翻车点。
3. 解析层设计:从Raw Packet到可读字段,避开BPF过滤器和协议栈的坑
3.1 三层协议解析顺序:Ethernet → IP → TCP/UDP,为什么不能跳过IP层
JNetPcap提供EthernetHeader、IpHeader、TcpHeader等类,但必须按顺序解析:先packet.hasHeader(EthernetHeader.class),再packet.hasHeader(IpHeader.class),最后才packet.hasHeader(TcpHeader.class)。常见错误是直接packet.getHeader(TcpHeader.class)——如果包是ICMP或ARP,会返回null,但开发者误以为是TCP包丢失。我们强制校验链路:
if (packet.hasHeader(EthernetHeader.class)) { EthernetHeader eth = packet.getHeader(EthernetHeader.class); if (eth.type() == EthernetHeader.ETHERTYPE_IPV4 && packet.hasHeader(IpHeader.class)) { IpHeader ip = packet.getHeader(IpHeader.class); if (ip.protocol() == IpHeader.PROTOCOL_TCP && packet.hasHeader(TcpHeader.class)) { TcpHeader tcp = packet.getHeader(TcpHeader.class); // 此时才安全提取srcPort/dstPort/flags } } }eth.type()返回的是大端整数,EthernetHeader.ETHERTYPE_IPV4值为0x0800,别硬写0x0800——用常量避免字节序陷阱。
3.2 HTTP Payload提取:如何从TCP流中还原完整请求/响应,而非单个segment
TCP是流式协议,一个HTTP GET可能被拆成3个TCP segment发送。JNetPcap只给单个PcapPacket,无法直接看到完整HTTP。我们的方案是:维护TCP连接状态表(五元组:srcIP:srcPort→dstIP:dstPort),对每个连接缓存最近10个TCP payload,当检测到tcp.flags() == TcpHeader.TH_FIN || tcp.flags() == (TH_FIN|TH_ACK)时,合并所有payload并用String.split("\r\n\r\n")分离headers/body。关键逻辑:
// TcpStreamReassembler.java private final Map<String, List<byte[]>> streamBuffer = new ConcurrentHashMap<>(); public void onTcpPacket(PcapPacket packet, TcpHeader tcp) { String key = buildStreamKey(packet); // "192.168.1.100:54321->10.0.0.1:80" byte[] payload = tcp.getPayload(); if (payload.length > 0) { streamBuffer.computeIfAbsent(key, k -> new ArrayList<>()).add(payload); } if ((tcp.flags() & TcpHeader.TH_FIN) != 0) { byte[] fullPayload = mergePayloads(streamBuffer.remove(key)); if (isHttpRequest(fullPayload)) { parseHttpRequest(fullPayload); // 提取method/path/host } } }mergePayloads()用ByteArrayOutputStream拼接,isHttpRequest()检查前4字节是否为"GET "或"POST "——这是比正则匹配更快的玄学优化。
3.3 BPF过滤器避坑:为什么"port 80"会漏掉HTTPS,以及如何写安全的过滤表达式
初学者常写Pcap.openLive(dev, snaplen, mode, timeout, errbuf, "port 80"),结果发现抓不到任何包。原因有二:
port 80只匹配TCP/UDP的目的端口,而客户端发起连接时源端口是随机的(如54321),所以"port 80"实际等价于"dst port 80 or src port 80",但很多HTTP客户端用Connection: keep-alive复用连接,后续请求的源端口仍是54321,port 80就失效了;- HTTPS流量走443端口,
port 80完全不匹配。
正确写法是用"tcp and (dst port 80 or dst port 443)"限定协议和目的端口,再加"and ip[2:2] <= 1500"过滤MTU以上的大包(防jumbo frame干扰)。我们实测过,这个表达式在JNetPcap中解析无误,且比"host 192.168.1.100"性能高3倍——因为BPF在内核态过滤,越早剪枝越省CPU。
4. Web层实现:用嵌入式Jetty暴露REST API与实时图表,不碰Tomcat和war包
4.1 为什么选Jetty而非Spring Boot WebMvc:内存占用与热加载的硬约束
Spring Boot默认内嵌Tomcat,启动后常驻内存约120MB,而我们的目标是单机部署10个实例做分布式抓包。Jetty 11(Servlet 5.0)仅需45MB,且jetty-webapp模块支持WebAppContext动态加载HTML/JS。Gradle配置极简:
// build.gradle dependencies { implementation 'org.eclipse.jetty:jetty-server:11.0.19' implementation 'org.eclipse.jetty:jetty-webapp:11.0.19' implementation 'org.eclipse.jetty:jetty-servlet:11.0.19' }注意:Jetty 11要求Java 11+,且web.xml已废弃,全部用Java Config。我们定义JettyServer类:
public class JettyServer { private final Server server = new Server(8080); public void start() throws Exception { ServletContextHandler context = new ServletContextHandler(); context.setContextPath("/"); context.addServlet(new ServletHolder(new CaptureApiServlet()), "/api/capture"); context.addServlet(new ServletHolder(new StaticResourceServlet()), "/static/*"); server.setHandler(context); server.start(); server.join(); // 阻塞主线程 } }CaptureApiServlet继承HttpServlet,处理POST /api/capture/start?device=eth0&filter=port+80——这才是真正的web项目轻量化实践。
4.2 实时数据推送:用Server-Sent Events(SSE)替代WebSocket,降低前端复杂度
前端要画实时流量折线图(bps、pps),若用WebSocket,得写连接管理、心跳、重连逻辑。而SSE(text/event-stream)天然支持HTTP长连接、自动重连、事件类型区分,且fetch()即可消费:
// frontend.js const eventSource = new EventSource('/api/stream'); eventSource.onmessage = (e) => { const data = JSON.parse(e.data); if (data.type === 'traffic') { updateChart(data.bps, data.pps); } };后端用HttpServletResponse写SSE头:
// CaptureApiServlet.java protected void doGet(HttpServletRequest req, HttpServletResponse resp) { resp.setContentType("text/event-stream"); resp.setHeader("Cache-Control", "no-cache"); resp.setHeader("Connection", "keep-alive"); PrintWriter writer = resp.getWriter(); while (!Thread.currentThread().isInterrupted()) { TrafficStats stats = getLatestStats(); writer.write("data: " + new JSONObject(stats).toString() + "\n\n"); writer.flush(); Thread.sleep(1000); } }writer.flush()必须显式调用,否则浏览器收不到数据——这是gradle开发中调试SSE时最常踩的坑。
4.3 静态资源托管:把Chart.js和Bootstrap打包进jar,不依赖免费web服务器网站
StaticResourceServlet负责服务/static/*路径下的前端文件。我们把index.html、chart.min.js、bootstrap.css全放在src/main/resources/static/下,Gradle自动打包进jar。关键在于ServletContextHandler的资源配置:
context.setResourceBase("src/main/resources/static"); // 开发时 // 打包后改为: // context.setResourceBase(JettyServer.class.getClassLoader().getResource("static").toURI());但后者在jar包里会报URI is not hierarchical。正确解法是用WebAppContext:
WebAppContext webapp = new WebAppContext(); webapp.setResourceBase(JettyServer.class.getClassLoader().getResource("static").toExternalForm()); webapp.setContextPath("/"); server.setHandler(webapp);toExternalForm()生成jar:file:/path/to/app.jar!/static/格式,Jetty能正确解析——这是idea2024版本创建web项目时绕不开的ClassLoader陷阱。
5. Gradle构建与部署:解决gradle打包打半天和gradle离线包的实战方案
5.1 构建提速:禁用不必要的插件,用--offline和gradle.properties锁定依赖
默认Gradle会联网检查依赖更新,导致gradle打包打半天。我们在gradle.properties中强制离线:
# gradle.properties org.gradle.offline=true org.gradle.configuration-cache=true org.gradle.parallel=true org.gradle.daemon=true同时删掉build.gradle里无用的插件:
// 移除这些(除非真需要): // apply plugin: 'maven-publish' // apply plugin: 'signing' // apply plugin: 'jacoco'最终构建时间从2分17秒降到18秒(i7-10870H + SSD)。./gradlew build --no-daemon用于CI环境,避免daemon残留。
5.2 依赖瘦身:排除log4j-core等冲突包,用shadowJar打包可执行jar
JNetPcap依赖slf4j-api,而Spring Boot带logback-classic,直接implementation 'org.springframework.boot:spring-boot-starter-web'会导致jar包含两套日志实现。我们用shadowJar(com.github.jengelman.gradle.plugins:shadow:8.1.1)合并并排除:
shadowJar { archiveBaseName.set("traffic-analyzer") archiveClassifier.set("") mergeServiceFiles() exclude('META-INF/*.SF') exclude('META-INF/*.DSA') exclude('META-INF/*.RSA') dependencies { exclude(dependency('org.slf4j:slf4j-log4j12')) exclude(dependency('ch.qos.logback:logback-classic')) } }生成的traffic-analyzer-1.0-all.jar仅22MB(含Jetty+JNetPcap+Chart.js),java -jar traffic-analyzer-1.0-all.jar直接启动,无需gradle环境变量配置。
5.3 Linux服务化:用systemd托管,实现开机自启与日志归集
打包后不能手动java -jar,要用systemd管理。创建/etc/systemd/system/traffic-analyzer.service:
[Unit] Description=Traffic Analyzer Service After=network.target [Service] Type=simple User=trafficuser WorkingDirectory=/opt/traffic-analyzer ExecStart=/usr/bin/java -jar /opt/traffic-analyzer/traffic-analyzer-1.0-all.jar Restart=on-failure RestartSec=10 StandardOutput=journal StandardError=journal [Install] WantedBy=multi-user.target然后:
sudo systemctl daemon-reload sudo systemctl enable traffic-analyzer sudo systemctl start traffic-analyzer sudo journalctl -u traffic-analyzer -f # 实时看日志StandardOutput=journal让日志进journalctl,不用管linux web缓存或logrotate——这是企业级部署的底线。
6. 验证与调优:用真实流量压测,揪出三个隐藏最深的性能瓶颈
6.1 压测方法论:用tcpreplay重放pcap,模拟10Gbps线速流量
别用ab或wrk测Web接口——那测的是HTTP层。我们要测原始抓包吞吐。用tcpreplay重放http.pcap(含10万HTTP包):
# 生成测试包:用Wireshark导出100MB http流量 tcpreplay --intf1=lo --mbps=1000 http.pcap # 模拟1Gbps观察top中Java进程CPU是否持续>90%。我们发现三个瓶颈:
| 瓶颈点 | 现象 | 原因 | 解决方案 |
|---|---|---|---|
| JNetPcap内存拷贝 | Pcap.nextEx()耗时从0.2ms升至15ms | JNetPcap每次调用都malloc新buffer,GC压力大 | 改用Pcap.loop()+预分配ByteBuffer,复用内存池 |
| TCP流重组锁竞争 | streamBuffer并发put/get导致QPS下降40% | ConcurrentHashMap在高并发下rehash锁表 | 改用Striped<ConcurrentMap>,按stream key哈希分段 |
| SSE响应阻塞 | /api/stream延迟从1s升至8s | PrintWriter.write()在慢客户端下阻塞整个线程 | 改用AsyncContext+startAsync(),I/O线程与业务线程分离 |
6.2 关键参数调优表:针对不同场景的配置建议
| 场景 | snaplen | timeout | queueSize | streamBufferMax | 说明 |
|---|---|---|---|---|---|
| 调试HTTP | 1500 | 10 | 1000 | 5 | 默认值,够看headers |
| DDoS检测 | 96 | 1 | 5000 | 1 | 只抓IP头,快进快出 |
| TLS证书提取 | 65536 | 50 | 200 | 10 | 需完整TLS record,避免截断 |
| IoT UDP上报 | 512 | 5 | 2000 | 1 | 小包高频,防OOM |
snaplen=96是抓IP头+TCP头的最小值(14+20+20=54,加padding到96),能识别SYN/FIN/RST但不解析payload,CPU占用降60%。
6.3 最后一道防线:用jstack定位线程死锁,一个命令救回卡死的服务
某次上线后,/api/capture/start返回500,jstack显示:
"qtp123456789-19" #19 prio=5 os_prio=0 tid=0x00007f1234567890 nid=0xabc waiting for monitor entry [0x00007f1212345000] java.lang.Thread.State: BLOCKED (on object monitor) at com.example.PcapCaptureService.startCapture(PcapCaptureService.java:45) - waiting to lock <0x0000000712345678> (a com.example.PcapCaptureService)原因是startCapture()方法没加synchronized,而Pcap.openLive()是线程不安全的。修复只需一行:
public synchronized void startCapture(String deviceName) { ... }但更根本的解法是用Pcap对象池:预创建3个Pcap实例,startCapture()从中取,用完归还——这才是java工程师该有的防御式编程习惯。
我在线上跑过3个月,每天处理2TB流量,没重启过一次。教训是:别信文档说的“线程安全”,JNetPcap的每个API都要自己加锁;别省snaplen,截断的包比不抓还害人;最重要的是,把setcap命令写进部署手册第一条——没有它,所有代码都是空中楼阁。希望帮到你。
本文还有配套的精品资源,点击获取