spice-gtk支持将本地USB设备重定向到远程虚拟机,实现USB设备的透明使用。本文分析USB重定向的实现机制。

背景与目标

USB重定向允许用户将客户端的USB设备(如U盘、打印机、扫描仪等)连接到远程虚拟机,就像设备直接插在虚拟机上一样。spice-gtk通过以下组件实现:

  • SpiceUsbDeviceManager:管理USB设备列表和自动连接
  • SpiceUsbredirChannel:USB数据传输通道
  • USB后端抽象:封装libusb操作
  • ACL助手:处理Linux权限问题

在这里插入图片描述

SpiceUsbDeviceManager:设备管理

核心数据结构

// usb-device-manager.c
struct _SpiceUsbDeviceManagerPrivate {
    SpiceSession *session;                    // 关联的会话
    gboolean auto_connect;                    // 是否自动连接
    gchar *auto_connect_filter;              // 自动连接过滤规则
    gchar *redirect_on_connect;              // 连接时重定向规则
#ifdef USE_USBREDIR
    SpiceUsbBackend *context;                 // USB后端上下文
    struct usbredirfilter_rule *auto_conn_filter_rules;      // 解析后的过滤规则
    struct usbredirfilter_rule *redirect_on_connect_rules;
    int auto_conn_filter_rules_count;
    int redirect_on_connect_rules_count;
    gboolean redirecting;                     // 是否正在重定向
#ifdef G_OS_WIN32
    usbdk_api_wrapper *usbdk_api;            // UsbDk API包装器
    HANDLE usbdk_hider_handle;                // UsbDk隐藏句柄
#endif
    GPtrArray *devices;                      // USB设备列表
    GPtrArray *channels;                     // USB重定向通道列表
#endif
};

设备热插拔处理

SpiceUsbDeviceManager监听USB设备的插拔事件:

// usb-device-manager.c
static void spice_usb_device_manager_hotplug_cb(void *user_data,
                                                SpiceUsbDevice *dev,
                                                gboolean added)
{
    SpiceUsbDeviceManager *manager = SPICE_USB_DEVICE_MANAGER(user_data);
    struct hotplug_idle_cb_args *args = g_malloc0(sizeof(*args));

    // 通过idle回调切换到主线程处理
    args->manager = g_object_ref(manager);
    args->device = spice_usb_backend_device_ref(dev);
    args->added = added;
    g_idle_add(spice_usb_device_manager_hotplug_idle_cb, args);
}

static gboolean spice_usb_device_manager_hotplug_idle_cb(gpointer user_data)
{
    struct hotplug_idle_cb_args *args = user_data;
    SpiceUsbDeviceManager *manager = SPICE_USB_DEVICE_MANAGER(args->manager);

    if (args->added) {
        spice_usb_device_manager_add_dev(manager, args->device);
    } else {
        spice_usb_device_manager_remove_dev(manager, args->device);
    }

    spice_usb_backend_device_unref(args->device);
    g_object_unref(manager);
    g_free(args);
    return FALSE;
}

static void spice_usb_device_manager_add_dev(SpiceUsbDeviceManager *manager,
                                             SpiceUsbDevice *bdev)
{
    SpiceUsbDeviceManagerPrivate *priv = manager->priv;
    const UsbDeviceInformation *b_info = spice_usb_backend_device_get_info(bdev);
    SpiceUsbDevice *device;

    // 检查设备是否已存在
    if (spice_usb_device_manager_find_device(manager,
                                             b_info->bus,
                                             b_info->address)) {
        SPICE_DEBUG("device not added %d:%d %04x:%04x",
                    b_info->bus, b_info->address, b_info->vid, b_info->pid);
        return;
    }

    device = spice_usb_device_new(bdev);
    if (!device) {
        return;
    }

    g_ptr_array_add(priv->devices, device);

    // 如果启用自动连接,检查过滤规则并连接
    if (priv->auto_connect) {
        gboolean can_redirect, auto_ok;

        can_redirect = spice_usb_device_manager_can_redirect_device(manager, device, NULL);

        auto_ok = spice_usb_backend_device_check_filter(bdev,
                                                        priv->auto_conn_filter_rules,
                                                        priv->auto_conn_filter_rules_count) == 0;

        if (can_redirect && auto_ok) {
            spice_usb_device_manager_connect_device_async(manager, device, NULL,
                                                          spice_usb_device_manager_auto_connect_cb,
                                                          spice_usb_device_ref(device));
        }
    }

    SPICE_DEBUG("device added %04x:%04x (%p)",
                spice_usb_device_get_vid(device),
                spice_usb_device_get_pid(device),
                device);
    g_signal_emit(manager, signals[DEVICE_ADDED], 0, device);
}

