当 DCOM 在若干年前登上历史舞台时用于演示其功能的最常用示例之一就是远程剪贴板管理器通过使用 DCOM 编程模型一个组件能够读取和写入存储在另一台计算机上但连接到同一网络的剪贴板内容(当然只有在安全设置允许的情况下才能生效) 但是当 DCOM 提供基础结构以构建到系统组件(例如剪贴板)的远程访问时无论是 Windows 还是 DCOM 都无法提供能够直接对剪贴板进行远程访问的 API开发人员可以利用的技巧是以本地代理与远程存根之间的交互为基础的应用程序调入本地代理进而在网络传输层上序列化该调用并将其传送到远程主机然后该应用程序宿主将剪贴板处理程序组件的一个本地副本实例化以对 Windows 本地副本的剪贴板进行读取和写入操作 MicrosoftNET Framework 提供了 Clipboard 类来包装系统剪贴板上的主要操作该 Clipboard 类作为 Windows 窗体基础结构的一部分在 SystemWindowsForms 命名空间中进行声明该类上的方法允许您在单个应用程序的上下文中获得并设置剪贴板的当前内容 我的一位客户要构建一个不使用剪贴板的 ASPNET 电子商务站点然而该团队中的一名开发人员意识到负责填写后端表格的人员需要持续不断地在计算机间传送大量数据(大部分为纯文本)他们找到的最快方法是创建能够跨网络共享的临时文本文件尽管可以接受这个特定技巧但整个过程都不太智能特别是文本首先会在 Microsoft Word 或 Microsoft Internet Explorer 中突出显示然后被复制到剪贴板接着再粘贴到一个新的 Notepad 文档中最后该文档被拖放到网络文件夹中在具备了良好的意志和新的 DCOM 存储器后有悟性的开发人员会想到一个定制的远程剪贴板查看器和管理器可以更快更有效地完成工作 图 剪贴板查看器 剪贴板查看器(请参见图 )是一个旧的 Windows 附件它不再出现在开始菜单中但是依然可以作为 clipbrdexe 从 System 文件夹中使用剪贴板查看器充当所有连接到网络的计算机上的剪贴板的管理器如果当前的安全设置允许您就可以连接到 Windows 的一个远程实例并监视计算机的剪贴板虽然查看器只是一个查看器(它显示当前内容并可让您删除内容)但是它不提供向剪贴板中输入新文本的用户界面此外剪贴板查看器基于分布式技术(十几年前的旧技术)— 网络动态数据交换(或缩写为 NetDDE)所以我的客户决定编写一个自定义版本的剪贴板查看器以作为实际的分布式应用程序因为他使用的是 NET Framework所以他在计划时就想到了使用 NET Remoting 来设计实用工具 安装远程组件 NET Remoting 是一种机制能够实现在不同 AppDomains 中运行的组件之间的通讯所有基于 NET Framework 的应用程序至少由一个(主)AppDomain 构成但是更多的 AppDomains 可以通过编程方式创建AppDomain 代表一个托管子进程该子进程存在于由操作系统和 CPU 管理的物理进程上下文中公共语言运行库 (CLR) 可确保不能从一个 AppDomain 访问包含在另一个 AppDomain 中的任何数据无论两个应用程序域是位于同一个应用程序中同一计算机的两个截然不同的应用程序中还是运行于物理分隔的计算机上分离机制都是完全相同的 从体系结构上讲NET Remoting 的角色取决于系统将调用上下文从客户端封送到服务器然后将结果发送回客户端的能力远程组件是一个具有公共方法的类它可以直接或间接地来自 MarshalByRefObject该类被编译为程序集它部署在服务器计算机上并通过宿主应用程序与客户端进行交互宿主应用程序负责侦听传入调用的特定端口将它接收到的参数转换为对象的本地调用并将返回值封送回调用方图 显示了访问网络计算机剪贴板的远程组件的体系结构 图 访问剪贴板的远程组件 客户端(假设运行在 Machine 上)发出一个对服务器端对象(假设驻留在 Machine 上)的远程调用该调用由侦听协议端口的应用程序宿主接收然后再进行处理该调用上下文包括要调用的远程方法的名称及其参数通过图 中所示的代码可以实现封装剪贴板函数的组件marshalbyref 类将公开一个 Copy 方法以使客户端能够将文本写入远程剪贴板以及一个 Paste 方法以将远程剪贴板的内容粘贴到本地上下文中图 中的代码和基本模式应该比较易于理解特别是如果您阅读了我在 年 月刊上发表的 NET Remoting 介绍文章(请参阅 NET Remoting Design and Develop Seamless Distributed Applications for the Common Language Runtime)则更是如此截至目前一切都还不错然而您可能已经注意到Copy 和 Paste 方法的主体都是空的起初我认为这部分代码的内容无足轻重但是我大错特错了我解决问题的方式只是对 Win? 和 Visual Studio? 进行了一些改善 NET 剪贴板 API 正如先前提到的那样基于 NET Framework 的应用程序使用 SystemWindowsForms 命名空间中的 Clipboard 类来管理剪贴板该类(不一定要实例化)包含一对静态方法 — GetDataObject 和 SetDataObjectGetDataObject 检索当前存储在系统剪贴板中的数据SetDataObject 将指定的数据对象置于系统内存中 剪贴板支持多种数据格式包括用户定义的格式Win 剪贴板 API 定义一组预先定义的格式并用助记键常量(一个整数值例如 CF_TEXT 或 CF_BITMAP)对其进行标识 由于该剪贴板是系统组件因此表示它的 NET Framework 类只为一组低级别的 API 函数提供包装可能会使您感到惊讶的是剪贴板的 NET Framework 实现不是基于原始 Win 函数和消息的……NET Framework 中的 Clipboard 类通过 OLE 剪贴板协议执行操作您是说 OLE 是的没错!本专栏打算回顾一些早期技术它们是 Windows 发展过程中的重要里程碑 大约在 年前Microsoft 引入了 OLE 作为一种全包含组件技术引用 Kraig Brockschmidt 在 Inside OLE(Microsoft Press 年)一书中的话OLE 被定义为一个基于对象服务的统一环境与使用 Win 不同您无法只使用 OLE(或者是它的继任者 COM)来编写一个完整的应用程序但是OLE 采用了某些现有的系统功能并以一种更通用更广泛的方式来公开它们那么什么是 OLE 剪贴板协议呢? 从本质上说OLE 剪贴板是一组接口旨在泛化基于 Windows 的应用程序与剪贴板之间发生的数据交换Platform SDK 只定义几个基本的数据类型(多数为文本和位图)OLE 剪贴板协议通过提供对任意数据格式和存储介质的支持来扩展模型只要应用程序所需的数据格式未超出 Platform SDK 提供的范围那么对于基于 Windows 的应用程序来说OLE 剪贴板协议就不是必需的基于 OLE 的协议比 Win 剪贴板 API 的功能更强大并且在 NET Framework 中构建剪贴板支持时OLE 是一个相当合理的选择 在 OLE 和 NET Framework 中执行剪贴板操作的关键接口是 IDataObject这两个接口具有不同的方法集但都扮演着同样的角色IDataObject 提供一种独立于格式的机制以传输数据并且由 Clipboard 类在拖放操作中使用(在 NET Framework 中OLE IDataObject 接口被重命名为 IOleDataObject)图 列出了在 IDataObject 接口上定义的方法 以下代码片段显示了一个基于 NET Framework 的应用程序如何将纯文本复制到剪贴板 ClipboardSetDataObject(text) SetDataObject 方法提取一个对象并将它作为 IDataObject 实例复制到剪贴板中如果该参数是一个已实现 IDataObject 接口的对象则直接进行复制否则该方法将对象打包到一个动态创建的DataObject 类实例中如图 所示 DataObject 类可实现 IDataObject 和 IOleDataObject 接口剪贴板的 SetDataObject 方法接受普通的对象参数 public static void SetDataObject(object) public static void SetDataObject(object bool) 这段代码允许您将简单数据(例如文本和位图)作为原生对象传入而将数据改写的重担转嫁给内置的基础结构SetDataObject 方法还具有一个要求额外 Boolean 参数的重载该参数指示在当前应用程序终止之后置于剪贴板中的数据是否应该保持可用如果您使用一个参数的重载则剪贴板的内容会在退出时刷新 与将数据写入剪贴板相比从剪贴板读取数据更明了些这是因为数据推断不能委托给 NET Framework以下代码片段显示了托管应用程序如何从剪贴板中进行读取 IDataObject data data = ClipboardGetDataObject() GetDataObject 方法返回一个 IDataObject 数据包应用程序必须将其解包 // Verify that the data object contains plain text if (dataGetDataPresent(DataFormatsText)) { // Extrapolate and display the text string text = dataGetData(DataFormatsText); MessageBoxShow(text); } IDataObject 接口上的 GetDataPresent 方法(请参见图 )采用一个参数来识别数据类型(例如纯文本)如果该数据对象的内容与指定的类型相匹配则该方法会返回真请注意无论是存储对象的原生类型相匹配还是对象的类型能够转换为所需类型该方法都会返回真例如如果数据对象包含 HTML 文本但是用户要求纯文本那么 GetDataPresent 会返回真 从基于 NET Framework 的应用程序中使用剪贴板 API 不费吹灰之力但是如果您试图从远程对象来使用它就不那么简单了 构建远程剪贴板处理程序 Windows 窗体应用程序通常以单线程单元 (STA) 模式运行通过将 [STAThread] 属性添加到应用程序的 Main 例程C# 项目将这一模式清晰地呈现在您面前在 Visual Basic NET 中该设置是隐式的对于许多 GUI 应用程序而言STA 模式绝对是必要的因为这些应用程序依赖于由并不始终支持纯多线程环境的操作系统所公开的服务这是典型的 OLE 和 COM 服务例如剪贴板和拖放操作 事实上您不能在来自 MTA 池的线程中使用剪贴板对象请试验下面的小型控制台应用程序 using SystemWindowsForms; class Test { [MTAThread] static void Main() { ClipboardSetDataObject(MSDNMag); } } 这段代码会抛出一个线程状态异常如图 所示 图 线程状态异常 能够在基于 NET Framework 的应用程序中进行 OLE 调用之前程序员必须确保当前线程以 STA 模式运行实际上托管对象负责以一种线程安全的方式来公开它们的共享数据……NET Framework 支持线程单元只为获得向后兼容性相反COM 组件使用线程单元出于该原因CLR 需要在与 COM 对象发生任何交互之前先创建一个线程单元STAThread 和 MTAThread 属性都是声明性编程接口用于为应用程序选择线程模型在上面的代码片段中MTAThread 属性设置了控制台应用程序以创建一个托管线程并对其进行配置以输入一个 MTA 单元只要应用程序调入 OLE/COM 的内容就可以检测到单元沖突并引发一个异常通常控制台和 Windows 窗体应用程序以 STA 模式运行(除非指定其他模式) 当我第一次构建 marshalbyref 对象以将文本复制到远程剪贴板时我保留了默认设置但是只要执行流到达 Clipboard 类就会引发线程异常请记住远程调用始终是通过 MTA 线程解决的 在调入 Win OLE 方法之前Clipboard 类会根据当前线程的单元模式执行预备检查这通过发出一个对 ApplicationOleRequired 方法的调用来完成该方法检验 OLE 是否在当前线程上进行初始化或者亲自进行初始化(如果需要)该方法从列出三种可行值(TASTA 和 Unknown)的 ApartmentState 枚举中返回一个值您可以通过下面的代码检查当前的线程模型 ConsoleWriteLine(ApplicationOleRequired()ToString()) 调入远程对象的客户端应用程序是一个单线程的 Windows 窗体应用程序……NET Remoting 宿主是一个显式标记为 STA 的控制台应用程序但是用于远程调用的线程的单元状态是 MTA您在图 中看到的错误消息其实是以前的结论 对于如何解决 NET Remoting 文档和可用参考资料(包括出色的 )中的问题我还没有找到理想的解决方法所以我采用了 Kraig Brockschmidt 在有关 OLE 剪贴板的章节中给出的建议Kraig 指出只有当 OLE 剪贴板能够为您带来附加值时才应该使用它(如 NET Framework 所做的那样)在只需要交换文本和位图时您可以坚持使用 Win 剪贴板 API作为回报这样做会减少线程之争图 显示了一个 Win DLL它导出一对公共函数以将纯文本复制并粘贴到剪贴板 当编码 Win 方式时您必须首先打开并清空剪贴板您需要一个窗口句柄来打开剪贴板这是因为剪贴板只能属于一个窗口对象OpenClipboard 是要调用的 API 函数EmptyClipboard 函数会释放存储在剪贴板中的全局数据的所有句柄之后当前让剪贴板打开的窗口便成为新的所有者要将数据复制到剪贴板您必须分配一块最好以 GHND 标志标记的共用内存该标志表示其可移动并可初始化为零复制到剪贴板或从中读取的任何数据都被打包到共用内存的句柄中在 Win 级别上可以打包到存储介质中的数据格式种类限制为几种例如 HTML纯文本或独立于设备的位图如果您使用 OLE则可以使用更多格式和存储介质 虽然将 Win DLL 和 P/Invoke 平台用于 Win 交互操作会限制剪贴板只能使用几种格式但是不需要更改线程模型并且也可以通过远程使用图 显示了远程剪贴板处理程序对象的最终代码两个 DLL 公共函数映射到 marshalbyref 类的静态外部成员CopyToClipboard 的签名不难转换为 CLR 类型系统利用字符串更改 LPCTSTR 并完成操作导入 PasteFromClipboard 函数需要一点技巧在图 中通过引用声明该函数接受一个字符串并返回一个布尔值 将类似签名转换为 NET 代码的有效方式涉及到 StringBuilder 对象的使用 [DllImport(clipdll)] private static extern bool PasteFromClipboard(StringBuilder text) 您首先将对字符串的引用替换为初始化的 StringBuilder 对象然后使用 StringBuilder 类上的 ToString 方法来获取该字符串 StringBuilder buf = new StringBuilder() PasteFromClipboard(buf)return bufToString() 应当注意的是StringBuilder 对象与 String 对象不同前者是可增长的对象而后者是传统的字符串它在本质上不会改变换言之在您串联两个字符串时NET Framework 会创建一个新的字符串其大小为二者之和在您将字符串添加到 StringBuilder 对象时只是将输入文本追加到现有缓沖区而已 远程剪贴板客户端 远程剪贴板客户端由两个元素组成即客户端和远程组件的主服务器要部署该客户端您必须将主服务器(请参见图 )和带有远程组件的程序集一起复制到要到达的任何计算机上您必须将客户端应用程序复制到您希望在网络中从其进行复制或粘贴的所有计算机上 图 远程剪贴板应用程序 NET Remoting 基础结构不会自动启动服务器端主机来接收对远程组件的传入调用用户负责启动并运行这种 stub 程序您可以对该任务使用 Microsoft Internet 信息服务 (IIS)或编写自己的应用程序使用 IIS 会受到一些限制(需要 HTTP 通道)但是该方法可让您不必管理远程处理宿主或者您可以编写自定义应用程序该应用程序只需注册一条服务器通道(TCPHTTP 或自定义类型)并通过服务器 URI 将远程类型与已知类型相关联 在图 中宿主是为端口 创建 TCP 通道并将 MsdnMagClipboardHandler 类型(请参见图 )标记为已知的控制台应用程序用于调用对象的 URI 是 ClipboardHandler您必须手动启动和终止控制台应用程序您也可以决定将其编写为 GUI 应用程序并提供一个用户界面来暂停或终止端口监视要暂停监视您可以取消注册通道如果您不希望编写控制台应用程序则可以选择创建一个 Windows 服务该服务提供相同的功能而不需要手动处理启动/停止操作 NET Remoting 组件的客户端必须完成一项基本任务即使远程类型可由其余的应用程序识别该任务通过使用 RemotingConfiguration 类的其中一个静态成员(RegisterWellKnownClientType 方法)来执行以下代码片段显示了如何将类型注册为已知 RemotingConfigurationRegisterWellKnownClientType( typeof(MsdnMagClipboardHandler) tcp://expotwo/ClipboardHandler) RegisterWellKnownClientType 方法使用两个参数第一个是待定的类型第二个是一个 URI它包括用于封送的传输协议服务器名称或 IP 地址要使用的端口以及远程对象的昵称如之前的代码所示当然该端口必须与远程主机在其上进行侦听的端口相匹配虽然已知类型的概念易于理解但不能在本地定义远程类型并且对任何方法或属性的任何引用都必须以不同的方式进行处理在每个调用前面编译器都必须生成一些普通的代码以将调用封送到远程服务器并返回出于这个原因必须识别并标记源代码中对远程对象的所有引用以便实时 (JIT) 编译器可以为其正确生成动态代码 RegisterWellKnownClientType 方法的内部实现并不复杂它可缓存类型信息并将其添加到已知类型的全局哈希表它将类型用作密钥而将 URI 字符串用作成对的值已知类型的编程接口决不允许您取消注册类型如果您试图将一个已知类型重定向到另一个 URI则会引发一个异常 正如您看到的那样RegisterWellKnownClientType(以及类似方法如 RegisterActivatedClientType)通过在类型和服务器 URI 之间建立一对一的关系来运行如果应用程序需要从不同的服务器调用相同的类型应该怎么做呢?好这就是在构建远程剪贴板处理程序时要面对的下一个问题 如图 所示客户端应用程序必须多次注册相同的远程类型为每台连接的服务器注册一次远程剪贴板客户端必须能够对网络上可用的所有计算机的剪贴板进行读取和写入操作这些计算机都会运行同一宿主和同一类型的实例即图 中的 MsdnMagClipboardHandler 类如何多次将同一类型注册到不同的 URI 您有两个选择第一个要求跳过内置的配置机制您不用将远程类型标记为已知而只需使用 ActivatorGetObject 方法的一个重载来创建并获取对象的远程实例 string uri = @tcp\\expotwo\ClipboardHandler object o = ActivatorGetObject(typeof(MsdnMagClipboardHandler) uri) MsdnMagClipboardHandler clip clip = (MsdnMagClipboardHandler) o GetObject 方法为指定类型和 URL 所指示的对象获取或创建代理该解决方案为您提供了极大的灵活性因为它并不依赖于服务器的数量及其位置 第二个选择是如果您明确知道客户端始终与之协同工作的服务器则可使用更为严格的方法将传统的已知类型方法扩展到多台服务器的情况其思想是通过派生几乎完全相同的新类来重命名远程类型在计算机名之后的命名空间中将新类设为根(尽管这是任意的) 图 中的代码显示了如何使用继承来重命名 MsdnMagClipboardHandler 类如果重新编译则 ExpoTwo 和 ExpoStar 命名空间中的新类将不会添加额外的代码并且不需要系统开销要支持新服务器您必须添加新的命名空间声明应当注意的是该解决方案缺乏灵活性因为它对服务器的名称进行硬编码并且任何更改(例如将新服务器添加到列表)都要求重新编译以下代码说明了来自客户端的远程调用 ExpoTwoRemoteClipboardHandler rc rc = new ExpoTwoRemoteClipboardHandler() rcCopy(TextToCopyText) 在调用已知类型上的方法时NET Remoting 基础结构会验证宿主应用程序是否已启动并处于运行状态如果不是则会引发一个套接字异常引发的特定异常是 SocketException该类属于 SystemNetSockets 命名空间客户端应用程序的示例项目引用了包含远程对象的组件 尽管其功能不会引发异常但该方法或许不是实际情况中的最佳选择版本控制类依赖项甚至大小都是确定备选方法的理由例如您可以通过为所有公共方法甚至接口定义带有空实现的基类来避免链接原始程序集尽管在后一种情况中您不能在客户端上只使用 new operator 来获取远程对象的实例实际上按照设计您无法在接口上调用 new您可以使用 ActivatorGetObject 来检索在指定 URI 处实现给定接口的对象的实例有关该特定点的详细信息以及与 NET Remoting 相关的工作的有价值资源请参阅 小结 图 中所示的客户端应用程序提供了在特定计算机上复制和粘贴纯文本的按钮通过使用低级别 API 调用的 Win DLL 来完成系统剪贴板上的物理操作对于跨网络中的计算机共享剪贴板这一问题 上的文章提供了更为有用的方法 图 复制和粘贴到特定计算机 对于我的客户端用途而言Win 基本格式就足够了希望将来没有这些限制我的解决方案将模拟 ASPNET 会话的分布式体系结构引入充当宿主的 Windows 服务并添加在 STA 线程上以远程方式操纵剪贴板的 marshalbyref 类最后我会将一对重载添加到现有 Clipboard 类的方法中 请将给 Dino 的问题和意见发送至 c Dino Esposito 是一位讲师兼顾问现居住在意大利的罗马他着有 Building Web Solutions with ASPNET and ADONET 和 Applied XML Programming for NET这两本书均出版自 Microsoft Press他的大部分时间都用于讲授有关 ASPNET 的课程以及会议演讲Dino 最近为 Microsoft Press 出版了 Programming ASPNET 一书您可以通过 与 Dino 取得联系 |