TigerVNC 源码架构深度解析:从协议状态机到拥塞控制的设计哲学

本文基于 TigerVNC 1.16.80 源码(2026 年当前主分支),从构建系统、核心库分层、RFB 协议状态机、编码决策引擎、多线程解码器、屏幕变化追踪、TCP Vegas 拥塞控制、平台适配层等多个维度,进行代码级的深度剖析。适合对远程桌面协议实现、高性能网络编程感兴趣的开发者。


前言

TigerVNC 是 Linux/Unix 生态中最主流的开源 VNC 实现,Fedora、RHEL、CentOS、Ubuntu 等发行版默认打包的 VNC 方案都是它。项目脱胎于 RealVNC 4 和 X.org 的代码库,2009 年从 TightVNC 分叉独立后活跃维护至今,当前的主要维护者是 Cendio AB 的 Pierre Ossman。

因为工作关系,我最近系统性地通读了 TigerVNC 的核心源码。整体感受:代码组织非常干净,分层抽象做得好,关键路径上的性能优化也很到位。尤其是拥塞控制和编码自适应选择这两块,有不少值得借鉴的工程经验。

这篇文章把分析结果整理出来,尽量做到"有图有码有真相",不是简单的目录罗列。


一、构建系统与编译约束

1.1 CMake 构建

TigerVNC 使用 CMake >= 3.10.0 构建。一个比较值得注意的约束:项目显式拒绝 MSVC 编译器,Windows 下必须使用 MinGW-w64 工具链。

cmake 复制代码
# CMakeLists.txt 顶层
if(MSVC)
  message(FATAL_ERROR "TigerVNC cannot be built with Visual Studio. Please use MinGW")
endif()

原因推测和 VNC 项目大量使用 POSIX 接口(fd_setgettimeofdaysys/socket.h 等)有关,MinGW 对这些接口的兼容比 MSVC 好得多。

1.2 C/C++ 标准与编译选项

cmake 复制代码
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=gnu99")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11")

C++ 标准采用 GNU++11。选择 GNU++11 而非标准 C++11,是因为代码中使用了 __attribute__((format)) 等 GCC 扩展。在编译警告方面,项目开启了相当严格的检查:

cmake 复制代码
add_compile_definitions(_FORTIFY_SOURCE=2)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -Wformat=2 -Wvla")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wformat=2 -Wvla")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wzero-as-null-pointer-constant")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wsuggest-override")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wshadow")

Debug 模式下直接 -Werror(警告当错误),并且开启了 Address Sanitizer 和 Thread Sanitizer 的可选支持:

cmake 复制代码
option(ENABLE_ASAN "Enable address sanitizer support" OFF)
option(ENABLE_TSAN "Enable thread sanitizer support" OFF)

另一个有意思的细节:Release 构建保留 assert 断言

cmake 复制代码
# We want to keep our asserts even in release builds so remove NDEBUG
set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -UNDEBUG")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -UNDEBUG")

在 Release 构建中保留 assert 不算常见做法。对于 VNC 这种处理网络数据和像素缓冲的程序来说,这个决策是合理的------宁可断言失败后 crash 重启,也不要默默输出花屏数据或产生内存越界。

1.3 顶层目录结构

复制代码
tigervnc-src/
├── common/        ← 跨平台核心库(协议、网络、编解码)------ 代码量大头
├── vncviewer/     ← 跨平台 VNC 客户端(基于 FLTK)
├── win/           ← Windows 专用服务端(winvnc,标记为 unmaintained)
├── unix/          ← Unix/Linux 服务端(x0vncserver、w0vncserver、vncserver)
├── java/          ← Java 版 Viewer(可选构建)
├── tests/         ← 单元测试(GTest 框架)
├── cmake/         ← CMake 自定义 Find 模块
├── po/            ← 国际化翻译文件(gettext .po)
├── media/         ← 图标等静态资源
├── contrib/       ← 第三方贡献代码
├── doc/           ← 文档
└── release/       ← 发布打包脚本

核心设计原则:common/ 下是纯粹的协议逻辑,零平台依赖win/unix/ 只负责各自平台的屏幕捕获和输入注入。vncviewer/ 通过 FLTK 实现跨平台 GUI,少量平台特定代码用条件编译隔离(Surface_X11.cxxSurface_Win32.cxxSurface_OSX.cxx)。


二、核心库四层架构

common/ 目录内部按职责分为四个子模块,形成一个自底向上的清晰分层:

复制代码
┌─────────────────────────────────────────────────────────┐
│              rfb --- RFB 协议层(核心,代码量最大)          │
│  编解码器 · 安全认证 · 连接状态机 · 更新追踪 · 拥塞控制   │
├─────────────────────────────────────────────────────────┤
│              network --- 网络传输层                        │
│          TCP Socket · Unix Domain Socket                │
├─────────────────────────────────────────────────────────┤
│              rdr --- 数据流 I/O 层                        │
│    Buffered · Zlib · AES · TLS · Hex · Fd · Mem        │
├─────────────────────────────────────────────────────────┤
│              core --- 基础设施层                          │
│    Config · Logger · Timer · Region/Rect · i18n        │
└─────────────────────────────────────────────────────────┘

上层只依赖下层,不存在反向依赖。这使得每一层都可以独立测试和理解。

2.1 基础设施层 common/core/

提供与协议无关的通用工具,主要包括:

模块 类/文件 职责
配置 Configuration 全局参数管理,支持命令行和配置文件
日志 Logger / LogWriter 日志框架,三种后端:syslog、文件、stdio
定时器 Timer 事件循环中的定时器抽象,支持 Callback 接口
区域 Region / Rect 屏幕区域几何运算(并、交、差),脏区追踪的核心数据结构
国际化 i18n / gettext 多语言翻译
字符串 string format() 等格式化工具
时间 time 时间获取和计算

RegionRect 是高频使用的关键数据结构。 VNC 的核心工作就是追踪"屏幕上哪些像素发生了变化",所有的脏区计算------合并多个变化区域、求交集裁剪、计算差集------都依赖 Region 类。它内部使用了 pixman 库的区域运算实现,性能很好。

2.2 数据流 I/O 层 common/rdr/

这一层是 TigerVNC 中设计得最精巧的部分之一。它定义了抽象基类 InStream / OutStream,然后通过装饰器模式叠加不同的传输功能。

先看一下 InStream 的核心接口设计:

cpp 复制代码
// common/rdr/InStream.h
class InStream {
public:
    // 返回当前缓冲区中可直接读取的字节数
    inline size_t avail() { return end - ptr; }

    // 确保至少有 length 字节可用,不足时调用 overrun() 拉取更多数据
    inline bool hasData(size_t length);

    // 支持 restore point,用于协议解析中的回溯
    inline bool hasDataOrRestore(size_t length);
    inline void setRestorePoint();
    inline void gotoRestorePoint();

    // 基础读取
    inline uint8_t readU8();
    inline uint16_t readU16();
    inline uint32_t readU32();
    inline void readBytes(uint8_t* data, size_t length);

protected:
    // 子类实现:从底层拉取数据到缓冲区
    virtual bool overrun(size_t needed) = 0;