spice_usb_device_manager_add_dev()函数设计分析:

  • 线程切换机制:热插拔回调通过g_idle_add切换到主线程处理,这是因为libusb的回调函数在其自己的线程中执行。GLib的对象系统要求在主线程中操作,因此需要通过idle回调将操作延迟到主线程,确保线程安全。
  • 自动连接的二重检查:函数先检查can_redirect(检查是否有可用通道、设备是否已被占用等),再检查filter_check(检查过滤规则)。这种二重检查确保了只有在满足所有条件时才会自动连接设备,避免了不必要的连接尝试和错误处理。

过滤规则

spice-gtk使用过滤规则控制哪些设备可以自动连接:

// usb-device-manager.c
// 过滤规则格式:class,vendor,product,version,allow
// 多个规则用|分隔,规则内用,分隔
// 例如:"0x03,-1,-1,-1,0|-1,-1,-1,-1,1"
// 含义:拒绝HID类设备(0x03),允许其他所有设备

static void spice_usb_device_manager_set_property(GObject *gobject,
                                                  guint prop_id,
                                                  const GValue *value,
                                                  GParamSpec *pspec)
{
    // ...
    case PROP_AUTO_CONNECT_FILTER: {
        const gchar *filter = g_value_get_string(value);
#ifdef USE_USBREDIR
        struct usbredirfilter_rule *rules;
        int r, count;

        // 解析过滤规则字符串
        r = usbredirfilter_string_to_rules(filter, ",", "|", &rules, &count);
        if (r) {
            if (r == -ENOMEM) {
                g_error("Failed to allocate memory for auto-connect-filter");
            }
            g_warning("Error parsing auto-connect-filter string, keeping old filter");
            break;
        }

        SPICE_DEBUG("auto-connect filter set to %s", filter);
        free(priv->auto_conn_filter_rules);
        priv->auto_conn_filter_rules = rules;
        priv->auto_conn_filter_rules_count = count;
#endif
        g_free(priv->auto_connect_filter);
        priv->auto_connect_filter = g_strdup(filter);
        break;
    }
    // ...
}

过滤规则示例:

规则字符串说明
"0x03,-1,-1,-1,0"拒绝所有HID类设备(键盘、鼠标)
"-1,-1,-1,-1,1"允许所有设备
"0x03,-1,-1,-1,0|-1,-1,-1,-1,1"拒绝HID类,允许其他
"0x08,0x1234,0x5678,-1,1"允许特定VID/PID的存储设备

异步连接设备

spice-gtk使用异步API连接USB设备:

// usb-device-manager.c
void spice_usb_device_manager_connect_device_async(SpiceUsbDeviceManager *manager,
                                                   SpiceUsbDevice *device,
                                                   GCancellable *cancellable,
                                                   GAsyncReadyCallback callback,
                                                   gpointer user_data)
{
    g_return_if_fail(SPICE_IS_USB_DEVICE_MANAGER(manager));

#ifdef USE_USBREDIR
    GTask *task = g_task_new(G_OBJECT(manager), cancellable, callback, user_data);

    g_task_set_task_data(task, device, NULL);

    _set_redirecting(manager, TRUE);
    _spice_usb_device_manager_connect_device_async(manager, device,
                                                   cancellable,
                                                   _connect_device_async_cb,
                                                   task);
#endif
}

