Qemu中VNC Server分析
条评论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 | static QemuOptsList qemu_vnc_opts = { |
上述参数含义
名称 | 解释 | ||
---|---|---|---|
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主要数据结构 | |||
|
VncDisplay:代表一个VNC Server的结构体,在qemu解析参数并初始化vnc的时候会调用到vnc_display_init,在这里会分配一个VncDisplay实例,并加入到全局链表vnc_displays(如果配置了多个vnc,则全局链表中有多个VncDisplay实例,即代表qemu有多个VNC Server),可以看到VncDisplay结构体中的大部分参数和我们之前讲的qemu命令行中vnc参数是一致的,这里不再对VncDisplay结构体的参数详细阐述。
1 | struct VncState |
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 | struct VncRect |
VncRect:表示VNC 变化的区域信息,包含了变化区域坐上面坐标(x,y),以及变化区域的宽w,高h
VncRectEntry:将所有VNC 变化区域信息组成链表放在VncRectEntry结构中
VncJob:表示一个更新的job,每个job包含当时所有的变化区域信息
VncJobQueue:表示job队列,以及处理job的线程信息
VNC初始化流程分析
在vl.c中的main函数会解析vnc参数:
1 | case QEMU_OPTION_vnc: |
之后在解析完参数后会调用vnc_init_func初始化vnc:
1 | /* init remote displays */ |
vnc的实现主要在ui/vnc.c文件中(vnc的各种数据压缩方式的实现在其他文件)。 初始化入口是vnc_init_func函数,vnc_init_func函数中会调用vnc_display_init初始化VncDisplay,之后调用vnc_display_open监听客户端连接,vnc_init_func实现如下:
1 | int vnc_init_func(void *opaque, QemuOpts *opts, Error **errp) |
函数vnc_display_init主要是初始化对应的VncDisplay以及启动worker线程处理vnc数据更新队列,以及注册对应的dcl_ops(监听显示设备变化的操作集):
1 | void vnc_display_init(const char *id, Error **errp) |
DisplayChangeListenerOps表示当显示设备发生改变时所触发的操作,dcl_ops定义了vnc对显示设备变化时的相关操作:
1 | static const DisplayChangeListenerOps dcl_ops = { |
register_displaychangelistener负责设置刷新定时器(调用dpy_refresh),更新console等:
1 | void register_displaychangelistener(DisplayChangeListener *dcl) |
vnc_display_open中主要对qemu命令行传入的参数校验,之后会根据入参设置vnc监听地址、vnc密码、连接方式,共享方式、认证模式、连接数、压缩方式等,最后开始监听socket等待客户端连接:
1 | void vnc_display_open(const char *id, Error **errp) |
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 | static int vnc_display_listen(VncDisplay *vd, |
其中vnc_listen_io是客户端连接时的回调处理函数,到此vnc初始化完成,等待客户端的连接。
VNC客户端连接流程分析
上面讲到vnc_listen_io是在vnc客户端连接时触发的回调函数,vnc_listen_io的流程如下:
1 | static void vnc_listen_io(QIONetListener *listener, |
vnc_connect是真正处理客户端连接的函数:会为当前连接进来的client分配一个VncState,并,之后初始化VncState中的各项参数:各种buffer(包括input/output buffer,job buffer以及各种压缩方式的buffer),设置AUTH方式,为lossy_rect分配内存,调整gui_update的更新间隔为VNC_REFRESH_INTERVAL_BASE等
1 | static void vnc_connect(VncDisplay *vd, QIOChannelSocket *sioc, |
vnc_connect中调用qio_channel_add_watch注册的回调处理函数vnc_client_io负责对client进行数据读写:
1 | gboolean vnc_client_io(QIOChannel *ioc G_GNUC_UNUSED, |
vnc_update_server_surface用于初始化显存内容缓存空间以及设置更新的像素:
1 | static void vnc_update_server_surface(VncDisplay *vd) |
VNC协议消息交互流程
vnc客户端和服务端的协议交互是从vnc_start_protocol函数开始,vnc_start_protocol中首先发送服务端RFB版本,然后注册数据读取回调处理函数为protocol_version:
1 | void vnc_start_protocol(VncState *vs) |
protocol_version这个函数负责接收客户端发过来的版本信息,并调用客户端的Auth处理函数对客户端连接进行认证:
1 | static int protocol_version(VncState *vs, uint8_t *version, size_t len) |
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 | void start_client_init(VncState *vs) |
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 | static int protocol_client_msg(VncState *vs, uint8_t *data, size_t len) |
另外说下键盘设备如何处理这些事件的(鼠标设备也是类似),上面的key_event,最终会调到qemu_input_event_send_key函数,这个函数里会调用qemu_input_event_new_key初始一个新的输入事件,并设置事件类型为INPUT_EVENT_KIND_KEY, 之后调用函数qemu_input_event_send_key发送键盘事件:
1 | static InputEvent *qemu_input_event_new_key(KeyValue *key, bool down) |
qemu_input_event_send_key函数最终会调到真正执行键盘输入的函数qemu_input_event_send_impl,这个函数里会先找到执行事件的设备handler,然后调用handler的event函数处理InputEvent:
1 | void qemu_input_event_send_impl(QemuConsole *src, InputEvent *evt) |
handler可以为ps2设备,virtio-tablet,usb-tablet等,这里不再对设备如何处理输入事件详细说明
framebuffer更新流程
dc_ops操作集中的回调函数指针dpy_gfx_update是在供显卡设备绘图完成后调用的,负责更新变化区域,vnc中dpy_gfx_update指向的是vnc_dpy_update函数:
1 | static void vnc_dpy_update(DisplayChangeListener *dcl, |
显卡设备更新是由dc_ops中的函数指针dpy_refresh触发的,dpy_refresh由上面的gui_setup_refresh创建的定时器调用,dpy_refresh负责获取display中的变化区域内容并创建vncjob插入queue:
1 | static void vnc_refresh(DisplayChangeListener *dcl) |
vnc_update_client负责创建vncjob,并根据bitmap中的dirty区域计算起始坐标(x,y),之后将变化区域的w,h,x,y设置到VncRectEntry实例中并加入到vncjob中的rectangles链表中,之后将vncjob加入到queue中并通知worker线程消费queue:
1 | static int vnc_update_client(VncState *vs, int has_dirty) |
这里需要提到VNC的更新方式,我们知道VNC更新有三个选项:
VNC_STATE_UPDATE_NONE代表不更新给client;
VNC_STATE_UPDATE_INCREMENTAL代表低于缓存阈值的时候更新;
VNC_STATE_UPDATE_FORCE表示强制更新。
1 | typedef enum { |
VNC更新请求由客户端发起,在之前的protocol_client_msg中可以看到,VNC_MSG_CLIENT_FRAMEBUFFER_UPDATE_REQUEST就是client发过来的请求:
1 | case VNC_MSG_CLIENT_FRAMEBUFFER_UPDATE_REQUEST: |
framebuffer_update_request根据client发过来的incremental是否为真决定采用哪种更新方式:
1 | static void framebuffer_update_request(VncState *vs, int incremental, |
framebuffer发送流程
vnc_start_worker_thread会创建线程用来处理framebuffer更新(即消费上面的vncjob并将framebuffer发送给client):
1 | static void *vnc_worker_thread(void *arg) |
vnc_worker_thread_loop里是真正处理frameupdate的函数:
1 | static int vnc_worker_thread_loop(VncJobQueue *queue) |
vnc_send_framebuffer_update是负责对变化区域的数据进行编码及发送的
1 | int vnc_send_framebuffer_update(VncState *vs, int x, int y, int w, int h) |
vnc_raw_send_framebuffer_update负责将数据通过pixman库转换后写入到vs的output buffer中:
1 | int vnc_raw_send_framebuffer_update(VncState *vs, int x, int y, int w, int h) |
vnc_write_pixels_copy最后会调用vnc_write将数据发送给client:
1 | /* |