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