Skip to content

第 4 章:ADB 与隧道——谁监听,谁连接

“server” 描述的是业务角色,不一定描述网络角色。scrcpy 默认使用 adb reverse 时,桌面客户端监听 TCP,Android server 反而主动连接;回退到 adb forward 后角色才翻转。

一、ADB 在 scrcpy 中的三种职责

ADB 不只是“传输视频的通道”,它先后承担:

  1. 发现与选择设备:解析 adb devices -l
  2. 部署与启动:把 server 推到 /data/local/tmp,执行 app_process
  3. 端口隧道:在电脑 TCP socket 与设备 abstract local socket 之间转发字节。

一旦 socket 建好,音视频包不会经过 adb shell 的标准输入输出,而走由 ADB daemon 维护的透明隧道。

二、设备选择不是“取第一台”

server 线程先显式执行 adb start-server,再根据以下优先级构造 selector:

  1. --serial 指定的序列号;
  2. --select-usb
  3. --select-tcpip
  4. 环境变量 ANDROID_SERIAL
  5. 未指定时要求候选中只有一台可用设备。

选择逻辑集中在 run_server()。设备类型并不查询额外属性,而由 serial 形态推断:emulator- 前缀、包含 :adb-tls-connect 的视为非 USB,见 adb_device.c

这说明“serial”是 ADB 连接标识,不必是硬件序列号。

三、服务端如何被部署和启动

客户端将构建好的 scrcpy-server 推到固定设备路径,再组装类似命令:

text
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 的执行顺序是:

电脑:先创建 TCP listener(27183–27199 之一) → 注册 adb reverse → 启动 Android Server → Android:依次 connect(video/audio/control) 到 abstract socket → ADB 把连接送到电脑 listener → 电脑按相同顺序 accept

默认让桌面端先监听有重要优势:在启动 Android 进程前,接收端已准备好,不需要设备端反复探测“server socket 是否出现”。源码注释明确说明了这一点,见 adb_tunnel.c

端口范围而非固定端口

客户端默认在 27183–27199 中尝试。每个端口都先注册 reverse,再在本地 bind/listen;如果 bind 失败,则移除刚建立的 reverse 后尝试下一端口。这个顺序避免留下错误映射。

六、回退路径:adb forward

某些连接场景不支持 reverse,或用户显式要求 forward。此时:

Android:LocalServerSocket 监听 abstract socket → 桌面:通过 adb forward 暴露为 localhost TCP 端口 → 桌面依次 connect(video/audio/control) → Android 按相同顺序 accept

隧道选择逻辑见 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”标签。双方依靠固定顺序配对:

  1. video(若启用);
  2. audio(若启用);
  3. 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。

基于 scrcpy 4.1 源码的独立学习笔记