简介:面向Delphi开发者的远程桌面(远程控制)程序源码资源,包含服务端与客户端双端实现,并局部借助C++辅助完成,适合正在学习网络通信、桌面控制或希望借鉴成熟控制逻辑的中高级Delphi爱好者参考,也可为远程运维工具开发提供工程蓝本。服务端主要负责屏幕图像采集与命令接收,客户端负责画面呈现与远程操作发送,两者配合可完成基本的远程控制闭环。代码当前已完成大部分功能,作者仍围绕算法进行迭代,核心亮点是采用分块算法替代传统隔行扫描方案,能够更高效地处理屏幕图像变化区域的捕获与传输,对理解远程控制底层原理颇有帮助。压缩包采用rar格式,大小约1.15MB,体量轻量便于快速下载阅读;目前已有749人学习使用。通过阅读源码,可以梳理远程连接建立、画面数据传输、客户端与服务端协作流程,以及分块算法相对于隔行扫描在画面变化捕捉、带宽占用和响应速度上的差异,尤其适合用于对比研究和项目二次开发。 说实话,当我决定用Delphi写一个远程桌面(远程控制)程序并整理成带源代码的项目时,周围不少人都觉得我在“复古”。毕竟现在的TeamViewer、向日葵、RustDesk一个比一个成熟,为什么还要自己造轮子?但实际做过之后你会发现,远程控制这个场景的需求远没有想象中那么统一:公司机房几十台老旧的Windows工控机要批量运维,内网环境不允许数据出网;客户现场的收银机需要远程协助,但对方又不想装一堆全家桶;还有一些特定行业要求控制端体积小、行为可预期、源码可控。这些时候,一个用Delphi自研的远程桌面程序反而比商业软件更合适——它只做你需要的功能,没有任何多余的东西。这篇文章就把我从零搭建这套远程控制程序的全过程、核心源代码模块以及实战中踩过的坑完整记录下来,给同样需要“自己动手做远程工具”的朋友一条可复现的路线。
1. 项目定位:为什么还要自研远程控制
1.1 远程控制程序的核心价值
远程桌面/远程控制程序从功能上讲,核心无非三件事:看对方屏幕、模拟鼠标键盘输入、传送文件。但“能做”和“好用”之间差距巨大。我这边整理下来,一个真正能落地使用的远程控制程序还需要满足几个隐藏需求:
- 被控端能够无人值守地后台运行,开机自启、掉线自动重连。
- 控制端画面流畅,延迟不能大到鼠标飞出去才看到画面回传。
- 通信数据走自定义协议,不依赖第三方服务器中转,适用于局域网。
- 代码可维护、可扩展,随时能在控制协议里增加新指令,比如远程运行某个命令、批量修改配置。
我的使用场景是为公司内部维护一批Windows工控机,系统涵盖WinXP到Win11,硬件配置普遍不高。在这种环境下,第三方远程工具要么版本太新跑不动,要么安全策略不符合内网规范,所以“用Delphi自研一个远程控制服务端+客户端”就成了最合理的方案。
1.2 为什么选Delphi而不是Python、C#或C++
很多人的第一反应是:现在写这类工具用Python不是更快吗?用C#不是写起来更轻松吗?确实,从“快速出原型”的角度,Python和C#都有优势,但真正做产品级工具时,Delphi有自己的独特位置。
Delphi编译出来是原生x86/amd64程序,单exe即可运行,不需要在目标机器上预装.NET Framework或Python解释器。这一点在维护老旧的Windows工控机时是致命优势——很多机器处于孤立网络环境,根本没法在线安装运行时。Delphi对Windows API的封装非常成熟,VCL中直接调用Win32 API、操作GDI、处理Socket都很方便,底层控制力和开发效率之间有一个很好的平衡。另外,Delphi 7时代的老代码到Delphi 10.4/11/12基本还能平滑迁移,这对已经有大量历史代码的公司来说非常有吸引力。
我并不是说Python或C#不好。如果你要处理服务器端的远程控制、跨平台支持,RustDesk那种Rust方案或者Python的Twisted框架都很出色。但针对“Windows平台、轻量、可控、不改现有网络环境”的局域网远程控制工具,Delphi依然是性价比很高的选择。
2. 整体架构设计与协议约定
2.1 控制端与被控端的两层结构
我采用的是最常见的两层结构:被控端叫Service端(或Server端),控制端叫Client端。Server运行在目标电脑上,后台监听TCP端口;Client运行在你的电脑上,主动连接Server。
之所以不用三层结构(加一个中心服务器做信令和转发),是因为我的场景全部在局域网内,直连延迟最低、部署最省事。但如果你的需求是跨互联网远程控制,那确实需要一台带公网IP的转发服务器,或者实现NAT穿透。我这次的方案把网络层抽象出来,后续要加中间服务器,改动范围也集中在连接建立部分,不影响核心的屏幕采集和指令处理逻辑。
2.2 自定义通信协议:文本指令头 + 二进制数据体
远程控制程序最忌讳的是把协议设计得过于复杂。我一开始参考过VNC的RFB协议,发现它虽然规范,但维护成本高,而且对Delphi这种快速开发场景来说有点大材小用。最终我自己定了一套简单的协议:每个数据包由指令头和指令体组成,指令头是文本格式,指令体是二进制格式。
协议包的格式约定如下:
指令名|参数1=值1|参数2=值2\n [二进制数据体]举例:
GET_SCREEN|quality=70|width=1280|height=720\n MOUSE_MOVE|x=800|y=600\n MOUSE_CLICK|button=left|action=down\n KEY_EVENT|keycode=65\n FILE_PUSH|filename=report.txt|filesize=1024\n文本头部方便调试和扩展,数据体单独用二进制流传输,比如JPEG图像数据、文件内容。每条命令以换行符结尾,收到完整的一行后,通过指令名分发到对应处理函数。这种简单协议在实际运行中非常稳定,而且我能在telnet环境里手动模拟指令来排查问题,这一点对调试帮助巨大。
2.3 模块划分:五个核心功能单元
从代码组织上,我把整套程序拆成五个模块:
| 模块 | 职责 | 关键实现 |
|---|---|---|
| ScreenCapture | 抓取屏幕图像并压缩 | GDI BitBlt + JPEG编码 |
| InputSimulator | 模拟鼠标键盘输入 | SendInput / SetCursorPos |
| FileTransfer | 双向文件传输 | 分包发送 + CRC校验 |
| NetworkCore | TCP通信与协议解析 | Indy TIdTCPServer / TIdTCPClient |
| SessionManager | 认证、权限与连接状态管理 | 随机Token + 心跳保活 |
这样拆分的好处是,每个模块可以单独测试。比如ScreenCapture模块可以先用一个定时器把屏幕存成本地JPEG文件,验证图像质量;NetworkCore模块可以先做文本聊天功能,跑通了再叠加图像传输。模块边界清晰之后,代码写起来顺手,排错效率也高。
3. 核心源代码模块逐段拆解
3.1 屏幕截取与图像编码
屏幕截取是整个远程控制程序里最基础也最关键的环节。我早期用过一个简单的方案:直接整个屏幕截成BMP,然后转成JPEG发送。这在1080p分辨率下勉强能用,但网络稍差就明显卡顿。
最终采用的方案是GDI的方式,获取屏幕DC后通过BitBlt复制到位图,再交给JPEG编码器压缩。代码核心如下:
function CaptureScreenRect(SrcRect: TRect; var Bmp: TBitmap): Boolean; var DC: HDC; MemDC: HDC; BMPHandle: HBITMAP; W, H: Integer; begin Result := False; W := SrcRect.Right - SrcRect.Left; H := SrcRect.Bottom - SrcRect.Top; DC := GetDC(0); MemDC := CreateCompatibleDC(DC); BMPHandle := CreateCompatibleBitmap(DC, W, H); try SelectObject(MemDC, BMPHandle); if BitBlt(MemDC, 0, 0, W, H, DC, SrcRect.Left, SrcRect.Top, SRCCOPY) then begin Bmp := TBitmap.Create; Bmp.Handle := BMPHandle; Bmp.PixelFormat := pf24bit; Result := True; end; finally DeleteObject(BMPHandle); DeleteDC(MemDC); ReleaseDC(0, DC); end; end;注意几个细节。第一,PixelFormat要设置成pf24bit,而不是默认的pfDevice,否则在某些显卡驱动下保存JPEG会出现色彩失真。第二,截取区域不要直接写死全屏,而是预留一个SrcRect参数,这样后续做“局部刷新”优化时不用改动函数接口。第三,抓屏后应立即释放DC和位图句柄,否则长时间运行会逐渐耗尽GDI对象,导致被控端屏幕闪烁甚至系统异常。
JPEG压缩我直接用了VCL自带的TJPEGImage。这里有个经验值:remote assistance场景下JPEG质量设为70到80之间最合适,画质损失肉眼基本不可见,但体积比无损BMP小一个数量级。1080p屏幕上,质量75的JPEG压缩后普遍在30到80KB左右,局域网千兆环境下传输延迟可以控制在50ms内。
3.2 网络传输与粘包处理
远程控制的数据传输走的是TCP长连接。网络库我用的是Indy的TIdTCPServer和TIdTCPClient,因为Delphi集成度高,处理SSL和线程模型比较省心。TCP是流式协议,没有天然的“消息边界”,所以必须自己处理粘包和半包问题。
我的解决方案是严格按照“长度前缀”来读取数据包:自定义一个包头,包含指令长度、数据体长度等信息,然后按长度精确读取。代码结构如下:
procedure TNetworkCore.SendPacket(ACommand: string; AData: TStream); var Header: string; HeaderBytes: TBytes; DataLen, HeaderLen: Integer; begin DataLen := 0; if Assigned(AData) then DataLen := AData.Size; Header := Format('%s|datalen=%d'#10, [ACommand, DataLen]); HeaderBytes := TEncoding.UTF8.GetBytes(Header); FIOHandler.Write(HeaderBytes, Length(HeaderBytes)); FIOHandler.WriteBuffer(AData, DataLen); end;接收端读取时,先按行读取Header,解析出datalen,再精确读取DataLen字节的数据体。这样不管发送端发送多快、TCP怎么分包都不怕,每个包都能被完整还原。
这个模块我踩过最深的一个坑是:不要在Indy的OnExecute事件里直接读取大块图像数据并同步执行JPEG解码和屏幕绘制。因为Indy的OnExecute跑在独立的线程里,如果一个连接线程被阻塞,其他连接也会受影响。后来我改成接收线程只负责把数据包放入线程安全队列(TThreadedQueue),UI线程轮询队列并更新画面。实测性能提升非常明显,尤其是多客户端同时连接时稳定性大幅改善。
3.3 鼠标键盘事件注入
控制端发过来的鼠标坐标和按键动作,最终要落到被控端的真实输入上。早年Delphi程序里很多人用mouse_event和keybd_event,这两个API简单,但问题是它走的是旧式输入机制,在UAC权限提升的窗口、安全桌面等场景下会失效。更现代的做法是使用SendInput。
下面是我在InputSimulator模块中使用的鼠标点击核心代码:
procedure SendMouseButton(AButton: TMouseButton; AIsDown: Boolean); var Inp: TInput; begin FillChar(Inp, SizeOf(Inp), 0); Inp.Iype := INPUT_MOUSE; case AButton of mbLeft: if AIsDown then Inp.mi.dwFlags := MOUSEEVENTF_LEFTDOWN else Inp.mi.dwFlags := MOUSEEVENTF_LEFTUP; mbRight: if AIsDown then Inp.mi.dwFlags := MOUSEEVENTF_RIGHTDOWN else Inp.mi.dwFlags := MOUSEEVENTF_RIGHTUP; end; SendInput(1, Inp, SizeOf(Inp)); end;键盘模拟同理,也是通过SendInput发送KEYDOWN和KEYUP事件。这里要特别注意一个坐标问题:被控端如果开启了DPI缩放,控制端传过来的坐标必须经过DPI换算,否则在高DPI显示器上鼠标位置会偏得离谱。我在控制端发送鼠标坐标前,会先获取被控端屏幕的逻辑分辨率和物理分辨率,按下述公式换算:
真实坐标X := 物理像素坐标X * (逻辑分辨率宽度 / 物理分辨率宽度)否则你在自己2K屏幕上点得欢快,对端的1080P显示器上光标可能跑到右下角去,完全是“看不见的坑”。
4. 实测效果、性能优化与排障纪实
4.1 局域网内实测表现
我在公司内网里部署了一套,控制端和被控端都走千兆以太网,被控端是一台i5-6500处理器、8GB内存的普通办公机,系统为Windows 10专业版,而控制端是一台较新的移动工作站。
| 测试项目 | 实测结果 |
|---|---|
| 1080P全屏截图耗时 | 12ms ~ 25ms |
| JPEG压缩耗时(质量75) | 15ms ~ 30ms |
| 单帧图像大小 | 35KB ~ 90KB |
| 局域网端到端延迟 | 45ms ~ 80ms |
| 每秒刷新率 | 约 15 ~ 20 帧 |
这个流畅度对于运维和远程协助完全够用。如果网络环境差一些,比如Wi-Fi或跨交换机,我通常把JPEG质量降到60,再限制发送频率为每秒10帧,体验也还行。关键不是追求高帧率,而是保证画面的“可读性”和操作的“确定性”。
4.2 常见问题排查速查表
在开发和内部测试阶段,我遇到了不少问题,这里整理成速查表,直接给你参考:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 控制端连接被拒 | 服务端未启动、端口被防火墙拦截 | 查看服务端监听状态,放行TCP端口;不要忘了服务端也要允许入站规则 |
| 连上了但黑屏 | 屏幕抓取权限不足、Session 0隔离、JPEG解码失败 | 试验以管理员身份运行服务端;或改用服务模式与桌面交互 |
| 鼠标坐标偏移 | DPI缩放导致逻辑坐标与实际像素不一致 | 按DPI系数换算坐标;在程序里调用SetProcessDPIAware |
| 画面卡顿、延迟飙升 | JPEG质量太高、带宽不足、发送频率无限制 | 降JPEG质量、限制帧率、启用差异传输 |
| 服务端长时间运行崩溃 | GDI对象泄漏、句柄未释放 | 监控GDI句柄数量,确保每次截图后释放DC和Bitmap |
| 粘包导致指令解析错乱 | 自定义协议未处理TCP长度边界 | 严格采用长度前缀方案,用ReadLn读取头部,再按长度读数据体 |
4.3 性能优化三板斧
这套程序我先后改过三轮,最有效的优化其实就三招。
第一招,JPEG质量动态调节。在控制端根据最近5帧平均传输时间,动态决定服务端下次截图的压缩质量。网络好时用85,网络差时降到55,整个过程完全自动。
第二招,局部刷新。屏幕变化往往只占一小块区域,比如鼠标移动、输入框跳动。我在服务端保留上一帧的完整位图,截取新图后计算变化区域,只把变化区域裁剪出来压缩发送。这个优化在“阅读文档、写代码”这类静止画面场景下,能把带宽占用降一个数量级。
第三招,控制端画面缩放。如果被控端的屏幕分辨率比控制端大,等比例缩小显示,而不是让画面超出窗口。缩放后不仅看得更全,还能降低控制端解码和绘制的负载。
5. 权限、安全与使用边界
5.1 认证与加密
远程控制程序天然涉及敏感操作,如果没有认证和加密,等于把一台电脑的大门敞开给内网里的任何人。我在这套程序里加了两层防护。
第一层是连接认证。客户端连接服务端时,服务端生成一个随机挑战码发给客户端,客户端使用预共享密钥对挑战码做HMAC计算,返回校验值,服务端比对通过后才允许后续通信。这个过程保证了即使有人抓包,也无法直接重放认证数据。
第二层是传输加密。由于这套工具主要跑在可信内网,我暂时用了轻量级的流加密方式对图像数据和指令做混淆通信,避免明文传输。如果后续要跨公网使用,我会再接入TLS,Indy本身支持TIdSSLIOHandlerSocketBase,改动可控。
5.2 合规使用提醒
这一点我必须明确写在文章里:远程控制工具只能用于你自己拥有合法管理权限的设备,比如你自己管理的机房服务器、家里几台电脑、客户明确授权的运维项目。未经授权控制他人电脑,无论是出于好奇还是其他目的,都是违法行为。
我在被控端做了两个安全设计:一是连接时被控端屏幕右下角会弹出一个白名单提示图标,让现场的人知道当前正在被远程控制;二是在服务端配置里增加了IP白名单,只有允许的IP段才能发起连接。这两个设计不复杂,但能避免很多不必要的麻烦,也符合企业内部安全审计的要求。
5.3 兼容性经验小结
最后聊一下Delphi版本兼容性。我的代码全程用VCL原生组件,没有依赖第三方控件,从Delphi 7到Delphi 11/12的主分支都能编译通过。唯一需要留意的是Indy版本差异——老Delphi默认带Indy 10.0/10.1,新版本带Indy 10.6,API基本兼容但Socket选项定义略有不同。如果遇到编译错误,优先检查IOHandlerType和ReadTimeout的设置,这是版本差异最容易暴露的地方。
我自己维护这套源码的习惯是:主分支放在Delphi 10.4上,因为原生的HighDPI支持最好;同时保留一个Delphi 7兼容的branch,用来给老设备临时编译特殊版服务端。这种双分支的做法,让我在维护老旧Windows时少了很多折腾。
写在最后
从最初的“只想快速跑通抓屏”,到后来不断加功能、优化传输、完善安全性,这套远程控制程序前前后后折腾了一个多月。回头看来,最有价值的不是代码本身,而是对Windows底层图形机制、Socket编程和输入模拟API的理解全都落到了实处。如果让我给同样想自己写远程工具的人一句忠告,那就是:先把“抓屏+传图+点击”这个最小闭环跑通,再考虑花里胡哨的功能。等你真正把这三件事做好,远程控制也就完成了一大半。
本文还有配套的精品资源,点击获取