news 2026/10/11 11:17:12

VNC远程控制程序VC++源码解析:RFB协议与工程实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
VNC远程控制程序VC++源码解析:RFB协议与工程实现

简介:这份VC++源码资源面向希望深入理解远程桌面控制原理的C++开发者与网络编程学习者,以VNC(Virtual Network Computing)为实例,完整呈现基于RFB协议的控制端与被控端实现。源码将客户端与服务器分开编写,便于对照理解屏幕捕获、输入转发与画面更新的协作流程。压缩包共631个文件,约2.27MB,以c、h、cpp源码为主体,配合vcproj、sln等工程文件可直接在Visual Studio中打开编译,另有doc、txt、readme等说明文档及ico、bmp、cur等界面资源,整体结构清晰。内容覆盖套接字网络通信、RGB像素编解码、Windows API图形渲染、键盘鼠标事件处理、多线程同步以及可扩展的安全验证与自定义配置等关键知识点。目前已有852人学习下载,适合作为网络编程、图形处理与多线程实战的参考案例,也可在此基础上定制功能或优化性能。

1. VNC 远程控制程序 VC++ 源码:从协议到可编译工程的落地路径

很多人第一次接触远程桌面,是从装一个客户端、输个地址、点连接开始的,用起来像黑盒。但当你手里拿到一份 VNC 远程控制程序 VC++ 源码,事情就变了——你面对的不再是黑盒,而是 RFB 协议、帧缓冲更新、编码协商、输入事件注入这一整套可拆解、可改、可裁的机制。这份源码能解决的核心问题是:让你在 Windows 上用原生 C++ 把「屏幕采集 → 编码压缩 → 网络传输 → 远端解码渲染 → 反向输入回传」这条链路完整跑通,而不是停留在调 API 的层面。它适合两类人:一是想搞懂远程桌面底层怎么运转的 C++ 开发者,二是需要在自有系统里嵌入远程控制能力、又不想被第三方 SDK 绑死的工程师。下面按「协议先立住、再动手复现、最后讲坑」的顺序拆开讲。

2. RFB 协议与 VC++ 工程结构:先搞懂数据怎么流,再谈代码怎么写

2.1 RFB 协议的三段式握手,决定了你代码的骨架

VNC 的底层是 RFB(Remote Framebuffer)协议,它最大的特点是「瘦客户端」——服务端负责几乎所有计算,客户端只做解码和渲染。整个连接建立分三段:协议版本协商、安全类型协商、初始化消息交换。版本协商阶段双方各发 12 字节的版本字符串,比如RFB 003.008\n,取较低版本作为后续通信基准。安全类型阶段服务端列出支持的安全类型,客户端选一个,常见的是 None 和 VNC Authentication。初始化阶段客户端发 ClientInit(一个字节的共享标志),服务端回 ServerInit,里面包含帧缓冲宽高、像素格式、桌面名称。

这三段握手在 VC++ 里的体现就是 socket 上的顺序读写。我一般会把每一段封装成一个独立函数,返回值用 bool,失败就带错误码退出,而不是一股脑塞进一个 Connect() 里。原因是远程控制调试时,你经常需要单独验证某一段——比如怀疑是认证失败还是初始化失败,拆开就能快速定位。

// RFB 版本协商:发送本端版本,读取服务端版本,取较低者 bool NegotiateVersion(SOCKET sock, std::string& negotiated) { const char* clientVer = "RFB 003.008\n"; if (send(sock, clientVer, 12, 0) != 12) return false; char serverVer[13] = {0}; if (recv(sock, serverVer, 12, MSG_WAITALL) != 12) return false; // 比较主版本和次版本,取较小值作为后续通信版本 int cMajor = 3, cMinor = 8; int sMajor = (serverVer[4] - '0') * 100 + (serverVer[5] - '0') * 10 + (serverVer[6] - '0'); int sMinor = (serverVer[8] - '0') * 100 + (serverVer[9] - '0') * 10 + (serverVer[10] - '0'); // 实际比较逻辑按协议规范处理,这里简化为取小 negotiated = (sMinor < cMinor) ? std::string(serverVer, 12) : std::string(clientVer, 12); return true; }