    uint8_t* ptr;
    uint8_t* end;
};

注意两个设计细节:

  1. Restore Point 机制 :协议解析时,可能需要先"试探性"读取一些字节来判断消息类型,如果发现数据不够,需要回退到之前的位置等待更多数据到达。hasDataOrRestore() + setRestorePoint() 优雅地处理了这种场景。

  2. Debug 模式下的使用正确性检查RFB_INSTREAM_CHECK 宏在 Debug 构建中启用,追踪每次 hasData() 调用后实际读取的字节数是否与声明的一致,帮助发现协议解析中的逻辑错误。

具体实现的 Stream 子类:

Stream 类型 功能 使用场景
FdInStream / FdOutStream 基于文件描述符的原始 I/O TCP 连接的底层传输
BufferedInStream / BufferedOutStream 在底层流上叠加读写缓冲 减少系统调用次数
MemInStream / MemOutStream 内存中的读写 解码缓冲、临时数据暂存
ZlibInStream / ZlibOutStream 透明 Zlib 压缩/解压 ZRLE 编码、Tight 编码的数据流
AESInStream / AESOutStream 透明 AES 加密/解密 RSA-AES 安全认证后的加密通道
TLSInStream / TLSOutStream 透明 TLS 加密 TLS 安全类型
HexInStream / HexOutStream Hex 编码转换 调试和日志
TLSSocket TLS 安全套接字 建立 TLS 连接后的套接字替换
RandomStream 安全随机数生成 密钥交换中的随机数
FileInStream 文件读取 读取密码文件等

这种设计的核心优势:上层协议代码完全不感知底层传输的复杂性。 RFB 协议层调用 readU32()writeBytes() 时,不需要关心数据是经过 Zlib 压缩的、AES 加密的、还是走 TLS 隧道的。新增一种传输方式(比如未来要支持 Zstd 压缩),只需要实现一个新的 Stream 子类。

在安全握手阶段,CSecurityTLSCSecurityRSAAES 会通过 setStreams() 方法替换掉原始的 InStream/OutStream,把加密流"套"在外面:

cpp 复制代码
// CConnection 提供的接口
void setStreams(rdr::InStream* is, rdr::OutStream* os);

调用链大致是:

复制代码
初始:  FdInStream → InStream*
TLS 后:FdInStream → TLSInStream → InStream*(外层指针不变,底层已替换)

2.3 网络层 common/network/

相对薄的一层,封装了平台无关的套接字接口:

  • Socket --- 抽象基类,定义 getFd()shutdown()close() 等接口
  • TcpSocket --- TCP 套接字,支持 IPv4/IPv6 双栈,getPeerEndpoint() 返回对端地址字符串
  • UnixSocket --- Unix 域套接字(仅 Unix 平台),用于本地进程间通信

Socket 的设计比较标准,没有太多特殊之处。需要注意的是 TigerVNC 的服务端(VNCServerST)使用 单线程 + select/poll 事件循环,而不是每连接一个线程,所以 Socket 的非阻塞 I/O 很重要。


三、RFB 协议层深度剖析

common/rfb/ 是整个项目代码量最大、设计最核心的模块。RFB(Remote Framebuffer)协议定义在 RFC 6143 中,TigerVNC 在此基础上扩展了多种编码类型和安全类型。

3.1 连接状态机

CConnection(客户端侧)和 SConnection(服务端侧)是两个核心类,各自实现了一个完整的状态机。

客户端状态机 CConnection
cpp 复制代码
// CConnection 内部状态枚举
enum stateEnum {
    RFBSTATE_UNINITIALISED,
    RFBSTATE_PROTOCOL_VERSION,    // 协商协议版本
    RFBSTATE_SECURITY_TYPES,      // 协商安全类型
    RFBSTATE_SECURITY,            // 执行安全认证
    RFBSTATE_SECURITY_RESULT,     // 读取认证结果
    RFBSTATE_SECURITY_REASON,     // 读取拒绝原因
    RFBSTATE_INITIALISATION,      // 交换初始化信息
    RFBSTATE_NORMAL,              // 正常运行,消息循环
    RFBSTATE_CLOSING,             // 关闭中
    RFBSTATE_INVALID
};

processMsg() 是状态机的驱动函数,每次有数据可读时调用:

cpp 复制代码
bool CConnection::processMsg()
{
    switch (state_) {
    case RFBSTATE_PROTOCOL_VERSION:  return processVersionMsg();
    case RFBSTATE_SECURITY_TYPES:    return processSecurityTypesMsg();
    case RFBSTATE_SECURITY:          return processSecurityMsg();
    case RFBSTATE_SECURITY_RESULT:   return processSecurityResultMsg();
    case RFBSTATE_SECURITY_REASON:   return processSecurityReasonMsg();
    case RFBSTATE_INITIALISATION:    return processInitMsg();
    case RFBSTATE_NORMAL:            return reader_->readMsg();
    // ...
    }
}

每个 process*Msg() 方法返回 booltrue 表示该步骤已完成并进入下一个状态,false 表示数据不足、需要等待更多数据到达。这种设计使得状态机可以在非阻塞 I/O 环境下正确运行。

版本协商的具体流程 (以 processVersionMsg() 为例):

cpp 复制代码
bool CConnection::processVersionMsg()
{
    char verStr[27];
    int majorVersion, minorVersion;

    if (!is->hasData(12))      // 等待 12 字节 "RFB xxx.yyy\n"
        return false;

    is->readBytes((uint8_t*)verStr, 12);
    verStr[12] = '\0';

    if (sscanf(verStr, "RFB %03d.%03d\n", &majorVersion, &minorVersion) != 2) {
        throw protocol_error("Reading version failed, not an RFB server?");
    }

    // 只支持 3.3 ~ 3.8,超出范围则向下协商到 3.8
    if (server.beforeVersion(3,3)) {
        throw protocol_error("unsupported RFB protocol version");
    } else if (server.beforeVersion(3,7)) {
        server.setVersion(3,3);
    } else if (server.afterVersion(3,8)) {
        server.setVersion(3,8);
    }

    // 回复客户端选择的版本
    sprintf(verStr, "RFB %03d.%03d\n", server.majorVersion, server.minorVersion);
    os->writeBytes((const uint8_t*)verStr, 12);
    os->flush();

    state_ = RFBSTATE_SECURITY_TYPES;  // 进入下一阶段
    return true;
}

安全类型协商processSecurityTypesMsg())的逻辑更有意思。对于协议 3.3,服务端直接发一个 U32 指定唯一的安全类型;对于 3.7+,服务端发一个列表,客户端从中选择第一个匹配的。客户端的选择逻辑是按服务端的优先级顺序(而不是客户端自己的偏好):

cpp 复制代码
for (int i = 0; i < nServerSecTypes; i++) {
    uint8_t serverSecType = is->readU8();
    // 使用第一个匹配的类型------服务端的顺序即优先级
    if (secType == secTypeInvalid) {
        if (std::find(secTypes.begin(), secTypes.end(), serverSecType) != secTypes.end())
            secType = serverSecType;
    }
}
服务端状态机 SConnection

