自研远程桌面连接工具技术选型

0 阅读12分钟

上篇看了全貌,这篇钻进实现的细节里做一件最容易被跳过、却最影响成败的事:技术选型。很多项目不是死在"不会写",而是死在"选错了轮子,后面一路填坑"。ALSPD-DESK 的选型几乎每一项都来自实测,而不是拍脑袋。下面把六个关键决策一项项摊开讲理由和数字。

一、语言:三端为什么都用同一门语言

三端——被控端、控制端、中继——全部用 Python(开发基于 3.11+,实际开发环境是 3.13)。为什么不用 C++ 追求极致性能、不用 Rust 追求安全、不用 Go 追求并发?

核心原因就一条:加密、协议、连接这三块逻辑,三端必须完全一致,而同一门语言能最大化复用。端到端加密的密钥派生、消息的封包解包、握手的先后顺序,只要两端有一处写法不一致,连不上还是小事,更可怕的是"连上了但密钥对不上"或者"被重放攻击钻空子"。把公共逻辑放在一个公共模块里,三端直接 import,等于用同一份代码消除了最容易出现不一致的角落。

另外一个现实原因:Python 在 Windows 桌面端生态太成熟了。屏幕采集要 dxcam、mss,键鼠注入要 pywin32,界面要 PySide6,异步 WebSocket 有现成的库——这些都不是"理论上能调",而是"项目源码里已经调通并实测过"。对一个个人项目来说,能少造轮子就少造,把精力留给真正该打磨的加密和传输。

性能够不够?后面会看到,单帧处理链路实测在 20 多毫秒,目标是 20fps,余量充足;瓶颈从来不在 Python 解释器,而在网络带宽和编码。所以"为性能换语言"在这项目里不成立。

为什么不是 Go 或 Rust?说句实在话,Go 的并发模型很适合写网络服务,Rust 的内存安全也诱人,但这两门语言在 Windows 桌面端的"鼠标键盘屏幕"生态远没有 Python 成熟——dxcam、mss、pywin32、PySide6 这些轮子,Python 这边是直接 import 就能用,换过去往往要自己造或半造。对个人项目,"能直接用的轮子"比"理论上更优的语言"重要得多。何况三端共用一门语言带来的加密/协议一致性收益,已经盖过了语言本身的性能差异。选型不是选"看起来最厉害"的,是选"在这个场景里最不给自己添堵"的。

二、传输:为什么是 WebSocket over TLS

传输层选的是"WebSocket 跑在 TLS 之上",也就是 wss://。为什么不是裸 TCP,也不是 HTTP 轮询?

先说 WebSocket 本身的好处:

  • 全双工:画面从公司往家里推、键鼠从家里往公司发,是两条同时进行的流。WebSocket 一个连接上双向随便发,不用像 HTTP 那样请求-应答来回倒。
  • 消息边界天然清晰:WebSocket 天然按"帧/消息"切分,项目里每条消息就是一段完整的内容(类型字节 + 载荷),不用自己写分包逻辑。裸 TCP 你得自己数长度、拼包、处理半包,麻烦且易错。
  • 库成熟:异步 WebSocket 客户端/服务端都有现成实现,握手、ping/pong 保活、关闭码都帮你管好了。

再叠一层 TLS,是为了"穿墙"和"不显眼"。公司网络通常只放行加密流量,一个明文连出去的 socket 大概率被拦或被审计;而把流量包成 TLS,看起来就是正常的 HTTPS,既能走通 443,又不至于在日志里太扎眼。前面说过,TLS 在这里是"伪装层",真正锁内容的是应用层端到端加密;两层职责清楚,互不抢戏。

那为什么不用 QUIC 或者 HTTP/3?QUIC 跑在 UDP 上,443 端口的 UDP 在公司网络里常常被直接拦掉,反而比 TCP 上的 WebSocket 更难穿墙;而本项目要的就是"混在 HTTPS 流量里最不显眼",TCP + TLS + WebSocket 正好契合。HTTP/3 同理,且它的库成熟度在这个场景里没有明显优势。选最容易被放行、库最成熟的,而不是最新的,是工程上的成熟,不是保守。远程桌面的传输层要的是"稳稳地穿过公司防火墙",不是"用上最潮的协议"。

三、端口:为什么主用 443 还要带回退链