这段代码的关键参数是版本字符串的格式——第 4 到第 6 位是主版本,第 8 到第 10 位是次版本,固定 12 字节。MSG_WAITALL保证收满 12 字节再返回,否则在慢网络下你会收到半截字符串,解析直接翻车。协商结果决定后面用哪套消息格式,比如 3.8 支持安全类型列表,3.3 只支持固定的一种,这个分支必须在后续代码里体现。

2.2 帧缓冲更新请求:客户端主动拉,还是服务端主动推

RFB 的帧缓冲更新有两种模式:客户端发 FramebufferUpdateRequest 主动拉,或者服务端在共享模式下主动推。源码里通常实现的是「拉模式」——客户端发一个请求,指定增量和感兴趣的区域,服务端回一帧 FramebufferUpdate。增量模式下,服务端只发变化的矩形区域,这是省带宽的关键。

在 VC++ 里,这个请求是一个 10 字节的结构:消息类型(1 字节,值为 3)、增量标志(1 字节)、x 位置(2 字节)、y 位置(2 字节)、宽(2 字节)、高(2 字节)。全部用网络字节序。我一般会写一个SendFramebufferUpdateRequest函数,把参数打包成字节流再发。

// 发送帧缓冲更新请求,incremental=1 表示只请求变化区域 bool SendFramebufferUpdateRequest(SOCKET sock, bool incremental, uint16_t x, uint16_t y, uint16_t w, uint16_t h) { uint8_t buf[10]; buf[0] = 3; // 消息类型:FramebufferUpdateRequest buf[1] = incremental ? 1 : 0; // 增量标志 // 网络字节序写入坐标和尺寸 buf[2] = (x >> 8) & 0xFF; buf[3] = x & 0xFF; buf[4] = (y >> 8) & 0xFF; buf[5] = y & 0xFF; buf[6] = (w >> 8) & 0xFF; buf[7] = w & 0xFF; buf[8] = (h >> 8) & 0xFF; buf[9] = h & 0xFF; return send(sock, (char*)buf, 10, 0) == 10; }

参数里incremental是最容易踩坑的一个。第一次请求必须用全量(0),否则服务端不知道初始画面是什么,你收到的增量矩形可能引用不存在的背景。后续请求用增量(1),但要注意:如果客户端渲染速度跟不上,服务端可能积压多帧,这时候要么限制请求频率,要么在收到一帧后再发下一个请求,形成「请求-响应」的节拍,而不是无脑循环发。

2.3 编码方式选型:Raw、CopyRect、Hextile 各自适合什么场景

RFB 支持多种编码,源码里常见的是 Raw、CopyRect、Hextile,有些还会带 Tight 或 ZRLE。Raw 就是原始像素直接传,实现最简单,但带宽消耗最大,适合局域网或小分辨率。CopyRect 用于屏幕滚动场景——服务端告诉客户端「把某块区域复制到另一块」,客户端本地做内存拷贝,几乎不占带宽。Hextile 把画面切成 16x16 的瓦片,每个瓦片单独编码,适合有大片纯色区域的桌面。

选型逻辑很直接:局域网优先 Raw 或 Hextile,跨公网优先 Tight/ZRLE。但源码里如果只实现了 Raw,你也不用急着加——先把 Raw 跑通,确认握手、请求、渲染、输入这条链路没问题,再换编码。换编码时只需要改解码分支,协议框架不动。

// 根据编码类型分发到不同解码器 void DecodeRect(const uint8_t* data, size_t len, int encoding, int x, int y, int w, int h, uint8_t* framebuffer) { switch (encoding) { case 0: // Raw DecodeRaw(data, len, x, y, w, h, framebuffer); break; case 1: // CopyRect DecodeCopyRect(data, len, x, y, w, h, framebuffer); break; case 5: // Hextile DecodeHextile(data, len, x, y, w, h, framebuffer); break; default: // 未知编码,记录日志并跳过该矩形 break; } }

这段分发的关键是 encoding 编号必须和协议规范一致,Raw 是 0,CopyRect 是 1,Hextile 是 5。如果你自己扩展编码,编号要从 0xFFFFFF00 以上取,避免和标准冲突。解码后的像素要按 ServerInit 里协商的像素格式写入 framebuffer,格式不对会出现颜色错乱——这是新手最常见的翻车点之一。

3. 用 VC++ 把服务端和客户端跑起来:最小可运行工程的搭建步骤

