虚拟通道SDK编程指南

Posted by CoderLeonidas on March 13, 2020

虚拟通道SDK编程指南

使用虚拟通道SDK

Citrix Virtual Channel Software Development Kit (SDK) 为编写基于ICA协议的服务端程序和客户端驱动的额外虚拟通道提供了支持。服务端的虚拟通道程序是在XenApp和XenDesktop 服务器上的。该版本的SDK支持为Win32的Citrix插件编写新的虚拟通道。如果您想为其他客户端平台编写虚拟驱动,请联系Citrix。

虚拟通道SDK提供了:

  • Citrix虚拟驱动应用程序编程接口(VDAPI)与Citrix服务器API SDK (WFAPI SDK)中的虚拟通道函数一起用于创建新的虚拟通道。VDAPI提供的虚拟通道支持旨在简化编写自己的虚拟通道。
  • Windows监控API,增强了与ICA集成的第三方应用程序的视觉体验和支持。
  • 用于演示编程技术的几个虚拟通道示例程序的工作源代码。

虚拟通道SDK需要WFAPI SDK来编写虚拟通道的服务器端。

对于Presentation Server客户端版本6.0到9.0,使用虚拟服务器SDK版本2.3。若要为DOS 32客户端或较早于6.0版的客户端编写虚拟通道驱动,请使用虚拟通道SDK 2.1版

必要条件

系统要求:

您需要为Windows 4.11安装Citrix Reciver并安装WinFrame API SDK。您可以在任何平台上构建虚拟驱动和应用程序。要运行它们,需要一台运行XenApp或XenDesktop的服务器。

开发环境:

使用Microsoft Visual Studio 2017和 .net 4.0。服务器端开发也需要WFAPI SDK。

虽然编译器软件包包括c++,但是在这个SDK中只使用C代码。这个SDK还没有经过任何其他编译器或任何其他组合的测试。

执行环境:

  • 服务器要求: XenApp 6.5 or higher (earlier versions of XenApp and XenDesktop are also supported but the Windows Monitoring API is currently supported only on XenApp 5 Feature Pack 2 for Windows Server 2003, XenApp 6.0, and XenDesktop 4).

  • Windows32客户端要求:Windows 4.11的Receiver

编译过程:

此SDK中提供的源代码包括供Microsoft Visual Studio使用的解决方案文件。

  • 客户端虚拟驱动设计使用Visual Studio 2017构建,客户端解决方案文件位于\src\examples\vc\client\client_examples_win32.sln

  • 您可以在visual Studio 2017的visual interface中构建这个解决方案,将“Configuration”设置为“Release”,将“Platform”设置为“Win32”。

  • 编译后的目标文件和生成的二进制文件放在项目子文件夹中的Relese文件夹中。 比如对于vdmix来说,该输出文件夹是src\examples\vc\client\vdmix\Release

  • 服务器端示例可以用Visual Studio 2017构建,解决方案文件位于\src\examples\vc\server\server_2017.sln

  • 编译后的目标文件和生成的二进制文件放在项目子文件夹内的发布文件夹中。 比如对于ctxmix,使用VS2017 来构建,输出文件夹在src\examples\vc\server\ctxmix\Release

  • 还可以在打开调试信息的情况下构建组件。输出目录更改为调试对象的Debug

架构

ICA虚拟通道是运行Citrix XenApp的服务器和客户端设备之间交换通用数据包数据的双向无错误连接。开发人员可以使用虚拟通道向客户端添加功能。虚拟通道的用途包括:

  1. 对管理功能的支持
  2. 新的数据流(音频和视频)
  3. 新的设备,如扫描仪、读卡器和操纵杆

虚拟通道概述

ICA虚拟通道是客户端和运行Citrix XenApp或XenDesktop的服务器之间交换通用数据包数据的双向无错误连接。每个ICA虚拟通道的实现包含两个组件:

  • 运行XenApp或XenDesktop的计算机上的服务器端部分

  • 客户端设备上的客户端部分

    客户端虚拟通道驱动是一个可动态加载的模块(.dll),在客户端上下文中执行。您必须编写您的虚拟驱动。

该图演示了虚拟通道客户端-服务器连接

WinStation驱动负责从ICA数据流中多路分解(demultiplexed)虚拟通道数据,并将其路由到正确的处理模块(在本例中是虚拟驱动DLL)。WinStation驱动还负责通过ICA连接收集和发送虚拟通道数据到服务器。在客户端,WinStation驱动也称为客户端引擎,或简称为引擎。

WinStation 驱动==>客户端引擎

下面是使用虚拟通道进行客户端-服务器数据交换的概述

  • 客户端连接到运行XenApp或XenDesktop的服务器。客户端将有关它支持的虚拟通道的信息传递给服务器。
  • 服务器端应用程序启动,获取虚拟通道的句柄,并可选地查询关于该通道的其他信息。
  • 客户端虚拟驱动和服务器端应用程序使用以下两种方法传递数据:
  • 如果服务器应用程序有数据要发送给客户端,则立即将数据发送给客户端。当客户端接收到数据时,WinStation驱动从ICA流中解复用虚拟通道数据,并立即将其传递给客户端虚拟驱动。
  • 如果客户端虚拟驱动要向服务器发送数据,则可以立即发送数据,或者在下一次WinStation驱动轮询虚拟驱动时发送数据。当服务器接收到数据时,它将排队,直到虚拟通道应用程序读取它。无法通知服务器虚拟通道应用程序接收到数据。
  • 当服务器虚拟通道应用程序完成时,它关闭虚拟通道并释放所有分配的资源。

ICA和虚拟信道数据包

虚拟通道数据包封装在客户端和服务器之间的ICA流中。因为ICA是表示层协议,并且运行在几种不同的传输上,所以虚拟通道应用程序编程接口(API)允许开发人员编写自己的协议,而不必担心底层的传输。数据包被保留。

例如,如果向服务器发送100个字节,则当虚拟通道从ICA数据流进行多路复用(demultiplexed)时,服务器将接收相同的100个字节。编译后的代码独立于当前配置的传输协议运行。

ICA引擎为虚拟通道提供以下服务:

  • 打包封包

    ICA虚拟通道是基于的,这意味着如果一端使用一定数量的数据执行写操作,另一端在执行读操作时接收整个数据块。这与TCP形成了对比,例如,TCP是基于的,需要更高级别的协议来解析包边界。换句话说,虚拟信道包包含在ICA流中,ICA流由系统软件单独管理。

  • 误差校正

    即使基础传输不可靠,ICA也提供自己的可靠性机制。这保证了连接是无错误的,并且数据是按照发送的顺序接收的。

  • 流程控制

    虚拟通道API提供了几种类型的流控制。这允许设计人员在任何时候都只处理特定数量的数据。

客户端WinStation驱动和虚拟驱动交互

客户端虚拟驱动可以选择以两种模式之一发送数据

  • 轮询模式(Polling mode)

  • 立即模式(Immediate mode)

如果运行在轮询模式,WinStation驱动通过调用它的DriverPoll函数来定期轮询每个虚拟驱动。当调用DriverPoll时,虚拟驱动应该立即检查任何必要的状态信息,发送任何排队的数据,并将控制权返回给WinStation驱动。

如果在立即模式下操作,虚拟驱动可以在任何时候发送数据。例如,假设驱动在ICADataArrival函数中收到来自服务器的数据包。在即时发送数据模式下,驱动可以对收到的数据包立即发送数据,然后控制权将从ICADataArrival函数返回到WinStation驱动。

当虚拟驱动试图向服务器发送数据时,它应该准备好数据发送操作,有时会被拒绝。可能会出现这种情况,因为WinStation驱动支持合理但不过量的排队等待发送到服务器的积压数据。如果发送操作被拒绝,虚拟驱动必须安排稍后重试发送。

在任何情况下,无论是轮询模式还是立即模式,虚拟驱动都不能阻塞。当WinStation驱动调用任何驱动函数时,虚拟驱动必须立即检查任何必要的状态信息,发送任何排队的数据,并将控制权返回给WinStation驱动。

当用户启动客户端时,将执行以下过程:

  • 1 在客户端加载时,客户端引擎读取注册表中的配置存储以确定要配置的模块,包括如何配置虚拟通道驱动。
  • 2 客户端引擎通过调用Load函数加载注册表中配置存储中定义的虚拟通道驱动,该函数必须由虚拟通道驱动. dll显式导出。Load函数是在静态库文件 Vdapi.lib中定义的。 Vdapi.lib,在本SDK中提供。每个驱动都必须链接到这个库文件。Load函数将.dll中定义的驱动入口点转发给客户端引擎。
  • 3 对于每个虚拟通道,WinStation驱动调用DriverOpen函数,该函数建立并初始化虚拟通道。WinStation驱动将WinStation驱动中的一个发送数据输出函数的地址传递给虚拟通道驱动。虚拟通道驱动将ICADataArrival函数的地址传递给WinStation驱动。WinStation驱动在客户端加载时为每个虚拟驱动调用DriverOpen函数,而不是在服务器端应用程序打开虚拟通道时调用
  • 4 当虚拟通道数据从服务器到达时,WinStation驱动为该虚拟驱动调用ICADataArrival函数。
  • 5 如果使用发送数据轮询模式,则虚拟驱动无法启动数据传输。相反,WinStation驱动调用DriverPoll来轮询要发送到服务器的数据。要发送数据,虚拟通道驱动可以使用QueueVirtualWrite函数(这个地址是在初始化期间获得的)将数据块发送到服务器端版本的通道。DriverPoll期间,虚拟驱动可能试图发送一个或多个包(VirtualWrites),直到没有更多的数据发送,或QueueVirtualWrite函数返回错误代码CLIENT_ERROR_NO_OUTBUF,表明没有更多的缓冲空间,和数据没有被接受,而且必须稍后重试(通常在后来DriverPoll)。
  • 6 如果使用立即发送数据模式,则虚拟驱动可以随时发送数据。 要发送数据,虚拟通道驱动将使用SendData函数(此地址在初始化期间获得)将数据块发送到通道的服务器端版本。 虚拟驱动可能会尝试发送一个或多个数据包(VirtualWrites),直到没有更多数据要发送,或者SendData函数返回错误代码CLIENT_ERROR_NO_OUTBUF,以指示没有更多的缓冲区空间,并且所讨论的数据没被接受,必须稍后重试。 当WinStation驱动向虚拟驱动的DriverPoll函数发出特殊的“通知”调用时,通常会发生重试。 当WinStation驱动检测到缓冲区已被释放并且可以重试发送数据操作时,将发出通知DriverPoll调用。

Module.ini

XenApp插件使用存储在Module.ini中的设置来决定加载哪个虚拟通道。驱动程序开发人员还可以使用Module.ini来存储虚拟通道的参数。ini更改仅在安装之前有效。安装之后,必须修改注册表中的配置存储以添加或删除虚拟通道。

