VNC介绍

vnc是一个桌面传输协议,使用的是RFB协议格式,RFB协议是一个基于TCP的应用层传输协议。基于vnc协议实现的程序有很多,最出名的两个就是大家所熟悉的TightVNC和RealVNC。同样,Qemu模拟器中也实现了vnc(qemu中的vnc为Server端),qemu中的vnc是为了用于展示虚拟机的界面,方便用户和虚拟机交互,今天就来分析下Qemu中的vnc实现。

Qemu中VNC参数

Qemu是在vl.c的main函数中通过QEMU_OPTION_vnc选项对vnc命令行参数进行解析,vnc的参数格式定义在vnc.c文件中,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
static QemuOptsList qemu_vnc_opts = {
.name = "vnc",
.head = QTAILQ_HEAD_INITIALIZER(qemu_vnc_opts.head),
.implied_opt_name = "vnc",
.desc = {
{
.name = "vnc",
.type = QEMU_OPT_STRING,
},{
.name = "websocket",
.type = QEMU_OPT_STRING,
},{
.name = "tls-creds",
.type = QEMU_OPT_STRING,
},{
.name = "share",
.type = QEMU_OPT_STRING,
},{
.name = "display",
.type = QEMU_OPT_STRING,
},{
.name = "head",
.type = QEMU_OPT_NUMBER,
},{
.name = "connections",
.type = QEMU_OPT_NUMBER,
},{
.name = "to",
.type = QEMU_OPT_NUMBER,
},{
.name = "ipv4",
.type = QEMU_OPT_BOOL,
},{
.name = "ipv6",
.type = QEMU_OPT_BOOL,
},{
.name = "password",
.type = QEMU_OPT_BOOL,
},{
.name = "reverse",
.type = QEMU_OPT_BOOL,
},{
.name = "lock-key-sync",
.type = QEMU_OPT_BOOL,
},{
.name = "key-delay-ms",
.type = QEMU_OPT_NUMBER,
},{
.name = "sasl",
.type = QEMU_OPT_BOOL,
},{
.name = "acl",
.type = QEMU_OPT_BOOL,
},{
.name = "tls-authz",
.type = QEMU_OPT_STRING,
},{
.name = "sasl-authz",
.type = QEMU_OPT_STRING,
},{
.name = "lossy",
.type = QEMU_OPT_BOOL,
},{
.name = "non-adaptive",
.type = QEMU_OPT_BOOL,
},
{ /* end of list */ }
},
};

上述参数含义

名称 解释
vnc vnc地址
websocket 是否使用websocket协议
tls-creds tls证书
share vnc协议的共享策略(忽略、排他、强制共享三种策略)
display 展示哪个串口设备
head 和display一起使用,用于判断串口设备
connections vnc连接数限制,默认32
to 是否使用websocket协议
ipv4 vnc地址使用ipv4格式
ipv6 vnc地址使用ipv6格式
password vnc密码
reverse
lock-key-sync numlock以及capslock键状态同步
key-delay-ms 键盘输入延迟时间(单位:毫秒)
sasl 是否开启sasl认证
acl 不再使用
tls-authz vnc使用tls认证
sasl-authz vnc使用sasl认证
lossy 如果启用lossy,则禁用adaptive updates,即低质量画面传输
non-adaptive 是否使用高质量画面传输(禁用压缩)
## VNC主要数据结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
struct VncDisplay
{
QTAILQ_HEAD(, VncState) clients;
int num_connecting;
int num_shared;
int num_exclusive;
int connections_limit;
VncSharePolicy share_policy;
QIONetListener *listener;
QIONetListener *wslistener;
DisplaySurface *ds;
DisplayChangeListener dcl;
kbd_layout_t *kbd_layout;
int lock_key_sync;
QEMUPutLEDEntry *led;
int ledstate;
QKbdState *kbd;
QemuMutex mutex;

QEMUCursor *cursor;
int cursor_msize;
uint8_t *cursor_mask;

struct VncSurface guest; /* guest visible surface (aka ds->surface) */
pixman_image_t *server; /* vnc server surface */

const char *id;
QTAILQ_ENTRY(VncDisplay) next;
bool is_unix;
char *password;
time_t expires;
int auth;
int subauth; /* Used by VeNCrypt */
int ws_auth; /* Used by websockets */
int ws_subauth; /* Used by websockets */
bool lossy;
bool non_adaptive;
QCryptoTLSCreds *tlscreds;
QAuthZ *tlsauthz;
char *tlsauthzid;
#ifdef CONFIG_VNC_SASL
VncDisplaySASL sasl;
#endif
};

VncDisplay:代表一个VNC Server的结构体,在qemu解析参数并初始化vnc的时候会调用到vnc_display_init,在这里会分配一个VncDisplay实例,并加入到全局链表vnc_displays(如果配置了多个vnc,则全局链表中有多个VncDisplay实例,即代表qemu有多个VNC Server),可以看到VncDisplay结构体中的大部分参数和我们之前讲的qemu命令行中vnc参数是一致的,这里不再对VncDisplay结构体的参数详细阐述。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
struct VncState
{
uint64_t magic; /* 0x05b3f069b3d204bb */
QIOChannelSocket *sioc; /* The underlying socket,socket wrapper */
QIOChannel *ioc; /* The channel currently used for I/O */
guint ioc_tag;
gboolean disconnecting;

DECLARE_BITMAP(dirty[VNC_MAX_HEIGHT], VNC_DIRTY_BITS);
uint8_t **lossy_rect; /* Not an Array to avoid costly memcpy in
* vnc-jobs-async.c */

VncDisplay *vd; /*vnc server connect to*/
VncStateUpdate update; /* Most recent pending request from client */
VncStateUpdate job_update; /* Currently processed by job thread */
int has_dirty;
uint32_t features;
int absolute;
int last_x;
int last_y;
uint32_t last_bmask;
size_t client_width; /* limited to u16 by RFB proto */
size_t client_height; /* limited to u16 by RFB proto */
VncShareMode share_mode;

uint32_t vnc_encoding;

int major;
int minor;

int auth;
int subauth; /* Used by VeNCrypt */
char challenge[VNC_AUTH_CHALLENGE_SIZE];
QCryptoTLSSession *tls; /* Borrowed pointer from channel, don't free */
#ifdef CONFIG_VNC_SASL
VncStateSASL sasl;
#endif
bool encode_ws;
bool websocket;

#ifdef CONFIG_VNC
VncClientInfo *info;
#endif

/* Job thread bottom half has put data for a forced update
* into the output buffer. This offset points to the end of
* the update data in the output buffer. This lets us determine
* when a force update is fully sent to the client, allowing
* us to process further forced updates. */
size_t force_update_offset;
/* We allow multiple incremental updates or audio capture
* samples to be queued in output buffer, provided the
* buffer size doesn't exceed this threshold. The value
* is calculating dynamically based on framebuffer size
* and audio sample settings in vnc_update_throttle_offset() */
size_t throttle_output_offset;
Buffer output;
Buffer input;
/* current output mode information */
VncWritePixels *write_pixels;
PixelFormat client_pf;
pixman_format_code_t client_format;
bool client_be;

CaptureVoiceOut *audio_cap;
struct audsettings as;

VncReadEvent *read_handler;
size_t read_handler_expect;

bool abort;
QemuMutex output_mutex;
QEMUBH *bh;
Buffer jobs_buffer;

/* Encoding specific, if you add something here, don't forget to
* update vnc_async_encoding_start()
*/
VncTight tight;
VncZlib zlib;
VncHextile hextile;
VncZrle zrle;
VncZywrle zywrle;

Notifier mouse_mode_notifier;

QTAILQ_ENTRY(VncState) next;
};

VncState:表示VNC Client的状态信息,在VncDisplay有个client链表(QTAILQ_HEAD(, VncState) clients;),即每个VNC Server(VncDisplay)可能会有多个client连接(默认最高32个client),每个client都会有一些状态信息(包括client channel, dirty bitmap,宽高,共享模式,auth方式,连接方式(ws or tcp ),音频采集以及数据压缩方式等等),VncState就是用来报存这些状态信息的.