SConnection 的状态机结构与客户端对称,但增加了一些服务端特有的功能:

  • queryConnection() --- 允许外部决定是否接受连接(用于用户确认弹窗)
  • approveConnection() --- 回复连接审批结果
  • authenticated() --- 认证通过后调用,设置访问权限

SConnection 构造时接收 AccessRights 参数,控制客户端拥有哪些权限(查看、控制输入、剪贴板操作等)。

cpp 复制代码
class SConnection : public SMsgHandler {
public:
    SConnection(AccessRights accessRights);

    void setStreams(rdr::InStream* is, rdr::OutStream* os);
    void initialiseProtocol();
    bool processMsg();

    void approveConnection(bool accept, const char* reason);
    bool authenticated();

    // SMsgHandler 回调
    void setPixelFormat(const PixelFormat& pf) override;
    void setName(const char* name) override;
    void fence(uint32_t flags, unsigned len, const uint8_t data[]) override;
    void enableContinuousUpdates(bool enable, int x, int y, int w, int h) override;
    void frameBufferUpdateRequest(const Rect& r, bool incremental) override;
    void keyEvent(uint32_t keysym, uint32_t keycode, bool down) override;
    void pointerEvent(const Point& pos, uint16_t buttonmask) override;
    void clientCutText(const char* str) override;
    // ...
};

3.2 编码决策引擎 EncodeManager

编码是 VNC 性能的关键。EncodeManager 负责为每个屏幕更新矩形选择最优的编码方式。这个过程比我预想的要复杂不少。

编码器分类体系

TigerVNC 内部将编码器分为两个维度:

按编码方式(EncoderClass):

cpp 复制代码
enum EncoderClass {
    encoderRaw,        // 原始像素
    encoderRRE,        // Rise-and-Run-length Encoding
    encoderHextile,    // Hextile 编码
    encoderTight,      // Tight 编码(核心)
    encoderTightJPEG,  // Tight + JPEG 有损压缩
    encoderZRLE,       // ZRLE(Zlib Run-Length Encoding)
    encoderJPEG,       // 纯 JPEG 编码
    encoderClassMax,
};

按矩形内容类型(EncoderType):

cpp 复制代码
enum EncoderType {
    encoderSolid,        // 纯色矩形
    encoderBitmap,       // 2 色位图
    encoderBitmapRLE,    // 2 色位图 + RLE 压缩
    encoderIndexed,      // 调色板索引
    encoderIndexedRLE,   // 调色板索引 + RLE
    encoderFullColour,   // 全彩色
    encoderTypeMax,
};

两个维度交叉,形成一个 EncoderClass × EncoderType 的矩阵。每个编码器在矩阵中的特定位置有各自的统计信息,运行结束后可以输出详细的压缩效率报告。

编码选择的核心流程

EncodeManager::doUpdate() 是编码选择的入口。对于每个变化的矩形区域,处理流程如下:

复制代码
1. 先尝试检测纯色矩形 → writeSolidRects()
   ↓ 如果不是纯色
2. 对矩形进行 Palette 分析(统计颜色数量和分布)
   ↓ 根据颜色数量
3. 选择编码类型:
   - 1 色 → Solid
   - 2 色 → Bitmap(Tight 的 MonoRect 路径)
   - ≤256 色 → Indexed(Tight 的 IndexedRect 路径)
   - >256 色 → FullColour(Tight 的 FullColourRect 或 JPEG)
4. 根据客户端偏好和服务端配置,选择具体编码器

纯色矩形的检测是一个值得关注的优化:

cpp 复制代码
// 检测参数
static const int SolidSearchBlock = 16;     // 以 16×16 像素为检测块
static const int SolidBlockMinArea = 2048;  // 忽略小于此面积的块

writeSolidRects() 会遍历变化区域,用 16×16 的块去检测纯色区域。找到一个纯色块后,会尝试向四周扩展,尽量合并出更大的纯色矩形。这避免了把大面积的纯色区域(比如窗口标题栏、背景色)拆成大量小矩形分别编码。

矩形分割策略

对于大面积的变化区域,TigerVNC 不会直接发送一个巨大的矩形,而是先分割:

cpp 复制代码
// 分割参数
static const int SubRectMaxArea = 65536;   // 单个矩形最大面积 64K 像素
static const int SubRectMaxWidth = 2048;   // 单个矩形最大宽度 2048 像素

computeNumRects() 根据矩形面积和宽度计算需要分割成多少个子矩形。这个策略的好处是:小矩形可以更精确地选择编码方式(比如一个矩形里既有文字又有图片),同时也利于多线程解码的并行化。

Tight 编码器的内部决策树

TightEncoder 是 TigerVNC 的核心编码器,它的 writeRect() 方法根据 Palette 分析结果选择不同的压缩路径:

cpp 复制代码
void TightEncoder::writeRect(const PixelBuffer* pb, const Palette& palette)
{
    assert(pb->width() <= TIGHT_MAX_WIDTH);  // 2048

    switch (palette.size()) {
    case 0:                                    // 颜色数 > maxPaletteSize
        writeFullColourRect(pb);               // → 全彩色路径(可能叠加 JPEG)
        break;
    case 1:                                    // 纯色
        Encoder::writeSolidRect(pb, palette);  // → 直接写像素值
        break;
    case 2:                                    // 2 色
        writeMonoRect(pb, palette);             // → 单色位图路径
        break;
    default:                                   // 3~256 色
        writeIndexedRect(pb, palette);          // → 调色板索引路径
    }
}

每条路径内部还会根据压缩级别选择不同的 Zlib 参数。压缩级别配置表来自 VirtualGL 项目对不同 2D/3D 应用场景的实测数据:

cpp 复制代码
// 10 个压缩级别对应的 Zlib 设置
// { 索引色Zlib级别, 单色Zlib级别, 全彩色Zlib级别 }
static const TightConf conf[10] = {
    { 0, 0, 0 },  // 0 - 无压缩
    { 1, 1, 1 },  // 1
    { 3, 3, 2 },  // 2 - 默认级别
    { 5, 5, 2 },  // 3
    { 6, 7, 3 },  // 4
    { 7, 8, 4 },  // 5
    { 7, 8, 5 },  // 6
    { 8, 9, 6 },  // 7
    { 9, 9, 7 },  // 8
    { 9, 9, 9 }   // 9 - 最大压缩
};

注意全彩色的 Zlib 级别通常低于索引色------因为全彩色数据量大,过高的压缩级别带来的压缩比提升远不如 CPU 开销增加来得显著。

Tight 编码器还维护了 4 个独立的 Zlib 流zlibStreams[4]),用于同时压缩不同类型的数据而不互相干扰 Zlib 的字典状态。

JPEG 有损压缩与无损刷新机制

当质量级别(Quality Level)被设置时,Tight 编码器会使用 JPEG 有损压缩来进一步减少全彩色矩形的数据量。TigerVNC 使用 libjpeg-turbo 实现 JPEG 编解码,利用 SIMD 指令加速。

一个关键的设计是无损刷新机制 。使用 JPEG 有损压缩后,客户端显示的画面是有损的。TigerVNC 会追踪这些有损区域(lossyRegion),在画面停止变化后自动发送无损版本来替换:

cpp 复制代码
class EncodeManager : public core::Timer::Callback {
    bool needsLosslessRefresh(const core::Region& req);
    int getNextLosslessRefresh(const core::Region& req);
    void writeLosslessRefresh(const core::Region& req,
                              const PixelBuffer* pb,
                              const RenderedCursor* renderedCursor,
                              size_t maxUpdateSize);
    // ...
    core::Region lossyRegion;             // 记录所有有损压缩过的区域
    core::Region pendingRefreshRegion;    // 等待无损刷新的区域
    core::Timer recentChangeTimer;        // 检测画面是否仍在变化
};

触发逻辑:

  1. JPEG 编码时,将矩形加入 lossyRegion
  2. 启动 recentChangeTimer(超时 50ms)
  3. 如果 50ms 内同一区域又发生变化 → 重置定时器(画面仍在变化,不急着刷新)
  4. 如果 50ms 内无新变化 → 将 lossyRegion 转入 pendingRefreshRegion
  5. 下一次更新时,writeLosslessRefresh() 用 Tight 无损编码重新发送这些区域

RecentChangeTimeout = 50ms 的选择是个平衡:太短会浪费带宽(画面还在动画中就急着刷新),太长会让用户看到明显的 JPEG 伪影。

3.3 多线程解码器 DecodeManager

解码端的性能瓶颈往往在 CPU 密集的图像解压缩。TigerVNC 的 DecodeManager 实现了一个线程池并行解码的架构。

线程池初始化
cpp 复制代码
DecodeManager::DecodeManager(CConnection *conn_) : conn(conn_)
{
    size_t cpuCount = std::thread::hardware_concurrency();
    if (cpuCount == 0) {
        vlog.error("Unable to determine the number of CPU cores");
        cpuCount = 1;
    } else {
        // 上限 4 个线程------再多只会浪费 CPU 争抢锁
        if (cpuCount > 4)
            cpuCount = 4;
    }

    while (cpuCount--) {
        // 每个工作线程分配 2 个缓冲流,防止流水线停滞
        freeBuffers.push_back(new rdr::MemOutStream());
        freeBuffers.push_back(new rdr::MemOutStream());
        threads.push_back(new DecodeThread(this));
    }
}

上限 4 线程的决策是实际的工程判断------解码操作的并行度受限于屏幕更新的矩形数量,超过 4 个线程后锁竞争的增加会抵消并行收益。

解码流水线

decodeRect() 是解码的入口。它采用了一种流水线设计

cpp 复制代码
bool DecodeManager::decodeRect(const core::Rect& r, int encoding,
                               ModifiablePixelBuffer* pb)
{
    // 1. 从 freeBuffers 中获取一个空闲的 MemOutStream
    // 2. 主线程读取该矩形的压缩数据到 MemOutStream
    //    (某些编码如 CopyRect、Raw 可以直接解码,不走线程池)
    // 3. 创建 QueueEntry,加入 workQueue
    // 4. 通知工作线程

    QueueEntry* entry = new QueueEntry;
    entry->active = false;
    entry->rect = r;
    entry->encoding = encoding;
    entry->decoder = decoders[encoding];
    entry->pb = pb;
    entry->bufferStream = bufferStream;

    {
        std::lock_guard<std::mutex> lock(queueMutex);
        workQueue.push_back(entry);
    }
    consumerCond.notify_one();
    // ...
}

每个 QueueEntry 的结构:

cpp 复制代码
struct QueueEntry {
    bool active;                    // 是否正在被工作线程处理
    core::Rect rect;                // 矩形位置
    int encoding;                   // 编码类型
    Decoder* decoder;               // 解码器实例
    const ServerParams* server;     // 服务端参数
    ModifiablePixelBuffer* pb;      // 目标像素缓冲区
    rdr::MemOutStream* bufferStream;// 压缩数据缓冲
    core::Region affectedRegion;    // 受影响的区域
};

工作线程的核心循环:

cpp 复制代码
void DecodeManager::DecodeThread::worker()
{
    while (!stopRequested) {
        QueueEntry* entry = findEntry();  // 从 workQueue 中找一个未激活的 entry
        if (entry == nullptr) {
            std::unique_lock<std::mutex> lock(manager->queueMutex);
            manager->consumerCond.wait(lock);
            continue;
        }
        // 执行解码
        entry->decoder->decodeRect(entry->rect, entry->server,
                                   entry->bufferStream, entry->pb);
        entry->active = false;  // 标记完成
        manager->producerCond.notify_one();  // 通知主线程可以回收
    }
}

flush() 方法等待所有排队的工作完成,在帧更新结束时调用,确保所有解码结果已经写入帧缓冲区后才进入渲染阶段。

线程异常通过 threadException 指针传递回主线程:

cpp 复制代码
void DecodeManager::setThreadException()
{
    std::lock_guard<std::mutex> lock(queueMutex);
    if (threadException == nullptr)
        threadException = std::current_exception();
}

void DecodeManager::throwThreadException()
{
    if (threadException != nullptr) {
        std::exception_ptr e = threadException;
        threadException = nullptr;
        std::rethrow_exception(e);
    }
}

3.4 屏幕变化追踪 ComparingUpdateTracker

服务端的另一个性能关键组件是屏幕变化检测。ComparingUpdateTracker 通过逐像素比较当前帧和前一帧 来自动检测变化,是 x0vncserver 的核心组件。

64 像素块比较算法
cpp 复制代码
#define BLOCK_SIZE 64

整个屏幕被划分为 64×64 像素的块。compare() 方法遍历所有已知的变化区域(由 SimpleUpdateTracker 记录),逐块与前一帧比较:

cpp 复制代码
bool ComparingUpdateTracker::compare()
{
    if (firstCompare) {
        // 首次比较:复制整个帧缓冲区作为基准,标记全屏幕为变化
        oldFb.setSize(fb->width(), fb->height());
        // ... 逐块拷贝
        firstCompare = false;
        return false;
    }

    // 先处理 copy rect(窗口拖动等场景)
    copied.get_rects(&rects, copy_delta.x <= 0, copy_delta.y <= 0);
    for (auto i = rects.begin(); i != rects.end(); i++)
        oldFb.copyRect(*i, copy_delta);  // 同步更新旧帧缓冲区

    // 再逐块比较变化区域
    changed.get_rects(&rects);
    core::Region newChanged;
    for (auto i = rects.begin(); i != rects.end(); i++)
        compareRect(*i, &newChanged);

    changed = newChanged;
    return true;
}
compareRect() 的精细裁剪

compareRect() 不是简单地判断"变了没变",而是精确地找出变化矩形的边界:

cpp 复制代码
void ComparingUpdateTracker::compareRect(const core::Rect& r,
                                         core::Region* newChanged)
{
    int bytesPerPixel = fb->getPF().bpp / 8;

    for (int blockTop = r.tl.y; blockTop < r.br.y; blockTop += BLOCK_SIZE)
    {
        for (int blockLeft = r.tl.x; blockLeft < r.br.x; blockLeft += BLOCK_SIZE)
        {
            // 逐行 memcmp 扫描,找到第一行变化
            for (int y = blockTop; y < blockBottom; y++) {
                if (memcmp(oldPtr, newPtr, blockWidthInBytes) != 0) {
                    // 向上裁剪:找到第一行变化的 y
                    // 向下裁剪:从底部往上找最后一行变化
                    // 向左裁剪:逐列比较
                    // 向右裁剪:逐列比较
                    // 生成精确的变化矩形
                }
            }
        }
    }
}

