SPICE全链路分析(五):剪贴板共享与文件传输——VDAgent双向通信

当你在客户端复制文本,然后在虚拟机中粘贴时,数据经过了怎样的旅程?文件拖放又如何实现?本文追踪VDAgent通信的完整链路。

VDAgent通信架构

SPICE通过VDAgent(Virtual Desktop Agent)实现主机与虚拟机之间的增强功能:剪贴板共享、文件传输、分辨率调整、鼠标无缝集成等。通信架构涉及三端:客户端、服务端、Guest内的Agent进程。

为什么需要三端架构?

一个自然的问题是:为什么不让客户端直接与Guest Agent通信,而要通过服务端中转?

原因有三:

  1. 安全控制 :服务端的AgentMsgFilter可以在中转时过滤消息------管理员可以禁用剪贴板共享或文件传输而不需要修改Guest内的Agent。这对安全敏感环境(如金融、政府)至关重要
  2. 多客户端支持:多个客户端可以同时连接同一虚拟机,服务端负责将Agent消息路由到正确的客户端,或在多客户端间仲裁(如谁拥有剪贴板)
  3. 协议复用 :Agent消息通过MainChannel的SPICE_MSG_MAIN_AGENT_DATA传输,复用已有的SPICE通道基础设施(加密、认证、流控),无需为Agent单独建立通信链路

按需剪贴板(Clipboard by Demand)设计

SPICE的剪贴板不是"复制时立即传输数据",而是按需传输

  1. 复制方只发送GRAB("我拥有这些类型的数据"),不发送实际内容
  2. 粘贴方发送REQUEST("给我text/plain类型的数据")
  3. 复制方才发送CLIPBOARD(实际数据)

这种设计避免了不必要的数据传输------用户可能复制一张10MB的图片,但从不在另一端粘贴,按需传输就完全不会传这10MB。对于频繁复制但很少跨端粘贴的用户(大多数场景),这可以节省大量带宽。

这与X11剪贴板的Selection机制一致:X11的剪贴板也是"声明所有权+按需提供",SPICE的设计自然地映射到了X11模型。

服务端:RedCharDeviceVDIPort ↔ virtio-serial ↔ Guest Agent

cpp 复制代码
// server/reds.cpp
// VDAgent设备:RedCharDeviceVDIPort
// 与QEMU的virtio-serial端口对接
// Guest内的spice-vdagent进程通过/dev/virtio-ports/com.redhat.spice.0与主机通信

分析:RedCharDeviceVDIPort是SPICE侧的字符设备实现,负责将Agent消息封装为VDI块,通过virtio-serial传递给QEMU,最终到达Guest内核的字符设备,由spice-vdagent读取。

客户端:MainChannel ↔ SpiceGtkSession ↔ 系统剪贴板

c 复制代码
// spice-gtk: MainChannel 负责 Agent 消息的收发
// 通过 main-clipboard-selection-grab, main-clipboard-selection-request 等信号
// 与 GTK 的 GtkClipboard 交互

分析:客户端MainChannel接收和发送Agent数据。剪贴板相关信号在收到特定VDAgent消息时触发,由应用层连接到gtk_clipboard API实现与系统剪贴板的同步。

消息格式:VDIChunkHeader + VDAgentMessage + payload

c 复制代码
// VDI块头
struct VDIChunkHeader {
    uint32_t port;   // VDP_CLIENT_PORT 或 VDP_SERVER_PORT
    uint32_t size;  // 后续数据大小
};

// VDAgent消息头
struct VDAgentMessage {
    uint32_t protocol;  // VD_AGENT_PROTOCOL
    uint32_t type;      // VD_AGENT_CLIPBOARD_GRAB 等
    uint32_t size;     // 数据大小
    uint64_t opaque;   // 不透明数据
    uint8_t data[0];   // 消息内容
};

分析:VDIChunkHeader用于设备寻址,VDAgentMessage标识消息类型和载荷。整个结构通过SPICE_MSG_MAIN_AGENT_DATA在MainChannel上传输。

剪贴板:客户端 → Guest

步骤1:系统剪贴板变化

当用户在客户端复制内容时,GTK的剪贴板触发owner-change信号:

c 复制代码
// spice-gtk: 连接剪贴板信号
g_signal_connect(clipboard, "owner-change",
                 G_CALLBACK(clipboard_owner_change), channel);