VncState中包含的几种VNC编码方式

  • VncTight:即Tight Encoding,包含四种压缩类型:fill, jpeg, png, basic。Tight Encoding是一种自适应的轻量级编码方式,会根据传输数据动态决定使用哪种压缩方式。
  • VncZlib:zlib压缩,即vnc数据传输使用标准的zlib压缩。
  • VncHextile:Hextile编码是RRE编码(RRE是将象素颜色相同的某一个矩形区域作为一个整体传输)的变种,把屏幕分成16x16象素的小块,每块用Raw或RRE方式转送,详细可以参考RFB协议
  • VncZrle:zrle编码就是zlib的游标编码(run-length encoding),详细可以参考RFB协议
  • VncZywrle:zywrle表示ZLib YUV Wavelet Run Length Encoding,zywrle编码是为了提高VNC codec for Motion picture,即可以以更高质量的压缩数据,具体可参考zywrle说明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
struct VncRect
{
int x;
int y;
int w;
int h;
};
struct VncRectEntry
{
struct VncRect rect;
QLIST_ENTRY(VncRectEntry) next;
};
struct VncJob
{
VncState *vs;

QLIST_HEAD(, VncRectEntry) rectangles;
QTAILQ_ENTRY(VncJob) next;
};
struct VncJobQueue {
QemuCond cond;
QemuMutex mutex;
QemuThread thread;
bool exit;
QTAILQ_HEAD(, VncJob) jobs;
};

VncRect:表示VNC 变化的区域信息,包含了变化区域坐上面坐标(x,y),以及变化区域的宽w,高h

VncRectEntry:将所有VNC 变化区域信息组成链表放在VncRectEntry结构中

VncJob:表示一个更新的job,每个job包含当时所有的变化区域信息

VncJobQueue:表示job队列,以及处理job的线程信息

VNC初始化流程分析

在vl.c中的main函数会解析vnc参数:

1
2
3
case QEMU_OPTION_vnc:
vnc_parse(optarg, &error_fatal);
break;

之后在解析完参数后会调用vnc_init_func初始化vnc:

1
2
3
4
5
    /* init remote displays */
#ifdef CONFIG_VNC
qemu_opts_foreach(qemu_find_opts("vnc"),
vnc_init_func, NULL, &error_fatal);
#endif

vnc的实现主要在ui/vnc.c文件中(vnc的各种数据压缩方式的实现在其他文件)。 初始化入口是vnc_init_func函数,vnc_init_func函数中会调用vnc_display_init初始化VncDisplay,之后调用vnc_display_open监听客户端连接,vnc_init_func实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int vnc_init_func(void *opaque, QemuOpts *opts, Error **errp)
{
Error *local_err = NULL;
char *id = (char *)qemu_opts_id(opts);

assert(id);
vnc_display_init(id, &local_err);
if (local_err) {
error_propagate(errp, local_err);
return -1;
}
vnc_display_open(id, &local_err);
if (local_err != NULL) {
error_propagate(errp, local_err);
return -1;
}
return 0;
}

函数vnc_display_init主要是初始化对应的VncDisplay以及启动worker线程处理vnc数据更新队列,以及注册对应的dcl_ops(监听显示设备变化的操作集):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
void vnc_display_init(const char *id, Error **errp)
{
VncDisplay *vd;

if (vnc_display_find(id) != NULL) {
return;
}
vd = g_malloc0(sizeof(*vd));

vd->id = strdup(id);
QTAILQ_INSERT_TAIL(&vnc_displays, vd, next);

QTAILQ_INIT(&vd->clients);
vd->expires = TIME_MAX;
/*默认是英文键盘*/
if (keyboard_layout) {
trace_vnc_key_map_init(keyboard_layout);
vd->kbd_layout = init_keyboard_layout(name2keysym,
keyboard_layout, errp);
} else {
vd->kbd_layout = init_keyboard_layout(name2keysym, "en-us", errp);
}

if (!vd->kbd_layout) {
return;
}

vd->share_policy = VNC_SHARE_POLICY_ALLOW_EXCLUSIVE;/*默认独享*/
vd->connections_limit = 32; /*默认连接数为32*/

qemu_mutex_init(&vd->mutex);
/*启动工作线程处理framebuffer更新*/
vnc_start_worker_thread();

vd->dcl.ops = &dcl_ops;
register_displaychangelistener(&vd->dcl);
vd->kbd = qkbd_state_init(vd->dcl.con);
}

DisplayChangeListenerOps表示当显示设备发生改变时所触发的操作,dcl_ops定义了vnc对显示设备变化时的相关操作:

1
2
3
4
5
6
7
8
9
static const DisplayChangeListenerOps dcl_ops = {
.dpy_name = "vnc",
.dpy_refresh = vnc_refresh,
.dpy_gfx_update = vnc_dpy_update,
.dpy_gfx_switch = vnc_dpy_switch,
.dpy_gfx_check_format = qemu_pixman_check_format,
.dpy_mouse_set = vnc_mouse_set,
.dpy_cursor_define = vnc_dpy_cursor_define,
};

register_displaychangelistener负责设置刷新定时器(调用dpy_refresh),更新console等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
void register_displaychangelistener(DisplayChangeListener *dcl)
{
static const char nodev[] =
"This VM has no graphic display device.";
static DisplaySurface *dummy;
QemuConsole *con;

assert(!dcl->ds);

if (dcl->ops->dpy_gl_ctx_create) {
/* display has opengl support */
assert(dcl->con);
if (dcl->con->gl) {
fprintf(stderr, "can't register two opengl displays (%s, %s)\n",
dcl->ops->dpy_name, dcl->con->gl->ops->dpy_name);
exit(1);
}
dcl->con->gl = dcl;
}

trace_displaychangelistener_register(dcl, dcl->ops->dpy_name);
dcl->ds = get_alloc_displaystate();
/*将注册的dcl插入到listeners链表*/
QLIST_INSERT_HEAD(&dcl->ds->listeners, dcl, next);
/*设置refresh定时器,定时器调用的是dpy_refresh指针(指向的vnc_refresh)*/
gui_setup_refresh(dcl->ds);
if (dcl->con) {
dcl->con->dcls++;
con = dcl->con;
} else {
con = active_console;
}
if (dcl->ops->dpy_gfx_switch) {
if (con) {
dcl->ops->dpy_gfx_switch(dcl, con->surface);
} else {
if (!dummy) {
dummy = qemu_create_message_surface(640, 480, nodev);
}
dcl->ops->dpy_gfx_switch(dcl, dummy);
}
}
text_console_update_cursor(NULL);
}

