ARTICLE DETAIL

资讯详情

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

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

VC++实现VNC远程控制:RFB协议解析与工程实战 简介这份VNC远程控制程序VC源码基于RFB协议实现主要面向C网络编程和远程控制开发学习者也可用于课程设计或毕业设计。源代码采用控制端与被控端分离的结构围绕网络通信、屏幕图像捕捉、图像编码解码、键盘鼠标事件处理与多线程同步等核心环节展开有助于深入理解VNC的远程桌面控制机制。压缩包中共有631个文件体积仅2.27MB包含C/C源文件与头文件、Visual C工程文件、说明文档以及图标和位图资源文件类型多样目录组织清晰便于按需查阅与二次编译。已有851人学习/下载。通过系统研读可以掌握套接字编程、RFB数据编解码、屏幕捕获与消息循环等具体实现还可以参考其多线程同步与扩展接口设计为后续二次开发或自主编写远程控制工具奠定基础。 手头有VC功底又想自己搞一套远程控制程序拿VNC源码来研究是个特别对路的做法。VNCVirtual Network Computing这套远程桌面协议在Windows/Linux/macOS上都有现成实现但真正想懂它里面的屏幕捕获、像素编码、网络传输和鼠标键盘事件注入还得是自己把源码啃一遍。这篇文章我就以VC实现VNC远程控制程序为主线从协议原理到实际工程搭建把整个流程拆开讲透不管是想照抄作业还是想二次开发都能有个清晰的下手方向。1. 项目概览为什么要用VC重写一个VNC1.1 这个项目解决什么问题远程控制的需求非常典型你在办公室要操作家里那台机器或者你管理着一堆工控机、服务器不想每次都跑机房插显示器键盘。市面上有TeamViewer、向日葵、ToDesk但这类商业软件存在两个问题一是数据传输走第三方服务器内网环境不可控二是想定制功能很困难比如嵌入自己的认证系统、自定义编码策略、精简成几百KB的绿色版。自己用VC实现一个VNC Server/Client最直接的好处就是协议全在自己手里内网直连、无中转还能按需裁剪和加固。另一个层面的意义是学习价值。VNC协议RFB协议Remote FrameBuffer设计得很轻巧不同于RDP那种极其复杂的协议RFB的核心就是“服务端不断把屏幕变化发给你你把鼠标键盘事件发回去”文本总共也就几十页规范。用VC实现一遍GDI屏幕捕获、socket异步通信、多线程同步、图像编码算法这些东西全部会串起来对Windows桌面开发能力的提升非常明显。1.2 面向读者与技术栈这个项目最适合两类人一类是刚接触远程控制/屏幕共享领域的VC开发者想通过完整的源码工程理解整个链路另一类是有C/S开发经验、想在现有系统里集成远程协助功能的工程师。技术栈上我建议使用Visual Studio 2017或2019创建Win32控制台或MFC工程都行但不需要用到MFC的界面库纯Win32 API Winsock就足够。屏幕上用GDI的BitBlt截取变化区域网络上用阻塞式socket加select模型这两条路走通之后再去优化成异步I/O或完成端口。提示如果对RFB协议完全没概念强烈建议先把官方RFC 6143文档过一遍再动代码。这个文档不长但把版本协商、安全类型、像素格式、编码类型、消息格式都定义清楚了后面看源码会省很多力气。2. 整体架构设计RFB协议与模块划分2.1 RFB协议核心逻辑RFB协议是VNC的灵魂搞清楚它整个项目就成功了一半。整个流程分几步先是协议版本协商客户端发来“RFB 003.008”这样的12字节字符串服务端选择支持的最高版本回应然后是安全类型协商服务端列出一组安全类型编号客户端从中选一个0表示失败、1表示无认证、2表示VNC密码认证接下来是安全结果校验之后客户端发送初始化消息是否共享桌面以及期望的像素格式和编码方式服务端回复桌面尺寸和桌面名称。真正进入工作循环后消息分为服务端到客户端、客户端到服务端两类。服务端主要发FramebufferUpdate帧缓冲更新携带矩形区域和像素数据客户端主要发FramebufferUpdateRequest请求某个区域的数据带incremental标志时表示只发增量变化、KeyEvent键盘事件、PointerEvent鼠标事件以及ClientCutText客户端剪贴板。整个协议是“客户端主动拉、服务端推送”的模型incremental标志非常关键直接决定了你是一次全屏刷新还是只发改变过的脏矩形。每秒能跑多少帧、占用多少带宽很大程度上就看这个增量逻辑做得好不好。2.2 服务端与客户端双端结构源码工程我建议拆成两个独立模块再加一个共享的协议库。服务端Server的核心组件有三个屏幕捕获器ScreenCapturer、编码器Encoder、网络服务NetworkService。屏幕捕获器定时截屏把新旧帧做比较输出变化区域列表编码器把变化区域按指定编码类型压缩成字节流网络服务负责监听端口、处理客户端连接、按协议格式发送数据。客户端Client的逻辑更简单核心是把收到的像素数据画出来以及捕获本地输入发给服务端。VC下显示端用一个自绘窗口WM_PAINT里把解码后的位图贴上去输入捕获就是挂一个键盘钩子/鼠标钩子把消息转成RFB事件消息。协议库则放公共的头文件和工具函数比如像素格式转换、颜色值换算、编码/解码的公共代码两边共用避免重复实现。2.3 为什么选VC从崩溃现场到远程调试选VC还有一个非常实际的场景当被控端程序崩溃的时候我们需要能够在崩溃现场定位问题远程桌面本身就是最好的调试工具。VC里通过MiniDumpWriteDump生成转储文件再配合Visual Studio的“调试-加载转储”功能能直接看到崩溃时各个线程的调用栈。而且VC生成的本地代码性能远高于解释型语言在做视频级别的画面刷新时CPU占用和内存占用都明显更可控这对远程控制的实时性很重要。3. 核心模块与关键实现3.1 屏幕捕获GDI/BitBlt与性能优化屏幕捕获是用GDI还是DXGI取决于你要不要抓取DX内容。早期的VNC源码大多用BitBlt代码大概是这样HDC hScreenDC GetDC(NULL); HDC hMemDC CreateCompatibleDC(hScreenDC); HBITMAP hBitmap CreateCompatibleBitmap(hScreenDC, width, height); HGDIOBJ hOldBmp SelectObject(hMemDC, hBitmap); BitBlt(hMemDC, 0, 0, width, height, hScreenDC, 0, 0, SRCCOPY | CAPTUREBLT); // 读取像素数据 GetDIBits(hMemDC, hBitmap, 0, height, pixels, bmi, DIB_RGB_COLORS);这套方案的优点是兼容性好Windows XP到Windows 11都能跑。但也别指望它能多快纯BitBlt全屏捕获在1080p下大概要5-10毫秒再加上编码和传输只能做到每秒10-15帧左右。想要丝滑一点有两条思路一是只捕获变化区域。把屏幕划分成网格比如64x64的块每次截屏后和上一帧做像素级比较只把变化的块编码发送。这就是VNC增量更新的基础。改进方式是用两帧之间的掩码比较或者调用GetTickCount计算光标移动区域再把鼠标位置变化区域标记为脏矩形。二是用双缓冲加三缓冲。屏幕先拷贝到内存DC编码线程从这块缓冲区里读数据这样捕获和编码可以并行不会互相卡住。我实际项目里是起了一个捕获线程专门做BitBlt编码线程则从缓冲区拿数据两个线程通过事件对象同步帧率比单线程提升了一倍左右。注意BitBlt抓不到硬件加速的DirectX/OpenGL画面这些画面会是黑的或者残影。如果被控端要跑3D应用就得改用Desktop Duplication APIWindows 8这也是现代VNC实现的主流方案。3.2 像素编码Hextile与ZRLE编码器的任务是把裸像素压小再传出去。RFB协议定义了多种编码类型常用的是Raw原始数据、Hextile16x16格子、ZRLEzlib压缩游程编码、JPEG有损。初学者最合适先落地Hextile它的思路非常巧妙把矩形区域拆成若干个16x16的小格子每个格子独立编码。格子内部如果背景单一就用BackgroundSpecified子编码只写一个背景色如果有多个颜色就用SubrectsColoured子编码列出各子矩形的位置和颜色。这样在处理文字界面、白底表单时压缩率非常高而且解码极快。ZRLE更进阶它先把像素数据按行切块再用zlib压缩。核心代码可以这样组织z_stream zs { 0 }; deflateInit(zs, Z_DEFAULT_COMPRESSION); // 依次处理每个tile for each tile { BYTE subencoding tile_has_single_color ? 1 : 0; // 写入subencoding字节 if (tile_has_single_color) { // 写入4字节颜色值 } else { // 写入调色板 // 写入索引像素数据 } } // deflate数据流 deflate(zs, Z_FINISH);这里有个常见的性能坑zlib的压缩等级不要开太高Z_BEST_SPEED等级下压缩率只差几个百分点但速度能快好几倍。远程控制是低延迟场景优先保证帧率压缩率可以妥协。另外ZRLE里对16x16的小格子其实还有纯色优化做的时候别忽略纯色块在桌面UI里特别常见。3.3 网络传输与协议握手网络层面就是标准TCP服务端监听5900端口默认VNC端口也可以自定义。VC下用Winsock2流程是WSAStartup、socket、bind、listen、accept。握手阶段要注意字节序RFB协议里所有16位/32位整型默认都是大端big-endian和x86的小端little-endian相反必须用htons/htonl转换否则像素格式和坐标全都会乱掉。像素格式协商的核心结构体是这样的typedef struct { BYTE bitsPerPixel; // 每像素位数通常是16、24、32 BYTE depth; // 颜色深度比如24 BYTE bigEndianFlag; // 0表示小端 BYTE trueColourFlag; // 1表示真彩色 UINT16 redMax; UINT16 greenMax; UINT16 blueMax; BYTE redShift; BYTE greenShift; BYTE blueShift; BYTE padding[3]; } PixelFormat;服务端要先发自己的原生格式也可以根据客户端的SetPixelFormat消息切换格式。这里最容易出错的是padding字段和结构体对齐。如果在VC里直接按这个结构memcpy会因为默认的4字节对齐产生错位。解决方案是使用pragma pack(push, 1)或者干脆把每个字节单独写进缓冲区不依赖结构体。很多人第一次连VNC客户端黑屏/花屏查到最后都是这里出的问题。3.4 鼠标键盘事件注入客户端把鼠标键盘事件发回服务端后服务端需要在被控机上模拟输入。VC里最常用的就是SendInput。鼠标绝对定位时要注意RFB里的坐标范围是0到屏幕宽度/高度而SendInput的MOUSEINPUT里dx/dy是0到65535的绝对坐标需要做一次映射INPUT input { 0 }; input.type INPUT_MOUSE; input.mi.dx (LONG)(rfb_x * 65535 / (screen_width - 1)); input.mi.dy (LONG)(rfb_y * 65535 / (screen_height - 1)); input.mi.dwFlags MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE; SendInput(1, input, sizeof(INPUT));键盘事件相对简单但要注意RFB里传递的是X11 keysym需要维护一张keysym到虚拟键码VK_*的映射表。CtrlAltDel这种组合键在Windows下默认不能由SendInput直接注入得调用SendInput模拟一次CtrlAltEsc或者用SA系列API绕过UIPI限制。这个坑很多新手会踩到连接后按CtrlAltDel没反应就是Session隔离和UAC权限导致的。3.5 安全认证机制与常见漏洞规避VNC最常被吐槽的就是安全问题。默认的VNC认证Security Type 2用的是DES加密质询响应但密钥只有56位而且VNC的实现里密钥生成方式是把密码每个字节按位反转再重复填充到8字节所以密码实际上只有前8位有效暴力破解并不困难。更严重的是很多软件把“无认证”Security Type 1作为默认配置这直接把设备暴露在网络里网上扫描到开放5900端口的设备基本就能直接连进去。自己写源码时建议至少做三层加固一是不要支持无认证模式密码长度下限设到8位二是在TCP层做IP白名单只允许指定网段的客户端连接三是对传输层数据做AES加密。RFB 3.8协议本身不支持AES但可以仿照TLS的思路在版本协商后先走一段自定义的加密握手后续所有数据都走加密通道。如果只是内网使用也可以不加密但一定要确保被控端防火墙只放行可信来源的IP。4. 实操过程从VNC源码工程到跑通首个连接4.1 工程搭建与依赖处理我建议把解决方案分成三个项目SharedLib协议公共库、VNCServer控制端/服务端、VNCViewer被控端/客户端注意这里和VNC术语相反VNC里Server是被控端Viewer是控制端。SharedLib里放协议常量、消息结构体、像素格式转换函数作为静态库编译。工程属性里要设置字符集为Unicode链接器加上ws2_32.lib。如果用了zlib就把zlib源码直接编进SharedLib避免额外的DLL依赖。这里的zlib编译有个细节zlib的源码在VC下需要先运行bld_ml32.bat生成汇编对象否则会链接报错。没耐心折腾的可以直接用zlib官方提供的vc工程文件编译一个静态库。4.2 关键代码走读从连接建立到帧发送服务端等待连接的核心流程是“监听socket - 接受客户端 - 依次完成版本协商/安全协商/初始化 - 进入主循环”。Version协商这段代码非常直接char version[13] RFB 003.008\n; send(clientSock, version, 12, 0); recv(clientSock, clientVersion, 12, 0); // 如果客户端版本低于3.8建议直接拒绝 if (memcmp(clientVersion, RFB 003.008, 11) ! 0) { closesocket(clientSock); return -1; }认证之后客户端会发来SetPixelFormat和SetEncodings消息服务端解析完就进入主循环。主循环一般这样写每收到一个FramebufferUpdateRequest就检查它的incremental标志如果是0就全屏发送如果是1就只发脏矩形然后把屏幕捕获、编码、发送集中在一个工作线程里完成。我在工程里给脏矩形维护了一个链表捕获线程每次更新屏幕后把变化区域追加到链表中发送线程从链表里取矩形再经过编码器发送出去。这里要处理好同步我是用的临界区CRITICAL_SECTION锁粒度控制在“链表pop”这一步不做整帧锁这样并发性能更好。4.3 联调验证与数据帧观察跑通的第一步建议不用自己写客户端直接用RealVNC Viewer或者TigerVNC连过来验证。被控端启动后监听端口打印出来在VNC Viewer里输入IP端口看能不能顺利握手、认证、看到桌面。若能看到桌面说明你的协议栈已经基本OK。接下来再用Wireshark抓包过滤tcp.port 5900看消息序列是否符合RFC 6143的流程重点关注FramebufferUpdate里矩形坐标和像素格式是否和屏幕参数一致。我调试时候的一个经验把像素格式强制设成32bpp、编码类型设成Raw先排除编码和解码的干扰。确认这条通路完全正确后再切成Hextile/ZRLE。要是黑屏先在Raw下排查要是花屏优先怀疑像素格式的字节序和颜色掩码要是延迟高再去看编码器和脏矩形逻辑。这条排查路径能节省大量时间。5. 常见问题与排查技巧5.1 连接失败端口、防火墙与IP白名单这个场景十有八九是防火墙拦了。Windows防火墙默认不开放5900需要在入站规则里新建一条TCP 5900端口的放行规则。如果是公司网络还要确认路由器/NAT有没有做端口映射。另一个常见问题是服务端程序没监听成功很可能端口被别的进程占了用netstat -ano | findstr 5900能看到PID再确认进程。IP白名单写在accept之后如果不是白名单内地址就直接断开这能挡住网络上的扫描别偷懒跳过。5.2 画面花屏、黑屏、更新不全花屏先查像素格式。检查bitsPerPixel和depth是否匹配redMax/greenMax/blueMax以及各个shift是否准确。很多时候是自己实现了像素格式转换但转换公式写错了导致颜色通道错乱。黑屏则优先检查BitBlt的区域坐标有些显卡驱动下BitBlt会返回0表示失败建议每次调用后检查返回值必要时改用PrintWindow函数。更新不全通常是脏矩形合并算法有bug比如没有把重叠矩形合并导致有些区域虽然变动了但被后续矩形覆盖时漏掉了。5.3 鼠标坐标偏移与DPI缩放Windows上DPI缩放是个大坑如果被控机开了125%或150%缩放而VNC进程没有声明DPI感知系统会虚拟化它的坐标导致你看到的桌面逻辑分辨率和实际物理分辨率不一致。解决办法是在可执行文件上添加DPI感知声明最简单的做法是进程启动时调用SetProcessDPIAware()。如果想做得更完善在工程里的manifest文件里加 true/pm 。5.4 密码认证失败与DES陷进VNC认证的DES实现和我们常见的桌面加密用法不一样它没有填充逻辑密码不足8字节时直接在密码后补0密钥生成时先把密码每个字节的位序反转然后用这个反转后的密码重复填充成8字节。这块我当初对着指定实现调了很久才发现是字节序问题。建议对照TightVNC的vncEncryptBytes函数来看它的实现是完全正确且公开的直接移植过来最省事。5.5 性能瓶颈带宽与帧率取舍内网环境通常网速不是瓶颈帧率瓶颈在屏幕捕获的效率和编码器的速度上。实测下来纯GDI方案在1080p下CPU占用约15%-20%能跑到10-15帧在4K分辨率下纯BitBlt捕获就要占掉30%的CPU时间加上ZRLE编码帧率会掉到5帧左右。如果被控机就是普通办公电脑建议把最大帧率限制在15帧并开启“仅在有变化时发送”的逻辑没有画面变动时直接不发送任何数据这对带宽和CPU都是巨大节省。场景推荐配置预期体验内网办公、运维Hextile 15fps上限流畅操作文字界面跨网络连入ZRLE 10fps上限带宽占用低有轻微延迟视频播放/3DDesktop Duplication JPEG画面流畅但CPU占用高低配机器Raw 5fps上限能操作但较卡6. 写在最后我踩过的坑和后续扩展建议最后聊聊我自己在写这个项目时印象最深的几个细节。第一是千万别迷信网上的“完整源码”很多所谓的VNC VC源码是从旧版本裁剪来的协议版本还停留在3.3现代客户端连不上。我后来把所有逻辑都对照RFC 6143重写了一遍才彻底解决了兼容问题。第二是一定要留好日志模块把每次握手的版本、安全类型、像素格式、编码列表写进日志排查问题时这些信息比调试器还管用。第三是不要把编码器和网络发送放在同一个线程里做得很重实测下来分成两个线程帧率提升非常明显代码复杂度其实没增加多少。后续如果你想继续扩展我建议优先做几个方向把屏幕捕获改成Desktop Duplication API并把捕获结果转成BGRA纹理再编码在客户端增加H.264硬编解码支持在局域网场景下可以做到接近实时再就是实现文件传输和剪贴板双向同步这样远程协助才算真正可用。VNC这套动辄几百KB的源码里隐藏的是从像素到网络的完整链路每一条都值得多花时间去研究。本文还有配套的精品资源点击获取
返回列表