要在两台设备之间建立连接,您必须同时实现服务器端和客户端机制,因为一台设备必须打开服务器套接字,而另一台设备必须使用服务器设备的 MAC 地址发起连接。服务器设备和客户端设备通过不同方式获取所需的 BluetoothSocket。服务器在接受传入连接时接收套接字信息。客户端在向服务器打开 RFCOMM 通道时提供套接字信息。
当服务器和客户端在同一个 RFCOMM 通道上各自拥有一个已连接的 BluetoothSocket 时,它们就被认为是相互连接的。此时,每台设备都可以获取输入和输出流,并开始数据传输,这将在关于传输蓝牙数据的部分进行讨论。本节介绍如何在两台设备之间发起连接。
在尝试查找蓝牙设备之前,请确保您具有适当的蓝牙权限并设置您的应用以支持蓝牙。
连接技术
一种实现技术是自动将每台设备配置为服务器,以便每台设备都有一个打开的服务器套接字并监听连接。在这种情况下,任一设备都可以与另一设备发起连接并成为客户端。或者,一台设备可以显式托管连接并按需打开服务器套接字,而另一台设备则发起连接。
图 1. 蓝牙配对对话框。
以服务器身份连接
当您想连接两台设备时,其中一台必须通过持有打开的 BluetoothServerSocket 来充当服务器。服务器套接字的作用是监听传入的连接请求,并在接受请求后提供一个已连接的 BluetoothSocket。从 BluetoothServerSocket 获取 BluetoothSocket 后,BluetoothServerSocket 可以(也应该)被丢弃,除非您希望设备接受更多连接。
要设置服务器套接字并接受连接,请完成以下步骤序列:
通过调用
listenUsingRfcommWithServiceRecord(String, UUID)获取一个BluetoothServerSocket。该字符串是您的服务的可识别名称,系统会自动将其写入设备上的新服务发现协议 (SDP) 数据库条目。该名称是任意的,可以简单地是您的应用名称。通用唯一标识符 (UUID) 也包含在 SDP 条目中,并构成与客户端设备的连接协议的基础。也就是说,当客户端尝试连接到此设备时,它会带有一个唯一标识其想要连接的服务对应的 UUID。这些 UUID 必须匹配才能接受连接。
UUID 是一种标准化的 128 位字符串 ID 格式,用于唯一标识信息。UUID 用于标识在系统或网络中需要保持唯一性的信息,因为 UUID 重复的概率实际上为零。它独立生成,不使用集中授权机构。在这种情况下,它用于唯一标识您的应用的蓝牙服务。要获取用于您的应用的 UUID,您可以使用网络上的许多随机
UUID生成器之一,然后使用fromString(String)初始化一个 UUID。通过调用
accept()开始监听连接请求。这是一个阻塞调用。它会在连接被接受或发生异常时返回。只有当远程设备发送的连接请求包含与此监听服务器套接字注册的 UUID 匹配的 UUID 时,连接才会被接受。成功后,
accept()返回一个已连接的BluetoothSocket。除非您想接受更多连接,否则请调用
close()。此方法调用会释放服务器套接字及其所有资源,但不会关闭由
accept()返回的已连接BluetoothSocket。与 TCP/IP 不同,RFCOMM 在同一时间只允许每个通道有一个已连接的客户端,因此在大多数情况下,在接受已连接套接字后立即对BluetoothServerSocket调用close()是合理的。
由于 accept() 调用是阻塞调用,请不要在主 Activity UI 线程中执行它。在另一个线程中执行可以确保您的应用仍能响应其他用户交互。通常,将所有涉及 BluetoothServerSocket 或 BluetoothSocket 的工作放在您的应用管理的新线程中是合理的。要中止诸如 accept() 之类的阻塞调用,请从另一个线程对 BluetoothServerSocket 或 BluetoothSocket 调用 close()。请注意,BluetoothServerSocket 或 BluetoothSocket 上的所有方法都是线程安全的。
示例
以下是接受传入连接的服务器组件的简化线程:
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 并发起连接。
基本过程如下:
使用
BluetoothDevice,通过调用createRfcommSocketToServiceRecord(UUID)获取一个BluetoothSocket。此方法初始化一个
BluetoothSocket对象,允许客户端连接到BluetoothDevice。此处传入的 UUID 必须与服务器设备在调用listenUsingRfcommWithServiceRecord(String, UUID)打开其BluetoothServerSocket时使用的 UUID 匹配。要使用匹配的 UUID,请将 UUID 字符串硬编码到您的应用中,然后在服务器端和客户端代码中引用它。通过调用
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()。这样做会立即关闭已连接的套接字并释放所有相关的内部资源。