GATT服务器端开发:BluetoothGattServer、BluetoothGattServerCallback、自定义Service

好,咱们今天聊聊 BLE 开发中另一个重要角色——GATT 服务器端。说白了,就是让你的 Android 设备变成一个「被连接」的设备,而不是主动去扫描连接别人。

我刚开始做 BLE 项目时,大部分时间都在写客户端(Central),也就是手机去连手环、连传感器。直到有一次,产品经理说:「咱们能不能让手机当体温计,让平板来读数据?」嗯,这时候就得写 GATT 服务器端了。

你想想看,一个 BLE 设备要想被别人访问,它得先把自己的数据组织好,暴露出去。这个组织方式就是 GATT 协议规定的——Service、Characteristic、Descriptor 这三层结构。今天我们就来手把手搭一个自定义的 GATT 服务器。

GATT 服务器架构概览

先看一张图,帮你快速建立整体认知。GATT 服务器端的核心组件就三个:

  • BluetoothGattServer:服务器本体,负责接收连接、处理请求
  • BluetoothGattServerCallback:回调接口,所有事件都在这里处理
  • 自定义 Service/Characteristic:你暴露给外界的数据结构
GATT 服务器端核心架构 BluetoothGattServer 服务器本体 管理连接与请求 sendResponse() GattServerCallback 连接状态变化 读/写请求 MTU 更新 自定义 Service UUID 定义 Characteristic Descriptor 典型交互流程: 1. 客户端发起连接 → onConnectionStateChange() 回调 2. 客户端发现服务 → 系统自动回复 GATT 数据库 3. 客户端读写特征值 → onCharacteristicReadRequest() / onCharacteristicWriteRequest() 4. 服务器调用 sendResponse() 返回结果

第一步:获取 BluetoothGattServer

要创建 GATT 服务器,得先拿到 BluetoothManager,然后调用 openGattServer()。这个 API 会返回一个 BluetoothGattServer 实例,或者 null(如果设备不支持 BLE 服务器模式)。

val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
val bluetoothAdapter = bluetoothManager.adapter

// 注意:需要先确保蓝牙已开启
if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled) {
    // 处理蓝牙未开启的情况
    return
}

val gattServer = bluetoothManager.openGattServer(
    this,  // Context
    gattServerCallback  // BluetoothGattServerCallback
)
💡 个人经验:openGattServer() 必须在主线程调用。我踩过这个坑——在子线程里调,结果回调死活不触发。另外,一个 BluetoothAdapter 只能打开一个 GATT 服务器实例,重复调用会返回同一个对象。

第二步:实现 BluetoothGattServerCallback

这个回调接口是 GATT 服务器的心脏。所有客户端发起的操作,都会在这里通知你。你需要重写以下几个关键方法:

回调方法 触发时机 你需要做的事
onConnectionStateChange() 客户端连接/断开 记录连接状态,更新 UI
onCharacteristicReadRequest() 客户端读特征值 准备数据,调用 sendResponse()
onCharacteristicWriteRequest() 客户端写特征值 解析数据,执行逻辑,调用 sendResponse()
onDescriptorReadRequest() 客户端读描述符 通常用于 CCCD(客户端特征配置描述符)
onDescriptorWriteRequest() 客户端写描述符 处理通知/指示的启用/禁用
onMtuChanged() MTU 协商完成 记录新的 MTU 值,用于后续大包传输

来看一个完整的回调实现示例:

private val gattServerCallback = object : BluetoothGattServerCallback() {
    
    override fun onConnectionStateChange(
        device: BluetoothDevice, 
        status: Int, 
        newState: Int
    ) {
        when (newState) {
            BluetoothProfile.STATE_CONNECTED -> {
                Log.d(TAG, "设备已连接: ${device.address}")
                // 记录连接设备,用于后续发送通知
                connectedDevice = device
            }
            BluetoothProfile.STATE_DISCONNECTED -> {
                Log.d(TAG, "设备已断开: ${device.address}")
                connectedDevice = null
            }
        }
    }

    override fun onCharacteristicReadRequest(
        device: BluetoothDevice,
        requestId: Int,
        offset: Int,
        characteristic: BluetoothGattCharacteristic
    ) {
        // 根据特征值 UUID 返回不同数据
        val value = when (characteristic.uuid) {
            TEMPERATURE_CHAR_UUID -> {
                // 读取当前温度值
                val temp = readCurrentTemperature()
                floatToBytes(temp)
            }
            BATTERY_LEVEL_CHAR_UUID -> {
                byteArrayOf(currentBatteryLevel.toByte())
            }
            else -> {
                // 未知特征值,返回错误
                gattServer?.sendResponse(device, requestId, 
                    BluetoothGatt.GATT_INVALID_HANDLE, offset, null)
                return
            }
        }
        
        gattServer?.sendResponse(device, requestId, 
            BluetoothGatt.GATT_SUCCESS, offset, value)
    }

    override fun onCharacteristicWriteRequest(
        device: BluetoothDevice,
        requestId: Int,
        characteristic: BluetoothGattCharacteristic,
        preparedWrite: Boolean,
        responseNeeded: Boolean,
        offset: Int,
        value: ByteArray
    ) {
        // 处理写入请求
        when (characteristic.uuid) {
            COMMAND_CHAR_UUID -> {
                handleCommand(value)
            }
            TARGET_TEMP_CHAR_UUID -> {
                val targetTemp = bytesToFloat(value)
                setTargetTemperature(targetTemp)
            }
        }
        
        if (responseNeeded) {
            gattServer?.sendResponse(device, requestId, 
                BluetoothGatt.GATT_SUCCESS, offset, null)
        }
    }
    
    override fun onMtuChanged(device: BluetoothDevice, mtu: Int) {
        Log.d(TAG, "MTU 更新为: $mtu")
        currentMtu = mtu
    }
}
⚠️ 注意:sendResponse() 必须在回调方法内同步调用,不能放到异步线程里。我曾经把数据准备逻辑放到协程里,结果 sendResponse 调用时已经超时了,客户端一直收不到响应。另外,requestId 必须原样传回,不能自己造。

第三步:自定义 Service 和 Characteristic

现在我们来定义自己的 GATT 服务。假设我们要做一个「智能温度计」,暴露两个特征值:当前温度(只读)和目标温度(可读写)。

// 定义 UUID
val SERVICE_UUID = UUID.fromString("00001809-0000-1000-8000-00805F9B34FB")
val TEMPERATURE_CHAR_UUID = UUID.fromString("00002A1C-0000-1000-8000-00805F9B34FB")
val TARGET_TEMP_CHAR_UUID = UUID.fromString("00002A1D-0000-1000-8000-00805F9B34FB")

// 创建 Service
val tempService = BluetoothGattService(
    SERVICE_UUID,
    BluetoothGattService.SERVICE_TYPE_PRIMARY
)

// 创建只读特征值:当前温度
val temperatureChar = BluetoothGattCharacteristic(
    TEMPERATURE_CHAR_UUID,
    BluetoothGattCharacteristic.PROPERTY_READ,
    BluetoothGattCharacteristic.PERMISSION_READ
)

// 创建可读写特征值:目标温度
val targetTempChar = BluetoothGattCharacteristic(
    TARGET_TEMP_CHAR_UUID,
    BluetoothGattCharacteristic.PROPERTY_READ or 
    BluetoothGattCharacteristic.PROPERTY_WRITE,
    BluetoothGattCharacteristic.PERMISSION_READ or 
    BluetoothGattCharacteristic.PERMISSION_WRITE
)

// 添加特征值到 Service
tempService.addCharacteristic(temperatureChar)
tempService.addCharacteristic(targetTempChar)