vnc_display_open中主要对qemu命令行传入的参数校验,之后会根据入参设置vnc监听地址、vnc密码、连接方式,共享方式、认证模式、连接数、压缩方式等,最后开始监听socket等待客户端连接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
void vnc_display_open(const char *id, Error **errp)
{
VncDisplay *vd = vnc_display_find(id);
QemuOpts *opts = qemu_opts_find(&qemu_vnc_opts, id);
SocketAddress **saddr = NULL, **wsaddr = NULL;
size_t nsaddr, nwsaddr;
const char *share, *device_id;
QemuConsole *con;
bool password = false;
bool reverse = false;
const char *credid;
bool sasl = false;
int acl = 0;
const char *tlsauthz;
const char *saslauthz;
int lock_key_sync = 1;
int key_delay_ms;

if (!vd) {
error_setg(errp, "VNC display not active");
return;
}
vnc_display_close(vd);

if (!opts) {
return;
}

reverse = qemu_opt_get_bool(opts, "reverse", false);
if (vnc_display_get_addresses(opts, reverse, &saddr, &nsaddr,
&wsaddr, &nwsaddr, errp) < 0) {
goto fail;
}
/*设置vnc密码*/
password = qemu_opt_get_bool(opts, "password", false);
if (password) {
if (fips_get_state()) {
error_setg(errp,
"VNC password auth disabled due to FIPS mode, "
"consider using the VeNCrypt or SASL authentication "
"methods as an alternative");
goto fail;
}
if (!qcrypto_cipher_supports(
QCRYPTO_CIPHER_ALG_DES_RFB, QCRYPTO_CIPHER_MODE_ECB)) {
error_setg(errp,
"Cipher backend does not support DES RFB algorithm");
goto fail;
}
}
/*numlock和capslock状态同步*/
lock_key_sync = qemu_opt_get_bool(opts, "lock-key-sync", true);
key_delay_ms = qemu_opt_get_number(opts, "key-delay-ms", 10);
sasl = qemu_opt_get_bool(opts, "sasl", false);
#ifndef CONFIG_VNC_SASL
if (sasl) {
error_setg(errp, "VNC SASL auth requires cyrus-sasl support");
goto fail;
}
#endif /* CONFIG_VNC_SASL */
credid = qemu_opt_get(opts, "tls-creds");
if (credid) {
Object *creds;
creds = object_resolve_path_component(
object_get_objects_root(), credid);
if (!creds) {
error_setg(errp, "No TLS credentials with id '%s'",
credid);
goto fail;
}
vd->tlscreds = (QCryptoTLSCreds *)
object_dynamic_cast(creds,
TYPE_QCRYPTO_TLS_CREDS);
if (!vd->tlscreds) {
error_setg(errp, "Object with id '%s' is not TLS credentials",
credid);
goto fail;
}
object_ref(OBJECT(vd->tlscreds));

if (vd->tlscreds->endpoint != QCRYPTO_TLS_CREDS_ENDPOINT_SERVER) {
error_setg(errp,
"Expecting TLS credentials with a server endpoint");
goto fail;
}
}
if (qemu_opt_get(opts, "acl")) {
error_report("The 'acl' option to -vnc is deprecated. "
"Please use the 'tls-authz' and 'sasl-authz' "
"options instead");
}
acl = qemu_opt_get_bool(opts, "acl", false);
tlsauthz = qemu_opt_get(opts, "tls-authz");
if (acl && tlsauthz) {
error_setg(errp, "'acl' option is mutually exclusive with the "
"'tls-authz' option");
goto fail;
}
if (tlsauthz && !vd->tlscreds) {
error_setg(errp, "'tls-authz' provided but TLS is not enabled");
goto fail;
}

saslauthz = qemu_opt_get(opts, "sasl-authz");
if (acl && saslauthz) {
error_setg(errp, "'acl' option is mutually exclusive with the "
"'sasl-authz' option");
goto fail;
}
if (saslauthz && !sasl) {
error_setg(errp, "'sasl-authz' provided but SASL auth is not enabled");
goto fail;
}
/*设置vnc共享策略*/
share = qemu_opt_get(opts, "share");
if (share) {
if (strcmp(share, "ignore") == 0) {
vd->share_policy = VNC_SHARE_POLICY_IGNORE;
} else if (strcmp(share, "allow-exclusive") == 0) {
vd->share_policy = VNC_SHARE_POLICY_ALLOW_EXCLUSIVE;
} else if (strcmp(share, "force-shared") == 0) {
vd->share_policy = VNC_SHARE_POLICY_FORCE_SHARED;
} else {
error_setg(errp, "unknown vnc share= option");
goto fail;
}
} else {
vd->share_policy = VNC_SHARE_POLICY_ALLOW_EXCLUSIVE;
}
vd->connections_limit = qemu_opt_get_number(opts, "connections", 32);

#ifdef CONFIG_VNC_JPEG
vd->lossy = qemu_opt_get_bool(opts, "lossy", false);
#endif
vd->non_adaptive = qemu_opt_get_bool(opts, "non-adaptive", false);
/* adaptive updates are only used with tight encoding and
* if lossy updates are enabled so we can disable all the
* calculations otherwise */
if (!vd->lossy) {
vd->non_adaptive = true;
}

if (tlsauthz) {
vd->tlsauthzid = g_strdup(tlsauthz);
} else if (acl) {
if (strcmp(vd->id, "default") == 0) {
vd->tlsauthzid = g_strdup("vnc.x509dname");
} else {
vd->tlsauthzid = g_strdup_printf("vnc.%s.x509dname", vd->id);
}
vd->tlsauthz = QAUTHZ(qauthz_list_new(vd->tlsauthzid,
QAUTHZ_LIST_POLICY_DENY,
&error_abort));
}
#ifdef CONFIG_VNC_SASL
if (sasl) {
if (saslauthz) {
vd->sasl.authzid = g_strdup(saslauthz);
} else if (acl) {
if (strcmp(vd->id, "default") == 0) {
vd->sasl.authzid = g_strdup("vnc.username");
} else {
vd->sasl.authzid = g_strdup_printf("vnc.%s.username", vd->id);
}
vd->sasl.authz = QAUTHZ(qauthz_list_new(vd->sasl.authzid,
QAUTHZ_LIST_POLICY_DENY,
&error_abort));
}
}
#endif
/*设置vnc认证方式*/
if (vnc_display_setup_auth(&vd->auth, &vd->subauth,
vd->tlscreds, password,
sasl, false, errp) < 0) {
goto fail;
}
trace_vnc_auth_init(vd, 0, vd->auth, vd->subauth);

if (vnc_display_setup_auth(&vd->ws_auth, &vd->ws_subauth,
vd->tlscreds, password,
sasl, true, errp) < 0) {
goto fail;
}
trace_vnc_auth_init(vd, 1, vd->ws_auth, vd->ws_subauth);

#ifdef CONFIG_VNC_SASL
if (sasl) { /*sasl认证初始化*/
int saslErr = sasl_server_init(NULL, "qemu");

if (saslErr != SASL_OK) {
error_setg(errp, "Failed to initialize SASL auth: %s",
sasl_errstring(saslErr, NULL, NULL));
goto fail;
}
}
#endif
/*支持numlock和capslock状态同步*/
vd->lock_key_sync = lock_key_sync;
if (lock_key_sync) {
vd->led = qemu_add_led_event_handler(kbd_leds, vd);
}
vd->ledstate = 0;
/*显示设备信息*/
device_id = qemu_opt_get(opts, "display");
if (device_id) {
int head = qemu_opt_get_number(opts, "head", 0);
Error *err = NULL;

con = qemu_console_lookup_by_device_name(device_id, head, &err);
if (err) {
error_propagate(errp, err);
goto fail;
}
} else {
con = NULL;
}
/*如果不是默认的控制台则重新注册dcl_ops*/
if (con != vd->dcl.con) {
qkbd_state_free(vd->kbd);
unregister_displaychangelistener(&vd->dcl);
vd->dcl.con = con;
register_displaychangelistener(&vd->dcl);
vd->kbd = qkbd_state_init(vd->dcl.con);
}
qkbd_state_set_delay(vd->kbd, key_delay_ms);/*键盘延迟时间*/

if (saddr == NULL) {
goto cleanup;
}

if (reverse) {
if (vnc_display_connect(vd, saddr, nsaddr, wsaddr, nwsaddr, errp) < 0) {
goto fail;
}
} else { /*开始监听*/
if (vnc_display_listen(vd, saddr, nsaddr, wsaddr, nwsaddr, errp) < 0) {
goto fail;
}
}

if (qemu_opt_get(opts, "to")) {
vnc_display_print_local_addr(vd);
}

cleanup:
vnc_free_addresses(&saddr, &nsaddr);
vnc_free_addresses(&wsaddr, &nwsaddr);
return;

fail:
vnc_display_close(vd);
goto cleanup;
}

vnc_display_setup_auth函数负责设置vnc客户端连接的认证方式,vnc的认证有方式有:password,sasl,none三种,vnc的Channel可以有clear和tls两种模式,使用tls模式的时候可以使用anon和x509两种类型的证书,所以组合起来有9中方式:

  • clear + none
  • clear + password
  • clear + sasl
  • tls + anon + none
  • tls + anon + password
  • tls + anon + sasl
  • tls + x509 + none
  • tls + x509 + password
  • tls + x509 + sasl

vnc监听是在vnc_display_listen中完成的,同时注册了连接过来时的处理函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
static int vnc_display_listen(VncDisplay *vd,
SocketAddress **saddr,
size_t nsaddr,
SocketAddress **wsaddr,
size_t nwsaddr,
Error **errp)
{
size_t i;
/*tcp监听*/
if (nsaddr) {
vd->listener = qio_net_listener_new();
qio_net_listener_set_name(vd->listener, "vnc-listen");
for (i = 0; i < nsaddr; i++) {
if (qio_net_listener_open_sync(vd->listener,
saddr[i],
errp) < 0) {
return -1;
}
}

qio_net_listener_set_client_func(vd->listener,
vnc_listen_io, vd, NULL);
}
/*websocket监听*/
if (nwsaddr) {
vd->wslistener = qio_net_listener_new();
qio_net_listener_set_name(vd->wslistener, "vnc-ws-listen");
for (i = 0; i < nwsaddr; i++) {
if (qio_net_listener_open_sync(vd->wslistener,
wsaddr[i],
errp) < 0) {
return -1;
}
}

qio_net_listener_set_client_func(vd->wslistener,
vnc_listen_io, vd, NULL);
}

return 0;
}

其中vnc_listen_io是客户端连接时的回调处理函数,到此vnc初始化完成,等待客户端的连接。

