spice-gtk源码分析(二):SpiceSession与连接管理
连接管理:TCP连接、SSL/TLS握手、SASL认证通道生命周期:创建、管理和销毁所有通道配置管理:通过GObject属性系统管理连接参数迁移支持:处理虚拟机热迁移资源管理:管理图像缓存、GLZ解码窗口等共享资源理解的实现对于理解整个spice-gtk客户端的工作机制至关重要。
SpiceSession是spice-gtk的核心类,负责管理整个SPICE客户端会话。本文深入分析SpiceSession的实现细节,包括连接流程、通道管理、迁移支持等关键功能。
SpiceSession的角色
SpiceSession是整个客户端的中央管理器,可以理解为SPICE客户端的"大脑"。它负责:
- 连接管理:管理与SPICE服务器的TCP连接、SSL/TLS握手、SASL认证
- 通道生命周期:创建、管理和销毁所有通道对象
- 配置管理:存储和管理连接参数(host、port、password等)
- 迁移支持:处理虚拟机热迁移过程中的状态转移
- 资源管理:管理图像缓存、GLZ解码窗口等共享资源
- Agent通信:管理与Guest Agent的通信
SpiceSessionPrivate核心字段
SpiceSessionPrivate是SpiceSession的私有实现,包含了会话的所有状态:
// spice-session-priv.h (实际定义在spice-session.c中)
struct _SpiceSessionPrivate {
// 连接配置
char *host; // 服务器地址
char *unix_path; // Unix socket路径
char *port; // 普通端口
char *tls_port; // TLS端口
char *username; // 用户名
char *password; // 密码
char *ca_file; // CA证书文件路径
char *ciphers; // SSL密码套件
GByteArray *pubkey; // 服务器公钥
GByteArray *ca; // CA证书数据
char *cert_subject; // 证书主题
guint verify; // SSL验证标志
gboolean read_only; // 只读模式
SpiceURI *proxy; // 代理URI
// 功能开关
gboolean audio; // 是否启用音频
gboolean smartcard; // 是否启用智能卡
gboolean usbredir; // 是否启用USB重定向
gboolean gl_scanout; // 是否启用GL scanout
// 通道管理
int connection_id; // 连接ID
int protocol; // 协议版本
SpiceChannel *cmain; // Main Channel(弱引用)
GList *channels; // 所有通道列表
guint channels_destroying; // 正在销毁的通道计数
gboolean client_provided_sockets; // 是否使用客户端提供的socket
// 迁移相关
guint64 mm_time_offset; // 多媒体时间偏移
SpiceSession *migration; // 迁移目标会话
GList *migration_left; // 迁移中剩余的通道
SpiceSessionMigration migration_state; // 迁移状态
gboolean full_migration; // 是否完整迁移
guint disconnecting; // 断开连接计数
gboolean migrate_wait_init; // 等待迁移初始化
guint after_main_init; // Main Channel初始化后
gboolean for_migration; // 是否为迁移会话
// 共享资源
display_cache *images; // 图像缓存
SpiceGlzDecoderWindow *glz_window; // GLZ解码窗口
int images_cache_size; // 图像缓存大小
int glz_window_size; // GLZ窗口大小
uint32_t n_display_channels; // Display通道数量
guint8 uuid[16]; // VM UUID
gchar *name; // 会话名称
SpiceImageCompression preferred_compression; // 首选压缩方式
// 关联对象
SpiceAudio *audio_manager; // 音频管理器
SpiceUsbDeviceManager *usb_manager; // USB设备管理器
SpicePlaybackChannel *playback_channel; // Playback通道
PhodavServer *webdav; // WebDAV服务器
gint64 expired_time; // 过期时间
guint expired_timer_id; // 过期定时器ID
};
连接流程