使用内存INI函数从配置存储中读取数据。

虚拟通道数据包

ICA不定义虚拟信道包的内容。内容特定于特定的虚拟通道,不由ICA数据流管理器解释或管理。您必须为虚拟通道数据开发自己的协议

虚拟信道包的长度可以是ICA连接所支持的最大大小。这个大小独立于底层传输的大小限制。这些限制会影响服务器端WFVirtualChannelReadWFVirtualChannelWrite函数,以及客户端的QueueVirtualWriteSendData函数。最大数据包大小为5000字节(4996个数据字节加上ICA数据流管理器生成的数据包开销的4个字节)。

虚拟驱动和服务器端应用程序都可以查询最大数据包大小。有关查询客户端最大数据包大小的示例,请参见DriverOpen

流程控制

ICA虚拟通道为下游(服务器到客户端)流控制提供支持,但目前不支持上游流控制。服务器接收到的数据将排队等待使用。

某些传输协议(例如TCP/IP)提供流控制,而其他传输协议(例如IPX)不提供流控制。 如果需要数据流控制,您可能需要将其设计到虚拟通道中。

为ICA虚拟通道选择三种流控制类型之一:“None”,“Delay”或“ ACK”。 每个虚拟通道可以具有自己的流量控制方法。 流控制方法在初始化期间由虚拟驱动程序指定。

  • None

ICA不控制数据流。假设客户端可以处理所有发送的数据。您必须将任何所需的流控制实现为虚拟通道协议的一部分。这种方法是最难实现的,但是提供了最大的灵活性。Ping示例不使用流控制,也不需要它

  • Delay

延迟流控制是一种对从服务器发送的数据进行定速的简单方法。当客户端虚拟驱动指定延迟流控制时,它还提供以毫秒为单位的延迟时间。服务器在它发送的每个数据包之间等待指定的延迟时间。

  • ACK

ACK流控制提供了所谓的滑动窗口。使用ACK流控制,客户端指定它的最大缓冲区大小(它在任何时候可以处理的最大数据量)。服务器将发送到该数量的数据。当客户端虚拟驱动完成对其全部或部分缓冲区的处理时,发送一个ACK ICA包,指示处理了多少数据。然后,服务器可以发送更多的数据字节,直到客户端确认的字节数为止。

此ACK是不透明的,虚拟驱动必须显式地构造ACK包并将其发送到服务器。服务器发送整个数据包;如果下一个要发送的包比窗口大,服务器将阻止发送,直到窗口足够大以容纳整个包为止。

WIndows监控API

概述

这些api允许创建同步在主机(XenApp或XenDesktop)上运行的应用程序的可视方面与运行在Citrix插件上的相应可视元素的解决方案。api由两个不同的部分组成:客户端和主机端。客户端组件向第三方公开了以前不可用的功能。这包括在客户端桌面获取关于ICA窗口的信息(如句柄、尺寸、平移和缩放)、给定主机窗口的对应客户端窗口,以及设置一个回调函数,以便在ICA窗口更改时调用。主机组件是WinFrame API的一部分,它允许通过内核模式调用跟踪主机上的窗口位置。

关键点

这些API包涵以下特性:

  • 允许通过WinFrame API有效地跟踪主机上的窗口
  • 提供使用虚拟通道SDK与客户端桌面显示同步的方法。
  • 为第三方应用程序提供更好的视觉体验和支持,以更好地集成ICA。

API的使用

准备开始
  • Headers: wdapi.h
  • Libraries: wdica30.lib
架构

api提供了两个有它们自己的体系结构的不同的组件:主机端和客户端。主机组件是WinFrame API的一部分,它提供对被跟踪的窗口的更新。然后可以将此数据与Citrix插件通信,以便同步窗口位置。然后,虚拟通道SDK中的客户端组件允许第三方与ICA窗口进行同步。它为他们提供关于ICA窗口的大小和句柄的信息,以及它是平移还是缩放。总的来说,这些api允许第三方应用程序更好地与ICA集成,并提供更好的视觉体验。

客户端在虚拟通道SDK中扩展了当前的WdQueryInformation系统,以公开第三方以前无法使用的功能。用户调用预先存在的VdCallWd函数来调用WinStation驱动的QueryInformation函数,该函数执行请求的任务。

例子
  • 获取ICA窗口信息

这个示例展示了如何收集关于ICA窗口的信息。它用关于ICA窗口当前状态的信息填充一个结构体。这包括它的尺寸、句柄、视图区域尺寸和偏移量(例如,平移),以及它当前的模式(例如,缩放、平移、无缝)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
WDQUERYINFORMATION wdQueryInfo;
UINT16 uiSize;
int rc;
WDICAWINDOWINFO infoParam;
wdQueryInfo.WdInformationClass = WdGetICAWindowInfo;
wdQueryInfo.pWdInformation = &infoParam;
wdQueryInfo.WdInformationLength = sizeof(infoParam);
uiSize = sizeof(wdQueryInfo);
rc = VdCallWd(g_pVd, WDxQUERYINFORMATION, &wdQueryInfo, &uiSize);
if(CLIENT_STATUS_SUCCESS == rc)
{
// Successfully populated infoParam with ICA window
// information
}
  • 获取相应的客户端窗口

这个示例展示了如何为给定的服务器窗口获取相应的客户端窗口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
WDQUERYINFORMATION wdQueryInfo;
UINT16 uiSize;
int rc;
HWND window = 0x42; // example server window handle
wdQueryInfo.WdInformationClass = WdGetClientWindowFromServerWindow;
wdQueryInfo.pWdInformation = &window;
wdQueryInfo.WdInformationLength = sizeof(window);
uiSize = sizeof(wdQueryInfo);
rc = VdCallWd(g_pVd, WDxQUERYINFORMATION, &wdQueryInfo, &uiSize);
if(CLIENT_STATUS_SUCCESS == rc)
{
// Success, pWdInformation now points to the
//corresponding client window hwnd.
}
  • 注册ICA窗口回调

这个示例展示了如何注册一个在ICA窗口更改时调用的回调函数。它注册一个名为Foo的用户定义回调函数。之后,每当ICA窗口发生变化时,就会调用Foo,并传入当前ICA窗口模式。然后使用WdGetICAWindowInfo信息类收集关于ICA窗口的更多信息,如第一个示例所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
WDQUERYINFORMATION wdQueryInfo;
UINT16 uiSize;
int rc;
WDREGISTERWINDOWCALLBACKPARAMS callbackParams;
callbackParams.pfnCallback = &Foo; // Your callback function
wdQueryInfo.WdInformationClass = WdRegisterWindowChangeCallback;
wdQueryInfo.pWdInformation = &callbackParams;
wdQueryInfo.WdInformationLength = sizeof(callbackParams);
uiSize = sizeof(wdQueryInfo);
rc = VdCallWd(g_pVd, WDxQUERYINFORMATION, &wdQueryInfo, &uiSize);
if(CLIENT_STATUS_SUCCESS == rc)
{
// Callback successfully registered.
// Function Foo will be called whenever the ICA window
// mode, position, or size changes.
}
  • 取消注册ICA窗口回调

这个示例展示了如何取消注册以前的ICA窗口更改回调函数。它从前面的示例中注销回调函数Foo。当ICA窗口改变时,回调函数不再被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
WDQUERYINFORMATION wdQueryInfo;
UINT16 uiSize;
int rc;
wdQueryInfo.WdInformationClass = WdUnregisterWindowChangeCallback
wdQueryInfo.pWdInformation = &callbackParams.Handle;
// Previously returned handle
wdQueryInfo.WdInformationLength = sizeof(callbackParams.Handle);
uiSize = sizeof(wdQueryInfo);
rc = VdCallWd(g_pVd, WDxQUERYINFORMATION, &wdQueryInfo, &uiSize);
if(CLIENT_STATUS_SUCCESS == rc)
{
// Callback successfully unregistered
}

编程指南

作为一个整体,api为那些在主机和客户端桌面之间协调窗口的应用程序提供了更好的窗口控制。通常,主机使用API的WinFrame API组件来跟踪感兴趣的窗口。主机监听指定的邮件槽,以跟踪关于其窗口的更新。然后这些更新被传递到Citrix插件,在那里它们被用来正确地定位相应的窗口。Citrix插件使用Virtual Channel SDK中的api的客户端部分来同步它的窗口和ICA窗口。当ICA窗口发生更改时,可以通知Citrix插件,从而对其他第三方应用程序进行任何必要的更改。

编程参考

结构体
  • WDQUERYINFORMATION

已存在的,传递给WinStation驱动的QueryInformation方法的结构体。存储输入的同时也存储结果输出。

1
2
3
4
5
6
7
typedef struct _WDQUERYINFORMATION
{
WDINFOCLASS WdInformationClass;
LPVOID pWdInformation;
USHORT WdInformationLength;
USHORT WdReturnLength;
} WDQUERYINFORMATION, * PWDQUERYINFORMATION;
  • WdInformationonClass: 设置为与要调用的API函数对应的enum值。
  • pWdInformation: 此函数调用的必要输入参数(如果有)。如果调用返回任何内容,它也存储在这里。
  • WdInformationLength: 设置pWdInformation指向的输入数据的大小。
  • WdReturnLength: 返回时填写;pWdInformation指向的返回值的大小。
  • WDICAWINDOWINFO

结构体在使用WdGetICAWindowInfo类时作为输入传递。成功返回后,它将填充关于ICA窗口的信息。

1
2
3
4
5
6
7
typedef struct _WDICAWINDOWINFO
{
HWND hwnd;
WDICAWINDOWMODE mode;
UINT32 xWinWidth, yWinHeight, xViewWidth, yViewHeight;
INT xViewOffset, yViewOffset;
} WDICAWINDOWINFO, * PWDICAWINDOWINFO;
  • hwnd: ICA 窗口句柄.
  • mode: ICA 窗口的当前模式(如拉伸、平移、无缝等)
  • xWinWidth: ICA 窗口宽度.
  • yWinHeight: ICA 窗口高度.
  • xViewWidth: ICA 窗口视图区域宽度.
  • yViewHeight: ICA 窗口视图区域高度.
  • xViewOffset: 视图区域在x维度上的偏移量 (水平平移).
  • yViewOffset: 视图区域在y维度上的偏移量(垂直平移 ).
  • WDREGISTERWINDOWCALLBACKPARAMS

结构体在使用WdRegisterWindowChangeCallback信息类时作为输入传递。

