
跨平台Java Modbus RTU通信实战JSSCModbus4j替代RXTX方案工业物联网开发者经常面临一个经典难题如何在Windows、Linux和macOS等多平台上稳定实现Modbus RTU通信传统RXTX方案虽然广泛使用但其依赖DLL文件和平台兼容性问题让不少开发者头疼。本文将介绍一种基于JSSC和Modbus4j的纯Java解决方案彻底摆脱平台限制。1. 为什么需要替代RXTX方案RXTX库长期以来是Java串口通信的标准选择但在实际工业应用中暴露出几个关键问题平台依赖性强需要为每个操作系统编译特定版本的DLL/so文件部署复杂必须手动将本地库文件放入JRE的特定目录维护困难不同平台版本需要单独管理升级麻烦稳定性问题在某些Linux发行版上容易出现串口死锁相比之下JSSC(Java Simple Serial Connector)具有明显优势特性RXTXJSSC跨平台需要平台特定库纯Java实现部署复杂简单(Maven依赖)性能中等高活跃度维护停滞持续更新提示JSSC 2.9.4版本已修复了早期版本中的内存泄漏问题建议使用最新稳定版。2. 环境搭建与依赖配置2.1 Maven依赖配置首先需要在pom.xml中添加必要的仓库和依赖repositories repository idias-snapshots/id nameInfinite Automation Snapshot Repository/name urlhttps://maven.mangoautomation.net/repository/ias-snapshot//url snapshots enabledtrue/enabled /snapshots /repository repository idias-releases/id nameInfinite Automation Release Repository/name urlhttps://maven.mangoautomation.net/repository/ias-release//url releases enabledtrue/enabled /releases /repository /repositories dependencies dependency groupIdio.github.java-native/groupId artifactIdjssc/artifactId version2.9.4/version /dependency dependency groupIdcom.infiniteautomation/groupId artifactIdmodbus4j/artifactId version3.0.4/version /dependency /dependencies2.2 串口参数详解Modbus RTU通信需要正确配置串口参数以下是关键参数及其典型值波特率9600、19200、38400、57600、115200等数据位8(最常用)、7、6、5停止位1(最常用)、1.5、2校验位SerialPort.PARITY_NONE(无校验)SerialPort.PARITY_EVEN(偶校验)SerialPort.PARITY_ODD(奇校验)流控制SerialPort.FLOWCONTROL_NONE(无流控)SerialPort.FLOWCONTROL_RTSCTS_IN(硬件流控)3. 核心代码实现3.1 串口包装器实现Modbus4j需要通过SerialPortWrapper接口与串口通信以下是基于JSSC的实现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 JsscSerialPortWrapper implements SerialPortWrapper { private static final Logger LOG LoggerFactory.getLogger(JsscSerialPortWrapper.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 JsscSerialPortWrapper(String portName, int baudRate, int dataBits, int stopBits, int parity, int flowControlIn, int flowControlOut) { this.serialPort new SerialPort(portName); this.baudRate baudRate; this.dataBits dataBits; this.stopBits stopBits; this.parity parity; this.flowControlIn flowControlIn; this.flowControlOut flowControlOut; } Override public void close() throws Exception { if (serialPort.isOpened()) { serialPort.closePort(); LOG.debug(Port {} closed, serialPort.getPortName()); } } Override public void open() throws Exception { if (!serialPort.isOpened()) { serialPort.openPort(); serialPort.setParams(baudRate, dataBits, stopBits, parity); serialPort.setFlowControlMode(flowControlIn | flowControlOut); LOG.debug(Port {} opened with params: {}/{}/{}/{}, serialPort.getPortName(), baudRate, dataBits, stopBits, parity); } } Override public InputStream getInputStream() { return new SerialInputStream(serialPort); } Override public OutputStream getOutputStream() { return new SerialOutputStream(serialPort); } // 省略getter方法... }3.2 自定义输入输出流为了使JSSC与Modbus4j协同工作需要实现自定义的InputStream和OutputStreamimport jssc.SerialPort; import java.io.IOException; import java.io.InputStream; public class SerialInputStream extends InputStream { private final SerialPort serialPort; private int timeout 1000; // 默认超时1秒 public SerialInputStream(SerialPort serialPort) { this.serialPort serialPort; } public void setTimeout(int timeout) { this.timeout timeout; } Override public int read() throws IOException { try { byte[] buffer serialPort.readBytes(1, timeout); return buffer[0] 0xFF; } catch (Exception e) { throw new IOException(Serial port read error, e); } } Override public int read(byte[] b, int off, int len) throws IOException { try { byte[] buffer serialPort.readBytes(len, timeout); System.arraycopy(buffer, 0, b, off, buffer.length); return buffer.length; } catch (Exception e) { throw new IOException(Serial port read error, e); } } Override public int available() throws IOException { try { return serialPort.getInputBufferBytesCount(); } catch (Exception e) { throw new IOException(Failed to get available bytes, e); } } }4. 实战应用与问题排查4.1 完整Modbus RTU通信示例下面是一个读取保持寄存器的完整示例import com.serotonin.modbus4j.ModbusFactory; import com.serotonin.modbus4j.ModbusMaster; import com.serotonin.modbus4j.code.DataType; import com.serotonin.modbus4j.exception.ModbusInitException; import com.serotonin.modbus4j.locator.BaseLocator; import com.serotonin.modbus4j.serial.SerialPortWrapper; public class ModbusRtuExample { public static void main(String[] args) { // 1. 创建串口包装器 SerialPortWrapper wrapper new JsscSerialPortWrapper( /dev/ttyUSB0, // Linux串口设备 9600, // 波特率 8, // 数据位 1, // 停止位 SerialPort.PARITY_NONE, // 无校验 SerialPort.FLOWCONTROL_NONE, SerialPort.FLOWCONTROL_NONE ); // 2. 创建Modbus Master ModbusFactory factory new ModbusFactory(); ModbusMaster master factory.createRtuMaster(wrapper); try { // 3. 初始化连接 master.init(); // 4. 读取保持寄存器 BaseLocatorNumber locator BaseLocator.holdingRegister( 1, // 从站地址 0, // 寄存器地址 DataType.TWO_BYTE_INT_SIGNED // 数据类型 ); Number value master.getValue(locator); System.out.println(Read value: value); } catch (ModbusInitException e) { System.err.println(Modbus init failed: e.getMessage()); } finally { // 5. 关闭连接 master.destroy(); } } }4.2 常见问题与解决方案串口无法打开检查端口名称是否正确(Windows: COM3, Linux: /dev/ttyUSB0)确认用户有串口访问权限(Linux下可能需要将用户加入dialout组)通信超时增加超时时间((SerialInputStream)master.getInputStream()).setTimeout(3000)检查物理连接和Modbus从站地址数据校验错误确认串口参数(波特率、数据位等)与设备设置一致尝试启用奇偶校验跨平台路径问题使用系统属性动态构建端口路径String os System.getProperty(os.name).toLowerCase(); String port os.contains(win) ? COM3 : /dev/ttyUSB0;在实际工业项目中我们成功将这套方案部署到了Windows工控机、Linux网关和macOS开发机上完全消除了RXTX带来的跨平台问题。特别是在基于ARM架构的嵌入式Linux设备上JSSC表现出了更好的稳定性和更低的资源占用。