连接蓝牙设备

要创建两个设备之间的连接,您必须同时实现服务器端和客户端机制,因为一个设备必须打开服务器套接字,另一个设备必须使用服务器设备的 MAC 地址启动连接。服务器设备和客户端设备分别通过不同的方式获取所需的 BluetoothSocket。服务器在接受传入连接时接收套接字信息。客户端在打开到服务器的 RFCOMM 通道时提供套接字信息。

当服务器和客户端都在同一个 RFCOMM 通道上拥有连接的 BluetoothSocket 时,它们被视为彼此连接。此时,每个设备都可以获取输入和输出流,并且可以开始数据传输,这将在关于 传输蓝牙数据 的部分中讨论。本节介绍如何启动两个设备之间的连接。

在尝试查找蓝牙设备之前,请确保您拥有适当的 蓝牙权限为您的应用程序设置蓝牙

连接技术

一种实现技术是自动将每个设备准备为服务器,以便每个设备都有一个打开并正在监听连接的服务器套接字。在这种情况下,任何一个设备都可以启动与另一个设备的连接并成为客户端。或者,一个设备可以明确地托管连接并在需要时打开服务器套接字,而另一个设备启动连接。


图 1. 蓝牙配对对话框。

以服务器身份连接

当您想连接两个设备时,其中一个必须充当服务器,并保持一个开放的 BluetoothServerSocket。服务器套接字的目的是监听传入的连接请求,并在接受请求后提供一个已连接的 BluetoothSocket。当从 BluetoothServerSocket 获取 BluetoothSocket 后,可以(也应该)丢弃 BluetoothServerSocket,除非您希望设备接受更多连接。

要设置服务器套接字并接受连接,请完成以下步骤序列

  1. 通过调用 listenUsingRfcommWithServiceRecord(String, UUID) 获取 BluetoothServerSocket

    字符串是您服务的可识别名称,系统会自动将其写入设备上的新服务发现协议 (SDP) 数据库条目。名称是任意的,可以简单地是您的应用名称。通用唯一标识符 (UUID) 也包含在 SDP 条目中,并构成与客户端设备连接协议的基础。也就是说,当客户端尝试与该设备连接时,它会携带一个 UUID,该 UUID 唯一标识它想要连接的服务。为了接受连接,这些 UUID 必须匹配。

    UUID 是一种标准化的 128 位格式,用于表示唯一标识信息的字符串 ID。UUID 用于标识需要在系统或网络中保持唯一性的信息,因为 UUID 重复的概率实际上为零。它是独立生成的,无需使用集中式授权。在本例中,它用于唯一标识您的应用的蓝牙服务。要获取可用于应用的 UUID,可以使用网络上众多随机的 UUID 生成器之一,然后使用 fromString(String) 初始化 UUID。

  2. 通过调用 accept() 开始监听连接请求。

    这是一个阻塞调用。它在接受连接或发生异常时返回。只有当远程设备发送包含与该监听服务器套接字注册的 UUID 相匹配的 UUID 的连接请求时才会接受连接。成功后,accept() 将返回一个已连接的 BluetoothSocket

  3. 除非您想接受其他连接,否则请调用 close()

    此方法调用释放服务器套接字及其所有资源,但不会关闭由 accept() 返回的已连接的 BluetoothSocket。与 TCP/IP 不同,RFCOMM 每次只允许一个已连接的客户端,因此在大多数情况下,在接受连接套接字后立即对 BluetoothServerSocket 调用 close() 很有意义。

由于 accept() 调用是一个阻塞调用,因此不要在主活动 UI 线程中执行它。在另一个线程中执行它可确保您的应用仍然可以响应其他用户交互。通常,在您的应用管理的新线程中执行涉及 BluetoothServerSocketBluetoothSocket 的所有工作是有意义的。要中止诸如 accept() 这样的阻塞调用,请从另一个线程对 BluetoothServerSocketBluetoothSocket 调用 close()。请注意,BluetoothServerSocketBluetoothSocket 上的所有方法都是线程安全的。