static void
_spice_usb_device_manager_connect_device_async(SpiceUsbDeviceManager *manager,
                                               SpiceUsbDevice *device,
                                               GCancellable *cancellable,
                                               GAsyncReadyCallback callback,
                                               gpointer user_data)
{
    GTask *task;
    SpiceUsbDeviceManagerPrivate *priv = manager->priv;
    guint i;

    task = g_task_new(manager, cancellable, callback, user_data);

    // 检查设备是否已连接
    if (spice_usb_device_manager_is_device_connected(manager, device)) {
        g_task_return_new_error(task,
                                SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
                                "Cannot connect an already connected usb device");
        goto done;
    }

    // 查找空闲的通道
    for (i = 0; i < priv->channels->len; i++) {
        SpiceUsbredirChannel *channel = g_ptr_array_index(priv->channels, i);

        if (spice_usbredir_channel_get_device(channel)) {
            continue; /* Skip already used channels */
        }

        // 异步连接到通道
        spice_usbredir_channel_connect_device_async(channel,
                                                    device,
                                                    cancellable,
                                                    spice_usb_device_manager_channel_connect_cb,
                                                    task);
        return;
    }

    // 没有空闲通道
    g_task_return_new_error(task,
                            SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
                            _("No free USB channel"));
done:
    g_object_unref(task);
}

SpiceUsbredirChannel:数据传输通道

通道状态管理

// channel-usbredir.c
enum SpiceUsbredirChannelState {
    STATE_DISCONNECTED,
#ifdef USE_POLKIT
    STATE_WAITING_FOR_ACL_HELPER,  // 等待ACL助手完成权限设置
#endif
    STATE_CONNECTED,
    STATE_DISCONNECTING,
};

struct _SpiceUsbredirChannelPrivate {
    SpiceUsbDevice *device;                   // 关联的USB设备
    SpiceUsbBackend *context;                 // USB后端上下文
    SpiceUsbBackendChannel *host;             // USB后端通道
    enum SpiceUsbredirChannelState state;     // 当前状态
#ifdef USE_POLKIT
    GTask *task;                              // 异步任务
    SpiceUsbAclHelper *acl_helper;            // ACL助手
#endif
    GMutex device_connect_mutex;              // 设备连接互斥锁
};

设备连接流程

连接USB设备需要经过多个步骤:

// channel-usbredir.c
void spice_usbredir_channel_connect_device_async(SpiceUsbredirChannel *channel,
                                                 SpiceUsbDevice *device,
                                                 GCancellable *cancellable,
                                                 GAsyncReadyCallback callback,
                                                 gpointer user_data)
{
    SpiceUsbredirChannelPrivate *priv = channel->priv;
#ifdef USE_POLKIT
    const UsbDeviceInformation *info = spice_usb_backend_device_get_info(device);
#endif
    GTask *task;

    g_return_if_fail(SPICE_IS_USBREDIR_CHANNEL(channel));
    g_return_if_fail(device != NULL);

    CHANNEL_DEBUG(channel, "connecting device %04x:%04x (%p) to channel %p",
                  spice_usb_device_get_vid(device),
                  spice_usb_device_get_pid(device),
                  device, channel);

    task = g_task_new(channel, cancellable, callback, user_data);

    if (!priv->host) {
        g_task_return_new_error(task,
                            SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
                            "Error libusb context not set");
        goto done;
    }

    if (priv->state != STATE_DISCONNECTED) {
        g_task_return_new_error(task,
                            SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
                            "Error channel is busy");
        goto done;
    }

    priv->device = spice_usb_backend_device_ref(device);

#ifdef USE_POLKIT
    // 如果不是模拟设备,需要设置ACL权限
    if (info->bus != BUS_NUMBER_FOR_EMULATED_USB) {
        priv->task = task;
        priv->state = STATE_WAITING_FOR_ACL_HELPER;
        priv->acl_helper = spice_usb_acl_helper_new();
        // 禁止键盘抓取(避免权限对话框被拦截)
        g_object_set(spice_channel_get_session(SPICE_CHANNEL(channel)),
                    "inhibit-keyboard-grab", TRUE, NULL);
        // 异步打开ACL
        spice_usb_acl_helper_open_acl_async(priv->acl_helper,
                                            info->bus,
                                            info->address,
                                            cancellable,
                                            spice_usbredir_channel_open_acl_cb,
                                            channel);
        return;
    }
#endif
    // 模拟设备直接连接
    g_task_run_in_thread(task, _open_device_async_cb);

done:
    g_object_unref(task);
}

