跳转至内容

串口编程/Java串口

来自 Wikibooks,开放世界中的开放书籍

使用Java进行串口通信

[编辑 | 编辑源代码]

由于Java的平台无关性,串口接口很困难。串口接口需要一个标准化的API,以及平台特定的实现,这对Java来说很难。

不幸的是,Sun 在Java中并没有太关注串口通信。Sun 定义了一个串口通信API,称为 JavaComm,但该API的实现不是Java标准版的一部分。Sun 为少数几个,但不是所有Java平台提供了参考实现。特别是,在2005年底,Sun 悄然撤回了对JavaComm对Windows的支持。一些遗漏平台的第三方实现是可用的。JavaComm 在维护方面并没有得到太多关注,Sun 只进行了最基本的维护,除了 Sun 似乎响应了其 Sun Ray 瘦客户端买家的压力,并将JavaComm 适应了这个平台,同时放弃了对Windows的支持。

这种情况,以及 Sun 最初没有为 Linux 提供JavaComm实现的事实(从2006年开始,他们现在有了),导致了免费软件 RxTx 库的开发。RxTx 可用于多个平台,而不仅仅是 Linux。它可以与JavaComm结合使用(RxTx 提供特定于硬件的驱动程序),也可以单独使用。当用作JavaComm驱动程序时,JavaComm API和RxTx之间的桥接由JCLJavaComm for Linux)完成。JCLRxTx分发的一部分。

Sun 对JavaComm的忽视以及JavaComm的特定编程模型,使JavaComm获得了难以使用的声誉。RxTx - 如果不用作JavaComm驱动程序 - 提供了一个更丰富的接口,但不是标准化的。RxTx 支持的平台比现有的JavaComm实现更多。最近,RxTx 已经被采用,以提供与JavaComm相同的接口,只是包名不匹配 Sun 的包名。

那么,在应用程序中应该使用哪个库呢?如果需要最大的可移植性(对于“最大”的某些值而言),那么JavaComm 是一个不错的选择。如果某个特定平台没有可用的JavaComm实现,但有RxTx实现,那么RxTx 可以用作该平台上JavaComm的驱动程序。因此,通过使用 JavaComm,可以支持所有直接由 Sun 的参考实现或由 RxTx 与 JCL 支持的平台。这样,应用程序就不需要更改,并且可以针对单个接口,即标准化的JavaComm接口进行工作。

本模块讨论了JavaCommRxTx。它主要侧重于演示概念,而不是提供可运行的代码。那些想要盲目复制代码的人可以参考包附带的示例代码。那些想知道他们在做什么的人可能在这个模块中找到一些有用的信息。

jSSC (Java Simple Serial Connector) 也应该被考虑

还有一个名为jSerialComm t的库,它将其所有平台特定文件包含在它的 jar 中,这使其真正可移植,因为不需要安装。

  • 学习 串口通信和编程 的基础知识。
  • 准备好要与其通信的设备(例如,调制解调器)的文档。
  • 设置所有硬件和测试环境
  • 例如,使用终端程序手动与设备通信。这样做是为了确保测试环境设置正确,并且你已经理解了设备的命令和响应。
  • 下载要在特定操作系统上使用的 API 实现
  • 阅读
    • JavaComm 和/或 RxTx 安装说明(并按照说明进行操作)
    • API 文档
    • 附带的示例源代码

一般问题

[编辑 | 编辑源代码]

JavaCommRxTX 都有一些安装怪癖。强烈建议逐字逐句地遵循安装说明。如果说明说 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 中。

对于 JavaCommRxTx 来说,一个普遍的问题是,它们抵抗通过 Java WebStart 安装。

JavaComm 臭名昭著,因为它需要将一个名为 javax.comm.properties 的文件放在 JDK lib 目录中,而这无法通过 Java WebStart 完成。这尤其令人难过,因为对该文件的需求是 JavaComm 中某些不必要的​​设计/决策的结果,JavaComm 设计人员本可以轻松避免这种情况。Sun 始终拒绝纠正这个错误,声称该机制至关重要。关于 JavaComm,他们是在撒谎,尤其是因为 Java 很久以前就拥有一个服务提供商体系结构,专门用于此类目的。

属性文件的内容通常只有一行,即包含本机驱动程序的 java 类名,例如:

 driver=com.sun.comm.Win32Driver

以下是一个技巧,可以忽略那个死脑筋的属性文件,通过 Web Start 部署 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-to-serial 转换器)时,这通常会导致问题。可以通过系统属性覆盖此机制。有关详细信息,请参见 RxTx 安装说明。

