news 2026/8/23 12:11:06

基于JAVA与jssc实现跨平台Modbus RTU通信(一)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于JAVA与jssc实现跨平台Modbus RTU通信(一)

1. 为什么我们要抛弃RXTX,拥抱jssc?

如果你之前尝试过用Java搞Modbus RTU通信,大概率会搜到一堆“modbus4j + RXTX”的组合方案。我刚开始也是这么干的,照着教程一顿操作,在Windows上跑得挺欢,结果一打包放到Linux服务器上,直接就“扑街”了。控制台报错找不到某个.so文件,或者直接给你来个UnsatisfiedLinkError。那一刻,作为一个有点强迫症的开发者,内心是崩溃的。

问题的根源就在于RXTX这个库。它本质上是一个基于JNI(Java Native Interface)的库,也就是说,它的核心功能是用C/C++写的,然后通过JNI接口暴露给Java调用。这就意味着,你在Windows上开发,需要对应的rxtxSerial.dll;部署到Linux,需要librxtxSerial.so;换到Mac,又需要另一个本地库。这不仅仅是“跨平台”的问题,更是部署和维护的噩梦。想象一下,你写好的一个工业数据采集服务,每次部署新环境,都得先手动去拷一堆本地库文件,还得确保版本、路径都对得上,这太不“优雅”了。

jssc(Java Simple Serial Connector)的出现,简直就是来“救场”的。它的核心优势在于“纯Java”实现。jssc内部使用了一种叫做JSSC的机制,它自己处理了与操作系统底层串口API的交互,把那些平台相关的脏活累活都封装好了。对我们开发者来说,引入一个jar包,写一套代码,就能在Windows、Linux、Mac上无缝运行。这才是我们心目中理想的“一次编写,到处运行”的Java魅力。

我实测下来,jssc的稳定性也相当不错。在长时间运行的工业现场数据采集中,串口通信最怕的就是连接意外断开或者数据阻塞。jssc提供了比较清晰的异常处理和状态查询接口,比如getInputBufferBytesCount()可以实时知道缓冲区有多少数据待读,这为我们实现超时、重连等健壮性逻辑提供了便利。所以,如果你正在为Java串口通信的跨平台问题头疼,别犹豫了,转向jssc是条明路。

2. 项目搭建与环境准备

万事开头难,但把环境搭好就成功了一半。这里我会手把手带你走一遍,确保你的开发环境是“武装到位”的。

2.1 Maven依赖配置

首先,我们得把两个核心的“武器”库引入项目。我强烈建议使用Maven或Gradle来管理依赖,这能省去你手动下载jar包和处理依赖冲突的无数烦恼。

在你的pom.xml文件里,需要添加两个仓库地址和两个依赖项。这里有个小细节:modbus4j的官方仓库不在Maven中央仓库,所以我们需要显式声明。

<repositories> <!-- Infinite Automation的Snapshot仓库,用于获取开发中的版本 --> <repository> <releases> <enabled>false</enabled> </releases> <snapshots> <enabled>true</enabled> </snapshots> <id>ias-snapshots</id> <name>Infinite Automation Snapshot Repository</name> <url>https://maven.mangoautomation.net/repository/ias-snapshot/</url> </repository> <!-- Infinite Automation的Release仓库,用于获取稳定版本 --> <repository> <releases> <enabled>true</enabled> </releases> <snapshots> <enabled>false</enabled> </snapshots> <id>ias-releases</id> <name>Infinite Automation Release Repository</name> <url>https://maven.mangoautomation.net/repository/ias-release/</url> </repository> </repositories> <dependencies> <!-- 核心:纯Java串口通信库 --> <dependency> <groupId>io.github.java-native</groupId> <artifactId>jssc</artifactId> <version>2.9.4</version> </dependency> <!-- 核心:Modbus协议实现库 --> <dependency> <groupId>com.infiniteautomation</groupId> <artifactId>modbus4j</artifactId> <version>3.0.4</version> </dependency> </dependencies>