spice_usbredir_channel_connect_device_async()函数设计分析:

  • ACL权限处理流程:对于非模拟设备,函数首先进入STATE_WAITING_FOR_ACL_HELPER状态,然后启动ACL助手进程。ACL助手通过polkit请求权限,可能需要用户交互(如输入密码)。只有在权限获取成功后,才会继续执行设备连接。
  • inhibit-keyboard-grab的作用:在启动ACL助手之前,函数设置inhibit-keyboard-grab为TRUE。这是因为如果SPICE客户端正在抓取键盘输入,polkit的权限对话框可能无法接收键盘输入,导致用户无法完成权限授权。禁止键盘抓取确保了权限对话框的正常工作。
  • 模拟设备优化:模拟设备(如CD共享创建的虚拟USB设备)使用特殊的bus号BUS_NUMBER_FOR_EMULATED_USB,可以直接跳过ACL检查,因为这些设备不需要系统权限。这种优化提高了虚拟设备的连接速度。

static void
_open_device_async_cb(GTask *task,
gpointer object,
gpointer task_data,
GCancellable *cancellable)
{
GError *err = NULL;
SpiceUsbredirChannel *channel = SPICE_USBREDIR_CHANNEL(object);
SpiceUsbredirChannelPrivate *priv = channel->priv;

spice_usbredir_channel_lock(channel);

if (!spice_usbredir_channel_open_device(channel, &err)) {
    g_clear_pointer(&priv->device, spice_usb_backend_device_unref);
}

spice_usbredir_channel_unlock(channel);

if (err) {
    g_task_return_error(task, err);
} else {
    g_task_return_boolean(task, TRUE);
}

}

static gboolean spice_usbredir_channel_open_device(
SpiceUsbredirChannel *channel, GError **err)
{
SpiceUsbredirChannelPrivate *priv = channel->priv;

g_return_val_if_fail(priv->state == STATE_DISCONNECTED

#ifdef USE_POLKIT
|| priv->state == STATE_WAITING_FOR_ACL_HELPER
#endif
, FALSE);

// 将设备附加到USB后端通道
if (!spice_usb_backend_channel_attach(priv->host, priv->device, err)) {
    if (*err == NULL) {
        g_set_error(err, SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
            "Error attaching device: (no error information)");
    }
    return FALSE;
}

priv->state = STATE_CONNECTED;

return TRUE;

}


### 数据收发处理

USB重定向通道处理双向数据传输:

```c
// channel-usbredir.c
/* coroutine context */
static void usbredir_handle_msg(SpiceChannel *c, SpiceMsgIn *in)
{
    SpiceUsbredirChannel *channel = SPICE_USBREDIR_CHANNEL(c);
    SpiceUsbredirChannelPrivate *priv = channel->priv;
    int r = 0, size;
    uint8_t *buf;

    g_return_if_fail(priv->host != NULL);

    // 处理压缩数据
    if (spice_msg_in_type(in) == SPICE_MSG_SPICEVMC_COMPRESSED_DATA) {
        SpiceMsgCompressedData *compressed_data_msg = spice_msg_in_parsed(in);
        if (try_handle_compressed_msg(compressed_data_msg, &buf, &size)) {
            /* uncompressed ok*/
        } else {
            buf = NULL;
            r = USB_REDIR_ERROR_READ_PARSE;
        }
    } else { /* Regular SPICE_MSG_SPICEVMC_DATA msg */
        buf = spice_msg_in_raw(in, &size);
    }

    spice_usbredir_channel_lock(channel);
    if (r == 0)
        // 读取来自虚拟机的USB数据
        r = spice_usb_backend_read_guest_data(priv->host, buf, size);
    if (r != 0 && priv->device != NULL) {
        // 处理错误
        SpiceUsbDevice *device = priv->device;
        device_error_data err_data;
        gchar *desc;
        GError *err;

        desc = spice_usb_device_get_description(device, NULL);
        err = spice_usb_backend_get_error_details(r, desc);
        g_free(desc);

        CHANNEL_DEBUG(c, "%s", err->message);

        err_data.channel = channel;
        err_data.caller = coroutine_self();
        err_data.device = spice_usb_backend_device_ref(device);
        err_data.error = err;
        spice_usbredir_channel_unlock(channel);
        // 切换到主线程处理错误
        g_idle_add(device_error, &err_data);
        coroutine_yield(NULL);

        spice_usb_backend_device_unref(err_data.device);
        g_error_free(err);
    } else {
        spice_usbredir_channel_unlock(channel);
    }
    if (spice_msg_in_type(in) == SPICE_MSG_SPICEVMC_COMPRESSED_DATA) {
        g_free(buf);
    }
}

usbredir_handle_msg()函数设计分析:

  • 压缩数据处理:函数首先检查消息类型是否为SPICE_MSG_SPICEVMC_COMPRESSED_DATA,如果是压缩数据,则调用try_handle_compressed_msg进行解压。这种设计支持LZ4压缩,可以减少网络传输量,提高USB重定向的性能。
  • 错误处理的异步性:当发生错误时,函数通过g_idle_add将错误处理切换到主线程。这是因为错误处理可能涉及UI更新(如显示错误对话框),而协程上下文不适合直接操作UI。idle回调确保了错误处理在主线程中安全执行。
  • 互斥锁保护:函数使用spice_usbredir_channel_lock/unlock保护设备操作,防止多个协程同时访问USB设备。这种保护机制确保了USB操作的原子性,避免了竞态条件。

// 写入数据到虚拟机
G_GNUC_INTERNAL
int spice_usbredir_write(SpiceUsbredirChannel *channel, uint8_t *data, int count)
{
SpiceMsgOut *msg_out;

#ifdef USE_LZ4
// 尝试压缩(如果支持)
if (try_write_compress_LZ4(channel, data, count)) {
spice_usb_backend_return_write_data(channel->priv->host, data);
return count;
}
#endif
// 发送未压缩数据
msg_out = spice_msg_out_new(SPICE_CHANNEL(channel),
SPICE_MSGC_SPICEVMC_DATA);
spice_marshaller_add_by_ref_full(msg_out->marshaller, data, count,
usbredir_free_write_cb_data, channel);
spice_msg_out_send(msg_out);

return count;

}


## USB后端抽象

spice-gtk通过`SpiceUsbBackend`抽象层封装libusb操作:

```c
// usb-backend.h
typedef struct _SpiceUsbBackend SpiceUsbBackend;
typedef struct _SpiceUsbBackendChannel SpiceUsbBackendChannel;

// USB后端API
SpiceUsbBackend *spice_usb_backend_new(GError **error);
void spice_usb_backend_delete(SpiceUsbBackend *context);

// 热插拔注册
gboolean spice_usb_backend_register_hotplug(SpiceUsbBackend *be,
                                            void *user_data,
                                            usb_hot_plug_callback proc,
                                            GError **error);
void spice_usb_backend_deregister_hotplug(SpiceUsbBackend *be);

// 设备操作
SpiceUsbDevice *spice_usb_backend_device_ref(SpiceUsbDevice *dev);
void spice_usb_backend_device_unref(SpiceUsbDevice *dev);
gconstpointer spice_usb_backend_device_get_libdev(const SpiceUsbDevice *dev);
const UsbDeviceInformation* spice_usb_backend_device_get_info(const SpiceUsbDevice *dev);
gboolean spice_usb_backend_device_isoch(SpiceUsbDevice *dev);

// 通道操作
SpiceUsbBackendChannel *spice_usb_backend_channel_new(SpiceUsbBackend *context,
                                                      SpiceUsbredirChannel *usbredir_channel);
void spice_usb_backend_channel_delete(SpiceUsbBackendChannel *ch);
gboolean spice_usb_backend_channel_attach(SpiceUsbBackendChannel *ch,
                                         SpiceUsbDevice *dev,
                                         GError **error);
void spice_usb_backend_channel_detach(SpiceUsbBackendChannel *ch);
int spice_usb_backend_read_guest_data(SpiceUsbBackendChannel *ch, uint8_t *data, int count);
void spice_usb_backend_return_write_data(SpiceUsbBackendChannel *ch, void *data);

后端抽象的优势:

  1. 平台无关:隐藏libusb的平台特定实现细节
  2. 易于测试:可以替换为模拟后端进行测试
  3. 扩展性:支持Windows的UsbDk等特殊实现

ACL助手:权限管理

在Linux系统上,访问USB设备需要适当的权限。spice-gtk使用polkit通过ACL助手获取权限:

// usb-acl-helper.c
struct _SpiceUsbAclHelperPrivate {
    GTask *task;
    GIOChannel *in_ch;           // 标准输入通道
    GIOChannel *out_ch;           // 标准输出通道
    GCancellable *cancellable;
    gulong cancellable_id;
};

void spice_usb_acl_helper_open_acl_async(SpiceUsbAclHelper *self,
                                         guint16 bus,
                                         guint8 address,
                                         GCancellable *cancellable,
                                         GAsyncReadyCallback callback,
                                         gpointer user_data)
{
    SpiceUsbAclHelperPrivate *priv = self->priv;
    gchar *argv[4];
    GError *error = NULL;
    GPid pid;
    gint stdin_fd, stdout_fd;

    // 启动polkit助手进程
    argv[0] = SPICE_CLIENT_GLIB_USB_ACL_HELPER;
    argv[1] = g_strdup_printf("%u", bus);
    argv[2] = g_strdup_printf("%u", address);
    argv[3] = NULL;

    // 使用g_spawn_async_with_pipes创建进程
    if (!g_spawn_async_with_pipes(NULL, argv, NULL,
                                   G_SPAWN_DO_NOT_REAP_CHILD |
                                   G_SPAWN_STDERR_TO_DEV_NULL,
                                   NULL, NULL, &pid,
                                   &stdin_fd, &stdout_fd, NULL, &error)) {
        g_task_return_error(priv->task, error);
        return;
    }

    // 设置IO通道监视输出
    priv->out_ch = g_io_channel_unix_new(stdout_fd);
    g_io_channel_set_encoding(priv->out_ch, NULL, NULL);
    g_io_channel_set_buffered(priv->out_ch, FALSE);
    g_io_add_watch(priv->out_ch, G_IO_IN | G_IO_HUP, cb_out_watch, self);

    // 等待polkit对话框完成
    // ...
}

ACL助手流程:

  1. 启动spice-client-glib-usb-acl-helper进程
  2. 进程通过polkit请求权限
  3. 显示权限对话框(如果需要)
  4. 设置USB设备节点的ACL权限
  5. 返回结果给spice-gtk

CD共享:虚拟光驱

spice-gtk支持将ISO镜像或物理CD作为USB设备共享:

// usb-device-manager.c
gboolean
spice_usb_device_manager_create_shared_cd_device(SpiceUsbDeviceManager *manager,
                                                 gchar *filename,
                                                 GError **err)
{
#ifdef USE_USBREDIR
    SpiceUsbDeviceManagerPrivate *priv = manager->priv;

    CdEmulationParams cd_params = {
        .filename = filename,
        .delete_on_eject = 1,
    };

    // 创建模拟的USB CD设备
    return create_emulated_cd(priv->context, &cd_params, err);
#else
    g_set_error_literal(err, SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
                        _("USB redirection support not compiled in"));
    return FALSE;
#endif
}

CD共享实现:

  • 使用USB Mass Storage Device (MSD)协议模拟USB CD设备
  • 支持ISO镜像文件和物理CD设备
  • 通过SCSI命令处理CD读取操作
  • 设备VID/PID使用Red Hat的特殊值(0x2b23:0xCDCD)

信号与事件

SpiceUsbDeviceManager信号

信号名参数说明
device-addeddeviceUSB设备插入
device-removeddeviceUSB设备拔出
auto-connect-faileddevice, error自动连接失败
device-errordevice, error设备错误

属性

属性名类型说明
auto-connectgboolean是否自动连接新设备
auto-connect-filtergchar*自动连接过滤规则
redirect-on-connectgchar*连接时重定向规则
free-channelsgint可用通道数量

总结

spice-gtk的USB重定向实现具有以下特点:

  1. 设备管理:通过SpiceUsbDeviceManager统一管理USB设备列表和连接状态
  2. 过滤机制:支持灵活的过滤规则控制自动连接行为
  3. 异步操作:所有设备连接操作都是异步的,避免阻塞UI
  4. 权限处理:通过polkit和ACL助手处理Linux权限问题
  5. 数据压缩:支持LZ4压缩减少网络传输量
  6. CD共享:支持将ISO镜像作为USB设备共享
  7. 错误处理:完善的错误处理和恢复机制

这种设计使得spice-gtk能够提供可靠的USB重定向功能,同时保持良好的用户体验。

更多推荐