jSerialComm
[编辑 | 编辑源代码]

与 RxTx 和 JavaComm 相比,jSerialComm 在许多操作系统和平台(例如 Windows x86/x86_64、Linux x86/x86_64、ARM 甚至 Android - 特定库 jar 中的完整列表)上无需任何更改即可使用。但是它仍然需要访问设备的权限(有关更多信息,请参见 jSerialComm 主页)。

SerialPundit

SerialPundit 是另一个功能丰富的库,用于在 Java 中访问串行端口。它包括诸如检测何时将 FTDI232 等 USB-UART 设备插入系统、自动识别操作系统和 CPU 架构、无需任何安装、全面记录、经过良好测试以及提供支持/讨论组等功能。

JavaComm API

[编辑 | 编辑源代码]

Java 中用于串行通信的官方 API 是 JavaComm API。此 API 不是标准 Java 2 版本的一部分。相反,必须单独下载 API 的实现。不幸的是,JavaComm 没有得到 Sun 的太多关注,并且很长时间没有得到维护。Sun 偶尔会进行一些微不足道的错误修复,但没有进行长期需要的重大改进。

本节解释了 JavaComm API 的基本操作。提供的源代码保持简单,以演示重要要点。在真实应用程序中使用时,需要对其进行增强。

本章中的源代码不是唯一的示例代码。JavaComm 下载附带几个示例。这些示例包含的信息几乎比 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)重复安装。

IDE 通常有特定于 IDE 的方式来让 IDE 了解新的库(类和文档)。通常,像 JavaComm 这样的库不仅需要让 IDE 了解它本身,还需要让每个打算使用该库的项目都了解它。请阅读 IDE 的文档。需要注意的是,旧版的 JavaComm 2.0 附带了 JavaDoc API 文档,其结构采用的是历史上的 Java 1.0 JavaDoc 布局。一些现代 IDE 已经不再了解这种结构,无法将其集成到其帮助系统中。在这种情况下,需要使用外部浏览器来阅读文档(推荐的操作...)。

安装软件后,建议检查样本和 JavaDoc 目录。构建并运行一个样本应用程序以验证安装是否正确是很有意义的。样本应用程序通常需要进行一些小的调整才能在特定平台上运行(例如,更改硬编码的串口标识符)。在尝试运行样本应用程序时,最好有一些串行硬件可用,比如电缆、零调制解调器、串口转换器、真正的调制解调器、程控交换机等。 Serial_Programming:RS-232 ConnectionsSerial_Programming:Modems and AT Commands 提供了一些有关如何设置串行应用程序开发环境硬件部分的信息。

查找所需的串行端口

[编辑 | 编辑源代码]

使用 JavaComm 对串行线路进行编程时,通常需要做的三件事是

  1. 枚举 JavaComm 可用的所有串行端口(端口标识符),
  2. 从可用端口标识符中选择所需的端口标识符,以及
  3. 通过端口标识符获取端口。

枚举和选择所需的端口标识符通常在一个循环中完成

 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
 //
 ...

注意
JavaComm 本身从其平台特定的驱动程序中获取可用的串行端口标识符的默认列表。此列表实际上无法通过 JavaComm 配置。CommPortIdentifier.addPortName() 方法具有误导性,因为驱动程序类是平台特定的,其实现不属于公共 API。根据驱动程序的不同,端口列表可以在驱动程序中进行配置/扩展。因此,如果在 JavaComm 中找不到特定端口,有时修改驱动程序可以解决问题。

找到端口标识符后,就可以使用它来获取所需的端口

 //
 // 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.
 //
 ...

初始化串行端口

[编辑 | 编辑源代码]

串行端口的初始化非常简单。可以分别设置通信首选项(波特率、数据位、停止位、奇偶校验),也可以使用 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();

简单的数据传输

[编辑 | 编辑源代码]

简单的数据写入

[编辑 | 编辑源代码]

写入串行端口与基本的 Java IO 一样简单。但是,如果使用 AT Hayes 协议,则需要注意一些注意事项

  1. 不要在 OutputStream 上使用 println(或自动追加 "\n" 的其他方法)。调制解调器 AT Hayes 协议期望 "\r\n" 作为分隔符(无论底层操作系统是什么)。
  2. 在写入 OutputStream 后,如果调制解调器设置为回显命令行,则 InputStream 缓冲区将包含发送给它的命令的重复(带换行符),以及另一个换行符(对“AT”命令的答复)。因此,在写入操作过程中,请确保从 InputStream 中清除这些信息(实际上可以用于错误检测)。
  3. 如果使用 Reader/Writer(不是一个好主意),至少将字符编码设置为 US-ASCII,而不是使用平台的默认编码,该编码可能有效也可能无效。
  4. 由于使用调制解调器时的主要操作是不经修改地传输数据,因此应通过 InputStream/OutputStream 处理与调制解调器的通信,而不是使用 Reader/Writer。


