1. 为什么需要状态机?
L2CAP 的面向连接信道从生到死要经历一连串阶段:发起连接 → 等待应答 → 双向协商参数 → 传数据 → 拆链。不同阶段能处理哪些信令报文、收到报文后该干什么,如果没有统一的规定,各家芯片厂各写各的,互操作性就是灾难。
于是规范用状态机(State Machine) 来一锤定音:每条 L2CAP 面向连接信道对应一个独立的状态机实例 (收到一个 L2CAP_ConnectReq 就新建一个实例),状态机从 CLOSED 状态出生,描述它一生中每个状态下"来什么事件、做什么动作、去哪个状态"。
2. 六大状态总览
|----------------------|------------------------|--------------|
| 状态 | 一句话说明 | 类比 |
| CLOSED | 信道未连接 | 单身 |
| WAIT_CONNECT | 收到了连接请求,只能先回 "pending" | 被表白了,说"让我想想" |
| WAIT_CONNECT_RSP | 自己发出了连接请求,等对方答复 | 表白后等回复 |
| CONFIG | 双向协商配置选项(含多个子状态) | 谈判、签合同 |
| OPEN | 用户数据传输态 | 婚后正常生活 |
| WAIT_DISCONNECT | 已发出断链请求,等对方确认 | 提出离婚,等手续办完 |
整体流转图:

几个理解要点:
- 进入 CONFIG 的三条路 :从 CLOSED(直接回 success)、从 WAIT_CONNECT(pending 之后决定接受)、从 WAIT_CONNECT_RSP(对方接受了你的请求),三条路统统先落到子状态 WAIT_CONFIG;
- 离开 CONFIG 只有一条正路 :发起者路径(initiator path)和应答者路径(acceptor path)双双完成,才进 OPEN。一边谈妥了另一边还没谈,就得继续等;
- 断链的两条路 :任何状态收到
DisconnectReq都可以当场回DisconnectRsp然后直接 CLOSED(对方主动拆链不需要走 WAIT_DISCONNECT);只有自己主动 发起CloseChannel_Req才进 WAIT_DISCONNECT。
3. 逐状态事件表精讲
规范用"事件-条件-动作-下一状态"表格定义每个状态,下面挑干货讲:
3.1 CLOSED 状态
CLOSED 不是"什么都不理",它是信道的产房:
|----------------------------------------------------|---------------------------------------------------------------------------------------|
| 事件 | 处理 |
| OpenChannel_Req(本机上上层请求建链) | 发 ConnectReq → 进 WAIT_CONNECT_RSP |
| 收到 L2CAP_ConnectReq | 三选一:资源充足→回 success 进 CONFIG;需要时间→回 pending 进 WAIT_CONNECT;没资源/不批准→回 refused 留在 CLOSED |
| 收到 ConfigReq | 回 Command Reject(Invalid CID) |
| 收到 ConnectRsp / ConfigRsp / DisconnectRsp / Data | Ignore(静默丢弃) |
⚠️ 注意一个细节:CLOSED 状态收到 ConnectRsp 要忽略。因为 ConnectRsp 只对 WAIT_CONNECT_RSP 中的请求有意义,CLOSED 状态下收到它大概率是链路上残留的旧报文或对方状态错乱,不回绝、不报错,直接扔。
3.2 WAIT_CONNECT / WAIT_CONNECT_RSP
这两个状态是连接阶段的"等待位",有两条只在规范注释里、却非常实用的规则:
- WAIT_CONNECT 状态下不处理
DisconnectReq和ConfigReq------因为此时 Source CID / Destination CID 还没分配,报文无法关联到具体信道的状态机,自然无从处理;
- WAIT_CONNECT_RSP 状态下不处理
DisconnectReq------同理。
其余信令(ConfigRsp、DisconnectRsp、Data)一律 Ignore。收到 ConnectRsp 时三分支:
- success → 发出第一个
ConfigReq,进 CONFIG(WAIT_CONFIG 子状态);
- pending → 继续留在 WAIT_CONNECT_RSP(同时应启动 ERTX 定时器,见第六节);
- refused → 回 CLOSED,本次建链失败。
3.3 OPEN 状态
OPEN 是唯一能处理 L2CAP_Data 的状态,同时支持重配置(Reconfigure):
|--------------------------|-------------------------------------|-----------------------------|
| 事件 | 动作 | 下一状态 |
| SendData_Req | 按配置的模式(Basic/ERTM/Streaming)发数据 | OPEN |
| ReconfigureChannel_Req | 先把未发完的出向 SDU 传完 ,再发 ConfigReq | CONFIG(WAIT_CONFIG_REQ_RSP) |
| 收到 ConfigReq(参数可接受) | 传完 SDU,回 ConfigRsp(ok) | CONFIG(WAIT_SEND_CONFIG) |
| 收到 ConfigReq(不可接受) | 传完 SDU,回 ConfigRsp(fail) | OPEN(留在原地!) |
| 收到 DisconnectReq | 回 DisconnectRsp | CLOSED |
⭐ 两条容易被忽略的规则:
- "完成出向 SDU"是硬性要求 :进入重配置前,本地已拆包的所有 PDU 必须全部可靠送达并被对端确认,才允许切换到 CONFIG。也就是说重配置期间该方向数据流要被挂起(§7.1 也强调:重配置会话期间信道上所有数据流量应暂停,等配置结果出来再说);
- 收到不可接受的 ConfigReq 回 fail 后是回 OPEN 而不是断链------配合上一篇讲的"负响应可以带修改建议,触发新一轮配置请求",被拒不等于谈崩,只是这一轮没谈拢。
3.4 WAIT_DISCONNECT 状态
最简单的状态:只认 DisconnectReq(回 Rsp 后回 CLOSED)和 DisconnectRsp(回到 CLOSED),其余(ConnectRsp、ConfigRsp、Data)一律 Ignore。
4. CONFIG 状态的 8 个子状态
CONFIG 之所以复杂,是因为双向都要配置:每个设备同时扮演两个角色------
- Initiator(发起者) :自己发
ConfigReq,等对方的ConfigRsp;
- Acceptor(应答者) :收对方的
ConfigReq,回ConfigRsp。
两条路径可以先后进行,也可以完全并行,因此 CONFIG 内部拆成 8 个子状态:
|-------------------------|--------------------------------------------------------------|
| 子状态 | 含义(initiator 路径视角 / acceptor 路径视角) |
| WAIT_CONFIG | 已完成连接(或收到连接成功响应),但既没发配置请求,也没收到可接受的配置请求------一切尚未开始 |
| WAIT_SEND_CONFIG | acceptor 路径已收到可接受的请求并回成功;initiator 路径还没发起请求 |
| WAIT_CONFIG_REQ_RSP | initiator 已发出请求、还没等到成功响应;acceptor 还没收到可接受的请求 |
| WAIT_CONFIG_RSP | acceptor 路径已收尾(回过成功响应);initiator 还在等成功响应 |
| WAIT_CONFIG_REQ | initiator 路径已收尾(拿到成功响应);acceptor 还没收到可接受的请求 |
| WAIT_IND_FINAL_RSP | (仅 Lockstep)扩展流规范已交给本地 Controller,Controller 还没答复、最终配置响应也还没到 |
| WAIT_FINAL_RSP | (仅 Lockstep)Controller 已接受、自己也回了成功,就等对端的最终配置响应 |
| WAIT_CONTROL_IND | (仅 Lockstep)已收到对端成功响应,在等自己 Controller 接受或拒绝 |
主流程跳转图:

WAIT_CONFIG ──(ConfigureChannel_Req: 发ConfigReq)──► WAIT_CONFIG_REQ_RSP
│ │
│ (收到可接受的ConfigReq, 回ConfigRsp(success)) │
▼ │
WAIT_SEND_CONFIG ──(ConfigureChannel_Req: 发ConfigReq)──► WAIT_CONFIG_RSP
│ │
│ (收到可接受的ConfigReq, 回ConfigRsp(success)) │ (收到对端成功ConfigRsp)
▼ ▼
WAIT_CONFIG_REQ ──────(收到可接受的ConfigReq)──────────────────────► OPEN ✅
(回ConfigRsp(success))
记忆口诀 :看子状态名里有没有 _REQ / _RSP 后缀------
- 带
REQ_RSP:请求发出去了、响应没回来(initiator 未完成),同时 acceptor 侧也没就绪;
- 带
_RSP:只剩"等对端的响应"(acceptor 已完成,initiator 未完成);
- 带
_REQ:只剩"等对端的请求"(initiator 已完成,acceptor 未完成);
- 两个都缺:WAIT_CONFIG / WAIT_SEND_CONFIG。
💡 通俗类比:谈婚论嫁得双方家长都点头。你这一家(initiator path)谈妥了、对方家(acceptor path)还没给话,你就在 WAIT_CONFIG_REQ 里干等;两家都点头(双向配置均成功),婚礼(OPEN)才开始。
非成功转换:负响应 ≠ 谈崩
CONFIG 内收到负面事件时,规范的处理相当"温和":
|----------------------------|-------------------------|------------|
| 事件 | 动作 | 状态变化 |
| 收到 ConfigReq 但参数不可接受 | 回 ConfigRsp(fail) | 留在原子状态 |
| 收到 ConfigRsp 且对方拒绝了自己的选项 | 修改参数重新发 ConfigReq | 留在原等待子状态 |
| 收到无关的 ConfigRsp | Ignore | 不变 |
这与上一篇的伏笔完全呼应:收到失败响应时,正确姿势是参照对方在负响应里附带的"能接受的参数"修改后再谈,而不是赌气断链。
与配置过程无关的事件
CONFIG 的任何子状态下:
CloseChannel_Req(内部要求放弃)→ 发DisconnectReq→ WAIT_DISCONNECT;
- 收到
DisconnectReq→ 回DisconnectRsp→ 直接 CLOSED(配置没谈完也可以被拆);
- 收到
DisconnectRsp→ Ignore;
- 收到
L2CAP_Data→ 照常处理 PDU,留在原子状态。
5. Standard 与 Lockstep:两种配置过程
CONFIG 子状态里的 WAIT_IND_FINAL_RSP / WAIT_FINAL_RSP / WAIT_CONTROL_IND 只在 Lockstep 过程出现。
|--------------|-------------------|-----------------------------------------------------------------------------------------------------------|
| 对比项 | Standard 过程 | Lockstep 过程 |
| 使用条件 | 默认过程 | 双方都支持 Extended Flow Spec 时必须使用 |
| ConfigReq 数量 | 可多轮、多次发送 | 每方只能发一次 ConfigReq,禁止多轮谈判 |
| 负响应后重新协商 | 允许(发新的 ConfigReq) | 不允许 |
| 配置响应 result | success / fail | 先回 pending(0x0004)(把双向流规范交给本地 Controller 仲裁),Controller 通过后再回最终 success;Controller 拒绝则回 fail (0x0005) |
| 特殊约束 | --- | 带了 ExtFlowSpec 就禁止再带 QoS 和 Flush Timeout 选项 |
Lockstep 的四步舞(双方对称执行):
① 收到对端带 ExtFlowSpec 的 ConfigReq
② 回 ConfigRsp(pending)(可带对 ExtFlowSpec 的修改)
③ 自己发带 ExtFlowSpec 的 ConfigReq
④ 收到对端的 ConfigRsp(pending)
↓
此时才把 (本地+对端) 两个流规范一起交给 Controller 仲裁
↓
Controller 通过 → 发 ConfigRsp(success) → OPEN
Controller 拒绝 → 发 ConfigRsp(fail) → 回 WAIT_CONFIG
两条 Lockstep 的"死刑条款"(违者直接断链):
- 收到
ConfigRsp(success)却还没先收到过 pending → 断链(流程违规);
- 自己发 Best Effort、对端要 Guaranteed(或反过来)→ 断链(上一篇讲过的"双向服务类型必须一致")。
6. 两个保命定时器:RTX 与 ERTX
状态机的稳定运行离不开两个超时定时器,规范特意把它们从状态表里独立出来
6.1 RTX(Response Timeout eXpired)
- 触发 :每发出一个信令请求(包括
L2CAP_EchoReq),就为该请求启动一个 RTX;
- 超时 :可以重发一次重复的 Request (此时 RTX 重置为至少上一次两倍的新值),也可以直接拆链;
- 初始值 :实现自定义,但最小 1 秒、最大 60 秒 ;从计时开始到拆链的总时长不得超过 60 秒;
- 每条未决请求都要有自己的 RTX(每个 outstanding request 一个);
- 收到重复的 Request 时,要按第一次收到它时的状态上下文处理(不能当作新事件)。
💡 LE 上的补充规则 :LE 传输中,Peripheral 侧的 Host 可以在 RTX 超时后直接断链------参考信令链路的 flush timeout:flush timeout 越长(物理层重传越多、越可靠),L2CAP 层需要的重传就越少;若 flush timeout 是无限的,L2CAP 层干脆不做任何重传。
6.2 ERTX(Extended RTX)
- 触发 :对方对请求回了 "pending" (如
ConnectRsp(0x0001 connect pending)),说明对方还在"内部处理"(比如等上层应用决定接不接受),RTX 的量程可能不够,换更长的 ERTX;
- 超时 :同样可重发 Request 或拆链;若选择重发,ERTX 消失、换一个新的 RTX,计时从头来;
- 初始值 :最小 60 秒、最大 300 秒;总时长不得超过 300 秒;
- 每条收到 pending 的未决请求一个 ERTX,且每条请求同一时刻只能有一个(RTX 或 ERTX)。
💡 通俗类比:RTX 是"外卖 30 分钟没到就催单";ERTX 是"商家点了'出餐中',那就再宽限到 2 小时";真超时了就直接退款关单(直接 CLOSED,不再走礼貌性道别流程)。
7. 写在最后
L2CAP 面向连接信道的完整生命周期是:
ConnectReq/Rsp(确定 CID、PSM)
↓
CONFIG 状态:双向各自跑 initiator + acceptor 两条路径
↓ (选项怎么填?→ 上一篇;流程怎么走?→ 本篇)
OPEN 状态:按协商好的模式(Basic/ERTM/Streaming)收发数据
↓ (可 Reconfigure 回 CONFIG)
DisconnectReq/Rsp → CLOSED
三个最重要的心法:
- 配置是双向独立、且必须双向都完成------单向谈妥不等于信道可用;
- Ignore ≠ Reject------很多状态下信令会被静默丢弃,排错时别把"没回包"当成"没收到";
- 定时器是协议健壮性的地基------RTX/ERTX 的倍增重传与上限设计,保证了对端无响应时资源不被永久占用。