跳转到主要内容

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

当您初次登录到 developerWorks 时,将会为您创建一份概要信息。您在 developerWorks 概要信息中选择公开的信息将公开显示给其他人,但您可以随时修改这些信息的显示状态。您的姓名(除非选择隐藏)和昵称将和您在 developerWorks 发布的内容一同显示。

所有提交的信息确保安全。

  • 关闭 [x]

当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

所有提交的信息确保安全。

  • 关闭 [x]

蓝牙技术,第 2 部分: 创建蓝牙音乐商店

创建通用的 OBEX 文件传输客户机应用程序

Bruce Hopkins (bhopkins@gestalt-llc.com), 技术架构师, Gestalt LLC
Bruce Hopkins 是 Bluetooth for Java (Apress)的作者,也是 JB-22 开发包的创建者。他毕业于底特律的 Wayne 州立大学,拥有电子和计算机工程学士学位。他目前是 Gestalt LLC 的技术架构师,专攻分布式计算、Web 服务和无线技术。您可以通过他的电子邮件 bhopkins@gestalt-llc.com 与他联系。

简介: 对象交换(Object Exchange,OBEX)是在两个蓝牙设备之间发送和接收文件的首选方法。这个系列的 第 1 部分 介绍了 OBEX 的语义,解释了如何创建简单的 OBEX 服务器应用程序 FileServer.java。 在这篇文章中,将学习如何创建简单的 OBEX 客户机应用程序 FileClient.java,它能把文件传输到服务器应用程序。还将学习如何修改 OBEX 客户机应用程序,把它变成一个蓝牙音乐商店。

发布日期: 2005 年 12 月 15 日
级别: 初级
访问情况 : 1318 次浏览
评论: 


在这篇文章中,我将演示如何创建一个简单的 OBEX 客户机应用程序,这个程序能够把文件传输到服务器应用程序。您还将学习到如何把 OBEX 客户机应用程序修改成蓝牙音乐商店。OBEX 是在两个蓝牙设备之间发送和接收文件的首选方法。(在这个两部分构成的系列的 第 1 部分 中,我介绍了 OBEX 的语义并解释了如何创建 OBEX 服务器应用程序。)

我先从创建 OBEX 客户机应用程序开始。在研究构建 OBEX 客户机应用程序所需的代码之前,请简要查看一下 OBEX 服务器应用程序。图 1 显示了启动时的 FileServer.java 应用程序。


图 1. 启动时的 FileServer.java
启动时的 FileServer.java

图 1 可以看到,服务器应用程序已经就绪,正在等候客户机的连接,所以现在来看如何构建 OBEX 客户机应用程序。

创建 OBEX 客户机应用程序

图 2 中可以看到,FileClient.java 看起来很像 FileServer.java,所以我现在要跳过 FileClient.java 的细节。


图 2. 启动时的 FileClient.java
启动时的 FileClient.java

现在,就像在这个系列的 第 1 部分 中说过的,比起蓝牙服务器,蓝牙客户机要做的工作多得多,而且只要想一下,就会理解为什么。首先,客户机如何知道到哪里寻找服务器?(不要担心,对于每个蓝牙客户机/服务器应用程序都存在这个问题;不是单独针对当前情况的)。为了让任何 蓝牙客户机都能找到蓝牙服务器,客户机必须发现它。

蓝牙设备发现

DeviceDiscoverer.java 是个 helper 应用程序,FileClient.java 用它找到附近的远程蓝牙设备。清单 1 提供了 DeviceDiscoverer.java 的 import 语句、类声明和构造函数。


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

  }


清单 3. DeviceDiscoverer.inquiryCompleted()
                
  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. 发现过程之后的 FileClient.java
发现过程之后的 FileClient.java

看起来好像有点问题。根据 图 3,在区域内有四个远程蓝牙设备,但是怎么才能知道哪个正在运行 FileServer.java 呢?好问题。这正是服务发现发挥作用的地方。不要担心,我还要介绍另外一个 helper 类,它可以协助搜索所需要的特定服务。

蓝牙服务发现

清单 4 包含 ServiceDiscoverer.java 的 import 语句、类声明和构造函数。


清单 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.javarun() 方法。


清单 5. ServiceDiscoverer.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 所示。


清单 6. ServiceDiscoverer.servicesDiscovered()
                
  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. 服务搜索过程之后的 FileClient.java
服务搜索过程之后的 FileClient.java