配置好后,在IDE里刷新一下Maven项目,确保依赖都成功下载下来了。如果网络环境访问这些仓库有困难,你可能需要考虑配置镜像或者手动下载jar包,但那是下策了。

2.2 串口基础概念扫盲

在敲代码之前,我们得先搞清楚要和串口“说”哪些参数。这就像打电话,你得知道对方的电话号码(串口号)、说话语速(波特率)等。这里我整理了一个关键参数表,帮你快速理解:

参数名含义常见值备注
串口号 (commPortId)操作系统标识的串口设备名Windows:COM1,COM3
Linux:/dev/ttyS0,/dev/ttyUSB0
Mac:/dev/cu.usbserial-*
这是最容易出错的地方!Linux下权限问题(需sudo或用户加入dialout组)
波特率 (baudRate)数据传输速率,单位bps9600, 19200, 38400, 115200必须与设备设置完全一致,否则收到全是乱码
数据位 (dataBits)每个字节的数据位数8 (最常用), 7表示一个字符用几位二进制表示
停止位 (stopBits)字节结束的标志位1 (最常用), 1.5, 2用于同步,告诉接收方一个字节传输完毕
校验位 (parity)简单的错误检测位SerialPort.PARITY_NONE(无校验)
SerialPort.PARITY_EVEN(偶校验)
SerialPort.PARITY_ODD(奇校验)
用于检测传输过程中的单比特错误
流控制 (flowControl)控制数据流,防止缓冲区溢出SerialPort.FLOWCONTROL_NONE(无)
SerialPort.FLOWCONTROL_RTSCTS_IN(硬件)
普通Modbus RTU设备大多不需要流控

在实际项目中,这些参数通常由硬件设备(如PLC、传感器、仪表)的说明书决定。最保险的做法就是完全按照设备手册来设置,一个参数都不要猜。我曾经因为把波特率设成19200而设备是9600,对着乱码数据排查了一下午。

3. 核心桥梁:封装jssc为输入输出流

modbus4j库在设计上非常灵活,它不关心你的数据具体是从串口、TCP socket还是其他什么地方来的。它只要求你提供一个SerialPortWrapper接口的实现,这个接口会返回标准的JavaInputStreamOutputStream。我们的任务,就是利用jssc,造出这两个流。

3.1 打造串口输入流 (SerialInputStream)

InputStream的核心方法是read()。对于串口来说,读取数据有两个特点:1) 可能没有数据,需要等待;2) 数据是字节流。jssc的SerialPort.readBytes()方法提供了超时读取的功能,这正是我们需要的。

我写的SerialInputStream类,不仅仅是一个简单的包装,还增加了一些实用性功能:

import jssc.SerialPort; import java.io.IOException; import java.io.InputStream; public class SerialInputStream extends InputStream { private SerialPort serialPort; private int defaultTimeout = 1000; // 默认超时1秒,这个值可以根据实际情况调整 public SerialInputStream(SerialPort sp) { this.serialPort = sp; } public void setTimeout(int timeoutMillis) { this.defaultTimeout = timeoutMillis; } // 读取单个字节,使用默认超时 @Override public int read() throws IOException { byte[] buf = new byte[1]; int bytesRead = this.read(buf, 0, 1); return (bytesRead == -1) ? -1 : (buf[0] & 0xFF); // 转换为无符号整数 } // 核心方法:读取字节到数组,支持超时 @Override public int read(byte[] b, int off, int len) throws IOException { if (b == null) { throw new NullPointerException(); } else if (off < 0 || len < 0 || len > b.length - off) { throw new IndexOutOfBoundsException(); } else if (len == 0) { return 0; } try { // 关键:使用jssc的带超时读取方法 byte[] readBytes = serialPort.readBytes(len, defaultTimeout); if (readBytes == null || readBytes.length == 0) { return -1; // 超时或无数据 } int bytesToCopy = Math.min(readBytes.length, len); System.arraycopy(readBytes, 0, b, off, bytesToCopy); return bytesToCopy; } catch (Exception e) { throw new IOException("Failed to read from serial port", e); } } // 一个非常实用的方法:检查输入缓冲区有多少字节可读 @Override public int available() throws IOException { try { return serialPort.getInputBufferBytesCount(); } catch (Exception e) { throw new IOException("Failed to get available bytes", e); } } // 增强方法:阻塞式读取,直到读满指定长度或超时 public int blockingRead(byte[] buffer, int timeoutMillis) throws IOException { int totalRead = 0; long deadline = System.currentTimeMillis() + timeoutMillis; while (totalRead < buffer.length && System.currentTimeMillis() < deadline) { int remaining = buffer.length - totalRead; int read = this.read(buffer, totalRead, remaining); if (read == -1) { // 流结束或发生错误 break; } totalRead += read; if (totalRead < buffer.length) { // 如果还没读够,稍微休息一下,避免CPU空转 try { Thread.sleep(10); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); } } } return (totalRead == 0) ? -1 : totalRead; } }

这里有几个我踩过的坑和心得:

  1. 超时设置defaultTimeout非常重要。设得太短,可能设备还没响应完就超时了;设得太长,程序会在无响应时“假死”。工业设备响应一般在几十到几百毫秒,1秒是个比较安全的起点,后续可以根据设备特性调整。
  2. available()方法:这个方法不是必须的,但对于调试和优化很有用。你可以在读取前先看看缓冲区里积压了多少数据,避免盲目读取。
  3. blockingRead()方法:这是我自己加的“轮子”。有些设备返回的数据帧长度固定,或者协议头里包含了长度信息,我们需要一次性读够指定数量的字节。这个方法的实现比简单的循环读取更健壮,加入了超时控制,防止死等。

3.2 打造串口输出流 (SerialOutputStream)

输出流相对简单,主要就是把字节数组通过jssc写出去。

import jssc.SerialPort; import jssc.SerialPortException; import java.io.IOException; import java.io.OutputStream; public class SerialOutputStream extends OutputStream { private SerialPort serialPort; public SerialOutputStream(SerialPort sp) { this.serialPort = sp; } @Override public void write(int b) throws IOException { write(new byte[]{(byte) b}, 0, 1); } @Override public void write(byte[] b) throws IOException { write(b, 0, b.length); } @Override public void write(byte[] b, int off, int len) throws IOException { if (b == null) { throw new NullPointerException(); } if (off < 0 || len < 0 || len > b.length - off) { throw new IndexOutOfBoundsException(); } if (len == 0) { return; } byte[] dataToSend = new byte[len]; System.arraycopy(b, off, dataToSend, 0, len); try { boolean result = serialPort.writeBytes(dataToSend); if (!result) { throw new IOException("Serial port writeBytes method returned false. The data may not have been sent."); } } catch (SerialPortException e) { throw new IOException("Failed to write to serial port", e); } } // 可选:添加一个刷新方法,确保数据发送完毕 @Override public void flush() throws IOException { // jssc的writeBytes是同步的,通常不需要额外flush。 // 但有些场景下,可以在这里加入等待发送缓冲区清空的逻辑。 // 例如:while (serialPort.getOutputBufferBytesCount() > 0) { Thread.sleep(1); } } }

注意点serialPort.writeBytes()方法返回一个布尔值,表示写入操作是否成功加入发送队列。但这不保证数据已经物理发送到线缆上。对于绝大多数应用,这已经足够了。如果你需要极致的可靠性,可以在flush()方法里实现等待发送缓冲区清空的逻辑,但这会引入延迟。

4. 关键集成:实现SerialPortWrapper接口

这是连接我们自制的“流”与modbus4j库的核心桥梁SerialPortWrapper接口定义了modbus4j与串口交互的所有契约。我们的实现类SerialPortWrapperImpl就像一个适配器,一头握着jssc的SerialPort对象,另一头提供modbus4j需要的流。

import com.serotonin.modbus4j.serial.SerialPortWrapper; import jssc.SerialPort; import jssc.SerialPortException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.InputStream; import java.io.OutputStream; public class SerialPortWrapperImpl implements SerialPortWrapper { private static final Logger log = LoggerFactory.getLogger(SerialPortWrapperImpl.class); private final SerialPort serialPort; private final int baudRate; private final int dataBits; private final int stopBits; private final int parity; private final int flowControlIn; private final int flowControlOut; // 构造函数:保存所有串口配置参数 public SerialPortWrapperImpl(String commPortId, int baudRate, int dataBits, int stopBits, int parity, int flowControlIn, int flowControlOut) { this.baudRate = baudRate; this.dataBits = dataBits; this.stopBits = stopBits; this.parity = parity; this.flowControlIn = flowControlIn; this.flowControlOut = flowControlOut; // 创建jssc串口对象,此时尚未打开 this.serialPort = new SerialPort(commPortId); log.info("SerialPortWrapper created for port: {}", commPortId); } // modbus4j会在建立连接时调用此方法 @Override public void open() throws Exception { try { // 1. 打开物理串口 boolean opened = serialPort.openPort(); if (!opened) { throw new Exception("Failed to open serial port: " + serialPort.getPortName()); } // 2. 设置所有通信参数(必须!否则通信必失败) serialPort.setParams(baudRate, dataBits, stopBits, parity); // 3. 设置流控制(如果设备需要) if (flowControlIn != 0 || flowControlOut != 0) { serialPort.setFlowControlMode(flowControlIn | flowControlOut); } // 4. 可选:设置其他高级参数,比如超时 // serialPort.setEventsMask(SerialPort.MASK_RXCHAR); // 监听数据接收事件 // serialPort.addEventListener(...); // 添加事件监听器(异步模式) log.debug("Serial port {} opened successfully with params: {},{},{},{}, flowCtrl: in={}, out={}", serialPort.getPortName(), baudRate, dataBits, stopBits, parity, flowControlIn, flowControlOut); } catch (SerialPortException e) { log.error("Critical error opening serial port {}: {}", serialPort.getPortName(), e.getMessage(), e); // 如果打开失败,尝试清理资源 try { serialPort.closePort(); } catch (Exception ignore) {} throw new Exception("Cannot open serial port: " + serialPort.getPortName(), e); } } // modbus4j会在关闭连接时调用此方法 @Override public void close() throws Exception { try { if (serialPort != null && serialPort.isOpened()) { serialPort.closePort(); log.debug("Serial port {} closed.", serialPort.getPortName()); } } catch (SerialPortException e) { log.warn("Error while closing serial port {}: {}", serialPort.getPortName(), e.getMessage()); // 关闭时的异常通常可以忽略,但最好记录一下 throw new Exception("Error closing serial port", e); } } // 提供输入流给modbus4j读取响应数据 @Override public InputStream getInputStream() { SerialInputStream in = new SerialInputStream(this.serialPort); in.setTimeout(500); // 为输入流设置一个读取超时,比如500ms return in; } // 提供输出流给modbus4j发送请求数据 @Override public OutputStream getOutputStream() { return new SerialOutputStream(this.serialPort); } // --- 以下是一系列getter方法,modbus4j内部可能会查询这些参数 --- @Override public int getBaudRate() { return baudRate; } @Override public int getDataBits() { return dataBits; } @Override public int getStopBits() { return stopBits; } @Override public int getParity() { return parity; } @Override public int getFlowControlIn() { return flowControlIn; } @Override public int getFlowControlOut() { return flowControlOut; } }

这个实现类的几个关键设计思路:

  1. 延迟初始化:在构造函数中,我们只是用串口号创建了jssc的SerialPort对象,并没有真正打开端口。真正的打开操作(openPortsetParams)是在open()方法中完成的。这符合modbus4j对连接生命周期的管理。
  2. 集中配置:所有串口参数(波特率、数据位等)都在构造函数中传入并保存。open()方法里一次性设置到位,确保参数一致性。
  3. 健壮的异常处理:串口操作极易出错(端口被占用、参数错误、硬件断开)。在open()方法中,我做了细致的异常捕获和日志记录。如果打开失败,会尝试关闭端口并抛出清晰的异常信息,方便上层调用者排查。
  4. 资源管理close()方法确保端口被正确关闭,即使发生异常也尽量不影响主程序。这是一个好习惯,防止端口“卡死”在操作系统层面。
  5. 日志输出:使用SLF4J日志框架记录了关键操作和错误信息。在生产环境中,通过调整日志级别(DEBUG/INFO/ERROR),你可以清晰地看到串口何时打开、关闭,参数是什么,出了什么错,这对调试线上问题至关重要。

到这里,一个硬件无关、跨平台的Modbus RTU通信基础框架就搭建完毕了。我们已经成功用纯Java的jssc替换了依赖本地库的RXTX,并且按照modbus4j的要求,提供了标准的输入输出流接口。这套代码在Windows、Linux(包括树莓派等ARM设备)、macOS上都可以直接编译运行,无需任何额外的本地库文件。下次,我们就可以基于这个稳固的通信层,去实现具体的Modbus数据读取和写入功能了,比如读取温湿度传感器的寄存器,或者控制一个继电器的开关。你会发现,有了这个基础,后面的功能实现会变得异常顺畅。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/14 16:44:17

期货量化策略中的仓位加减仓_金字塔与倒金字塔

免责声明&#xff1a;本文基于个人使用体验&#xff0c;与任何厂商无商业关系。内容仅供技术交流参考&#xff0c;不构成投资建议。 一、前言 加仓、减仓方式会直接影响资金曲线和回撤。金字塔加仓与倒金字塔减仓是常见思路。做了二十年期货交易&#xff0c;我在仓位管理上用过…

作者头像 李华
网站建设 2026/7/14 16:44:19

n8n 严重漏洞可导致RCE和存储凭据暴露

聚焦源代码安全&#xff0c;网罗国内外最新资讯&#xff01;编译&#xff1a;代码卫士网络安全研究人员披露了位于n8n工作流自动化平台中的两个严重漏洞&#xff0c;它们可导致任意命令执行&#xff0c;现已修复。CVE-2026-27577&#xff08;CVSS评分&#xff1a;9.4&#xff0…

作者头像 李华
网站建设 2026/7/14 16:44:18

Mini LED vs OLED vs Micro LED:如何为你的智能家居选择最佳显示技术?

Mini LED vs OLED vs Micro LED&#xff1a;如何为你的智能家居选择最佳显示技术&#xff1f; 走进任何一家电子产品卖场&#xff0c;你都会被琳琅满目的屏幕所包围。从客厅里占据C位的巨幕电视&#xff0c;到手腕上记录健康的智能手表&#xff0c;再到带来沉浸体验的VR眼镜&am…

作者头像 李华
网站建设 2026/7/14 16:44:21

【物联网毕设】智能环境检测-STM32+MQTT+APP

目录 一 连线图 1 原理图 2 PCB效果 3 实物效果 4 APP效果 5 功能概括 &#xff08;1&#xff09;硬件端 &#xff08;2&#xff09;APP端 &#xff08;3&#xff09;云平台使用 &#xff08;4&#xff09;演示视频 二 底层代码使用方式 1 使用说明 2 下载程序 三…

作者头像 李华
网站建设 2026/7/14 16:44:20

Windows 第三方应用漏洞与木马攻防实操核心笔记

文中所有操作皆是在靶场中进行操作的Windows 第三方应用漏洞与木马攻防实操核心笔记核心框架本资料围绕Windows 第三方应用程序漏洞展开&#xff0c;从攻击视角讲解木马制作与植入&#xff0c;从防御视角梳理木马排查、日志分析、系统防护方法&#xff0c;最终总结攻防核心知识…

作者头像 李华
网站建设 2026/7/14 16:44:31

80%效率提升:douyin-downloader一站式抖音视频管理解决方案

80%效率提升&#xff1a;douyin-downloader一站式抖音视频管理解决方案 【免费下载链接】douyin-downloader 项目地址: https://gitcode.com/GitHub_Trending/do/douyin-downloader 在数字内容爆炸的时代&#xff0c;抖音作为国内领先的短视频平台&#xff0c;每天产生…

作者头像 李华