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() 也是阻塞的。如果没有数据,它会一直等。所以,读数据必须在独立线程里做。

个人经验:

我建议用 DataInputStreamDataOutputStream 包装一下,这样读写整型、浮点型数据更方便。比如:

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 完整通信流程总结

来,咱们捋一遍整个流程:

  1. 服务端:创建 BluetoothServerSocket,调用 accept() 等待连接
  2. 客户端:通过 BluetoothDevice 创建 BluetoothSocket,调用 connect()
  3. 连接建立:双方各自拿到对方的 BluetoothSocket
  4. 数据通信:通过 getInputStream() 和 getOutputStream() 读写数据
  5. 多线程:读写操作放在子线程,通过 Handler 更新UI
  6. 关闭连接:调用 socket.close(),释放资源

说实话,这个流程我写过不下几十次。从早期的Android 2.3到现在的Android 14,核心逻辑基本没变。唯一要注意的是,Android 6.0以后需要动态申请蓝牙权限,Android 12以后对蓝牙扫描权限又做了更严格的限制。但这些属于权限管理范畴,咱们后面章节会细讲。

核心要点:

  • BluetoothSocket 是通信管道,BluetoothServerSocket 只是监听器
  • UUID 必须一致,否则连接失败
  • 所有阻塞操作必须在子线程执行
  • 读取线程要有退出机制,防止内存泄漏
  • 连接前记得取消设备扫描

好了,这一章的内容就这些。下一章咱们会讲如何封装一个稳定的蓝牙通信库,包括重连机制、心跳包、数据分包等实战技巧。到时候我会分享一些我在智能家居项目里踩过的坑,保证让你少走弯路。