ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用Delphi手写远程桌面:屏幕采集、压缩传输与自定义协议实现

用Delphi手写远程桌面:屏幕采集、压缩传输与自定义协议实现 简介这是一份面向 Delphi 学习者的远程桌面与远程控制程序源代码适合想深入网络编程、Socket 通信及桌面控制的开发者参考。程序整体分为服务端和客户端服务端负责被控端画面采集与指令执行客户端负责发起连接、实时显示画面与发送控制指令其中部分模块结合 C 完成展现了跨语言协作的工程思路。代码采用分块传输算法与传统的隔行扫描方式有所不同画面更新效率和带宽占用上具备一定改进阅读后能理解远程控制的核心机制。资源包为 RAR 压缩文件约 1.15MB内容紧凑便于本地解压研读。目前已有 749 人学习下载在同类源码中具有一定参考度。通过这份源码读者可学到远程桌面通信框架、图像分块编码、指令交互流程以及客户端与服务端的协作模式也能借鉴作者对算法迭代的思考是一份适合进阶学习的实战代码资料。1. 方案选型手写远程桌面为什么不直接调系统RDP接口如果你接到一个“写个远程控制小程序”的需求第一反应多半是调用Windows自带的RDP ActiveX控件。网上也有大量封装好的代码段拖个控件、填上IP几行就能连上。但用Delphi手写一套远程桌面程序的人往往一开始就放弃了这条捷径原因很实际RDP ActiveX做的是“标准远程桌面协议”它要求目标机器开放3389端口需要操作系统级别的会话支持而且界面风格、授权机制、外网穿透方式全部被系统绑定。你没法轻易给它加“多用户同时观看”“屏幕标注”“文件拖拽传输”这类自定义功能更没法裁剪掉多余协议开销让它在一个极低带宽的弱网环境里跑起来。我选择自己写协议核心原因只有两个第一要拿到全量控制权。远程控制涉及屏幕采集、图像编码、输入回控、剪贴板同步、文件传输等模块每一步都要能按需定制。第二要能跨平台部署、免安装运行。目标机器可能没有管理员权限也可能不允许装驱动或系统服务一个纯绿色exe、直接双击就跑的方案最省心。三条技术路线概览如下方案优点难点适合场景RDP ActiveX封装开发量小稳定绑定系统会话扩展难外网穿透麻烦局域网快速运维VNC类开源库移植协议成熟社区资料多Delphi对接C库繁琐像素格式转换绕通用跨平台远程控制自研采集自定义协议完全可控裁剪灵活需要自己处理压缩、传输、回控全链路源码学习、定制功能最终我采用“BitBlt屏幕采集 zlib压缩 自绘输入回控 自定义TCP协议”的组合。这个方案不依赖任何第三方商业控件Delphi 7到Delphi 12都能直接编译单exe即可运行非常适合作为学习和二次开发的基座。2. 核心链路源代码拆解屏幕采集、压缩传输与回控实现2.1 屏幕采集从桌面窗口定位到像素数据远程桌面的源头是屏幕画面。Windows下抓屏最经典的是BitBlt通过获取桌面窗口的设备上下文DC把屏幕像素复制到内存位图再转换为原始字节流。Delphi里这个流程非常直接procedure CaptureScreen(var Bitmap: TBitmap); var ScreenDC: HDC; MemDC: HDC; begin ScreenDC : GetDC(0); // 获取整个屏幕的设备上下文 try Bitmap : TBitmap.Create; Bitmap.Width : ScreenWidth; Bitmap.Height : ScreenHeight; Bitmap.PixelFormat : pf24bit; MemDC : CreateCompatibleDC(ScreenDC); try SelectObject(MemDC, Bitmap.Handle); BitBlt(MemDC, 0, 0, Bitmap.Width, Bitmap.Height, ScreenDC, 0, 0, SRCCOPY); finally DeleteDC(MemDC); end; finally ReleaseDC(0, ScreenDC); end; end;这段代码的核心在于捕获桌面句柄GetDC(0)它拿到的是整个虚拟屏幕的DC包含所有显示器。如果做多屏扩展这里可以配合EnumDisplayMonitors或GetSystemMetrics(SM_XVIRTUALSCREEN)计算组合屏幕的起始坐标避免只能抓到主屏。实际项目里屏幕分辨率可能是4K、缩放可能是125%这部分留到踩坑章节详细展开。BitBlt的问题在于每次都是全屏抓取像素数据量巨大。1080p一张24位位图大约8MB30帧就是240MB直接传输必然不行。所以采集之后必须立刻处理先把位图扫描线转成紧凑的字节数组再交给压缩模块。2.2 编码与压缩zlib之外还可以做区域差分采集到裸像素后不能直接发压缩率才是决定远程流畅度的关键。Delphi自带的System.ZLib提供了TCompressionStream和THuffmanCompressionStream不需要引入外部DLL。但全屏位图无论怎么压缩帧间变化小时依旧浪费带宽所以我加了一层区域差分将屏幕划分为若干矩形区域逐块比较前后帧的像素哈希只有发生变化的区域才进入压缩和传输队列。type TRectBlock record X, Y, Width, Height: Integer; HasChanged: Boolean; end; procedure BuildChangedBlocks(Prev, Curr: PByteArray; BlockSize: Integer; var Blocks: array of TRectBlock); var i: Integer; begin for i : 0 to Length(Blocks) - 1 do begin Blocks[i].HasChanged : not CompareMem( Prev[Blocks[i].Y * ScreenWidth * 3 Blocks[i].X * 3], Curr[Blocks[i].Y * ScreenWidth * 3 Blocks[i].X * 3], Blocks[i].Width * Blocks[i].Height * 3); end; end;区域差分有两个关键参数分块大小和判定阈值。块太大了局部小变化会带动整块重传块太小了比较和碎块传输的开销反而压过收益。实测当屏幕大部分静止时16x16的块配合zlib压缩单帧数据可以压到3-8KB网络占用几乎可以忽略。如果帧率要求更高还可以加一个“每隔N帧做一次全屏关键帧”的机制防止长期差分包导致画质漂移。压缩传输的延迟也要控制。TCompressionStream默认压缩级别是zcDefault对远程桌面来说CPU占用和延迟的平衡点通常出现在zcFastest。因为屏幕画面本身就是高冗余数据快速压缩和最佳压缩的比率差距不到20%但耗时能差出好几倍弱机器上尤其明显。2.3 鼠标键盘回控SendInput的参数细节画面传输到控制端后控制端要能反向操作远端机器。低层实现有三条路SendInput、mouse_event、PostMessage。三条路里我推荐SendInput因为它是微软官方推荐的合成输入接口能进入UIPI用户界面特权隔离允许的正常路径而PostMessage在UAC高权限窗口前经常失效。procedure SendMouseMove(X, Y: Integer); var Input: TInput; begin ZeroMemory(Input, SizeOf(Input)); Input.Itype : INPUT_MOUSE; Input.mi.dx : Round(X * 65535 / ScreenWidth); Input.mi.dy : Round(Y * 65535 / ScreenHeight); Input.mi.dwFlags : MOUSEEVENTF_ABSOLUTE or MOUSEEVENTF_MOVE; SendInput(1, Input, SizeOf(Input)); end;注意这里有两个坑。第一dx/dy接收的是绝对坐标范围是0到65535不是像素坐标必须换算否则鼠标会飞到屏幕边角。第二多显示器场景下坐标原点是虚拟屏幕左上角而不是主屏左上角偏移量必须一起参与换算。滚轮消息也一样MOUSEEVENTF_WHEEL的mouseData表示滚动量正数向上、负数向下每个单位代表“一格”不是像素。键盘输入用INPUT_KEYBOARDKEYEVENTF_KEYUP必须成对发送。很多初学代码只发按下不发起遥控端按键会卡在重复触发状态这是个极易踩的低级陷阱。2.4 解码端显示自绘控件与无损缩放被控端发过来的是压缩后的字节流控制端拿到后先解压再重建为位图最后绘制到自绘控件。如果两端分辨率一致直接StretchBlt就行但大多数场景下控制端窗口小于远端分辨率所以要做等比缩放和滚动条配合。我在控制端使用自定义的TImage派生控件重写Paint方法procedure TRemoteView.Paint; var R: TRect; begin inherited; if FBitmap nil then begin R : CalculateClientRect(FBitmap.Width, FBitmap.Height); Canvas.StretchDraw(R, FBitmap); Canvas.DrawFocusRect(R); // 标注可视范围方便鼠标拖动 end; end;缩放之后鼠标坐标要做逆变换。比如远端宽度是1920控制端显示区域宽度是800那控制端鼠标X400对应远端的X960。逆变换公式很容易写错建议统一封装成ScreenToRemote和RemoteToScreen两个函数所有鼠标事件都走这两个函数换算别在事件处理里临时写裸公式。3. 权限、安全与会话隔离从“能跑”到“能用”的工程细节3.1 被控端以服务方式运行的Session隔离问题如果你的远程控制程序打算做成开机自启最常见的做法是注册Windows服务。但服务运行在Session 0服务会话里和用户登录的Session 1是隔离的——服务里执行BitBlt只能抓到黑屏或登录界面抓不到用户桌面。网上搜索“delphi 远程桌面 服务 黑屏”能翻到一堆求助帖根因就在这里。绕开这个问题的标准方案是双进程架构一个后台服务负责启动、监听端口、权限管理另一个普通用户态进程负责屏幕采集和输入注入。用户态进程通过CreateProcessAsUser配合WTSQueryUserToken启动到当前活动会话中。Delphi里用WtsApi32.pas可以拿到用户Tokenfunction GetActiveUserToken: THandle; var SessionId: DWORD; UserToken: THandle; begin Result : 0; if WTSGetActiveConsoleSessionId 0 then begin SessionId : WTSGetActiveConsoleSessionId; if WTSQueryUserToken(SessionId, UserToken) then Result : UserToken; end; end;如果只是做个局域网内部工具不追求服务化可以直接做成普通进程自启动配一个启动参数/autostart启动时最小化到托盘。这样实现简单Session隔离问题直接绕开缺点是没有登录桌面时进程不运行。3.2 安全校验与防护为什么不能裸奔在公网上带源代码的远程控制工具流传出去后最让人担心的不是学不会而是被滥用。我在这套程序里做了三层防护源码里都有体现这里建议保留。第一层是连接鉴权。控制端连接被控端时必须发送预共享的Token被控端校验通过后才接受后续数据。Token不应该硬编码在源文件里而是首次运行自动生成并存放在程序目录的配置文件中。第二层是通道加密。即使在局域网我也做了简单的XOR混淆或RC4加密。RC4在密码学上已经不算安全但胜在代码量极小、速度极快可以再套一层AES或者用Delphi自带的CryptAPI做握手加密。第三层是连接确认。被控端收到连接请求时任务栏气泡提示“有远程控制请求接入”用户可以选择接受或拒绝。这个功能在办公电脑上尤其重要否则很容易变成被人偷偷看屏幕的监控工具。网络踩坑方面如果被控端在公网或跨网段必须用路由器端口映射或内网穿透方案。标题热词里有“内网怎么远程桌面”“Win11远程桌面连接”这类高频问题其实就是NAT穿透没做好。防护层做法目的传输前Token鉴权防未授权连接传输中RC4/AES加密防抓包还原接入前界面确认提示防静默监视接入后心跳超时断开防断线残留失控3.3 多显示器与高DPI缩放不得不管的显示边界多显示器不仅在采集端要处理在控制端同样要处理。远端有三个屏幕时采集画面拼接成一张超宽全景图控制端默认只显示第一个屏幕区域用户可以通过快捷键或下拉菜单切换。这里有一个很容易算错的地方虚拟屏幕的坐标可能是负数副屏在主屏左边时处理坐标缩放时一定要把虚拟屏幕的原点偏移加回来。高DPI方面Win10及以后系统默认启用了DPI缩放。如果被控端缩放是150%而控制端用物理像素处理鼠标坐标回控时鼠标会偏移约三分之一。解决办法是在被控端进程启动时调用SetProcessDPIAwareprocedure SetDPIAware; begin if Assigned(SetProcessDPIAwarePtr) then SetProcessDPIAware; end;这样进程获取到的坐标就是真实物理像素不会经过系统缩放换算回控精度直接对得上。3.4 带宽与流畅度实测记录我拿这个程序做了几组实测供参考场景分辨率压缩等级帧率带宽占用局域网编辑Word1920x1080zcFastest12 FPS上限80-200 KB/s局域网播放视频2560x1440zcDefault20 FPS上限2-5 MB/s公网低带宽桌面静态1366x768zcMax8 FPS上限30-60 KB/s公网低带宽PPT切换1366x768zcMax8 FPS上限100-300 KB/s结论很简单办公场景用区域差分zlib足够流畅视频场景不是远程控制的强项。如果想优化视频流畅度可以加入H.264硬编码但那基本等于要接入FFmpeg或Intel Media SDK了属于另一条技术路线代码量不在一个量级。4. 踩坑记录与源码调试技巧高频采集、DPI坐标、剪贴板传输4.1 高频采集导致的CPU飙升问题第一版程序把采集帧率设成了30 FPS结果被控端i5处理器CPU占用直冲70%画面反而卡顿得厉害。原因在于BitBlt虽然快但它只是把像素从显卡复制到内存后续的扫描线转换、差分比较、zlib压缩全都消耗CPU。30帧全屏处理根本承受不住。优化思路是分层限帧高频区域检测比如鼠标滑动区域采用动态矩形扩展算法检测到鼠标移动时只采集鼠标周围一个扩展矩形内的变化区域静止画面降帧到3-5 FPS只做后台心跳和状态同步用户有按键或鼠标输入时临时提升到15 FPS输入停止两秒后回落这个方案的源码在ScreenDecoder.pas里体现为帧率控制器一个基于时间戳的令牌桶控制每次采集是否真正执行而不是无脑循环。4.2 缩放显示后鼠标漂移的根因控制端窗口自适应缩放后鼠标回控位置总是偏右下角。排查时发现不是换算公式错而是窗口非客户区的问题OnMouseMove事件里的X, Y是相对于控件客户区的坐标但缩放后的显示区域在客户区中还有留白边距直接拿X, Y去换算是拿留白当画面了。正确做法是先判断鼠标是否落在绘制区域内再做逆变换。类似问题还有拖拽滚动条后没有重新计算可视区域偏移导致画面跟手偏移。调试这类坐标问题我习惯在绘制矩形时先用IntersectRect剪裁出实际绘制区间再基于剪裁区间做所有坐标换算能少走很多弯路。4.3 剪贴板文本同步传输的实现远程控制没有剪贴板同步用起来非常难受。实现思路是两端各自监听系统剪贴板变化变化时把文本内容发给对端。Delphi里可以用TClipboard组件的OnChange事件但要注意避免回环本地收到远端剪贴板内容写入本地剪贴板后会再次触发本地OnChange又把内容发回远端形成广播风暴。解决方案是加一个“本次写入标志位”procedure TClipboardSync.OnClipboardChanged(Sender: TObject); begin if FLocalChange then begin FLocalChange : False; Exit; end; FRemoteText : Clipboard.AsText; SendClipboardText(FRemoteText); end; procedure TClipboardSync.ApplyRemoteText(Text: string); begin FLocalChange : True; Clipboard.AsText : Text; end;剪贴板文件传输比如远端复制文件控制端粘贴要比文本复杂得多涉及文件流分段发送热词里有“查看本地远程桌面保存密码”这类旁门左道我就不展开了纯文本同步对大多数场景已经够用。4.4 高频按键导致“键丢失”的排查遥控玩游戏或快速打字时部分按键偶尔漏发。抓包发现是TCP粘包导致接收端解析错位。解决方法是给所有控制消息包加固定长度包头type TPacketHeader packed record Magic: Word; // 固定为 $5A5A PacketType: Byte; // 1鼠标 2键盘 3剪贴板 DataLen: Integer; // 后续数据长度 end;接收端先读固定大小的包头校验Magic如果不对就丢弃并重新找包边界。这个机制看起来简单但能解决95%的“时灵时不灵”传输问题。类似的还有消息边界粘连问题处理原则是一律按流式解析不要假设每条Recv刚好对应一个完整报文。4.5 日志与断线重连的调试经验最后分享一个调试技巧远程控制程序最怕的是“连不上但不知道哪一步断了”。我在这套源码里加入了一个轻量日志模块所有关键节点监听端口、收到连接、鉴权通过、首次采集成功、首次回控成功都会写一行带时间戳的日志。日志文件默认轮转保存5个每个1MB。排查问题时先看两端日志序是不是对齐比看代码猜快十倍。断线重连方面控制端每两秒发一次心跳包被控端连续三次没收到心跳就主动断开并释放资源避免出现“假连接”占用。重新连接时控制端先发起全屏关键帧请求尽快恢复画面。这套源码做到现在性能、稳定性和易用性已经能支撑日常内部运维局域网内基本没有明显延迟公网配合内网穿透也能实现基础控制。学这套代码时不建议一上来就啃协议解析或压缩算法建议按照“采集-传输-显示-回控”的顺序逐步打开模块每一步都亲手跑一遍Demo比死记代码有用得多。本文还有配套的精品资源点击获取
返回列表