1
2
3
4
5
typedef struct _WDREGISTERWINDOWCALLBACKPARAMS
{
PFNWD_WINDOWCHANGED pfnCallback;
UINT32 Handle;
} WDREGISTERWINDOWCALLBACKPARAMS, *PWDREGISTERWINDOWCALLBACKPARAMS;
  • pfnCallback: 当ICA窗口更改(例如,其模式、维度、视图)时将调用的用户定义函数。这个函数应该有以下头部,UINT参数是ICA窗口的当前模式(参见WDICAWINDOWMODE): typedef VOID (cdecl * PFNWD WINDOWCHANGED) (UINT32)
  • Handle: 成功返回时,将填充此句柄。稍后,当取消注册回调时,可以使用它来标识句柄。
联合
  • WDICAWINDOWMODE

用于存储ICA窗口的当前模式

1
2
3
4
5
6
7
8
9
10
11
typedef union _WDICAWINDOWMODE
{
struct
{
UINT Reserved : 1;
UINT Seamless : 1;
UINT Panning : 1;
UINT Scaling : 1;
} Flags;
UINT Value;
} WDICAWINDOWMODE;
  • Reserved: 模式的保留部分,当前未使用。
  • Seamless: ICA窗口目前处于无缝模式。
  • Panning: ICA窗口当前正在平移(即,垂直/水平滚动).
  • Scaling: ICA窗口当前正在缩放.
  • Value: 模式的原始值.
枚举
  • WDINFOCLASS

WDINFOCLASS枚举有四个值供Windows监控API使用

  • WdGetICAWindowInfo
  • WdGetClientWindowFromServerWindow
  • WdRegisterWindowChangeCallback
  • WdUnregisterWindowChangeCallback

Cirix动态虚拟通道协议

架构

DVC协议的主要目的是在传统静态虚拟通道(SVCs)上提供一个通用的基于连接的通信基础设施

动态虚拟通道(DVCs)通过SVCs进行多路复用。一般来说,在ICA上每一项技术使用一个SVC。DVC协议提供了在逻辑上连接的动态虚拟通道端点之间创建和通信的能力。

动态虚拟通道是在ICA主机上运行的应用程序(第一个端点)和ICA客户端上运行的应用程序(第二个端点,称为DVC侦听器)之间创建的端到端连接。端到端DVC连接是通过ICA连接建立和维护的。

单独的DVC实例由DVC管理器创建和维护。有一个DVC管理器在主机上运行(作为设备驱动和服务实现),另一个在客户端上运行(作为虚拟驱动DLL实现)。主机负责创建_动态虚拟通道_,客户端负责创建和维护到客户端DVC应用程序的连接。

一旦建立了DVC连接,主机和客户端DVC应用程序就可以互相发送数据消息。这些消息可以由任何一方发起,发送和接收消息在任何一方都是相同的。

该协议允许DVC使用多个静态通道。默认情况下,每个代表特定技术的DVC插件都在单独的SVC上运行。这允许管理员通过管理各自的SVC的优先级来对各个DVC-remoted技术进行优先级排序。但是,还可以配置DVC客户端,以便两个或多个DVC插件可以共享SVC。 在极少数情况下,当DVC插件的数量大于可用SVC的数量时,这可能是理想的。 当前,ICA上最多支持64个SVC。

如何写在ICA上的DVC组件

Microsoft s DVC是在远程桌面协议上实现的,Citrix DVC协议是在ICA协议上实现的。要在ICA上编写DVC组件,可以使用Microsoft的DVC API。

DVC服务端API

DVC客户端API

Cirix动态虚拟通道设置

以下步骤在动态虚拟通道的生存期内执行:

    1. 客户机DVC管理器枚举所有已注册的DVC插件,并将列表发送到主机,主机由DVC插件友好名称和对应的SVC名称对组成。DVC插件友好名称可以是DLL名称或任何其他可用的友好名称。SVC名称可以是管理员分配的,也可以是由友好的名称生成的。SVC名称总是被截断为7个字符加上一个空终止符(总共8个字符),这是ICA协议使用的SVC名称大小。该列表作为,在初始ICA握手时在WinStation驱动程序(WD)级别交换的CAPABILITY_DYNAMIC_VIRTUAL_CHANNEL ICA能力的一部分发送(The list is sent as part of the CAPABILITY_DYNAMIC_VIRTUAL_CHANNEL ICA capability exchanged during the initial ICA handshake at the WinStation Driver (WD) level. )。参见ICA3.0协议规范(ica30.doc)了解有关CAPABILITY_DYNAMIC_VIRTUAL_CHANNEL的详细信息。
      • 通常,DVC插件是根据Microsoft定义的需求来注册的。Citrix为两个目的提供了额外的DVC插件注册
        • a. 未正确注册且不能以其他方式枚举的已知第三方插件的枚举。
        • b. 由管理员显式的为每个插件可选的分配SVC名称(Optional assignment by administrator of explicit SVC name per plug-in)。
    1. 主机DVC管理器通过CAPABILITY_DYNAMIC_VIRTUAL_CHANNEL ICA功能为从客户端收到的每个通道名称打开一个SVC。
    1. 主机将支持的DVC功能列表发送到客户端,并请求客户端支持的功能列表。 当前,出于优化目的,在打开的SVC之一上协商DVC功能,并假定它们适用于所有SVC。
    1. 客户端使用受支持的功能列表进行响应。列表通过相同的SVC发送。
    1. 然后,主机提交用于ICA连接生存期的DVC功能。列表通过相同的SVC发送
    1. 对于每个DVC插件,客户机都发送一个由DVC插件托管的所有当前可用侦听器的列表。每个列表通过各自的SVC发送:
      • a. 在主机提交DVC功能之后立即执行。在主机提交DVC功能之后立即执行。
      • b. 在任何时候,新的侦听器都会启动或现有的侦听器会关闭。
      • 备注: 虽然理论上侦听器可以在任何时刻关闭,但在实践中,一旦侦听器启动,它在整个DVC插件关闭之前都不会关闭。
    1. 主机读取侦听器列表,缓存来自客户机的任何未来更新,并将映射保存在表中。
    1. 随后,当主机应用程序调用虚拟通道开放API (WTSVirtualChannelOpenEx)时,主机在其表中查找侦听器名称,分配;;;;loolllllollollllllllllllllllloooooo范围内的新通道号,并将其链接到相应的侦听器。在侦听器中,通道号是惟一的。然后通过与侦听器关联的SVC发送DVC创建请求。客户机通过相同的SVC进行响应,成功或失败地在指定的侦听器上创建通道(DVC实例)。客户机的响应与主机应用程序通信。
  • 9.如果成功创建了DVC通道,则可以在ICA主机上运行的应用程序和ICA客户机上运行的DVC侦听器之间交换数据消息。发送和接收消息在主机和客户机之间是对称的,任何一方都可以发起发送消息。
    1. 最后,DVC通道由主机或客户机关闭。当主机应用程序关闭DVC实例句柄时,会触发关闭,但是也可以由客户机侦听器或客户机或主机DVC管理器(例如,在出现错误时)触发。
    1. 所有的DVC包都是通过SVC包发送的。
    1. 当ICA客户机存在时,客户机DVC管理器将关闭并卸载所有DVC插件,而这些插件又将关闭它们的侦听器。

命名静态虚拟通道

Channel Name字段提供用于特定DVC插件的静态虚拟通道的名称。默认情况下,使用的静态通道名称将使用DVC插件的模块文件名自动生成。为了确保生成唯一的名称,在发生冲突时,可以在名称的末尾使用一个或两个数字,以使其惟一,同时将名称的长度保持在最多7个字符。通道名称字段的解释如下:

  • Section: ChannelName
  • Feature: DVC
  • Attribute Name: INI_DVC_PLUGIN_<DVC plugin name>
  • Definition location: inc\icaini.h
  • Data Type: String
  • Access Type: Read
  • Unix Specific: No
  • Present in ADM: No

如果管理员希望提供显式名称,可以使用上述位置修改静态虚拟通道名称。

在ICA上编写DVC组件的步骤

本节将通过一个示例说明如何在ICA上编写DVC组件。Citrix DVC协议使用微软提供的现有接口在ICA上开发DVC组件。有关如何编写DVC服务器组件和DVC客户端组件的详细信息,请参阅:

服务端DVC插件例子

客户端DVC插件例子

下面的c++代码示例提供了一个如何创建服务器端动态虚拟通道(DVC)模块的示例。

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
254
255
256
257
#include <windows.h>
#include <wtsapi32.h>
#include <pchannel.h>
#include <crtdbg.h>

#pragma comment(lib, "wtsapi32.lib")

#define _MAX_WAIT       60000
#define MAX_MSG_SIZE    0x20000
#define START_MSG_SIZE  4
#define STEP_MSG_SIZE   113

DWORD OpenDynamicChannel(LPCSTR szChannelName, HANDLE *phFile);
DWORD WINAPI WriteThread(PVOID param);
DWORD WINAPI ReadThread(PVOID param);

INT _cdecl wmain(INT argc, __in_ecount( argc ) WCHAR **argv)
{
    DWORD rc;
    HANDLE hFile;

    rc = OpenDynamicChannel ( "DVC_Sample", &hFile );
    if ( ERROR_SUCCESS != rc )
    {
        return 0;
    }

    DWORD dwThreadId;
    HANDLE hReadThread = CreateThread(
        NULL,
        0,
        ReadThread,
        hFile,
        0,
        &dwThreadId );

    HANDLE hWriteThread = CreateThread(
        NULL,
        0,
        WriteThread,
        hFile,
        0,
        &dwThreadId );

    HANDLE ah[] = {hReadThread, hWriteThread};
    WaitForMultipleObjects(2, ah, TRUE, INFINITE );

    CloseHandle( hReadThread );
    CloseHandle( hWriteThread );
    CloseHandle( hFile );

    return 1;
}

/*
*  Open a dynamic channel with the name given in szChannelName.
*  The output file handle can be used in ReadFile/WriteFile calls.
*/
DWORD OpenDynamicChannel(
    LPCSTR szChannelName, 
    HANDLE *phFile )
{
    HANDLE hWTSHandle = NULL;
    HANDLE hWTSFileHandle;
    PVOID vcFileHandlePtr = NULL;
    DWORD len;
    DWORD rc = ERROR_SUCCESS;

    hWTSHandle = WTSVirtualChannelOpenEx(
        WTS_CURRENT_SESSION,
        (LPSTR)szChannelName,
        WTS_CHANNEL_OPTION_DYNAMIC );
    if ( NULL == hWTSHandle )
    {
        rc = GetLastError();
        goto exitpt;
    }

    BOOL fSucc = WTSVirtualChannelQuery(
        hWTSHandle,
        WTSVirtualFileHandle,
        &vcFileHandlePtr,
        &len );
    if ( !fSucc )
    {
        rc = GetLastError();
        goto exitpt;
    }
    if ( len != sizeof( HANDLE ))
    {
        rc = ERROR_INVALID_PARAMETER;
        goto exitpt;
    }

    hWTSFileHandle = *(HANDLE *)vcFileHandlePtr;

    fSucc = DuplicateHandle(
        GetCurrentProcess(),
        hWTSFileHandle,
        GetCurrentProcess(),
        phFile,
        0,
        FALSE,
        DUPLICATE_SAME_ACCESS );
    if ( !fSucc )
    {
        rc = GetLastError();
        goto exitpt;
    }

    rc = ERROR_SUCCESS;

exitpt:
    if ( vcFileHandlePtr )
    {
        WTSFreeMemory( vcFileHandlePtr );
    }
    if ( hWTSHandle )
    {
        WTSVirtualChannelClose( hWTSHandle );
    }

    return rc;
}