示例

以下是一个简化的线程,用于接受传入连接的服务器组件

Kotlin

private inner class AcceptThread : Thread() {

   private val mmServerSocket: BluetoothServerSocket? by lazy(LazyThreadSafetyMode.NONE) {
       bluetoothAdapter?.listenUsingInsecureRfcommWithServiceRecord(NAME, MY_UUID)
   }

   override fun run() {
       // Keep listening until exception occurs or a socket is returned.
       var shouldLoop = true
       while (shouldLoop) {
           val socket: BluetoothSocket? = try {
               mmServerSocket?.accept()
           } catch (e: IOException) {
               Log.e(TAG, "Socket's accept() method failed", e)
               shouldLoop = false
               null
           }
           socket?.also {
               manageMyConnectedSocket(it)
               mmServerSocket?.close()
               shouldLoop = false
           }
       }
   }

   // Closes the connect socket and causes the thread to finish.
   fun cancel() {
       try {
           mmServerSocket?.close()
       } catch (e: IOException) {
           Log.e(TAG, "Could not close the connect socket", e)
       }
   }
}

Java

private class AcceptThread extends Thread {
   private final BluetoothServerSocket mmServerSocket;

   public AcceptThread() {
       // Use a temporary object that is later assigned to mmServerSocket
       // because mmServerSocket is final.
       BluetoothServerSocket tmp = null;
       try {
           // MY_UUID is the app's UUID string, also used by the client code.
           tmp = bluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
       } catch (IOException e) {
           Log.e(TAG, "Socket's listen() method failed", e);
       }
       mmServerSocket = tmp;
   }

   public void run() {
       BluetoothSocket socket = null;
       // Keep listening until exception occurs or a socket is returned.
       while (true) {
           try {
               socket = mmServerSocket.accept();
           } catch (IOException e) {
               Log.e(TAG, "Socket's accept() method failed", e);
               break;
           }

           if (socket != null) {
               // A connection was accepted. Perform work associated with
               // the connection in a separate thread.
               manageMyConnectedSocket(socket);
               mmServerSocket.close();
               break;
           }
       }
   }

   // Closes the connect socket and causes the thread to finish.
   public void cancel() {
       try {
           mmServerSocket.close();
       } catch (IOException e) {
           Log.e(TAG, "Could not close the connect socket", e);
       }
   }
}

在此示例中,只希望一个传入连接,因此一旦接受连接并获取了 BluetoothSocket,应用就会将获取到的 BluetoothSocket 传递给一个单独的线程,关闭 BluetoothServerSocket 并退出循环。

请注意,当 accept() 返回 BluetoothSocket 时,套接字已连接。因此,您不应该像在客户端一样调用 connect()

特定于应用的 manageMyConnectedSocket() 方法旨在启动用于传输数据的线程,这将在有关 传输蓝牙数据 的主题中讨论。

通常,您应该在完成对传入连接的监听后立即关闭您的 BluetoothServerSocket。在此示例中,一旦获取了 BluetoothSocket 就会调用 close()。您可能还想在您的线程中提供一个公共方法,以便在您需要停止在该服务器套接字上监听时关闭私有的 BluetoothSocket

以客户端身份连接

为了与在开放的服务器套接字上接受连接的远程设备建立连接,您必须首先获取一个表示远程设备的 BluetoothDevice 对象。要了解如何创建 BluetoothDevice,请参阅 查找蓝牙设备。然后,您必须使用 BluetoothDevice 获取 BluetoothSocket 并启动连接。

