安卓蓝牙协议栈中的RFCOMM状态机分析
1.1 数据结构
1.1.1 tRFC_MCB
tRFC_MCB(type of rfcomm multiplexor control block的简写)代表了一个多路复用器。代表了RFCOMM规范中,图2.2中从上往下数的第2层,也就是“RFCOMM”所在的方框。一般地,两个设备间所有RFCOMM上层的端口都基于一个多路复用器,也就是这里的tRFC_MCB(也就是说,两个蓝牙设备间如果建立了RFCOMM连接,那么就有且仅有一个tRFC_MCB数据结构在维护RFCOMM层的状态)。
1.1.2 tPORT
tPORT代表了一个端口,也就是代表了RFCOMM规范中,图2.2最上面一层的内容(以数字标记成2,3…61)的那一层内容。两个蓝牙设备间每建立一个port的连接,那么就会分配一个tPORT来维护该连接的状态。例如安卓手机和蓝牙耳机建立了HF连接(HF基于RFCOMM),那么该安卓手机就会分配一个tPORT来代表该port连接的状态。不过需要注意的是,这里的tPORT并不会维护HF本身数据协议,只是将HF的协议数据当作平凡的RFCOMM数据包来同等对待。
2.1 状态机
2.1.1 RFCOMM多路复用器状态机
表2.1 RFCOMM多路复用器状态表
序号 |
简写 |
描述 |
1 |
RFC_MX_STATE_IDLE |
空闲状态(初始化后,未连接时) |
2 |
RFC_MX_STATE_WAIT_CONN_CNF |
等待连接应答(发出L2CAP连接请求后) |
3 |
RFC_MX_STATE_CONFIGURE |
L2CAP配置状态 |
4 |
RFC_MX_STATE_SABME_WAIT_UA |
等待回复SABM的UA命令 |
5 |
RFC_MX_STATE_WAIT_SABME |
等待SABM命令 |
6 |
RFC_MX_STATE_CONNECTED |
多路复用器已连接 |
7 |
RFC_MX_STATE_DISC_WAIT_UA |
等待回复DISC的UA命令 |
该状态机有七个状态,见表2.1中列出。每个状态下都需要能够处理表2.2中列出的事件;并且根据事件的不同,如果有需要那么就会切换到下一个状态中并且等待新的事件来处理。
2.1.1.1 RFC_MX_STATE_IDLE状态
处理事件RFC_MX_EVENT_START_REQ:1. 初始化RFCOMM所用的L2CAP对应的MTU长度为666字节(L2CAP默认MTU长度672减去RFCOMM所用的header长度5,再减去1的值)。2. 调用L2CAP层提供的L2CAP通道连接API(L2CA_ConnectReq),使用代表RFCOMM的PSM值0x0003发起和对方设备RFCOMM所用的L2CAP通道连接。3. 如果第二步经由L2CAP层返回的连接结果是0(代表失败),那么就接下来清理其他相关状态并且通过回调告知上层,连接失败了;否则存储相关状态并且将状态机切换到RFC_MX_STATE_WAIT_CONN_CNF状态。
该状态下不应该接收到事件RFC_MX_EVENT_START_RSP、RFC_MX_EVENT_CONN_CNF、RFC_MX_EVENT_CONF_IND以及RFC_MX_EVENT_CONF_CNF。
不处理的事件是RFC_EVENT_SABME、RFC_EVENT_DM、RFC_EVENT_TIMEOUT和RFC_EVENT_UA。
处理事件RFC_MX_EVENT_CONN_IND:1. 开启定时器T2,不过此时超时时间设置成120秒(见5.2.1节的解释内容)。2. 做出L2CAP连接请求的回复,并且接受该L2CAP通道连接。3. 向对方发出配置该L2CAP通道的配置请求,期望将该L2CAP的MTU设置成1691字节。4. 切换到状态RFC_MX_STATE_CONFIGURE中。
处理事件RFC_EVENT_DISC:发出DM帧作为回应,状态不作更改。
处理事件RFC_EVENT_UIH:发出DM帧作为回应,状态不作更改。
表2.2 RFCOMM多路复用器事件
序号 |
简写 |
描述 |
1 |
RFC_MX_EVENT_START_REQ |
要求建立RFCOMM底层的L2CAP通道连接 |
2 |
RFC_MX_EVENT_START_RSP |
|
3 |
RFC_MX_EVENT_CLOSE_REQ |
|
4 |
RFC_MX_EVENT_CONN_CNF |
|
5 |
RFC_MX_EVENT_CONN_IND |
|
6 |
RFC_MX_EVENT_CONF_CNF |
|
7 |
RFC_MX_EVENT_CONF_IND |
对端设备要求配置L2CAP |
8 |
RFC_MX_EVENT_QOS_VIOLATION_IND |
|
9 |
RFC_MX_EVENT_DISC_IND |
对端设备要求断开RFCOMM的L2CAP通道 |
10 |
RFC_MX_EVENT_TEST_CMD |
|
11 |
RFC_MX_EVENT_TEST_RSP |
|
12 |
RFC_MX_EVENT_FCON_CMD |
|
13 |
RFC_MX_EVENT_FCOFF_CMD |
|
14 |
RFC_MX_EVENT_NSC |
|
15 |
RFC_MX_EVENT_NSC_RSP |
|
16 |
RFC_EVENT_TIMEOUT |
|
17 |
RFC_EVENT_SABME |
|
18 |
RFC_EVENT_UA |
收到了UA帧 |
2.1.1.1 RFC_MX_STATE_WAIT_CONN_CNF状态
该状态中不应该收到事件RFC_MX_EVENT_START_REQ。
处理事件RFC_MX_EVENT_CONN_CNF:1. 如果收到的L2CAP连接回复的结果码不是成功,那么清理之前分配的tRFC_MCB数据体,回复使用RFCOMM的上层,告知连线失败,并且将状态切换至RFC_MX_STATE_IDLE。并且不处理本节描述的其他过程。2. 如果收到的结果码是成功,那么将状态切换至RFC_MX_STATE_CONFIGURE。并且发出该RFCOMM的L2CAP通道的配置请求给对方,期望将该L2CAP的MTU设置成1691字节。
处理事件RFC_MX_EVENT_CONF_IND:正常情况下,应该先收到对方对本地设备发出去的RFCOMM的L2CAP连接请求而作出的回复。不过有可能因为时序的问题,没有收到连接应答之前就收到了对方要求配置该RFCOMM的L2CAP通道的请求。不过为了兼容起见,这里假定连接已经“完成”,回复接受该配置请求的数据包即可,不作其他处理。不过状态仍维持在RFC_MX_STATE_WAIT_CONN_CNF。
处理事件RFC_MX_EVENT_DISC_IND:看起来对方不愿意建立RFCOMM的L2CAP连接。因此需要将状态切换成原来的RFC_MX_STATE_IDLE;并且通知上层profile,RFCOMM连接失败、相关的port也连接失败了。
处理事件RFC_EVENT_TIMEOUT:连接请求发生了超时、或者是对方不愿意接受该连接而一直保持沉默状态不回复。将状态切换成RFC_MX_STATE_IDLE。送出该L2CAP的断线请求。对方不愿意接受本地设备发出去的连接请求的原因之一可能是发生了5.2.1节中介绍的连线冲突(依据是否有pending_lcid可以判断出):此时对方坚持使用他所发起的连接。这样的话,连线冲突就算解决了,那么再让状态机执行一次再状态RFC_MX_STATE_IDLE下的事件RFC_MX_EVENT_CONN_IND。如果没有连线冲突,单纯是对方一直不回复本地的连接请求,那么需要通知上层RFCOMM以及相关联的port连接失败。
本状态中不处理其他的事件。
2.1.1.2 RFC_MX_STATE_CONFIGURE状态
该状态下不应接收到事件RFC_MX_EVENT_START_REQ和RFC_MX_EVENT_CONN_CNF。
处理事件RFC_MX_EVENT_CONF_IND:1. 如果对方提供了它自己的L2CAP MTU,那么保存对端的MTU值;2. 回复对端发过来的配置请求;3. 如果本地发出去的配置请求对方已经接受,而这里也表示本地也已经接受了对方的配置请求,那么如果本地是RFCOMM L2CAP连接的发起者,那么接下来就发出SABM命令给对方以尝试正式启用该RFCOMM L2CAP连接(见5.2.1节);随后开启定时器T1(20秒),定时器超期后送出事件RFC_EVENT_TIMEOUT。4. 如果本地设备是RFCOMM的L2CAP连接的接受者,切换到状态RFC_MX_STATE_WAIT_SABME,并设定定时器120秒(考虑到可能需要的配对事件),并且定时器超时后送出事件RFC_EVENT_TIMEOUT。
处理事件RFC_MX_EVENT_CONF_CNF:1. 如果对方没有同意配置,那么本地是L2CAP发起者的话,还需要通知上层连接失败;随后断开RFCOMM L2CAP连接(看起来有点武断?)。2. 对方同意配置:如果本地设备是RFCOMM L2CAP的发起者,那么接下来按照5.2.1节的内容,发出SABM以开启该RFCOMM多路复用器,并切换到状态RFC_MX_STATE_SABME_WAIT_UA;如果本地设备是RFCOMM L2CAP的接收者,那么设置定时器超时时间120秒(考虑到可能触发配对的等待或者用户的确认),切换到状态RFC_MX_STATE_WAIT_SABME等待对方发过来SABM。
处理事件RFC_MX_EVENT_DISC_IND:将状态切换成原来的RFC_MX_STATE_IDLE;并且通知上层profile,RFCOMM连接失败、相关的port也连接失败了。
处理事件RFC_EVENT_TIMEOUT:实施处理事件RFC_MX_EVENT_DISC_IND类似的处理。
不处理其他事件。
2.1.1.3 RFC_MX_STATE_WAIT_SABME状态
处理事件RFC_EVENT_SABME:停止定时器,并且回复UA帧给对方;切换到状态RFC_MX_STATE_CONNECTED。设置新的定时器2秒,如果2秒内没有收到对方发过来的PN帧,那么将会断开RFCOMM所用的L2CAP通道。
处理事件RFC_MX_EVENT_DISC_IND:将状态切换成原来的RFC_MX_STATE_IDLE;并且通知上层profile,RFCOMM连接失败、相关的port也连接失败了。
处理事件RFC_MX_EVENT_START_RSP:
处理事件RFC_EVENT_TIMEOUT:将状态切换至原来的状态RFC_MX_STATE_IDLE,发起断开RFCOMM的L2CAP连接,以及通知上层相关的连接失败。
处理事件RFC_MX_EVENT_CONF_IND以及RFC_MX_EVENT_CONF_CNF:由于安卓协议栈目前不支持重新配置RFCOMM的L2CAP通道,因此按照处理事件RFC_EVENT_TIMEOUT一样的处理过程即可。
在该状态下不处理其他事件。
2.1.1.4 RFC_MX_STATE_SABME_WAIT_UA状态
该状态下不应该收到事件RFC_MX_EVENT_START_REQ和RFC_MX_EVENT_CONN_CNF。
处理事件RFC_MX_EVENT_DISC_IND:切换至状态RFC_MX_STATE_IDLE;并且通知上层连线断开了。
处理事件RFC_EVENT_UA:停止定时器,切换到状态RFC_MX_STATE_CONNECTED;在多路控制器通道(通道号是零)上收到UA帧的设备必定是RFCOMM L2CAP连接的发起者(因为是它发出的SABM帧并且等待UA回复)。那么该设备尝试建立RFCOMM L2CAP连接的目的必定是为了建立某个RFCOMM通道的连接并且将来为某个应用层的profile所使用(例如用作HF profile)。那么自然接下来的动作就是通过发出UIH帧(命令类型是PN)来发起配置该HF的通道了。
处理事件RFC_EVENT_DM:停止定时器。并且执行对事件RFC_EVENT_TIMEOUT所处理的内容一样的处理。
处理事件RFC_EVENT_TIMEOUT:等待对方回复UA发生了超时。将状态切换至原来的状态RFC_MX_STATE_IDLE,发起断开RFCOMM的L2CAP连接,以及通知上层相关的连接失败。
处理事件RFC_MX_EVENT_CONF_IND以及RFC_MX_EVENT_CONF_CNF:这里重新收到了配置L2CAP相关的事件。由于安卓协议栈目前不支持重新配置RFCOMM的L2CAP通道,因此按照处理事件RFC_EVENT_TIMEOUT一样的处理过程即可。
在该状态下不处理其他事件。
2.1.1.5 RFC_MX_STATE_CONNECTED状态
处理事件RFC_EVENT_TIMEOUT以及RFC_MX_EVENT_CLOSE_REQ:1. 发出断开DLCI为0的控制数据包给对方。2. 开启一个3秒的定时器,检查断线是否完成。3. 装状态切换至RFC_MX_STATE_DISC_WAIT_UA。
处理事件RFC_MX_EVENT_DISC_IND:1. 通知上层连接已经断开。2. 将状态切换至RFC_MX_STATE_IDLE。
处理事件RFC_EVENT_DISC:该事件的出现表明对方期望断开RFCOMM的L2CAP连接。因此:1. 回复UA帧。2. 如果本地设备是RFCOMM的L2CAP的发起者,那么还要发起断开该L2CAP的断线请求。3. 通知上层相关连线已经断开。
在该状态下不处理其他事件。
2.1.1.6 RFC_MX_STATE_DISC_WAIT_UA状态
处理事件RFC_EVENT_UA、RFC_EVENT_DM和RFC_EVENT_TIMEOUT:1. 发出断开该RFCOMM所用的L2CAP通道的断线请求。2. 释放RFCOMM多路控制器的相关资源。3. 如果通过restart_required设置了重启L2CAP通道,那么还需要再次发起RFCOMM的L2CAP连线请求。
处理事件RFC_EVENT_DISC:发出通道0的UA帧。
处理事件RFC_EVENT_UIH:忽略收到的数据,并且发出通道0的DM帧。
处理事件RFC_MX_EVENT_START_REQ:等待断线应答UA期间却收到了要求连接的请求。那么将该请求通过标签restart_required设置为true,等待合适的机会再次发起连接。
处理事件RFC_MX_EVENT_DISC_IND:切换到状态RFC_MX_STATE_IDLE,并且通知上层连接断开了。
本状态不处理其他事件。
2.1.1.7 处理RFCOMM的L2CAP连接请求
RFCOMM层通过L2CAP的回调收到了来自对端设备的RFCOMM的L2CAP连接请求之后(也就是通过L2CAP层的回调RFCOMM_ConnectInd获知对方设备想要与本地设备建立连接),随即分配一个tRFC_MCB来维护该RFCOMM L2CAP连接的状态。如果无法分配tRFC_MCB来处理该连接,那么将会发出以资源不足(L2CAP_CONN_NO_RESOURCES:值为4)为由的拒绝连接回复,并且不再执行下列的处理过程。
如果分配来的tRFC_MCB的状态不是RFC_MX_STATE_IDLE(表示因为某种原因已经为该2设备间分配过一次了,也就是说两个设备间至多只能有一个tRFC_MCB来维护RFCOMM底层L2CAP连接)。那么此时要按照5.2.1节中描述的连线冲突来处理(发出L2CAP连接请求之后还没有得到回复之前收到了对方发过来的L2CAP连接请求),设置定时器超时时间是2~12秒之间的随机值,定时器超期后向tRFC_MCB分发RFC_EVENT_TIMEOUT事件;使用数据结构tRFC_MCB中的成员pending_lcid来保存发生该冲突的L2CAP的LCID。
随后向所分配的tRFC_MCB分发事件RFC_MX_EVENT_CONN_IND(此时该tRFC_MCB的状态是RFC_MX_STATE_IDLE)。
2.1.1.8 处理RFCOMM的L2CAP连接回复
本地设备发出RFCOMM的L2CAP连接请求之后,对方就会对该连接请求做出回复(也就是通过L2CAP的回调RFCOMM_ConnectCnf获知远端设备对本地设备发出去的连接请求作出了回复)。本小节描述本地设备如何处理该连接回复。
查找本地设备中事先分配的tRFC_MCB数据体。如果没有查找到,那么表示发生了什么错误,随机忽略来自L2CAP的该消息并且不处理下列的操作过程。
如果在所查找到的tRFC_MCB数据体中找到了pending_lcid的值(非零,见9.2.1.5节),那么代表发生了连接冲突。1. 如果连接回复中可以看出是对方设备拒绝本地设备发出的连接请求,那么本地设备就放弃之前的RFCOMM的L2CAP连接请求,并且清理相关之前分配的tRFC_MCB数据体,将该分配的tRFC_MCB的发起者状态标识is_initiator设置成false,代表该RFCOMM的L2CAP连接是对方发起的。随后切换至状态RFC_MX_STATE_IDLE;向该tRFC_MCB分发事件RFC_MX_EVENT_CONN_IND让其处理。并且随后不作本节后续描述的执行过程。2. 如果连接回复中可以看出对方接受了本地设备发出的连接请求,那么代表对方设备放弃了自己发向本地设备的L2CAP连接请求,那么本地设备也就不用“客气”了,回复拒绝对方的L2CAP连接即可(以资源不足的理由L2CAP_CONN_NO_RESOURCES来回复)。冲突的处理的其他信息,可以参考节。随后清理pending_lcid的值。
最后,向tRFC_MCB的状态机发送事件RFC_MX_EVENT_CONN_CNF让其处理。
2.1.1.9 处理RFCOMM的L2CAP配置请求
RFCOMM层通过L2CAP的回调收到了来自对端设备的RFCOMM的L2CAP配置请求之后(也就是RFCOMM_ConfigInd),在本地查找tRFC_MCB数据体,查找到之后向它的状态机送入事件RFC_MX_EVENT_CONF_IND让其处理。
2.1.1.10 处理收到的RFCOMM的L2CAP数据
RFCOMM层通过L2CAP的回调收到了来自对端设备的RFCOMM的L2CAP数据包之后(也就是通过RFCOMM_BufDataInd),第一步查找所关联的tRFC_MCB数据体来负责处理,如果没有查找到,那么忽略该数据包后续不作任何处理(按理应该断开所关联的L2CAP通道)。第二步解析该数据包,并且提取出表10.2中的事件。如果发生了数据包校验错误,那么丢弃该包数据并且不作后续处理。第三步判断是不是发送至多路复用器控制通道的数据包(也就是DLCI为零)。如果是DLCI为零,那么表示是发送给多路复用器的通道的。