65.9K
CodeProject 正在变化。 阅读更多。
Home

一种在 C# 中实现的,无需 IIS 即可查看 aspx 页面的简单协议

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (45投票s)

2004年2月18日

13分钟阅读

viewsIcon

423316

downloadIcon

6867

介绍了如何使用 C# 编写可插入的异步协议,并提供了一个有用的协议以实现 ASP.NET 站点的本地执行。

引言

自从我开始处理 asp 页面以来,我一直希望能够直接从文件系统测试它们,而无需配置 IIS 或设置虚拟目录。尤其是在 CodeProject 等网站上下载和查看示例代码时。在开发一个基于 Web 的验收测试应用程序时,我发现了实现这一目标的方法。首先,ASP.NET 允许您在 IIS 之外托管运行时。其次,Microsoft 提供了异步可插入协议接口,用于为 IE 创建自定义协议。我只需要将自定义协议连接到 ASP.NET 运行时的一个实例,一切都会顺利进行。我的最终解决方案非常接近那个目标,但在解决过程中遇到了不少问题。

我包含了两个协议:echo: 和 aspx:。echo 协议只是将 URL 行作为 HTML 文本回显。这是我编写的测试协议,只是为了确保我正确处理了头信息。Aspx 将在本地运行任何 aspx 页面,并在 IE 中加载结果。它支持大多数标头和回发,但目前不支持 cookie。我还添加了一个外壳扩展,允许您右键单击任何 aspx 文件并使用 aspx 协议执行它。

使用代码

您可以在任何使用常规协议的地方使用该协议,例如在运行命令行或从浏览器链接。aspx 协议应该可以让您在无需安装任何 Web 服务器的情况下测试、开发和分发您的 Web 应用程序。由于我编写了 2 个协议,所以我将注册和基本缓冲区代码提取到一个基类以及一些属性中。如果您想尝试自己的协议,只需为您的类分配一个属性,重写 start 方法并继承自 BaseProtocol。您还可以使用 ContextHandlerAttribute 来注册您编写的任何 ShellExtensions

注册

由于协议是一个 COM 组件,它必须能够从任何目录加载,因此它需要注册到 GAC 或使用 DEVPATH 进行设置,以及使用 regasm 进行注册。我包含了一个 register.cmd 文件来注册组件。如果您遇到问题,很可能是您的 .NET 安装不在默认位置。

如果您想编译代码并以这种方式安装,首先将其注册到 GAC 或使用 DEVPATH 变量,这在设置好后会更容易。使用 fuslogvw.exe 对于配置 DEVPATH 至关重要。请注意:您需要在 DEVPATH 中放置的目录中包含尾随的 \,因为无论是什么,最后一个字符都会被截断。然后您必须对该组件运行 regasm。该组件处理其自身的注册以及设置协议和外壳扩展所需的所有其他注册表项。

接口

IInternetProtocol 继承自 IInternetProtocolRoot。  IInternetProtocolRoot 涵盖了启动和停止下载内容所需的方法。  IInternetProtocol 添加的 4 个方法都涉及获取下载结果。  所有方法都必须实现异步下载并处理用户中止、暂停和恢复下载等情况。  幸运的是,我们可以进行同步下载。  经过大量试错,我确定只需要实现 3 个方法就能让这个简单协议工作。

首先调用 Start 方法,并将完整的地址字符串(包括初始协议字符串)传入。  这告诉我们开始下载,并提供了两个接口。  一个用于通知 IE,另一个用于从请求中获取额外信息。  由于我们只是实现一个简单的协议,我们只需要 URL,可以专注于第一个接口。  在这里,我们至少需要做两件事:准备我们的数据,然后发送数据已准备好读取的通知。  我们通过创建写入器、写入数据流并刷新写入器来准备数据,如下所示。

    public void WriteBasicMessage(string Message, string Title)
{
StreamWriter Writer = new StreamWriter(Stream);
Writer.Write("{0}", Message, Title);
Writer.Flush();
Stream.Position = 0;
}
为了通知 IE,我们需要调用 IInternetProtocolSink 上的两个方法,第一个是 ReportData,传入最后一个通知代码和我们流中的字节数。警告:文档说明您只需传入一个数字,告诉您下载的进度。当我传入 100 的值,认为使用百分比就可以时,对于大文件下载会出现错误。直到我编写了自己的 UrlMon 客户端并看到它正在将文件大小返回给此方法时,我才弄清楚问题所在。当我尝试这样做时,一切都神奇地开始工作了。第二个方法是 ReportResult。我只传入 S_OK 和 200 表示成功,这就可以了。