VNC客户端连接流程分析

上面讲到vnc_listen_io是在vnc客户端连接时触发的回调函数,vnc_listen_io的流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void vnc_listen_io(QIONetListener *listener,
QIOChannelSocket *cioc,
void *opaque)
{
VncDisplay *vd = opaque;
/*判断是否为websocket server*/
bool isWebsock = listener == vd->wslistener;
/*设置client channel名称*/
qio_channel_set_name(QIO_CHANNEL(cioc),
isWebsock ? "vnc-ws-server" : "vnc-server");
/*设置TCPNODELAY*/
qio_channel_set_delay(QIO_CHANNEL(cioc), false);
/*真正处理vnc client连接的函数*/
vnc_connect(vd, cioc, false, isWebsock);
}

vnc_connect是真正处理客户端连接的函数:会为当前连接进来的client分配一个VncState,并,之后初始化VncState中的各项参数:各种buffer(包括input/output buffer,job buffer以及各种压缩方式的buffer),设置AUTH方式,为lossy_rect分配内存,调整gui_update的更新间隔为VNC_REFRESH_INTERVAL_BASE等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
static void vnc_connect(VncDisplay *vd, QIOChannelSocket *sioc,
bool skipauth, bool websocket)
{
/*设置VncState对应的VncDisplay*/
VncState *vs = g_new0(VncState, 1);
bool first_client = QTAILQ_EMPTY(&vd->clients);
int i;
/*将VncState实例加入到对应的VncDisplay的client链表中*/
trace_vnc_client_connect(vs, sioc);
vs->magic = VNC_MAGIC;
vs->sioc = sioc;
object_ref(OBJECT(vs->sioc));
vs->ioc = QIO_CHANNEL(sioc);
object_ref(OBJECT(vs->ioc));
vs->vd = vd;
/*初始化input/output buffer,job buffer以及各种压缩方式的buffer)*/
buffer_init(&vs->input, "vnc-input/%p", sioc);
buffer_init(&vs->output, "vnc-output/%p", sioc);
buffer_init(&vs->jobs_buffer, "vnc-jobs_buffer/%p", sioc);

buffer_init(&vs->tight.tight, "vnc-tight/%p", sioc);
buffer_init(&vs->tight.zlib, "vnc-tight-zlib/%p", sioc);
buffer_init(&vs->tight.gradient, "vnc-tight-gradient/%p", sioc);
#ifdef CONFIG_VNC_JPEG
buffer_init(&vs->tight.jpeg, "vnc-tight-jpeg/%p", sioc);
#endif
#ifdef CONFIG_VNC_PNG
buffer_init(&vs->tight.png, "vnc-tight-png/%p", sioc);
#endif
buffer_init(&vs->zlib.zlib, "vnc-zlib/%p", sioc);
buffer_init(&vs->zrle.zrle, "vnc-zrle/%p", sioc);
buffer_init(&vs->zrle.fb, "vnc-zrle-fb/%p", sioc);
buffer_init(&vs->zrle.zlib, "vnc-zrle-zlib/%p", sioc);
/*设置VNC认证方式*/
if (skipauth) {
vs->auth = VNC_AUTH_NONE;
vs->subauth = VNC_AUTH_INVALID;
} else {
if (websocket) {
vs->auth = vd->ws_auth;
vs->subauth = VNC_AUTH_INVALID;
} else {
vs->auth = vd->auth;
vs->subauth = vd->subauth;
}
}
VNC_DEBUG("Client sioc=%p ws=%d auth=%d subauth=%d\n",
sioc, websocket, vs->auth, vs->subauth);
/*为lossy_rect分配内存*/
vs->lossy_rect = g_malloc0(VNC_STAT_ROWS * sizeof (*vs->lossy_rect));
for (i = 0; i < VNC_STAT_ROWS; ++i) {
vs->lossy_rect[i] = g_new0(uint8_t, VNC_STAT_COLS);
}
/*设置gui_update的更新间隔时间*/
VNC_DEBUG("New client on socket %p\n", vs->sioc);
update_displaychangelistener(&vd->dcl, VNC_REFRESH_INTERVAL_BASE);
/*设置socket为nonblocking*/
qio_channel_set_blocking(vs->ioc, false, NULL);
if (vs->ioc_tag) {
g_source_remove(vs->ioc_tag);
}
if (websocket) {
/*设置websocket握手以及数据收发的回调处理函数*/
vs->websocket = 1;
if (vd->tlscreds) {
vs->ioc_tag = qio_channel_add_watch(
vs->ioc, G_IO_IN, vncws_tls_handshake_io, vs, NULL);
} else {
vs->ioc_tag = qio_channel_add_watch(
vs->ioc, G_IO_IN, vncws_handshake_io, vs, NULL);
}
} else {
/*设置数据收发的回调处理函数*/
vs->ioc_tag = qio_channel_add_watch(
vs->ioc, G_IO_IN, vnc_client_io, vs, NULL);
}
/*这里是将client地址缓存起来*/
vnc_client_cache_addr(vs);
vnc_qmp_event(vs, QAPI_EVENT_VNC_CONNECTED);
/*更新VNC的连接数*/
vnc_set_share_mode(vs, VNC_SHARE_MODE_CONNECTING);

vs->last_x = -1;
vs->last_y = -1;
/*设置音频的采样率为44100Hz,通道为双声道,16位*/
vs->as.freq = 44100;
vs->as.nchannels = 2;
vs->as.fmt = AUDIO_FORMAT_S16;
vs->as.endianness = 0;

qemu_mutex_init(&vs->output_mutex);
/*注册一个bh,回调处理函数位vnc_jobs_bh,这里用到了qemu的aio*/
vs->bh = qemu_bh_new(vnc_jobs_bh, vs);
/*将VncState实例加入到对应的VncDisplay的client链表中*/
QTAILQ_INSERT_TAIL(&vd->clients, vs, next);
if (first_client) {
/*初始化显存内存缓存空间*/
vnc_update_server_surface(vd);
}
/*触发显卡设备刷新,在显示设备的gfx_update中都会调用dpx_gfx_update,即将更新区域通知给vnc*/
graphic_hw_update(vd->dcl.con);

if (!vs->websocket) {
/*这里开始vnc的RFB协议交互*/
vnc_start_protocol(vs);
}
/*如果连接超出,将disconnect这个client*/
if (vd->num_connecting > vd->connections_limit) {
QTAILQ_FOREACH(vs, &vd->clients, next) {
if (vs->share_mode == VNC_SHARE_MODE_CONNECTING) {
vnc_disconnect_start(vs);
return;
}
}
}
}

vnc_connect中调用qio_channel_add_watch注册的回调处理函数vnc_client_io负责对client进行数据读写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
gboolean vnc_client_io(QIOChannel *ioc G_GNUC_UNUSED,
GIOCondition condition, void *opaque)
{
VncState *vs = opaque;

assert(vs->magic == VNC_MAGIC);
if (condition & G_IO_IN) {
/*将数据读取到VncState中的input buffer,之后调用回调read_handler进行读取数据处理*/
if (vnc_client_read(vs) < 0) {
/* vs is free()ed here */
return TRUE;
}
}
if (condition & G_IO_OUT) {
/*将VncState中的output buffer数据通过QIOChannel的write操作发送给client*/
vnc_client_write(vs);
}

if (vs->disconnecting) {
if (vs->ioc_tag != 0) {
g_source_remove(vs->ioc_tag);
}
vs->ioc_tag = 0;
}
return TRUE;
}

vnc_update_server_surface用于初始化显存内容缓存空间以及设置更新的像素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static void vnc_update_server_surface(VncDisplay *vd)
{
int width, height;

qemu_pixman_image_unref(vd->server);
vd->server = NULL;

if (QTAILQ_EMPTY(&vd->clients)) {
return;
}

width = vnc_width(vd);
height = vnc_height(vd);
/*pixman是一个像素处理库,这里是初始化显存内容的缓存空间*/
vd->server = pixman_image_create_bits(VNC_SERVER_FB_FORMAT,
width, height,
NULL, 0);
/*这里设置dirty map为全部(坐标(0,0),使用整个显示区域的宽和高),即第一次连接会全像素更新*/
memset(vd->guest.dirty, 0x00, sizeof(vd->guest.dirty));
vnc_set_area_dirty(vd->guest.dirty, vd, 0, 0,
width, height);
}

VNC协议消息交互流程