先破除一个常见误解:端口号不影响安全强度。安全性来自加密算法和密钥,不来自端口。把 443 换成 8080,加密强度一模一样。端口真正只影响两件事——能不能穿公司防火墙、会不会被注意到。

实测的端口对照是这样的:

端口穿防火墙隐蔽性安全性
443几乎必放行就是标准 HTTPS,最泯然众人同样强
8443多数放行HTTPS 备用端口,常见同样强
80几乎必放行明文 HTTP 端口上跑 TLS,略怪同样强
8080多数放行HTTP 备用端口,非常常见同样强
22看策略SSH,可能被重点审计同样强
随机高位端口常被直接拦容易被当成木马回连同样强

有个反直觉的点值得一提:安全设备有一条经典规则——"往非标准端口发持续加密流量 = 木马回连的典型特征"。所以随机高位端口反而更容易被标记,443 混在海量正常 HTTPS 里最难分辨。

因此主用 443,但项目不是"只试 443"。它维护一条回退链,按顺序逐个尝试,第一个连上的就生效:443 → 8443 → 80 → 8080。为什么非要回退?因为真实环境千奇百怪:公司防火墙可能只放行其中几个;443 在 VPS 上也可能被别的服务占用(实测里 80 端口就被占用过,连不上)。有回退链在,哪条路通走哪条,连接才稳。回退不是越多越安全,而是"按穿墙成功率排序,逐一兜底"。

补一个实测结论,让回退链更有说服力。在真实公司网络的侦察里:443 + TLS + 证书指纹匹配——可用,且没遇到中间人;8443 和 8080——被公司防火墙直接超时;80——被 VPS 上别的服务占用了,连不上。也就是说,443 不是"理论最优",是"实测唯一稳定可用"的那一个。回退链存在的意义,就是当某天 443 也因故不通时,还有 8443 / 80 / 8080 能试,而不是让你干瞪眼。但日常绝大多数情况,第一条 443 就通了。

有人会问:那直接走 SSH 隧道(22 端口)不也一样加密吗?能,但代价不划算——SSH 隧道要你先有一台开着 22、且你能登进去的服务器,还要管理密钥、保持隧道,复杂度一下子上去了;而且很多公司网络对 22 出访是重点审计的,未必比 443 稳。本项目要的是"双击就能连、不用我管隧道",所以把隧道那层也省了,直接 wss 跑 443。

四、采集:为什么选 DXGI(桌面复制)

屏幕采集是整条链路的第一环,直接决定了"画面新不新、卡不卡"。项目在 Windows 上首选 DXGI 桌面复制(通过 dxcam 库),并保留 GDI(通过 mss 库)作为回落。实测数字最能说明问题:

后端有新帧时画面静止时
DXGI(dxcam) ✅ 首选4.65 ms0.11 ms
GDI(mss)回落33 ms33 ms(无论是否变化都要付)

DXGI 比 GDI 快 7 倍;更关键的是画面静止时便宜 300 倍。为什么这个"静止时"的数字这么要命?因为办公场景——打字、看文档、写代码——大部分时间画面根本没动。DXGI 在画面无变化时 grab() 直接返回空,几乎零开销;而 GDI 不管你动没动,每帧都要付 33ms。对一天到晚大部分时间静止的桌面,DXGI 是决定性优势。

DXGI 为什么这么快?因为它走的是 Windows 的"桌面复制"接口,直接从显卡的合成表面拿画面,不经过 CPU 截屏那套"把显存拷出来"的流程,所以静止时几乎不费事。代价是它对环境挑剔——这也就是它两个硬限制的来源:一是每个显示器只允许一个复制器,被录屏软件、截图工具占用时会初始化失败;二是 RDP 远程会话下不支持。所以回落不是"备胎心理",是真实会踩到的坑——而且项目在回落时会明确提示"只是慢约 7 倍,不影响使用",而不是悄悄降级让你一头雾水。

补充一句合规与联动:采集与键鼠注入都发生在公司电脑上的 Agent 一侧。如果你要在公司设备部署,先确认公司的 IT 与安全政策,并且只在本人拥有、或已获得明确授权的设备上运行;首次使用建议保持只读模式,确认链路稳定并亲手验证过急停机制后再开启注入。

