连接蓝牙设备

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

当服务器和客户端在同一个 RFCOMM 通道上各自拥有一个已连接的 BluetoothSocket 时,它们就被认为是相互连接的。此时,每台设备都可以获取输入和输出流,并开始数据传输,这将在关于传输蓝牙数据的部分进行讨论。本节介绍如何在两台设备之间发起连接。

在尝试查找蓝牙设备之前,请确保您具有适当的蓝牙权限设置您的应用以支持蓝牙

连接技术

一种实现技术是自动将每台设备配置为服务器,以便每台设备都有一个打开的服务器套接字并监听连接。在这种情况下,任一设备都可以与另一设备发起连接并成为客户端。或者,一台设备可以显式托管连接并按需打开服务器套接字,而另一台设备则发起连接。


图 1. 蓝牙配对对话框。

以服务器身份连接

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

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

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

    该字符串是您的服务的可识别名称,系统会自动将其写入设备上的新服务发现协议 (SDP) 数据库条目。该名称是任意的,可以简单地是您的应用名称。通用唯一标识符 (UUID) 也包含在 SDP 条目中,并构成与客户端设备的连接协议的基础。也就是说,当客户端尝试连接到此设备时,它会带有一个唯一标识其想要连接的服务对应的 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 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。在此示例中,close() 在获取 BluetoothSocket 后立即被调用。您可能还想在您的线程中提供一个公共方法,以便在您需要停止监听该服务器套接字时关闭私有的 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() 是阻塞调用,您应始终在与主 Activity (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()。这样做会立即关闭已连接的套接字并释放所有相关的内部资源。