vnc客户端和服务端的协议交互是从vnc_start_protocol函数开始,vnc_start_protocol中首先发送服务端RFB版本,然后注册数据读取回调处理函数为protocol_version:

1
2
3
4
5
6
7
8
9
10
11
12
void vnc_start_protocol(VncState *vs)
{
/*将RFB版本写进缓存,vnc_write并不直接调用qio_channel的write操作,而是先写进VncState中的output buffer*/
vnc_write(vs, "RFB 003.008\n", 12);
/*vnc_flush才是最终调用QIO_channel的write操作的函数*/
vnc_flush(vs);
/*这里会将protocol_version赋值给read_handler,在之前的vnc_client_io中我们知道vnc_client_read会调用read_handler回调函数执行读取操作,这里就是调用protocol_version分析客户端发送过来的版本信息*/
vnc_read_when(vs, protocol_version, 12);

vs->mouse_mode_notifier.notify = check_pointer_type_change;
qemu_add_mouse_mode_change_notifier(&vs->mouse_mode_notifier);
}

protocol_version这个函数负责接收客户端发过来的版本信息,并调用客户端的Auth处理函数对客户端连接进行认证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
static int protocol_version(VncState *vs, uint8_t *version, size_t len)
{
char local[13];
/*头12个字节为version*/
memcpy(local, version, 12);
local[12] = 0;

if (sscanf(local, "RFB %03d.%03d\n", &vs->major, &vs->minor) != 2) {
VNC_DEBUG("Malformed protocol version %s\n", local);
vnc_client_error(vs);
return 0;
}
VNC_DEBUG("Client request protocol version %d.%d\n", vs->major, vs->minor);
/*客户端主版本必须为3,并且子版本必须为3,4,5,7,8*/
if (vs->major != 3 ||
(vs->minor != 3 &&
vs->minor != 4 &&
vs->minor != 5 &&
vs->minor != 7 &&
vs->minor != 8)) {
VNC_DEBUG("Unsupported client version\n");
vnc_write_u32(vs, VNC_AUTH_INVALID);
vnc_flush(vs);
vnc_client_error(vs);
return 0;
}
/* Some broken clients report v3.4 or v3.5, which spec requires to be treated
* as equivalent to v3.3 by servers
*/
if (vs->minor == 4 || vs->minor == 5)
vs->minor = 3;
/*如果子版本等于3,直接进行认证*/
if (vs->minor == 3) {
trace_vnc_auth_start(vs, vs->auth);
if (vs->auth == VNC_AUTH_NONE) {
vnc_write_u32(vs, vs->auth);
vnc_flush(vs);
trace_vnc_auth_pass(vs, vs->auth);
start_client_init(vs);
} else if (vs->auth == VNC_AUTH_VNC) {
VNC_DEBUG("Tell client VNC auth\n");
vnc_write_u32(vs, vs->auth);
vnc_flush(vs);
start_auth_vnc(vs);
} else {
trace_vnc_auth_fail(vs, vs->auth,
"Unsupported auth method for v3.3", "");
vnc_write_u32(vs, VNC_AUTH_INVALID);
vnc_flush(vs);
vnc_client_error(vs);
}
} else {
/*如果子版本不等于3则调用protocol_client_auth进行认证*/
vnc_write_u8(vs, 1); /* num auth */
vnc_write_u8(vs, vs->auth);
vnc_read_when(vs, protocol_client_auth, 1);
vnc_flush(vs);
}

return 0;
}

protocol_client_auth用于处理其他版本的认证,之后会根据认证方式选择认证函数:none直接调用start_client_init,vnc认证调用start_auth_vnc,VENCRYPT认证调用start_auth_vencrypt,sasl认证调用start_auth_sasl,除了none认证外,其他认证方式认证完都会调用start_client_init。这里不对认证做具体分析,直接从start_client_init函数开始,start_client_init会调用protocol_client_init初始化client的参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
void start_client_init(VncState *vs)
{
vnc_read_when(vs, protocol_client_init, 1);
}

static int protocol_client_init(VncState *vs, uint8_t *data, size_t len)
{
char buf[1024];
VncShareMode mode;
int size;

mode = data[0] ? VNC_SHARE_MODE_SHARED : VNC_SHARE_MODE_EXCLUSIVE;
switch (vs->vd->share_policy) {
case VNC_SHARE_POLICY_IGNORE:
/*
* Ignore the shared flag. Nothing to do here.
*
* Doesn't conform to the rfb spec but is traditional qemu
* behavior, thus left here as option for compatibility
* reasons.
*/
break;
case VNC_SHARE_POLICY_ALLOW_EXCLUSIVE:
/*
* Policy: Allow clients ask for exclusive access.
*
* Implementation: When a client asks for exclusive access,
* disconnect all others. Shared connects are allowed as long
* as no exclusive connection exists.
*
* This is how the rfb spec suggests to handle the shared flag.
*/
if (mode == VNC_SHARE_MODE_EXCLUSIVE) {
VncState *client;
QTAILQ_FOREACH(client, &vs->vd->clients, next) {
if (vs == client) {
continue;
}
if (client->share_mode != VNC_SHARE_MODE_EXCLUSIVE &&
client->share_mode != VNC_SHARE_MODE_SHARED) {
continue;
}
vnc_disconnect_start(client);
}
}
if (mode == VNC_SHARE_MODE_SHARED) {
if (vs->vd->num_exclusive > 0) {
vnc_disconnect_start(vs);
return 0;
}
}
break;
case VNC_SHARE_POLICY_FORCE_SHARED:
/*
* Policy: Shared connects only.
* Implementation: Disallow clients asking for exclusive access.
*
* Useful for shared desktop sessions where you don't want
* someone forgetting to say -shared when running the vnc
* client disconnect everybody else.
*/
if (mode == VNC_SHARE_MODE_EXCLUSIVE) {
vnc_disconnect_start(vs);
return 0;
}
break;
}
/*初始化client的共享模式*/
vnc_set_share_mode(vs, mode);

/*判断连接数是否超限*/
if (vs->vd->num_shared > vs->vd->connections_limit) {
vnc_disconnect_start(vs);
return 0;
}

assert(pixman_image_get_width(vs->vd->server) < 65536 &&
pixman_image_get_width(vs->vd->server) >= 0);
assert(pixman_image_get_height(vs->vd->server) < 65536 &&
pixman_image_get_height(vs->vd->server) >= 0);
vs->client_width = pixman_image_get_width(vs->vd->server);
vs->client_height = pixman_image_get_height(vs->vd->server);
/*将显示的宽高发送给client*/
vnc_write_u16(vs, vs->client_width);
vnc_write_u16(vs, vs->client_height);
/*初始化每像素的颜色数据的格式,以及设置write_pixels回调处理函数*/
pixel_format_message(vs);

if (qemu_name) {
size = snprintf(buf, sizeof(buf), "QEMU (%s)", qemu_name);
if (size > sizeof(buf)) {
size = sizeof(buf);
}
} else {
size = snprintf(buf, sizeof(buf), "QEMU");
}
/*发送qemu_name*/
vnc_write_u32(vs, size);
vnc_write(vs, buf, size);
vnc_flush(vs);
/*记录认证信息*/
vnc_client_cache_auth(vs);
vnc_qmp_event(vs, QAPI_EVENT_VNC_INITIALIZED);
/*开始处理vnc客户端发来的消息*/
vnc_read_when(vs, protocol_client_msg, 1);

return 0;
}

protocol_client_msg是接收处理客户端发送消息的函数,主要处理的消息类型有以下几种:

  • VNC_MSG_CLIENT_SET_PIXEL_FORMAT:用于设置像素格式
  • VNC_MSG_CLIENT_SET_ENCODINGS:设置编码方式
  • VNC_MSG_CLIENT_FRAMEBUFFER_UPDATE_REQUEST:请求framebuffer更新
  • VNC_MSG_CLIENT_KEY_EVENT:键盘事件
  • VNC_MSG_CLIENT_POINTER_EVENT:鼠标事件
  • VNC_MSG_CLIENT_CUT_TEXT:获取client的剪贴文本
  • VNC_MSG_CLIENT_QEMU:这个是QEMU特有的消息类型,当收到这个消息的时候会对payload再次解析,获取子类型,根据子类型的值进行处理,子类型包括如下值:
    • VNC_MSG_CLIENT_QEMU_EXT_KEY_EVENT:处理扩展键
    • VNC_MSG_CLIENT_QEMU_AUDIO:音频设置选项,包括三种子选项:
      • VNC_MSG_CLIENT_QEMU_AUDIO_ENABLE:启用音频
      • VNC_MSG_CLIENT_QEMU_AUDIO_DISABLE:禁用音频
      • VNC_MSG_CLIENT_QEMU_AUDIO_SET_FORMAT:设置音频格式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