/* 
*  Write a series of random messages into the dynamic virtual channel.
*/
DWORD WINAPI WriteThread(PVOID param)
{
    HANDLE  hFile;
    BYTE    WriteBuffer[MAX_MSG_SIZE];
    DWORD   dwWritten;
    BOOL    fSucc;
    BYTE    b = 0;
    HANDLE  hEvent;

    hFile = (HANDLE)param;

    hEvent = CreateEvent( NULL, FALSE, FALSE, NULL );

    for( ULONG msgSize = START_MSG_SIZE; 
        msgSize < MAX_MSG_SIZE; 
        msgSize += STEP_MSG_SIZE )
    {
        OVERLAPPED  Overlapped = {0};
        Overlapped.hEvent = hEvent;

        for( ULONG i = 0; i < msgSize; i++, b++ )
        {
            WriteBuffer[i] = b;
        }

        fSucc = WriteFile(
            hFile,
            WriteBuffer,
            msgSize,
            &dwWritten,
            &Overlapped );
        if ( !fSucc )
        {
            if ( GetLastError() == ERROR_IO_PENDING )
            {
                DWORD dw = WaitForSingleObject( Overlapped.hEvent, _MAX_WAIT );
                _ASSERT( WAIT_OBJECT_0 == dw );
                fSucc = GetOverlappedResult(
                    hFile,
                    &Overlapped,
                    &dwWritten,
                    FALSE );
            }
        }

        if ( !fSucc )
        {
            DWORD error = GetLastError();
            return error;
        }

        _ASSERT( dwWritten == msgSize );
    }

    return 0;
}

/* 
*  Read the data from the dynamic virtual channel reconstruct the original 
*  message and verify its content.
*/
DWORD WINAPI ReadThread(PVOID param)
{
    HANDLE hFile;
    BYTE ReadBuffer[CHANNEL_PDU_LENGTH];
    DWORD dwRead;
    BYTE b = 0;
    CHANNEL_PDU_HEADER *pHdr;
    BOOL fSucc;
    HANDLE hEvent;

    hFile = (HANDLE)param;
    pHdr = (CHANNEL_PDU_HEADER*)ReadBuffer;

    hEvent = CreateEvent( NULL, FALSE, FALSE, NULL );

    for( ULONG msgSize = START_MSG_SIZE; 
        msgSize < MAX_MSG_SIZE; 
        msgSize += STEP_MSG_SIZE )
    {
        OVERLAPPED  Overlapped = {0};
        DWORD TotalRead = 0;
        do {
            Overlapped.hEvent = hEvent;
            
            // Read the entire message.
            fSucc = ReadFile(
                hFile,
                ReadBuffer,
                sizeof( ReadBuffer ),
                &dwRead,
                &Overlapped );
            if ( !fSucc )
            {
                if ( GetLastError() == ERROR_IO_PENDING )
                {
                    DWORD dw = WaitForSingleObject( Overlapped.hEvent, INFINITE );
                    _ASSERT( WAIT_OBJECT_0 == dw );
                    fSucc = GetOverlappedResult(
                        hFile,
                        &Overlapped,
                        &dwRead,
                        FALSE );
                }
            }

            if ( !fSucc )
            {
                DWORD error = GetLastError();
                return error;
            }

            ULONG packetSize = dwRead - sizeof( *pHdr );
            TotalRead += packetSize;
            PBYTE pData = (PBYTE)( pHdr + 1 );
            for ( ULONG i = 0; i < packetSize; pData++, i++, b++ )
            {
                _ASSERT( *pData == b );
            }

            _ASSERT( msgSize == pHdr->length );

        } while( 0 == ( pHdr->flags & CHANNEL_FLAG_LAST ));

        _ASSERT( TotalRead == msgSize );
    }

    return 0;
}

下面的c++代码示例提供了如何创建远程桌面连接(RDC)客户机动态虚拟通道(DVC)插件的示例。

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
/*
 *  Sample "echo" plugin that listens on endpoint "DVC_Sample".
 *
 */
#include "stdafx.h"
#include "resource.h"
#include "DVCPlugin_i.h"
#include <tsvirtualchannels.h>

using namespace ATL;

#define CHECK_QUIT_HR( _x_ )    if(FAILED(hr)) { return hr; }

// CDVCSamplePlugin implements IWTSPlugin.
class ATL_NO_VTABLE CDVCSamplePlugin :
    public CComObjectRootEx<CComMultiThreadModel>,
    public CComCoClass<CDVCSamplePlugin, &CLSID_DVCSamplePlugin>,
    public IWTSPlugin
{
public:

    DECLARE_REGISTRY_RESOURCEID(IDR_PLUGIN)

    BEGIN_COM_MAP(CDVCSamplePlugin)
        COM_INTERFACE_ENTRY(IWTSPlugin)
    END_COM_MAP()

    DECLARE_PROTECT_FINAL_CONSTRUCT()

    HRESULT FinalConstruct()
    {
        return S_OK;
    }

    void FinalRelease()
    {
    }

    // IWTSPlugin.
    //
    HRESULT STDMETHODCALLTYPE
        Initialize(IWTSVirtualChannelManager *pChannelMgr);

    HRESULT STDMETHODCALLTYPE
        Connected() 
    {
        return S_OK; 
    }

    HRESULT STDMETHODCALLTYPE
        Disconnected(DWORD dwDisconnectCode)
    {
        // Prevent C4100 "unreferenced parameter" warnings.
        dwDisconnectCode;

        return S_OK; 
    }

    HRESULT STDMETHODCALLTYPE
        Terminated()
    {
        return S_OK; 
    }

};

OBJECT_ENTRY_AUTO(__uuidof(DVCSamplePlugin), CDVCSamplePlugin)

// CSampleChannelCallback implements IWTSVirtualChannelCallback.
class ATL_NO_VTABLE CSampleChannelCallback :
    public CComObjectRootEx<CComMultiThreadModel>,
    public IWTSVirtualChannelCallback
{
    CComPtr<IWTSVirtualChannel> m_ptrChannel;

public:

    BEGIN_COM_MAP(CSampleChannelCallback)
        COM_INTERFACE_ENTRY(IWTSVirtualChannelCallback)
    END_COM_MAP()

    VOID SetChannel(IWTSVirtualChannel *pChannel)
    {
        m_ptrChannel = pChannel;
    }

    // IWTSVirtualChannelCallback
    //
    HRESULT STDMETHODCALLTYPE
        OnDataReceived(
        ULONG cbSize,
        __in_bcount(cbSize) BYTE *pBuffer
        )
    {
        return m_ptrChannel->Write(cbSize, pBuffer, NULL); 
    }

    HRESULT STDMETHODCALLTYPE
        OnClose()
    { 
        return m_ptrChannel->Close(); 
    }
};


// CSampleListenerCallback implements IWTSListenerCallback.
class ATL_NO_VTABLE CSampleListenerCallback :
    public CComObjectRootEx<CComMultiThreadModel>,
    public IWTSListenerCallback
{
public:

    BEGIN_COM_MAP(CSampleListenerCallback)
        COM_INTERFACE_ENTRY(IWTSListenerCallback)
    END_COM_MAP()

    HRESULT STDMETHODCALLTYPE
        OnNewChannelConnection(
        __in IWTSVirtualChannel *pChannel,
        __in_opt BSTR data,
        __out BOOL *pbAccept,
        __out IWTSVirtualChannelCallback **ppCallback );
};

// IWTSPlugin::Initialize implementation.
HRESULT
    CDVCSamplePlugin::Initialize(
    __in IWTSVirtualChannelManager *pChannelMgr
    )
{
    HRESULT hr; 
    CComObject<CSampleListenerCallback> *pListenerCallback;
    CComPtr<CSampleListenerCallback> ptrListenerCallback;
    CComPtr<IWTSListener> ptrListener;

    // Create an instance of the CSampleListenerCallback object.
    hr = CComObject<CSampleListenerCallback>::CreateInstance(&pListenerCallback);
    CHECK_QUIT_HR("CSampleListenerCallback::CreateInstance");
    ptrListenerCallback = pListenerCallback;

    // Attach the callback to the "DVC_Sample" endpoint.
    hr = pChannelMgr->CreateListener( 
        "DVC_Sample", 
        0, 
        (CSampleListenerCallback*)ptrListenerCallback, 
        &ptrListener);
    CHECK_QUIT_HR("CreateListener");

    return hr;
}

// IWTSListenerCallback::OnNewChannelConnection implementation.
HRESULT
    CSampleListenerCallback::OnNewChannelConnection(
    __in IWTSVirtualChannel *pChannel,
    __in_opt BSTR data,
    __out BOOL *pbAccept,
    __out IWTSVirtualChannelCallback **ppCallback )
{
    HRESULT hr;
    CComObject<CSampleChannelCallback> *pCallback;
    CComPtr<CSampleChannelCallback> ptrCallback;

    // Prevent C4100 "unreferenced parameter" warnings.
    data;

    *pbAccept = FALSE;

    hr = CComObject<CSampleChannelCallback>::CreateInstance(&pCallback);
    CHECK_QUIT_HR("CSampleChannelCallback::CreateInstance");
    ptrCallback = pCallback;

    ptrCallback->SetChannel(pChannel);

    *ppCallback = ptrCallback;
    (*ppCallback)->AddRef();

    *pbAccept = TRUE;

    return hr;
}

使用示例程序

虚拟通道SDK中包含的示例程序是可构建的、工作的虚拟通道。用这些例子可以:

  • 通过构建一个已知的工作示例程序来验证虚拟通道SDK安装是否正确。
  • 提供可以修改以适应您的需求的代码的工作示例。
  • 探索虚拟通道SDK中提供的特性和功能。

每个示例程序都包含一个客户端虚拟驱动一个服务器应用程序。服务器端应用程序从ICA会话中的命令行运行。单个虚拟通道包含一个应用程序对