比较顺序是:先找第一行变化 → 再找最后一行变化 → 再裁剪左右边界。这样生成的变化矩形是精确的,不会多传一个没变化的像素。

性能统计

ComparingUpdateTracker 维护了两个关键计数器:

cpp 复制代码
unsigned long long totalPixels, missedPixels;
  • totalPixels --- SimpleUpdateTracker 报告的变化像素总数(粗粒度)
  • missedPixels --- 经过逐像素比较后实际确认的变化像素数(细粒度)

两者的比值反映了比较算法的过滤效率。在典型桌面场景下(鼠标移动、文本编辑),这个过滤能减少 50%~80% 的无效传输。

3.5 TCP Vegas 拥塞控制 Congestion

这是 TigerVNC 中一个容易被忽略但非常重要的组件。VNC 的核心体验问题是延迟------如果服务端拼命往网络里灌数据,TCP 缓冲区会迅速积压(buffer bloat),导致用户操作后几百毫秒才看到画面更新。

Congestion 类实现了一个应用层的拥塞控制算法,灵感来自 TCP Vegas。

设计动机

源码注释说得很直接:

cpp 复制代码
/*
 * This code implements congestion control in the same way as TCP in
 * order to avoid excessive latency in the transport. This is needed
 * because "buffer bloat" is unfortunately still a very real problem.
 *
 * The basic principle is TCP Congestion Control (RFC 5618), with the
 * addition of using the TCP Vegas algorithm. The reason we use Vegas
 * is that we run on top of a reliable transport so we need a latency
 * based algorithm rather than a loss based one.
 */

为什么不直接用 TCP 自带的拥塞控制?因为 TCP 的拥塞控制(CUBIC、BBR 等)是丢包驱动的,而 VNC 跑在 TCP 上不会丢包,所以需要基于延迟的算法。TCP Vegas 通过测量 RTT 变化来判断网络是否拥塞,正好适合这个场景。

核心参数
cpp 复制代码
static const unsigned INITIAL_WINDOW = 16384;   // 初始窗口 16KB
static const unsigned MINIMUM_WINDOW = 4096;    // 最小窗口 4KB(≈3*MSS)
static const unsigned MAXIMUM_WINDOW = 4194304;  // 最大窗口 4MB(Linux 默认上限)
关键接口
cpp 复制代码
class Congestion {
public:
    // 频繁调用,更新当前流位置
    void updatePosition(unsigned pos);

    // 发送一个 Ping 标记(放在输出流中的特定位置)
    void sentPing();

    // 收到 Ping 对应的 Pong(服务端通过 Fence 消息回复)
    void gotPong();

    // 判断当前是否拥塞
    bool isCongested();

    // 预估多久后解除拥塞(毫秒),-1 表示未知
    int getUncongestedETA();

    // 获取当前带宽估计(字节/秒)
    size_t getBandwidth();

    // 调试用:输出拥塞窗口和 TCP 层拥塞窗口
    void debugTrace(const char* filename, int fd);
};
RTT 测量

RTT 测量利用了 RFB 协议的 Fence 机制。Fence 是 TigerVNC 扩展的一种消息类型,服务端发送一个 Fence Request,客户端原样回复一个 Fence Response。通过在输出流的关键位置插入 Fence,可以精确测量数据从发出到被对方处理的时间。

cpp 复制代码
struct RTTInfo {
    struct timeval tv;   // 发送时间
    unsigned pos;        // 发送时的流位置
    unsigned extra;      // 额外缓冲区中的数据量
    bool congested;      // 发送时是否已拥塞
};

维护了一个 pings 列表记录所有未回复的 Ping。当收到 Pong 时,计算 RTT = 当前时间 - Ping 发送时间。

拥塞窗口调整
cpp 复制代码
unsigned baseRTT;        // 基准 RTT(网络最小延迟)
unsigned congWindow;     // 当前拥塞窗口大小
bool inSlowStart;        // 是否处于慢启动阶段

慢启动阶段 :从空闲状态恢复时,窗口从 INITIAL_WINDOW(16KB)开始,每收到一个 Pong 就翻倍。这样可以在高带宽网络上快速爬升到合理速率。

拥塞检测

cpp 复制代码
void Congestion::updateCongestion()
{
    // 计算当前 RTT 与基准 RTT 的差值
    // 如果差值超过阈值 → 网络拥塞,减小窗口
    // 如果差值在阈值内 → 网络空闲,增大窗口

    // 更新基准 RTT(取最小值,但要定期老化避免过期)
    if (currentRTT < baseRTT)
        baseRTT = currentRTT;
}

isCongested() 的判断逻辑

  1. 检查当前"飞行中"的数据量(已发送但未确认)是否超过拥塞窗口
  2. 检查输出缓冲区是否积压了过多数据
  3. 如果两者都满足 → 拥塞,暂停发送

VNCSConnectionST 在每次发送帧更新前都会调用 isCongested(),如果拥塞就推迟发送。这就是为什么 TigerVNC 在高延迟网络下仍然能保持较好响应性的原因。

3.6 安全认证体系

TigerVNC 实现了完整的安全类型层次结构:

复制代码
CSecurity(客户端侧)/ SSecurity(服务端侧)
├── CSecurityNone / SSecurityNone         --- 无认证
├── CSecurityVncAuth / SSecurityVncAuth   --- VNC 密码认证(DES 加密,56 位密钥)
├── CSecurityPlain / SSecurityPlain       --- 明文用户名密码(须配合 TLS)
├── CSecurityTLS / SSecurityTLS          --- TLS 加密通道(GnuTLS 库)
├── CSecurityVeNCrypt / SSecurityVeNCrypt --- VeNCrypt 扩展框架(嵌套认证)
├── CSecurityRSAAES / SSecurityRSAAES    --- RSA-AES 认证(Nettle 库)
├── CSecurityStack / SSecurityStack      --- 组合安全层(如 TLS + VncAuth)
├── CSecurityDH                          --- Diffie-Hellman 密钥交换
└── CSecurityMSLogonII                   --- MSLogon II 兼容模式

CSecurityStack / SSecurityStack 是组合安全层的实现。例如 TLS + Plain 的工作方式:先用 TLS 建立加密通道,然后在加密通道内执行明文密码认证。这比单纯用 VncAuth(DES 加密,容易被破解)安全得多。

RSA-AES 是较新的认证方式,使用 Nettle 加密库,流程大致是:

  1. 客户端和服务端各自生成 RSA 密钥对
  2. 交换公钥
  3. 用 RSA 加密协商一个 AES 会话密钥
  4. 后续通信全部用 AES 加密

SecurityClientSecurityServer 是安全类型的管理入口,负责根据配置决定启用哪些安全类型,以及协商时的优先级排序。