static int protocol_client_msg(VncState *vs, uint8_t *data, size_t len)
{
int i;
uint16_t limit;
uint32_t freq;
VncDisplay *vd = vs->vd;
/*更新刷新间隔*/
if (data[0] > 3) {
update_displaychangelistener(&vd->dcl, VNC_REFRESH_INTERVAL_BASE);
}

switch (data[0]) {
case VNC_MSG_CLIENT_SET_PIXEL_FORMAT: /*设置像素格式*/
if (len == 1)
return 20;

set_pixel_format(vs, read_u8(data, 4),
read_u8(data, 6), read_u8(data, 7),
read_u16(data, 8), read_u16(data, 10),
read_u16(data, 12), read_u8(data, 14),
read_u8(data, 15), read_u8(data, 16));
break;
case VNC_MSG_CLIENT_SET_ENCODINGS: /*设置编码方式*/
if (len == 1)
return 4;

if (len == 4) {
limit = read_u16(data, 2);
if (limit > 0)
return 4 + (limit * 4);
} else
limit = read_u16(data, 2);

for (i = 0; i < limit; i++) {
int32_t val = read_s32(data, 4 + (i * 4));
memcpy(data + 4 + (i * 4), &val, sizeof(val));
}

set_encodings(vs, (int32_t *)(data + 4), limit);
break;
case VNC_MSG_CLIENT_FRAMEBUFFER_UPDATE_REQUEST:
if (len == 1)
return 10;

framebuffer_update_request(vs,
read_u8(data, 1), read_u16(data, 2), read_u16(data, 4),
read_u16(data, 6), read_u16(data, 8));
break;
case VNC_MSG_CLIENT_KEY_EVENT: /*键盘事件*/
if (len == 1)
return 8;

key_event(vs, read_u8(data, 1), read_u32(data, 4));
break;
case VNC_MSG_CLIENT_POINTER_EVENT: /*鼠标事件*/
if (len == 1)
return 6;

pointer_event(vs, read_u8(data, 1), read_u16(data, 2), read_u16(data, 4));
break;
case VNC_MSG_CLIENT_CUT_TEXT: /*文本剪贴*/
if (len == 1) {
return 8;
}
if (len == 8) {
uint32_t dlen = read_u32(data, 4);
if (dlen > (1 << 20)) {
error_report("vnc: client_cut_text msg payload has %u bytes"
" which exceeds our limit of 1MB.", dlen);
vnc_client_error(vs);
break;
}
if (dlen > 0) {
return 8 + dlen;
}
}

client_cut_text(vs, read_u32(data, 4), data + 8);
break;
case VNC_MSG_CLIENT_QEMU: /*qemu相关的消息*/
if (len == 1)
return 2;

switch (read_u8(data, 1)) {
case VNC_MSG_CLIENT_QEMU_EXT_KEY_EVENT:
if (len == 2)
return 12;

ext_key_event(vs, read_u16(data, 2),
read_u32(data, 4), read_u32(data, 8));
break;
case VNC_MSG_CLIENT_QEMU_AUDIO: /*是否支持qemu音频*/
if (len == 2)
return 4;

switch (read_u16 (data, 2)) {
case VNC_MSG_CLIENT_QEMU_AUDIO_ENABLE: /*启用音频*/
audio_add(vs);
break;
case VNC_MSG_CLIENT_QEMU_AUDIO_DISABLE: /*禁用音频*/
audio_del(vs);
break;
case VNC_MSG_CLIENT_QEMU_AUDIO_SET_FORMAT: /*设置音频格式*/
if (len == 4)
return 10;
switch (read_u8(data, 4)) {
case 0: vs->as.fmt = AUDIO_FORMAT_U8; break;
case 1: vs->as.fmt = AUDIO_FORMAT_S8; break;
case 2: vs->as.fmt = AUDIO_FORMAT_U16; break;
case 3: vs->as.fmt = AUDIO_FORMAT_S16; break;
case 4: vs->as.fmt = AUDIO_FORMAT_U32; break;
case 5: vs->as.fmt = AUDIO_FORMAT_S32; break;
default:
VNC_DEBUG("Invalid audio format %d\n", read_u8(data, 4));
vnc_client_error(vs);
break;
}
vs->as.nchannels = read_u8(data, 5);
if (vs->as.nchannels != 1 && vs->as.nchannels != 2) {
VNC_DEBUG("Invalid audio channel count %d\n",
read_u8(data, 5));
vnc_client_error(vs);
break;
}
freq = read_u32(data, 6);
/* No official limit for protocol, but 48khz is a sensible
* upper bound for trustworthy clients, and this limit
* protects calculations involving 'vs->as.freq' later.
*/
if (freq > 48000) {
VNC_DEBUG("Invalid audio frequency %u > 48000", freq);
vnc_client_error(vs);
break;
}
vs->as.freq = freq;
break;
default:
VNC_DEBUG("Invalid audio message %d\n", read_u8(data, 4));
vnc_client_error(vs);
break;
}
break;

default:
VNC_DEBUG("Msg: %d\n", read_u16(data, 0));
vnc_client_error(vs);
break;
}
break;
default:
VNC_DEBUG("Msg: %d\n", data[0]);
vnc_client_error(vs);
break;
}

vnc_update_throttle_offset(vs);
/*继续调用protocol_client_msg处理后续的消息*/
vnc_read_when(vs, protocol_client_msg, 1);
return 0;
}

另外说下键盘设备如何处理这些事件的(鼠标设备也是类似),上面的key_event,最终会调到qemu_input_event_send_key函数,这个函数里会调用qemu_input_event_new_key初始一个新的输入事件,并设置事件类型为INPUT_EVENT_KIND_KEY, 之后调用函数qemu_input_event_send_key发送键盘事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
static InputEvent *qemu_input_event_new_key(KeyValue *key, bool down)
{
InputEvent *evt = g_new0(InputEvent, 1);
evt->u.key.data = g_new0(InputKeyEvent, 1);
evt->type = INPUT_EVENT_KIND_KEY;
evt->u.key.data->key = key;
evt->u.key.data->down = down;
return evt;
}

void qemu_input_event_send_key(QemuConsole *src, KeyValue *key, bool down)
{
InputEvent *evt;
evt = qemu_input_event_new_key(key, down);
if (QTAILQ_EMPTY(&kbd_queue)) {
qemu_input_event_send(src, evt);
qemu_input_event_sync();
qapi_free_InputEvent(evt);
} else if (queue_count < queue_limit) {
qemu_input_queue_event(&kbd_queue, src, evt);
qemu_input_queue_sync(&kbd_queue);
} else {
qapi_free_InputEvent(evt);
}
}

qemu_input_event_send_key函数最终会调到真正执行键盘输入的函数qemu_input_event_send_impl,这个函数里会先找到执行事件的设备handler,然后调用handler的event函数处理InputEvent:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void qemu_input_event_send_impl(QemuConsole *src, InputEvent *evt)
{
QemuInputHandlerState *s;

qemu_input_event_trace(src, evt);

/* pre processing */
if (graphic_rotate && (evt->type == INPUT_EVENT_KIND_ABS)) {
qemu_input_transform_abs_rotate(evt);
}

/* 找到类型为INPUT_EVENT_KIND_KEY的handler, send event */
s = qemu_input_find_handler(1 << evt->type, src);
if (!s) {
return;
}
/*调用handler的event函数执行输入事件*/
s->handler->event(s->dev, src, evt);
s->events++;
}

handler可以为ps2设备,virtio-tablet,usb-tablet等,这里不再对设备如何处理输入事件详细说明

framebuffer更新流程

dc_ops操作集中的回调函数指针dpy_gfx_update是在供显卡设备绘图完成后调用的,负责更新变化区域,vnc中dpy_gfx_update指向的是vnc_dpy_update函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
static void vnc_dpy_update(DisplayChangeListener *dcl,
int x, int y, int w, int h)
{
/*获取对应的display*/
VncDisplay *vd = container_of(dcl, VncDisplay, dcl);
struct VncSurface *s = &vd->guest;
/*更新vd中的变化区域*/
vnc_set_area_dirty(s->dirty, vd, x, y, w, h);
}