虚拟通道SDK包含的示例程序如下:

  • Ping:记录通过虚拟通道发送的测试包的往返延迟时间。
  • Mix:演示在远程客户端上调用函数(例如获取时间)的机制。
  • Over:简单的异步应用程序,演示如何编写应用程序,其中服务器必须异步地接收来自客户端的响应,并且发送到客户端的包的类型与接收到的包的类型不同。

每个示例都包含程序、包格式和其他必要信息的描述。

Ping

Ping是一个简单的程序,它记录通过虚拟通道发送的测试包的往返延迟时间。服务器向客户端发送一个数据包,客户端用一个包含它从服务器收到原始数据包的时间的数据包进行响应。这个序列重复指定的次数,然后程序显示每个ping的往返时间和平均往返延迟时间。

在这个例子中,BEGIN包和END包之间没有明显的区别。提供这两种类型的包作为编写自己的虚拟通道协议的示例。

这个程序演示了:

  • 如何同步传输数据。事件的顺序是:{SrvWrite, ClntReadClntWrite, SrvRead} {SrvWrite, ClntRead}{…}。服务器在发送下一个数据包之前等待客户端回复。
  • 如何从Module.ini文件中读取参数数据(在本例中是向客户端发送数据包的次数)。

Ping使用SendData函数立即传输数据,而不是等待轮询。

包格式

下面的包在客户端和服务器之间交换:

1
2
3
4
5
6
7
8
9
typedef struct PING
{
USHORT uSign; // Signature
USHORT uType; // Type, BEGIN or END, from server
USHORT uLen; // Packet length from server
USHORT uCounter; // Sequencer
ULONG ulServerMS; // Server millisecond clock
ULONG ulClientMS; // Client millisecond clock
} PING, *PPING;

Mix

Mix演示了一种可用于调用远程客户端上的函数(例如获取时间)的机制。这个程序演示了一个可扩展的方案,用于从服务器向客户端进行函数调用,允许服务器指定何时需要客户端响应,何时不需要。这种方法可以提高性能,因为服务器不必一直等待客户端的回复。

服务器调用一系列简单的函数:

  • AddNo:添加两个数字并返回和作为返回值
  • DispStr:向日志文件中写入一个字符串。没有返回值(write-only函数)
  • Gettime:读取客户端时间并将其作为返回值返回。这些函数的实际实现在客户端。根据所执行的函数,服务器有条件地等待来自客户端的响应。例如,服务器等待AddNoGettime函数的结果,而不是只写的函数DispStr的结果,它不会返回任何结果。

包格式

1
2
3
4
5
6
7
8
9
10
typedef struct MIXHEAD
{
USHORT uType; // Packet type
USHORT uFunc; // Index of Function
ULONG uLen; // Length of data
USHORT fRetReq; // True if return value required
ULONG dwRetVal; // Return Value from client
USHORT dwLen1; // length of data for #1 LpVoid
USHORT dwLen2; // length of data for #2 LpVoid
} MIXHEAD, *PMIXHEAD;

数据由上面的结构和被调用函数的参数组成。uLen是被发送数据的总长度,包括参数。dwLen1是指针参数指向的数据长度。

事件序列

Mix程序演示了以下事件序列。请看下一页的图表:

该图演示了从顶部开始使用Mix程序时发生的事件序列。

Over

Over是一个简单的异步应用程序。它演示了如何编写一个应用程序,在这个应用程序中,服务器必须异步地接收来自客户端的响应,并且发送到客户端的包的类型与接收到的包的类型不同。

当Over程序运行起来,它会:

  • 派生一个等待客户端响应的线程。
  • 开始向客户端发送带有序列号的数据包
  • (发送最后一个数据包后)发送一个序列号为NO_MORE_DATA的数据包,然后关闭连接。

客户端接收数据包并检查序列号。对于每个能被10整除的序列号,客户端将序列号增加7并向服务器发送响应。这些数字是任意选择的,以演示客户端可以在任何时候异步地向服务器发送数据

用于从服务器向客户端发送数据的包类型与用于从客户端接收数据的包类型不同。

包格式- 从服务器到客户端

1
2
3
4
5
6
7
8
typedef struct OVER
{
USHORT uSign; // Signature
USHORT uType; // Type, BEGIN or END, from server
USHORT uLen; // Packet length from server
USHORT uCounter; // Sequencer
ULONG ulServerMS; // Server millisecond clock
} OVER, *POVER;

包格式 - 从客户端到服务器

1
2
3
4
5
6
typedef struct DRVRESP
{
USHORT uType; // Type OVERFLOW_JUMP from client
USHORT uLen; // Packet length from client
USHORT uCounter; // seqUencer
} DRVRESP, * PDRVRESP;

事件序列

该图演示了从顶部开始使用Over程序时发生的事件序列。

构建例子

使用Visual Studio或.NET构建服务器端示例

  • 创建一个新的Win32控制台项目。Citrix建议将项目名称与示例相关联(例如,ctxping)。您可以将项目的位置设置为src\examples\vc\server目录,以便.c源文件随时可用。

  • 添加以下目录,以在项目设置中包含c++预处理器的搜索路径(其中vcsdk是安装虚拟通道SDK的目录)

    • vcsdk\src\examples\vc\shared\inc
    • vcsdk\src\shared\inc\Citrix
  • 指向wfapi的include和library路径。 从vcsdk安装路径\src\examples\build中打开文件wfapi.mak :

- 将WFAPILIB设置为WFAPI的library目录的完整路径
1
- 将`WFAPIINC`设置为WFAPI的`include`目录的完整路径。

WFAPI SDK会将 Wfapi.lib 安装到指定的library目录。

使用Visual Studio为Win32构建一个客户端示例

  • 切换到目录 <vcsdk_unzipped_location>\src\examples\vc\
  • 在命令提示符处,设置一个环境变量 :WFAPILIBPATH = C:\Program Files (x86)\Citrix\WfApiSDK 或者 WFAPILIBPATH = C:\Program Files\Citrix\WfApiSDK,视架构而定
  • 对于要构建的每个示例,键入:

    1
    2
    
      cd client
      msbuild client_examples_win32.sln /p:Configuration=Release /p:Platform=win32 /verbosity:detailed
    

准备和部署虚拟驱动

在客户端安装虚拟驱动之前,将平台的虚拟驱动复制到客户端设备并配置客户端MSI。

//TODO

运行示例的虚拟通道

  • 在使用客户端示例配置的客户端上,使用关联的服务器端示例连接到运行XenApp的服务器。

  • 在ICA会话(session)中,运行服务器端可执行文件。

服务器端示例查询客户端虚拟驱动,然后显示驱动信息。使用-d参数显示详细信息。

针对Ping: CTXPING发送单独的Ping计数。PingCount的默认值是3,但是可以在Module.ini文件的[Ping]部分中设置。每个ping由一个BEGIN包和一个END包组成。

调试Win32虚拟驱动

使用TRACE功能在客户端记录事件。要启用TRACE语句,必须构建虚拟驱动的debug版本。当调试模块安装在客户端上时,TRACE语句将调试信息写入文件。

在运行时,您可以指定跟踪哪个类和事件标志。这允许您只跟踪您需要的部分,最小化的降低性能。

虚拟通道的类标志是00000080。有关类和事件标志的完整列表,请参见Logflags.h(在src\inc\中)。

  • 为客户端平台编译虚拟驱动的调试版本。
  • 如果正在运行,请关闭客户端设备上的客户端
  • 将库的已编译调试版本复制到客户端设备上客户端安装的位置的目录中。比如说,对于Ping示例程序,就拷贝 VdpingN.dllC:\Program Files\Citrix\ICA Client
  • 切换到包含客户端和类型的目录:wfcrun32 connection /c:xxxxxxxx /e:yyyyyyyy /logfile:filename 其中
    • connection是远程应用管理器(Remote Application Manager)的连接的名字
    • xxxxxxxx是你想记录的事件标志
    • yyyyyyyy 是你想记录的类标志
    • filename是要将日志保存到的文件的相对路径

客户端将Appsrv.ini文件存储在每个用户的配置文件目录中。当使用事件日志( event logging)启动ICA会话(session)时,添加/iniappsrv:%userprofile%\”application data”\icaclient\appsrv.ini 到命令行的末尾。

远程部署客户端虚拟通道

要远程部署虚拟通道,请根据以下管理模板(.adm)文件进行更改。 CustomVC是虚拟通道的通道名的占位符。

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
;Group Policy template for Citrix Online Plug-in.
;Citrix Online Plug-in Client Extensions template
;Description:
;This file is provided as a base for third-party extensions
;to the Citrix Online Plug-in client.
;Copyright (C) Citrix Systems, Inc. All Rights Reserved.
;
CLASS MACHINE
CATEGORY !!Citrix
	CATEGORY !!ICAClient
		CATEGORY !!Third Party
			#if version >= 4
			EXPLAIN !!Explain_Third Party
			#endif
			; Continued below...
			
			; Continued from above
			; Remotely define virtual channel
			POLICY !!Policy_CustomVirtualChannel
				EXPLAIN !!Explain_CustomVirtualChannel
				KEYNAME "Software\Policies\Citrix\ICA Client\Engine\Lockdown
				Profiles\All Regions\Lockdown\Virtual Channels\Third
				Party\CustomVC"
				VALUENAME "VCEnable"
				VALUEON"true,false" VALUEOFF "false" ACTIONLISTON
				KEYNAME"Software\Citrix\ICA Client\Engine\Lockdown
				Profiles\All Regions\Lockdown\Virtual Channels\Third
				Party\CustomVC"
					VALUENAME "VCEnable"
					VALUE""
					KEYNAME "Software\Citrix\ICA
				Client\Engine\Configuration\Advanced\Modules\ICA 3.0"
					VALUENAME "VirtualDriverEx"
					VALUE"CustomVC"
					KEYNAME "Software\Citrix\ICA
				Client\Engine\Configuration\Advanced\Modules\CustomVC"
					VALUENAME "DriverName"
					VALUE"Unsupported"
					KEYNAME "Software\Citrix\ICA
				Client\Engine\Configuration\Advanced\Modules\CustomVC"
					VALUENAME "DriverNameWin16"
					VALUE"Unsupported"
					KEYNAME "Software\Citrix\ICA
				Client\Engine\Configuration\Advanced\Modules\CustomVC"
					VALUENAME
					"DriverNameWin32" VALUE
					"VDCustomVC.DLL"
					END ACTIONLISTON
					ACTIONLISTOFF
						KEYNAME "Software\Citrix\ICA
				Client\Engine\Configuration\Advanced\Modules\ICA 3.0"
					VALUENAME
					"VirtualDriverEx" VALUE""
					END ACTIONLISTOFF
			END POLICY
		END CATEGORY
	END CATEGORY
END CATEGORY
[strings]
Citrix="Citrix Components"
ICAClient="Presentation Server Client"
Third Party="Extensions"
Explain_Third Party="These policies control extensions to the
		standard Citrix Presentation Server Client."
