第 4 章:ADB 与隧道——谁监听,谁连接
“server” 描述的是业务角色,不一定描述网络角色。scrcpy 默认使用
adb reverse时,桌面客户端监听 TCP,Android server 反而主动连接;回退到adb forward后角色才翻转。
一、ADB 在 scrcpy 中的三种职责
ADB 不只是“传输视频的通道”,它先后承担:
- 发现与选择设备:解析
adb devices -l; - 部署与启动:把 server 推到
/data/local/tmp,执行app_process; - 端口隧道:在电脑 TCP socket 与设备 abstract local socket 之间转发字节。
一旦 socket 建好,音视频包不会经过 adb shell 的标准输入输出,而走由 ADB daemon 维护的透明隧道。
二、设备选择不是“取第一台”
server 线程先显式执行 adb start-server,再根据以下优先级构造 selector:
--serial指定的序列号;--select-usb;--select-tcpip;- 环境变量
ANDROID_SERIAL; - 未指定时要求候选中只有一台可用设备。
选择逻辑集中在 run_server()。设备类型并不查询额外属性,而由 serial 形态推断:emulator- 前缀、包含 : 或 adb-tls-connect 的视为非 USB,见 adb_device.c。
这说明“serial”是 ADB 连接标识,不必是硬件序列号。
三、服务端如何被部署和启动
客户端将构建好的 scrcpy-server 推到固定设备路径,再组装类似命令:
adb -s <serial> shell
CLASSPATH=/data/local/tmp/scrcpy-server.jar
app_process / com.genymobile.scrcpy.Server 4.1
scid=<hex> video_codec=... audio=...关键代码在 execute_server()。参数不通过 JSON 或环境块传递,而是紧凑的 key=value argv。这样无需额外解析依赖,也避免标准流与媒体流混用。
参数注入防线
用户提供的 encoder、crop、codec options 等最终进入 shell 命令。源码的 validate_string() 禁止空格、引号、重定向、命令替换和换行等特殊字符,见 server.c 193–205 行。
这不是完美 shell escaping,而是更保守的输入域缩减。原因是 Windows 的命令构造无法共享完全一致的 quoting 规则。
四、唯一 socket 名
每次运行生成一个随机 31 位 scid。桌面端创建 scrcpy_<8位十六进制>,Android 端用同一规则生成 abstract socket 名,见 DesktopConnection.getSocketName()。
选择 31 位而非完整 32 位,是为避免 Java 侧带符号整数问题。唯一名称允许同一设备上并行运行多个 scrcpy 实例,并让旧进程或残留 tunnel 不会窃取新连接。
五、默认路径:adb reverse
adb reverse localabstract:<name> tcp:<port> 的效果,是设备连接 local abstract socket 时,字节被转到电脑的 TCP 监听端口。scrcpy 的执行顺序是:
默认让桌面端先监听有重要优势:在启动 Android 进程前,接收端已准备好,不需要设备端反复探测“server socket 是否出现”。源码注释明确说明了这一点,见 adb_tunnel.c。
端口范围而非固定端口
客户端默认在 27183–27199 中尝试。每个端口都先注册 reverse,再在本地 bind/listen;如果 bind 失败,则移除刚建立的 reverse 后尝试下一端口。这个顺序避免留下错误映射。
六、回退路径:adb forward
某些连接场景不支持 reverse,或用户显式要求 forward。此时:
隧道选择逻辑见 sc_adb_tunnel_open(),Android 两种分支见 DesktopConnection.open()。
为什么有 dummy byte
forward 模式里,TCP connect() 成功只说明 ADB 转发端口存在,不保证 abstract socket 后面的 Java server 已准备好。设备端在第一条实际 socket 上立即写一个 dummy byte;客户端连接后先读这一字节,才能确认链路尽头真的有人监听。
只有第一条启用通道写 dummy byte,因为其余连接发生时 server 已进入 accept 循环。若 video 禁用,则 audio 成为 first socket;video/audio 都禁用时 control 是 first socket。这个“first”由能力组合动态决定。
七、三条连接如何无标签配对
连接本身不发送“我是 video”标签。双方依靠固定顺序配对:
- video(若启用);
- audio(若启用);
- control(若启用)。
这是一种隐式协议。好处是零额外握手;风险是两端 options 必须一致。客户端正是同一个 sc_server_params 同时决定服务端 argv 和本地 socket 分配,因此通常不会漂移。
连接完成后,若 control 存在,客户端先把 TCP_NODELAY 应用到 control socket,降低小消息延迟;然后从第一条 socket 读取固定长度设备名。对应逻辑在 sc_server_connect_to()。
八、TCP/IP 设备切换
--tcpip 有两类语义:
- 已知地址:直接
adb connect ip[:port]; - 未知地址:从当前 USB 设备读取 IP,执行
adb tcpip 5555,等待属性生效,再连接。
源码不会执行固定 sleep 后盲连,而是轮询 service.adb.tcp.port,最多等待约 10 秒,见 wait_tcpip_mode_enabled()。已知地址前缀 + 还表示先断开旧连接再重连。
九、隧道何时关闭
隧道只负责建立连接。三条 socket 一旦被 accept/connect,ADB 转发规则可以立即移除,现有连接仍保持。sc_server_connect_to() 不论成功失败都会离开时关闭 tunnel,避免端口映射长期泄漏。
运行结束时真正要中断的是已建立 socket,而不是 tunnel 规则。server 线程依次 net_interrupt() video/audio/control,让各自阻塞读取醒来,再等待 Android 进程退出。
十、设计取舍
| 选择 | 收益 | 代价 |
|---|---|---|
| reverse 优先 | listener 可在设备进程前就绪 | 部分 ADB 场景不支持 |
| forward 自动回退 | 兼容性更高 | 网络角色翻转,需 dummy byte |
| 固定连接顺序 | 零握手开销 | 双端配置必须严格一致 |
| 每实例随机 socket 名 | 支持并行、隔离残留 | 必须传递 scid |
| 建链后移除 tunnel | 不污染 ADB 映射 | 生命周期逻辑更细 |
十一、本章小结
ADB 层解决的是部署、发现和透明建链,媒体协议从 socket 建成后才开始。下一章逐字节拆解这些 socket 上真正流动的内容,并说明为什么媒体、控制和设备消息采用三种不同 framing。