static void vnc_set_area_dirty(DECLARE_BITMAP(dirty[VNC_MAX_HEIGHT],
VNC_MAX_WIDTH / VNC_DIRTY_PIXELS_PER_BIT),
VncDisplay *vd,
int x, int y, int w, int h)
{
/*获取display的宽高*/
int width = vnc_width(vd);
int height = vnc_height(vd);

/*增加宽度,以及左移x值,为了确保能覆盖变化区域*/
/* this is needed this to ensure we updated all affected
* blocks if x % VNC_DIRTY_PIXELS_PER_BIT != 0 */
w += (x % VNC_DIRTY_PIXELS_PER_BIT);
x -= (x % VNC_DIRTY_PIXELS_PER_BIT);

x = MIN(x, width);
y = MIN(y, height);
w = MIN(x + w, width) - x;
h = MIN(y + h, height);
/*根据上面的w,h,x,y设置bitmap(以像素大小按行更新)*/
for (; y < h; y++) {
bitmap_set(dirty[y], x / VNC_DIRTY_PIXELS_PER_BIT,
DIV_ROUND_UP(w, VNC_DIRTY_PIXELS_PER_BIT));
}
}

显卡设备更新是由dc_ops中的函数指针dpy_refresh触发的,dpy_refresh由上面的gui_setup_refresh创建的定时器调用,dpy_refresh负责获取display中的变化区域内容并创建vncjob插入queue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
static void vnc_refresh(DisplayChangeListener *dcl)
{
VncDisplay *vd = container_of(dcl, VncDisplay, dcl);
VncState *vs, *vn;
int has_dirty, rects = 0;

if (QTAILQ_EMPTY(&vd->clients)) {
update_displaychangelistener(&vd->dcl, VNC_REFRESH_INTERVAL_MAX);
return;
}
/*这里触发显卡设备更新,显卡设备更新的最后会调用dpy_gfx_update指针将更新区域设置到bitmap*/
graphic_hw_update(vd->dcl.con);

if (vnc_trylock_display(vd)) {
update_displaychangelistener(&vd->dcl, VNC_REFRESH_INTERVAL_BASE);
return;
}
/*这里会真正获取变化的内容并将变化区域的内存指针赋给vd->server*/
has_dirty = vnc_refresh_server_surface(vd);
vnc_unlock_display(vd);
/*判断是否有client,有的话则为每个vs创建vncjob并插入到对应的队列中*/
QTAILQ_FOREACH_SAFE(vs, &vd->clients, next, vn) {
rects += vnc_update_client(vs, has_dirty);
/* vs might be free()ed here */
}
/*如果图像有变化就缩短刷新间隔,提供更高帧率*/
if (has_dirty && rects) {
vd->dcl.update_interval /= 2;
if (vd->dcl.update_interval < VNC_REFRESH_INTERVAL_BASE) {
vd->dcl.update_interval = VNC_REFRESH_INTERVAL_BASE;
}
} else {
vd->dcl.update_interval += VNC_REFRESH_INTERVAL_INC;
if (vd->dcl.update_interval > VNC_REFRESH_INTERVAL_MAX) {
vd->dcl.update_interval = VNC_REFRESH_INTERVAL_MAX;
}
}
}

vnc_update_client负责创建vncjob,并根据bitmap中的dirty区域计算起始坐标(x,y),之后将变化区域的w,h,x,y设置到VncRectEntry实例中并加入到vncjob中的rectangles链表中,之后将vncjob加入到queue中并通知worker线程消费queue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
static int vnc_update_client(VncState *vs, int has_dirty)
{
VncDisplay *vd = vs->vd;
VncJob *job;
int y;
int height, width;
int n = 0;

if (vs->disconnecting) {
vnc_disconnect_finish(vs);
return 0;
}
/*判断是否需要更新*/
vs->has_dirty += has_dirty;
if (!vnc_should_update(vs)) {
return 0;
}

if (!vs->has_dirty && vs->update != VNC_STATE_UPDATE_FORCE) {
return 0;
}

/*
* Send screen updates to the vnc client using the server
* surface and server dirty map. guest surface updates
* happening in parallel don't disturb us, the next pass will
* send them to the client.
*/
/*创建job*/
job = vnc_job_new(vs);

height = pixman_image_get_height(vd->server);
width = pixman_image_get_width(vd->server);
/*计算x,y值并在vnc_job_add_rect中创建rect然后加入到vncjob的rectangles链表中*/
y = 0;
for (;;) {
int x, h;
unsigned long x2;
unsigned long offset = find_next_bit((unsigned long *) &vs->dirty,
height * VNC_DIRTY_BPL(vs),
y * VNC_DIRTY_BPL(vs));
if (offset == height * VNC_DIRTY_BPL(vs)) {
/* no more dirty bits */
break;
}
y = offset / VNC_DIRTY_BPL(vs);
x = offset % VNC_DIRTY_BPL(vs);
x2 = find_next_zero_bit((unsigned long *) &vs->dirty[y],
VNC_DIRTY_BPL(vs), x);
bitmap_clear(vs->dirty[y], x, x2 - x);
h = find_and_clear_dirty_height(vs, y, x, x2, height);
x2 = MIN(x2, width / VNC_DIRTY_PIXELS_PER_BIT);
if (x2 > x) {
n += vnc_job_add_rect(job, x * VNC_DIRTY_PIXELS_PER_BIT, y,
(x2 - x) * VNC_DIRTY_PIXELS_PER_BIT, h);
}
if (!x && x2 == width / VNC_DIRTY_PIXELS_PER_BIT) {
y += h;
if (y == height) {
break;
}
}
}

vs->job_update = vs->update;
vs->update = VNC_STATE_UPDATE_NONE;
/*将job加入到queue,然后通知worker线程消费*/
vnc_job_push(job);
vs->has_dirty = 0;
return n;
}

这里需要提到VNC的更新方式,我们知道VNC更新有三个选项:
VNC_STATE_UPDATE_NONE代表不更新给client;

VNC_STATE_UPDATE_INCREMENTAL代表低于缓存阈值的时候更新;
VNC_STATE_UPDATE_FORCE表示强制更新。

1
2
3
4
5
typedef enum {
VNC_STATE_UPDATE_NONE,
VNC_STATE_UPDATE_INCREMENTAL,
VNC_STATE_UPDATE_FORCE,
} VncStateUpdate;

VNC更新请求由客户端发起,在之前的protocol_client_msg中可以看到,VNC_MSG_CLIENT_FRAMEBUFFER_UPDATE_REQUEST就是client发过来的请求:

1
2
3
4
5
6
case VNC_MSG_CLIENT_FRAMEBUFFER_UPDATE_REQUEST:
if (len == 1)
return 10;

framebuffer_update_request(vs,read_u8(data, 1), read_u16(data, 2), read_u16(data, 4),read_u16(data, 6), read_u16(data, 8));
break;

framebuffer_update_request根据client发过来的incremental是否为真决定采用哪种更新方式:

1
2
3
4
5
6
7
8
9
10
11
12
static void framebuffer_update_request(VncState *vs, int incremental,
int x, int y, int w, int h)
{
if (incremental) {
if (vs->update != VNC_STATE_UPDATE_FORCE) {
vs->update = VNC_STATE_UPDATE_INCREMENTAL;
}
} else {
vs->update = VNC_STATE_UPDATE_FORCE;
vnc_set_area_dirty(vs->dirty, vs->vd, x, y, w, h);
}
}

framebuffer发送流程

vnc_start_worker_thread会创建线程用来处理framebuffer更新(即消费上面的vncjob并将framebuffer发送给client):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static void *vnc_worker_thread(void *arg)
{
VncJobQueue *queue = arg;

qemu_thread_get_self(&queue->thread);
/*vnc_worker_thread_loop里是真正处理frameupdate的函数*/
while (!vnc_worker_thread_loop(queue)) ;
vnc_queue_clear(queue);
return NULL;
}

void vnc_start_worker_thread(void)
{
VncJobQueue *q;

if (vnc_worker_thread_running())
return ;
/*创建VncJobQueue,所有将要更新的frame都挂到这个queue上*/
q = vnc_queue_init();
/*创建vnc_worker线程,执行函数是vnc_worker_thread*/
qemu_thread_create(&q->thread, "vnc_worker", vnc_worker_thread, q,
QEMU_THREAD_DETACHED);
queue = q; /* Set global queue */
}

