4、经典蓝牙Socket通信:BluetoothSocket与BluetoothServerSocket、建立RFCOMM连接、数据流读写(InputStream/OutputStream)、多线程处理
好,咱们进入经典蓝牙最核心的部分——Socket通信。
说实话,很多初学者一听到Socket就头大,觉得是网络编程里的东西。其实蓝牙Socket和TCP Socket非常像。你想想看,RFCOMM协议本身就是模拟串口通信,底层走的也是类似TCP的可靠传输。所以,如果你写过网络Socket,那蓝牙Socket上手会很快。
4.1 核心类:BluetoothSocket 与 BluetoothServerSocket
Android SDK里封装了两个关键类:
- BluetoothSocket:客户端用的。发起连接,读写数据。
- BluetoothServerSocket:服务端用的。监听连接请求,接受客户端。
它们的关系,说白了就是“服务员”和“顾客”。服务端开个门等着,客户端敲门进来,然后双方开始聊天。
重要区别:
BluetoothSocket 是连接建立后的通信句柄。BluetoothServerSocket 只负责监听和接受连接,一旦 accept() 返回了新的 BluetoothSocket,它的任务就完成了。
我在项目中遇到过有人把 ServerSocket 当成通信管道来用,结果数据发不出去还找不到原因。嗯,这里要注意:ServerSocket 不是用来收发数据的。
4.2 建立RFCOMM连接
RFCOMM是蓝牙协议栈里最常用的串口模拟协议。Android里建立RFCOMM连接分两步:
4.2.1 服务端:创建并监听
// 服务端代码
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
BluetoothServerSocket serverSocket =
adapter.listenUsingRfcommWithServiceRecord("MyService", MY_UUID);
// 开始监听(阻塞)
BluetoothSocket socket = serverSocket.accept();
// 连接建立后,关闭ServerSocket
serverSocket.close();
这里有个坑:listenUsingRfcommWithServiceRecord 的第一个参数是服务名称,随便起。第二个参数是UUID,必须和客户端一致。我曾经因为UUID写错了一个字母,调试了整整一下午……
4.2.2 客户端:发起连接
// 客户端代码
BluetoothDevice device = ...; // 从扫描结果获取
BluetoothSocket socket =
device.createRfcommSocketToServiceRecord(MY_UUID);
// 发起连接(阻塞)
socket.connect();
注意:connect() 是阻塞方法。我建议在子线程里调用,否则主线程卡住,系统会弹ANR对话框。
避坑指南:
我曾经在 connect() 之前忘了调用 adapter.cancelDiscovery(),结果连接速度慢得离谱。蓝牙规范里说,扫描设备和连接设备不能同时进行,否则会降低性能。所以,连接前一定要取消扫描。
4.3 数据流读写:InputStream / OutputStream
连接建立后,通信就靠流了。BluetoothSocket 提供了两个方法:
getInputStream():读数据getOutputStream():写数据
用法和普通Java IO一模一样。我习惯这样封装:
// 发送数据
OutputStream os = socket.getOutputStream();
String msg = "Hello, Bluetooth!";
os.write(msg.getBytes("UTF-8"));
os.flush();
// 接收数据
InputStream is = socket.getInputStream();
byte[] buffer = new byte[1024];
int bytesRead = is.read(buffer);
String received = new String(buffer, 0, bytesRead, "UTF-8");
这里有个细节:read() 也是阻塞的。如果没有数据,它会一直等。所以,读数据必须在独立线程里做。
个人经验:
我建议用 DataInputStream 和 DataOutputStream 包装一下,这样读写整型、浮点型数据更方便。比如:
DataInputStream dis = new DataInputStream(is);
int length = dis.readInt(); // 先读长度
byte[] data = new byte[length];
dis.readFully(data); // 再读内容
这样能避免粘包问题。嗯,蓝牙通信里粘包虽然不如TCP那么频繁,但也不是没有。
4.4 多线程处理
蓝牙通信天生就是异步的。你不能在UI线程里做任何阻塞操作。我一般会设计三个线程:
| 线程 | 职责 | 说明 |
|---|---|---|
| 连接线程 | 建立连接 | 执行 connect() 或 accept() |
| 读取线程 | 持续读数据 | 循环调用 read(),解析数据 |
| 写入线程 | 发送数据 | 通常用队列+Handler实现 |
我常用的架构是这样的:
public class BluetoothChatThread extends Thread {
private BluetoothSocket socket;
private InputStream inputStream;
private OutputStream outputStream;
private Handler handler; // 用于回调到UI
public void run() {
try {
inputStream = socket.getInputStream();
outputStream = socket.getOutputStream();
byte[] buffer = new byte[1024];
int bytes;
while (true) {
bytes = inputStream.read(buffer);
// 发送消息到UI线程
handler.obtainMessage(MSG_READ, bytes, -1, buffer).sendToTarget();
}
} catch (IOException e) {
// 连接断开处理
}
}
public void write(byte[] data) {
try {
outputStream.write(data);
outputStream.flush();
} catch (IOException e) {
// 写入失败处理
}
}
}
注意:
读取线程里的 while(true) 一定要有退出机制。我见过有人忘记处理 socket 关闭时的异常,导致线程永远卡在 read() 里。正确的做法是:捕获 IOException 后,break 退出循环,并清理资源。
4.5 完整通信流程总结
来,咱们捋一遍整个流程:
- 服务端:创建 BluetoothServerSocket,调用 accept() 等待连接
- 客户端:通过 BluetoothDevice 创建 BluetoothSocket,调用 connect()
- 连接建立:双方各自拿到对方的 BluetoothSocket
- 数据通信:通过 getInputStream() 和 getOutputStream() 读写数据
- 多线程:读写操作放在子线程,通过 Handler 更新UI
- 关闭连接:调用 socket.close(),释放资源
说实话,这个流程我写过不下几十次。从早期的Android 2.3到现在的Android 14,核心逻辑基本没变。唯一要注意的是,Android 6.0以后需要动态申请蓝牙权限,Android 12以后对蓝牙扫描权限又做了更严格的限制。但这些属于权限管理范畴,咱们后面章节会细讲。
核心要点:
- BluetoothSocket 是通信管道,BluetoothServerSocket 只是监听器
- UUID 必须一致,否则连接失败
- 所有阻塞操作必须在子线程执行
- 读取线程要有退出机制,防止内存泄漏
- 连接前记得取消设备扫描
好了,这一章的内容就这些。下一章咱们会讲如何封装一个稳定的蓝牙通信库,包括重连机制、心跳包、数据分包等实战技巧。到时候我会分享一些我在智能家居项目里踩过的坑,保证让你少走弯路。