顺带说一个和采集联动的取舍:项目默认对画面做"原生分辨率"采集(target_width 为 0),不做激进降采样。原因来自带宽实测——办公场景(打字、看文档)画面大部分静止,更新包中位只有 3.3 KB,折合 0.54 Mbps,低得惊人;只有全屏滚动才吃带宽。既然带宽根本不是瓶颈,就没必要为了省带宽把文字压糊,原生分辨率下文字最清晰,办公体验最好。降采样作为可选项保留、默认关着,是把"清晰度"置于"省带宽"之前的一个有数据支撑的决定。

五、注入:SendInput + 扫描码 + 归一化坐标

控制端要把鼠标键盘"打"回公司电脑,用的是 Windows 原生的 SendInput API,而不是自己模拟驱动或装个键盘钩子。这里每一项都有讲究:

  • 鼠标用绝对坐标 + 虚拟桌面(MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK):绝对坐标保证多显示器也能落到正确位置,虚拟桌面标志还能绕过 Windows"提高指针精确度"带来的加速度干扰,点哪儿是哪儿。
  • 键盘发扫描码而非虚拟键码:扫描码是物理按键的编码,发出去后由目标机器按它自己的键盘布局解释。这样你在家里按"A",目标机器上也是它布局里的"A",不会因为两边布局不同就串键。
  • 坐标归一化到 0~1:控制端发的是"屏幕宽高的比例",而不是像素。好处是——无论 Agent 端是否降采样、是否开了 DPI 缩放,都能正确还原回真实位置。采集降不降分辨率,都不影响你点的准不准。
  • 点击前必须先移动光标:否则会在旧位置点一下。所以一次鼠标点击,项目其实是发了"移动 + 按下/抬起"两条输入,保证落点在最新位置。

这些选择凑在一起的目标只有一个:让远程操作"像本地一样准",同时尽量不侵入系统(不装钩子、不拦输入)。

为什么不更进一步、用驱动级注入去追求极致精度?因为驱动级注入往往需要内核驱动或管理员权限,还要面对签名、杀软拦截那一整套麻烦,代价远大于那一点点精度收益。SendInput 是用户态 API,不需要提权、不需要装驱动,和项目"免管理员权限"的整体取向一致。对一个办公远程桌面,"够准、且完全不碰系统底层"比"极其精准、但要改系统"划算得多。这也呼应了前面架构篇里"被控机不需要管理员权限"的承诺——注入这一层,同样守住了它。

六、打包:为什么用 PyInstaller

三端都是 Python,但使用的人(未来的你、或你家里那台电脑)大概率不想装一套 Python 环境。所以项目用 PyInstaller 把代码打成 Windows 可执行文件:

  • 被控端 Agent 用"目录式"打包(onedir),因为要带着一整个运行目录一起拷走,不能只拷一个 exe;
  • 控制端 Viewer 用"单文件"打包(onefile),拷回家双击就能用,免装 Python。

为什么是 PyInstaller 而不是别的?它在 Windows + Python 这条线上生态最成熟,支持把动态加载的扩展一并收集进来(项目源码里针对 dxcam 的编译扩展、comtypes、python-socks 等都做了 --collect-all 处理),还自带 --selftest 自检,能在"没连网、没动键鼠"的情况下提前抓出"某个模块没打进去"这类只在运行时才暴露的坑。

打包过程确实踩过坑(比如漏打扩展导致运行时报"没有 cv2"),但这些坑已经以构建参数和自检的形式被填平了,不构成换工具的理由。对"个人自用、要便携、要免安装"这个目标,PyInstaller 是当下最贴合的一档。

把上面六项选型收个尾,一眼看全:

决策项选择一句话理由
语言Python 三端统一加密/协议复用,Windows 桌面生态成熟
传输WebSocket over TLS全双工、消息边界清晰、易穿墙
端口主 443 + 回退链穿墙最稳、最不显眼、实测唯一稳定可用
采集DXGI 首选 + GDI 回落静止 0.11ms,比 GDI 快 7 倍、静息便宜 300 倍
注入SendInput + 扫描码 + 归一化用户态、多屏准、尊重目标键盘布局
打包PyInstaller便携、免装 Python、自带自检

选型的本质,从来不是"哪个最牛",而是"哪个最贴合场景、最不给自己挖坑"。上面六项,每一项背后都是一次实测或一个真踩过的坑——这也是为什么这个项目敢说"核心目标已达成并跑过真实公网验证",而不是停在设想里。