序列程式設計/序列 Java
序列程式設計: 簡介和 OSI 網路模型 -- RS-232 接線和連線 -- 典型的 RS232 硬體配置 -- 8250 UART -- DOS -- MAX232 驅動器/接收器系列 -- Windows 中的 TAPI 通訊 -- Linux 和 Unix -- Java -- Hayes 相容調變解調器和 AT 命令 -- 通用序列匯流排 (USB) -- 形成資料包 -- 錯誤糾正方法 -- 雙向通訊 -- 資料包恢復方法 -- 序列資料網路 -- 實際應用開發 -- 序列連線上的 IP
由於 Java 的平臺無關性,序列介面很困難。序列介面需要一個標準化的 API,並具有特定於平臺的實現,這對 Java 來說很困難。
不幸的是,Sun 並沒有在 Java 中過多關注序列通訊。Sun 定義了一個序列通訊 API,稱為 JavaComm,但 API 的實現不是 Java 標準版的一部分。Sun 為一些(但並非所有)Java 平臺提供了一個參考實現。特別是,在 2005 年底,Sun 默默地撤回了對 Windows 的 JavaComm 支援。一些被遺漏平臺的第三方實現是可用的。JavaComm 並沒有看到多少維護活動,Sun 只執行了最低限度的維護,除了 Sun 顯然回應了其 Sun Ray 瘦客戶機買家的壓力,並將 JavaComm 移植到此平臺,同時放棄了對 Windows 的支援。
這種情況以及 Sun 最初沒有為 Linux 提供 JavaComm 實現(從 2006 年開始,他們現在提供了)導致了免費軟體 RxTx 庫的開發。RxTx 可用於許多平臺,不僅限於 Linux。它可以與 JavaComm 一起使用(RxTx 提供特定於硬體的驅動程式),也可以單獨使用。當用作 JavaComm 驅動程式時,JavaComm API 和 RxTx 之間的橋接由 JCL(JavaComm for Linux)完成。JCL 是 RxTx 發行版的一部分。
Sun 對 JavaComm 和 JavaComm 特定程式設計模型的疏忽使得 JavaComm 備受批評。RxTx(如果不用作 JavaComm 驅動程式)提供了更豐富的介面,但該介面沒有標準化。RxTx 支援的平臺比現有的 JavaComm 實現更多。最近,RxTx 被採用以提供與 JavaComm 相同的介面,只是包名稱與 Sun 的包名稱不匹配。
那麼,應用程式應該使用哪個庫呢?如果需要最大程度的移植性(對於“最大程度”的某種價值),那麼 JavaComm 是一個不錯的選擇。如果某個特定平臺沒有可用的 JavaComm 實現,但有一個 RxTx 實現,那麼 RxTx 可以用作該平臺上 JavaComm 的驅動程式。因此,透過使用 JavaComm,可以支援所有平臺,這些平臺要麼直接由 Sun 的參考實現支援,要麼由 RxTx 與 JCL 支援。這樣,應用程式就不需要更改,並且可以針對單個介面(標準化的 JavaComm 介面)進行工作。
本模組討論了 JavaComm 和 RxTx。它主要側重於演示概念,而不是提供可執行的程式碼。那些想要盲目複製程式碼的人可以參考軟體包附帶的示例程式碼。那些想要了解他們正在做什麼的人可能會在本模組中找到一些有用的資訊。
還應考慮 jSSC(Java 簡單序列聯結器)
還有一個名為 jSerialComm 的庫,它在 jar 檔案中包含了所有平臺特定的檔案,這使其真正便攜,因為不需要安裝。
- 瞭解 序列通訊和程式設計 的基礎知識。
- 準備好要與之通訊的裝置(例如調變解調器)的文件。
- 設定所有硬體和測試環境
- 例如,使用終端程式手動與裝置通訊。這樣做是為了確保測試環境設定正確,並且您已經理解了裝置的命令和響應。
- 下載您要用於特定作業系統的 API 實現
- 閱讀
- JavaComm 和/或 RxTx 安裝說明(並遵循說明)
- API 文件
- 隨附的示例原始碼
JavaComm 和 RxTX 都存在一些安裝上的問題。強烈建議您逐字逐句地按照安裝說明進行操作。如果說明中指出將 jar 檔案或共享庫放到特定目錄,那麼請認真對待!如果說明中指出特定檔案或裝置需要具有特定所有權或訪問許可權,那麼也請認真對待。許多安裝問題僅僅是因為沒有精確地按照說明操作。
尤其需要注意的是,某些版本的 JavaComm 附帶兩種安裝說明。一種適用於 Java 1.2 及更高版本,另一種適用於 Java 1.1。使用錯誤的說明會導致安裝失敗。另一方面,某些版本的 RxTx(包括構建和軟體包)附帶的說明不完整。在這種情況下,需要獲取 RxTx 的相應原始碼分發版,其中應該包含完整的說明。
許多 Linux 發行版在它們的儲存庫中提供 RxTx 軟體包(ArchLinux - 'java-rxtx',Debian/Ubuntu - 'librxtx-java'),這些軟體包只包含庫的平臺特定部分,但通常可以直接使用。
此外,還需要注意,Windows JDK 安裝通常會包含多達三個 VM,因此也包含三個擴充套件目錄。
- 一個作為 JDK 的一部分,
- 一個作為 JDK 附帶的私有 JRE 的一部分,用於執行 JDK 工具,以及
- 一個作為 JDK 附帶的公共 JRE 的一部分,用於執行應用程式
有些人甚至聲稱在 \Windows 目錄層次結構中還有一個第四個 JRE。
JavaComm 至少應該作為 JDK 和所有公共 JRE 中的擴充套件進行安裝。
JavaComm 和 RxTx 都存在一個普遍的問題,即它們無法透過 Java WebStart 安裝。
JavaComm 臭名昭著,因為它需要將名為 javax.comm.properties 的檔案放到 JDK lib 目錄中,而這無法透過 Java WebStart 完成。這一點尤其令人沮喪,因為該檔案的存在是 JavaComm 中一些不必要的設計 / 決定的結果,本可以很容易地被 JavaComm 設計人員避免。Sun 一直拒絕糾正這個錯誤,聲稱該機制是必不可少的。在 JavaComm 中,他們是在撒謊,尤其是考慮到 Java 很久以前就有了專門用於此類目的的服務提供者架構。
屬性檔案的內容通常只是一行,即具有本機驅動程式的 Java 類名,例如:
driver=com.sun.comm.Win32Driver
以下是一個技巧,可以忽略那個腦殘的屬性檔案,透過 WebStart 部署 JavaComm。它存在嚴重的缺點,並且可能無法在更新的 JavaComm 版本中使用(如果 Sun 真的決定釋出新版本)。
首先,關閉安全管理器。Sun 公司的一個笨蛋程式設計師決定,即使在最初載入後,也反覆檢查可怕的 javax.comm.properties 檔案的存在,除了檢查該檔案之外,沒有其他明顯的原因。
System.setSecurityManager(null);
然後,在初始化 JavaComm API 時,手動初始化驅動程式
String driverName = "com.sun.comm.Win32Driver"; // or get as a JNLP property
CommDriver commDriver = (CommDriver)Class.forName(driverName).newInstance();
commDriver.initialize();
在某些平臺上,RxTx 需要更改序列裝置的所有權和訪問許可權。這同樣無法透過 WebStart 完成。
在程式啟動時,您可以要求使用者以超級使用者身份執行必要的設定。
此外,RxTx 具有一個模式匹配演算法,用於識別“有效”的序列裝置名稱。當想要使用非標準裝置(例如 USB 轉序列轉換器)時,這通常會導致問題。可以透過系統屬性覆蓋此機制。有關詳細資訊,請參閱 RxTx 安裝說明。
與 RxTx 和 JavaComm 相比,jSerialComm 在許多作業系統和平臺(例如 Windows x86/x86_64、Linux x86/x86_64、ARM 甚至 android - 具體庫 jar 檔案中的完整列表)上無需任何更改即可使用。但是,它仍然需要訪問裝置的許可權(有關詳細資訊,請訪問 jSerialComm 主頁)。
SerialPundit
SerialPundit 是另一個功能豐富的庫,用於在 Java 中訪問序列埠。它包括以下功能:檢測 FTDI232 等 USB-UART 裝置何時插入系統;自動識別作業系統和 CPU 架構;無需任何安裝;全面記錄;經過良好測試;以及支援/討論組。
Java 中序列通訊的官方 API 是 JavaComm API。該 API 不是標準 Java 2 版本的一部分。相反,API 的實現需要單獨下載。不幸的是,JavaComm 沒有得到 Sun 的太多關注,並且已經很長時間沒有得到真正的維護。Sun 有時會進行一些微不足道的錯誤修復,但沒有進行早已過期的主要大修。
本節介紹 JavaComm API 的基本操作。提供的原始碼保持簡單,以便演示重要要點。在實際應用中需要對其進行擴充套件。
本章中的原始碼並非唯一的可用示例程式碼。JavaComm 下載包附帶多個示例。這些示例幾乎包含比 API 文件更詳細的 API 使用資訊。不幸的是,Sun 沒有提供任何真正的教程或介紹性文字。因此,研究示例程式碼以瞭解 API 機制是值得的。但是,也應該研究 API 文件。但最好的方法是研究示例並使用它們。由於缺乏易於使用的應用程式以及人們難以理解 API 的程式設計模型,因此該 API 經常受到指責。該 API 實際上比它的名聲要好,而且功能齊全。但僅此而已。
該 API 使用回撥機制通知程式設計師有新的資料到達。最好研究這種機制,而不是依賴於輪詢埠。與 Java 中的其他回撥介面(例如 GUI 中的介面)不同,此介面只允許一個監聽器監聽事件。如果多個監聽器需要監聽序列事件,則主監聽器需要以一種方式實現,以便將資訊分發到其他輔助監聽器。
Sun 的 JavaComm 網頁指向 下載位置。在該位置,Sun 目前(2007 年)為 Solaris/SPARC、Solaris/x86 和 Linux x86 提供 JavaComm 3.0 實現。下載需要註冊 Sun 線上帳戶。下載頁面提供了註冊頁面的連結。註冊的目的尚不清楚。可以無需註冊下載 JDK 和 JRE,但對於幾乎微不足道的 JavaComm,Sun 引用了法律和政府對軟體分發和出口的限制。
JavaComm 的 Windows 版本不再正式提供,Sun - 違反了他們自己的產品生命週期終結政策 - 沒有將其提供給 Java 產品檔案。但是,2.0 Windows 版本(javacom 2.0)仍然可以從 這裡 下載。
按照下載包附帶的安裝說明進行操作。某些版本的 JavaComm 2.0 附帶兩種安裝說明。兩種說明中最明顯的一種不幸的是錯誤的,它適用於古老的 Java 1.1 環境。引用同樣古老的 Java 1.2 的資訊(jdk1.2.html)才是正確的。
尤其是 Windows 使用者通常不知道他們在多個位置(通常是三個到四個位置)安裝了相同 VM 的副本。某些 IDE 也喜歡附帶自己的私有 JRE/JDK 安裝,一些 Java 應用程式也是如此。需要對每個 VM 安裝(JDK 和 JRE)重複安裝,這些 VM 應該與序列應用程式的開發和執行一起使用。
IDE 通常有 IDE 特定的方法來讓 IDE 知道新庫(類和文件)。通常像 JavaComm 這樣的庫不僅需要讓 IDE 知道它本身,還需要讓每個應該使用該庫的專案都知道它。請閱讀 IDE 的文件。需要注意的是,舊的 JavaComm 2.0 版本附帶了 JavaDoc API 文件,這些文件採用歷史上的 Java 1.0 JavaDoc 佈局。一些現代的 IDE 不再支援這種結構,無法將其整合到他們的幫助系統中。在這種情況下,需要使用外部瀏覽器閱讀文件(推薦的操作...)。
安裝軟體後,建議檢查示例和 JavaDoc 目錄。構建並執行一個示例應用程式以驗證安裝是否正確是很有意義的。示例應用程式通常需要一些小調整才能在特定平臺上執行(例如,對硬編碼的序列埠識別符號進行更改)。在嘗試示例應用程式時,最好有一些序列硬體,比如電纜、空閒調變解調器、分線盒、真正的調變解調器、程控交換機等。 Serial_Programming:RS-232 Connections 和 Serial_Programming:Modems and AT Commands 提供了一些關於如何設定序列應用程式開發環境的硬體部分的資訊。
查詢所需的序列埠
[edit | edit source]使用 JavaComm 對序列線路進行程式設計時,通常需要做的前三件事是
- 列舉所有可供 JavaComm 使用的序列埠(埠識別符號),
- 從可用埠識別符號中選擇所需的埠識別符號,以及
- 透過埠識別符號獲取埠。
列舉和選擇所需的埠識別符號通常在一個迴圈中完成。
import javax.comm.*;
import java.util.*;
...
//
// Platform specific port name, here= a Unix name
//
// NOTE: On at least one Unix JavaComm implementation JavaComm
// enumerates the ports as "COM1" ... "COMx", too, and not
// by their Unix device names "/dev/tty...".
// Yet another good reason to not hard-code the wanted
// port, but instead make it user configurable.
//
String wantedPortName = "/dev/ttya";
//
// Get an enumeration of all ports known to JavaComm
//
Enumeration portIdentifiers = CommPortIdentifier.getPortIdentifiers();<br>
//
// Check each port identifier if
// (a) it indicates a serial (not a parallel) port, and
// (b) matches the desired name.
//
CommPortIdentifier portId = null; // will be set if port found
while (portIdentifiers.hasMoreElements())
{
CommPortIdentifier pid = (CommPortIdentifier) portIdentifiers.nextElement();
if(pid.getPortType() == CommPortIdentifier.PORT_SERIAL &&
pid.getName().equals(wantedPortName))
{
portId = pid;
break;
}
}
if(portId == null)
{
System.err.println("Could not find serial port " + wantedPortName);
System.exit(1);
}
//
// Use port identifier for acquiring the port
//
...
找到埠識別符號後,就可以使用它來獲取所需的埠
//
// Use port identifier for acquiring the port
//
SerialPort port = null;
try {
port = (SerialPort) portId.open(
"name", // Name of the application asking for the port
10000 // Wait max. 10 sec. to acquire port
);
} catch(PortInUseException e) {
System.err.println("Port already in use: " + e);
System.exit(1);
}
//
// Now we are granted exclusive access to the particular serial
// port. We can configure it and obtain input and output streams.
//
...
初始化序列埠
[edit | edit source]序列埠初始化非常簡單。可以單獨設定通訊首選項(波特率、資料位、停止位、奇偶校驗),也可以使用 setSerialPortParams(...) 方便方法一次設定所有首選項。
作為初始化過程的一部分,將在示例中配置用於通訊的輸入和輸出流。
import java.io.*;
...
//
// Set all the params.
// This may need to go in a try/catch block which throws UnsupportedCommOperationException
//
port.setSerialPortParams(
115200,
SerialPort.DATABITS_8,
SerialPort.STOPBITS_1,
SerialPort.PARITY_NONE);
//
// Open the input Reader and output stream. The choice of a
// Reader and Stream are arbitrary and need to be adapted to
// the actual application. Typically one would use Streams in
// both directions, since they allow for binary data transfer,
// not only character data transfer.
//
BufferedReader is = null; // for demo purposes only. A stream would be more typical.
PrintStream os = null;
try {
is = new BufferedReader(new InputStreamReader(port.getInputStream()));
} catch (IOException e) {
System.err.println("Can't open input stream: write-only");
is = null;
}
//
// New Linux systems rely on Unicode, so it might be necessary to
// specify the encoding scheme to be used. Typically this should
// be US-ASCII (7 bit communication), or ISO Latin 1 (8 bit
// communication), as there is likely no modem out there accepting
// Unicode for its commands. An example to specify the encoding
// would look like:
//
// os = new PrintStream(port.getOutputStream(), true, "ISO-8859-1");
//
os = new PrintStream(port.getOutputStream(), true);
//
// Actual data communication would happen here
// performReadWriteCode();
//
//
// It is very important to close input and output streams as well
// as the port. Otherwise Java, driver and OS resources are not released.
//
if (is != null) is.close();
if (os != null) os.close();
if (port != null) port.close();
簡單資料傳輸
[edit | edit source]簡單資料寫入
[edit | edit source]寫入序列埠與基本的 Java IO 一樣簡單。但是,如果您使用 AT Hayes 協議,則需要注意一些注意事項。
- 不要在 OutputStream 上使用 println(或其他自動附加 "\n" 的方法)。AT Hayes 協議要求調變解調器將 "\r\n" 作為分隔符(無論底層作業系統如何)。
- 寫入 OutputStream 後,如果調變解調器設定為回顯命令列,則 InputStream 緩衝區將包含傳送到它的命令(帶換行符)的重複,以及另一個換行符(對“AT”命令的回答)。因此,在寫入操作的一部分中,請確保從 InputStream 中清除這些資訊(實際上可以用於錯誤檢測)。
- 當使用 Reader/Writer(不是一個好主意)時,至少將字元編碼設定為 US-ASCII,而不是使用平臺的預設編碼,這可能起作用也可能不起作用。
- 由於使用調變解調器時的主要操作是無損傳輸資料,因此應透過 InputStream/OutputStream(而不是 Reader/Writer)來處理與調變解調器的通訊。
// Write to the output
os.print("AT");
os.print("\r\n"); // Append a carriage return with a line feed
is.readLine(); // First read will contain the echoed command you sent to it. In this case: "AT"
is.readLine(); // Second read will remove the extra line feed that AT generates as output
簡單資料讀取(輪詢)
[edit | edit source]如果您正確執行了寫入操作(見上文),那麼讀取操作就像一個命令一樣簡單
// Read the response
String response = is.readLine(); // if you sent "AT" then response == "OK"
簡單讀/寫問題
[edit | edit source]之前部分演示的從序列埠讀/寫資料的簡單方法存在嚴重缺陷。這兩種活動都是用阻塞 I/O完成的。這意味著,當
- 沒有可供讀取的資料,或者
- 用於寫入的輸出緩衝區已滿(裝置不接受(更多)資料),
讀或寫方法(之前的示例中的os.print() 或 is.readLine())不返回,並且應用程式停止執行。更準確地說,執行讀或寫的執行緒被阻塞。如果該執行緒是主應用程式執行緒,則應用程式會凍結,直到解決阻塞條件(資料變得可供讀取或裝置再次接受資料)。
除非應用程式非常原始,否則應用程式的凍結是不可接受的。例如,至少應該允許使用者互動以取消通訊。需要的是非阻塞 I/O或非同步 I/O。但是,JavaComm 基於 Java 的標準阻塞 I/O 系統(InputStream、OutputStream),但有一些變化,如下所示。
提到的“變化”是 JavaComm 透過事件通知機制提供了一些對非同步 I/O的有限支援。但是,在 Java 中,在阻塞 I/O 系統之上實現非阻塞 I/O的通用解決方案是使用執行緒。實際上,這是一個用於序列寫入的可行解決方案,強烈建議使用單獨的執行緒寫入序列埠,即使使用了事件通知機制,如後文所述。
讀取也可以在單獨的執行緒中處理。但是,如果使用 JavaComm 事件通知機制,則這不是嚴格必要的。所以總結
| 活動 | 架構 |
|---|---|
| 讀取 | 使用事件通知和/或單獨執行緒 |
| 寫入 | 始終使用單獨執行緒,可以選擇使用事件通知 |
以下部分提供一些詳細資訊。
事件驅動的序列通訊
[edit | edit source]簡介
[edit | edit source]JavaComm API 提供了一種事件通知機制來克服阻塞 I/O帶來的問題。但是,按照 Sun 的典型方式,這種機制並非沒有問題。
原則上,應用程式可以向特定的SerialPort 註冊事件偵聽器,以便隨時瞭解該埠發生的重大事件。對於讀取和寫入資料的兩個最有趣的事件型別是
javax.comm.SerialPortEvent.DATA_AVAILABLE以及javax.comm.SerialPortEvent.OUTPUT_BUFFER_EMPTY.
但也存在兩個問題。
- 每個
SerialPort只能註冊一個事件偵聽器。這迫使程式設計師編寫“巨型”偵聽器,根據事件型別進行區分。 OUTPUT_BUFFER_EMPTY是一種可選的事件型別。Sun 在文件中隱晦地指出,並非所有 JavaComm 實現都支援生成此型別的事件。
在詳細介紹之前,下一部分將介紹實現和註冊序列事件處理程式的主要方法。請記住,一次只能有一個處理程式,並且它必須處理所有可能的事件。
設定序列事件處理程式
[edit | edit source] import javax.comm.*;
/**
* Listener to handle all serial port events.
*
* NOTE: It is typical that the SerialPortEventListener is implemented
* in the main class that is supposed to communicate with the
* device. That way the listener has easy access to state information
* about the communication, e.g. when a particular communication
* protocol needs to be followed.
*
* However, for demonstration purposes this example implements a
* separate class.
*/
class SerialListener implements SerialPortEventListener {
/**
* Handle serial events. Dispatches the event to event-specific
* methods.
* @param event The serial event
*/
@Override
public void serialEvent(SerialPortEvent event){
//
// Dispatch event to individual methods. This keeps this ugly
// switch/case statement as short as possible.
//
switch(event.getEventType()) {
case SerialPortEvent.OUTPUT_BUFFER_EMPTY:
outputBufferEmpty(event);
break;
case SerialPortEvent.DATA_AVAILABLE:
dataAvailable(event);
break;
/* Other events, not implemented here ->
case SerialPortEvent.BI:
breakInterrupt(event);
break;
case SerialPortEvent.CD:
carrierDetect(event);
break;
case SerialPortEvent.CTS:
clearToSend(event);
break;
case SerialPortEvent.DSR:
dataSetReady(event);
break;
case SerialPortEvent.FE:
framingError(event);
break;
case SerialPortEvent.OE:
overrunError(event);
break;
case SerialPortEvent.PE:
parityError(event);
break;
case SerialPortEvent.RI:
ringIndicator(event);
break;
<- other events, not implemented here */
}
}
/**
* Handle output buffer empty events.
* NOTE: The reception of this event is optional and not
* guaranteed by the API specification.
* @param event The output buffer empty event
*/
protected void outputBufferEmpty(SerialPortEvent event) {
// Implement writing more data here
}
/**
* Handle data available events.
*
* @param event The data available event
*/
protected void dataAvailable(SerialPortEvent event) {
// implement reading from the serial port here
}
}
實現偵聽器後,可以使用它來偵聽特定的序列埠事件。為此,需要將偵聽器的例項新增到序列埠。此外,需要單獨請求接收每種事件型別。
SerialPort port = ...;
...
//
// Configure port parameters here. Only after the port is configured it
// makes sense to enable events. The event handler might be called immediately
// after an event is enabled.
...
//
// Typically, if the current class implements the SerialEventListener interface
// one would call
//
// port.addEventListener(this);
//
// but for our example a new instance of SerialListener is created:
//
port.addEventListener(new SerialListener());
//
// Enable the events we are interested in
//
port.notifyOnDataAvailable(true);
port.notifyOnOutputEmpty(true);
/* other events not used in this example ->
port.notifyOnBreakInterrupt(true);
port.notifyOnCarrierDetect(true);
port.notifyOnCTS(true);
port.notifyOnDSR(true);
port.notifyOnFramingError(true);
port.notifyOnOverrunError(true);
port.notifyOnParityError(true);
port.notifyOnRingIndicator(true);
<- other events not used in this example */
資料寫入
[edit | edit source]| 本節為存根。 您可以透過 擴充套件它 來幫助 Wikibooks。 |
為寫入設定單獨的執行緒
[edit | edit source]| 本節為存根。 您可以透過 擴充套件它 來幫助 Wikibooks。 |
使用單獨的執行緒進行寫入只有一個目的:避免在序列埠未準備好寫入時整個應用程式阻塞。
一個簡單、執行緒安全的環形緩衝區實現
[edit | edit source]使用單獨的執行緒進行寫入,與主應用程式執行緒分離,意味著需要一種方法將需要寫入的資料從應用程式執行緒傳遞到寫入執行緒。一個共享的、同步的資料緩衝區,例如一個byte[]就可以。此外,還需要一種方法讓主應用程式確定它是否可以寫入資料緩衝區,或者資料緩衝區當前是否已滿。如果資料緩衝區已滿,則可能表示序列埠未準備好,並且輸出資料已排隊。主應用程式將不得不輪詢共享資料緩衝區中新空間的可用性。但是,在主應用程式輪詢之間,它可以執行其他操作,例如更新 GUI,提供一個可以中止傳送的命令提示符等。
乍一看,PipedInputStream/PipedOutputStream對似乎很適合這種通訊。但如果管道流真的有用,Sun 就不可能是 Sun 了。如果對應的PipedOutputStream沒有及時清除,PipedInputStream會阻塞。因此應用程式執行緒會阻塞。這正是使用單獨執行緒想要避免的情況。java.nio.Pipe也存在同樣的問題。它的阻塞行為依賴於平臺。而且,將 JavaComm 使用的經典 I/O 適配到 NIO 並不是一項愉快的任務。
在本文中,使用了一個非常簡單的同步環形緩衝區來將資料從一個執行緒傳遞到另一個執行緒。在實際應用中,實現可能需要更加複雜。例如,在實際應用中,對緩衝區實現 OutputStream 和 InputStream 檢視是有意義的。
環形緩衝區本身沒什麼特別的,在多執行緒方面也沒有什麼特殊屬性。只是這個簡單的資料結構在這裡用來提供資料緩衝。實現方式保證了對該資料結構的訪問是執行緒安全的。
/**
* Synchronized ring buffer.
* Suitable to hand over data from one thread to another.
**/
public '''synchronized''' class RingBuffer {
/** internal buffer to hold the data **/
protected byte buffer[];
/** size of the buffer **/
protected int size;
/** current start of data area **/
protected int start;
/** current end of data area **/
protected int end;
/**
* Construct a RingBuffer with a default buffer size of 1k.
*/
public RingBuffer() {
this(1024);
}
/**
* Construct a RingBuffer with a certain buffer size.
* @param size Buffer size in bytes
*/
public RingBuffer(int size) {
this.size = size;
buffer = new byte[size];
clear();
}
/**
* Clear the buffer contents. All data still in the buffer is lost.
*/
public void clear() {
// Just reset the pointers. The remaining data fragments, if any,
// will be overwritten during normal operation.
start = end = 0;
}
/**
* Return used space in buffer. This is the size of the
* data currently in the buffer.
* <nowiki><p></nowiki>
* Note: While the value is correct upon returning, it
* is not necessarily valid when data is read from the
* buffer or written to the buffer. Another thread might
* have filled the buffer or emptied it in the mean time.
*
* @return currently amount of data available in buffer
*/
public int data() {
return start <= end
? end - start
: end - start + size;
}
/**
* Return unused space in buffer. Note: While the value is
* correct upon returning, it is not necessarily valid when
* data is written to the buffer or read from the buffer.
* Another thread might have filled the buffer or emptied
* it in the mean time.
*
* @return currently available free space
*/
public int free() {
return start <= end
? size + start - end
: start - end;
}
/**
* Write as much data as possible to the buffer.
* @param data Data to be written
* @return Amount of data actually written
*/
int write(byte data[]) {
return write(data, 0, data.length);
}
/**
* Write as much data as possible to the buffer.
* @param data Array holding data to be written
* @param off Offset of data in array
* @param n Amount of data to write, starting from <code>off</code>.
* @return Amount of data actually written
*/
int write(byte data[], int off, int n) {
if(n <= 0) return 0;
int remain = n;
// @todo check if off is valid: 0= <= off < data.length; throw exception if not
int i = Math.min(remain, (end < start ? start : buffer.length) - end);
if(i > 0) {
System.arraycopy(data, off, buffer, end, i);
off += i;
remain -= i;
end += i;
}
i = Math.min(remain, end >= start ? start : 0);
if(i > 0 ) {
System.arraycopy(data, off, buffer, 0, i);
remain -= i;
end = i;
}
return n - remain;
}
/**
* Read as much data as possible from the buffer.
* @param data Where to store the data
* @return Amount of data read
*/
int read(byte data[]) {
return read(data, 0, data.length);
}
/**
* Read as much data as possible from the buffer.
* @param data Where to store the read data
* @param off Offset of data in array
* @param n Amount of data to read
* @return Amount of data actually read
*/
int read(byte data[], int off, int n) {
if(n <= 0) return 0;
int remain = n;
// @todo check if off is valid: 0= <= off < data.length; throw exception if not
int i = Math.min(remain, (end < start ? buffer.length : end) - start);
if(i > 0) {
System.arraycopy(buffer, start, data, off, i);
off += i;
remain -= i;
start += i;
if(start >= buffer.length) start = 0;
}
i = Math.min(remain, end >= start ? 0 : end);
if(i > 0 ) {
System.arraycopy(buffer, 0, data, off, i);
remain -= i;
start = i;
}
return n - remain;
}
}
有了這個環形緩衝區,現在可以以受控的方式將資料從一個執行緒傳遞到另一個執行緒。任何其他執行緒安全的、非阻塞機制也可以。這裡的關鍵點是,寫入操作在緩衝區已滿時不會阻塞,在沒有要讀取的資料時也不會阻塞。
將緩衝區與序列事件一起使用
[edit | edit source]在寫入中使用 OUTPUT_BUFFER_EMPTY 事件
[edit | edit source]參考設定序列事件處理程式一節中介紹的事件處理程式骨架,現在可以使用一個簡單、執行緒安全的環形緩衝區實現一節中的共享環形緩衝區來支援OUTPUT_BUFFER_EMPTY事件。該事件並非所有 JavaComm 實現都支援,因此程式碼可能永遠不會被呼叫。但是,如果該事件可用,它就是確保最佳資料吞吐量的構建塊之一,因為序列介面不會長時間處於閒置狀態。
提出的事件偵聽器骨架有一個方法outputBufferEmpty(),可以按如下方式實現。
RingBuffer dataBuffer = ... ;
/**
* Handle output buffer empty events.
* NOTE: The reception is of this event is optional and not
* guaranteed by the API specification.
* @param event The output buffer empty event
*/
protected void outputBufferEmpty(SerialPortEvent event) {
//TODO
}
| 本節為存根。 您可以透過 擴充套件它 來幫助 Wikibooks。 |
讀取資料
[edit | edit source]| 本節為存根。 您可以透過 擴充套件它 來幫助 Wikibooks。 |
以下示例假設資料的目的地是某個檔案。只要資料可用,它就會從序列埠獲取並寫入檔案。這是一個極其簡化的檢視,因為在實際應用中,需要檢查資料以查詢檔案結束指示,例如,返回到調變解調器命令模式。
import javax.comm.*;
...
InputStream is = port.getInputStream();
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream("out.dat"));
/**
* Listen to port events
*/
class FileListener implements SerialPortEventListener {
/**
* Handle serial event.
*/
void serialEvent(SerialPortEvent e) {
SerialPort port = (SerialPort) e.getSource();
//
// Discriminate handling according to event type
//
switch(e.getEventType()) {
case SerialPortEvent.DATA_AVAILABLE:
//
// Move all currently available data to the file
//
try {
int c;
while((c = is.read()) != -1) {
out.write(c);
}
} catch(IOException ex) {
...
}
break;
case ...:
...
break;
...
}
if (is != null) is.close();
if (port != null) port.close();
}
在一個應用程式中處理多個埠
[edit | edit source]| 本節為存根。 您可以透過 擴充套件它 來幫助 Wikibooks。 |
調變解調器控制
[edit | edit source]JavaComm 嚴格關注序列介面的處理以及透過該介面傳輸資料。它不瞭解也不提供對更高階協議的支援,例如,用於控制消費級調變解調器的 Hayes 調變解調器命令。這僅僅不是 JavaComm 的工作,也不是錯誤。
與任何其他特定序列裝置一樣,如果希望透過 JavaComm 控制調變解調器,則需要在 JavaComm 之上編寫必要的程式碼。頁面"Hayes 相容調變解調器和 AT 命令"提供了處理 Hayes 調變解調器所需的必要基本通用資訊。
一些作業系統,例如 Windows 或某些 Linux 發行版,提供了一種或多或少標準化的方式,用於為特定調變解調器型別或品牌配置作業系統的調變解調器控制命令。例如,Windows 調變解調器“驅動程式”通常只是登錄檔項,描述了特定調變解調器(實際驅動程式是通用序列調變解調器驅動程式)。JavaComm 本身沒有訪問此類作業系統特定資料的規定。因此,您必須提供一個單獨的僅 Java 功能,允許使用者為使用特定調變解調器配置應用程式,或者需要新增一些平臺特定的(本機)程式碼。
RxTx
[edit | edit source]| 本節為存根。 您可以透過 擴充套件它 來幫助 Wikibooks。 |
概述和版本
[edit | edit source]由於 Sun 沒有為 Linux 提供 JavaComm API 的參考實現,因此人們為 Java 和 Linux 開發了 RxTx [1]。然後,RxTx 移植到其他平臺。最新版本的 RxTx 已知可在 100 多個平臺上執行,包括 Linux、Windows、Mac OS、Solaris 和其他作業系統。
RxTx 可以獨立於 JavaComm API 使用,也可以用作 JavaComm API 的所謂的提供程式。為了做到後者,還需要一個名為 JCL 的包裝器 [2]。JCL 和 RxTx 通常與 Linux/Java 發行版一起打包,或者 JCL 完全整合到程式碼中。因此,在嘗試單獨獲取它們之前,值得檢視 Linux 發行版的 CD。
由於 Sun 對 JavaComm API 的支援有限,而且文件不完善,因此似乎有一種趨勢是放棄 JavaComm API,而直接使用 RxTx 而不是透過 JCL 包裝器使用它。但是,RxTx 的文件非常稀疏。特別是,RxTX 人員喜歡把他們的版本和包內容弄得亂七八糟(例如,是否包含整合 JCL)。從 RxTx 版本 1.5 開始,RxTx 包含公共 JavaComm 類替換類。由於法律原因,它們不在java.comm包中,而是在gnu.io包中。但是,當前可用的兩個 RxTx 版本的打包方式不同
- RxTx 2.0
- RxTx 版本,應該用作 JavaComm 提供程式。這個版本應該起源於 RxRx 1.4,這是在新增
gnu.io包之前的 RxTx 版本。 - RxTx 2.1
- 具有完整
gnu.io包替換java.comm的 RxTx 版本。該版本應該起源於 RxTx 1.5,gnu.io支援從該版本開始。
因此,如果希望針對原始 JavaComm API 進行程式設計,則需要
- Sun 的通用 JavaComm 版本。在撰寫本文時,實際上是 Unix 包(包含對各種 Unix 版本的支援,例如 Linux 或 Solaris)。即使在 Windows 上使用,也需要 Unix 包來提供通用的
java.comm實現。只有用 Java 實現的部分會被使用,而 Unix 本機庫會被忽略。 - RxTx 2.0,以便在通用 JavaComm 版本之下具有與 JavaComm 包附帶的版本不同的提供程式。
但是,如果只想針對gnu.io替換包進行程式設計,則
- 只需要 RxTx 2.1。
將 JavaComm 應用程式轉換為 RxTx
[edit | edit source]因此,如果您屬於因 Sun 放棄對 JavaComm 的 Windows 支援而感到失望的眾多使用者之一,您需要將 JavaComm 應用程式轉換為 RxTx。如上所述,有兩種方法可以做到這一點。兩者都假設您首先成功安裝了 RxTx 的版本。然後,您可以選擇:
- 使用 RxTx 2.0 作為 JavaComm 提供程式
- 將應用程式移植到 RxTx 2.1
第一個選項已在前面解釋過。第二個選項出奇地簡單。要將某個應用程式從使用 JavaComm 移植到使用 RxTx 2.1,只需將應用程式原始碼中所有對 `java.comm` 的引用替換為對 `gnu.io` 的引用。如果原始 JavaComm 應用程式編寫正確,就無需再做其他操作了。
RxTx 2.1 甚至提供了 `contrib/ChangePackage.sh` 工具來執行 Unix 環境下原始碼樹的全域性替換。在其他平臺上,使用支援良好重構功能的 IDE 可以輕鬆實現全域性替換。
- jSerialComm 主頁 在 github 上 - 包含有關如何使用它的資訊 - Javadoc
- Sun Java 通訊 API
- Linux 上的 Java Comm 序列埠 API 操作指南
- jSSC - java 序列埠庫。可在 Win32(Win98-Win7)、Win64(x86-64)、Linux x86、Linux x86-64 上執行
- RxTx 主頁(檔案只能透過 FTP 訪問(2017 年 5 月))
- jRxTx 在 github 上 - RxTx 的一個新包裝器,提供了一個“與 RXTX 相比,新的改進的 API”
- 非官方 Java Web Start/JNLP 常見問題解答 - 如何將 Web Start 和 Comm API 結合使用?
- SerialIO 提供其 SerialPort 軟體包的免費試用版
- Ben Resner 提供了 其 SimpleSerial 軟體包的免費下載 以及 一個沒有 C++ 程式碼的更新版本
- gurux.serial.java 是一個易於使用的適用於 Windows 和 Linux 的開源序列埠庫
序列程式設計: 簡介和 OSI 網路模型 -- RS-232 接線和連線 -- 典型的 RS232 硬體配置 -- 8250 UART -- DOS -- MAX232 驅動器/接收器系列 -- Windows 中的 TAPI 通訊 -- Linux 和 Unix -- Java -- Hayes 相容調變解調器和 AT 命令 -- 通用序列匯流排 (USB) -- 形成資料包 -- 錯誤糾正方法 -- 雙向通訊 -- 資料包恢復方法 -- 序列資料網路 -- 實際應用開發 -- 序列連線上的 IP