现在我们来看看处理这些接口的第二个陷阱。文档指出,读取的调用应由 Lock 和 Unlock 的调用括起来,但您可能会在 Unlock 后收到一些读取调用。嗯,也很有可能在 Lock 调用之前收到 Read 调用。这对我们组件编写者意味着什么。我们必须在调用 ReportData 之前在 Start 方法中初始化我们的缓冲区,而不是在 Lock 方法中刷新所有内容,尽管后者似乎更自然。在此之后,只要我们使用 .NET 流来存储数据并使用 byte[] 数组进行传输,Read 方法就相对简单了。我的第一个实现使用了基于 StringBuilderStringWriter,这还可以,直到我遇到编码问题。切换到 MemoryStream 并为每种情况创建适当的 Writer 对象是正确的选择。

这能正常工作,但 IE 将输出解释为文本文件,而没有考虑任何 HTML 格式。我需要将编码标头发送给客户端,以便它知道这是 HTML 文本。为此,我不得不访问另外几个接口。首先,我们需要调用 IServiceProvider,这是一个用于从接口获取延迟初始化对象的标准接口。然后我们只需查询 IHttpNegotiate 接口。这允许我们做两件事:从客户端获取请求标头并发送响应标头。对于我们简单的 echo 协议,我们只需要发送一些基本标头。它们看起来像这样。
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length:234
Content_Type: text/html; 标头告诉客户端我们发送的是 HTML 内容,内容长度告诉它我们发送了多少。如果仅仅为了在浏览器中输出几行 HTML 就显得工作量很大,那是因为确实如此。然而,这为我们打下了基础,为接下来的酷炫部分做准备。在不使用 Web 服务器的情况下本地运行 ASP.NET。

ASPXProtocol

在 IIS 之外托管 ASP.NET 是这个项目中记录最完善的部分。一个很好的例子是 Cassini,它是 asp matrix 使用的开源 Web 服务器。由于有许多优秀的文章,我将只提供简要概述。托管 ASP.NET 有两个步骤:初始化运行时并将请求发送给它。为了安全起见,ASP.NET 在单独的 AppDomains 中加载。这可以防止一个站点的代码在没有特殊权限的情况下访问另一个站点。但是,这对我们来说更复杂,因为我们需要从我们的域发送请求到 ASP.NET 域。为了简化这一点,System.Web.Hosting 命名空间包含 ApplicationHost.CreateApplicationHost 方法。这会为我们创建一个主机对象并向我们返回一个代理引用。第一次遇到这个可能很令人困惑,但一旦你习惯了,它就会非常有意义。相信我。一旦我们拥有了主机对象的副本,它有什么用呢?主机对象充当了我们与 ASP.NET 运行时的网关,所以我们需要给它一些有用的功能。我们只需要给它一个简单的方法,我决定称我的方法为 SendRequest。它会直接调用 HttpRuntime.ProcessRequest 方法。这会加载运行时,处理请求并返回响应。

初始化运行时的一个难点是我们需要找到站点的根目录和虚拟目录的位置。通常这些信息由 IIS 维护。在我们的例子中,我们没有这些信息,需要搜索站点的根目录。一个简单的方法在大多数情况下都适用。首先,我们回溯到文件系统的根目录。如果找到 global.asax 文件,我们可以很有把握地确定我们找到了包含该页面的站点的根目录。如果找不到 global.asax 文件,那么我们就使用包含 web.config 的最低级目录。如果这失败了,那么就使用包含 aspx 页面的最低级目录。这适用于几乎所有的站点,除了没有 asax 文件的虚拟目录。我计划在该对象中添加一个配置站点,以便在需要时配置这些。

System.Web.Hosting 命名空间包含 SimpleWorkerRequest 类来处理基本请求。这不包括 POST 或标头数据,所以我不得不使用一个派生类来处理这些。另外,如果我在 ASP.NET AppDomain 之外创建它,我就会收到错误,所以我不得不创建一个简单的数据类,并将其传递给 SendRequest 方法。之后,我只是匹配了一些对数据类的重写,一切都运行得很顺利。只有一个问题。SimpleWorkerRequest 使用 TextWriter 来输出内容。这对于简单的文本和直接输出到文件来说效果很好,但如果内容是图像,则会导致问题。TextWriter 在写入时进行编码和解码。由于我直接流式传输到 IE,它会进行自己的解码,这导致流被编码两次。我重写了几个方法,并替换为 BinaryWriter,一切都正常工作,包括 gif 和 jpeg 图像。我偶然发现了这个捷径,因为我可以像 aspx 页面一样让 ASP.NET 处理图像文件。我需要直接加载这些图像文件以获得更健壮的解决方案。现在我已经完成了托管,我只是将它与我为 echoProtocol 编写的协议代码结合起来,一切运行正常。我可以通过一个链接本地加载 aspx 页面,如 aspx:c:\test\test.aspx。但是页面中的链接是断的,POST 也不起作用。

修复链接和 IInternetProtocolInfo