根据 图 4,我已经发现了匹配的服务,它存在于 ibook 上。您可能注意到,虽然我在 ibook 上发现了匹配的服务,我还继续在 Vector 中迭代并搜索服务。可以看到,两个 helper 类方便地得到了服务器的连接 URL。既然有了到服务器的 URL,就让我们连接服务器并向它发送文件!

连接服务器并发送文件

为了让 FileClient.java 的代码尽量简洁,我把所有 OBEX 客户机的代码都分离到一个叫做 ObjectPusher.java 的文件中。当然,ObjectPusher.java 是多线程的,这样在文件传输过程中,就不会挂起 GUI 应用程序。清单 7 显示了 ObjectPusher.run() 的代码。


清单 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


图 5. 文件传输过程之后的 FileClient.java
文件传输过程之后的 FileClient.java

结束语:创建蓝牙音乐商店

第 1 部分 中,学习了如何创建 OBEX 服务器应用程序。在这篇文章中,学习了如何创建通用的 OBEX 客户机应用程序。可以看到,OBEX 创建起来更难,但是这篇文章提供了几个 helper 类,可以在设备发现和服务发现过程中提供帮助。图 6 显示了一个我称之为蓝牙音乐商店的简单应用程序。


图 6. 蓝牙音乐商店
蓝牙音乐商店

这是 FileClient.java 的一个修改版,采用了 helper 类 DeviceDiscoverer.javaServiceDiscoverer.javaObjectPusher.java。使用蓝牙音乐商店,可以选择 MP3 格式的歌曲或铃音,并把它发送到任何支持 OBEX 的手机、PDA 或计算机上。很酷,是么?

那么为什么我把它叫做“音乐商店”呢?当然,如果您拥有音乐文件、铃音或 podcast 的版本,那么您就可以容易地采用这个应用程序为基础,做一个售货应用程序,销售音频文件了!


参考资料

学习

获得产品和技术

  • 请下载这篇文章中使用的全部三个 FileClient、FileServer 和蓝牙音乐商店应用程序,它们都在一个 ZIP 文件中

  • Open OBEX Project 正在规划 Object Exchange 协议的开放源码实现,它是一个会话协议,用二进制 HTTP 协议描述最合适。

  • 这篇文章使用的示例是用 JB-22 Java Bluetooth Development kit 创建的。JB-22 是一个提供蓝牙硬件和软件特性的完整的 Java 蓝牙开发包,起价 $199。

讨论

关于作者

Bruce Hopkins

Bruce Hopkins 是 Bluetooth for Java (Apress)的作者,也是 JB-22 开发包的创建者。他毕业于底特律的 Wayne 州立大学,拥有电子和计算机工程学士学位。他目前是 Gestalt LLC 的技术架构师,专攻分布式计算、Web 服务和无线技术。您可以通过他的电子邮件 bhopkins@gestalt-llc.com 与他联系。

关于报告滥用的帮助

报告滥用

谢谢! 此内容已经标识给管理员注意。


关于报告滥用的帮助

报告滥用

报告滥用提交失败。 请稍后重试。


developerWorks:登录


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 使用条款

 


当您初次登录到 developerWorks 时,将会为您创建一份概要信息。您在 developerWorks 概要信息中选择公开的信息将公开显示给其他人,但您可以随时修改这些信息的显示状态。您的姓名(除非选择隐藏)和昵称将和您在 developerWorks 发布的内容一同显示。

请选择您的昵称:

当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

(长度在 3 至 31 个字符之间)


单击提交则表示您同意developerWorks 的条款和条件。 使用条款.

 


为本文评分

评论

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology
ArticleID=163098
ArticleTitle=蓝牙技术,第 2 部分: 创建蓝牙音乐商店
publish-date=12152005
author1-email=bhopkins@gestalt-llc.com
author1-email-cc=

标签

Help
使用 搜索 文本框在 My developerWorks 中查找包含该标签的所有内容。

使用 滑动条 调节标签的数量。

热门标签 显示了特定专区最受欢迎的标签(例如 Java technology,Linux,WebSphere)。

我的标签 显示了特定专区您标记的标签(例如 Java technology,Linux,WebSphere)。

使用搜索文本框在 My developerWorks 中查找包含该标签的所有内容。热门标签 显示了特定专区最受欢迎的标签(例如 Java technology,Linux,WebSphere)。我的标签 显示了特定专区您标记的标签(例如 Java technology,Linux,WebSphere)。