2026/9/12 3:01:18

基于Java的实时环境监测系统:串口采集、MQTT接入与WebSocket推送

基于Java的实时环境监测系统:串口采集、MQTT接入与WebSocket推送 简介这是一套基于Java的实时环境监测系统设计源码面向环境监测领域的开发者和学习者可用于大气、水质、土壤等场景的环境数据采集、处理与展示。压缩包共50个文件包含30个Java源文件、13个XML配置文件以及properties、gitignore、readme、备份和日志等辅助文件整体仅76KB包体轻量但目录结构清晰。项目按env-gather-entity、env-gather-interface、env-gather-impl三个模块组织将数据实体、功能接口与具体实现分层隔离便于理解Java工程的分层设计思想XML配置负责Maven依赖与系统参数Log目录存放运行日志Backup目录保留备份文件对调试和容错都有实际帮助。这套源码尤其适合作为课程设计、毕业设计或环境监测入门项目的参考资料目前已有243人学习下载代码量适中能帮助读者快速建立实时数据记录与管理系统的整体认知。1. 基于Java的实时环境监测系统源头不在代码而在链路很多人在找“基于Java环境的实时环境监测系统设计源码”时第一反应是先把一堆类拷进IDE跑通了再说。但真正做过环境监测项目的人会告诉你这个标题的适用场景是把传感器数据从采集端送到浏览器这一整条链路跑起来——采样、解析、上报、推送。Java环境指的不只是装了JDK而是指这个系统能跑在通用服务器上、不依赖特定桌面环境并且能跟串口设备、消息队列、Web页面顺畅对接。常见做法是分成采集层、接入层、存储层、推送层四段来设计。适合做毕业设计、课程设计以及中小型园区、机房、养殖场的环境监控项目。别急着找源码先把从传感器到页面的调用链想清楚否则代码到位了也踩不响。2. 数据采集层写Java串口读ModBus传感器校验位比格式更关键2.1 为什么串口在环境监测系统里还占着主位实时环境监测的采集端设备比如温湿度传感器、PM2.5传感器、风速风向仪九成以上走的是RS485串口通过ModBus-RTU协议对外输出数据。很多Java开发者写惯了HTTP接口第一次面对串口会下意识觉得这是“单片机的事”但实际上Java通过串口库常见的是jSerialComm或RXTX读写串口非常成熟代码写起来跟操作文件流没本质区别。选型上我一般用jSerialComm跨平台、不用像RXTX那样还要单独编译动态库。串口读数据的本质是字节流传感器按固定的寄存器地址和数据帧格式往外吐字节。所谓实时监测在采集层就是“不停地读、按帧切分、按协议解析、然后交给上层”。这一层最影响系统稳定性的不是读得快不快而是能不能正确识别一帧的边界。ModBus-RTU一帧数据通常是设备地址1字节 功能码1字节 数据区N字节 CRC校验2字节。2.2 用Java串口流读取并切帧的最小可跑代码下面这段代码是环境监测系统中采集网关的典型写法用一个循环持续读取串口输入流读到的字节先暂存再做帧切分。SerialPort serialPort new SerialPortBuilder() .commPort(/dev/ttyS0) // Linux下的串口设备名 .baudRate(9600) // 和传感器一致常见是9600 .dataBits(SerialPort.DATABITS_8) .stopBits(SerialPort.STOPBITS_1) .parity(SerialPort.PARITY_NONE) .build(); serialPort.open(); serialPort.setDTR(false); serialPort.setRTS(false); byte[] buffer new byte[1024]; ByteArrayOutputStream frameCollector new ByteArrayOutputStream(); while (running) { int len serialPort.getInputStream().read(buffer); if (len 0) { frameCollector.write(buffer, 0, len); byte[] allBytes frameCollector.toByteArray(); if (allBytes.length 8) { // 至少 地址功能码数据CRC // 按CRC校验判断是否完整帧见下方校验方法 int frameEnd findValidFrame(allBytes); if (frameEnd 0) { handleFrame(Arrays.copyOfRange(allBytes, 0, frameEnd)); frameCollector.reset(); } } } }关于波特率、数据位、停止位、校验位这四个参数必须一字不差地按传感器手册配置最常见的定位问题就是“数据读出来了但全是乱码”九成因为波特率不一致。setDTR和setRTS有些传感器会忽略但少数设备在DTR为true时不会向外发数据所以统一置false更省事。2.3 解析帧与CRC校验不校验的源码跑两天就废帧切出来之后要判断边界是否准确ModBus-RTU没有帧头标志靠的是“两个帧之间至少有3.5个字符时间的静默间隔”和“CRC校验通过”两个条件。在Java代码里CRC校验是用查表法算出来的接收到的帧尾部两个字节低位在前和计算值一致才能认定这帧有效。private int findValidFrame(byte[] dataBytes) { int length dataBytes.length; if (length 8) return -1; int crcValue ModBusCrc.crc16(dataBytes, 0, length - 2); int receivedCrc (dataBytes[length - 1] 0xFF) 8 | (dataBytes[length - 2] 0xFF); if (crcValue receivedCrc) return length; return -1; }crc16方法是从0xFFFF初值开始对每个字节与查表结果做异或实现属于经典ModBus算法网上可查真正要注意的是CRC在帧内是“低字节在前”。很多自己写解析的源码功能码、寄存器都读对了就是校验总不过直接去掉校验也能跑但数据一旦出现一个错位字节后续整个数据流全乱且没有自愈能力。2.4 解析结果如何喂给上层统一成定长字符串采集层解析出的结果是浮点数比如温度23.5、湿度61.2、PM2.5浓度102这些值不能直接往数据库或消息队列里扔。常见做法是统一封装成一个数据对象再序列化为定长或固定分隔符的字符串比如deviceId,temp,humidity,pm25,timestamp。这样做的目的是让接入层不要感知传感器型号差异。实时监测系统的换型需求很常见今天接温湿度明天换颗粒物传感器只要采集层把数据格式定死上层代码一行不用改这是源码设计的一个核心边界。3. 接入层MQTT把实时监测数据送进Java后端重连与QoS是两个必调参数3.1 为什么接入层要选MQTT而不是HTTP轮询从采集网关到Java服务端是环境监测系统的第二跳。如果采集设备数量少、频率低用HTTP定时上报也能跑但生产环境里经常有几十上百个采集点网关断网恢复、网络抖动是常态。实时监测系统要用的是MQTT基于发布/订阅模型天然支持大量设备同时上报而且有会话保持机制设备断线重连后能接着收没推送完的消息。在Java生态里最常用的是Eclipse Paho客户端它可以嵌入到Spring Boot服务里作为一个生命周期组件。对比HTTPMQTT的连接是长连接寄存器里不保留历史状态服务端只负责转发消息这让接入层能水平扩展。你完全可以部署多个Java服务实例订阅同一个主题负载由MQTT Broker来分发。3.2 用Paho写一个带断线重连和遗嘱消息的采集服务String broker tcp://localhost:1883; String clientId env-gateway-01; MqttClient client new MqttClient(broker, clientId, new MemoryPersistence()); MqttConnectOptions options new MqttConnectOptions(); options.setAutomaticReconnect(true); // 网络恢复后自动重连 options.setCleanSession(false); // 保存离线消息重连后继续接收 options.setConnectionTimeout(10); options.setKeepAliveInterval(30); // 心跳间隔单位秒 options.setWill(devices/ clientId /status, offline.getBytes(), 2, true); client.connect(options); client.setCallback(new MqttCallbackExtended() { public void connectComplete(boolean reconnect, String serverURI) { if (reconnect) { /* 重连成功后补发缓存数据 */ } } }); client.subscribe(env/data, 1);这里最容易被忽略的是setAutomaticReconnect和setCleanSession的组合前者保证的是TCP层面的重连后者保证的是Broker帮客户端暂存离线期间的消息。不要只开自动重连而把CleanSession设成true那样重连回来就像一个全新客户端离线期间的数据全丢这在环境监测里会造成一段时间的数据黑洞。3.3 QoS等级怎么选实时监测场景里的误用与坑MQTT有三种QoS等级实时监测系统里大部分团队会选QoS 1至少一次。QoS 0最快但会丢消息QoS 2最稳但吞吐量下降明显。但对环境监测数据而言QoS 1有重复投递的可能所以消费端必须做幂等处理。最简单的幂等做法是用deviceId timestamp作为唯一键入库存Redis或数据库时先查重。有些面试题里会问“MQTT的QoS 1为什么需要去重”实际项目中踩到就是这句话的答案。3.4 接入后为什么还要在Java端挂一个Redis最新值实时监测页面打开时要立刻看到当前温度湿度不能让用户等下一轮数据推送。所以接入层消费到消息后会把最新值写入Rediskey设计成env:latest:deviceId。这个缓存还有个好处是给告警判断提供“当前值”不用每次从MySQL查最新记录。如果你用Spring Boot的RedisTemplate直接写缓存注意increment()方法对字符串类型会报“not an integer or out of range”所以实时值用opsForValue().set()计数器才用increment()。系统在开始阶段有值初始化逻辑之后都是覆盖写不存在这类整数转换问题。4. 推送层Spring WebSocket把监测值实时送到浏览器而不是靠前端轮询4.1 为什么实时环境监测系统要WebSocket而不是HTTP长轮询数据到达Java后端只完成了一半用户在浏览器端要看到温湿度曲线不断刷新。有些源码会简单用前端定时器3秒调一次REST接口这叫伪实时延迟、请求压力、服务器开销都不划算。真正落到浏览器端的做法是WebSocket一条TCP连接全双工通信服务端有数据就直接往连接里推。在Java环境里实现WebSocket最省事的是用Spring Boot的spring-boot-starter-websocket它背后是Tomcat的WebSocket实现。环境监测系统的场景是采集网关上报的数据频率可能几秒一条但浏览器端的展示刷新不需要这么密推送层可以做聚合缓存1秒内的数据然后批量推送给前端能明显降低WebSocket消息量。4.2 一个可直接改用的WebSocket推送HandlerComponent public class EnvWebSocketHandler extends TextWebSocketHandler { // 维护在线会话key可以是订阅的设备IDvalue是WebSocketSession private final MapString, WebSocketSession sessionMap new ConcurrentHashMap(); Override public void afterConnectionEstablished(WebSocketSession session) { String deviceId (String) session.getAttributes().get(deviceId); sessionMap.put(deviceId, session); } Override protected void handleTextMessage(WebSocketSession session, TextMessage message) { // 客户端可以发送 { action: subscribe, deviceId: env-01 } // 借此实现会话与设备的绑定 String payload message.getPayload(); JSONObject json JSONObject.parseObject(payload); String deviceId json.getString(deviceId); session.getAttributes().put(deviceId, deviceId); sessionMap.put(deviceId, session); } Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { sessionMap.values().removeIf(s - s.equals(session)); } public void pushToDevice(String deviceId, String dataJson) { WebSocketSession session sessionMap.get(deviceId); if (session ! null session.isOpen()) { session.sendMessage(new TextMessage(dataJson)); } } }这段代码在环境监测系统里比较实用因为一个监控大屏上可能同时订阅多个设备数据。把deviceId放到session的attributes里是习惯做法前端连接时带上token后端握手拦截器再解析出deviceId比前端发消息注册更安全。如果你只做一个全局广播的大屏可以简化成直接把所有session存进一个set但多设备场景下按deviceId分发才能避免数据串台。4.3 推送频率的取舍别把数据一秒推十次环境监测系统的传感器采样频率通常是2秒到5秒一次页面曲线更新也按这个节奏。如果采样是2秒一次推送层就不必每秒都发否则前端图表绘制压力大后端网络带宽也浪费。我会在推送层加一个简单的聚合缓冲每1秒将收集到的多条记录合并成一条JSON数组再推送。这比每条都推送少了很多TCP小包显著降低网关设备和Web应用之间的网络开销。实时监测大屏要显示的告警数据可以走另一条通道比如env/alarm/{deviceId}主题这个主题的数据由告警规则触发不经过缓冲直接推送。两级推送的设计能让“实时”更集中在关键事件上。4.4 数据格式与前端对接的细节WebSocket推送的JSON体建议固定成以下结构{ deviceId: env-01, type: sample, ts: 1702522800000, data: { temp: 23.5, humidity: 61.2, pm25: 102.0 } }type字段区分sample和alarm前端拿到sample就更新曲线alarm就弹窗并高亮。ts统一用毫秒时间戳不要传格式化好的字符串图表库直接拿它做x轴避免时区转换和字符解析的开销。最后在WebSocket握手的时候注意放宽同源检查的配置限于开发环境使用上线前收紧否则被别的网页裸连到你的推送服务会造成信息泄露。5. 一类特别隐蔽的排错数据中断但不报错如何用日志定位和闭环环境监测系统跑一段时间后最让人头疼的不是代码逻辑而是“数据源时不时断一下又自动恢复”。这种问题在开发环境很难复现等到现场才会暴露。以下是一个经常被忽略的排查方向和对应的排查技巧把它当成“链路不通”来查而不是“代码有Bug”。先给采集层加上带时间戳的原始字节日志。串口数据的原始字节用十六进制输出binlog这行是判断“传感器有没有发”最直接的手段。如果日志里持续有字节输出但解析层一帧都切不出来问题基本在CRC校验和帧格式定义如果连字节都没有问题在物理链路RS485转换器供电不稳、线序不对。我一般会用logger.info(raw[{}] len{}, HexFormat.of().formatHex(buf, 0, len), len)输出在现场排查时这一行能省去一半抓瞎时间。数据中断的另一个常见根因在MQTT客户端的长连接被网络设备静默断开。Paho的自动重连默认是打开的但它不保证重连后马上恢复到原来的订阅状态。因此在自定义的MqttCallbackExtended里有一个connectComplete回调当reconnect参数为true时必须在回调里重新订阅主题并补发断点缓存。注意这里不能把重连和“数据恢复”画等号——重连只是通道恢复离线期间的数据还需要从本地队列补发。具体做法是用一个内存队列暂存待上报数据每次有新数据且MQTT未连接时放入队列重连成功后把队列清空发送。队列的长度不能无限增长需要设置上限到达上限后丢弃“相对最不重要的记录”。常见做法是把告警类数据优先保留普通采样数据可丢。这样能保证“断线期间关键告警不丢”这就是实时监测系统的断线不丢数据逻辑里能主动做的一部分。加上这一步系统的整体可靠性才算闭环。本文还有配套的精品资源点击获取