要在两台设备之间建立连接,您必须同时实现服务器端和客户端机制,因为一台设备必须打开服务器套接字,而另一台设备必须使用服务器设备的 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()
。这样做会立即关闭已连接的套接字并释放所有相关的内部资源。