连接蓝牙设备

如需在两台设备之间创建连接,您必须同时实现服务器端和客户端机制,因为其中一台设备必须开放服务器套接字,而另一台设备必须使用服务器设备的 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() 调用属于阻塞调用,因此请勿在主 activity 界面线程中执行它,在其他线程中执行它可确保您的应用仍然可以响应其他用户互动。通常,您可以在应用管理的新线程中执行涉及 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

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

  2. 通过调用 connect() 发起连接。请注意,此方法属于阻塞调用。

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

由于 connect() 是阻塞调用,因此您应始终在独立于主 activity(界面)线程的线程中执行此连接过程。

示例

以下是发起蓝牙连接的客户端线程的基本示例:

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()。这样做会立即关闭已连接的套接字并释放所有相关的内部资源。