// 添加 Service 到 GATT 服务器
gattServer?.addService(tempService)

🔑 关键点:

  • PROPERTY 决定客户端能做什么(读、写、通知等)
  • PERMISSION 决定是否需要加密连接才能操作
  • UUID 建议使用标准 SIG UUID 或自定义 UUID(格式:0000xxxx-0000-1000-8000-00805F9B34FB)

第四步:发送通知(Notification)

有时候服务器需要主动通知客户端数据变化,比如温度值更新了。这时候就需要用到通知机制。通知需要客户端先订阅(通过写 CCCD 描述符),然后服务器才能发送。

// 在 Service 中添加 CCCD 描述符
val cccdDescriptor = BluetoothGattDescriptor(
    UUID.fromString("00002902-0000-1000-8000-00805F9B34FB"),
    BluetoothGattDescriptor.PERMISSION_READ or 
    BluetoothGattDescriptor.PERMISSION_WRITE
)
temperatureChar.addDescriptor(cccdDescriptor)

// 在回调中处理 CCCD 写入
override fun onDescriptorWriteRequest(
    device: BluetoothDevice,
    requestId: Int,
    descriptor: BluetoothGattDescriptor,
    preparedWrite: Boolean,
    responseNeeded: Boolean,
    offset: Int,
    value: ByteArray
) {
    if (descriptor.uuid == UUID.fromString("00002902-0000-1000-8000-00805F9B34FB")) {
        // 解析 CCCD 值
        val enableNotification = value[0].toInt() == 0x01
        Log.d(TAG, "通知已${if (enableNotification) "启用" else "禁用"}")
    }
    
    if (responseNeeded) {
        gattServer?.sendResponse(device, requestId, 
            BluetoothGatt.GATT_SUCCESS, offset, null)
    }
}

// 发送通知
fun sendTemperatureNotification(temperature: Float) {
    val device = connectedDevice ?: return
    val value = floatToBytes(temperature)
    
    // 设置特征值数据
    temperatureChar.value = value
    
    // 发送通知
    gattServer?.notifyCharacteristicChanged(
        device,
        temperatureChar,
        false  // false 表示通知,true 表示指示
    )
}
💡 我的建议:通知(Notification)和指示(Indication)的区别在于:通知不需要客户端确认,速度快但不可靠;指示需要客户端确认,可靠但慢。对于温度这种频繁变化的数据,用通知就够了。对于关键指令(比如「开始测量」),用指示更稳妥。

完整流程串联

把上面所有步骤串起来,一个完整的 GATT 服务器启动流程如下:

  1. 获取 BluetoothManager 和 BluetoothAdapter
  2. 调用 openGattServer() 创建服务器实例
  3. 构建自定义 Service 和 Characteristic
  4. 调用 addService() 注册到服务器
  5. 在回调中处理客户端的读写请求
  6. 需要时调用 notifyCharacteristicChanged() 发送通知
  7. 页面销毁时调用 close() 关闭服务器
override fun onDestroy() {
    super.onDestroy()
    gattServer?.close()  // 关闭服务器,释放资源
}
⚠️ 别忘了:GATT 服务器使用完后一定要 close(),否则下次打开会失败。我见过有的同事在 Activity 重建时忘记关闭旧的服务器实例,结果新的 openGattServer() 一直返回 null,排查了半天才发现是资源泄漏。

好了,GATT 服务器端开发的核心内容就这些。说白了就是三步:建服务器、写回调、定义数据。你想想看,其实和写一个 HTTP 服务器很像——监听端口、处理请求、返回数据。只不过 BLE 的协议栈帮你封装了底层的蓝牙通信细节。

在实际项目中,我建议你把 Service 的定义和回调处理封装成一个独立的类,方便复用和测试。另外,多设备连接时要注意管理好每个设备的连接状态和通知订阅状态,这块容易出 bug。


公众号:蓝海资料掘金营,微信deep3321