Policy_CustomVirtualChannel="Additional Virtual Channel"
Explain_CustomVirtualChannel=" This policy controls a virtual
		channel.\n\nSupplier:\nMy company.\n\nReference:"

Ping示例的管理模板更改

对于ping虚拟通道示例,按如下方式编辑.adm模板文件(文本中的更改为黑体)。

注意:Memory INI函数需要引用VCEnable的例子中的行。虚拟通道使用的每个参数都必须出现在这个文件中。客户端使用这些来对虚拟通道设置安全限制。

最佳实践

Citrix建议使用.adm文件定制组策略GUI(GroupPolicy GUI)的以下部分:

  • 指定一个名称(模板文件中的附加虚拟通道)来描述虚拟通道提供的功能。
  • 解释说明
  • 供应方
  • 参考(例如,添加URL或电子邮件地址以访问更多信息)
  • 在双击策略时出现的GUI(可选的,也可根据需要)。不要更改Citrix Components\Presentation Server Client\Extensions文件夹的名称。

使用现有的管理工具远程部署虚拟通道DLL,并使用上面的GUI启用和配置。您可以对组织内的整个计算机组执行此操作。

编程指南

虚拟通道由一个7个字符(或更短)的ASCII名称引用。在以前的几个ICA协议版本中,对虚拟通道进行了编号;数字现在是根据ASCII名称动态分配的,这使得实现更加容易。

在开发仅供内部使用的虚拟通道代码时,可以使用与现有虚拟通道不冲突的任何7个字符的名称。只使用大写和小写的ASCII字母和数字。在添加自己的虚拟通道时,请遵循现有的命名约定。

预定义的通道以OEM标识符CTX开头,仅供Citrix使用。

设计建议

遵循这些建议,可使您的虚拟通道更容易设计和增强

  • 在设计自己的虚拟通道协议时,要考虑添加特性的灵活性。虚拟通道具有在初始化期间交换的版本号,以便客户端和服务器都能检测到可以使用的最大功能级别。例如,如果客户端在版本3,而服务器在版本5,服务器不会发送任何功能超过版本3的包,因为客户端不知道如何解释新的包。

    • 因为虚拟通道协议的服务器端可以作为一个单独的进程实现,所以在服务器上编写使用citrix提供的虚拟通道支持进行接口的代码要比在客户端上编写容易得多(在客户端上,代码必须适合现有的代码结构)。虚拟通道的服务器端简单地打开通道,对通道进行读写操作,完成后关闭通道。
    • 为服务器端编写代码与编写应用程序类似,后者使用系统导出的服务。编写一个应用程序来处理虚拟通道通信比较容易,因为它可以为每个支持虚拟通道的ICA连接运行一次。
    • 为客户端编写类似于编写驱动,除了使用系统服务外,驱动还必须向系统提供服务。如果编写了一个服务,它必须管理多个连接。
  • 如果您正在设计用于新的虚拟通道的新硬件(例如,一种改进的压缩视频格式),请确保能够检测到硬件,以便客户端能够确定是否安装了硬件。如果在服务器使用新数据格式之前硬件可用,则客户端可以与服务器通信。或者,您可以让虚拟驱动转换新的数据格式,以便与旧的硬件一起使用。

  • 可能会有一些限制阻止您的新虚拟通道以最佳水平执行。如果客户端通过调制解调器或串行连接连接到运行XenApp的服务器,带宽可能不够大,无法正确支持音频或视频数据。您可以使您的协议具有自适应能力,因此当带宽减少时,性能会优雅地下降,可能通过正常发送声音而降低视频的帧速率来适应可用的带宽。

  • 要确定问题发生在何处(连接、实现或协议),首先要使连接和通信工作。然后,在虚拟通道完成并调试之后,进行一些时间试验并记录结果。这些结果为测量进一步的优化(如压缩和其他增强)奠定了基础,从而使通道所需的带宽更少。

  • pVdPoll变量中的时间戳有助于解决虚拟驱动中的计时问题。它是一个包含当前时间(以毫秒为单位)的ULONGpVdPoll变量是指向DLLPOLLDLL_HPC_POLL结构的指针。有关这些结构的定义,请参见Dllapi.h(在src\inc\中)。

服务端函数概述

服务器端函数是指向由ICAsubsystem在XenApp或XenDesktop服务器上提供的虚拟通道服务的入口点。Wfapi.h 包含常量和函数原型。

使用这些函数来打开和关闭虚拟通道,以及读取、写入、查询和清除传入或传出的数据。

函数调用约定中的“IN”和“OUT”仅供说明。它们在Windef.h中定义为空格符。如果您不能访问Windef.h,将下列内容添加到项目的头文件中:

1
2
3
4
5
6
#ifndef IN
#define IN
#endif
#ifndef OUT
#define OUT
#endif
函数 描述
WFVirtualChannelClose 关闭一个虚拟通道句柄
WFVirtualChannelOpen 打开一个指定的虚拟通道句柄
WFVirtualChannelPurgeInput 清除从客户端发送到特定虚拟通道上的服务器的所有排队输入数据
WFVirtualChannelPurgeOutput 清除从服务器发送到特定虚拟通道上的客户端的所有排队输出数据
WFVirtualChannelQuery 返回一个虚拟通道相关的数据
WFVirtualChannelRead 从虚拟通道读取数据
WFVirtualChannelWrite 向虚拟通道写数据

客户端函数概述

客户端软件构建在模块化的可配置体系结构上,允许可替换的、可配置的模块(如虚拟通道驱动)处理ICA连接的各个方面。这些模块经过特殊的格式化并可动态加载。为了实现这种模块化功能,每个模块(包括虚拟通道驱动)实现一组固定的功能入口点

有三组函数:

  • 用户定义的函数
  • 虚拟驱动辅助函数
  • 内存INI函数。

用户定义函数

为了使编写虚拟通道更容易,动态加载由WinStation驱动处理,而WinStation驱动又调用用户定义的函数。这简化了虚拟通道的创建,因为您所要做的就是填充函数并将您的虚拟通道驱动与Vdapi.lib链接起来。(由这个SDK提供)。

函数 描述
DriverClose 释放私有驱动数据。在卸载虚拟驱动之前调用(通常在客户端退出时)。
DriverGetLastError 返回虚拟驱动设置的最后一个错误。未使用; 与通用前端VDAPI的链接。
DriverInfo 获取虚拟驱动的信息
DriverOpen 执行虚拟驱动的所有初始化。在客户端加载虚拟驱动时调用一次(在启动时)
DriverPoll 允许驱动检查计时器和其他状态信息,将排队的数据发送到服务器,并执行任何其他需要的处理。定期调用,以查看虚拟驱动是否有要写入的数据。
DriverQueryInformation 从虚拟驱动获取运行时信息。
DriverSetInformation 设置虚拟驱动中的运行时信息
ICADataArrival 指示数据已交付。当数据到达虚拟信道时调用

虚拟驱动辅助函数

虚拟驱动使用辅助函数来发送数据和管理虚拟通道。当WinStation驱动初始化虚拟驱动时,WinStation驱动将指针传递给辅助函数,而虚拟驱动将指针传递给用户定义的函数。

VdCallWd作为VDAPI的一部分被链接进来,并且可以在所有用户实现的函数中使用。其他参数是在使用WDxSETINFORMATION参数调用VdCallWd时的DriverOpen期间获得的。

虚拟通道驱动可以通过在驱动打开期间获得的SendDataQueueVirtualWrite函数从私有缓冲区发送数据。如果WinStation驱动本身不能缓冲数据,这两个函数中的任何一个都可能拒绝接受数据。然后,该通道需要在下一次驱动轮询时机,重试发送操作。

函数 描述
☆SendData 向服务器发送一个信道协议包,附带一个通知选项
QueueVirtualWrite 向服务器发送一个信道协议包。这是一个遗留功能。对新的虚拟驱动使用上面的SendData
☆VdCallWd 用于从WinStation驱动(WD)查询和设置信息

内存INI函数

内存INI函数读取客户端引擎从注册表中的配置存储中读取并存储在内存INI结构中的数据。 必须使用这些函数,因为一些客户端设备将这些信息存储在ROM中,只有引擎可以访问这些INI数据。内存INI函数从这个内存INI结构中读取值,就像从配置存储中读取值一样。用于指定虚拟通道的配置存储在Software\Citrix\ICA Client\Configuration\Advanced\Modules\中。

重要:根据安全限制,对配置数据的访问可能受到限制。特别是,虚拟通道可能无法访问ICA文件的所有内容。这是由 HKEY_LOCAL_MACHINE\SOFTWARE \Citrix\ICA Client\Engine\Lock down Profiles\All Regions\Lock down 这个注册表键来控制的。您可以使用组策略文件来修改注册表项。

函数 描述
miGetPrivateProfileBool 返回一个bool
miGetPrivateProfileInt 返回一个integer
miGetPrivateProfileLong 返回一个long
miGetPrivateProfileString 返回一个string

编程参考


DriverClose

当ICA连接终止时,WinStation驱动在卸载虚拟驱动之前调用此函数。

调用约定

INT Driverclose( PVD pVD, PDLLCLOSE pVdClose, PUINT16 puiSize);

参数

  • pVD: 指向虚拟驱动控制结构体的指针
  • pVdClose: 指向一个标准的驱动关闭信息结构。
  • puiSize: 指向驱动的关闭信息结构的大小的指针。这是一个输入参数。

返回值

如果函数成功,则返回值为CLIENT_STATUS_SUCCESS。 如果函数失败,则返回值为对应于错误条件的 CLIENT_ERROR_*。有关以CLIENT_ERROR开头的错误值列表,请参见Clterr.h(在src\inc\中)。

注释

DriverClose被调用时,所有的私有驱动数据被释放。虚拟驱动不需要释放虚拟通道或写钩子。 pVdClose结构体当前包含一个NotUsed的元素。这个结构体可以忽略。

DriverGetLastError

此函数未被使用,但可用于链接公共前端,VDAPI。

调用约定

INT DriverGetLastError(PVD pVD,PVDLASSTERROR pVdLastError);

参数

  • pVD: 指向虚拟驱动控制结构体的指针
  • pVdLastError: 指向接收最后错误信息的结构体的指针

返回值

驱动返回CLIENT_STATUS_SUCCESS

注释

该功能目前对虚拟驱动没有实际意义;它提供了与可加载模块接口的兼容性。


DriverInfo

获取有关虚拟驱动的信息,如驱动的版本级别。

调用约定

INT DriverInfo(PVD pVD, PDLLINFO pVdInfo, PUINT16 puiSize);