3.7 像素格式处理 PixelFormat

PixelFormat 类处理客户端和服务端之间像素格式的差异。不同平台的帧缓冲区可能使用不同的位深、字节序和颜色分量布局:

cpp 复制代码
class PixelFormat {
    // 三种表示形式:
    // a) Pixel --- 原生整数像素值
    // b) Buffer --- 字节流(考虑字节序和填充)
    // c) RGB --- 8位 R/G/B 分量

    inline Pixel pixelFromBuffer(const uint8_t* buffer) const;
    inline void bufferFromPixel(uint8_t* buffer, Pixel pixel) const;
    inline Pixel pixelFromRGB(uint16_t r, uint16_t g, uint16_t b) const;
    void bufferFromRGB(uint8_t* dst, const uint8_t* src, int pixels) const;

    // 常用检测
    bool is888(void) const;        // 是否为 8:8:8 格式(最常见)
    bool isBigEndian(void) const;
};

TigerVNC 对最常见的 32 位 8:8:8 格式做了专门的快速路径优化------is888() 返回 true 时,像素拷贝可以绕过逐像素的位运算,直接 memcpy

3.8 服务端核心 VNCServerST

VNCServerST(Single-Threaded VNC Server)是服务端的顶层控制器:

cpp 复制代码
class VNCServerST : public VNCServer, public core::Timer::Callback {
    // Socket 管理
    bool addSocket(network::Socket* sock, bool outgoing, AccessRights ar);
    void removeSocket(network::Socket* sock);
    void getSockets(std::list<network::Socket*>* sockets);

    // 事件处理(由外部事件循环驱动)
    void processSocketReadEvent(network::Socket* sock);
    void processSocketWriteEvent(network::Socket* sock);

    // 帧缓冲区管理
    void setPixelBuffer(PixelBuffer* pb, const ScreenSet& layout);

    // 连接管理
    std::list<VNCSConnectionST*> clients;
};

单线程设计VNCServerST 的所有连接共享一个事件循环。外部的 main() 函数使用 select()poll() 等待所有 socket 的可读/可写事件,然后分发到 processSocketReadEvent() / processSocketWriteEvent()

每个连接的 VNCSConnectionST 持有自己的 EncodeManagerCongestion 实例:

cpp 复制代码
class VNCSConnectionST : private SConnection, public core::Timer::Callback {
    EncodeManager encoder;   // 独立的编码器(维护各自的 Zlib 流状态)
    Congestion congestion;   // 独立的拥塞控制(各自测量各自的 RTT)
    // ...
};

这种设计的好处是:不同客户端可能有不同的网络条件和编码偏好,各自独立的编码器和拥塞控制器可以分别适配。


四、跨平台客户端 vncviewer/

VNC Viewer 基于 FLTK(Fast Light Toolkit)构建,这是一个轻量级跨平台 C++ GUI 库。

4.1 核心类 CConn

CConn 继承自 rfb::CConnection,是 Viewer 侧的连接管理核心:

cpp 复制代码
class CConn : public rfb::CConnection {
    // FLTK 事件循环集成
    static void socketEvent(FL_SOCKET fd, void* data);
    void processNextMsg(core::Timer*);

    // CConnection 回调实现
    void getUserPasswd(bool secure, std::string* user, std::string* password) override;
    bool verifyCertificate(unsigned int status, ...) override;
    bool verifyHostKey(const uint8_t* key, size_t length, ...) override;
    void initDone() override;
    void setName(const char* name) override;
    void bell() override;
    void setCursor(int width, int height, ...) override;

    // 帧更新回调
    void framebufferUpdateStart() override;
    void framebufferUpdateEnd() override;
    bool dataRect(const core::Rect& r, int encoding) override;

    // 剪贴板
    void handleClipboardRequest() override;
    void handleClipboardAnnounce(bool available) override;
    void handleClipboardData(const char* data) override;

private:
    DesktopWindow* desktop;     // 远程桌面窗口
    network::Socket* sock;      // 网络套接字
    int serverPort;
};

socketEvent() 是 FLTK 事件循环的回调入口。FLTK 的 Fl::add_fd() 注册 socket 的可读事件,触发时调用 processNextMsg() 驱动 CConnection::processMsg() 状态机。

dataRect() 是帧更新的核心回调,它将收到的矩形数据交给 DecodeManager 解码。

4.2 桌面窗口 DesktopWindow

DesktopWindow 继承自 Fl_Window,是远程桌面的可视化容器:

cpp 复制代码
class DesktopWindow : public Fl_Window {
    void updateWindow();                    // 刷新到屏幕
    void resizeFramebuffer(int w, int h);   // 帧缓冲区缩放
    void setCursor();                       // 更新本地光标
    void setLEDState(unsigned int state);   // LED 状态同步

    // 剪贴板
    void handleClipboardRequest();
    void handleClipboardData(const char* data);

    // 全屏管理
    void fullscreen_on();
    void grabKeyboard();
    void ungrabKeyboard();

    // 远程桌面缩放
    void remoteResize();

private:
    Viewport* viewport;                     // 视口渲染层
    Fl_Scrollbar* hScroll, *vScroll;        // 滚动条
    CConn* cc;                              // 关联的连接
};

Viewport 负责实际的像素渲染,DesktopWindow 处理窗口管理(全屏切换、滚动条、键盘捕获等)。当远程桌面比本地窗口大时显示滚动条,反之可以缩放或拉伸。

4.3 Surface 平台抽象

Surface 类封装了不同平台的渲染后端:

平台 文件 渲染技术
X11/Linux Surface_X11.cxx cairo
Windows Surface_Win32.cxx GDI (BitBlt)
macOS Surface_OSX.cxx Core Graphics

上层 Viewport 通过 Surface::draw()Surface::blit() 统一接口绘制像素,不需要关心底层用的是 cairo 还是 GDI。


五、Windows 服务端 win/

复制代码
win/
├── rfb_win32/     ← Windows 专用 RFB 适配层(核心)
├── winvnc/        ← winvnc 主程序
├── vncconfig/     ← 配置工具(GUI 界面)
└── wm_hooks/      ← Windows 消息钩子 DLL(注入到用户进程)

⚠️ 注意:winvnc 目前标记为 unmaintained,可能在快速用户切换或远程桌面启用时出现问题。

5.1 SDisplay 的多接口继承

SDisplay 是 Windows 服务端的屏幕管理类,它同时实现了多个接口:

cpp 复制代码
class SDisplay : public SDesktop,          // RFB 桌面接口
                 WMMonitor::Notifier,      // 显示器变化通知
                 Clipboard::Notifier,      // 剪贴板变化通知
                 public EventHandler        // 事件处理接口
{
    void init(VNCServer* vs) override;
    void start() override;
    void stop() override;
    void pointerEvent(const core::Point& pos, uint16_t buttonmask) override;
    void keyEvent(uint32_t keysym, uint32_t keycode, bool down) override;
    void handleClipboardRequest() override;
    void notifyClipboardChanged(bool available) override;
    void notifyDisplayEvent(WMMonitor::Notifier::DisplayEventType evt) override;
};

5.2 两种屏幕捕获策略

