Android Bluetooth(二)— 连接和传输数据

3,512 阅读2分钟

前一篇文章已经介绍了如何扫描和配对附近的蓝牙设备,本篇文章介绍下如何使用Bluetooth API为两台蓝牙设备建立连接并传输数据。

官方文档

建立连接

蓝牙设备间通过Socket通信,两台设备建立连接,需要其中一台设备作为服务端,另一台设备作为客户端,当服务端和客户端在同一 RFCOMM 通道上分别拥有已连接的 BluetoothSocket 时,即可将二者视为彼此连接。

服务端

通过BluetoothAdapterlistenUsingRfcommWithServiceRecord获取BluetoothServerSocket,调用BluetoothServerSocketaccept方法监听连接请求,连接后调用BluetoothServerSocket的close方法。

class BluetoothTransferController(private val bluetoothAdapter: BluetoothAdapter {
    
    private val bluetoothUUID = UUID.fromString("fc5deb71-9d4b-460b-b725-b06ea79bda5a")
    private var acceptThread: AcceptThread? = null

    fun startAtAcceptClient() {
        if (acceptThread != null) {
            acceptThread?.cancel()
            acceptThread = null
        }
        acceptThread = AcceptThread()
        acceptThread?.start()
    }

    inner class AcceptThread() : Thread() {
        private val bluetoothServerSocket: BluetoothServerSocket? by lazy(LazyThreadSafetyMode.NONE) {
            bluetoothAdapter.listenUsingRfcommWithServiceRecord("ExampleDemo", bluetoothUUID)
        }

        override fun run() {
            super.run()
            var waitConnect = true
            while (waitConnect) {
                try {
                    // accept为阻塞方法,不能在主线程中调用
                    val bluetoothSocket = bluetoothServerSocket?.accept()
                    if (bluetoothSocket != null) {
                        // 后续可以通过bluetoothSocket传输数据
                        connected(this)
                        bluetoothServerSocket?.close()
                        waitConnect = false
                    }
                } catch (e: IOException) {
                    waitConnect = false
                }
            }
        }

        fun cancel() {
            try {
                bluetoothServerSocket?.close()
            } catch (e: IOException) {
                e.printStackTrace()
            }
        }
    }
}

客户端

通过BluetoothDevicecreateRfcommSocketToServiceRecord方法创建BluetoothSocket,调用BluetoothSocketconnect方法发起连接。

class BluetoothTransferController() {
    
    // 与服务端使用相同的UUID
    private val bluetoothUUID = UUID.fromString("fc5deb71-9d4b-460b-b725-b06ea79bda5a")
    private var connectThread: ConnectThread? = null

    fun connectToOtherBluetoothDevice(bluetoothDevice: BluetoothDevice) {
        if (connectThread != null) {
            connectThread?.cancel()
            connectThread = null
        }
        connectThread = ConnectThread(bluetoothDevice)
        connectThread?.start()
    }

    inner class ConnectThread(private val bluetoothDevice: BluetoothDevice) : Thread() {

        private val bluetoothSocket: BluetoothSocket? by lazy(LazyThreadSafetyMode.NONE) {
            bluetoothDevice.createRfcommSocketToServiceRecord(bluetoothUUID)
        }

        override fun run() {
            super.run()
            bluetoothSocket?.run {
                try {
                    // connect为阻塞方法,不能在主线程中调用
                    connect()
                    // 后续可以通过bluetoothSocket传输数据
                    connected(this)
                } catch (e: IOException) {
                    try {
                        close()
                    } catch (e: IOException) {
                        e.printStackTrace()
                    }
                }
            }
        }

        fun cancel() {
            try {
                bluetoothSocket?.close()
            } catch (e: IOException) {
                e.printStackTrace()
            }
        }
    }
}

传输数据

建立连接后,通过 BluetoothSocket 获取InpuStreamOutputStream来传输数据。

class BluetoothTransferController(private val bluetoothAdapter: BluetoothAdapter, private val connectedCallback: () -> Unit, private val receiveCallback: (byteArray: ByteArray) -> Unit) {

    private var connectedThread: ConnectedThread? = null

    fun writeData(sendData: ByteArray) {
        connectedThread?.writeData(sendData)
    }

    private fun connected(bluetoothSocket: BluetoothSocket) {
        if (connectedThread != null) {
            connectedThread?.cancel()
            connectedThread = null
        }
        connectedThread = ConnectedThread(bluetoothSocket)
        connectedThread?.start()
    }

    inner class ConnectedThread(private val bluetoothSocket: BluetoothSocket) : Thread() {

        private var inputStream: InputStream? = null
        private var outputStream: OutputStream? = null
        private var connected = false

        init {
            connected = bluetoothSocket.isConnected
            try {
                if (bluetoothSocket.isConnected) {
                    inputStream = bluetoothSocket.inputStream
                    outputStream = bluetoothSocket.outputStream
                }
            } catch (e: IOException) {
                e.printStackTrace()
            }
        }

        override fun run() {
            super.run()
            connectedCallback.invoke()
            val buffer = ByteArray(1024)
            while (connected) {
                inputStream?.let {
                    try {
                        var length: Int
                        while (it.read(buffer).also { length = it } != -1) {
                            val dataByteArray = buffer.copyOf(length)
                            receiveCallback.invoke(dataByteArray)
                        }
                    } catch (e: IOException) {
                        e.printStackTrace()
                    }
                }
            }
        }

        fun writeData(sendData: ByteArray) {
            try {
                outputStream?.write(sendData)
                outputStream?.flush()
            } catch (e: IOException) {
                e.printStackTrace()
            }
        }

        fun cancel() {
            try {
                inputStream?.close()
                outputStream?.close()
                bluetoothSocket.close()
            } catch (e: IOException) {
                e.printStackTrace()
            }
        }
    }
}

需要注意的是,发送的数据可能会被拆分为多个字节数组,所以在接收端需要确保接受所有字节数组并正确排序。

示例

发送字符串效果如图:

服务端客户端
fuwuduna.gifkehuduan.gif

演示代码已在示例Demo中添加。

ExampleDemo github

ExampleDemo gitee