Bluedroid-Study-Notes-Bluetooth Sockets

本文介绍蓝牙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)。

1

BluetoothSocket

2.1 作为服务器连接

创建BluetoothServerSocket,用于监听RFComm连接请求:

1
2
BluetoothServerSocket mmServerSocket = 
bluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID)

监听连接请求,返回BluetoothSocket

1
2
3
BluetoothSocket socket = mmServerSocket.accept();  // 监听连接是阻塞执行的。可以监听多个连接,也可以只监听一个连接

mmServerSocket.close(); // 最后,这个mmServerSocket,需要java层主动close,否则会造成fd泄露。

管理连接

通常,会创建一个ConnectedThread来管理新的连接。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 获取输入输出stream
InputStream mmInStream = mmSocket.getInputStream();
OutputStream mmOutStream = mmSocket.getOutputStream();

// 读取数据
byte[] mmBuffer = new byte[1024];
int numBytes = mmInStream.read(mmBuffer);

// 发送数据
mmOutStream.write(bytes);

// 关闭socket
mmSocket.close();

2.2 作为客户端连接

如果远程设备在开放服务器套接字上接受连接,则为了发起与此设备的连接,必须首先获取表示该远程设备的BluetoothDevice对象。然后使用BluetoothDevice来获取BluetoothSocket并发起连接。

查询已配对设备

1
2
3
4
5
6
7
8
Set<BluetoothDevice> pairedDevices = bluetoothAdapter.getBondedDevices();

if (pairedDevices.size() > 0) {
for (BluetoothDevice device : pairedDevices) {
String deviceName = device.getName();
String deviceHardwareAddress = device.getAddress();
}
}

发现设备

调用startDiscovery() 这个异步操作可以发起扫描,约12s。

应用需要针对ACTION_FOUND Intent 注册一个 BroadcastReceiver,以便接受每台设备发现的设备的相关信息。系统会为每台设备广播此 Intent。

Intent 包含额外字段EXTRA_DEVICEEXTRA_CLASS,两者分别包含BluetoothDeviceBluetoothClass

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
protected void onCreate(Bundle savedInstanceState) {
...

// 注册广播
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
registerReceiver(receiver, filter);
}

// 创建广播接收器
private final BroadcastReceiver receiver = new BroadcastReceiver() {
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (BluetoothDevice.ACTION_FOUND.equals(action)) {
// 获取发现的设备
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
String deviceName = device.getName();
String deviceHardwareAddress = device.getAddress(); // MAC address
}
}
};

发起蓝牙连接

1
2
3
4
5
6
7
8
9
10
11
BluetoothSocket device;  // 远端设备
BluetoothSocket mmSocket = device.createRfcommSocketToServiceRecord(MY_UUID); // 获取BluetoothSocket

// 取消扫描
bluetoothAdapter.cancelDiscovery();

// 发起连接
mmSocket.connect();

// 关闭连接
mmSocket.close();

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的启动和连接流程图如下:

6
Bluetooth sockets 作为client的启动和连接流程图如下:

8

3.3 fd 的流转

client的流程比较简单,一个连接只有一组fd。但是server是支持多个连接的,所以它有多组fd。那么 java 层和 btif 层以及协议栈更底层的实现,这些连接又是怎么进行关联管理以及数据收发的呢?

server listen

第一步,app 监听连接,native 层通过socketpair创建一组 fd,其中 app_fd 会返回给 java 层。这个 app_fd 会跟 java 层的 BluetoothServerSocket 关联。

2
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。

5

看到这里,我们不禁疑惑,为何要这样设置?搞这么复杂的目的是什么?– 答案是为了支持多个 client 连接。

我们先看下,只有一个连接时,上面的 slot 是怎么发数据的:

  1. java 通过 accept_slot 的 app_fd 写数据

  2. native 的 poll thread 监听到读事件

  3. fd 和 id 关联,找到 accept_slot

  4. 通过 accept_slot -> rfc_handle 写 rfcomm 数据

4

可以看到,最终效果是,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。

7