本文介绍蓝牙socket的实现,主要是btif上层的部分,屏蔽了对stack和l2cap的细节。
Overview
网络上的两个程序通过一个双向的通信连接实现数据的交互,这个连接的一端称为一个socket。蓝牙同样可以使用socket来实现客户端和服务器程序。
BluetoothSocket
蓝牙 sockets与TCP sockets类似。在服务端,使用BluetoothServerSocket
创建一个监听的server socket。当accept一个新的连接时,会返回一个新的BluetooothSocket
来管理连接。
在客户端,使用一个单独的BluetoothSocket
来发起连接和管理连接。
1 | Notes:server有两个socket,一个用于监听连接,一个用于管理新建立的连接。而client只有一个单独的socket发起和管理连接。 |
btif_sock
java层的 socket 和 native 层的交互关系是:安卓应用在java层通过系统 API 进行socket 控制和通信,java 层通过 jni 访问蓝牙 native 层的接口。java 和 native 之间通过一组 socketpair 的 fd 来完成通道的控制和数据的通信。
蓝牙有三种类型:rfcomm socket,L2CAP socket,以及sco socket。这三种类型在java 层和 native 层是一一对应的。
RFCOMM
最常用的Bluetoth socket是RFCOMM,RFCOMM是蓝牙面向连接的流式传输,也就是我们常说的Serial Port Profile (SPP)。RFCOMM的底层是L2CAP。
L2CAP
L2CAP是蓝牙的逻辑链路控制和适配协议,它提供协议信道复用、分段与重组、信道流控和差错控制。
l2cap socket是不经过rfcomm的socket,直接借助l2cap完成数据通信。l2cap socket 通常也称为 Connection-Oriented Channel (CoC)。
BluetoothSocket
2.1 作为服务器连接
创建BluetoothServerSocket,用于监听RFComm连接请求:
1 | BluetoothServerSocket mmServerSocket = |
监听连接请求,返回BluetoothSocket
:
1 | BluetoothSocket socket = mmServerSocket.accept(); // 监听连接是阻塞执行的。可以监听多个连接,也可以只监听一个连接 |
管理连接:
通常,会创建一个ConnectedThread来管理新的连接。
1 | // 获取输入输出stream |
2.2 作为客户端连接
如果远程设备在开放服务器套接字上接受连接,则为了发起与此设备的连接,必须首先获取表示该远程设备的BluetoothDevice
对象。然后使用BluetoothDevice
来获取BluetoothSocket
并发起连接。
查询已配对设备:
1 | Set<BluetoothDevice> pairedDevices = bluetoothAdapter.getBondedDevices(); |
发现设备:
调用startDiscovery()
这个异步操作可以发起扫描,约12s。
应用需要针对ACTION_FOUND
Intent 注册一个 BroadcastReceiver,以便接受每台设备发现的设备的相关信息。系统会为每台设备广播此 Intent。
Intent 包含额外字段EXTRA_DEVICE
和EXTRA_CLASS
,两者分别包含BluetoothDevice
和BluetoothClass
。
1 |
|
发起蓝牙连接:
1 | BluetoothSocket device; // 远端设备 |
btif_sock
本节介绍btif socket的工作机制,主要目的是弄清楚java层是怎么和native层进行交互的。
3.1 btif_sock_thread
btif_sock_thread 的实现主要是创建一个线程,监听和管理 fd 事件,并回调分发给对应的 rfcomm socket 或 l2cap socket 处理。它的实现比较琐碎,这里提炼一些要点如下:
btif_sock_init(
初始化时,会创建一个pthread 线程,轮询监听 fd 的事件,完成对 socket 的数据读写和生命周期控制。- 线程的routine为
sock_poll_thread()
。它是一个无限循环,每一次都重新准备 pfds[] 数组,监听要 poll 的 fd。当poll()
被唤醒时,根据 pfds 返回的 revents,处理对应的 cmd 或者 data 事件。 - cmd fd 总是第一个fd,也就是 pfds[0]。在 cmd sock 的处理函数中,会处理 CMD_ADD_FD, CMD_REMOVE_FD, CMD_WAKEUP, CMD_USER_PRIVATE, CMD_EXIT 这些事件。
- data fd 则回调给
btsock_signaled()
函数,由 rfcomm 或 l2cap 的 signaled 函数处理。 - 由于 poll 的 struct pollfd 没法带更多的与 fd 关联的数据,因此 btif_sock_thread的实现中,定义了一个thread_slot_t的静态数组,通过 fd 和 poll struct 关联,绑定了 user_id,flags,types等用户数据。
3.2 btif_sock_rfc
Bluetooth sockets 作为server的启动和连接流程图如下:
Bluetooth sockets 作为client的启动和连接流程图如下:
3.3 fd 的流转
client的流程比较简单,一个连接只有一组fd。但是server是支持多个连接的,所以它有多组fd。那么 java 层和 btif 层以及协议栈更底层的实现,这些连接又是怎么进行关联管理以及数据收发的呢?
server listen
第一步,app 监听连接,native 层通过socketpair创建一组 fd,其中 app_fd 会返回给 java 层。这个 app_fd 会跟 java 层的 BluetoothServerSocket 关联。
server open
第二步,client 来连接 server,server accept 一个连接。
新建立一个连接,btif_sock_rfc 会创建一个 accept slot。新的 accept slot 创建一组新的 socket pair,然后把新的app_fd,通过 sock_connect_signal_t 消息由 srv_slot 的 fd 返回给 java app。
接着,accept_slot 绑定 bta 返回的 open_handle 和 open_handle 对应的 port handle。
而 srv_slot 则绑定 bta 返回的 new_listen_handle 以及对应的 port handle。
最后这两个 slot 交换 id。
看到这里,我们不禁疑惑,为何要这样设置?搞这么复杂的目的是什么?– 答案是为了支持多个 client 连接。
我们先看下,只有一个连接时,上面的 slot 是怎么发数据的:
java 通过 accept_slot 的 app_fd 写数据
native 的 poll thread 监听到读事件
fd 和 id 关联,找到 accept_slot
通过 accept_slot -> rfc_handle 写 rfcomm 数据
可以看到,最终效果是,java的第一个BluetoothSocket的fd 和 accept_slot 关联,最终通过第一个连接时 bta 返回的 open_handle 来读写数据。
第三步,后续连接建立。
后续连接,继续交换 slot id,app 通过 app_fd,找到accept_slot,再找到连接时 bta 返回的open_handle 发数据,handle 和 slot 每建立一个连接,就进行一次偏移。
依赖这种机制,slot id 依次增加,srv_slot 的 fd 总是和 java 的 BluetoothServerSocket 关联,而 accept_slot 的 fd 总是和新连接返回的 BluetoothSocket 关联,发数据的 rfc_handle 以及用于流控的 port_handle,也是 bta 建立连接时返回的正确的 handle。