1. 创建SpiceSession
// spice-session.c
SpiceSession *spice_session_new(void)
{
return g_object_new(SPICE_TYPE_SESSION, NULL);
}
static void spice_session_init(SpiceSession *session)
{
SpiceSessionPrivate *s;
gchar *channels;
SPICE_DEBUG("New session (compiled from package " PACKAGE_STRING ")");
s = session->priv = spice_session_get_instance_private(session);
s->expired_time = 0;
s->expired_timer_id = 0;
channels = spice_channel_supported_string();
SPICE_DEBUG("Supported channels: %s", channels);
g_free(channels);
// 初始化图像缓存和GLZ解码窗口
s->images = cache_image_new((GDestroyNotify)pixman_image_unref);
s->glz_window = glz_decoder_window_new();
update_proxy(session, NULL);
}
2. 设置连接属性
应用程序通过GObject属性系统设置连接参数:
// 方式1:直接设置属性
g_object_set(session,
"host", "192.168.1.100",
"port", "5930",
"password", "secret",
NULL);
// 方式2:通过URI设置
g_object_set(session,
"uri", "spice://192.168.1.100?port=5930&password=secret",
NULL);
URI解析在spice_session_set_property()中处理:
// spice-session.c
case PROP_URI: {
const gchar *uri = g_value_get_string(value);
if (uri && *uri) {
if (spice_uri_parse_session(session, uri) < 0) {
g_warning("Failed to parse URI: %s", uri);
}
}
break;
}
3. 建立连接
调用spice_session_connect()开始连接:
// spice-session.c
gboolean spice_session_connect(SpiceSession *session)
{
SpiceSessionPrivate *s;
g_return_val_if_fail(SPICE_IS_SESSION(session), FALSE);
s = session->priv;
g_return_val_if_fail(!s->disconnecting, FALSE);
// 断开之前的连接(保留Main Channel)
session_disconnect(session, TRUE);
s->client_provided_sockets = FALSE;
// 创建Main Channel(如果不存在)
if (s->cmain == NULL)
s->cmain = spice_channel_new(session, SPICE_CHANNEL_MAIN, 0);
// 清空GLZ解码窗口
glz_decoder_window_clear(s->glz_window);
// 连接Main Channel
return spice_channel_connect(s->cmain);
}
设计分析:
spice_session_connect()函数的设计体现了几个关键考虑:
- 保留Main Channel:调用
session_disconnect(session, TRUE)时传入TRUE参数,表示保留Main Channel。这是因为Main Channel是第一个连接的通道,负责协议握手和通道列表协商,在重连时应该保持其状态。- 清空GLZ解码窗口:GLZ(Graphics Lempel-Ziv)是一种图像压缩算法,解码窗口存储了历史图像数据用于解压缩。清空窗口确保重连时不会使用旧的、可能无效的压缩数据,避免图像显示错误。
- Main Channel的特殊地位:Main Channel必须在其他通道之前连接,因为只有通过Main Channel才能获取服务器支持的通道列表,然后才能创建和连接其他通道(Display、Inputs、Cursor等)。
4. Main Channel创建和协议握手
Main Channel是第一个连接的通道,负责协议握手和通道列表协商:
// channel-main.c
static void spice_main_channel_init(SpiceMainChannel *channel)
{
SpiceMainChannelPrivate *c = spice_main_channel_get_instance_private(channel);
c->mouse_mode = SPICE_MOUSE_MODE_SERVER;
c->requested_mouse_mode = SPICE_MOUSE_MODE_SERVER;
c->agent_connected = FALSE;
c->agent_caps_received = FALSE;
// ...
}
Main Channel连接成功后,会收到服务器发送的通道列表消息,然后创建对应的通道对象。
通道列表管理
SpiceSession使用GList管理所有通道:
// spice-session.c
void spice_session_channel_new(SpiceSession *session, SpiceChannel *channel)
{
g_return_if_fail(SPICE_IS_SESSION(session));
g_return_if_fail(SPICE_IS_CHANNEL(channel));
SpiceSessionPrivate *s = session->priv;
// 添加到通道列表
s->channels = g_list_prepend(s->channels, channel);
// 特殊处理Main Channel
if (SPICE_IS_MAIN_CHANNEL(channel)) {
gboolean all = spice_strv_contains(s->disable_effects, "all");
g_object_set(channel,
"disable-wallpaper", all || spice_strv_contains(s->disable_effects, "wallpaper"),
"disable-font-smooth", all || spice_strv_contains(s->disable_effects, "font-smooth"),
"disable-animation", all || spice_strv_contains(s->disable_effects, "animation"),
NULL);
CHANNEL_DEBUG(channel, "new main channel, switching");
s->cmain = channel;
} else if (SPICE_IS_PLAYBACK_CHANNEL(channel)) {
// 保存Playback Channel引用
g_warn_if_fail(s->playback_channel == NULL);
s->playback_channel = SPICE_PLAYBACK_CHANNEL(channel);
}
// 发送channel-new信号
g_signal_emit(session, signals[SPICE_SESSION_CHANNEL_NEW], 0, channel);
}
设计分析:
spice_session_channel_new()函数的实现细节值得注意:
- 使用
g_list_prepend而非g_list_append:g_list_prepend是O(1)操作,而g_list_append需要遍历到列表末尾,是O(n)操作。由于通道列表主要用于遍历和查找,顺序并不重要,使用prepend可以提高性能。- Main Channel的特殊处理:Main Channel需要应用
disable_effects配置(禁用壁纸、字体平滑、动画等效果),这些设置只对Main Channel有效,体现了Main Channel作为控制通道的特殊地位。channel-new信号的作用:这个信号允许应用程序在新通道创建时进行响应,例如自动创建对应的Display对象来处理Display Channel。这种设计将通道创建和应用程序逻辑解耦,提高了代码的灵活性。
通道销毁时从列表中移除:
// spice-session.c
static void spice_session_channel_destroy(SpiceSession *session, SpiceChannel *channel)
{
SpiceSessionPrivate *s = session->priv;
// 从列表中移除
s->channels = g_list_remove(s->channels, channel);
// 清除特殊引用
if (channel == SPICE_CHANNEL(s->cmain)) {
s->cmain = NULL;
}
if (SPICE_IS_PLAYBACK_CHANNEL(channel) && s->playback_channel == SPICE_PLAYBACK_CHANNEL(channel)) {
s->playback_channel = NULL;
}
// 发送channel-destroy信号
g_signal_emit(session, signals[SPICE_SESSION_CHANNEL_DESTROY], 0, channel);
}
连接属性详解
基本连接属性
| 属性名 | 类型 | 说明 |
|---|---|---|
host |
gchar* |
服务器地址(IP或主机名) |
port |
gchar* |
普通端口号(字符串格式) |
tls-port |
gchar* |
TLS端口号 |
password |
gchar* |
连接密码 |
username |
gchar* |
用户名(SASL认证) |
unix-path |
gchar* |
Unix socket路径 |
SSL/TLS相关属性
| 属性名 | 类型 | 说明 |
|---|---|---|
ca-file |
gchar* |
CA证书文件路径 |
ca |
GByteArray* |
CA证书数据 |
verify |
SpiceSessionVerify |
SSL验证标志(PUBKEY/HOSTNAME/SUBJECT) |
ciphers |
gchar* |
SSL密码套件 |
cert-subject |
gchar* |
证书主题 |
功能开关属性
| 属性名 | 类型 | 说明 |
|---|---|---|
audio |
gboolean |
是否启用音频 |
smartcard |
gboolean |
是否启用智能卡 |
usbredir |
gboolean |
是否启用USB重定向 |
gl-scanout |
gboolean |
是否启用GL scanout |
read-only |
gboolean |
只读模式 |
URI解析
URI解析在spice_uri_parse_session()中实现:
// spice-session.c
static int spice_uri_parse_session(SpiceSession *session, const gchar *uri)
{
SpiceSessionPrivate *s = session->priv;
gchar *host = NULL, *port = NULL, *tls_port = NULL;
gchar *username = NULL, *password = NULL;
const gchar *authority, *path, *query;
gboolean tls_scheme = FALSE;
// ...
// 解析scheme
if (g_str_has_prefix(uri, "spice+unix://")) {
// Unix socket
path = uri + strlen("spice+unix://");
} else if (g_str_has_prefix(uri, "spices://") ||
g_str_has_prefix(uri, "spice+tls://")) {
// TLS连接
tls_scheme = TRUE;
authority = uri + strlen("spices://");
} else if (g_str_has_prefix(uri, "spice://")) {
// 普通连接
authority = uri + strlen("spice://");
}
// 解析authority(host:port)
// 解析query参数(port, tls-port, password等)
// ...
// 应用解析结果
s->host = host;
s->port = port;
s->tls_port = tls_port;
s->username = username;
s->password = password;
return 0;
}
迁移支持
spice-gtk支持虚拟机热迁移,迁移过程涉及状态机的管理:
SpiceSessionMigration枚举
// spice-session.h
typedef enum {
SPICE_SESSION_MIGRATION_NONE, // 无迁移
SPICE_SESSION_MIGRATION_SWITCHING, // 切换主机(销毁并重连)
SPICE_SESSION_MIGRATION_MIGRATING, // 无缝迁移(重连)
SPICE_SESSION_MIGRATION_CONNECTING, // 连接到目标主机
} SpiceSessionMigration;
迁移状态机
迁移过程的状态转换:
- NONE → CONNECTING:开始迁移,创建迁移目标会话
- CONNECTING → MIGRATING:迁移目标会话连接成功
- MIGRATING → NONE:迁移完成或失败
// spice-session.c
void spice_session_start_migrating(SpiceSession *session,
gboolean full_migration)
{
SpiceSessionPrivate *s = session->priv;
SpiceSessionPrivate *m;
g_return_if_fail(s->migration != NULL);
m = s->migration->priv;
g_return_if_fail(m->migration_state == SPICE_SESSION_MIGRATION_CONNECTING);
s->full_migration = full_migration;
spice_session_set_migration_state(session, SPICE_SESSION_MIGRATION_MIGRATING);
// 交换连接信息
SWAP_STR(s->host, m->host);
SWAP_STR(s->port, m->port);
SWAP_STR(s->tls_port, m->tls_port);
SWAP_STR(s->unix_path, m->unix_path);
// 记录迁移中剩余的通道
s->migration_left = spice_session_get_channels(session);
}
设计分析:
spice_session_start_migrating()函数中的SWAP_STR宏是一个巧妙的设计:SWAP_STR宏通过交换指针的方式实现连接信息的转移,而不是复制字符串。这样做的好处是:
- 性能优化:避免了字符串的复制操作,只需要交换两个指针,是O(1)操作。
- 内存管理简化:不需要担心字符串的内存释放和重新分配,只需要在最终清理时释放一次。
- 状态一致性:通过交换指针,源会话和目标会话的连接信息可以无缝切换,确保迁移过程中状态的一致性。
这种设计体现了C语言中指针操作的优雅性,在保证功能正确的同时最大化了性能。
迁移中的通道交换
迁移时,需要将通道的socket从源会话交换到目标会话:
// spice-session.c
void spice_session_channel_migrate(SpiceSession *session, SpiceChannel *channel)
{
SpiceSessionPrivate *s = session->priv;
SpiceChannel *mig_channel;
if (s->migration == NULL)
return;
// 查找目标会话中对应的通道
mig_channel = spice_session_lookup_channel(s->migration,
spice_channel_get_channel_id(channel),
spice_channel_get_channel_type(channel));
// 交换通道socket
spice_channel_swap(channel, mig_channel, !s->full_migration);
}
Agent状态管理
Guest Agent是运行在虚拟机内的代理程序,提供剪贴板同步、文件传输等功能。Main Channel负责管理与Agent的通信:
// channel-main.c
struct _SpiceMainChannelPrivate {
bool agent_connected; // Agent是否连接
bool agent_caps_received; // 是否收到Agent能力
int agent_tokens; // Agent令牌
VDAgentMessage agent_msg; // Agent消息缓冲区
guint8 *agent_msg_data; // Agent消息数据
guint agent_msg_pos; // Agent消息位置
uint8_t agent_msg_size; // Agent消息大小
uint32_t agent_caps[VD_AGENT_CAPS_SIZE]; // Agent能力
// ...
};
Agent连接状态通过agent-connected属性暴露:
// channel-main.c
gboolean spice_main_channel_get_agent_connected(SpiceMainChannel *channel)
{
g_return_val_if_fail(SPICE_IS_MAIN_CHANNEL(channel), FALSE);
return spice_main_channel_get_instance_private(channel)->agent_connected;
}
安全连接
SSL/TLS参数
spice-gtk支持SSL/TLS加密连接,相关参数包括:
- CA证书:通过
ca-file或ca属性指定 - 验证标志:
verify属性控制验证方式SPICE_SESSION_VERIFY_PUBKEY:验证公钥匹配SPICE_SESSION_VERIFY_HOSTNAME:验证主机名匹配SPICE_SESSION_VERIFY_SUBJECT:验证证书主题匹配
- 密码套件:通过
ciphers属性指定
SASL认证
如果服务器支持SASL,客户端可以选择SASL认证:
// spice-channel.c
if (spice_channel_test_common_capability(channel, SPICE_COMMON_CAP_AUTH_SASL)) {
CHANNEL_DEBUG(channel, "Choosing SASL mechanism");
auth.auth_mechanism = GUINT32_TO_LE(SPICE_COMMON_CAP_AUTH_SASL);
spice_channel_write(channel, &auth, sizeof(auth));
if (!spice_channel_perform_auth_sasl(channel))
return FALSE;
}
SASL认证需要用户名和密码,通过username和password属性设置。
信号
SpiceSession定义了以下信号:
channel-new
当新通道创建时发出:
// spice-session.h
void (*channel_new)(SpiceSession *session, SpiceChannel *channel);
应用程序可以连接此信号来处理新通道:
g_signal_connect(session, "channel-new",
G_CALLBACK(on_channel_new), NULL);
static void on_channel_new(SpiceSession *session, SpiceChannel *channel, gpointer user_data)
{
if (SPICE_IS_DISPLAY_CHANNEL(channel)) {
// 处理Display Channel
SpiceDisplay *display = spice_display_new(session, 0);
// ...
}
}
channel-destroy
当通道销毁时发出:
// spice-session.h
void (*channel_destroy)(SpiceSession *session, SpiceChannel *channel);
disconnected
当会话断开连接时发出(内部使用)。
总结
SpiceSession是spice-gtk的核心管理器,负责:
- 连接管理:TCP连接、SSL/TLS握手、SASL认证
- 通道生命周期:创建、管理和销毁所有通道
- 配置管理:通过GObject属性系统管理连接参数
- 迁移支持:处理虚拟机热迁移
- 资源管理:管理图像缓存、GLZ解码窗口等共享资源
理解SpiceSession的实现对于理解整个spice-gtk客户端的工作机制至关重要。
更多推荐
所有评论(0)