基本过程如下

  1. 使用 BluetoothDevice,通过调用 createRfcommSocketToServiceRecord(UUID) 获取 BluetoothSocket

    此方法将初始化一个 BluetoothSocket 对象,允许客户端连接到 BluetoothDevice。此处传递的 UUID 必须与服务器设备在调用 listenUsingRfcommWithServiceRecord(String, UUID) 以打开其 BluetoothServerSocket 时使用的 UUID 相匹配。要使用匹配的 UUID,请将 UUID 字符串硬编码到您的应用中,然后在服务器代码和客户端代码中引用它。

  2. 通过调用 connect() 启动连接。请注意,此方法是一个阻塞调用。

    客户端调用此方法后,系统会执行 SDP 查找,以查找具有匹配 UUID 的远程设备。如果查找成功且远程设备接受连接,它将共享在连接期间使用的 RFCOMM 通道,并且 connect() 方法将返回。如果连接失败,或者如果 connect() 方法超时(大约 12 秒后),则该方法将抛出 IOException

由于 connect() 是一个阻塞调用,因此您应该始终在与主活动 (UI) 线程分离的线程中执行此连接过程。

示例

以下是启动蓝牙连接的客户端线程的基本示例

Kotlin

private inner class ConnectThread(device: BluetoothDevice) : Thread() {

   private val mmSocket: BluetoothSocket? by lazy(LazyThreadSafetyMode.NONE) {
       device.createRfcommSocketToServiceRecord(MY_UUID)
   }

   public override fun run() {
       // Cancel discovery because it otherwise slows down the connection.
       bluetoothAdapter?.cancelDiscovery()

       mmSocket?.let { socket ->
           // Connect to the remote device through the socket. This call blocks
           // until it succeeds or throws an exception.
           socket.connect()

           // The connection attempt succeeded. Perform work associated with
           // the connection in a separate thread.
           manageMyConnectedSocket(socket)
       }
   }

   // Closes the client socket and causes the thread to finish.
   fun cancel() {
       try {
           mmSocket?.close()
       } catch (e: IOException) {
           Log.e(TAG, "Could not close the client socket", e)
       }
   }
}

Java

private class ConnectThread extends Thread {
   private final BluetoothSocket mmSocket;
   private final BluetoothDevice mmDevice;

   public ConnectThread(BluetoothDevice device) {
       // Use a temporary object that is later assigned to mmSocket
       // because mmSocket is final.
       BluetoothSocket tmp = null;
       mmDevice = device;

       try {
           // Get a BluetoothSocket to connect with the given BluetoothDevice.
           // MY_UUID is the app's UUID string, also used in the server code.
           tmp = device.createRfcommSocketToServiceRecord(MY_UUID);
       } catch (IOException e) {
           Log.e(TAG, "Socket's create() method failed", e);
       }
       mmSocket = tmp;
   }

   public void run() {
       // Cancel discovery because it otherwise slows down the connection.
       bluetoothAdapter.cancelDiscovery();

       try {
           // Connect to the remote device through the socket. This call blocks
           // until it succeeds or throws an exception.
           mmSocket.connect();
       } catch (IOException connectException) {
           // Unable to connect; close the socket and return.
           try {
               mmSocket.close();
           } catch (IOException closeException) {
               Log.e(TAG, "Could not close the client socket", closeException);
           }
           return;
       }

       // The connection attempt succeeded. Perform work associated with
       // the connection in a separate thread.
       manageMyConnectedSocket(mmSocket);
   }

   // Closes the client socket and causes the thread to finish.
   public void cancel() {
       try {
           mmSocket.close();
       } catch (IOException e) {
           Log.e(TAG, "Could not close the client socket", e);
       }
   }
}

请注意,在此代码段中,在连接尝试发生之前调用了 cancelDiscovery()。您应该始终在 connect() 之前调用 cancelDiscovery(),尤其是因为 cancelDiscovery() 无论设备发现当前是否正在进行,都会成功。如果您的应用需要确定设备发现是否正在进行,您可以使用 isDiscovering() 进行检查。

特定于应用的 manageMyConnectedSocket() 方法旨在启动用于传输数据的线程,这将在有关 传输蓝牙数据 的部分中讨论。

完成使用 BluetoothSocket 后,始终调用 close()。这样做会立即关闭连接套接字并释放所有相关的内部资源。