3.1 工程目录与依赖:只留必要的,别一上来就堆库

一份能跑的 VNC VC++ 源码,目录结构通常分三块:core(协议编解码)、net(socket 封装)、ui(窗口和渲染)。依赖方面,Windows 下只需要 Winsock2,链接ws2_32.lib即可。不需要 MFC 也能跑,用 Win32 API 创建窗口和接收输入就够。我一般会建一个空的 Win32 项目,把 core 和 net 作为静态库编进去,ui 作为主工程。

# 在 VS 开发者命令行下编译 core 静态库 cl /c /EHsc /I include core/*.cpp lib /OUT:core.lib *.obj # 编译主程序并链接 winsock cl /EHsc /I include ui/main.cpp core.lib ws2_32.lib user32.lib gdi32.lib

编译参数里/EHsc启用标准 C++ 异常,/I include指定头文件路径。链接时ws2_32.lib是 Winsock 必须的,user32.lib和gdi32.lib用于窗口和位图渲染。如果你用 CMake,写一个CMakeLists.txt把这三块组织起来,跨版本编译会省心很多。

3.2 服务端:屏幕采集与帧缓冲更新的最小实现

服务端要做的事:监听端口、接受连接、发 ServerInit、响应 FramebufferUpdateRequest、把屏幕内容编码后发回。屏幕采集用 GDI 的BitBlt把桌面拷到内存 DC,再取像素数据。最小实现里用 Raw 编码,直接把像素按行发出去。

// 采集屏幕并发送 Raw 编码的帧缓冲更新 void SendRawFramebuffer(SOCKET sock, int x, int y, int w, int h) { HDC hScreen = GetDC(NULL); HDC hMem = CreateCompatibleDC(hScreen); HBITMAP hBmp = CreateCompatibleBitmap(hScreen, w, h); SelectObject(hMem, hBmp); BitBlt(hMem, 0, 0, w, h, hScreen, x, y, SRCCOPY); BITMAPINFO bmi = {0}; bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); bmi.bmiHeader.biWidth = w; bmi.bmiHeader.biHeight = -h; // 负值表示自上而下 bmi.bmiHeader.biPlanes = 1; bmi.bmiHeader.biBitCount = 32; bmi.bmiHeader.biCompression = BI_RGB; std::vector<uint8_t> pixels(w * h * 4); GetDIBits(hMem, hBmp, 0, h, pixels.data(), &bmi, DIB_RGB_COLORS); // 发送 FramebufferUpdate 消息头 + Raw 像素 // 消息头包含矩形数量、每个矩形的位置尺寸和编码类型 // 此处省略消息头打包,重点在像素采集 send(sock, (char*)pixels.data(), (int)pixels.size(), 0); DeleteObject(hBmp); DeleteDC(hMem); ReleaseDC(NULL, hScreen); }

biHeight设为负值是为了让像素从上到下排列,和 RFB 的坐标原点在左上角一致。如果设正值,图像会上下颠倒,这是 GDI 采集的经典坑。biBitCount用 32 位,对应 RFB 的 32bpp 像素格式,但要注意字节序——RFB 默认是大端,而 Windows 是小端,发之前需要做字节交换,或者协商时把像素格式设成小端。

3.3 客户端:解码渲染与输入事件回传

客户端收到 FramebufferUpdate 后,解析矩形列表,逐个解码,把像素写入本地 framebuffer,再通过 GDI 的StretchDIBits或SetDIBitsToDevice渲染到窗口。输入回传方面,鼠标事件对应 PointerEvent(消息类型 5),键盘事件对应 KeyEvent(消息类型 4)。

// 处理鼠标事件并回传给服务端 void SendPointerEvent(SOCKET sock, uint8_t buttonMask, uint16_t x, uint16_t y) { uint8_t buf[6]; buf[0] = 5; // 消息类型:PointerEvent buf[1] = buttonMask; // 按键掩码:bit0 左键,bit1 中键,bit2 右键 buf[2] = (x >> 8) & 0xFF; buf[3] = x & 0xFF; buf[4] = (y >> 8) & 0xFF; buf[5] = y & 0xFF; send(sock, (char*)buf, 6, 0); }