SDisplayCore 是屏幕捕获的抽象接口,有两个实现:

cpp 复制代码
class SDisplayCore {
    virtual void setScreenRect(const core::Rect& screenRect_) = 0;
    virtual void flushUpdates() = 0;
    virtual const char* methodName() const = 0;
};
策略 原理 优缺点
轮询 SDisplayCorePolling 定时截取屏幕区域,与前一帧比较 简单可靠,CPU 开销大
WM 钩子 SDisplayCoreWMHooks 注入 DLL 拦截 GDI 绘图调用 效率高,实现复杂,可能不稳定

轮询模式 的实现比较直接:启动一个定时器,每隔 N 毫秒用 BitBlt 截取屏幕到 DeviceFrameBuffer,然后交给 ComparingUpdateTracker 比较。

WM 钩子模式 更有意思:wm_hooks/ 目录下是一个 DLL,它被注入到目标进程,通过 SetWindowsHookEx() 拦截 WH_CALLWNDPROCWH_GETMESSAGE,实时捕获窗口的 WM_PAINTWM_ERASEBKGND 等消息,将变化区域通知回 winvnc 主进程。

5.3 帧缓冲区架构

复制代码
DeviceFrameBuffer
    ├── DeviceContext       --- GDI 设备上下文封装
    └── DIBSectionBuffer    --- DIB Section 内存映射

DIBSectionBuffer 的核心:
    HBITMAP hBitmap;        --- GDI 位图句柄
    void* pvBits;           --- 直接指向像素数据的指针
    BITMAPINFOHEADER bmi;   --- 位图格式描述

DIBSectionBuffer 利用 Windows 的 DIB Section 特性,创建一块 GDI 和应用程序可以同时访问的内存区域。BitBlt 将屏幕内容拷贝到这块内存后,TigerVNC 可以直接通过 pvBits 指针读取像素数据,不需要额外的拷贝。

5.4 其他关键组件

组件 职责
Clipboard 剪贴板双向同步(本地↔远程)
SInput 将远程键盘/鼠标事件注入 Windows 会话
WMHooks / WMNotifier Windows 消息拦截
WMPoller 窗口轮询(辅助检测 WM 钩子遗漏的变化)
WMWindowCopyRect 窗口移动检测(生成 CopyRect 指令)
WMCursor 光标形状捕获
Service Windows 服务框架封装
CleanDesktop 清理桌面壁纸(减少传输数据量)
TrayIcon 系统托盘图标
RegConfig 从注册表读取配置

CleanDesktop 是个有趣的优化------连接时可以选择移除桌面壁纸,用纯色背景代替。虽然用户体验会受影响,但在低带宽网络下可以显著减少传输量。


六、Unix/Linux 服务端 unix/

Unix 端提供三种服务端实现:

6.1 x0vncserver --- X11 屏幕共享

捕获已有的 X11 桌面,依赖以下 X11 扩展:

扩展 头文件 用途 检测宏
Xdamage <X11/extensions/Xdamage.h> 检测屏幕区域变化 HAVE_XDAMAGE
Xfixes <X11/extensions/Xfixes.h> 光标形状捕获 HAVE_XFIXES
Xrandr <X11/extensions/Xrandr.h> 屏幕分辨率/旋转 HAVE_XRANDR
XTest <X11/extensions/XTest.h> 注入键盘/鼠标事件 HAVE_XTEST

Xdamage 扩展是 x0vncserver 的性能关键------它能在 X Server 层面通知哪些区域发生了变化,比纯轮询高效得多。如果 Xdamage 不可用,x0vncserver 会退回到 ComparingUpdateTracker 的逐像素比较。

6.2 w0vncserver --- Wayland 屏幕共享

较新的 Wayland 支持实现。Wayland 的安全模型不允许客户端直接读取屏幕内容,所以需要通过 PipeWire 间接捕获:

复制代码
w0vncserver → PipeWire 客户端 → PipeWire 服务(由合成器授权)→ 屏幕帧数据

依赖链:GLib/Gio → PipeWire → Wayland Client Protocol。

6.3 vncserver --- 虚拟会话

这不是一个独立程序,而是一个 Perl/Shell 启动脚本。它的工作流程:

  1. 启动 Xvnc(一个内嵌了 VNC 服务端的 X Server)
  2. 在 Xvnc 上运行用户指定的窗口管理器或桌面环境
  3. 用户通过 VNC Viewer 连接到 Xvnc

这是 Linux 上最常见的 VNC 使用方式------每个 VNC 会话对应一个独立的虚拟桌面。