static void clipboard_owner_change(GtkClipboard *clipboard,
                                  GdkEvent *event,
                                  gpointer user_data)
{
    SpiceMainChannel *channel = user_data;
    SpiceMainChannelPrivate *c = channel->priv;
    guint selection = ...;  // 根据clipboard确定selection

    if (!c->agent_connected)
        return;

    // 获取剪贴板中的数据类型
    gtk_clipboard_request_contents(clipboard, atom_targets,
                                   clipboard_targets_received, channel);
}

分析:owner-change在剪贴板内容变化或所有权变化时触发。request_contents异步获取支持的MIME类型,在回调中构建GRAB消息。

步骤2:客户端发送Grab

获取到剪贴板支持的类型后,发送VD_AGENT_CLIPBOARD_GRAB:

c 复制代码
// spice-gtk: channel-main.c
void spice_main_channel_clipboard_selection_grab(SpiceMainChannel *channel,
                                                 guint selection,
                                                 guint32 *types,
                                                 int ntypes)
{
    SpiceMainChannelPrivate *c = channel->priv;
    VDAgentClipboardGrab *grab;
    size_t msgsize;

    g_return_if_fail(c->agent_connected);
    msgsize = sizeof(VDAgentClipboardGrab);
    if (test_agent_cap(channel, VD_AGENT_CAP_CLIPBOARD_SELECTION)) {
        msgsize += 4;
    }

    grab = g_malloc0(msgsize);
    grab->types[0] = types[0];  // text/plain, text/html 等
    // ...

    agent_msg_queue_add(channel, VD_AGENT_CLIPBOARD_GRAB, grab, msgsize);
}

分析:agent_msg_queue_add将消息加入队列,受Agent Token流控约束。GRAB仅声明"我拥有这些类型的数据",不携带实际内容,实现按需传输(clipboard by demand)。

步骤3:服务端中转

MainChannelClient收到SPICE_MSGC_MAIN_AGENT_DATA后,交由reds_on_main_agent_data处理:

cpp 复制代码
// server/main-channel.cpp
case SPICE_MSGC_MAIN_AGENT_DATA:
    reds_on_main_agent_data(reds, this, message, size);
    break;
cpp 复制代码
// server/reds.cpp
void reds_on_main_agent_data(RedsState *reds, MainChannelClient *mcc,
                             const void *message, size_t size)
{
    RedCharDeviceVDIPort *dev = reds->agent_dev.get();
    AgentMsgFilterResult res;

    res = agent_msg_filter_process_data(&dev->priv->write_filter,
                                       static_cast<const uint8_t *>(message), size);
    switch (res) {
    case AGENT_MSG_FILTER_OK:
        break;
    case AGENT_MSG_FILTER_DISCARD:
        return;  // 剪贴板被禁用时丢弃
    case AGENT_MSG_FILTER_PROTO_ERROR:
        mcc->shutdown();
        return;
    }

    header = reinterpret_cast<VDIChunkHeader *>(dev->priv->recv_from_client_buf->buf);
    header->port = VDP_CLIENT_PORT;
    header->size = size;
    dev->write_buffer_add(dev->priv->recv_from_client_buf);
}

分析:agent_msg_filter根据copy_paste_enabled等配置过滤消息。通过过滤的消息添加VDI块头后写入设备缓冲区,由RedCharDevice的写路径送入virtio-serial,最终到达Guest Agent。

步骤4:Guest请求数据

用户在Guest中执行粘贴时,Guest的spice-vdagent向服务端发送VD_AGENT_CLIPBOARD_REQUEST。服务端从virtio-serial读取后,通过MainChannel转发给客户端:

c 复制代码
// 服务端从 RedCharDeviceVDIPort 读取
// read_one_msg_from_device() 解析 VDAgentMessage
// 类型为 VD_AGENT_CLIPBOARD_REQUEST 时,通过管道发送给 MainChannelClient
// MainChannelClient 将消息封装为 SPICE_MSG_MAIN_AGENT_DATA 发往客户端

分析:CLIPBOARD_REQUEST的size/opaque等可包含selection、type信息。客户端收到后触发main-clipboard-selection-request信号,应用层需响应并提供数据。

步骤5:客户端提供数据

客户端连接main-clipboard-selection-request信号,在回调中从系统剪贴板读取并发送:

c 复制代码
// spice-gtk
// 信号连接:
// g_signal_connect(channel, "main-clipboard-selection-request",
//                  G_CALLBACK(clipboard_selection_request_cb), ...);

