Java 標準版 (SE) 不包含串口通訊 API,需依賴擴展庫。歷史上有兩大主流方案:**Sun 官方 JavaComm (已停止維護)** 與 **開源 RXTX (社區活躍、跨平台)**。現代開發中,**RXTX (或其衍生版 jSerialComm) 是首選**。本文系統講解串口編程核心流程、JavaComm/RXTX 關鍵差異、安裝坑點、事件驅動模型與最佳實踐代碼。
rxtx-2.2pre2 或更新版 (如 rxtx-2.3、jSerialComm)。rxtxSerial.dll → %JAVA_HOME%/bin (或 jre/bin)。RXTXcomm.jar → %JAVA_HOME%/lib/ext (或項目 classpath/模塊路徑)。# 將用戶加入 dialout 組 (擁有 /dev/ttyS* /dev/ttyUSB* 權限)
sudo usermod -a -G dialout $USER
# 重新登錄生效
或臨時授權:sudo chmod 666 /dev/ttyUSB0 (不安全,僅測試用)。
| 庫 | 包名 | 備註 |
|---|---|---|
| JavaComm | javax.comm.* | 官方標準,已停更 |
| RXTX 2.0 (兼容層) | javax.comm.* | 作為 JavaComm 實現提供者 |
| RXTX 2.1+ / jSerialComm | gnu.io.* / com.fazecast.jSerialComm.* | 原生 API,功能更全,**推薦新項目直接用** |
遷移技巧:若舊代碼用 javax.comm,引入 RXTX 2.0 兼容層即可無縫運行;新項目建議直接用 gnu.io 或 jSerialComm API。
import gnu.io.*;
import java.util.Enumeration;
public class SerialPortUtil {
public static CommPortIdentifier findPort(String targetPortName) {
Enumeration portEnum = CommPortIdentifier.getPortIdentifiers();
while (portEnum.hasMoreElements()) {
CommPortIdentifier pid = portEnum.nextElement();
if (pid.getPortType() == CommPortIdentifier.PORT_SERIAL
&& pid.getName().equals(targetPortName)) {
return pid;
}
}
return null;
}
}
public static SerialPort openPort(CommPortIdentifier portId, String appName, int timeoutMs)
throws PortInUseException, UnsupportedCommOperationException {
SerialPort serialPort = (SerialPort) portId.open(appName, timeoutMs);
// 配置參數:波特率、數據位、停止位、校驗位
serialPort.setSerialPortParams(
9600, // 波特率
SerialPort.DATABITS_8, // 數據位 8
SerialPort.STOPBITS_1, // 停止位 1
SerialPort.PARITY_NONE // 無校驗
);
// 可選:設置流控 (硬件 RTS/CTS 或軟件 XON/XOFF)
// serialPort.setFlowControlMode(SerialPort.FLOWCONTROL_RTSCTS_IN | SerialPort.FLOWCONTROL_RTSCTS_OUT);
return serialPort;
}
// 發送
OutputStream os = serialPort.getOutputStream();
os.write("AT\r\n".getBytes(StandardCharsets.US_ASCII)); // 注意:AT 指令用 \r\n,編碼 US-ASCII
os.flush();
// 接收 (阻塞等待)
InputStream is = serialPort.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.US_ASCII));
String response = reader.readLine(); // 讀取一行 (以 \n 結尾)
缺點:阻塞線程,無法同時處理發送/接收/UI,適合簡單單線程工具。
public class SerialListener implements SerialPortEventListener {
private final InputStream is;
private final OutputStream os;
private final RingBuffer txBuffer = new RingBuffer(4096); // 發送緩衝區
private final Object writeLock = new Object();
public SerialListener(SerialPort port) throws IOException {
this.is = port.getInputStream();
this.os = port.getOutputStream();
}
@Override
public void serialEvent(SerialPortEvent event) {
switch (event.getEventType()) {
case SerialPortEvent.DATA_AVAILABLE:
handleDataAvailable();
break;
case SerialPortEvent.OUTPUT_BUFFER_EMPTY:
handleOutputBufferEmpty();
break;
// 其他事件: BI, CD, CTS, DSR, FE, OE, PE, RI ...
}
}
private void handleDataAvailable() {
try {
byte[] buffer = new byte[1024];
int len = is.read(buffer);
if (len > 0) {
String data = new String(buffer, 0, len, StandardCharsets.US_ASCII);
// 處理接收數據: 解析協議、回調業務層...
System.out.println("RX: " + data);
}
} catch (IOException e) { /* 處理錯誤 */ }
}
// 非阻塞發送:主線程寫入環形緩衝區,事件線程實際發送
public void sendAsync(byte[] data) {
synchronized (writeLock) {
txBuffer.write(data);
// 觸發發送 (若端口空閒)
// 實際發送邏輯在 handleOutputBufferEmpty 中
}
}
private void handleOutputBufferEmpty() {
synchronized (writeLock) {
byte[] data = new byte[txBuffer.data()];
int len = txBuffer.read(data);
if (len > 0) {
try {
os.write(data, 0, len);
os.flush();
} catch (IOException e) { /* 錯誤處理 */ }
}
}
}
}
// 註冊監聽器
serialPort.addEventListener(new SerialListener(serialPort));
serialPort.notifyOnDataAvailable(true);
serialPort.notifyOnOutputEmpty(true); // 需驅動支持
RingBuffer) — 線程安全異步發送核心用於主線程 (業務) 與事件線程 (發送) 間解耦傳遞數據,避免阻塞。關鍵操作:
write(byte[]):寫入數據,返回實際寫入量 (滿則返回可寫長度)。read(byte[]):讀取數據,返回實際讀取量 (空則返回 0)。synchronized 保護頭尾指針,無鎖版本可用 AtomicInteger + 數組 CAS。AT\r\n、解析 OK/ERROR/CONNECT、處理回顧、超時重試。ATE0 → 撥號 ATD<號碼>; → 數據傳輸 → 掛斷 ATH。.dll/.so/.dylib) 需隨平台打包。com.fazecast:jSerialComm:2.10.0。| 現象 | 原因 | 解決 |
|---|---|---|
NoClassDefFoundError: javax.comm.CommPortIdentifier | JAR 未在 classpath / 模塊路徑錯誤 | 檢查依賴、模塊信息、運行時 JRE |
UnsatisfiedLinkError: rxtxSerial.dll | 原生庫未找到/版本不匹配(32/64位)/多 JRE 衝突 | 確保 dll 在 java.library.path 指向的目錄,統一 64 位 |
端口打開失敗 PortInUseException | 端口被其他進程佔用 / 上次未正確關閉 | 檢查串口調試助手、關閉串口 port.close()、重啟機器 |
| 收到亂碼 | 編碼不一致 (設備發 ASCII/UTF-8/二進制) | 確認設備手冊,指定 StandardCharsets.US_ASCII 或二進制處理 |
| 發送無響應 | 換行符錯誤 (需 \r\n)、流控未關、波特率不匹配 | 用 \r\n、關閉流控測試、核對波特率 |
// Maven: com.fazecast:jSerialComm:2.10.0
import com.fazecast.jSerialComm.*;
SerialPort[] ports = SerialPort.getCommPorts(); // 獲取所有端口
SerialPort port = ports[0]; // 選擇目標
port.setBaudRate(9600);
port.setNumDataBits(8);
port.setNumStopBits(1);
port.setParity(SerialPort.NO_PARITY);
port.openPort();
// 異步讀取 (內部線程池)
port.addDataListener(new SerialPortDataListener() {
@Override public int getListeningEvents() { return SerialPort.LISTENING_EVENT_DATA_AVAILABLE; }
@Override public void serialEvent(SerialPortEvent event) {
byte[] buffer = new byte[port.bytesAvailable()];
int numRead = port.readBytes(buffer, buffer.length);
// 處理數據...
}
});
// 發送
port.writeBytes("AT\r\n".getBytes(), "AT\r\n".length());
port.closePort();
優勢:無需安裝原生庫、API 現代化、支持 Java 9+ 模塊化、單 JAR 部署、活躍維護。**新項目首選**。
Java 串口開發核心在於:**正確安裝原生庫、掌握事件驅動異步模型、處理編碼與協議細節、妥善管理資源**。從 JavaComm 到 RXTX 再到 jSerialComm,工具在進化,但「掃描端口→打開配置→異步收發→關閉釋放」的核心流程不變。建議新項目直接使用 jSerialComm,老項目維護可用 RXTX 兼容層。深入理解底層原理,才能在工業控制、醫療設備、儀表采集等場景中構建穩定可靠的通訊鏈路。