参数

  • pVD: 指向虚拟驱动控制结构体的指针
  • pVdInfo: 指向一个标准的驱动闭信息结构体的指针。
  • puiSize: 指向驱动的信息结构体的大小的指针。这是一个输出参数。

返回值

如果函数成功,则返回CLIENT_STATUS_SUCCESS 如果函数失败是因为pVdInfo指向的缓冲区太小,则返回CLIENT_ERROR_BUFFER_TOO_SMALL。通常,当一个 CLIENT_ERROR_* 结果码返回时,ICA会话也会断开连接。CLIENT_ERROR_BUFFER_TOO_SMALL 是一个异常,不会导致ICA会话断开。相反,WinStation驱动尝试使用失败调用返回的pVdInfoByteCount再次调用DriverInfo

注释

当客户端启动时,它调用这个函数来检索特定于模块的信息,以便传输到主机。此信息通过WFVirtualChannelQuery返回到虚拟通道的服务器端。

虚拟驱动必须通过在pVdInfo缓冲区中返回一个结构体来支持这个调用。这种结构可以是开发人员定义的特定于虚拟通道的结构,但是它必须以VD_C2H结构体开头,而VD_C2H结构又必须以MODULE_C2H结构体开头。除了ChannelMask字段外的所有VD_C2H结构体字段都要填写。有关这些结构的定义,请参见ica-c2h.h(在src\inc\中)。

虚拟驱动必须首先检查给定的信息缓冲区的大小与虚拟驱动所需的大小(VD C2H结构)。输入缓冲区的大小以pVdInfo->ByteCount表示。

如果缓冲区太小,无法存储驱动需要发送的信息,则会在ByteCount字段中填写正确的大小,而驱动返回CLIENT_ERROR_BUFFER_TOO_SMALL。

如果缓冲区足够大,驱动必须使用模块定义的结构体来填充它。至少,该结构体必须包含VD C2H结构体。VD C2H结构必须是缓冲区中的第一个数据;其他特定于通道的数据可以随后提供。该结构的所有相关字段都由该函数填充。流量控制方法在VDFLOW结构体(VD C2H结构的一个元素)中指定。Ping示例包含一个流控制选择。

在调用DriverOpen之后,WinStation驱动在初始化时调用这个函数两次。第一个调用包含一个NULL信息缓冲区和一个大小为0的 缓冲区。该驱动将使用所需的缓冲区大小填充pVdInfo->ByteCount,并返回CLIENT_ERROR_BUFFER_TOO_SMALL。WinStation驱动分配该大小的缓冲区并重试操作。

虚拟驱动器不能改变pVdinfo->pBuffer所指向的数据缓冲区。 WinStation驱动将字节交换信息存储在这个缓冲区中。

参数puiSize必须初始化为驱动信息结构的大小。


DriverOpen

初始化虚拟驱动。客户端引擎在加载客户端时调用此用户编写的函数一次。

调用约定

INT DriverOpen(PVD pVD, PVDOPEN pVdOpen. PUINT16 puiSize);

参数

  • pVD: 指向虚拟驱动控制结构体的指针
  • pVdOpen: 指向一个标准的驱动Open结构体的指针。
  • puiSize: 指向驱动的Open结构体的大小的指针。这是一个输出参数。

返回值

如果成功,则返回CLIENT_STATUS_SUCCESS 如果函数失败,则返回值为对应于错误条件的 CLIENT_ERROR_*。有关以CLIENT_ERROR开头的错误值列表,请参见Clterr.h(在src\inc\中)。

注释

本节中的代码片段取自vdping示例。

DriverOpen函数必须:

  1. 分配一个虚拟通道

填写WDQUERYINFORMATION结构并调用VdCallWd。WinStation驱动在pVd中填写OpenVirtualChannel结构(包括通道号)和数据

1
2
3
4
5
6
7
8
WDQUERYINFORMATION wdqi;
OPENVIRTUALCHANNEL OpenVirtualChannel;
wdqi.WdInformationClass = WdOpenVirtualChannel;
wdqi.pWdInformation = &OpenVirtualChannel;
wdqi.WdInformationLength = sizeof(OPENVIRTUALCHANNEL);
OpenVirtualChannel.pVCName = CTXPING_VIRTUAL_CHANNEL_NAME;
rc = VdCallWd(pVd, WDxQUERYINFORMATION, &wdqi);
/* do error processing here */

在调用VdCallWd之后,通道号在opambient通道结构的通道元素中分配。保存通道号。 比如:

1
g_usVirtualChannelNum = OpenVirtualChannel.Channel;
  1. 可选地指定一个指向私有数据结构的指针

如果您希望虚拟驱动为状态数据分配内存,它可以在每次调用时通过将指针放置在虚拟驱动结构中来返回该数据的指针,如下所示:

1
pVd->pPrivate = pMyStructure;
  1. 与WinStation驱动交换入口点数据

虚拟驱动必须向客户端WinStation驱动注册写钩子( write hook)。写钩子是在接收到这个虚拟通道的数据时要调用的虚拟驱动的入口点。 WinStation驱动返回指向函数的指针,这些函数必须用于填充输出缓冲区,并将数据发送到WinStation驱动以传输到服务器。

WDSETINFORMATION wdsi;
VDWRITEHOOK vdwh;
// Fill in a write hook structure
vdwh.Type = g_usVirtualChannelNum;
vdwh.pVdData = pVd;
vdwh.pProc = (PVDWRITEPROCEDURE) ICADataArrival;
// Fill in a set information structure
wdsi.WdInformationClass = WdVirtualWriteHook;
wdsi.pWdInformation = &vdwh;
wdsi.WdInformationLength = sizeof(VDWRITEHOOK);
rc = VdCallWd( pVd, WDxSETINFORMATION, &wdsi);
/* do error processing here */

在写钩子的注册过程中,WinStation驱动将输出缓冲区虚拟驱动辅助函数的入口点传递给VDWRITEHOOK结构中的虚拟驱动。DriverOpen函数将它们保存在全局变量中,这样虚拟驱动中的辅助函数就可以使用它们。WinStation驱动还传递一个指向WinStation驱动数据区域的指针,DriverOpen函数也会保存这个数据区域(因为它是虚拟驱动辅助函数的第一个参数)。

1
2
3
4
5
6
7
// Record pointers to functions used
// for sending data to the host.
pWd = vdwh.pWdData;
pOutBufReserve = vdwh.pOutBufReserveProc;
pOutBufAppend = vdwh.pOutBufAppenProc;
pOutBufWrite = vdwh.pOutBufWriteProc;
pAppendVdHeader = vdwh.pAppendVdHeaderProc;
  1. 确定WinStation驱动的版本。

新的虚拟驱动应该确定WinStation驱动是否支持新的SendData API和no polling模式。使用WdVirtualWriteHookEx信息类来检索此信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Do extra initialization to determine if
// we are talking to an HPC client.
wdsi.WdInformationClass = WdVirtualWriteHookEx;
wdsi.pWdInformation = &vdwhex;
wdsi.WdInformationLength = sizeof(VDWRITEHOOKEX);
vdwhex.usVersion = HPC_VD_API_VERSION_LEGACY; // Set version
// to 0; older clients will do nothing
rc = VdCallWd(pVd, WDxQUERYINFORMATION, &wdsi, &uiSize);
if(CLIENT_STATUS_SUCCESS != rc)
{
return( rc );
}
g_fIsHpc = (HPC_VD_API_VERSION_LEGACY != vdwhex.usVersion);
// If version returned, this is HPC or later
g_pSendData = vdwhex.pSendDataProc; // save HPC SendData
// API address

返回的usVersion可能是下列值之一:返回的usVersion可能是下列值之一:

1
2
3
4
5
typedef enum _HPC_VD_API_VERSION
{
HPC_VD_API_VERSION_LEGACY = 0, // legacy VDs
HPC_VD_API_VERSION_V1 = 1, // VcSdk API Version 1
} HPC_VD_API_VERSION;

如果返回的usVersion是HPC_VD_API_VERSION_LEGACY,则该引擎是较早的引擎。任何其他值都表示新引擎。返回的实际版本表示支持的API版本。目前,将返回的唯一其他值是HPC_VD_API_VERSION_V1。应该设置g_fIsHpc标志,以指示新的API可用。

WdVirtualWriteHookEx调用还返回一个指针(g pSendData)。这是一个指向SendData函数的指针。保存此值供以后使用。

  1. 在WinStation驱动中设置API选项。

如果这个虚拟驱动是由HPC WinStation驱动加载的,那么设置这个驱动将使用的API选项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if(g_fIsHpc)
{
WDSET_HPC_PROPERITES hpcProperties;
hpcProperties.usVersion = HPC_VD_API_VERSION_V1;
hpcProperties.pWdData = g_pWd;
hpcProperties.ulVdOptions = HPC_VD_OPTIONS_NO_POLLING;
wdsi.WdInformationClass = WdHpcProperties;
wdsi.pWdInformation = &hpcProperties;
wdsi.WdInformationLength = sizeof(WDSET_HPC_PROPERITES);
rc = VdCallWd(pVd, WDxSETINFORMATION, &wdsi, &uiSize);
if(CLIENT_STATUS_SUCCESS != rc)
{
return(rc);
}
}

将usVersion字段设置为通知引擎此驱动将使用的VD API的版本。这允许引擎在这个级别上为这个驱动维护VD API的兼容性,即使将来引擎API会发生变化。

pWdData指针必须指向相同的数据,该数据是由WdVirtualWriteHook VdCallWd调用在DriverOpen中返回的pWdData字段所指向的.

ulVdOptions字段是按位的或下列任何位定义的:

1
2
3
4
5
6
7
8
9
10
typedef enum _HPC_VD_OPTIONS
{
HPC_VD_OPTIONS_NO_POLLING =0x0001, // Flag indicating that
// channels on this VD do not
// require send data polling
HPC_VD_OPTIONS_NO_COMPRESSION =0x0002 // Flag indicating
// that channels on this VD
// send data that does not
// need reducer compression
} HPC_VD_OPTIONS;
  1. 分配所有内存需要的驱动和做任何初始化。您可以从返回的VDWRITEHOOK结构中的MaximumWriteSize元素获得最大ICA缓冲区大小。

注意:vdwh.MaximumWriteSize比您可以使用的实际最大值大一个字节,因为它还包括通道号。

1
2
3
4
5
g_usMaxDataSize = vdwh.MaxiumWriteSize - 1;
if(NULL == (pMyData = malloc( g_usMaxDataSize )))
{
return(CLIENT_ERROR_NO_MEMORY);
}
  1. 返回puiSize中VDOPEN结构的大小。客户端引擎使用它来确定虚拟通道驱动的版本。

DriverPoll

允许虚拟驱动检查计时器和其他状态信息,将排队的数据发送到服务器,并执行任何其他需要的处理。主客户端轮询循环可以定期调用此函数。