static void clipboard_selection_request_cb(SpiceMainChannel *channel,
                                           guint selection,
                                           guint32 type,
                                           gpointer user_data)
{
    GtkClipboard *clipboard = gtk_clipboard_get_for_display(..., selection);
    gtk_clipboard_request_contents(clipboard, type_to_atom(type),
                                   clipboard_received_cb, channel);
}

static void clipboard_received_cb(GtkClipboard *clipboard,
                                  GtkSelectionData *data,
                                  gpointer user_data)
{
    SpiceMainChannel *channel = user_data;
    const guchar *buf = gtk_selection_data_get_data(data);
    gsize len = gtk_selection_data_get_length(data);

    spice_main_channel_clipboard_selection_notify(channel, selection, type, buf, len);
}

分析:request_contents异步获取指定MIME类型的数据。clipboard_selection_notify构建VD_AGENT_CLIPBOARD消息并发送,数据经服务端中转到Guest,完成粘贴。

剪贴板:Guest → 客户端(反向)

类似流程但方向相反

当用户在Guest中复制、在客户端粘贴时,流程相反:

  1. Guest Agent 发送 VD_AGENT_CLIPBOARD_GRAB
  2. 服务端 读取后通过 MainChannel 发给客户端
  3. 客户端 main-clipboard-selection-grab 信号
  4. 客户端可调用 gtk_clipboard_set_with_data() 声明拥有剪贴板
  5. 用户在客户端粘贴时,客户端请求剪贴板内容
  6. 客户端发送 VD_AGENT_CLIPBOARD_REQUEST 经服务端到 Guest
  7. Guest Agent 提供数据,经服务端中转到客户端
  8. 客户端 gtk_selection_data_set() 或类似API放入系统剪贴板

分析:双向剪贴板的核心是"请求-响应"模式。拥有方通过GRAB声明,需求方通过REQUEST获取,数据提供方通过CLIPBOARD消息发送实际内容。服务端仅做透明中转。

客户端 gtk_selection_data_set() 放入系统剪贴板

c 复制代码
// 当收到 Guest 的 CLIPBOARD 数据时
// 客户端调用 gtk_clipboard_set_with_data() 或
// 在 selection-get 回调中 set_selection_data

分析:GTK剪贴板采用延迟供应机制,在收到paste请求时才通过selection-get回调提供数据。SPICE在收到CLIPBOARD后,可先缓存,待客户端应用请求时再提供;或主动调用set_with_data使剪贴板立即可用。

AgentMsgFilter过滤机制

copy_paste_enabled / file_xfer_enabled 配置

c 复制代码
// server/agent-msg-filter.c
void agent_msg_filter_config(AgentMsgFilter *filter,
                             gboolean copy_paste, gboolean file_xfer,
                             gboolean use_client_monitors_config)
{
    filter->copy_paste_enabled = copy_paste;
    filter->file_xfer_enabled = file_xfer;
    filter->use_client_monitors_config = use_client_monitors_config;
}

AgentMsgFilterResult agent_msg_filter_process_data(AgentMsgFilter *filter,
                                                   const uint8_t *data, uint32_t len)
{
    struct VDAgentMessage msg_header;
    memcpy(&msg_header, data, sizeof(msg_header));
    msg_header.type = GUINT32_FROM_LE(msg_header.type);

    switch (msg_header.type) {
    case VD_AGENT_CLIPBOARD:
    case VD_AGENT_CLIPBOARD_GRAB:
    case VD_AGENT_CLIPBOARD_REQUEST:
    case VD_AGENT_CLIPBOARD_RELEASE:
        if (!filter->copy_paste_enabled) {
            filter->result = AGENT_MSG_FILTER_DISCARD;
        }
        break;
    case VD_AGENT_FILE_XFER_START:
    case VD_AGENT_FILE_XFER_STATUS:
    case VD_AGENT_FILE_XFER_DATA:
        if (!filter->file_xfer_enabled) {
            filter->result = AGENT_MSG_FILTER_DISCARD;
        }
        break;
    default:
        break;
    }
    return filter->result;
}

分析:过滤器在消息进入设备前检查。copy_paste_enabled和file_xfer_enabled由spice_server_set_agent_file_xfer等API配置,允许管理员禁用剪贴板或文件传输,满足安全策略。

按消息类型过滤

每种VDAgent消息类型可独立过滤。MONITORS_CONFIG在use_client_monitors_config时返回特殊结果,用于显示器配置覆盖逻辑。

文件传输全链路

拖放触发

用户从文件管理器拖放文件到SpiceDisplay时,GTK触发drag-data-received:

