====== SPICE 视频重定向的传输路径选择 ====== ===== 1 借鉴 ===== ==== 1.1 spice-stream-agent ==== * {{:public:it:spice:spice-stream-agent.png |}} * 数据传输通过 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 ==== * {{:public:it:spice:webdav.png |}}   * 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 协议进行视频重定向流传输路径 * {{:public:it:spice:video-redirect-path.png |}} * 优点:只借用已有通道类型,不需要修改新增 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 通道设备:
* 用 VS 编译个测试串口程序, 编译为  TestPortChannel.exe, 对应串口名为 ''\\.\Global\org.spice.spicy'' #include #include //#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 的处理流程 === @startuml spice-gtk-port-channel skinparam sequenceMessageAlign center skinparam shadowing false header spice-gtk 中 port-channel 的处理流程 V0.1.0 by weiyongjiu hide footbox participant MainChannel participant PortChannel participant Spicy ==port通道创建== [-> MainChannel: SPICE_MSG_MAIN_CHANNELS_LIST activate MainChannel create PortChannel MainChannel -> PortChannel: g_object_new() \n SPICE_CHANNEL_PORT deactivate MainChannel PortChannel -> Spicy: signal "channel-new" activate PortChannel activate Spicy Spicy -> Spicy: channel_new() 进行初始化 deactivate Spicy deactivate PortChannel ==数据传输== [-> PortChannel: SPICE_MSG_SPICEVMC_DATA activate PortChannel PortChannel -> PortChannel: port_handle_msg() PortChannel -> Spicy++: signal "port-data" Spicy -> Spicy --: port_data() 输出数据到 stdin deactivate PortChannel @enduml 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'' 来发送数据