链接的问题在于,在没有任何额外信息的情况下,IE 会将页面中的链接附加到页面的当前 URL 上。如果我们正在访问 aspx:c:/test/test.aspx 并有一个指向 aspx:c:/test/test2.aspx 的链接,它将被解释为 aspx:c:/test/test.aspxaspx:c:/test/test2.aspx。每个协议都有自己的 URL 组合方式。幸运的是,IInternetProtocolInfo 接口为我们提供了一种告知客户端如何组合这些 URL 的方法。我们需要解析 3 种主要的链接类型:完全限定的链接需要按原样处理,而不包含当前页面。  以 / 开头的链接需要映射到我们正在运行的站点的根目录。  本地链接需要从当前目录开始映射。  IInternetProtocolInfo 的实现提供了一个很好的最小化接口实现,请参考 DB2XML Implements Pluggable Protocol Handler

POST 数据和 IInternetBindInfo

要实现这一目标,需要设置 COM 管道和 Marshal 对象,并找到这个 KB 文章。  HOWTO: Handle POST Requests in a Pluggable Protocol Handler 按照这篇文章中的说明实现了以下代码。

    public byte[] GetPostData(BINDINFO BindInfo)
    {
      if (BindInfo.dwBindVerb != BINDVERB.BINDVERB_POST)
        return new byte[0];
      byte[] result = new byte[0];
      if (BindInfo.stgmedData.enumType == TYMED.TYMED_HGLOBAL)
      {
        UInt32 length = BindInfo.cbStgmedData;
        result = new byte[length];

        Marshal.Copy(BindInfo.stgmedData.u, result, 0, (int)length);
        if (BindInfo.stgmedData.pUnkForRelease == null)
          Marshal.FreeHGlobal(BindInfo.stgmedData.u);
      }
      return result;
    }

浏览文件

为了方便起见,我添加了一个简单的函数,它将 URL 通过 System.IO.Directory.Exists 方法运行。如果它是一个目录,则协议返回父目录和子目录以及本地文件的简单列表。这使得查找特定文件更容易。

COM 注册

注册协议需要添加一些额外的注册表项。这可以通过在每个导出的 .NET 对象中添加几个静态方法并使用属性来完成。由于我实现了几个对象,我将其重构为一个基类,并创建了一个简单的属性,在注册期间将特定信息从派生类反馈给基类。

外壳集成

添加一些注册表项就足以进行大多数注册了,指向 c:\program files\internet explorer\iexplore.exe。但是,我想做一些更强大的事情。这需要另一个对象,这次实现 IContextMenuIShellExtInit。这很简单,我只需查找 http 的映射,将请求发送到为该映射注册的相同应用程序。 .NET 框架示例提供了一个实现 IContextMenu 的不错示例。

在 .NET 中实现

毫无疑问,这个项目中最繁琐的部分是正确地处理 COM 互操作。互操作有许多工具可以轻松访问任何带有类型库的组件。我最初尝试从这些接口的 IDL 生成类型库,并使用 tlbimp.exe 导入它们。我在这些导入中遇到了许多问题,最终手动实现了这些接口。这篇文章描述了对我帮助最大的技术。

然而,处理需要直接内存访问的低级接口的学习曲线很陡峭,而且文档很少。如果没有我多年来学习 IDL 和在 C++ 下进行 COM 的经验,我将完全迷失方向。不得不学习一种全新的语法来表达基本的 IDL 结构并不是一件有趣的事情。不过,我惊喜地发现 CLR COM 互操作能够处理我抛给它的所有接口。我本当以为互操作的效果只会和 VB6 差不多。

未来的增强

我目前还有一些收尾工作正在抽空进行。首先,我正在开发一个基于 HTML 的配置站点,以便您可以配置虚拟域,例如 aspx:www.mydomain.com/,将其映射到硬盘上的目录。其次,我需要弄清楚如何让 cookie 工作。我需要让 IE 来处理它们,或者自己实现它们。我认为这是协议的责任,但我不太确定。另外,目前我不会向响应添加任何额外的标头信息,如 MIME 类型或创建日期等。在未来添加这些信息会很好。

参考文献

这个出色的控件启动了这个项目。 HTML Editor

我最近一直在开发一个针对 Web 开发的单元/验收测试应用程序。我有一个版本可以自动控制 IE,但应用程序经常失去对 IE 的控制,并且对网络没有足够的底层控制。我编写了一个使用 WebBrowser 控件的版本和一个使用 HtmlEditor 控件的版本,而 HtmlEditor 控件迄今为止是最稳定的。但是,它在出错时不会提供任何 HTTP 状态通知。我编写这个协议是为了更多地了解 MSHTML 和 UrlMon 在底层是如何工作的。我也计划将其用于自动化测试的某些阶段,因为它允许我在页面执行期间以及页面执行之间访问 ASP.NET 进程的内部。这对于确保我的所有页面都能正确处理会话丢失、缓存和其他灾难性事件至关重要。

历史

  • 第一个版本。
© . All rights reserved.