c 复制代码
// spice-gtk: spice-widget.c
static void drag_data_received_callback(SpiceDisplay *self,
                                        GdkDragContext *drag_context,
                                        gint x, gint y,
                                        GtkSelectionData *data,
                                        guint info, guint time,
                                        gpointer *user_data)
{
    const guchar *buf = gtk_selection_data_get_data(data);
    gchar **file_urls = g_uri_list_extract_uris((const gchar*)buf);
    int n_files = g_strv_length(file_urls);
    GFile **files = g_new0(GFile*, n_files + 1);

    for (i = 0; i < n_files; i++) {
        files[i] = g_file_new_for_uri(file_urls[i]);
    }
    g_strfreev(file_urls);

    spice_main_channel_file_copy_async(d->main, files, 0, NULL, NULL, NULL,
                                       file_transfer_callback, NULL);

    for (i = 0; i < n_files; i++)
        g_object_unref(files[i]);
    g_free(files);
    gtk_drag_finish(drag_context, TRUE, FALSE, time);
}

分析:g_uri_list_extract_uris解析text/uri-list格式(如file:///path/to/file)。每个URI转为GFile后传入file_copy_async。GFile是GLib的文件抽象,支持本地和GIO可访问的任意后端。

spice_main_channel_file_copy_async() 启动传输

c 复制代码
// spice-gtk: channel-main.c
void spice_main_channel_file_copy_async(SpiceMainChannel *channel,
                                        GFile **sources,
                                        GFileCopyFlags flags,
                                        GCancellable *cancellable,
                                        GFileProgressCallback progress_callback,
                                        gpointer progress_callback_data,
                                        GAsyncReadyCallback callback,
                                        gpointer user_data)
{
    FileTransferOperation *xfer_op;
    SpiceFileTransferTask *xfer_task;

    xfer_op = g_new0(FileTransferOperation, 1);
    xfer_op->channel = channel;
    xfer_op->progress_callback = progress_callback;
    xfer_op->xfer_task = g_hash_table_new(g_direct_hash, g_direct_equal);

    for (i = 0; sources[i] != NULL; i++) {
        xfer_task = spice_file_transfer_task_new(channel, sources[i], flags);
        g_hash_table_insert(xfer_op->xfer_task,
                           GINT_TO_POINTER(xfer_task->task_id), xfer_task);
        xfer_op->stats.num_files++;
    }

    xfer_op->task = g_task_new(NULL, cancellable, callback, user_data);
    file_transfer_start_next(xfer_op);
}

分析:为每个文件创建SpiceFileTransferTask,用task_id索引。GTask管理异步完成回调和取消。file_transfer_start_next启动第一个文件,完成后依次处理下一个。

传输过程

  1. VD_AGENT_FILE_XFER_START:客户端构建文件名、大小等信息,通过agent_msg_queue发送
  2. 服务端中转到Guest Agent
  3. Guest Agent回复 VD_AGENT_FILE_XFER_STATUS (CAN_SEND_DATA):表示可接收数据
  4. 客户端分块读取文件:g_file_read_async() 或 g_input_stream_read_async()
  5. 每块通过 VD_AGENT_FILE_XFER_DATA 发送
  6. 服务端中转到Guest,Guest Agent写文件
  7. 最后发送 size=0 的 FILE_XFER_DATA 表示文件结束
  8. Guest Agent 发送完成确认

分析:CAN_SEND_DATA是流控机制,避免客户端 flooding。分块大小通常为几十KB,平衡网络效率和内存占用。

SpiceFileTransferTask生命周期

c 复制代码
// 创建 -> spice_file_transfer_task_new()
// 初始化 -> 发送 FILE_XFER_START,等待 CAN_SEND_DATA
// 读取 -> 异步读取文件块
// 发送 -> 每块封装为 FILE_XFER_DATA,受 agent token 限制
// 完成 -> 发送 size=0,收到完成确认后标记任务完成
// 取消 -> GCancellable 取消时,发送 CAN_NOT_SEND_DATA 或类似状态

分析:每个文件一个独立任务,多文件串行或可并行(取决于实现)。任务状态可与UI进度条绑定。

进度和取消

c 复制代码
// GFileProgressCallback 原型
typedef void (*GFileProgressCallback)(goffset current_num_bytes,
                                      goffset total_num_bytes,
                                      gpointer user_data);

// 在读取/发送时调用
if (progress_callback) {
    progress_callback(total_sent, file_size, progress_callback_data);
}

// GCancellable 用于取消
if (g_cancellable_is_cancelled(cancellable)) {
    // 停止传输,清理资源
}

分析:progress_callback在每块发送后调用,用于更新进度条。GCancellable由调用方创建并传入,用户点击取消时g_cancellable_cancel(),传输循环检查并退出。

服务端Agent数据读写路径

写入路径(Client → Guest)

复制代码
Client SPICE_MSGC_MAIN_AGENT_DATA
  -> MainChannelClient::handle_message
  -> reds_on_main_agent_data
  -> agent_msg_filter_process_data (过滤)
  -> 添加 VDIChunkHeader
  -> RedCharDeviceVDIPort::write_buffer_add
  -> virtio-serial 写入
  -> Guest spice-vdagent 读取

分析:写入路径在MainChannel的接收逻辑中,无需额外线程。Token流控在客户端限制发送速率。

读取路径(Guest → Client)

复制代码
Guest spice-vdagent 写入 virtio-serial
  -> QEMU 通知 SPICE
  -> RedCharDeviceVDIPort::read_one_msg_from_device
  -> 解析 VDIChunkHeader + VDAgentMessage
  -> 封装为 RedPipeItem
  -> MainChannelClient 发送 SPICE_MSG_MAIN_AGENT_DATA
  -> Client 接收并处理

分析:读取由RedCharDevice的read路径触发,通常在主循环或worker中。消息解析后通过channel的send机制发给对应的MainChannelClient。

总结

VDAgent通信实现了剪贴板和文件传输的双向透明中转:

  1. 剪贴板客户端→Guest:owner-change → GRAB → 服务端过滤并中转 → Guest收到GRAB;用户粘贴 → Guest发REQUEST → 客户端request → CLIPBOARD(data) → 服务端中转 → Guest粘贴
  2. 剪贴板Guest→客户端:流程对称,GRAB/REQUEST/CLIPBOARD方向相反
  3. AgentMsgFilter:按copy_paste_enabled、file_xfer_enabled过滤,满足安全策略
  4. 文件传输:drag_data_received → file_copy_async → FILE_XFER_START → CAN_SEND_DATA → FILE_XFER_DATA分块 → 完成
  5. SpiceFileTransferTask:每文件一任务,支持进度回调和取消

整个设计依赖MainChannel的Agent数据和Token流控,服务端不解析应用层载荷,仅做透明转发,保持了协议的可扩展性。

Agent Token流控

Token机制的工作原理

Agent消息(剪贴板、文件传输等)通过MainChannel传输,但最终消费者是Guest Agent------一个运行在Guest用户态的进程,其处理速度受Guest CPU和virtio-serial吞吐量限制。如果客户端不加限制地发送Agent数据,可能导致:

  1. 服务端的RedCharDeviceVDIPort写缓冲区溢出
  2. virtio-serial环满,QEMU阻塞SPICE的写入
  3. Guest Agent来不及处理,消息堆积导致内存耗尽

Token机制是一个经典的信用(credit)流控方案:

c 复制代码
// 初始化:服务端在 SPICE_MSG_MAIN_INIT 中设置初始 token 数
// init.agent_tokens = REDS_AGENT_WINDOW_SIZE  (通常为10)

// 客户端发送流程:
void agent_msg_queue_many(SpiceMainChannel *channel, int type, void *data, size_t len)
{
    // 将消息加入 agent_msg_queue
    // ...
    agent_send_msg_queue(channel);  // 尝试发送
}

static void agent_send_msg_queue(SpiceMainChannel *channel)
{
    SpiceMainChannelPrivate *c = channel->priv;
    
    while (c->agent_tokens > 0 && !g_queue_is_empty(c->agent_msg_queue)) {
        item = g_queue_pop_head(c->agent_msg_queue);
        c->agent_tokens--;  // 消耗一个token
        // 将item封装为 SPICE_MSGC_MAIN_AGENT_DATA 发送
    }
    // tokens耗尽或队列为空时停止
}

// 服务端补充token:
// 当 RedCharDeviceVDIPort 成功将数据写入 virtio-serial 后
// 发送 SPICE_MSG_MAIN_AGENT_TOKEN 给客户端
// 客户端收到后:c->agent_tokens += msg->num_tokens
// 触发 agent_send_msg_queue() 继续发送队列中的消息

设计考量 :Token窗口大小(REDS_AGENT_WINDOW_SIZE = 10)是一个权衡------太小会导致频繁等待Token补充,增加RTT延迟;太大会在Guest处理慢时积压过多数据。10是一个经验值,在大多数场景下工作良好。

剪贴板与文件传输的竞争 :两者共享同一个Token池和agent_msg_queue。大文件传输会持续消耗Token,可能使剪贴板GRAB/REQUEST等小消息排在长队列后面。这是当前设计的一个局限------理论上可以通过优先级队列改善,但实际中文件传输块不大(FILE_XFER_CHUNK_SIZE),影响有限。

VDAgent能力协商

连接建立时,Guest Agent 通过 VD_AGENT_ANNOUNCE_CAPABILITIES 声明能力:

c 复制代码
// 能力标志示例
#define VD_AGENT_CAP_CLIPBOARD_BY_DEMAND  (1 << 0)  // 按需剪贴板
#define VD_AGENT_CAP_CLIPBOARD_SELECTION  (1 << 1)  // 多选择支持
#define VD_AGENT_CAP_FILE_XFER            (1 << 2)  // 文件传输

分析:客户端根据能力决定是否发送 GRAB、是否支持多 selection 等。能力协商保证了新旧版本 Agent 的兼容性。

剪贴板MIME类型

SPICE剪贴板支持多种MIME类型,常见有:

c 复制代码
// text/plain - 纯文本
// text/html - HTML片段
// image/png, image/jpeg - 图像
// 类型在 GRAB 的 types[] 中声明
// REQUEST 时指定请求的类型

分析:Guest Agent 和客户端根据各自支持的格式协商。文本粘贴最常用,图像需要两端都支持相应编解码。

VDI端口与virtio-serial

virtio-serial通信原理

virtio-serial是QEMU提供的虚拟串口设备,基于virtio标准实现高效的Host-Guest通信。与传统串口不同,virtio-serial使用共享内存的virtqueue实现,吞吐量接近内存拷贝速度。

复制代码
>通信路径:

Client  ──SPICE_MSGC_MAIN_AGENT_DATA──→  SPICE Server
                                              │
                                    RedCharDeviceVDIPort
                                              │
                                    VDIChunkHeader + payload
                                              │
                                         virtio-serial
                                              │
                                    /dev/virtio-ports/com.redhat.spice.0
                                              │
                                    Guest spice-vdagent

VDIChunkHeaderport字段有两个值:

  • VDP_CLIENT_PORT (1):标识消息来自客户端,Guest Agent据此知道这是远端的请求
  • VDP_SERVER_PORT (2):标识消息来自Guest,服务端读取时只转发CLIENT_PORT的消息,忽略SERVER_PORT(服务端自身处理)

消息分块 :VDAgentMessage的最大载荷为VD_AGENT_MAX_DATA_SIZE (2048)字节。大于此限制的消息(如大型图片剪贴板数据或文件传输块)会被自动分块------发送方拆分为多个VDI块,接收方通过msg_data_to_read状态跟踪重组。服务端的agent_msg_filter必须能正确处理分块消息,不能在消息中间切断过滤。

文件传输Task ID

每个SpiceFileTransferTask有唯一task_id,用于关联FILE_XFER_START、FILE_XFER_DATA、FILE_XFER_STATUS消息。Guest Agent用task_id区分并发传输的多个文件。

分析:多文件拖放时,每个文件独立task_id,消息可交错传输。Guest Agent维护每个task的状态机(等待START、接收数据、完成)。

总结(补充)

VDAgent架构的优势在于:服务端无需解析剪贴板或文件内容,仅做透明转发,降低了复杂度和安全风险。配置化的AgentMsgFilter使管理员可灵活控制功能开关,满足不同部署场景的安全合规要求。

相关推荐
@hdd1 天前
SPICE全链路分析(四):输入事件的旅程——从键盘按下到Guest响应
spice·远程桌面协议
@hdd4 天前
spice-gtk源码分析(八):音频播放与录制
spice·远程桌面协议·spice-gtk
@hdd9 天前
SPICE源码分析(十二):网络传输与安全机制
spice·远程桌面协议
@hdd13 天前
SPICE源码分析(一):整体架构与实现框架
spice·远程桌面协议
云雾J视界2 个月前
SPICE仿真进阶:AI芯片低功耗设计中的瞬态/AC分析实战
低功耗·仿真·spice·ai芯片·ac·均值估算
冰山一脚20132 年前
libusb注意事项笔记
spice
冰山一脚20132 年前
libspice显示命令调用流程分析
spice
冰山一脚20132 年前
spice VDAgent简介
spice