Clipboard

待办事项

  • 解释如何在同一流上混合二进制和字符 I/O
  • 修复示例以使用流


 // 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

简单的数据读取(轮询)

[编辑 | 编辑源代码]

如果正确执行了写入操作(见上文),则读取操作就像一个命令一样简单

 // Read the response
 String response = is.readLine(); // if you sent "AT" then response == "OK"

简单读取/写入问题

[编辑 | 编辑源代码]

在前面几节中演示的简单串行端口读取和/或写入方法存在严重的缺点。这两种操作都是使用阻塞 I/O 完成的。这意味着,当

  • 没有可供读取的数据,或者
  • 用于写入的输出缓冲区已满(设备不再接受(更多)数据),

读取或写入方法(在前面的示例中为os.print()is.readLine())不会返回,应用程序将停止运行。更准确地说,执行读取或写入操作的线程将被阻塞。如果该线程是主应用程序线程,则应用程序将冻结,直到解决阻塞条件(数据可供读取或设备再次接受数据)。

除非应用程序非常原始,否则应用程序冻结是不可接受的。例如,至少应允许某些用户交互来取消通信。需要的是非阻塞 I/O异步 I/O。但是,JavaComm 基于 Java 的标准阻塞 I/O 系统(InputStreamOutputStream),但有所不同,如下文所述。

提到的“不同”之处在于,JavaComm 通过事件通知机制提供了一些对异步 I/O 的有限支持。但是,Java 中在阻塞 I/O 系统之上实现非阻塞 I/O 的一般解决方案是使用线程。实际上,这对于串行写入是可行的解决方案,强烈建议使用单独的线程写入串行端口 - 即使使用事件通知机制,如下文所述。

读取也可以在单独的线程中处理。但是,如果使用 JavaComm 事件通知机制,则没有必要这样做。因此,总结

活动 架构
读取 使用事件通知和/或单独线程
写入 始终使用单独线程,可以选择使用事件通知

以下几节将提供一些详细信息。

事件驱动的串行通信

[编辑 | 编辑源代码]

JavaComm API 提供了事件通知机制,以克服阻塞 I/O 带来的问题。但是,以典型的 Sun 方式,这种机制并非没有问题。

原则上,应用程序可以向特定的 SerialPort 注册事件侦听器,以便及时了解该端口上发生的重大事件。用于读取和写入数据的两个最有趣的事件类型是

  • javax.comm.SerialPortEvent.DATA_AVAILABLE
  • javax.comm.SerialPortEvent.OUTPUT_BUFFER_EMPTY.

但也有两个问题

  1. 每个 SerialPort 只能注册一个事件侦听器。这迫使程序员编写“怪物”侦听器,根据事件类型进行区分。
  2. OUTPUT_BUFFER_EMPTY 是一个可选的事件类型。Sun 在文档中隐晦地指出,并非所有 JavaComm 实现都支持生成此类型的事件。

在详细介绍之前,下一节将介绍实现和注册串行事件处理程序的主要方法。请记住,只能有一个处理程序,并且它必须处理所有可能的事件。

设置串行事件处理程序

[编辑 | 编辑源代码]
 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 */

数据写入

[编辑 | 编辑源代码]
Clipboard

待办事项


为写入设置独立线程
[编辑 | 编辑源代码]

使用独立线程进行写入只有一个目的:避免当串行端口无法写入时,整个应用程序阻塞。

一个简单的、线程安全的环形缓冲区实现
[编辑 | 编辑源代码]

使用独立线程进行写入,独立于某些主应用程序线程,意味着需要一种方法将需要写入的数据从应用程序线程传递到写入线程。一个共享的、同步的数据缓冲区,例如 byte[],就可以实现。此外,还需要一种方法让主应用程序确定它是否可以写入数据缓冲区,或者数据缓冲区当前是否已满。如果数据缓冲区已满,则可能表明串行端口未准备好,输出数据已排队。主应用程序将必须轮询共享数据缓冲区中新空间的可用性。然而,在主应用程序轮询之间,它可以执行其他操作,例如更新 GUI、提供具有中止发送功能的命令提示符等。