调用约定

INT DriverPoll(PVD pVD, PVOID pVdPoll, PUINT16 puiSize);

参数

  • pVD: 指向虚拟驱动控制结构体的指针
  • pVdOpen: 指向一个标准的驱动轮询信息结构体的指针 (DLLPOLL or DLL_HPC_POLL)。
  • puiSize: 指向驱动的轮询信息结构体的大小的指针。这是一个输出参数。

返回值

  • 如果成功,则返回CLIENT_STATUS_SUCCESS
  • 如果驱动在此轮询通上没有数据,则它将返回CLIENT_STATUS_NO_DATA。
  • 如果所有虚拟通道都返回CLIENT_STATUS_NO_DATA,WinStation驱动可能会减慢轮询过程。
  • 如果通过QueueVirtualWrite或SendData函数发送的数据被阻塞(CLIENT_ERROR_NO_OUTBUF), DriverPoll应该返回CLIENT_STATUS_ERROR_RETRY,这样WinStation驱动就不会减慢轮询。虚拟驱动应该在下一次轮询时再次尝试。
  • 如果虚拟驱动不能分配输出缓冲区,它将返回CLIENT_STATUS_ERROR_RETRY,以便WinStation驱动不会减慢轮询。然后,虚拟驱动尝试在下一次轮询时获得输出缓冲区。
  • 如果轮询已经通过HPC_VD_OPTIONS_NO_POLLING选项禁用,那么DriverPoll将至少被调用一次,并且只有在虚拟驱动请求轮询时,或者当它请求在可以重试发送操作时得到通知时才会调用。返回值的设置与上面的轮询情况相同。如果所有数据都已成功发送,则返回CLIENT_STATUS_SUCCESS。如果没有可发送的数据,则返回CLIENT_STATUS_NO_DATA。如果发送操作被阻塞,且虚拟驱动有更多数据要发送,则返回CLIENT_STATUS_ERROR_RETRY。

CLIENT_ERROR_*开头的返回值是致命错误;ICA会话将被断开

注释

在等待期望的结果(例如输出缓冲区的可用性)时,不允许虚拟驱动阻塞。

Ping示例包括可能在驱动轮询中发生的处理类型示例。

DriverQueryInformation

从虚拟驱动获取运行时信息。

调用约定

INT DriverQueryInformation(PVD pVD, PVDQUERYINFORMATION pVdQueryInformation, PUINT16 puiSize);

参数

  • pVD: 指向虚拟驱动控制结构体的指针
  • pVdQueryInformation: 指向指定要查询的信息和结果缓冲区的结构的指针。
  • puiSize: 指向查询信息大小和解析结构的指针。这是一个输出参数。

返回值

CLIENT_STATUS_SUCCESS

注释

该功能目前对虚拟驱动没有实际意义;它提供了与可加载模块接口的兼容性。此时除了LastError之外,没有通用的查询函数。LastError查询是通过DriverGetLastError函数完成的。


DriverSetInformation

设置虚拟驱动中的运行时信息。

调用约定

INT DriverSetInformation(PVD pVD, PVDSETINFORMATION pVdSetInformation, PUINT16 puiSize);

参数

  • pVD: 指向虚拟驱动控制结构体的指针
  • pVdQueryInformation: 指向一个结构的指针,该结构指定信息类、指向任何附加数据的指针以及附加数据的字节大小(如果有)。
  • puiSize: 指向信息结构大小的指针。这是一个输入参数。

返回值

CLIENT_STATUS_SUCCESS

注释

这个函数可以接收两个信息类:

  • VdDisableModule: 当连接被关闭时。
  • VdFlush: 当服务器端虚拟通道应用程序调用WFPurgeInput或WFPurgeOutput时。VdSetInformation结构包含一个指向VDFLUSH结构的指针,该结构指定调用了哪个清除函数。

SendData

使用通知选项(notification option)向服务器发送一个虚拟信道包。

调用约定

INT WFCAPI SendData(DWORD pWd, USHORT usChannel, LPBYTE pData,USHORT usLen, LPVOID pUserData, UINT32 uiFlags);

参数

  • pWd: 指向WinStation驱动控件结构的指针
  • usChannel: 虚拟通道号。
  • pData:指向包含要发送的虚拟通道数据的数据缓冲区的指针。
  • usLen:数据缓冲区中数据的字节长度。
  • pUserData:这是一个指向任意VD用户数据的指针,它将在发送应该重试的通知时被传递回回调函数(DriverPoll)。参见下面描述的DriverPoll和SENDDATA NOTIFY标志。
  • uiFlags: 标志位,用来来控制SendData函数的操作。该值由许多按位或’在一起的标志组成。每个标志都控制SendData接口的某个方面。目前只定义了一个标志。参见SENDDATA_*枚举
    • SENDDATA_NOTIFY: 如果设置了这个标志,并且当SendData返回代码为CLIENT_ERROR_NO_OUTBUF,表示引擎没有缓冲来容纳出站(outbound)包时,引擎将在稍后可以重试发送操作时通知虚拟驱动。通知通过DriverPoll方法发生。

返回值

SendData函数将返回以下值之一:

  • CLIENT_STATUS_SUCCESS:
    • 数据被复制到虚拟写缓冲区中。
    • 用户的缓冲区是空闲的(free)。
    • 使设置了SENDDATA通知标志,也不会发生回调
    • 下一个SendData调用可以立即发出。
  • CLIENT_ERROR_NO_OUTBUF:

    • 无法调度虚拟写( virtual write , 从虚拟写缓冲区中)。如果请求了一个通知,那么在虚拟驱动应该重试发送时,DriverPoll将在稍后的某个时间使用该通知进行驱动。
    • 如果没有请求通知,虚拟驱动应该从DriverPoll返回,并在重试发送之前等待下一次轮询。这假设虚拟驱动选择了轮询操作模式。
  • CLIENT_ERROR_BUFFER_STILL_BUSY:

    • 如果用户调用了SendData请求通知,而返回代码是CLIENT_ERROR_NO_OUTBUF,那么在发生通知之前,用户不能发出另一个SendData调用。如果在通知发生之前发出了另一个调用,则将返回代码CLIENT_ERROR_BUFFER_STILL_BUSY
  • CLIENT_ERROR_*:任何其他错误都应该导致虚拟驱动和会话关闭

注意:如果用户在HPC通道选项中指定了HPC_VD_OPTIONS_NO_POLLING,那么虚拟驱动必须假设它的DriverPoll函数在收到这些错误之一后不会再被调用。

注释

此函数用于将通道协议发送到服务器。引擎要么接受所有数据,要么拒绝所有数据,在这种情况下通道需要稍后重试(通常在DriverPoll中)。 这个函数的地址是在pSendDataProc中的钩子注册后从VDWRITEHOOKEX结构中获得的。VDWRITEHOOK结构提供了pWd。


ICADataArrival

当在驱动监视的虚拟通道上接收到数据时,WinStation驱动将调用此函数。这个函数的地址在DriverOpen期间传递给WinStation驱动。

调用约定

VOID wfcapi ICADataArrival(PVD pVD, USHORT uchan, LPBYTE pBuf, USHORT Length);

参数

  • pVD: 指向虚拟驱动控制结构体的指针
  • uChan: 虚拟通道号。
  • pBuf:指向包含服务器端应用程序发送的虚拟通道数据的数据缓冲区的指针。
  • Length:数据缓冲区中数据的字节长度。

返回值

此函数不返回任何值。

注释

这个函数名是用户定义函数的占位符;实际的函数不必被称为ICADataArrival,尽管它必须匹配函数签名(参数和返回类型)。虽然它必须匹配函数签名(参数和返回类型)。这个函数的地址是在DriverOpen期间提供给WinStation驱动的。虽然ICA将包控制数据到虚拟通道数据都做了前缀,但在调用此函数之前,将删除此前缀。

在虚拟驱动从这个函数返回之后,WinStation驱动将考虑所交付的数据。如果以后需要处理,虚拟驱动必须从这个包中保存它需要的任何信息。

不允许此函数阻塞。使用您自己的线程或DriverPoll函数(启用了轮询)进行任何所需的延迟处理。

虚拟驱动可以在从ICADataArrival函数中接收到此数据后将数据发送到服务器,但是请注意,当缓冲区不可用于容纳发送操作时,发送操作可能会立即返回一个错误。虚拟驱动可能不会阻塞此函数以等待发送操作完成。

如果虚拟驱动正在处理多个虚拟通道,请使用uChan参数来确定发送这些数据的通道。查看DriverOpen获取更多信息。


miGetPrivateProfileBool

调用约定

参数

返回值

注释


miGetPrivateProfileInt

调用约定

参数

返回值

注释


miGetPrivateProfileLong

调用约定

参数

返回值

注释


miGetPrivateProfileString

调用约定

参数

返回值

注释


QueueVirtualWrite

调用约定

参数

返回值

注释


VdCallWd

调用客户端WinStation驱动来查询和设置关于虚拟通道的信息。这是虚拟驱动访问WinStation驱动的主要方法。对于通用的虚拟通道驱动,这设置了虚拟写钩子。

调用约定

INT VdCallWd( PVD pVd, USHORT ProcIndex, PVOID pParam);

参数

  • pVd:指向虚拟驱动控制结构体的指针
  • ProcIndex:索引的WinStation驱动调用例程。对于虚拟驱动,这可以是WDxQUERYINFORMATION,也可以是WDxSETINFORMATION。
  • pParam:指向参数结构体的指针

返回值

如果成功,则返回CLIENT_STATUS_SUCCESS 如果失败,它返回一个与失败相关的错误代码;使用DriverGetLastError获取扩展的错误信息。

注释

这个函数是一个用来调用WinStation驱动中的例程的通用机制。

对于虚拟驱动,此函数的惟一有效用法是:

  • 使用WDxQUERYINFORMATION分配虚拟信道。
  • 在驱动打开期间使用WDxSETINFORMATION与WinStation驱动交换函数指针。

有关更多信息,请参见DriverOpen或Ping示例。

在成功返回时,VDWRITEHOOK结构包含指向输出缓冲区虚拟驱动辅助函数的指针,以及指向WinStation驱动控制块的指针(缓冲区调用需要该指针)


WFVirtualChannelClose

调用约定

参数

返回值

注释


WFVirtualChannelOpen

调用约定

参数

返回值

注释


WFVirtualChannelPurgeInput

调用约定

参数

返回值

注释


WFVirtualChannelPurgeOutput

调用约定

参数

返回值

注释


WFVirtualChannelQuery

调用约定

参数

返回值

注释


WFVirtualChannelRead

调用约定

参数

返回值

注释


WFVirtualChannelWrite

调用约定

参数

返回值

注释

链接

SDK下载