6.4 辅助工具

  • vncpasswd --- VNC 密码文件管理(创建/修改 ~/.vnc/passwd
  • vncconfig --- 运行时配置工具(剪贴板同步、分辨率调整等)

七、外部依赖关系

复制代码
                    ┌──────────┐
                    │ TigerVNC │
                    └────┬─────┘
           ┌─────────────┼─────────────┐
           │             │             │
      必需依赖       功能依赖       平台依赖
    ┌──────┴──────┐  ┌────┴────┐  ┌────┴────┐
    │  zlib       │  │FLTK 1.x │  │ X11扩展 │
    │  pixman     │  │GnuTLS   │  │ PipeWire│
    │  libjpeg-   │  │Nettle   │  │ PAM     │
    │  turbo      │  │FFmpeg   │  │ GDI/Win │
    └─────────────┘  └─────────┘  └─────────┘
依赖库 必要性 用途 性能影响
zlib 必需 数据压缩(ZRLE、Tight 编码) 基础
pixman 必需 区域运算、像素操作 Region 类的基础
libjpeg-turbo 必需 JPEG 编解码加速 SIMD 加速带来 2-4× 提升
FLTK 1.3/1.4 Viewer 必需 跨平台 GUI 框架 ---
GnuTLS 可选 TLS 加密通信 ---
Nettle 可选 RSA-AES 安全认证 ---
FFmpeg (libav) 可选 H.264 编解码(Linux/macOS) 视频编码场景显著
PAM 可选 Unix 系统认证 ---
gettext 可选 多语言翻译 ---

libjpeg-turbo 对 TigerVNC 性能影响极大。相比标准 libjpeg,libjpeg-turbo 利用 SIMD(SSE2/AVX2/NEON)指令加速 DCT 变换和色彩空间转换,在 Tight 编码的 JPEG 路径上能带来 2-4 倍的编解码性能提升。这也是 TigerVNC 相比其他 VNC 实现(如 RealVNC、TightVNC)性能优势的主要来源之一。


八、架构设计亮点总结

8.1 流式 I/O 的装饰器模式

rdr/ 层的 Stream 抽象是整个项目中最优雅的设计之一。通过将压缩、加密等功能实现为可叠加的流装饰器,协议层代码完全不感知传输层的复杂性。这也是 TigerVNC 能够方便地支持多种安全类型组合(如 TLS + Plain、TLS + VncAuth)的根本原因。

8.2 平台代码的彻底隔离

common/ 目录包含了全部跨平台逻辑,而 win/unix/vncviewer/ 中的平台特定代码通过编译条件或文件命名严格隔离。核心协议代码零平台依赖,可以在不修改一行代码的情况下编译到任何平台。

8.3 编码决策的自适应策略

EncodeManager 不是简单地"选一种编码用到底",而是对每个矩形独立分析像素特征(颜色数、分布、面积),然后从 EncoderClass × EncoderType 矩阵中选择最优的组合。这种细粒度的自适应策略是 TigerVNC 在各种桌面场景下都能保持良好性能的关键。

8.4 无损刷新机制

JPEG 有损压缩后立即追踪 lossyRegion,画面停止变化后 50ms 自动触发无损重传。这个机制让用户在高带宽利用率和最终图像质量之间取得了很好的平衡------动画播放时用 JPEG 省带宽,停下来后自动恢复无损画质。

8.5 应用层拥塞控制

在 TCP 之上叠加 TCP Vegas 风格的拥塞控制,看似多余,实际上解决了 TCP 层拥塞控制无法解决的问题:TCP 的拥塞窗口是连接级别的,而 VNC 的帧更新是应用级别的。TigerVNC 通过 Fence/Ping 机制测量应用层的实际 RTT,比 TCP 层的 RTT 估计更精确,从而避免了 buffer bloat 导致的操作延迟。

8.6 多线程解码 + 上限控制

DecodeManager 用线程池并行解码,但上限 4 个线程------这是实际工程经验的体现。VNC 的屏幕更新通常被分割成几十到几百个小矩形,4 个线程已经能充分利用多核,再多只会增加锁竞争。

8.7 64 像素块精细化比较

ComparingUpdateTracker 不是简单地标记"这一行变了",而是先找到第一行变化,再找到最后一行变化,再逐列裁剪左右边界。这种精细化比较在鼠标移动场景下尤其有效------鼠标光标的变化通常只有几个像素宽,精确裁剪后传输量大幅减少。


九、架构全景图

复制代码
┌───────────────────────────────────────────────────────────────┐
│                        应用层                                  │
│  ┌───────────┐ ┌───────────┐ ┌─────────────┐ ┌────────────┐  │
│  │ vncviewer │ │  winvnc   │ │x0vncserver  │ │w0vncserver │  │
│  │  (FLTK)   │ │  (Win32)  │ │   (X11)     │ │ (Wayland)  │  │
│  └─────┬─────┘ └─────┬─────┘ └──────┬──────┘ └──────┬─────┘  │
│        │             │              │               │         │
│   ┌────┴────┐   ┌────┴─────┐  ┌────┴─────┐   ┌────┴──────┐  │
│   │ CConn   │   │rfb_win32 │  │XDamage/  │   │ PipeWire │  │
│   │ Desktop │   │SDisplay  │  │XTest/    │   │ Wayland  │  │
│   │ Viewport│   │WMHooks   │  │XFixes    │   │ Client   │  │
│   └────┬────┘   └────┬─────┘  └────┬─────┘   └────┬──────┘  │
├────────┼──────────────┼────────────┼───────────────┼─────────┤
│                   RFB 协议层 (common/rfb)                    │
│                                                              │
│  ┌─────────────────┐  ┌──────────────┐  ┌────────────────┐  │
│  │ EncodeManager   │  │ 安全认证      │  │ CConnection    │  │
│  │ ┌─ Solid       │  │ ┌─ TLS       │  │ SConnection    │  │
│  │ ├─ Bitmap/Mono │  │ ├─ RSA-AES   │  │ VNCSConnection │  │
│  │ ├─ Indexed     │  │ ├─ VeNCrypt  │  │ (状态机驱动)   │  │
│  │ ├─ FullColour  │  │ ├─ VncAuth   │  └────────────────┘  │
│  │ └─ JPEG+无损刷新│  │ └─ Stack组合 │                      │
│  ├─────────────────┤  └──────────────┘  ┌────────────────┐  │
│  │ DecodeManager   │                    │ Congestion     │  │
│  │ (多线程线程池)   │                    │ (TCP Vegas)    │  │
│  │ 4 线程上限       │                    │ Fence RTT 测量 │  │
│  ├─────────────────┤  ┌──────────────┐  └────────────────┘  │
│  │ ComparingUpdate │  │ PixelFormat  │                      │
│  │ (64 像素块比较)  │  │ PixelBuffer  │                      │
│  └─────────────────┘  └──────────────┘                      │
├──────────────────────────────────────────────────────────────┤
│                   网络层 (common/network)                     │
│               TcpSocket · UnixSocket · TLS                   │
├──────────────────────────────────────────────────────────────┤
│                   I/O 流层 (common/rdr)                      │
│     Buffered · Zlib · AES · TLS · Hex · Fd · Mem            │
│              (装饰器模式,可任意组合叠加)                     │
├──────────────────────────────────────────────────────────────┤
│                   基础设施层 (common/core)                    │
│     Config · Logger · Timer · Region/Rect · i18n · string   │
└──────────────────────────────────────────────────────────────┘

十、阅读源码的建议路线

如果你打算在此基础上做二次开发或学习 VNC/RFB 协议实现,建议按以下路线阅读:

  1. 先看 I/O 流层common/rdr/InStream.hOutStream.h)--- 理解数据读写的基本抽象
  2. 再看协议状态机CConnection.cxx)--- 从 processVersionMsg() 开始,跟着状态机走一遍完整的握手流程
  3. 然后看编码TightEncoder.cxx)--- 理解 writeRect() 的 Palette 决策树
  4. 接着看编码管理器EncodeManager.cxx)--- 理解自适应选择的全局策略
  5. 最后看拥塞控制Congestion.cxx)--- 理解应用层如何与 TCP 层协同

整个阅读过程中,core::LogWriter 的日志输出是理解运行时行为的好帮手------TigerVNC 的日志级别分得很细(error/status/info/debug),开启 debug 级别可以看到状态机的每一步转移和每个矩形的编码选择。


文中源码分析基于 TigerVNC 1.16.80 版本,项目地址:https://github.com/TigerVNC/tigervnc

相关推荐
笨蛋不要掉眼泪11 小时前
MySQL架构揭秘:MVCC解决读一致性问题
数据库·mysql·架构
张忠琳11 小时前
【kubernetes】Node Feature Discovery 0.20.0-devel 源码深度解析 — 第四卷:CLI入口、工具链与全局架构总结
云原生·容器·架构·kubernetes·nvidia
不好听61311 小时前
BFF 架构实战:从前端直调 API 到加入中间层
前端·架构
Georgeviewer14 小时前
数字化工程落地复盘:通用SaaS架构的本地化缺陷与自研系统最优解|皖禾数智实战
大数据·人工智能·架构
柒和远方14 小时前
V043:BFF 层流式输出:从前端直连到生产级流式网关的架构演进
javascript·架构·node.js
Imagination官方博客16 小时前
边缘AI处理器的架构创新
人工智能·架构·gpu算力
momo18 小时前
智析AgentData-Analysis-Agent 0→1 架构、搭建与联调实战报告
架构
XUHUOJUN19 小时前
Azure Stack Hub 套餐、计划与订阅模型深度解析(上篇)
架构·azure stack
Sam_Deep_Thinking20 小时前
DDD里的领域服务,到底什么时候该用?
程序员·架构·系统架构·ddd