乍一看,PipedInputStream/PipedOutputStream 对似乎是这种通信的不错选择。但是,如果管道流真正有用,Sun 就不应该是 Sun 了。PipedInputStream 阻塞,如果对应的 PipedOutputStream 未及时清除。因此,应用程序线程将被阻塞。这正是通过使用独立线程想要避免的事情。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;
     }
 }

使用这个环形缓冲区,可以以受控的方式将数据从一个线程传递到另一个线程。任何其他线程安全、非阻塞机制也可以实现。这里关键点在于,当缓冲区已满时,写入不会阻塞,当没有内容可读时,写入也不会阻塞。

将缓冲区与串行事件一起使用
[编辑 | 编辑源代码]
在写入中使用 OUTPUT_BUFFER_EMPTY 事件
[编辑 | 编辑源代码]

参考 设置串行事件处理程序 一节中介绍的事件处理程序框架,现在可以使用 一个简单的、线程安全的环形缓冲区实现 一节中的共享环形缓冲区来支持 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
        
    }

数据读取

[编辑 | 编辑源代码]

以下示例假设数据的目标是某个文件。每当数据可用时,就会从串行端口获取数据并写入文件。这是一种极其简化的视图,因为在现实中,需要检查数据以获取文件结束指示,例如,返回到调制解调器命令模式。

 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();
     }

在一个应用程序中处理多个端口

[编辑 | 编辑源代码]

调制解调器控制

[编辑 | 编辑源代码]

JavaComm 严格关注串行接口的处理和通过该接口传输数据。它不了解或不提供任何对更高层协议的支持,例如,用于控制消费级调制解调器的 Hayes 调制解调器命令。这仅仅不是 JavaComm 的工作,也不是错误。

与任何其他特定串行设备一样,如果希望通过 JavaComm 控制调制解调器,则需要在 JavaComm 之上编写必要的代码。页面 “兼容 Hayes 的调制解调器和 AT 命令” 提供了处理 Hayes 调制解调器所需的必要基本通用信息。

某些操作系统,例如 Windows 或某些 Linux 发行版,提供了一种或多或少标准化的方法,用于为操作系统配置特定调制解调器类型或品牌的调制解调器控制命令。例如,Windows 调制解调器“驱动程序”通常只是注册表项,描述了特定调制解调器(实际驱动程序是通用的串行调制解调器驱动程序)。JavaComm 本身没有访问此类操作系统特定数据的规定。因此,您要么需要提供一个独立的仅限 Java 的工具来允许用户为使用特定调制解调器配置应用程序,要么需要添加一些平台特定(原生)代码。

概述和版本

[编辑 | 编辑源代码]

由于 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
RxTx 版本,带有完整的 gnu.io 包,用于替换 java.comm。该版本应该起源于 RxTx 1.5,这是 gnu.io 支持开始的地方。

因此,如果要针对原始 JavaComm API 进行编程,则需要

  1. Sun 的通用 JavaComm 版本。截至撰写本文时,这实际上是 Unix 软件包(它包含对各种 Unix 版本的支持,如 Linux 或 Solaris)。即使在 Windows 上使用,也需要 Unix 软件包来提供通用的 java.comm 实现。只有 Java 中实现的部分被使用,而 Unix 原生库被忽略。
  2. 为了使用不同于 JavaComm 包提供的通用 JavaComm 版本的提供者,需要使用 RxTx 2.0。

但是,如果只是想针对 `gnu.io` 替换包进行编程,那么

  • 只需要 RxTx 2.1。

将 JavaComm 应用程序转换为 RxTx

[编辑 | 编辑源代码]

因此,如果您属于 Sun 停止对 JavaComm 的 Windows 支持后感到失望的大量人群,则需要将 JavaComm 应用程序转换为 RxTx。如上所述,有两种方法可以做到。两者都假设您设法首先安装了 RxTx 版本。然后,选项是:

  1. 使用 RxTx 2.0 作为 JavaComm 提供者
  2. 将应用程序移植到 RxTx 2.1

第一个选项已经解释过。第二个选项出奇地简单。要将某个应用程序从使用 JavaComm 移植到使用 RxTx 2.1,只需将应用程序源代码中所有对 `java.comm` 的引用替换为对 `gnu.io` 的引用。如果原始 JavaComm 应用程序编写得当,就无需再做任何操作。

RxTx 2.1 甚至提供了 `contrib/ChangePackage.sh` 工具来对 Unix 下的源代码树执行全局替换。在其他平台上,使用支持一组完善的重构功能的 IDE 可以轻松执行全局替换。

另请参阅

[编辑 | 编辑源代码]
华夏公益教科书