P2P(七)高级特性与运维
前面 6 篇走完了 P2P 从初始化到建群的标准流程——但相亲角的故事不止于此。有人不谈判直接立群(Autonomous GO),有人存了密码下次秒重连(Persistent Group),有人在群里发广告(Service Discovery),还有人一边在别人群里当客人一边自己当老大(并发)。出问题了怎么查?四层日志一网打尽。本文是 P2P 系列的 "番外篇"——四个独立高级功能 + 运维排障,按需阅读。
本章导读
前面 6 篇按 P2P 的标准路径层层推进:初始化《P2P(一)》《P2P(二)》拉起相亲角的管理处和场地,设备发现《P2P(三)》逛一圈收集名单,GO 谈判《P2P(四)》坐下来商量谁当老大,Provision Discovery + WPS《P2P(五)》对暗号验身份,Group Formation《P2P(六)》建群发钥匙。这是一条完整的 "陌生人建群" 路径——从互不相识到建群完毕,每一步都走。
但相亲角里的故事远不止 "按规矩来"。你见过有人一进相亲角直接挂牌 "我建群了,谁来都行" 吗?见过两个老熟人见面连暗号都不对就秒进群吗?见过在群里发传单说 "我会投屏"" 我有打印机 " 的吗?见过一个人同时在两个群里——这边当客人那边当老大——的吗?
这四个场景对应的正是 P2P 协议的四个高级特性:Autonomous GO(不谈判直接当老大)、Persistent Group + Invitation(存凭证下次秒重连)、Service Discovery(群内服务广告)、STA+P2P 并发(一人分饰两角)。它们不是标准流程的必经环节,但在特定场景下比标准流程高效得多——从十几秒的 "标准路径" 压缩到一两秒的 "高级路径"。
再加上运维视角——出问题怎么排查?四层日志(Framework / Supplicant / QCOM 驱动 / MTK 驱动)的关键字速查、常见问题诊断、超时常量对照——构成本文第五部分。
四个子话题相互独立,按需阅读。如果你只对 "怎么不谈判直接建群" 感兴趣,跳到第 1 节;只想搞清楚 "为什么 Miracast 投屏秒连",读第 2 节;只需要 "怎么用 logcat 排查 P2P 问题",直奔第 5 节。
本文五个话题在软件栈中各就各位:橙色粗箭头是 Autonomous GO 的核心链,从 App 一路下穿到 wpa_supplicant(WifiP2pManager.createGroup → P2pStateMachine → groupAdd → P2pIface::addGroup/addGroupInternal → wpas_p2p_group_add → wpas_start_wps_go);Persistent Group / Service Discovery / 并发 / 日志则各落在栈的不同层。层间的 Binder / AIDL / nl80211 标出跨进程边界。
1 Autonomous GO —— 不谈判,直接当老大?
如果标准 GO Negotiation 是双方坐下来讨价还价,Autonomous GO 就是一个人直接拍桌子:"我就是老大,谁来都行。" 没有三轮握手、没有 Provision Discovery 协商——GO 单方面启动 WPS Registrar,等 Client 通过 WPS 来连(§1.3/§1.4 展开)。协议允许你这么做,前提是你不需要预先验证来者的身份。
1.1 什么时候需要 Autonomous GO?
标准的 P2P 建群流程要先 Find(发现设备),再 GO Negotiation(谈判谁当老大),再 Provision Discovery + WPS(验明正身),最后 Group Formation(建群)。这条路径走下来,快则 5 秒,慢则 15 秒以上。
但如果你的使用场景是 "我创建一个开放群组,谁来都行"——比如 Miracast Source 投屏到电视、或者创建一个 "开放 WiFi 热点" 让周围设备直接连——你不需要 "先发现一个特定设备然后和它谈判"。你只需要 "我当老大,群建好,有人来就连"。Autonomous GO 就是为这个场景设计的。从协议设计哲学看,Autonomous GO 是 P2P 中 "单方发起" 模式的体现——标准 GO Negotiation 假定双方地位对等、需要协商决定谁当老大;而 Autonomous GO 面向的是权力不对称的场景:Miracast Source 理应做 GO、Sink 理应做 Client,角色从一开始就是确定的,协商纯属浪费时间。协议允许这种不对称,是因为它把 "谁当老大" 从运行时协商挪到了设计期决定。
💡相亲角比喻:标准流程像是相亲——先逛一圈看名单、约出来谈条件、对暗号验身份,最后建群。Autonomous GO 相当于你不逛街、不约谈、不对暗号,直接在相亲角摆一张桌子、挂上牌子 "我是老大,信道 6,密码 xxxxx,谁来都行"——桌椅板凳全摆好,人员不限,来了就坐。省掉了找特定对象、谈判、对暗号的所有时间。
1.2 Framework 入口:WifiP2pManager.createGroup()
Android App 层调用 WifiP2pManager.createGroup(channel, listener) 来启动 Autonomous GO。这个方法在 Framework 层的入口与我们之前见过的 connect() / discoverPeers() 走的是同一套消息传递机制——AsyncChannel.sendMessage():
1 | // packages/modules/Wifi/framework/java/android/net/wifi/p2p/WifiP2pManager.java:2489 |
还有另一个重载版本,可以传入 WifiP2pConfig 来指定群组参数(名称、密码、频段、是否持久化等):
1 | // packages/modules/Wifi/framework/java/android/net/wifi/p2p/WifiP2pManager.java:2534 |
消息到达 P2pStateMachine 后,InactiveState 处理 CREATE_GROUP 消息——关键点在这里:它跳过了 ProvisionDiscoveryState(PD 交换)和 GO Neg 握手帧。回忆标准流程:用户点 connect 后,InactiveState 先转 ProvisionDiscoveryState(PD 交换),PD 成功后才转 GroupNegotiationState(GO 谈判)。
而 CREATE_GROUP 消息的路径不同——在 InactiveState 直接调用 groupAdd() 委托链下到 supplicant,成功后同样进入 GroupNegotiationState(它是 GroupCreatingState 的子状态),但这个状态下不会发生 GO Neg 帧交换——supplicant 已经把群建好,GroupNegotiationState 只是等待 P2P_GROUP_STARTED 事件。
但这里有一个容易被忽略的边界:如果设备此刻已经在另一个 Group 里(状态机正停在 GroupCreatedState),CREATE_GROUP 消息根本不会命中 InactiveState 的分支——GroupCreatedState 的 switch 里没有 CREATE_GROUP,消息一层层冒泡,最终由 DefaultState 的兜底分支回 CREATE_GROUP_FAILED + WifiP2pManager.BUSY(WifiP2pServiceImpl.java:2247,DefaultState 类定义在 2176 行)。也就是说,Android 的 P2P 栈同一时刻只允许一个 Group——已在群里时再调 createGroup,App 收到的是 BUSY 失败回调,而不是在已有群之上叠加第二个 GO。
1.3 Supplicant:wpas_p2p_group_add () —— 跳过谈判的直接建组
Framework 最终调用 HAL 层的 groupAdd(networkId, isPersistent)(ISupplicantP2pIfaceHal.java:286),由 SupplicantP2pIfaceHalAidlImpl 翻译成 AIDL 调用,跨过 Binder 落到 supplicant 进程内的 C++ AIDL 服务端 P2pIface::addGroup()(p2p_iface.cpp:423),后者经 validateAndCall 分派到同文件的 addGroupInternal()(p2p_iface.cpp:1716),最终落入 wpas_p2p_group_add():
1 | // wpa_supplicant/p2p_supplicant.c:7095 |
这个函数是 Autonomous GO 的核心编排器,五步完成全部工作:
- 停止正在进行的 Find——正在搜索就不能建群,必须先停掉
- 选信道——
wpas_p2p_select_go_freq()如果调用者指定了 freq 就用它,没指定就用首选频率或 ACS(Automatic Channel Selection) - 生成群参数——
p2p_go_params()填充 SSID(DIRECT-xy-...)和随机 passphrase(8+ 个 ASCII 字符) - 获取 GO 接口——如果还没创建,
wpas_p2p_get_group_iface()会创建 P2P-GO 类型的虚拟接口 - 启动 WPS GO——在 AP 接口上启动 WPS Registrar,开始发射 Beacon(周期性广播帧),等待 Client 来连
p2p_go_params() 做了什么?很简单——SSID 和 passphrase 如果上层还没设置,就当场随机生成:
1 | // src/p2p/p2p.c:1836 |
两个关键随机值:p2p_build_ssid() 生成 DIRECT-xy 前缀 + 随机字符的 SSID,p2p_random() 从大小写字母和数字中均匀分布地选择 passphrase 字符,符合 P2P 规范 v2.0 第 3.2.1 节对 Credential 的要求——设备不支持 WPA3-Personal 时用 WPA2-Personal + AES + 64 位十六进制 Network Key(支持 WPA3 时用 8-63 字符 Passphrase)。
注意:这两个值都是随机生成的——那想加入的 Client 怎么知道?这就是 Autonomous GO 的一个隐性前提:凭证的传递必须走带外(out-of-band)信道。P2P 规范 v2.0 第 3.2.1 节明说了这一点:passphrase "may be delivered using WSC, or in the case of a Legacy Client ... by means outside the scope of this specification"——也就是说协议只保证凭证存在,不定义分发的无线机制。
那凭证怎么从 GO 手里出来?它不像 GO Negotiation 那样在无线帧里协商出凭证,而是让 GO 侧把凭证通过控制接口事件带出到上层:wpas_p2p_group_started()(p2p_supplicant.c:1350)在群建好后发出 P2P-GROUP-STARTED 事件(wpa_ctrl.h:273),事件串里携带 ssid="DIRECT-xy-..." 和 passphrase="...",由 App 显示成二维码或文字让用户分享。所以 Autonomous GO 的 "谁来都行" 有一个前提——来的人得先从带外渠道拿到 SSID 和密码(扫码、手动输入、NFC 触碰都行),拿到之后才不需要谈判。这正是它与 "开放 WiFi 热点" 的本质区别:热点可以是完全开放(无加密),而 P2P GO 的群凭证总是存在的,只是分发的渠道在协议之外。
一个值得单独说透的点是 Autonomous GO 的 WPS Config Methods 是怎么确定的——因为它和标准流程的 "双方协商" 逻辑完全不同。
先澄清一条容易误以为成立的调用链:wpas_start_wps_go() 本身完全不碰 WPS IE(Information Element,信息元素),它只把 SSID/密码/PSK 拷进新建的 wpa_ssid。
真正把 Config Methods 写进 WPS IE 的是 p2p_build_wps_ie()(src/p2p/p2p_build.c:860),它读的是 p2p->cfg->config_methods——而这个值不是此刻协商出来的,是 GO 单方面宣告本机配置:初始化时 p2p.config_methods = wpa_s->wps->config_methods(p2p_supplicant.c:5063),而 wpa_s->wps->config_methods 在 wps_supplicant.c:1606 从 wpa_supplicant.conf 的 config_methods= 配置项解析而来(未配置时默认是 Display + Keypad,不包含 PUSHBUTTON——想要支持 PBC 按键直连,必须在 conf 里显式写 config_methods=push_button)。
对比标准流程就看清了这个区别的本质:GO Negotiation 里双方通过 GO Neg Req/Resp 帧交换各自的 Device Password ID(PIN/PBC 方法偏好),再取交集;Autonomous GO 没有对端输入,GO 单方面决定自己提供哪些 WPS 方法,Client 只能在 GO 提供的范围内选择。从相亲角的比喻说,标准流程是 "双方商量好对暗号的方式再对"(PIN 还是 PBC),Autonomous GO 是 "摊主单方面挂出招牌:我这儿只有这两种暗号方式,你要来就只能用这些"——验证来者身份的方式被降级为 "GO 说了算"。
1.4 与正常 GO Negotiation 的区别
把 Autonomous GO 和标准 GO Negotiation 放在一起对比,差异一目了然:
| 步骤 | 标准流程(GO Negotiation) | Autonomous GO |
|---|---|---|
| 前提 | 需要先发现 peer device | 不需要——不针对任何特定设备 |
| GO 角色确定 | p2p_go_det() 比 GO Intent 值 | 调用方直接指定自己就是 GO |
| 三轮握手 | GO Neg Req → GO Neg Resp → GO Neg Confirm | 不存在——跳过 |
| WPS 启动 | GO 和 Client 双方参与 | 只在 GO 侧启动 WPS Registrar,等 Client 来连 |
| peer 设备查找 | p2p_get_device(peer_addr) 必须找到 | 不查——go_dev_addr 就是自己 |
| Group Formation | 从 P2P_PROVISIONING 状态进入 | 从 P2P_IDLE 直接进 |
从代码流程上看,标准路径在 Framework 侧会经过 ProvisionDiscoveryState(PD 交换)→ GroupNegotiationState(GO 谈判)→ GroupCreatedState——前两个状态都是 GroupCreatingState 的子状态;supplicant 侧经过 P2P_CONNECT → P2P_CONNECT_LISTEN → P2P_GO_NEG → P2P_PROVISIONING。
而 Autonomous GO 在 Framework 侧从 InactiveState 直接进入 GroupCreatingState 下的 GroupNegotiationState——跳过 ProvisionDiscoveryState,且这个 GroupNegotiationState 里不会发生 GO Neg 帧交换——supplicant 侧从 P2P_IDLE 直接调用 wpas_start_wps_go(),群已经建好,Framework 侧只需等 P2P_GROUP_STARTED 事件转入 GroupCreatedState。中间跳过了整个 GO Negotiation + Provision Discovery 帧交换引擎。
下图把两条路径并排摆在一起:左列是标准流程的五步(Find → Provision Discovery → GO Neg 三轮握手 → WPS → Group Formation),右列是 Autonomous GO 的直接建组(createGroup → wpas_p2p_group_add → 启动 WPS GO 等 Client),中间的红色叉号标出被跳过的那三个环节。
1.5 使用场景对比
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| Miracast Source(手机投屏到电视) | Autonomous GO | Source 端直接当 GO,不需要和 Sink 谈判 |
| 创建 "开放群组" 让周围设备加入 | Autonomous GO | 不需要预先知道谁来连 |
| 两台手机互传文件 | 标准流程 | 需要先发现对方、确认双方愿意 |
| 手机连 P2P 打印机 | 标准流程 | 打印机通常是 GO,手机作为 Client 加入 |
| 老熟人再次快速建群 | Persistent Group + Invitation(见第 2 节) | 比 Autonomous GO 更快——连 WPS 都跳过 |
1.6 驱动侧:GO 模式 Beacon + Auth/Assoc
Autonomous GO 建群完成后,驱动侧的 Beacon 发射、Auth/Assoc 帧处理、四次握手(WPA2 四步密钥协商)与上一篇《P2P(六)Group Formation》中描述的 GO 侧路径完全相同。wpas_start_wps_go() 设置好 GO 参数后请求扫描(wpa_supplicant_req_scan()),状态机随后进入连接流程调用 wpa_supplicant_create_ap() 拉起 hostapd 内嵌 AP,经 ieee802_11_set_beacon()(src/ap/beacon.c:3186)→ wpa_driver_nl80211_set_ap() 下发 NL80211_CMD_SET_BEACON/NL80211_CMD_START_AP,驱动和固件开始发射 Beacon。
驱动侧 QCOM 走 SAP FSM(SAP_STARTING 状态 → WMI_VDEV_START_REQUEST_CMDID),MTK 走 P2P Role FSM(P2P_ROLE_STATE_IDLE → P2P_ROLE_STATE_AP_CHNL_DETECTION)。这些细节在《P2P(六)Group Formation》第 7 节已经完整展开,这里不再重复。
值得单独说明的一点是:Autonomous GO 因为没有经过 GO Negotiation,驱动侧不需要执行模式从 "P2P Device" 到 "P2P GO" 的切换——因为 P2P Device 从未处于连接状态。 直接以 GO 模式启动一次新的 AP 接口即可,这比标准流程少了一次模式切换。用相亲角的比喻说,Autonomous GO 就是那个开场自带桌椅、直接挂牌开张的摊主——别人还在逛街约谈对暗号,他已经把场子支好等人来坐了。省掉的模式切换,本质上就是省掉了一次 "从客人变成摊主" 的转身。
顺带说清一个双平台问题:Autonomous GO、Persistent Group、Service Discovery 这三个特性在 QCOM 和 MTK 驱动里都没有特化路径。 原因很简单——它们完全是 wpa_supplicant 用户态的概念,驱动只看到一次普通的 P2P GO 接口启动(QCOM 的 SAP_STARTING、MTK 的 P2P_ROLE_STATE_IDLE→AP_CHNL_DETECTION,与标准建群无异)或一次普通的管理帧收发(SD 的 GAS 帧走 p2p_send_action() → QCOM wlan_hdd_mgmt_tx() / MTK p2pFuncTxMgmtFrame(),见 §3.2)。Persistent Group 的凭证存储全在 supplicant 的配置里,驱动根本不感知 "这个群之前建过";SD 的查询队列也只在 p2p->sd_queries 链表上(§3.3),驱动只是转发 Action 帧。
所以排查这三个特性时,不需要去驱动侧找特化代码——问题要么在 supplicant 的协议状态机,要么在框架层的命令参数,驱动只负责透传。
2 Persistent Group + Invitation —— 老熟人见面,暗号都不用对?
上一节是 "不谈判直接建群"——省掉了寻找特定对象 + 谈判的时间。但如果群之前已经建过、凭证还在,能不能连 WPS 暗号也跳过?这就是 Persistent Group + Invitation:把上次的群密码存下来,下次见面秒重连。
相亲角里,两个之前就认识的人见面——"我们上次那个群还在吧?还是你当老大,密码我还记着呢。" 不需要重新逛街、谈判、对暗号——直接进群。从协议角度看,Invitation Request/Response 两帧交换(~100ms)替代了 Find(~2-5s)+ GO Neg(~100ms × 3)+ WPS(~1-15s)——时间从 5-15 秒压缩到 1-2 秒。
2.1 Persistent Group 是怎么创建的?
Persistent Group 不是在 "创建 Persistent Group" 这一刻被标记的——它是正常 GO Negotiation + WPS 成功之后的副产品。当 WPS 完成、wpas_group_formation_completed() 检查到 ssid->mode == WPAS_MODE_P2P_GO 且 persistent_group 标志为 1 时,组凭证(SSID、PSK、GO 的 Device Address)被保存到 supplicant 的配置存储中。
在 wpa_supplicant.conf 里,一个 Persistent Group 的网络块长这样:
1 | network={ |
mode=3 表示这个网络是 P2P Group 模式(0=infrastructure, 1=IBSS, 2=AP, 3=P2P)。disabled=2 表示 "这个网络当前不在使用,但凭证是有效的——以后可以重新 invoke"。
在代码层面,ssid->mode = WPAS_MODE_P2P_GO(mode=3 的 supplicant 内表示)是由 wpas_group_formation_completed() 设置的(《P2P(六)Group Formation》第 1 节已展示),persistent 标志在若干处被标记——比如 wpas_p2p_group_add() 中第 7128 行 params.persistent_group = persistent_group,或 GO Negotiation 中 p2p_go_complete() 根据 P2P_GROUP_CAPAB_PERSISTENT_GROUP flag 设置。
2.2 Invitation 流程:两帧完成秒重连
Persistent Group 的凭证存下来了,但怎么用?答案是 Invitation 协议——P2P 规范 v2.0 第 3.1.5 节定义的 "邀请" 机制。整个流程只有两帧:
Invitation Type 有两个值:Type=0 表示 "我现在的群里缺人,你来加入我的活跃群",Type=1 表示 "我们之前有个 Persistent Group,请重新激活它"。Persistent Group 的 "秒重连" 用的是 Type=1。为什么 Persistent Group 不能直接连接而必须走 Invitation?因为它是异步重建——群早已解散,上次的 GO 可能已经不在附近、信道也可能已经改变,双方只是各自保留了一份凭证,谁也不知道对方现在是否可连。Invitation 就是 "把解散的群重新拉起来" 的机制:先通过两帧确认 "你我都在、凭证都还在、信道可用",再进入 Group Formation。它不是简单的 "快速连接" 命令,而是一次 "重建群的协商"——如果 GO 已经离开,Invitation 会失败,双方退回 Find 重新发现。
发起方:wpas_p2p_invite () → p2p_invite ()
Framework 调用 HAL 层 invite(WifiP2pGroup group, String peerAddress) → AIDL → supplicant 的 wpas_p2p_invite():
1 | // wpa_supplicant/p2p_supplicant.c:7879 |
关键的分叉在 ssid->mode:如果保存的网络是 WPAS_MODE_P2P_GO,发起方就是上次的 GO,邀请对方(Client)回来;如果是 Client,邀请目标就是 GO 的地址(ssid->bssid)。
p2p_invite() 在 src/p2p/p2p_invitation.c:670,做了几件事:从设备列表中找到 peer、通过 p2p_prepare_channel() 选一个双方都支持的信道(参考 pref_freq,兜底 Listen/Oper freq),然后调用 p2p_invite_send() 发送 Invitation Request 帧。
帧构建:p2p_build_invitation_req ()
p2p_invite_send()(p2p_invitation.c:582)调用 p2p_build_invitation_req() 构建帧。如 P2P 规范 v2.0 第 4.2.9.5 节和 Table 102 所定义,Invitation Request 帧的 P2P IE 包含这些属性:
1 | // src/p2p/p2p_invitation.c:18 (精简展示关键字段) |
P2P 规范 v2.0 Table 102 对这些属性的要求(R = Required, O = Optional):
| 属性 | 要求 | 说明 |
|---|---|---|
| Invitation Flags (Attr 18) | R | bit0:Invitation Type——0 = 邀请加入活跃群,1 = 要求重新激活 Persistent Group |
| Configuration Timeout (Attr 5) | R | GO 和 Client 各自需要多长配置时间 |
| Operating Channel (Attr 17) | GO 发必须 / Client 发可选 | 目标工作信道 |
| Group BSSID (Attr 7) | GO 发或 Client Type=0 时 | P2P Group 的 BSSID |
| Channel List (Attr 11) | R | 本设备支持作为 Group Operating Channel 的所有信道 |
| Group ID (Attr 15) | R | GO Device Address + SSID |
| P2P Device Info (Attr 13) | R | 本机设备信息(含 Device Address、Config Methods) |
接收方:invitation_process 回调
接收方收到 Invitation Request 后,P2P 核心层通过 p2p->cfg->invitation_process 函数指针回调上层——在 wpa_supplicant 中注册为 wpas_invitation_process()(p2p_supplicant.c:3215,注册在 p2p_supplicant.c:5033)。该回调查找本地保存的 Persistent Group:
- 解析 Invitation Flags——Type=1 表示要找 Persistent Group
- 按 Group ID(GO Device Address + SSID)查找本地配置——
wpa_config_get_network()在配置存储中找mode=3的网络 - 找到后检查
disabled=2——说明凭证还在,可以 reconnect - 检查
persistent_reconnect标志——决定是自动重连还是通知用户
那如果本地配置里找不到这个 Persistent Group 呢?——比如配置文件损坏、群被 App 删过、或换过设备。wpas_invitation_process() 在遍历 wpa_s->conf->ssid 找不到 disabled=2 且 SSID / GO 地址都匹配的网络块后,会打一条日志 "requested reinvocation of an unknown group" 并返回 P2P_SC_FAIL_UNKNOWN_GROUP(p2p_supplicant.c:3292-3294;该状态码值 = 8,定义在 ieee802_11_defs.h:1844)——这次 Invitation Response 以 Status=8 拒绝重连。
发起方收到这个状态码后也不会傻等:在 wpas_invitation_result()(p2p_supplicant.c:3534)的结果回调里,P2P_SC_FAIL_UNKNOWN_GROUP 会触发 wpas_remove_persistent_client() / wpas_remove_persistent_peer()(p2p_supplicant.c:3560/3599)把对端从自己的持久组 client 列表里摘掉——因为对端已经没有这个群的凭证,继续留着只会让下一次 Invitation 再失败一次。
Invitation Response 帧的格式由 P2P 规范 v2.0 Table 106 定义:Status 属性(成功 / 失败)、Configuration Timeout、Operating Channel(GO 响应时)、Group BSSID(GO 响应时)、Channel List。GO 响应时 Operating Channel 必须来自 Channel List 中的信道(常见信道交集)。
发起方发出 Invitation Request 后,100 毫秒内须收到 Invitation Response,否则视为流程失败(P2P 规范 v2.0 第 3.1.5.1 节)。
但如果对端不在线呢?——设备关机、超出范围、或者正在忙别的信道。发起方的超时与重试机制是这样的:p2p_invite_send() 把帧交给驱动后,驱动在发送完成的回调 p2p_invitation_req_cb()(p2p_invitation.c:629)里设一个超时——发送成功(帧确实发射出去)为 500ms,发送失败为 100ms(p2p_invitation.c:645-646)。这个不对称是刻意的:发送成功说明帧真的上了空口——对端此刻可能正处在一个 Listen/Scan 周期里、稍后就会回来收帧并发 Response,所以给足 500ms 的应答窗口;发送失败则说明对端大概率不在当前社交信道上(帧根本没发出去),等再久也是白等,100ms 后尽快重发试下一个时机才划算。
超时后状态机切到 P2P_INVITE_LISTEN 短暂监听,再经 p2p_timeout_invite_listen()(p2p.c:4218)重发;每重发一轮 invitation_reqs++,上限 100 次(p2p.c:4220 的 invitation_reqs < 100),超过后打日志 "Invitation Request retry limit reached" 并回退 P2P_IDLE——Invitation 失败,双方退回 Find 重新发现。
另外有一条特殊重试路径:收到 P2P_SC_FAIL_NO_COMMON_CHANNELS(双方不在同一社交信道)时,supplicant 会随机换一个社交信道重发一次(p2p_invitation.c:506),而不是直接放弃。注意这与 §5.3 速查表里 "Invitation Response wait 100ms" 不矛盾——100ms 是收到 Request 后 Response 的应答时限(规范要求),500ms/100ms 是发送方等 Response 的超时(supplicant 实现);前者约束被邀请方,后者约束发起方。
一个常见的安全疑问是:Invitation 帧里带了 Group BSSID、Channel List、Group ID,攻击者把它捕获并重放,会不会把 Persistent Group 的 PSK 套走?答案是不会——Invitation 帧里没有 PSK,也没有 Replay Counter。P2P 规范 v2.0 通篇没有为 Invitation 帧定义重放计数器,唯一的 "事务标识" 是 Public Action 帧头的 Dialog Token(请求方自选、每发一帧自增,p2p_invitation.c:57)。而 Invitation Response 帧体(Table 106)只有 Status、Configuration Timeout、Operating Channel、Group BSSID、Channel List——没有任何一个字段携带密钥材料。
PSK 从不出现在 Invitation 帧里,它只在后续的四次握手(EAPOL)中作为共享秘密使用——持久组 re-invoke 时双方本已各存一份 PSK,Invitation 只是 "重新拉起群" 的握手,不是 "传输密钥" 的通道。攻击者重放一个 Invitation Request,最多让被邀请方误以为你想重连而回一个 Response;要真正建立连接,仍需通过后续的 WPA2 四次握手验证 PSK,而这把钥匙攻击者并没有。换句话说,P2P 的安全性不靠 Invitation 帧本身,而靠 WPS / 四次握手里的 PSK 认证。
2.3 persistent_reconnect=1 与 =0 的行为差异
Persistent Group 不总是自动重连。persistent_reconnect 是 P2P Capability 属性中的 Group Capability Bitmap 的一个 bit(P2P 规范 v2.0 第 4.1.4 节 Table 28)。它的值决定了 Invitation 被接收后的行为:
| persistent_reconnect | 被邀请方收到 Invitation Request 后的行为 |
|---|---|
| 1 | 自动接受——wpas_invitation_process() 检测到 persistent_reconnect=1,跳过用户交互,直接回复 Success,进入 Auth/Assoc/四次握手 |
| 0 | 通知用户——supplicant 发 P2P-INVITATION-RECEIVED 事件到 Framework,Framework 进入 UserAuthorizingInviteRequestState 弹窗让用户决定 |
persistent_reconnect=1 是 "无感重连" 的关键——用户在点击 "连接" 的那一刻,如果之前已经在这个 Persistent Group 里且 persistent_reconnect=1,整个流程跳过了 Find、GO Neg、WPS,连 Provision Discovery 中的 PIN/PBC 选择也跳过了——因为 WPS Credential 已经在 Persistent Group 配置里。从框架角度看,这相当于 "connect 调用执行了已保存的凭证,直接进入 Group Formation"。
说清楚时间差才能理解这个设计的价值:
| 步骤 | 耗时 |
|---|---|
| Find(Listen + Scan 循环) | 2-5s |
| GO Negotiation(三轮握手 100ms × 3 + 状态切换) | ~500ms |
| Provision Discovery | ~100ms |
| WPS(EAP-WSC 八轮 + PIN/PBC 交互) | 1-15s(取决于用户输入 PIN 的时间) |
| Auth + Assoc + 四次握手 | ~200ms |
| 标准流程合计 | ~5-15s |
| Invitation Request + Response(两帧 100ms) | ~100ms |
| Auth + Assoc + 四次握手(用已存凭证) | ~200ms |
| Persistent Invitation 合计 | ~1-2s |
8 到 15 倍的提速——不是因为代码写得快,而是因为省掉了三个协议阶段。
2.4 Persistent Group 的持久化:不只是存密码
Persistent Group 的存储不仅仅是 "记住密码"。wpa_supplicant 在配置文件中存储了一个完整的 network 块,包含:
- SSID(
DIRECT-xy-...)、PSK(64 位十六进制) mode=3(P2P Group 模式)、disabled=2(未激活但凭证有效)p2p_client_list——上次群里的 Client 地址列表- 关联的 P2P Device Address(GO 的设备地址,用于识别 "这个群是谁创建的")
在重新 invoke 时,wpa_supplicant 用 p2p_client_list 来确认 "对面的设备是不是之前在这个群里的"——这比靠 Device Name 匹配可靠得多。密码不会重新生成——p2p_go_params() 中检查 passphrase_set 标志,如果是已保存的 Persistent Group,passphrase 是预设的,不需要重新随机生成。
但 Persistent Group 的 BSSID 和 Operating Channel 不是固定的。P2P 规范 v2.0 第 3.2.5 节明确规定:"The P2P Interface Address (and therefore BSSID) and operating channel of the P2P Group may not be the same for each session." 不是 "may not be",事实上 每次 invoke 的 BSSID 大概率不同——因为 GO 每次创建的虚拟接口 MAC 地址可能不同;信道也会因为当前环境(已有 STA 连接占用的信道、雷达干扰等)而改变。规范只是把 "不能假设固定" 这件事说成 "可能不同",留有设计的弹性空间。所以 Peer 不能假设 "上次的信道这次还能用"——这就是为什么 Invitation 帧里必须有 Channel List 和 Operating Channel。
放到老熟人的场景里:两人常约在同一个茶馆,但上次坐的桌子这次可能被别人占了——所以出发前先确认一句 "还坐老位置吗",对方回 "还在",才值得起身。Invitation 帧里的 Channel List 和 Operating Channel,就是那两句确认。
3 Service Discovery —— 群里发广告,谁会投屏、谁有打印机?
群建好了——但在这个群里,周围有多少潜在的服务可以用?谁知道谁会投屏、谁有打印机?Service Discovery 就是在 P2P 群里发 "服务广告" 的机制——基于 GAS/ANQP 协议框架,在设备发现阶段就能查询对方支持什么服务,不用等到连上才知道。
3.1 为什么需要 Service Discovery?
先想象没有 Service Discovery 的情况:扫描到一堆 P2P 设备——张三、李四、王五——你只知道他们的设备名,但你不知道谁会投屏、谁有文件共享服务、谁的打印机能打。要搞清楚,你得一个个连上去——Connect → GO Negotiation → WPS → Auth/Assoc → 四次握手 ——每个可能十几秒,连上去发现 "哦,你不支持投屏",再断掉,再连下一个。
Service Discovery 在设备发现阶段就解决了这个问题——在 Find 过程中(Listen/Scan 交替的间隙),发起一个 SD Query:"你们谁支持 Wi-Fi Display?" 周边设备在 SD Response 里回答 "我会,我的 RTSP(Real Time Streaming Protocol)port 是 7236"。然后用户只连接那个支持投屏的设备。查询在关联之前完成——不需要建立 P2P Group,不需要 WPS,不需要对暗号。
3.2 协议基础:GAS + ANQP
Service Discovery 的帧交换走的是 GAS(Generic Advertisement Service) + ANQP(Access Network Query Protocol) 框架,由 IEEE 802.11-2020 定义(第 9.4.2.92 节 Advertisement Protocol element,第 11.22.3 节 GAS 过程)。P2P 规范 v2.0 第 4.2.11 节和 Appendix C 在此基础上定义了 Vendor-Specific 内容。
GAS 提供的是 "预关联信息查询" 的通用容器——设备在关联之前(pre-association)就可以通过 Public Action 帧交换查询/响应信息。ANQP 定义查询/响应的信息结构。P2P 的 Service Discovery 是在 ANQP 的 Vendor-Specific(OUI=50:6F:9A,Wi-Fi Alliance)里填入自己的 TLV 结构。
为什么不直接用 IP 层的 UDP 广播发服务广告?因为 SD 发生在关联之前——此时设备还没有 IP 地址、没有建立 P2P Group,IP 层通信根本不可用。GAS 正是 IEEE 802.11 为这种 "预关联信息查询" 场景定义的标准广告协议,ANQP 在其上提供查询 - 响应模型。P2P SD 之所以还要在 GAS 上做分片,是因为 P2P Action 帧本身有大小限制——2.4/5GHz 下管理帧净荷约 1400B(见 3.4 节),而一个 SD 响应(尤其 Bonjour/UPnP 的服务描述)可能远超一帧。
整个 SD 协议走的是 Public Action 帧——GAS Initial Request(Action=10)→ GAS Initial Response(Action=11)→ GAS Comeback Request(Action=12)→ GAS Comeback Response(Action=13)。
这在发送路径上与 GO Negotiation 的帧走的是同一套 P2P Action 帧下发机制——回顾上一篇《管理帧的发送——控制面》的管理帧发送体系:p2p_send_action() → wpa_drv_send_action() → driver_nl80211_send_action() → NL80211_CMD_FRAME,驱动侧走 QCOM 的 wlan_hdd_mgmt_tx() 或 MTK 的 p2pFuncTxMgmtFrame()。
3.3 发起请求:p2p_sd_request ()
App 通过 WifiP2pManager.addServiceRequest() → Framework WifiP2pNative.p2pServDiscReq() → HAL → AIDL → supplicant 的 wpas_p2p_sd_request(),最终落在 P2P 核心层:
1 | // src/p2p/p2p_sd.c:879 |
两个关键设计点:
队列模型。SD 查询不是 "发一个等一个"——p2p_sd_request() 把查询挂进 p2p->sd_queries 链表头部(头插法),supplicant 在 Find 过程中(Listen 和 Scan 的间隙)逐个取出来执行。这个队列没有长度限制——Android Framework 会把所有 App 的服务请求聚合成单条 supplicant 查询,天然限制了查询数量(见 3.7 节)。
查询何时移除。链上的查询不会无限滞留——有两个清理时机。一是显式取消:上层调用 WifiP2pManager.clearServiceRequests() → supplicant 的 p2p_sd_cancel_request()(p2p_sd.c:940)→ p2p_unlink_sd_query()(p2p_sd.c:116)把查询摘下来 free 掉。二是响应完成后自动移除:定向查询(for_all_peers=0)在收到对端的 GAS 响应后会自动摘除——无论是 GAS Initial Response(p2p_rx_gas_initial_resp(),p2p_sd.c:480)还是分片 Comeback 收齐(p2p_rx_gas_comeback_resp(),p2p_sd.c:701),处理完都调用 p2p_unlink_sd_query() 摘掉已完成的查询。
但广播查询(for_all_peers=1)不会因一个 peer 的响应就移除——它要等所有已发现设备都响应过(或 Find 结束)才退场;p2p_free_sd_queries()(p2p_sd.c:156)在 P2P 栈销毁时兜底清空整个链表。所以不存在 "查询过期" 的定时机制——只有显式取消、响应完成、栈销毁三条退路。
定向 vs 广播。dst 参数决定查询范围:为 NULL 时是广播查询——for_all_peers=1,所有已经发现的设备都会被标记 sd_pending_bcast_queries++,接下来的各个 peer 在处理时会各收到一份查询。非 NULL 时只查特定设备——你的 App 已经知道某台打印机支持 IPP 打印,只是想在连之前确认一下它当前的 IPP 端口号。
3.4 响应处理与分片:什么情况下 SD 不能一帧传完?
p2p_sd_response () 的分片决策
响应方收到查询后,第一步是判断响应数据能不能一帧装下——分片阈值在 2.4/5GHz 是 1400B、60GHz 是 928B:
1 | // src/p2p/p2p_sd.c:422 |
分片的逻辑很直观——就像相亲角公告栏上贴广告,一张传单太长、一次贴不完,就先贴第一段并挂出 "还有后续" 的牌子,对方来催再一段段补齐:
- 如果响应数据小于等于
max_len(2.4/5GHz 为 1400 字节,60GHz 为 928 字节)——直接一帧 GAS Initial Response 发完,comeback_delay=0 - 如果响应数据超过
max_len——保存在p2p->sd_resp缓冲区,sd_frag_id=0,第一个 GAS Initial Response 带comeback_delay=1,请求方收到后发 GAS Comeback Request,响应方每次发一个片段,片段 ID 递增,最后一个片段的More GAS Fragmentsbit 为 0 - 每次请求方来要下一个片段时,
sd_frag_id++,按max_len步进sd_resp_pos
P2P 规范 v2.0 Appendix C.3 定义了 GAS Fragmentation 的完整机制:GAS Query Response Fragment ID 前 7 位是片段编号(从 0 开始递增),第 8 位是 More GAS Fragments 标志(1 = 还有更多,0 = 最后一个)。2.4/5GHz 和 60GHz 的最大 MMPDU 大小不同,所以分片阈值也不同。
sd_resp 缓冲区的单响应限制
值得注意的一个限制:p2p->sd_resp 是一个全局单缓冲区——同一时刻只能为一个 peer 的一个查询保存分片响应。如果有新的查询触发了分片,旧的 sd_resp 直接被 wpabuf_free() 丢弃:
1 | if (p2p->sd_resp) { |
源码注释也承认了这个限制并指出 TODO:"可以考虑为每个 peer 单独存储分片响应",但当前实现选择了节省内存的单缓冲区方案。后果是:在 SD 分片传输过程中,如果又来了一个需要分片的 SD 响应,前一个就被丢弃了。
另一个值得了解的恢复不对称:请求方有超时兜底,响应方没有。 请求方发出 GAS Initial Request 后进入 P2P_SD_DURING_FIND 状态并设 200ms 超时(p2p.c:3461),超时后走 p2p_continue_find() 继续找下一个 peer——广播查询不会因单次超时被删除,而是换下一个 peer 重试(sd_reqs > 100 才放弃,p2p_sd.c:89)。
但响应方这一侧没有这样的兜底。 响应方一旦把分片响应存进 p2p->sd_resp,如果请求方的 GAS Comeback Request 丢失或请求方中途放弃,这段缓冲区没有任何超时清理——它只能被下一条新的分片响应顶掉(p2p_sd.c:447)、全部发完(p2p_sd.c:686)或 P2P 栈销毁(p2p.c:3166)时才释放。也就是说,在 GAS Comeback 交换卡住时,响应方会一直抱着这段内存不放,直到下一次 SD 分片请求到来——这是实现层面一个真实的资源滞留点。
回到问题本身:如果某个 SD 分片丢失,请求方靠 200ms 超时 + continue_find 重试;响应方不重发已发出的片段,只等请求方重新发 Comeback Request(前提是它还在等)。规范的 GAS 分片没有 ACK / 重传机制,丢片靠请求方超时重来兜底。
最后补一个数据完整性边界:如果对端返回的 SD 响应格式本身是坏的——长度越界、IE 元素 ID 不对、ANQP Vendor OUI 不匹配——会发生什么?p2p_rx_gas_initial_resp()(p2p_sd.c:480)和 p2p_rx_gas_comeback_resp()(p2p_sd.c:701)都内置了一条防御性解析链:每一层格式检查失败就 p2p_dbg() 打一条日志然后直接 return 丢弃,绝不上抛给上层、也不推进状态机。
典型命中点:帧太短("Too short GAS Initial Response frame")、Status Code 非 0("Service Discovery failed: status code %u")、Advertisement Protocol IE 缺失("Unexpected IE in GAS Initial Response")、ANQP Vendor OUI 不匹配("Unsupported ANQP vendor OUI-type")。
关键后果是:被丢弃的坏响应不会触发查询移除——p2p_unlink_sd_query() 只发生在成功解析完的路径(3.3 节已述),坏响应让查询继续挂在 sd_queries 上,由 200ms 超时 + p2p_continue_find() 换下一个 peer 重试兜底。换句话说,SD 对 "格式错误的响应" 的态度是 "忽略它、让超时机制重来",而不是把解析错误上抛给 Framework 或 App。
把 3.3 节的查询入队和 3.4 节的响应分片串起来看,整个 SD 生命周期是这样的:
3.5 Service Protocol Types —— 广告分四种
P2P 规范 v2.0 Table 120 定义了 12 种 Service Protocol Type(值 0-11,另有 255 为 Vendor Specific)。SD Query/Response 的 TLV 结构中的 Service Protocol Type 字段决定了查询 / 响应数据的解析方式:
| 值 | 协议 | 典型查询内容 |
|---|---|---|
| 0 | All Service Protocol Types | 查询所有服务 |
| 1 | Bonjour | DNS-SD 查询——例如 _ipp._tcp.local(查找 IPP 打印机) |
| 2 | UPnP | SSDP 查询——例如 urn:schemas-upnp-org:device:MediaRenderer:1 |
| 3 | WS-Discovery | Web Services Dynamic Discovery |
| 4 | Wi-Fi Display | WFD Device Info、RTSP Port——Miracast 专用 |
| 255 | Vendor Specific | 厂商自定义 OUI 前缀 |
Bonjour(Apple 的零配置网络)和 UPnP(通用即插即用)是 SD 最常见的两种查询类型——前者用于 Apple 生态和打印机发现,后者用于智能家居设备。P2P 规范 v2.0 Appendix E(Bonjour)和 Appendix F(UPnP)给出了详细的推荐实践。
值 5-10 是 WiGig 家族——5/6 是 WiGig Display Extension over MAC(TX/RX,60GHz 投屏)、7/8 是 WiGig Serial Extension over MAC(Host/Device,USB 串行重定向)、9 是 WiGig Bus Extension、10 是 WiGig SD Extension,全部面向 60GHz 毫米波生态,日常 2.4/5GHz 的 Android 设备基本碰不到;11 是 Peer-to-Peer services(P2Ps,Wi-Fi Alliance 的通用 P2P 服务层),用于在现有 P2P 会话之上承载更上层的服务发现。12-254 保留未用。
3.6 Miracast:SD 查询中最常见的应用
Miracast(Wi-Fi Display)是 P2P Service Discovery 的最常见应用场景。在 Miracast 的场景里,Source(手机)和 Sink(电视)在建立 P2P 连接之前,通过 SD 交换彼此的 Wi-Fi Display 能力。
WFD 的信息通过两个关键数据结构交换:
- wfd_dev_info(WFD Device Information):在 supplicant 中不是独立的 C struct,而是
struct p2p_data中的一个struct wpabuf *字段(p2p_i.h:614)——保存 WFD Device Information 子元素(subelement ID=0)的原始字节。包含设备类型(Source/Sink/Primary Sink/Secondary Sink)、支持的视频编码格式、分辨率、HDCP 支持等。由p2p_set_wfd_dev_info()设置,在 Group Formation 或 SD 过程中作为 WFD IE 的一部分发送。 - RTSP 端口:WFD Sink 监听的 RTSP(Real Time Streaming Protocol)端口号(控制端口通常为 TCP 7236,数据端口动态协商)。RTSP 会话的建立和端口协商是应用层行为——由 Android Framework 的
WifiDisplayController处理,不在 wpa_supplicant 的 C 代码范围。wpa_supplicant 只在 P2P/WFD IE 中交换设备能力信息,RTSP 会话在 P2P 连接建立之后由上层应用独立建立。
在 supplicant 代码中,WFD IE 的处理由 CONFIG_WIFI_DISPLAY 编译选项控制——编译时开启后,P2P Invitation 帧也可以携带 WFD IE(p2p_build_invitation_req() 中第 29-48 行),前提是 inv_role == P2P_INVITE_ROLE_ACTIVE_GO。
这里有个值得对比的差异:SD 帧从不携带 WPS IE,Invitation 帧才可能带。 看源码就能确认——p2p_build_sd_query()(p2p_sd.c:170)和 p2p_build_sd_response()(p2p_sd.c:211)只填 ANQP Vendor-Specific 内容(OUI Subtype + Service Update Indicator + SD TLV),全程不调用 p2p_build_wps_ie();而 p2p_build_invitation_req() 在 dev_pw_id >= 0 时才调用 p2p_build_wps_ie()(p2p_invitation.c:103,注释写明 "WSC IE in Invitation Request for NFC static handover")。
为什么会有这个差异?原因在语义上很清晰:SD 是 "访问信息"——预关联阶段查询对方支持什么服务,此时配网方法(Config Methods / Device Password ID)还没进入协商,WPS IE 没有用武之地;Invitation 是 "重建安全连接"——当它需要通过 NFC 标签上的 Device Password ID 走静态切换(P2P 规范 v2.0 第 3.1.5.1 节明确要求 GO 在这种情况下把 WSC IE 放进 Invitation Request)时,才带上 WPS IE。
常规的持久组 re-invoke(wpas_p2p_invite 传 dev_pw_id=-1)也不带 WPS IE——PSK 已经在双方配置里,不需要在帧里重申配网方法。一句话:WPS IE 的使命是 "协商配网方法",而配网发生在连接建立阶段(GO Neg / WPS),不在服务发现阶段(SD)。
Miracast 的一个完整流程是:Source 做 Autonomous GO(第 1 节),Sink 通过 SD 发现 Source 的 WFD 能力,然后 Sink(作为 P2P Client)通过 Invitation(第 2 节)加入 Source 的群——三个高级特性串联在一起。
3.7 Android 层的 SD 查询聚合
SD 的帧协议和 supplicant 队列都看完了,还差最后一个环节——这些查询从 App 到 supplicant 的路上,Android 是怎么组织的?答案不是 "限流",而是 "聚合"。Android Framework 在 WifiP2pServiceImpl 中并没有为 SD 查询实现 per-peer 的频率限制——它做的是 "聚合":把每个 App 通过 addServiceRequest() 提交的请求存进各自的 ClientInfo.mReqList,在 Find 启动时把所有客户端的请求拼成一条查询字符串,统一通过 WifiP2pNative.p2pServDiscReq()(WifiP2pNative.java:944)发给 supplicant——而不是每个 App 各发各的查询。
supplicant 的 P2P 核心层(p2p_sd.c)同样没有内置的公平策略——查询只是被追加到 p2p->sd_queries 链表中,在 Find 过程中按顺序执行。聚合 + 顺序执行这个组合,天然限制了 SD 查询对信道资源的占用——因为 SD 查询和 GO Negotiation、PD 等协议共享 Public Action 帧的发送资源。就像发广告也得排队上墙——公告栏(Public Action 帧)就那么大,SD 查询和 GO 谈判、PD 只能轮流贴。而比 "排队发广告" 更极端的资源竞争,发生在设备层面:STA 和 P2P 两个连接要抢同一块射频——这正是下一节的主题。
4 STA+P2P 并发 —— 一个人同时在两个群里?
手机连着家里的 WiFi(STA 模式),同时又想开 P2P 热点给同事传文件——同一块 WiFi 芯片怎么同时干两件事?本章从芯片能力的硬约束谈起,追踪 Framework 的 ActiveModeWarden 到 QCOM 和 MTK 的实现差异——单信道分时复用 vs 双频同时工作,以及对上层应用的影响。
4.1 硬件约束:你的芯片能 "一心二用" 吗?
WiFi 芯片的核心限制不是 CPU 或内存——是射频链路数量。大多数手机只有一套 2.4GHz + 5GHz 射频(或 2.4G/5G/6G 各一套),同一时刻只能在一个信道上收发。
所以 "STA+P2P 并发" 的场景本质上是:芯片在一套射频上,怎么在 STA 信道(比如信道 1)和 P2P GO 信道(比如信道 6)之间切换?这个问题的核心矛盾在于:你在一个群说话时听不到另一个群的呼叫——切去 STA 信道处理流量时,P2P Client 发的帧没人应答;切回 P2P 信道时,STA 侧又可能错过了 AP 的 Beacon。单射频分时复用让两个 "群" 处于半聋半哑状态。
两种策略:
| 策略 | 硬件要求 | 原理 | 优缺点 |
|---|---|---|---|
| MCC(Multi-Channel Concurrency) | 一套射频即可 | 在 STA 和 P2P 信道之间分时切换——类比 CPU 的时间片轮转:一段时间服务 STA,切到 P2P 信道服务 P2P Client,再切回来 | 任何 WiFi 芯片都支持;但两类操作的吞吐量都打折 |
| DBS(Dual Band Simultaneous) | 两套独立射频 | STA 用 2.4GHz 射频连 AP,P2P GO 用 5GHz 射频开群——真正的 "同时进行",不需要切换信道 | 高通高端芯片支持(QCA 系列);吞吐量不打折,但增加功耗和成本 |
| SCC(Single-Channel Concurrency) | 一套射频 | STA 和 P2P 在同一个信道上——不需要切换,Beacon / 数据帧共享同一个物理信道 | 仅当 STA AP 和 P2P GO 恰好在同一信道时才可行 |
大多数中低端手机的 WiFi 芯片只有一套射频,只能走 MCC——这就是为什么你开着 WiFi 又用 P2P 传文件时,两边速度都慢的原因。
4.2 Framework:ActiveModeWarden 与 WifiNative 的并发决策
硬件约束画出了并发能力的天花板,但真正拍板 "能不能并发、要不要让路" 的是 Framework。Android Framework 的并发管理由两个组件配合完成。ActiveModeWarden(packages/modules/Wifi/service/java/com/android/server/wifi/ActiveModeWarden.java:116)是所有 WiFi 模式的总调度员——负责管理 STA、AP、P2P、NAN 等接口的生命周期,在创建新模式之前检查能否并发。WifiNative.isP2pStaConcurrencySupported()(WifiNative.java:3881)负责查询芯片固件,确认硬件是否支持 STA+P2P 同时工作。
Framework 层面的决策流程:
- 检查硬件能力——
WifiNative.isP2pStaConcurrencySupported()通过 Vendor HAL 查询固件的接口组合矩阵(iface_combinations),返回 STA+P2P 是否可以共存 - 如果支持 DBS 且 STA 和 P2P 目标信道在不同频段——直接允许,不需要协调
- 如果只有单射频——
ActiveModeWarden评估当前 STA 连接的重要程度(是否在传输数据、是否活跃),决定是否允许 P2P 抢占信道时间 - 如果当前 STA 信道和 P2P 目标信道相同——标注 SCC,不受并发限制
P2pStateMachine 在创建群组前通过 FrequencyConflictState 处理信道冲突场景——如果 STA 当前占用的信道和 P2P GO 目标信道不同且不支持 DBS,Framework 会暂停 P2P 建群流程,等待信道协调完成或 STA 释放当前信道。
4.3 QCOM:policy_mgr 的 DBS 检测与 NOA 调度
Framework 的决策最终要落到驱动侧执行——先看 QCOM 是怎么做的。QCOM 驱动使用 policy_mgr(Policy Manager)模块(components/cmn_services/policy_mgr/)来做并发决策。它不是一个简单的 "能不能并发" 的布尔判断,而是一个持续运行的决策引擎——在每次连接状态变化时重新评估并发策略。
policy_mgr 的核心能力:
DBS 检测——查询固件能力,判断当前平台是否支持 DBS。如果支持且 STA(2.4GHz)和 P2P(5GHz)在不同频段,policy_mgr 允许两个连接同时活跃,不需要 NOA(Notice of Absence,缺席通告)。
MCC/SCC 决策——当两个连接在同一个频段时,policy_mgr 尝试把它们移到同一个信道上(SCC),减少信道切换的开销。如果无法移到相同信道——强制进入 MCC 模式,使用 NOA 调度。
NOA 调度——当 GO 需要暂时离开去处理 STA 事务时,GO 通过 hdd_set_p2p_noa()(core/hdd/src/wlan_hdd_p2p.c:500)下发 NOA 参数。NOA 的完整路径在《P2P(六)Group Formation》第 7.3 节已经详细展开:Host(wpa_supplicant)通过 P2P_SET noa 命令指定 count、duration、interval,驱动负责下发 WMI 命令给固件,固件负责在指定时间窗口内暂停 GO 侧的 Beacon 和帧收发,切换到 STA 信道处理 STA 事务。
QCOM 的 START_AP 是同步的——qdf_wait_single_event() 阻塞等待固件确认 Beacon 已开始发射(《P2P(六)Group Formation》第 7.1 节)。这意味着 GO 启动后,Beacon 的可靠性是有保证的——但 NOA 窗口期间,Client 的帧发送仍然会被延迟。
把三种并发策略和 NOA 调度放在一张图上,对比关系就清楚了——上半是 MCC/DBS/SCC 三种策略的信道时间轴,下半是 GO 用 NOA 缺席窗口去 STA 信道时,P2P Client 撞上窗口发帧无人应答的场景:
4.4 MTK:接口组合矩阵与 P2P PS
MTK 平台的并发管理采用了不同的架构哲学——没有集中的 policy_mgr 决策引擎,而是通过接口组合矩阵声明硬件并发能力,配合 P2P Role FSM 和 P2P Power Save 机制来处理实际并发:
接口组合矩阵——MTK 在 gl_vendor.c:3956-4040 中通过 mtk_ifaces_combinations[] 结构体数组(元素类型 mtk_wifi_iface_combination,gl_vendor.h:614)预先声明了芯片支持的并发组合。其中 sta_p2p[] 数组(gl_vendor.c:3972)明确声明 "最多 1 个 STA + 1 个 P2P 接口" 可以共存,这条规则在 cfg80211 注册时提交给内核——wpa_supplicant 创建 P2P 接口前,内核已根据此矩阵判断是否允许。当实际运行中两个接口必须在不同信道上共存时,MTK 通过 time_slicing_duty_cycle_percent 字段(gl_vendor.h:1075)向用户空间报告每个接口被分配到的实际空口时间百分比——SCC/DBS 下是 100%,MCC 分时下小于 100%。
P2P Role FSM 管理角色迁移——从 Idle 到 GO/Client 的每个状态迁移都检查当前是否有活跃的 STA 连接。如果 STA 正在特定的信道上活跃,P2P FSM 会尝试在这个信道上启动 GO(SCC)以避免信道切换。
P2P PS(Power Save 模式)——当必须走 MCC(两个连接在不同信道)时,MTK 通过 priv_driver_set_p2p_ps()(gl_wext_priv.c:18099)设置 GO 的 Opportunistic Power Save 参数——CTWindow(Client Traffic Window,单位 ms)指定了 GO 在每个 Beacon 之后保持醒着的时间长度。GO 在需要去 STA 信道时,CTWindow 缩短或进入 PS 状态——Client 被告知 GO 进入了省电状态(通过 P2P 规范 v2.0 第 3.3.3 节的 OPPS 机制),不在这个期间发送数据。Client 侧收到 OPPS 通知后要做的是主动退避:它从每个 Beacon 的 P2P IE 里解析 CTWindow 与 OppPS 位,据此判断 GO 的清醒窗口,把自己的上行帧调度进窗口内发送——窗口外发的帧没人应答,只能留到下一个 CTWindow 重试。规范 3.3.3.1 明确承认这个代价:OPPS 的省电收益以增加 Client 传输时延为交换。
MTK 的 START_AP 是纯异步的——mtk_p2p_cfg80211_start_ap() 通过 mboxSendMsg() 把消息塞进队列就返回 0,固件实际处理在后台进行(《P2P(六)Group Formation》第 7.2 节)。这个差异在并发场景下影响更大:GO 还没真正准备好时,STA 侧可能已经开始通信——两个操作的时间窗口重叠在 MCC 模式下会导致 Beacon 延迟发射,Client 扫描不到 GO。
4.5 限制与妥协:应用层怎么感知到并发的影响?
无论 QCOM 还是 MTK,单信道下的 STA+P2P 并发都逃不过物理定律的约束——同一信道同一时刻只能有一个设备在发送。GO Client 能感知到的具体症状包括:
- P2P 数据传输吞吐量下降——因为 GO 在 NOA 或 PS 窗口期间不接收数据
- 延迟增加——Client 的数据帧可能在 GO 离开期间排队等待
- GO Beacon 丢失——Client 错过连续多个 Beacon(当 GO 在 STA 信道上处理较长的事务时)
- 上层 Socket 超时——TCP 连接在 GO 离开期间积累的 RTT 飙升可能触发重传超时
NOA 不是完美的。它只在 Beacon 里声明了 "GO 接下来会缺席哪些时间片",但 Client 无法知道 GO 离开的确切时刻——Client 可能恰好撞上 GO 缺席的窗口发帧,发出的帧石沉大海,只能靠 802.11 重传兜底。这个 "发出去没人应" 的空窗期是并发损耗的根本来源。
对 App 开发者来说,这些症状的应对策略是:如果知道设备可能同时运行 STA 和 P2P,降低对 P2P 链路的实时性期望,使用 Binder 或更大超时的 Socket,或通过 WifiP2pManager.requestConnectionInfo() 检查当前 groupOwnerAddress 和频段信息来判断是否处于并发状态。
要从架构上根治 "一个射频两个群" 的分时矛盾,就得靠 Wi-Fi 7 的 MLO(Multi-Link Operation)——让 STA 和 P2P 各占一条物理链路,从硬件层面把 "分时" 变成 "同时"(详见《连接(五)安全协议与 MLO》)。
这里能看清 NOA 与 MLO 的架构取舍本质:NOA 是协议层的软协作——GO 在 Beacon 里宣告缺席计划,Client 必须主动解析 NOA 属性并配合避开,但两者之间没有任何强制约束,Client 无法精确预知 GO 离开的瞬间,撞上窗口的帧只能靠 802.11 重传兜底,损耗是概率性的、无法根除;MLO 是物理层的硬并行——每条链路各配一套独立的射频收发(QCOM 的 mlo_connect() 逐条建立伙伴链路、MTK 的 CFG_SUPPORT_802_11BE_MLO 把 MLO 能力渗透进 AIS/SAA/AAA FSM),STA 与 P2P 不再共享任何时隙,"发出去没人应" 的空窗期从物理上消失。代价也很清楚:NOA 零硬件成本、牺牲吞吐;MLO 需要芯片多提供一套射频、增加功耗与成本。这正是 "一人分饰两角" 的极限:同一副身体同一时刻只能在一个群里说话。MCC 是赶场子轮班,DBS 是租了第二个场地,而 MLO 才让两个角色各拥有一条完整的链路,从此不再互相踩脚。
5 日志诊断 —— 出问题怎么查?
前面四节讲的是 "怎么用"——这一节讲 "怎么查"。P2P 出问题就像一场纠纷,排查就是去翻聊天记录——但聊天记录有四个版本(Framework / Supplicant / 驱动 / 固件),问题的根源可能藏在任何一层。本节给出四层日志关键字速查表和常见问题的诊断路径。
5.1 四层日志关键字速查
排障的第一步是知道去哪一层看什么。下表把四个层级的日志来源、关键字和排查内容列在一起:
| 层级 | 日志来源 | 关键字 / TAG | 查什么 |
|---|---|---|---|
| Framework | adb logcat | WifiP2pService、WifiP2pNative、P2pStateMachine | 状态转移(InactiveState -> GroupCreatingState)、API 调用成败、事件分发 |
| Supplicant | adb logcat(wpa_supplicant 日志转发到 logcat)或 wpa_cli | P2P-DEVICE-FOUND、P2P-GO-NEG-*、P2P-GROUP-STARTED、P2P-INVITATION-*、WPS-* | 协议事件(设备发现、GO 谈判、WPS 成败、组状态变化) |
| QCOM 驱动 | adb shell cat /proc/kmsg 或 dmesg | wlan、p2p、sap_fsm、HDD、WMI | WMI 命令下发 / 响应、SAP 状态迁移、NOA 参数、peer 管理 |
| MTK 驱动 | adb shell cat /proc/kmsg | P2P、P2P_FSM、P2P_DEV_FSM、RLM | P2P 状态机事件、Action 帧收发、信道切换 |
为什么按这四层查?因为每一层回答的是不同层面的问题——Framework 层看行为(用户做了什么、状态机是否按预期转移),Supplicant 层看协议(握手状态、事件是否发出),驱动层看硬件(射频参数、WMI 命令是否被固件接受),驱动之下还能下探固件看底层(芯片状态机的实际推进)。排障时从上往下逐层对照:先确认行为层正确,再下探协议、硬件、芯片,定位 "哪一层断掉了"。
这张图把上面这张表和这段排障思路合在了一起:左边箭头提示从上往下逐层对照,右侧每一层列出该层的日志来源和关键字——排查时照着这一列去 grep 就行。
常用诊断命令速查
在实际排障时,最先动手的工具不是日志而是命令行——Android 提供了一组无需 root 的 P2P 诊断命令,能直接触发 / 查询各层状态,实现在 WifiP2pShellCommand.java(packages/modules/Wifi/service/java/com/android/server/wifi/p2p/WifiP2pShellCommand.java:46,入口注释 "adb shell cmd wifip2p"):
| 命令 | 作用 | 对应源码位置 |
|---|---|---|
adb shell cmd wifip2p list-peers | 列出当前发现的 P2P 设备 | WifiP2pShellCommand.java:135 |
adb shell cmd wifip2p start-peer-discovery | 手动启动 Find | WifiP2pShellCommand.java:88 |
adb shell cmd wifip2p stop-peer-discovery | 手动停止 Find | WifiP2pShellCommand.java:123 |
adb shell cmd wifip2p start-service-discovery | 手动启动 Service Discovery | WifiP2pShellCommand.java:127 |
adb shell cmd wifip2p stop-service-discovery | 手动停止 SD | WifiP2pShellCommand.java:131 |
adb shell cmd wifip2p list-saved-groups | 列出已保存的 Persistent Group | WifiP2pShellCommand.java:292 |
adb shell cmd wifip2p delete-saved-group <id> | 删除指定 Persistent Group | WifiP2pShellCommand.java:303 |
adb shell cmd wifip2p create-group | 手动创建 Autonomous GO | WifiP2pShellCommand.java:174 |
adb shell cmd wifip2p remove-group | 手动拆除当前 Group | WifiP2pShellCommand.java:178 |
adb shell cmd wifip2p set-device-name <name> | 设置本机 P2P 设备名 | WifiP2pShellCommand.java:182 |
adb shell cmd wifip2p connect <address> | 手动发起连接(可带频段 / 方法参数) | WifiP2pShellCommand.java:369 |
排障思路:先 list-peers 看设备发现是否正常(对应四层里的 Find 层),再 start-service-discovery 看 SD 是否工作,list-saved-groups 确认 Persistent Group 凭证是否还在——用命令快速复现,再用上面的四层日志定位根源。比如 "重连不成功",先 list-saved-groups 确认凭证在不在,再查 supplicant 日志里的 P2P-INVITATION-* 事件。
Framework 层日志示例
Framework 层最值得盯的是状态转移方向和事件分发,logcat 里长这样:
1 | // logcat 输出示例: |
Framework 日志通过 WifiP2pServiceImpl 的 logd() 方法输出,关键信息是状态转移方向和处理的消息类型。
Supplicant 层事件字符串
Supplicant 的 P2P 事件统一定义在 src/common/wpa_ctrl.h 中,以宏字符串的形式存在,通过 wpa_msg() 发出:
| 事件宏 | 字符串值 | 触发时机 |
|---|---|---|
P2P_EVENT_DEVICE_FOUND | P2P-DEVICE-FOUND | 扫描发现设备 |
P2P_EVENT_GO_NEG_SUCCESS | P2P-GO-NEG-SUCCESS | GO 谈判成功 |
P2P_EVENT_GO_NEG_FAILURE | P2P-GO-NEG-FAILURE | GO 谈判失败 |
P2P_EVENT_GROUP_STARTED | P2P-GROUP-STARTED | Group 启动(含 IP、SSID、GO 角色) |
P2P_EVENT_GROUP_REMOVED | P2P-GROUP-REMOVED | Group 被移除 |
P2P_EVENT_INVITATION_RECEIVED | P2P-INVITATION-RECEIVED | 收到 Invitation |
P2P_EVENT_INVITATION_RESULT | P2P-INVITATION-RESULT | Invitation 结果 |
P2P_EVENT_PROV_DISC_PBC_REQ | P2P-PROV-DISC-PBC-REQ | PBC 方式 PD 请求 |
P2P_EVENT_SERV_DISC_RESP | P2P-SERV-DISC-RESP | SD 响应到达 |
P2P_EVENT_FIND_STOPPED | P2P-FIND-STOPPED | Find 停止 |
P2P_EVENT_PERSISTENT_PSK_FAIL | P2P-PERSISTENT-PSK-FAIL id= | Persistent Group PSK 失败 |
完整的事件列表在 wpa_ctrl.h 第 261-306 行,这些是 P2P 问题排查的核心依据。
QCOM 驱动关键日志
QCOM 使用分层日志宏(hdd_debug()、hdd_err()、sap_debug()、sap_err()、wmi_info())输出。关键消息包括:
1 | "P2P_SET GO noa: count=%d interval=%d duration=%d" // NOA 参数 |
MTK 驱动关键日志
MTK 使用 DBGLOG(P2P, level, ...) / DBGLOG(RLM, level, ...) 宏输出,日志级别包括 TRACE、INFO、STATE、WARN、ERROR。关键文件包括 p2p_dev_fsm.c(Device 侧的 P2P 状态机)、p2p_role_fsm.c(Role 切换状态机)、p2p_func.c(帧构建 / 解析工具函数)。
5.2 常见问题诊断
下面按出现频率从高到低列出四个最常见的 P2P 排障场景——发现不到设备、连接 / 建群超时、群组异常拆除、SD 返回异常。每个场景的检查清单都按 5.1 节四层日志表从上往下走:先看 Framework 行为,再下探 Supplicant 协议事件,最后查驱动。这四类问题刚好覆盖了 P2P 从 "找到设备" 到 "建群" 再到 "群组维持" 的完整生命周期,每份清单都是同一条自上而下的排查路径,只是各层关注点不同——Framework 层看行为、Supplicant 层看协议、驱动层看硬件。
问题一:发现不到设备
检查清单:
- Framework 日志——
WifiP2pService是否在 InactiveState 收到了DISCOVER_PEERS消息?WIFI_P2P_DISCOVERY_CHANGED_ACTION广播是否发出? - Supplicant 日志——是否有
P2P-DEVICE-FOUND事件?如果没有,说明 Find 过程压根没发现设备——检查 social channels(1/6/11)是否可用、本机 P2P 接口是否正常创建 - 驱动日志——QCOM:
wlantag 下是否有 scan 相关 WMI 命令?MTK:P2P_FSM是否有 scan 事件处理? - 硬件能力——是否支持 DBS?如果正在使用 STA 且不支持 DBS,P2P Find 需要在 STA 信道和 Social Channels 之间分时切换,可能导致发现延迟显著增加
问题二:连接 / 建群超时
检查清单:
- GO Negotiation 超时——supplicant 侧的
P2P_GO_NEG_CNF_MAX_RETRY_COUNT = 1(p2p_i.h:17),GO Neg Confirm 在收不到 ACK 时立即重发最多 1 次;整体 GO Neg 超时为 120 秒(p2p_go_neg_wait_timeout,p2p_go_neg.c:1316) - WPS 超时——P2P 规范 v2.0 第 3.1.4.1 节规定 Group Formation 不得超过 15 秒。WPS 超时后
WPS-TIMEOUT事件触发,调用wpas_p2p_group_formation_failed() - Invitation 超时——P2P 规范 v2.0 第 3.1.5.1 节规定 100ms 内必须收到 Response;如果收到
Fail: information is currently unavailable,需在 120 秒内收到对端重发的 Invitation Request(Invitation 超时 / 重试相关常量见 §5.3 速查表) - supplicant 日志中查找
P2P-GROUP-FORMATION-FAILURE——它后面跟着失败原因(WPS timeout、GO Neg 失败等)
问题三:群组异常拆除
检查清单:
- Client 主动离开——GO 侧 hostapd 收到 Disassociation 帧 →
p2p_group_notif_disassoc()(src/p2p/p2p_group.c:690)把成员从 group 移除;日志关键字:驱动侧AP_STA_DISCONNECTED事件、supplicant 侧P2P: Remove client消息 - GO 异常——检查 supplicant crash(
wpa_supplicant died日志)、驱动 crash(QCOM SSR recovery 触发、MTK L1/L2/L3 恢复流程触发) - Beacon 丢失——Client 连续收不到 GO 的 Beacon → 判定 GO 已离开 → 主动断连。驱动日志中查找 Beacon timeout 相关消息
问题四:SD 超时或返回异常
检查清单:
- 检查
sd_frag_id——如果 SD 响应超过 1400 字节(2.4/5GHz)或 928 字节(60GHz),需要分片。确认sd_frag_id是否在递增——如果不递增说明 Comeback 交换卡住了 - 检查
sd_resp缓冲区被覆盖——如果同时有多个 SD 查询,p2p->sd_resp单缓冲区可能被覆盖(见 3.4 节),导致前一个查询的响应片段不完整 - supplicant 日志——查找
"Drop previous SD response"消息——这说明 sd_resp 缓冲区被覆盖,前一个 SD 响应被丢弃 - Framework 日志——
P2P_SERV_DISC_RESP_EVENT(WifiP2pMonitor 事件常量 = BASE+38)是否被触发
5.3 P2P 状态与超时常量速查
在翻四层聊天记录之前,先对照相亲角管理处自己定下的 "时限账本"——每个环节该等多久、重试几次,都记在这张速查表上:
| 常量 | 值 | 定义位置 | 说明 |
|---|---|---|---|
P2P_GO_NEG_CNF_MAX_RETRY_COUNT | 1 | p2p_i.h:17 | GO Neg Confirm 最大重试次数 |
| Listen busy-loop 延迟 | 100ms | p2p.c:3997 | Listen 操作未能启动时延迟 100ms 避免忙循环 |
| GO Neg overall timeout | 120s | p2p_go_neg.c:1316 | 整个 GO Negotiation 的最大允许时间 |
P2P_SCAN_TIMEOUT | 35s | p2p.c:47 | P2P 扫描阶段的最大时长 |
| Group Formation timeout | 15s | P2P 规范 v2.0 第 3.1.4.1 节 | WPS 配网必须在 15 秒内完成 |
| Invitation Response wait | 100ms | P2P 规范 v2.0 第 3.1.5.1 节 | 发送 Invitation Request 后等 Response 的最长时间 |
WPS_PBC_WALK_TIME | 120s | wps_defs.h:380 | PBC 模式下的行走时间(按钮按下后有效窗口) |
| PD Response wait | 100ms | P2P 规范 v2.0 第 3.2.3 节 | Provision Discovery Response 等待时间 |
| GAS Comeback Delay | 1 TU | P2P 规范 v2.0 Appendix C.2 | 如果分片,Initial Response 中设置 Comeback Delay=1 |
这张速查表就是相亲角管理处的 "聊天记录速查手册"——遇到纠纷,先从 Framework 层的行为记录翻起,再逐层下探到协议(Supplicant)、硬件(驱动)、芯片(固件),定位断掉的那一环,然后对症下药。
6 系列总结 —— 相亲角的七课
P2P 系列 7 篇文章,从相亲角的管理处挂牌《P2P(一)》《P2P(二)》(初始化)、到逛一圈收集名单《P2P(三)》(设备发现)、约出来谈谁当老大《P2P(四)》(GO Negotiation)、对暗号验身份《P2P(五)》(Provision Discovery + WPS)、发钥匙建群《P2P(六)》(Group Formation)、再到高级玩法《P2P(七)》(本篇),完整追踪了 Wi-Fi Direct 从无到有的全过程。
6.1 七篇回顾
| 篇章 | 核心问题 | 代码跨度 | 关键设计决策 |
|---|---|---|---|
| 初始化 | 相亲角怎么开张? | Framework 20 状态机 + supplicant p2p_init + QCOM/MTK 驱动初始化 | StateMachine 的串行消费模型、eloop 事件驱动、p2p_data 回调注册模式 |
| 设备发现 | 怎么发现周围有谁? | Framework 发现路由 + supplicant Find(Listen/Scan 交替)+ 驱动 Probe 帧收发 | Social Channels 1/6/11、P2P IE + WSC IE 双层身份、Listen/Scan 分时 |
| GO 谈判 | 谁当老大?在哪碰头? | Framework GroupNegotiationState + supplicant 三次握手 + GO Intent 决策 | p2p_go_det() 意愿值对比、十级信道选择的兜底策略、Autonomous GO 绕过谈判 |
| PD + WPS | 怎么证明不是骗子? | Framework ProvisionDiscoveryState + supplicant PD 帧 + WPS EAP-WSC 八轮 | PIN/PBC 方法协商、EAP-WSC DH 密钥交换、15 秒超时窗口 |
| 建群 | 怎么正式开张? | GO Beacon → Auth/Assoc → 四次握手 → DHCP → GROUP_STARTED | GO 先行发射 Beacon、WPS 基于已有 Beacon 执行、NOA 调度 GO 离场 |
| 本篇 | 高级特性 + 运维 | Autonomous GO + Persistent Invitation + SD + 并发 + 日志诊断 | 跳过谈判、秒重连、预关联查询、分时复用、四层排障 |
6.2 关键设计思想回顾
回过头看这七篇文章,有几个设计思想贯穿始终——它们不是任何一行代码直接写出来的,但理解它们才能真正理解 P2P 协议栈:
模块化回调设计。P2P 核心层(src/p2p/)不知道 wpa_supplicant 的存在——它通过 struct p2p_config 中的函数指针(cfg->send_action、cfg->go_neg_completed、cfg->prov_disc_resp_cb)向外面的世界回调。这意味着 P2P 核心层可以被任何上层框架使用——Android 的 wpa_supplicant、Linux 桌面上的 wpa_supplicant、嵌入式设备的私有实现,都可以注册自己的回调。这是真正的 "协议引擎" 设计——不依赖任何特定的平台。
状态机层级。Framework 的 P2pStateMachine(20 个子状态)、Supplicant 的 P2P 状态(P2P_IDLE → P2P_SEARCH → P2P_GO_NEG → P2P_PROVISIONING)、驱动的 SAP FSM 和 P2P Role FSM——三层状态机各管各的,通过消息/事件/命令异步通信。这个三层异步架构的好处是:每一层可以独立演进(Framework 可以加新状态不影响 supplicant),坏处是出问题时排查困难——状态不一致时需要在三层之间对照时间线。
顺着这个系统视角还能回答一个贯穿全文的问题:每个子话题完成后,谁负责清理资源? 答案是 Framework 的 P2pStateMachine 承担了唯一的资源所有权。WifiP2pService(SystemService 外壳)在 onStart() 里 publishBinderService(WIFI_P2P_SERVICE, mImpl) 把 WifiP2pServiceImpl 暴露给 App(WifiP2pService.java:44),后者构造时 new P2pStateMachine(...).start()(WifiP2pServiceImpl.java:755)——此后所有 P2P 操作(建群、邀请、SD、并发)都在这台状态机上跑。
WiFi 关闭时,状态机收到 DISABLE_P2P 消息,P2pEnabledState 执行 mWifiNative.teardownInterface()(WifiP2pServiceImpl.java:3261)拆掉 P2P 接口,经 P2pDisablingState 回到 P2pDisabledState,App 的 Channel、WifiNative、HAL 代理随之释放。群组拆除则由 GroupCreatedState 处理 REMOVE_GROUP:mWifiNative.p2pGroupRemove()(WifiP2pServiceImpl.java:6143)→ AIDL removeGroup() → supplicant P2P_GROUP_REMOVE 删掉 GO 虚拟接口。所以 "谁建谁拆" 的闭环永远在 Framework 状态机里完成——驱动层只执行固件指令,不持有资源所有权。
协议分层。P2P 站在 IEEE 802.11 的肩膀上——它不重新定义认证(用 802.11 Auth)、关联(用 802.11 Assoc)、安全密钥交换(用 WPA2 四次握手)、IP 分配(用 DHCP)。P2P 只在 "发现和群管理" 层面定义自己的协议——Device Discovery、Service Discovery、GO Negotiation、Provision Discovery、Invitation。这种 "借力不重复" 的设计让 P2P 利用了现有基础设施的成熟实现——加密、密钥派生、帧加密,全都由 WPA2 的已有代码完成,P2P 不需要再写一遍。
双平台差异本质。QCOM 和 MTK 在 P2P 实现上的差异不是 "谁更优",而是设计哲学的差异——QCOM 倾向于同步等待(qdf_wait_single_event 让 hostapd 等到固件确认)、集中式策略引擎(policy_mgr 统一做并发决策);MTK 倾向于异步返回(mbox 消息发出即返回)、显式状态机(P2P Role FSM 的每个状态迁移有明确入口和出口)。这两种风格在嵌入式驱动设计中各有拥趸——QCOM 的同步模型简化了上层逻辑(hostapd 不需要处理 "命令已发送但还没生效" 的中间状态),但阻塞了调用线程;MTK 的异步模型不阻塞线程,但把 "时间窗口内状态可能不一致" 的复杂度交给了上层。
6.3 接下来
P2P 系列到此完结。相亲角的故事讲完了——从挂牌、逛名单、谈条件、对暗号、建群到高级玩法,Wi-Fi Direct 的每一层代码都走了一遍。但 "配网" 本身还有另一个主角——DPP(Easy Connect,设备配网协议)。DPP 不需要输密码、不需要按按钮,扫个二维码就能配网——它在设计哲学上与 WPS(P2P 配网的底层协议)完全不同。下一篇,我们从 DPP 的 QR Code 出发,追踪一场没有密码的配网。
源码仓库:
- AOSP wpa_supplicant 8: https://w1.fi/wpa_supplicant/
- AOSP packages/modules/Wifi: https://android.googlesource.com/platform/packages/modules/Wifi
- QCOM qcacld-3.0: https://source.codeaurora.org/quic/la/platform/vendor/qcom-opensource/wlan/qcacld-3.0
- MTK kernel_modules-connectivity-wlan-core-gen4m (MTK 内核模块仓库)
相关规范:
- Wi-Fi Alliance, "Wi-Fi Direct Specification v2.0"
- Wi-Fi Alliance, "Wi-Fi Simple Configuration Technical Specification v2.0.2"
- IEEE 802.11-2020, Section 9.4.2.92 (Advertisement Protocol element), Section 11.22.3 (GAS procedures)