SPICE 视频重定向的传输路径选择
1 借鉴
1.1 spice-stream-agent
- 数据传输通过 StreamDev,StreamDev 是 char_device 的一种,虚拟化上是 virtio spiceport类型设备;
- guest 与 server 之间的协议(stream-device.h)是额外自定添加的;不影响 server 与 client 之间的协议
- 在 server 与 client 之间没有增加新类型通道,stream-channel 实现的通道类型是 DisplayChannel
StreamChannel* stream_channel_new(RedsState *server, uint32_t id) { return g_object_new(TYPE_STREAM_CHANNEL, "spice-server", server, "core-interface", reds_get_core_interface(server), "channel-type", SPICE_CHANNEL_DISPLAY, // TODO this id should be after all qxl devices "id", id, "migration-flags", 0, "handle-acks", TRUE, // TODO sure ?? NULL); }
- 在 client 端,收到通道列表时就会额外创建一个屏幕来对应这个 DisplayChannel, 表现为有两个屏幕显示
- 发送创建 SPICE_MSG_DISPLAY_SURFACE_CREATE 消息时,标识 SPICE_SURFACE_FLAGS_STREAMING_MODE 来表示全屏流显示
// give an hint to client that we are sending just streaming // see spice.proto for capability check here if (red_channel_client_test_remote_cap(rcc, SPICE_DISPLAY_CAP_MULTI_CODEC)) { surface_create.flags |= SPICE_SURFACE_FLAGS_STREAMING_MODE; }
- server 与 client 之间传输视频流, SPICE_MSG_DISPLAY_STREAM_DATA
- 视频重定向如果直接借用这种方式来传输流,需要修改客户端对新增 DisplayChannel 的操作,魔改协议,得不偿失
- 问题:是否可以直接把数据插入到主 DisplayChannel 的 视频流里? 有点复杂
1.2 webdav
- guest 与 server 之间的传输方式是相同的,都是 spiceport virtio 设备
- server 与 client 之间传输新增了一个通道类型 WebDAVChannel,但 spice-protocol 里 只新增了通道类型标识 SPICE_CHANNEL_WEBDAV,没有新增其它消息 ;再看 WebDAVChannel 的声明如下:
channel PortChannel : SpicevmcChannel { client: message { uint8 event; } @declare event = 201; server: message { uint32 name_size; uint8 *name[name_size] @zero_terminated @marshall @nonnull; uint8 opened; } @declare init = 201; message { uint8 event; } @declare event; }; channel WebDAVChannel : PortChannel { };
可以看到 WebDAVChannel 协议是继承 PortChannel 通道协议,而且完全没有新增元素
- 查看 server 代码,是能自动识别创建任意名称的 PortChannel 通道:
else if (strcmp(char_device->subtype, SUBTYPE_PORT) == 0) { if (strcmp(char_device->portname, "org.spice-space.webdav.0") == 0) { dev_state = spicevmc_device_connect(reds, char_device, SPICE_CHANNEL_WEBDAV); } else if (strcmp(char_device->portname, "org.spice-space.stream.0") == 0) { dev_state = RED_CHAR_DEVICE(stream_device_connect(reds, char_device)); } else { dev_state = spicevmc_device_connect(reds, char_device, SPICE_CHANNEL_PORT); } }
- 那么传输路径与其复用WebDAVChannel 不如直接创建新名称的 PortChannel
2 传输方案
2.1 方案一
直接使用 PortChannel 协议进行视频重定向流传输路径
- 优点:只借用已有通道类型,不需要修改新增 SPICE 协议, guest与client之间的传输数据内容即可自行随意定义
- 优点:不需要修改 spice-server 代码,
- 缺点:spice-gtk 客户端需要和 之前的 demo codec-agent 一样自行解码收到的视频数据,并根据坐标自行画在屏幕,并处理遮盖问题
2.2 方案二
直接把数据插入到主 DisplayChannel 的 视频流里传输
- 优点:不需要动客户端代码
- 缺点:需要修改 spice-server 代码,介入到主 DisplayChannel 对视频流的处理(涉及到播放区域与视频流编号),复杂度未能估算
- 缺点:相比方案一,遮盖计算从client 转移到了 server
- 缺点:如果不修改 server 与 client 之间的协议,只能传输符合 SPICE 协议格式的数据, 比如 视频流编码格式必须一致
2.3 问题
- virtio 的传输速率是否足够快? 足够
- virtio 是否足够稳定?
- portchannel 是否有足够稳定的用例?
3 PortChannel 详解
3.1 验证
在试验时发现 spice-gtk 里的工具 spicy.c 已经有对特定名称为 org.spice.spicy 的 spiceport 的 传输测试代码,所以测试 portchannel 传输就比较简单了:
- guest Windows10虚拟机添加名为 org.spice.spicy 的 spiceport 通道设备:
<channel type="spiceport"> <source channel="org.spice.spicy"/> <target type="virtio" name="org.spice.spicy" state="disconnected"/> <alias name="channel2"/> <address type="virtio-serial" controller="0" bus="0" port="3"/> </channel>
- 用 VS 编译个测试串口程序, 编译为 TestPortChannel.exe, 对应串口名为
\\.\Global\org.spice.spicy
#include <iostream> #include <windows.h> //#define SPICE_PORT_NAME L"\\\\.\\Global\\com.troila.newbee.0" #define SPICE_PORT_NAME_SPICY L"\\\\.\\Global\\org.spice.spicy" int main() { HANDLE port_handle = ::CreateFile(SPICE_PORT_NAME_SPICY, GENERIC_WRITE | GENERIC_READ, 0, NULL, OPEN_EXISTING, 0,//FILE_FLAG_OVERLAPPED, NULL); if (port_handle == INVALID_HANDLE_VALUE) { std::cout << "open spice port failed! err:" << ::GetLastError() << std::endl; }else { std::cout << "open spice port successfully" << std::endl; char write_buf[] = "hello troila!\n"; const DWORD buf_size = sizeof(write_buf); char read_buf[buf_size] = {0}; DWORD size = 0; DWORD totalsize = 0; BOOL ret = ::WriteFile(port_handle, (LPCVOID)write_buf, buf_size, &size, NULL); if (ret) { std::cout << "write successfully" << std::endl; } else { std::cout << "write failed! err:" << ::GetLastError() << std::endl; goto end; } size = 0; while (totalsize < buf_size) { ret = ::ReadFile(port_handle, read_buf+totalsize, buf_size-totalsize, &size, NULL); if (!ret) { std::cout << "write failed! err:" << ::GetLastError() << std::endl; goto end; } totalsize += size; std::cout << "read " << size << std::endl; } std::cout << "read content: " << read_buf << std::endl; end: ret = ::CloseHandle(port_handle); std::cout << "close handle ret:" << ret << std::endl; } }
- 在 client 端(Ubuntu 18.04) 安装
sudo apt install spice-client-gtk
, 启动spicy
并连接 - 在 guest 使用管理员权限执行 TestPortChannel.exe, 此时 spicy 日志显示对应 portchannel 打开并收到了 hello troila!
- 在 spicy 随意输入十几个字母,guest 端 TestPortChannel.exe 日志显示收到对应字母,并关闭串口
open spice port successfully write successfully read 1 ... read content: hello guest!!! close handle ret:1
- spicy 显示 对应 portchannel 已关闭
** Message: 10:53:14.947: main channel: opened port 0x5561c6fcd270 org.spice.spicy: opened hello troila! port 0x5561c6fcd270 org.spice.spicy: closed
双向传输验证完毕
3.2 流程详解
3.2.1 spice-gtk 对 port-channel 的处理流程
PortChannel 对象构造过程:
- PortChannel 继承 SpiceChannel, 首先构造 spice_channel_class_init()
- spice_channel_constructed()
- spice_session_channel_new()
- g_signal_emit(session, signals[SPICE_SESSION_CHANNEL_NEW], 0, channel);
- SPICE_SESSION_CHANNEL_NEW “channel-new”
- PortChannel 自身构造函数 spice_port_channel_class_init()
- channel_set_handlers() 设置以下消息对应的处理函数
- SPICE_MSG_PORT_INIT
- SPICE_MSG_PORT_EVENT
- SPICE_MSG_SPICEVMC_DATA
- 消息对应的处理函数 port_handle_msg()
- g_coroutine_signal_emit() SPICE_PORT_DATA “port-data”
3.3 client 端 port-channel 的使用方式
- spice-gtk 实现了一个 SpicePortChannel, 详细文档 https://www.spice-space.org/api/spice-gtk/SpicePortChannel.html
- 增加对 glib 信号
SPICE_SESSION_CHANNEL_NEW
channel-new
的处理函数,按类型与通道名来获知特定名字 portchannel 的建立,并进行初始化 - 初始化时增加对该通道信号
port-event
,port-data
的处理函数, - 响应
port-event
用于获取通道打开关闭事件 - 响应
port-data
来接收数据 - 使用函数
spice_port_write_async
,spice_port_write_finish
来发送数据