vnc_worker_thread_loop里是真正处理frameupdate的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
static int vnc_worker_thread_loop(VncJobQueue *queue)
{
VncJob *job; /*每次从queue上取一个VncJob处理*/
VncRectEntry *entry, *tmp; /*更新区域*/
VncState vs = {}; /*vnc client*/
int n_rectangles;
int saved_offset;

/*等待queue上有job出现*/
vnc_lock_queue(queue);
while (QTAILQ_EMPTY(&queue->jobs) && !queue->exit) {
qemu_cond_wait(&queue->cond, &queue->mutex);
}
/* Here job can only be NULL if queue->exit is true */
job = QTAILQ_FIRST(&queue->jobs);
vnc_unlock_queue(queue);
/*判断魔法数字是否正确*/
assert(job->vs->magic == VNC_MAGIC);

if (queue->exit) {
return -1;
}
/*获取job后,锁住job对应的vs的output锁*/
vnc_lock_output(job->vs);
if (job->vs->ioc == NULL || job->vs->abort == true) {
vnc_unlock_output(job->vs);
goto disconnected;
}
if (buffer_empty(&job->vs->output)) {
/*
* Looks like a NOP as it obviously moves no data. But it
* moves the empty buffer, so we don't have to malloc a new
* one for vs.output
*/
buffer_move_empty(&vs.output, &job->vs->output);
}
vnc_unlock_output(job->vs);
/*开始拷贝output buffers,同时将rectangles发送给client*/
/* Make a local copy of vs and switch output buffers */
vnc_async_encoding_start(job->vs, &vs);
vs.magic = VNC_MAGIC;

/* 开始通过VNC_MSG_SERVER_FRAMEBUFFER_UPDATE消息发送rectangles*/
n_rectangles = 0;
vnc_write_u8(&vs, VNC_MSG_SERVER_FRAMEBUFFER_UPDATE);
vnc_write_u8(&vs, 0);
saved_offset = vs.output.offset;
vnc_write_u16(&vs, 0);

vnc_lock_display(job->vs->vd);
/*将job下的所有rectangles发送给client*/
QLIST_FOREACH_SAFE(entry, &job->rectangles, next, tmp) {
int n;

if (job->vs->ioc == NULL) {
vnc_unlock_display(job->vs->vd);
/* Copy persistent encoding data */
vnc_async_encoding_end(job->vs, &vs);
goto disconnected;
}
/*这里会对将要发送的数据编码,然后发送*/
n = vnc_send_framebuffer_update(&vs, entry->rect.x, entry->rect.y,entry->rect.w, entry->rect.h);

if (n >= 0) {
n_rectangles += n;
}
g_free(entry);
}
vnc_unlock_display(job->vs->vd);

/* Put n_rectangles at the beginning of the message */
vs.output.buffer[saved_offset] = (n_rectangles >> 8) & 0xFF;
vs.output.buffer[saved_offset + 1] = n_rectangles & 0xFF;

vnc_lock_output(job->vs);
if (job->vs->ioc != NULL) {
buffer_move(&job->vs->jobs_buffer, &vs.output);
/* Copy persistent encoding data */
vnc_async_encoding_end(job->vs, &vs);

qemu_bh_schedule(job->vs->bh);
} else {
buffer_reset(&vs.output);
/* Copy persistent encoding data */
vnc_async_encoding_end(job->vs, &vs);
}
vnc_unlock_output(job->vs);

disconnected:
vnc_lock_queue(queue);
QTAILQ_REMOVE(&queue->jobs, job, next);
vnc_unlock_queue(queue);
qemu_cond_broadcast(&queue->cond);
g_free(job);
vs.magic = 0;
return 0;
}

vnc_send_framebuffer_update是负责对变化区域的数据进行编码及发送的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
int vnc_send_framebuffer_update(VncState *vs, int x, int y, int w, int h)
{
int n = 0;
bool encode_raw = false;
size_t saved_offs = vs->output.offset;
/*根据client的encoding类型选择对应的编码方式进行编码*/
switch(vs->vnc_encoding) {
case VNC_ENCODING_ZLIB:
n = vnc_zlib_send_framebuffer_update(vs, x, y, w, h);
break;
case VNC_ENCODING_HEXTILE:
vnc_framebuffer_update(vs, x, y, w, h, VNC_ENCODING_HEXTILE);
n = vnc_hextile_send_framebuffer_update(vs, x, y, w, h);
break;
case VNC_ENCODING_TIGHT:
n = vnc_tight_send_framebuffer_update(vs, x, y, w, h);
break;
case VNC_ENCODING_TIGHT_PNG:
n = vnc_tight_png_send_framebuffer_update(vs, x, y, w, h);
break;
case VNC_ENCODING_ZRLE:
n = vnc_zrle_send_framebuffer_update(vs, x, y, w, h);
break;
case VNC_ENCODING_ZYWRLE:
n = vnc_zywrle_send_framebuffer_update(vs, x, y, w, h);
break;
default:
encode_raw = true;
break;
}

/* If the client has the same pixel format as our internal buffer and
* a RAW encoding would need less space fall back to RAW encoding to
* save bandwidth and processing power in the client. */
if (!encode_raw && vs->write_pixels == vnc_write_pixels_copy &&
12 + h * w * VNC_SERVER_FB_BYTES <= (vs->output.offset - saved_offs)) {
vs->output.offset = saved_offs;
encode_raw = true;
}
/*裸数据发送*/
if (encode_raw) {
vnc_framebuffer_update(vs, x, y, w, h, VNC_ENCODING_RAW);
n = vnc_raw_send_framebuffer_update(vs, x, y, w, h);
}

return n;
}

vnc_raw_send_framebuffer_update负责将数据通过pixman库转换后写入到vs的output buffer中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int vnc_raw_send_framebuffer_update(VncState *vs, int x, int y, int w, int h)
{
int i;
uint8_t *row;
VncDisplay *vd = vs->vd;

row = vnc_server_fb_ptr(vd, x, y);
for (i = 0; i < h; i++) {
/*之前在protocol_client_init里调用pixel_format_message设置write_pixels的回调函数为vnc_write_pixels_copy,这里最后就调用到vnc_write_pixels_copy*/
vs->write_pixels(vs, row, w * VNC_SERVER_FB_BYTES);
row += vnc_server_fb_stride(vd);
}
return 1;
}

vnc_write_pixels_copy最后会调用vnc_write将数据发送给client:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/*
* Scale factor to apply to vs->throttle_output_offset when checking for
* hard limit. Worst case normal usage could be x2, if we have a complete
* incremental update and complete forced update in the output buffer.
* So x3 should be good enough, but we pick x5 to be conservative and thus
* (hopefully) never trigger incorrectly.
*/
/*阈值是用来防止client恶意的不停请求致使buffer溢出*/
#define VNC_THROTTLE_OUTPUT_LIMIT_SCALE 5

void vnc_write(VncState *vs, const void *data, size_t len)
{
assert(vs->magic == VNC_MAGIC);
if (vs->disconnecting) {
return;
}
/* Protection against malicious client/guest to prevent our output
* buffer growing without bound if client stops reading data. This
* should rarely trigger, because we have earlier throttling code
* which stops issuing framebuffer updates and drops audio data
* if the throttle_output_offset value is exceeded. So we only reach
* this higher level if a huge number of pseudo-encodings get
* triggered while data can't be sent on the socket.
*
* NB throttle_output_offset can be zero during early protocol
* handshake, or from the job thread's VncState clone
*/
if (vs->throttle_output_offset != 0 &&
(vs->output.offset / VNC_THROTTLE_OUTPUT_LIMIT_SCALE) >
vs->throttle_output_offset) {
trace_vnc_client_output_limit(vs, vs->ioc, vs->output.offset,
vs->throttle_output_offset);
vnc_disconnect_start(vs);
return;
}
buffer_reserve(&vs->output, len);

if (vs->ioc != NULL && buffer_empty(&vs->output)) {
if (vs->ioc_tag) {
g_source_remove(vs->ioc_tag);
}
/*这里是判断vnc_client_io处理有没有启用,没有的话启动vnc_client_io处理线程处理数据发送*/
vs->ioc_tag = qio_channel_add_watch(
vs->ioc, G_IO_IN | G_IO_OUT, vnc_client_io, vs, NULL);
}
/*将数据添加到output buffer中,之后vnc_client_io会将buffer发送*/
buffer_append(&vs->output, data, len);
}