4. 经典蓝牙Socket通信:BluetoothSocket的建立、RFCOMM通道连接、服务端监听、数据收发
经典蓝牙的Socket通信,说白了就是让两台设备像TCP/IP那样建立连接、收发数据。只不过底层走的是RFCOMM协议——它模拟了串口通信,上层用起来跟读写文件差不多。
我个人习惯把这一章看作蓝牙开发的“基本功”。你想想看,无论是文件传输、聊天工具,还是IoT设备控制,底层都离不开这套Socket机制。今天我就带你把它彻底搞明白。
4.1 RFCOMM协议:蓝牙世界的“串口线”
RFCOMM(Radio Frequency Communication)是蓝牙协议栈中位于L2CAP之上的一个协议。它的作用很简单:模拟RS-232串口通信。
为什么需要它?因为很多传统应用(比如串口调试、打印机)都是基于串口通信的。RFCOMM让这些应用可以无缝迁移到蓝牙上,不需要改上层逻辑。
我在项目中遇到过一件事:有个客户想把老式的串口条码扫描枪改成蓝牙版。我们直接在RFCOMM层做了一层桥接,上层代码几乎没动就搞定了。这就是RFCOMM的价值——兼容性极好。
| 协议层 | 作用 |
|---|---|
| RFCOMM | 模拟串口,提供流式数据传输 |
| L2CAP | 数据分包、重组、多路复用 |
| HCI | 主机与蓝牙芯片的通信接口 |
4.2 服务端监听:listenUsingRfcommWithServiceRecord
服务端要做的事很简单:创建一个监听Socket,然后等着客户端连上来。
Android提供了BluetoothAdapter.listenUsingRfcommWithServiceRecord()方法。这个方法需要两个参数:
- 服务名称:随便起一个,比如"ChatService"
- UUID:唯一标识这个服务的128位UUID
嗯,这里要注意:UUID必须和服务端、客户端约定好。如果两边UUID对不上,连接会直接失败。
核心要点:UUID是RFCOMM连接的“钥匙”。同一个服务,两端必须使用完全相同的UUID。
// 服务端:启动监听
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
BluetoothServerSocket serverSocket =
adapter.listenUsingRfcommWithServiceRecord(
"MyBluetoothService",
UUID.fromString("00001101-0000-1000-8000-00805F9B34FB")
);
// 阻塞等待客户端连接
BluetoothSocket socket = serverSocket.accept();
// 连接建立后,关闭监听(通常只接受一个连接)
serverSocket.close();
我曾经踩过一个坑:accept()是阻塞的,如果在主线程调用,UI会卡死。一定要放到子线程里跑。
警告:BluetoothServerSocket.accept() 是阻塞调用,必须在工作线程中执行,否则会触发NetworkOnMainThreadException。
4.3 客户端连接:主动发起RFCOMM连接
客户端这边更直接——拿到远端设备的BluetoothDevice对象,然后调用createRfcommSocketToServiceRecord()方法。
注意:连接前最好先调用BluetoothAdapter.cancelDiscovery()停止设备发现。因为发现过程会占用蓝牙资源,可能导致连接变慢甚至失败。
// 客户端:发起连接
BluetoothDevice remoteDevice = ...; // 通过扫描或配对获取
BluetoothSocket socket =
remoteDevice.createRfcommSocketToServiceRecord(
UUID.fromString("00001101-0000-1000-8000-00805F9B34FB")
);
// 停止发现(重要!)
BluetoothAdapter.getDefaultAdapter().cancelDiscovery();
// 连接(阻塞)
socket.connect();
我记得有一次调试,客户端一直连不上服务端。查了半天,发现是UUID写错了一个字符。这种问题很难排查,因为蓝牙层不会报“UUID不匹配”这种友好错误,只会返回连接超时。
小技巧:调试时可以在服务端打印自己的UUID,客户端也打印一遍,人工比对。别笑,这招救过我很多次。
4.4 数据收发:InputStream和OutputStream
连接建立后,Socket会提供两个流:
getInputStream():读取远端发来的数据getOutputStream():向远端发送数据
用法跟Java标准Socket一模一样。我习惯用byte数组做缓冲区,每次读写固定长度。
// 发送数据
OutputStream os = socket.getOutputStream();
String message = "Hello Bluetooth!";
os.write(message.getBytes());
os.flush();
// 接收数据
InputStream is = socket.getInputStream();
byte[] buffer = new byte[1024];
int bytesRead = is.read(buffer);
String received = new String(buffer, 0, bytesRead);
这里有个坑:read()也是阻塞的。如果对方没发数据,线程会一直等。我建议在读取线程里设置超时,或者用独立线程管理。
最佳实践:收发数据分别用独立线程处理。发送线程只管写,接收线程只管读。这样即使一端卡住,另一端还能正常工作。
4.5 完整通信流程:一张图看懂
下面这张图总结了经典蓝牙Socket通信的完整流程。我把它画成了时序图,方便你理解每一步的先后关系。
4.6 避坑指南:我踩过的那些雷
做蓝牙Socket开发,有几个坑几乎每个人都会遇到。我把自己踩过的列出来,你遇到了可以直接翻回来查。
- UUID不匹配:服务端和客户端必须用同一个UUID。我曾经因为大小写问题折腾了一下午——UUID是大小写不敏感的,但有些人写的是大写,有些人写的是小写,肉眼很难看出来。
- 主线程阻塞:
accept()和connect()都是阻塞的。我建议用线程池管理,不要手动new Thread。 - 流未关闭:通信结束后,一定要关闭InputStream、OutputStream和Socket。否则下次连接可能失败。
- 发现未停止:客户端连接前一定要调用
cancelDiscovery()。我见过有人漏掉这步,结果连接成功率直接掉到30%。
我的习惯:每次连接前都检查一下蓝牙适配器的状态。如果正在发现,先停掉再连。这个检查花不了1毫秒,但能避免很多奇怪的问题。
好了,经典蓝牙Socket通信的核心内容就这些。从监听、连接到收发数据,每一步都不复杂,但细节决定成败。你只要把UUID、线程、流管理这三件事做好,基本不会出大问题。