buttonMask是位掩码,左键是 1,中键是 2,右键是 4,同时按下就是按位或。坐标要映射到远端分辨率——如果本地窗口和远端分辨率不一致,需要按比例换算,否则鼠标位置会偏。键盘事件更复杂,需要把 Windows 虚拟键码转成 RFB 的 keysym,这个映射表在源码里通常单独一个文件,漏掉某个键就会导致远端按不出来。

4. 避坑与排查:VNC 源码调试中最容易翻车的五个点

4.1 连接建立后黑屏,日志显示握手成功

现象是客户端连上后窗口全黑,但抓包看握手和初始化都正常。原因通常是像素格式不匹配——服务端发的像素格式是 32bpp 大端,客户端按小端解析,颜色全乱或全黑。解决方法是检查 ServerInit 里的 pixelFormat 结构,确认 redShift、greenShift、blueShift 和字节序,客户端解码时严格按这个格式转换。我一般会在解码前打印一次像素格式,确认无误再往下走。

4.2 画面卡顿,帧率上不去

现象是画面能显示但明显卡顿,鼠标移动有延迟。原因多半是请求节拍不对——客户端无脑循环发 FramebufferUpdateRequest,服务端积压多帧,网络缓冲区满了之后延迟越来越大。解决方法是改成「收到一帧再发下一个请求」的同步模式,或者限制请求频率到 30fps 以下。另外检查是否用了增量模式,全量模式每帧传整屏,带宽吃不消。

4.3 键盘输入远端无响应

现象是鼠标能用但键盘按了没反应。原因通常是 keysym 映射缺失,或者 KeyEvent 的 downFlag 没设对。RFB 的 KeyEvent 里有一个 downFlag,按下是 1,松开是 0,两个事件都要发,只发按下不发松开,远端会认为键一直按着。解决方法是补全映射表,并确保按下和松开成对发送。

4.4 多显示器环境下只采集到主屏

现象是服务端只传了主显示器的画面,副屏内容看不到。原因是 GDI 的GetDC(NULL)只拿主屏 DC,多屏需要枚举显示器并分别采集,或者用虚拟桌面的整体 DC。解决方法是调用EnumDisplayMonitors获取每个显示器的区域,分别 BitBlt 后拼接到一个大的 framebuffer 里,再按矩形发送。

4.5 编译通过但运行时报 ws2_32.lib 找不到

现象是链接阶段报错,提示无法解析的外部符号。原因是项目没有链接 Winsock 库,或者WSAStartup没调用。解决方法是在链接器输入里加上ws2_32.lib,并在程序启动时调用WSAStartup(MAKEWORD(2,2), &wsaData),退出时调用WSACleanup()。这个坑在第一次建工程时几乎必踩,记住就行。

5. 进阶技巧:把 Raw 换成 Hextile,带宽能降多少

Raw 编码在 1920x1080 分辨率下,每帧要传约 8MB 数据,局域网还行,跨网络基本不可用。Hextile 把画面切成 16x16 的瓦片,每个瓦片根据内容选择编码方式——纯色瓦片只传一个像素值,复杂瓦片才传原始数据。实测在典型办公桌面场景下,Hextile 能把带宽降到 Raw 的 20% 到 40%,具体取决于画面复杂度。

实现 Hextile 解码的关键是理解瓦片的子编码标志:Raw、BackgroundSpecified、ForegroundSpecified、AnySubrects、SubrectsColoured。每个瓦片先读一个字节的标志位,根据标志决定后续读什么。下面是一个简化的解码框架:

// Hextile 瓦片解码:按标志位逐瓦片处理 void DecodeHextileTile(const uint8_t* data, size_t& offset, int tileX, int tileY, int tileW, int tileH, uint8_t* framebuffer, int fbWidth) { uint8_t flags = data[offset++]; uint32_t bg = 0, fg = 0; if (flags & 0x02) { // BackgroundSpecified bg = ReadPixel(data, offset); } if (flags & 0x04) { // ForegroundSpecified fg = ReadPixel(data, offset); } // 先用背景色填充整个瓦片 FillTile(framebuffer, fbWidth, tileX, tileY, tileW, tileH, bg); if (flags & 0x08) { // AnySubrects uint8_t count = data[offset++]; for (int i = 0; i < count; i++) { uint32_t color = (flags & 0x10) ? ReadPixel(data, offset) : fg; uint8_t xy = data[offset++]; uint8_t wh = data[offset++]; int sx = (xy >> 4) & 0x0F; int sy = xy & 0x0F; int sw = ((wh >> 4) & 0x0F) + 1; int sh = (wh & 0x0F) + 1; FillSubrect(framebuffer, fbWidth, tileX + sx, tileY + sy, sw, sh, color); } } }

