简介:这是一套基于VC++实现的VNC远程控制程序完整源码,面向期望掌握RFB协议与远程桌面底层原理的C++开发者,也适合网络编程、图形渲染方向的学习者对照实践。压缩包共631个文件,大小仅2.27MB,核心代码由183个C文件、136个头文件和83个C++文件构成,同时附带VC++工程文件(vcproj/dsp/sln)、文档及图标等辅助资源,控制端与被控端代码分开组织,便于逐个模块研读。目前已有851人学习下载。源码完整覆盖TCP/IP网络通信、屏幕图像捕获与编码、键盘鼠标事件转发、线程同步等VNC关键环节,并集成jpeg图像压缩相关实现,可帮助读者理解远程控制软件从底层协议到界面交互的完整链路;工程中丰富的GDI调用和多线程设计,也为后续扩展加密认证、性能优化或自定制功能提供了可直接修改的蓝本。
1. VNC项目到底在解决什么问题
1.1 一句话讲透VNC的工作方式
VNC这名字看着高大上,拆开就是Virtual Network Computing,虚拟网络计算。它解决的事情特别直白:让你在一台电脑的屏幕上,看到并且操作另一台电脑的桌面。这个“另一台电脑”可以跟你在同一个办公室,也可能在几千公里外的机房——只要网络能通就行。
早年我第一次拿到VNC远程控制程序VC++源码的时候,第一反应是这东西怎么会有那么多文件。服务端、客户端、协议解析、编码器、认证模块、平台相关的绘图代码,加起来几百个文件是常态。等你把代码捋顺了才发现,核心逻辑其实就像一个“截屏+转发”的管道:服务端把屏幕抓下来,压缩编码,通过网络发给客户端;客户端收到数据,解码还原成图像显示出来,再把你的键盘鼠标操作传回服务端。就这么一个循环,构成了远程桌面的全部体验。
1.2 为什么一份VC++写的VNC源码值得研究
如果你只是想在两台电脑之间远程控制,说实话,装个现成的VNC软件就够了,甚至Windows自带的远程桌面也不错。但研究源码的人,目的通常不是“用”,而是“改”:有人要定制一个轻量级远程工具嵌进自己的系统,有人要解决特定分辨率、特定网络下的画面延迟问题,有人想学习RFB(Remote Framebuffer Protocol)协议怎么落地,还有人纯粹是想看老前辈用C++怎么写网络程序和图像编码。
这份VC++源码的价值就在这里。它不只是一堆能编译的代码,更是一套完整的远程控制协议实现范本。你不需要从零去啃RFC文档,直接看代码就能明白握手、认证、帧缓冲更新、编码协商这些抽象概念是怎么变成实际字节流的。对于做嵌入式、做运维工具、做跨平台远控产品的人来说,这相当于拿到了一套可以直接套用的骨架。
1.3 版本扫盲:TightVNC、UltraVNC怎么选
网上常见的VNC源码主要分几个分支:TightVNC、UltraVNC、RealVNC的开源版,还有早期的标准VNC。我手上这份是VC++工程,对应TightVNC分支的可能性比较大,因为TightVNC在Windows上的工程化做得最规整,模块划分清晰,适合学习。
TightVNC的代码风格偏简洁,默认编码采用Tight编码(这也是它名字的由来),在低带宽下表现不错,源码阅读门槛相对低一些。UltraVNC的扩展功能多,比如文件传输、聊天、漫游光标这些,代码量更大,二次开发功能找起来方便,但对新手不太友好。如果你是为了学习,我建议从TightVNC入手;如果你是想在现有功能堆上改东西,UltraVNC更合适。RealVNC开源版经过多次重构,代码风格更现代,但结构也相对复杂。
2. 源码架构与三个核心协议机制
2.1 RFB协议的三阶段:握手、初始化、交互
RFB协议是VNC的底层通信协议,整个流程可以分成三个阶段。
第一阶段是握手(Handshake)。客户端连上服务端的5900端口后,双方先交换协议版本号(比如3.3、3.7、3.8),然后协商安全类型。老版本默认只用VNC Password认证,新版本还支持None、TLS等。这个阶段的代码通常在ProtocolVersion和Security协商函数里,数据结构就是几个结构体直接写到socket上,非常简单粗暴。
第二阶段是初始化(Initialization)。认证通过后,服务端把屏幕的宽、高、像素格式(比如RGB565还是RGB888)、桌面名称发给客户端;客户端收到后,回一个标志位表示自己是否支持像素格式协商。从这一步开始,双方才真正建立起“显示”的上下文。
第三阶段是交互循环(Interaction Loop)。客户端会周期性地发FrameBufferUpdateRequest请求,服务端收到请求后检查屏幕是否有变化,把变化区域编码成字节流发回去;客户端解码并显示。同时,客户端把键盘事件、鼠标点击、鼠标移动这些消息打包发给服务端,服务端模拟输入。源码里这一循环就是两个while循环,一个是客户端的接收线程,一个是服务端的发送线程,理解了这两个线程就理解了VNC的一大半。
2.2 图像编码:Raw、Hextile、ZRLE怎么选
画面传输如果直接按原始像素发,数据量大到没法用。所以RFB协议定义了多种编码方式,客户端在初始化时发送自己支持的编码列表,服务端从中选择一种。
这几个编码里,Raw是最笨的办法,像素点不压缩直接发,适合内网极低延迟场景。Hextile把屏幕分成16x16的小块,每块记录自己的位置、颜色数据,如果块内颜色单一还可以只发一个颜色值,压缩率比Raw好很多,CPU开销也小。ZRLE用zlib压缩运行长度编码数据,压缩率高但对CPU要求高,适合带宽紧张的环境。
TightVNC源码里还实现了Tight编码,它是在ZRLE基础上的进一步优化,会根据图像内容动态选择调色板编码、灰度编码或纯色填充,还会做JPEG压缩预处理。研究编码器代码时,我强烈建议你先跑一遍看效果,再去看代码实现——这样你才能把“为什么这个编码器在桌面文字场景下表现好、在照片场景下表现差”这种问题跟代码对应起来。
2.3 VNC认证是怎么保证安全的
VNC的密码认证用的是挑战-响应机制,过程不传明文密码。服务端在安全协商完成后,生成一个16字节的随机挑战数据发给客户端;客户端把这个随机数用密码的DES加密结果作为密钥做一次DES加密,把加密结果返回给服务端;服务端用自己保存的密码做同样的操作,比对结果是否一致。
不过要说句公道话,VNC的这套认证放到今天是相当脆弱的。DES密钥有效长度只有56位,VNC还把密码截断到8个字符,这意味着暴力破解难度很低。源码里能看到一个固定密钥表desTable,整个VNC生态都用这张表,一旦密钥交换过程被截获,密码就很容易被还原。所以,如果你的VNC服务暴露在公网,一定要配合防火墙白名单或者隧道使用,不要把5900端口裸奔出去。这一点在阅读认证代码时值得反复体会:协议设计有时代局限性,二次开发时要主动加强安全性。
3. VC++工程编译实战清单
3.1 编译前置环境与工程转换
先说结论,这份源码可以在Visual Studio 2017、2019甚至2022上编译,但过程要花点心思。老版本的VNC源码多是VC6工程,文件后缀是.dsp、.dsw,新版VS打开时会有转换向导,把它转成.vcxproj工程即可。
转换之后,有几个环境设置你大概率要手动调整。项目属性里要确保字符集用的是“使用多字节字符集”(很多老代码风格依赖char),而不是Unicode;C/C++语言标准选“默认”就好,不用强行改成C++14、17,老代码往往不兼容新标准。另外,如果编译报错找不到Windows SDK,去安装一下对应版本的SDK组件,VS安装器里勾选就能装。
3.2 从源码生成可执行文件的完整流程
我以Windows平台为例,整理一份可以直接照着做的流程:
- 下载源码压缩包,解压到一个没有空格的纯英文路径,比如 D:\vnc_src\,避免老代码处理路径时出问题。
- 用Visual Studio打开转换后的.sln文件,右键解决方案,选择“配置管理器”,把平台切换为x64(或者保持x86,看你的目标环境)。
- 在解决方案里找到winvnc和vncviewer两个项目,右键分别设为启动项目。winvnc是服务端,vncviewer是客户端。
- 编译分两步走:先生成vnc_lib这类公共库项目(如果有的话),再生成winvnc和vncviewer。右键解决方案→重新生成解决方案,VS会自动处理项目依赖。
- 到输出目录找到生成的winvnc.exe和vncviewer.exe,先在本机启动winvnc,再用vncviewer连接127.0.0.1测试。
整个过程顺利的话,十分钟内能看到一个能跑的VNC系统。但老工程没这么听话,下面这几个报错你大概率会碰到。
3.3 编译期常见的五个坑
| 报错现象 | 原因 | 处理方式 |
|---|---|---|
| 无法打开包含文件 winsock2.h | Windows SDK不完整或路径没配好 | 确认安装了“Windows SDK”组件,在项目属性VC++目录里检查Include路径 |
| std::min/std::max 相关编译错误 | Windows.h里定义了min/max宏,和STL冲突 | 在预处理器定义里加 NOMINMAX |
| strcpy、sprintf 提示C4996 | 新版VS的安全CRT警告 | 预处理器里加 _CRT_SECURE_NO_WARNINGS |
| error C2440 类型转换错误 | 老代码里有隐式类型转换,新编译器不允许 | 手动加static_cast修正,优先看是char还是const char引起 |
| 链接时找不到 wsock32.lib / ws2_32.lib | 没添加网络库依赖 | 在项目属性的“链接器→输入→附加依赖项”里补充这两个lib |
我自己的经验是,不要一报错就改代码。先检查项目属性这种“环境级”配置,90%的老工程移植问题都是环境配置不对,而不是代码逻辑有问题。真遇到需要改代码的地方,最好用条件编译宏包起来,别把老代码的逻辑破坏掉。
4. 源码走读:一条连接的一生
4.1 服务端:从监听端口到画面发送
VNC服务端的入处在源码里通常是一个叫vncServer的类,核心是一个监听socket,默认端口5900。代码启动流程看着复杂,其实就是一个标准的Windows socket服务:创建socket、bind、listen,然后accept循环等待客户端连接。
每来一个客户端,服务端就启动两个线程:一个专门的线程做RFB握手和认证,通过后进入消息循环;另一个线程做屏幕轮询,定时检查屏幕变化区域,把变化内容编码发送。屏幕抓取在Windows老代码里多用BitBlt配合GetDC(NULL),也就是把整个桌面DC的内容拷到内存位图里,再做差分比较。新版代码里有些实现改用了Desktop Duplication API,效率更高,但原理一样:抓到屏幕变化,标记脏矩形,只编码这些区域。
看服务端代码时,我最建议你关注两个函数:一个负责处理客户端发来的FrameBufferUpdateRequest,另一个负责响应客户端设置像素格式。前者决定画面更新的触发时机,后者决定图像字节的排列方式,这两块写明白了,VNC的画面传输机制你就通了一大半。
4.2 客户端:从密码输入到屏幕渲染
客户端代码相对简单直接。启动后读取命令行参数连服务器,弹出密码框,走完握手认证流程。初始化完成后,客户端定时(通常是每秒5到20次)发送FrameBufferUpdateRequest,然后在一个循环里接收服务端数据。
收到数据后的核心处理在vncDecoder里。它要先解析消息头,判断消息类型是FrameBufferUpdate还是SetPixelFormat、SetEncodings等;如果是图像更新消息,就根据编码类型分派到对应的解码函数:Raw解码就是memcpy,Hextile解码要处理16x16的块循环,Tight解码要先解zlib再还原像素。解码完的图像数据往屏幕DC上贴,用StretchDIBits或者AlphaBlend都行。
客户端代码里有意思的地方是鼠标键盘事件的处理。鼠标事件被编码成相对服务端屏幕坐标的绝对坐标,比如服务端屏幕是1920x1080,你点击客户端窗口的像素点(960,540),发送的就是坐标(960,540);键盘事件则映射为X11键码格式,和VNC协议里定义的keysym对应。这些映射关系在源码里有大段的switch case,如果你想做自定义键位映射,就在这里动手。
4.3 二次开发时最值得改的三个位置
基于这份源码做二次开发,最常动刀的有三处。
第一处是编码器选择逻辑,在服务端的帧缓冲发送函数里。默认条件下它会根据网络延迟和上次发送大小自动选编码,你可以改成固定用某一种编码,比如局域网强制Hextile,或者低带宽强制Tight。第二处是客户端认证流程,如果你要做企业内部的无密码快捷登录,可以跳过一次挑战响应,直接发空密码或者预设凭证。第三处是消息循环里对未知消息类型的处理,RFB协议预留了扩展消息号,你可以自己定义消息类型做自定义指令,比如远程执行命令、传输文件、发送心跳包。我见过有人把VNC改成远程考试监控工具,就是往这条扩展通道里塞自定义数据。
改代码之前,一定先确认自己手上这份源码有没有线程安全问题,因为VNC这种老项目都是裸线程加全局变量的风格。加一个标志位或者队列时,记得用临界区或者原子变量保护,不然改着改着就出现随机崩溃。
5. 高频实战场景与对应修改思路
5.1 分辨率自适应:从固定桌面到动态调整
很多人在使用VNC时遇到分辨率问题,服务器端是4K屏,客户端是1080P笔记本,远程过去界面大得没法看。解决思路有两条路。
一条是在服务端做分辨率缩放,即采样屏幕后先缩放到客户端支持的分辨率再编码,改动集中在编码发送前的图像处理环节,需要引入一个缩放算法(双线性插值就行)。另一条是让客户端以窗口模式显示,不做缩放,靠滚动条看整个桌面。源码里通常两种都支持,窗口模式下客户端会把服务端桌面大小当作窗口客户区大小,然后处理WM_HSCROLL和WM_VSCROLL消息。如果你有root权限的Linux服务器,也可以用xrandr这类工具直接改服务端分辨率,配合源码里的分辨率变更通知消息,客户端会自动刷新桌面大小。
5.2 文件传输:协议本身没有,但可以自己加
这是VNC最被吐槽的点之一,标准RFB协议没有文件传输功能。现成VNC软件里的文件传输都是各自扩展实现的。
如果用UltraVNC源码,它本身就带了文件传输通道,数据走的是自定义消息号,你在客户端和服务端各写一个文件列表请求、传输请求的处理函数就能跑起来。如果用TightVNC,需要自己扩协议:在双方协商好扩展消息范围(比如消息号247以上)后,定义文件元数据、文件内容、传输结束三类消息,然后实现分块读写。文件内容消息的结构可以很简单:消息号+文件名长度+文件名+文件数据块,每块不超过32KB,避免UDP分包问题。这个工程量不大,但如果中途要断点续传,就得加偏移量字段和校验值,复杂度会上一个台阶。
5.3 开机自启和后台运行
Windows桌面版的VNC服务端默认是普通进程,开机自启通常靠注册表Run键。这有个坑:用户没登录时服务端不会启动,或者启动后没有交互桌面。热词里提到的“ubuntu 22.04安装vnc 关闭终端就失效”就是这个问题的Linux版本,本质都是服务端进程依附于某一个用户会话,会话关闭进程就跟着退出。
解决思路是把vnc server改造成Windows服务。VC++源码里一般会有Service相关的代码文件,比如winservice.cpp,把服务端注册成Service后用Service Control Manager管理,设置自动启动。注意VNC这种需要访问用户桌面的程序,注册成服务后还要考虑会话0隔离的问题,Windows下会给会话0单独一块非交互空间,你的服务如果没有特殊处理就看不到真实桌面。常规做法是让服务保持在LocalSystem账户下运行,并且勾选服务属性里的“允许服务与桌面交互”,或者使用创建一个用户进程并返回Session 1的逻辑。Linux下的思路类似,把vncserver写进systemd的service单元,指定User和Display变量,开机自启就不会随终端关闭。
6. 踩坑实录与常见问题排查
6.1 日常使用中的高频问题
结合VNC的使用习惯和源码实现,我把高频问题整理成了一份速查表,可以根据现象快速定位。
| 现象 | 可能原因 | 排查/解决思路 |
|---|---|---|
| 客户端能连接但黑屏 | 服务端被抓屏的GDI接口返回空DC,或者会话锁屏 | 检查服务端是否运行在锁定会话;改用服务端本机登录状态;看代码里GetDC(NULL)是否失败 |
| 画面一直刷新却很卡 | 编码器选择了Tight/JPEG,CPU占用过高 | 改为Hextile或Raw测试;降低刷新率;缩小分辨率 |
| 连上后几秒就断开 | 认证成功但初始化消息响应超时;防火墙拦截了后续数据包 | 抓包看断开前最后一个消息;关闭防火墙测试;确认VNC端口没有被端口占用冲突 |
| 鼠标点击位置不对 | 服务端和客户端分辨率不一致,坐标映射逻辑出错 | 检查客户端的屏幕坐标系换算代码;调整缩放比例为1:1测试 |
| 远程复制粘贴失效 | 剪贴板处理线程没有及时同步 | 有的分支实现了剪贴板消息同步,检查WM_CLIPBOARDUPDATE消息的注册是否成功 |
| 修改密码后连接总是失败 | 服务端密码文件没刷新,或配置文件路径指错 | 确认密码文件写入路径;删除旧密码文件重启服务端 |
6.2 调试远程控制程序的独门技巧
调试VNC这类网络图形程序,常规断点手段不够用,因为它同时涉及网络、图形和多线程。我有三个习惯分享出来。
第一个习惯是开日志宏。老VNC源码里基本都有调试日志的开关,通常在common.h或者vnc.h里定义一个DEBUG宏,打开后运行目录会输出日志文件,记录每个消息的收发时间、消息类型、长度。碰到疑难杂症先看日志,比瞎猜快得多。
第二个习惯是抓包。wireshark抓本地回环的5900端口流量,能看到协议交互的每个字节。配合RFC6143文档,你会清晰地看到握手时版本号、安全类型、随机挑战数据的流转,也能直观地看到FrameBufferUpdate消息里编码数据的分布。把这个习惯养成,调试任何自定义协议都有底气。
第三个习惯是虚拟桌面测试。不要在真实工作机上反复试分辨率修改和全屏代码,容易把环境搞崩。用Windows自带的虚拟桌面功能或者单独建一个低分辨率虚拟机来跑测试,确认代码稳定后再拿到真实环境验证。
6.3 我踩过的坑和补救方式
以前我改过一套VNC源码做远程协助工具,在编码器选择上想当然地全程用Tight编码,结果内网测试时CPU占用率飙到90%,画面延迟严重。后来分析发现,Tight编码的压缩计算量太大,在局域网带宽充足的情况下反而是个累赘。改成“局域网自动用Hextile,弱网自动切Tight”的策略后,问题立刻解决。这件事给我的教训是:VNC源码里的默认参数往往经过项目团队的实践调整,改之前要理解他们的设计意图,不要一味追求新特性。
另一个坑是字符编码混乱。老代码大量使用char数组和strcpy,在VS2019上编译通过后,运行起来中文桌面名显示为乱码,因为字符串被当成了ANSI而系统是UTF-8环境。统一改用MultiByteToWideChar转换一次,或者把项目字符集调整成“使用Unicode字符集”并适配相关API调用,才彻底解决。
最后还有一件事提醒所有做VNC二次开发的同行:VNC这个协议太老了,很多基础设计(密码认证、图像编码、消息协商)都有明显的时代印记。你可以在源码基础上升级它,但务必保留协议兼容层,不然客户端和服务端的版本一旦不匹配,就会出现更新一个端就断连的尴尬。
我在实际改这套代码时最大的体会是:VNC的源码没有多高深,难的是它横跨的网络、图形、输入模拟、并发处理这几个领域都要求你有基础功底。但反过来想,正是因为它的综合性,啃完一份VNC源码,你对Windows网络编程、GDI绘图、多线程协作的理解都会上一个台阶。如果你也是奔着这个目标来的,那就别急着跑通一个demo就收工,多花几个晚上把代码读透,收获一定比你预想的大。
本文还有配套的精品资源,点击获取