级别: 初级 Bruce Hopkins, 技术架构师, Gestalt LLC
2005 年 12 月 15 日 对象交换(Object Exchange,OBEX)是在两个蓝牙设备之间发送和接收文件的首选方法。这个系列的 第 1 部分 介绍了 OBEX 的语义,解释了如何创建简单的 OBEX 服务器应用程序 FileServer.java 。 在这篇文章中,将学习如何创建简单的 OBEX 客户机应用程序 FileClient.java ,它能把文件传输到服务器应用程序。还将学习如何修改 OBEX 客户机应用程序,把它变成一个蓝牙音乐商店。
在这篇文章中,我将演示如何创建一个简单的 OBEX 客户机应用程序,这个程序能够把文件传输到服务器应用程序。您还将学习到如何把 OBEX 客户机应用程序修改成蓝牙音乐商店。OBEX 是在两个蓝牙设备之间发送和接收文件的首选方法。(在这个两部分构成的系列的 第 1 部分 中,我介绍了 OBEX 的语义并解释了如何创建 OBEX 服务器应用程序。)
我先从创建 OBEX 客户机应用程序开始。在研究构建 OBEX 客户机应用程序所需的代码之前,请简要查看一下 OBEX 服务器应用程序。图 1 显示了启动时的 FileServer.java 应用程序。
从 图 1 可以看到,服务器应用程序已经就绪,正在等候客户机的连接,所以现在来看如何构建 OBEX 客户机应用程序。
从 图 2 中可以看到,FileClient.java 看起来很像 FileServer.java ,所以我现在要跳过 FileClient.java 的细节。
现在,就像在这个系列的 第 1 部分 中说过的,比起蓝牙服务器,蓝牙客户机要做的工作多得多,而且只要想一下,就会理解为什么。首先,客户机如何知道到哪里寻找服务器?(不要担心,对于每个蓝牙客户机/服务器应用程序都存在这个问题;不是单独针对当前情况的)。为了让任何 蓝牙客户机都能找到蓝牙服务器,客户机必须发现它。
DeviceDiscoverer.java 是个 helper 应用程序,FileClient.java 用它找到附近的远程蓝牙设备。清单 1 提供了 DeviceDiscoverer.java 的 import 语句、类声明和构造函数。
import javax.bluetooth.*; import java.util.*;
public class DeviceDiscoverer implements DiscoveryListener { FileClient client; Vector remoteDevices = new Vector(); DiscoveryAgent discoveryAgent; public DeviceDiscoverer(FileClient client) { this.client = client; try { LocalDevice localDevice = LocalDevice.getLocalDevice(); discoveryAgent = localDevice.getDiscoveryAgent();
client.updateStatus("[client:] LocalDevice properties: " + localDevice.getFriendlyName() + " (" + localDevice.getBluetoothAddress() + ")"); client.updateStatus("[client:] Searching for Bluetooth devices in the vicinity..."); discoveryAgent.startInquiry(DiscoveryAgent.GIAC, this);
} catch(Exception e) { e.printStackTrace(); } }
|
在 清单 1 中可以看出,DeviceDiscoverer.java 实现了 javax.bluetooth.DiscoveryListener 。这样,在发现远程蓝牙设备的时候,helper 类就会得到通知。要启动设备发现过程,首先需要得到 javax.bluetooth.LocalDevice 的实例,这就允许得到 javax.bluetooth.DiscoveryAgent 的实例。在实例化 DiscoveryAgent 之后,就可以自由地调用 discoveryAgent.startInquiry() 了,它将启动设备的发现过程。
在 清单 1 中您可能注意到 LocalDevice 具有一些关于您自己的蓝牙设备的持久信息,例如它的友好名称(像 “Bruce's laptop” 或 “Joe's PDA”)。LocalDevice 也知道您的 6 字节蓝牙地址,例如 00:0A:3E:56:57:B5。出于信息性的目的,DeviceDiscoverer.java 在进入发现过程之前会显示这个信息。
如果回头看 图 2,可以看到 FileClient.java 有三个按钮,其中一个按钮的名称是 Discover Devices。当点击 Discover Devices 按钮时,会实例化 helper 类 DeviceDiscoverer.java 。如果是初次接触蓝牙,您可能会认为设备发现过程是瞬间完成的。但不幸的是,不是这样的。
不过,好消息是:因为 helper 类 DeviceDiscoverer.java 是一个 DiscoveryListener ,所以在发现蓝牙设备时,它会异步地得到通知。对于在附近发现的每个远程蓝牙设备,Java 虚拟机(JVM)都会调用 deviceDiscovered() 方法。当设备发现过程结束时,JVM 还会调用 inquiryCompleted() 方法。清单 2 和 清单 3 分别演示了对 deviceDiscovered() 和 inquiryCompleted() 的调用。
清单 2. DeviceDiscoverer.deviceDiscovered()
public void deviceDiscovered(RemoteDevice remoteDevice, DeviceClass cod) {
try{ remoteDevices.addElement(remoteDevice); client.updateStatus("[client:] New device discovered : " + remoteDevice.getFriendlyName(true)+ " (" + remoteDevice.getBluetoothAddress() + ")" );
} catch(Exception e){ e.printStackTrace(); }
}
|
public void inquiryCompleted(int discType) { String inqStatus = null; if (discType == DiscoveryListener.INQUIRY_COMPLETED) { inqStatus = "[client:] Inquiry completed"; } else if (discType == DiscoveryListener.INQUIRY_TERMINATED) { inqStatus = "[client:] Inquiry terminated"; } else if (discType == DiscoveryListener.INQUIRY_ERROR) { inqStatus = "[client:] Inquiry error"; } client.updateStatus(inqStatus); client.serviceButton.setEnabled(true); client.deviceButton.setEnabled(false); }
|
在 清单 2 中可以看出,每当发现新的蓝牙设备,我就把它加入 Vector 并显示远程设备的友好名称和蓝牙地址。当设备发现过程结束时,我用发现过程的状态更新客户机,不管是成功还是失败。图 3 显示了 FileClient.java 实例化了 DeviceDiscoverer.java 并发现附近所有蓝牙设备之后的情况。
请看发现过程之后的 FileClient.java 。
看起来好像有点问题。根据 图 3,在区域内有四个远程蓝牙设备,但是怎么才能知道哪个正在运行 FileServer.java 呢?好问题。这正是服务发现发挥作用的地方。不要担心,我还要介绍另外一个 helper 类,它可以协助搜索所需要的特定服务。
清单 4 包含 ServiceDiscoverer.java 的 import 语句、类声明和构造函数。
import javax.bluetooth.*; import java.io.*; import java.util.Vector;
public class ServiceDiscoverer extends Thread implements DiscoveryListener { UUID[] uuidSet = {new UUID("8841", true)}; int[] attrSet = {0x0100, 0x0003, 0x0004}; FileClient client; ServiceRecord serviceRecord; String connectionURL; Vector deviceList;
public ServiceDiscoverer(FileClient client, Vector deviceList) {
this.client = client; this.deviceList = deviceList;
}
|
从 清单 4 可以看出,ServiceDiscoverer.java 与第一个 helper 类 DeviceDiscoverer.java 非常相似,具体来说就是二者都实现了相同的接口。但是,ServiceDiscoverer.java 的不同之处在于,它需要在独立的线程中运行;如果它不这么做,FileClient.java 的用户界面在它搜索附近蓝牙设备上的服务时就会挂起。
您还记得第 1 部分中说过每个蓝牙服务(不论是否使用 OBEX)都必须拥有惟一的标识符么?您可能会回忆起我把 FileServer.java 的 UUID 设为 8841,与我在 ServiceDiscoverer.java 中设置的值相同。这样,ServiceDiscoverer.java
在远程蓝牙设备上只会找到 UUID 为 8841 的服务。请注意 UUID 可以是 4 位长或 16 位长;我选择短 UUID
是为了提供一个更容易的示例。我还请您注意,在构造函数中,我传递进一个 Vector,它包含第一个 helper 类发现的所有远程蓝牙设备。清单 5 提供了 ServiceDiscoverer.java 的 run() 方法。
public void run(){
try { LocalDevice localDevice = LocalDevice.getLocalDevice(); DiscoveryAgent discoveryAgent = localDevice.getDiscoveryAgent(); RemoteDevice remoteDevice = null;
for(int i=0; i < deviceList.size(); i++){ remoteDevice = (RemoteDevice)deviceList.get(i);
client.updateStatus("[client:] Searching for Services on: " + remoteDevice.getFriendlyName(true)+ " (" + remoteDevice.getBluetoothAddress() + ")" ); discoveryAgent.searchServices(attrSet, uuidSet, remoteDevice, this); try{ Thread.sleep(2000); } catch (Exception e){ }
}
} catch(Exception e) { e.printStackTrace(); }
}
|
现在,因为 ServiceDiscoverer.java 实现的接口与其他 helper 类实现的接口相同,所以它用同样的方式得到 DiscoveryAgent ,就像我在 清单 1 中做的那样。您应当注意到,我在远程蓝牙设备的 Vector 上迭代,搜索每个设备上存在的服务。
这些示例在真正的蓝牙硬件上测试过,所以您应当看到,当我在 Vector 中迭代时,我在继续循环之前,“后退” 了两秒。“后退”
的期间取决于硬件;也可能根本不需要。但是如果没有它,蓝牙硬件可能只会在传递进的远程蓝牙设备中搜索第一个设备上的服务,而忽略其他设备。对于每个匹配
UUID 的服务,JVM 都调用 servicesDiscovered() 方法,如 清单 6 所示。
public void servicesDiscovered(int transID, ServiceRecord[] servRecord) { for(int i = 0; i < servRecord.length; i++) {
DataElement serviceNameElement = servRecord[i].getAttributeValue(0x0100); String serviceName = (String)serviceNameElement.getValue();
if(serviceName.equals("FTP")){
client.updateStatus("[client:] A matching service has been found"); try { connectionURL = servRecord[i].getConnectionURL(1,false); } catch (Exception e){ client.updateStatus("[client:] oops"); } client.updateStatus("[client:] The connection URL is: " + connectionURL ); client.serviceButton.setEnabled(false); client.connButton.setEnabled(true); } } }
|
您可能从第 1 部分回忆起来,对于所有的蓝牙服务器来说,UUID 是必需的,服务名称是可选的;但是,我们为服务指定的服务名称是 “FTP”。在 清单 6 中可以看出,我检查了服务名称是不是 FTP,但是要记住蓝牙设备不一定指定服务名称。在确定已经找到匹配的服务之后,我把连接 URL 保存在 String 中,并在客户机上显示一些信息。服务搜索过程之后的 FileClient.java 截屏如 图 4 所示。
根据 图 4,
我已经发现了匹配的服务,它存在于 ibook 上。您可能注意到,虽然我在 ibook 上发现了匹配的服务,我还继续在 Vector
中迭代并搜索服务。可以看到,两个 helper 类方便地得到了服务器的连接 URL。既然有了到服务器的
URL,就让我们连接服务器并向它发送文件!
为了让 FileClient.java 的代码尽量简洁,我把所有 OBEX 客户机的代码都分离到一个叫做 ObjectPusher.java 的文件中。当然,ObjectPusher.java 是多线程的,这样在文件传输过程中,就不会挂起 GUI 应用程序。清单 7 显示了 ObjectPusher.run() 的代码。
public void run(){
try{ connection = Connector.open(connectionURL); client.updateStatus("Connection obtained");
ClientSession cs = (ClientSession)connection; HeaderSet hs = cs.createHeaderSet();
cs.connect(hs); client.updateStatus("OBEX session created");
InputStream is = new FileInputStream(file); byte filebytes[] = new byte[is.available()]; is.read(filebytes); is.close();
hs = cs.createHeaderSet(); hs.setHeader(HeaderSet.NAME, file.getName()); hs.setHeader(HeaderSet.TYPE, "text/plain"); hs.setHeader(HeaderSet.LENGTH, new Long(filebytes.length));
Operation putOperation = cs.put(hs); client.updateStatus("Pushing file: " + file.getName()); client.updateStatus("Total file size: " + filebytes.length + " bytes");
OutputStream outputStream = putOperation.openOutputStream(); outputStream.write(filebytes); client.updateStatus("File push complete");
outputStream.close(); putOperation.close(); cs.disconnect(null);
connection.close(); } catch (Exception e){ }
}
|
显然,ObjectPusher.java 的主要目的是把文件从客户机送到服务器。我从接受连接 URL 并创建连接对象开始。有了连接之后,就能创建 OBEX 会话。
下一步是把要发送的文件转换成字节数组。然后,设置 OBEX 头,并调用 cs.put() 以发起 OBEX PUT 操作。这会返回一个 javax.obex.Operation 对象,我把它命名为 putOperation 。然后创建 OutputStream ,用它发送文件数据,当字节数组写入 OutputStream 的时候,PUT 操作完成。图 5 显示了文件传输过程之后的 FileClient.java 。
在 第 1 部分 中,学习了如何创建 OBEX 服务器应用程序。在这篇文章中,学习了如何创建通用的 OBEX 客户机应用程序。可以看到,OBEX 创建起来更难,但是这篇文章提供了几个 helper 类,可以在设备发现和服务发现过程中提供帮助。图 6 显示了一个我称之为蓝牙音乐商店的简单应用程序。
这是 FileClient.java 的一个修改版,采用了 helper 类 DeviceDiscoverer.java 、ServiceDiscoverer.java 和 ObjectPusher.java 。使用蓝牙音乐商店,可以选择 MP3 格式的歌曲或铃音,并把它发送到任何支持 OBEX 的手机、PDA 或计算机上。很酷,是么?
那么为什么我把它叫做“音乐商店”呢?当然,如果您拥有音乐文件、铃音或 podcast 的版本,那么您就可以容易地采用这个应用程序为基础,做一个售货应用程序,销售音频文件了!
学习
获得产品和技术
- 请下载这篇文章中使用的全部三个 FileClient、FileServer 和蓝牙音乐商店应用程序,它们都在一个 。
- 正在规划 Object Exchange 协议的开放源码实现,它是一个会话协议,用二进制 HTTP 协议描述最合适。
- 这篇文章使用的示例是用 创建的。JB-22 是一个提供蓝牙硬件和软件特性的完整的 Java 蓝牙开发包,起价 $199。
讨论
|