标志位的含义:0x01 是 Raw,表示整个瓦片直接传原始像素;0x02 是背景色指定;0x04 是前景色指定;0x08 是有子矩形;0x10 是子矩形带颜色。子矩形的坐标和尺寸各占 4 位,所以单个子矩形最大 16x16,正好一个瓦片。解码时先填背景,再画子矩形,顺序不能反,否则背景会覆盖子矩形。

换编码后要做的验证:同一画面分别用 Raw 和 Hextile 传一帧,对比像素是否一致。我一般会写一个离屏的 framebuffer 对比函数,逐像素比较,有差异就打印坐标。另外注意 Hextile 的瓦片边界——画面宽高不是 16 的整数倍时,最后一个瓦片是不完整的,解码时要按实际宽高裁剪,否则会越界写内存。

带宽对比可以用一个简单的计数器:在 send 调用处累加发送字节数,跑 10 秒后打印平均值。办公场景下 Raw 大约 5 到 8 MB/s,Hextile 大约 1 到 2 MB/s,差距很明显。如果还要进一步压缩,可以在 Hextile 基础上加 zlib,但那就接近 Tight 编码了,实现复杂度会上升一个量级。

最后说个我自己的习惯:每次改编码或改协议分支,先用 Wireshark 抓一段包,确认字节流和协议规范对得上,再去看代码。远程控制这类程序,网络层的问题占七成,盯着代码猜不如直接看包。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/11 11:16:45

多视角三维重建实战:SfM+MVS端到端流程与参数调优

简介&#xff1a;本资源是一个面向计算机视觉学习者与三维重建初/中级开发者的多视角三维重建实战项目&#xff0c;聚焦于从图像序列到三维模型的完整算法实现与工程落地。项目涵盖特征匹配、立体视觉、稠密重建与纹理映射等核心环节&#xff0c;适用于虚拟现实建模、文化遗产数…

作者头像 李华
网站建设 2026/10/11 11:15:13

CTP穿透式账户测试全指南:从证书鉴权到一键通过

简介&#xff1a;面向CTP量化交易开发者与期货公司技术人员&#xff0c;这份资源提供了穿透式监管升级后的一键账户测试方案。内含可运行的AutoTrader程序及完整C工程源码&#xff0c;支持自动开仓、撤单与平仓&#xff0c;配置setting.ini即可对螺纹钢主力合约发起测试&#x…

作者头像 李华
网站建设 2026/10/11 11:13:55

等价类划分法从原理到实战:如何用最少用例提升黑盒测试覆盖

做测试时间久了你会发现&#xff0c;很多用例集堆得老高&#xff0c;缺陷率却还是上不去&#xff0c;问题多半出在用例设计上。黑盒测试里最难过的关不是“测什么”&#xff0c;而是“怎么用最少的用例把该测的测到位”。等价类划分法&#xff0c;就是黑盒测试中最基础也最实用…

作者头像 李华
网站建设 2026/10/11 11:13:34

DBeaver CE 24.2.2 Windows 可用性全指南:安装、配置与问题排查

简介&#xff1a;这是一份面向Windows平台的DBeaver Community Edition 24.2.2 免安装压缩包&#xff0c;属于开源数据库管理工具与SQL客户端&#xff0c;支持MySQL、PostgreSQL、Oracle等多种数据库。其定位是让用户无需经历复杂安装配置即可启动&#xff0c;尤其适合数据库初…

作者头像 李华
网站建设 2026/10/11 11:12:13

PLC底层系统依赖风险:从授权到期到国产替代的工程实践

1. 一条产线停摆背后的技术真相前阵子跟几个做自动化集成的老朋友吃饭&#xff0c;席间有人提到一个事&#xff1a;某工厂一条运行了三年多的产线&#xff0c;突然因为控制系统授权到期&#xff0c;整线趴窝了整整两天。设备没坏&#xff0c;电机没烧&#xff0c;机械臂也没卡死…

作者头像 李华