`
kimmking
  • 浏览: 537237 次
  • 性别: Icon_minigender_1
  • 来自: 中华大丈夫学院
社区版块
存档分类
最新评论

IIS 7.0: 使用集成的 ASP.NET 管道增强应用程序

阅读更多
IIS 7.0
使用集成的 ASP.NET 管道增强应用程序
Mike Volodarsky
代码下载位置: PHPandIIS2008_01.exe (171 KB)
Browse the Code Online

本文以 IIS 7.0 FastCGI 组件的预发布版为基础。文中包含的所有信息均有可能变更。
本文讨论:
  • ASP.NET 集成模式
  • 添加用户身份验证
  • 启用对搜索引擎友好的 URL
  • 使用输出缓存提升性能
本文使用了以下技术:
IIS 7.0, .NET Framework
大约一年前,我撰写了一篇 IIS 7.0 概述,发表在《MSDN® 杂志》上(请参阅 msdn.microsoft.com/msdnmag/issues/07/03/IIS7 上的“IIS 7.0:探索用于 Windows Vista 的 Web 服务器和更多内容”)。那是在 IIS 7.0 随 Windows Vista® 发布前的几个月。从那时起,用户便有机会亲身体验新 IIS 7.0 组件化且可扩展的体系结构以及其他改进。
自 Windows Vista 发布后,我们一直努力工作,以确保 IIS 7.0 成为 Windows Server® 2008 中安全可靠的 Web 服务器,加强它的稳定性、性能和对承载环境的支持。我们还深刻认识到 IIS 7.0 即将成为一个灵活的 Web 应用程序平台的含义。除了作为 ASP 和 ASP.NET 等 Microsoft 应用程序框架的优秀平台,我们还希望它成为当今使用的其他多种应用程序框架的主要平台。为了促进这个目标的实现,我们增加了对 FastCGI 的支持,这是一个开放的 Web 服务器标准,它使 IIS 能够承载 PHP、Ruby on Rails 和 Perl 等应用程序框架。我们还与 PHP 的创建者 Zend Technologies 合作,以便在 Windows® 和 IIS 中提供一个可靠的高性能 PHP 实现。
IIS 7.0 不仅仅局限于提供对流行应用程序框架的生产支持。IIS 7.0 采用新的 Microsoft® .NET Framework 扩展性模型,可以充分发挥 ASP.NET 集成模式的作用,将关键功能添加到使用任何框架开发的应用程序中。这让您可以添加很酷的功能,如访问控制或新的 URL 方案,还可以显著提高性能,通常无需改动一行代码。
在本文中,我们将深入探讨 IIS 7.0 ASP.NET 集成功能,以此来增强一个并非采用 ASP.NET 开发的应用程序。我将向您展示如何才能使用现有的 ASP.NET 功能来增强应用程序,同时利用 IIS 级别的 ASP.NET 扩展性开发新功能并将其添加到应用程序。
我们要讨论的应用程序是一个流行的 PHP 图库应用程序,名为 Qdig (qdig.sourceforge.net)。我会在不改动一行 PHP 代码的情况下,向您展示如何向图库程序中添加新的便利功能。首先,我会使用 ASP.NET 成员身份和表单身份验证功能对图库进行密码保护。然后,我还会使用搜索引擎友好的 URL 代替不雅观的参数化查询字符串 URL 对其进行升级。最后,我会使用 ASP.NET 输出缓存显著改进该应用程序的性能。
不过,我们首先需要了解一下 IIS 中新的 PHP 支持的背景,这对允许像 PHP 这样的应用程序框架够享有全部 IIS 7.0 功能集是个核心问题。

IIS 和 PHP
IIS 一直支持 PHP,但其支持方式不足以在生产环境中承载现实中的许多 PHP 应用程序。这是因为 IIS 运行 PHP 应用程序采用的两种方式均有局限性:使用通用网关接口 (CGI) 协议和使用 PHP ISAPI 扩展。
因为 CGI 要求每个请求都对应一个单独的进程,所以使用 CGI 托管的应用程序在 Windows 上性能表现极差。另一方面,使用 IIS 高性能、多线程 ISAPI 接口的 PHP 应用程序通常表现得不够稳定,这是由于某些流行的 PHP 扩展缺乏线程安全性。
IIS 团队开发了 FastCGI 组件,试图解决这些问题。开放式 FastCGI 协议能够使 PHP 和许多其他要求单线程环境的应用程序框架(包括 Ruby on Rails、Perl 和 Python)更可靠地在 IIS 上运行。与标准的 CGI 实现不同,FastCGI 通过维护一个工作进程池而允许进程重用,每个进程一次处理不超过一个请求,从而使性能得到很大改进。FastCGI 还得益于以社区为中心的开发和测试模型。
与此同时,Zend Technologies 努力改进 PHP 脚本引擎和 Windows 核心扩展的总体性能和稳定性,进行了大量的性能改进,修复了许多特定于 Windows 的 bug。
在我写这篇文章的时候,php.net/downloads 上发布了 PHP 5.2.3,这是针对 Windows 承载而优化的第三个版本的 PHP,它具备针对 IIS FastCGI 平台进行了优化的快速非线程安全版本。Windows Server 2008 从 Beta 3 版本开始便内置了 IIS FastCGI 支持;对于 Windows Vista、Windows XP,可作为单独的技术预览版本下载而获得 IIS FastCGI 支持;Windows Server 2003 则提供 Go-Live 版本 (iis.net/fastcgi)。当 Windows Vista SP1 发布时,此组件还会作为安装包的一部分提供。用于 Windows XP 和 Windows Server 2003 的最终版本也将会在近期发布。您现在就可以阅读可供您运行 IIS FastCGI 的选择的更多相关信息,网址是 mvolo.com/blogs/serverside/archive/2007/10/09/IIS-FastCGI-and-PHP_3A00_-What-you-absolutely-need-to-know-to-host-PHP-applications-on-IIS-6-and-IIS-7.aspx

设置应用程序
带有 FastCGI 的 IIS 7.0 使设置 PHP 应用程序变得相当简单。首先,创建一个网站,并添加 myphpgallery 主机头绑定,这样就可以从本地计算机上通过 http://myphpgallery 访问该网站。(我还将 myphpgallery 主机名添加到 %windir%\system32\drivers\etc\hosts 中,使我的计算机知道到哪里找到该网站。)
接着,从 qdig.sourceforge.net 下载最新版本的 Qdig,并将其解压缩到网站的根目录下。为了做好测试图库的准备,我在网站的根目录下放入一组图像(您也可以创建子目录,然后将图像放入其中)。在本例中,我使用了 Windows Vista 附带的一些示例图像。
我从 php.net/downloads 下载了用于 Windows 的最新非线程安全版本的 PHP(本文截稿时为 PHP 5.2.3),并将其解压缩到 C:\php。此时,我做好了运行程序的准备,不过还需要对 php.ini 做一些 Qdig 所必需的调整:
  1. 将 php-recommended.ini 重命名为 php.ini
  2. 设置“register_long_arrays=On”
  3. 启用 GD 扩展,即“extension=php_gd2.dll”
  4. 设置正确的扩展路径,即“extension_dir=./ext”
从 IIS 的角度看,让 PHP 应用程序运行只需要几个步骤。首先,将 PHP/FastCGI 处理程序映射添加到 PHP-CGI.EXE,如以下文章所述:对于 Windows Vista,请参阅go.microsoft.com/fwlink/?LinkId=104195;对于 Windows Server 2008 和 Windows Vista SP1,请参阅go.microsoft.com/fwlink/?LinkId=104196。然后添加 index.php,作为默认文档。
最后,因为 Qdig 动态生成缩略图,所以我需要为 IIS_IUSRS 组授予对应用程序 qdig-files 子目录的写入访问权限,从而使 IIS 工作进程可以写入该目录。
这样就可以了。此时我可以点击进入 http://myphpgallery 并查看图像了,如图 1 所示。
Figure 1 The Qdig Gallery (单击该图像获得较大视图)

确保图库安全
Qdig 是一个简单的图库程序,旨在顺畅地完成一项任务:让您通过 Web 浏览您的图像集合。因此,它没有 Web 应用程序通常需要的某些较为复杂的功能。访问控制便是此类功能之一,可用来限制对 Web 应用程序的部分或全部的访问权限。
遗憾的是,在 PHP 中实现访问控制需要从零开始实现凭据存储和基于 cookie 的身份验证,而这么做很难得到正确结果。相比之下,ASP.NET 采用成员身份服务和一组内置的凭据存储提供程序、表单身份验证模块和一组预制的登录控件,提供了一套完整的解决方案,可以轻而易举地实现访问控制。
在早期版本的 IIS 中,这对 Qdig 图库的意义不大,因为它不是 ASP.NET 应用程序。而在 IIS 7.0 中,ASP.NET 集成模式引擎专门针对这种情形而设计,从而使 ASP.NET 功能可用于任何内容,包括其他应用程序框架。通过使用 ASP.NET 集成模式的功能,Qdig 图库可以利用 ASP.NET 成员身份、表单身份验证和登录控件,就如同本机 ASP.NET 应用程序一样。实际上,如果您已经拥有一个使用表单身份验证的 ASP.NET 应用程序,则可以将验证表单放入 Qdig 图库程序,只需几个步骤,便可使图库具有与其他应用程序相同的用户安全性。这样可以在整个站点中获得一致的用户安全性。
若要从零开始为图库实现表单身份验证解决方案,首先必须选择一个成员身份凭据存储提供程序来存储用户凭据。ASP.NET 2.0 随附了两个内置的提供程序,一个适用于 SQL ServerTM,另一个适用于 Active Directory®
SQL Server 提供程序可与 SQL Server 2005 Express Edition 一起使用,对许多应用程序来说,这是一个极好的选择。SQL Server 2005 Express Edition 与成员身份提供程序配合使用的最大好处是能够在第一次使用时自动创建数据库表,并部署到应用程序的 App_Data 子目录中。这不需要任何外部的数据库部署,而且可以产生一个凭据存储数据库,该数据库可以与您的应用程序文件一起进行 Xcopy。
默认情况下,SQL Server 成员身份提供程序配置为使用一个连接字符串(使用本地 SqlExpress 实例)连接到 aspnetdb.mdf 数据库,该数据库位于应用程序的 App_Data 目录中。具体实现只需执行下列三个步骤:
  1. 下载和安装 SQL Server 2005 Express Edition,让它作为名为 SqlExpress 的实例运行(默认)。(下载位置是 microsoft.com/sql/editions/express。)
  2. 在应用程序根目录下创建 App_Data 子目录,并使 IIS_IUSRS 可以写入该目录。
  3. 打开 IIS 管理器,为应用程序创建几个用户。为此,请在左侧树状视图中选择您的网站,然后单击“.NET 用户”功能图标(请参见图 2)。请务必在完成操作后关闭该工具,因为 SQL Server Express Edition 每次只允许一个用户标识访问数据库。
Figure 2 Configuring Users in IIS Manager (单击该图像获得较大视图)
请注意 IIS 管理器与 ASP.NET 成员身份基础结构之间的集成,您可以在用来配置其他 IIS 身份验证和访问控制功能的管理工具中直接管理用户和角色。实际上,当您创建第一个用户时,如果尚不存在成员身份数据库,IIS 管理器就会在应用程序中自动创建一个。

表单身份验证
既然成员身份已经就绪,接下来就需要为应用程序启用表单身份验证了。我还需要让实现身份验证服务的表单身份验证模块针对应用程序中的所有请求运行,而不只是针对 ASP.NET 请求(这是为实现向后兼容的默认行为)。可以直接在网站的 web.config 文件中完成所有这些设置,如下所示:
<configuration>
    <system.webServer>
        <modules>
            <remove name="FormsAuthentication" />
            <add name="FormsAuthentication" 
             type="System.Web.Security.FormsAuthenticationModule" />
        </modules>
    </system.webServer>
    <system.web>
        <authentication mode="Forms" />
    </system.web>
</configuration> 
请注意,尽管已经在服务器级别声明了 FormsAuthentication 模块,但是仍有必要删除它,然后再重新声明,以便清除 preCondition 属性,默认情况下该属性设置为 managedHandler。这会强制该模块只针对发送给托管 (ASP.NET) 处理程序的请求运行。既然您希望表单身份验证同时还保护 PHP 应用程序,就需要删除该模块。
此时,应用程序已配置为使用表单身份验证。但是,我没有指明任何内容真正要求用户经身份验证后才能访问,我依然保留了匿名身份验证,允许对网站的匿名访问,所以绝对不需要使用身份验证。实际上,我只需要允许对登录页面的匿名访问,其余内容则需要进行身份验证。这可以通过在配置文件中配置声明式授权规则来实现。图 3 中显示了我的 web.config 文件中的完整配置。
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <system.webServer>
        <!-- The PHP handler mapping -->
        <handlers>
            <add name="PHP via FastCGI" path="*.php" verb="*"                            modules="FastCgiModule" scriptProcessor="
                c:\php\php-cgi.exe" resourceType="Unspecified" />
        </handlers>
        <!-- Add index.php as a default document -->        
        <defaultDocument>
            <files>
                <add value="index.php" />
            </files>
        </defaultDocument>
        <security>
            <!-- Deny access to anonymous users -->
            <authorization>
                <add accessType="Deny" users="?" />
            </authorization>
        </security>
        <!-- Let the FormsAuthentication module run for all requests -->
        <modules>
            <remove name="FormsAuthentication" />
            <add name="FormsAuthentication" type=
                "System.Web.Security.FormsAuthenticationModule" />
        </modules>
    </system.webServer>
    <system.web>
        <!-- Turn on Forms Authentication for this application -->
        <authentication mode="Forms" />
    </system.web>
</configuration>

采用此配置,只有经过身份验证的用户才能访问 PHP 图库。URL 授权功能会拒绝未经验证的用户,并由表单身份验证功能将其重定向到登录页面,用户可通过该页面使用成员身份服务和 SQL Server 凭据数据库进行登录。
最后一步是登录页面,它将所有这些功能连成一体,该页面提供一个登录 Web 表单,使用成员身份服务验证用户的凭据,并向用户颁发一个加密的表单身份验证登录票证。如果您此时想要卷起衣袖准备编写代码,请打住。不用您亲自动手,此登录页面已经备好了。您要做的全部工作就是在应用程序的根目录下创建一个叫做 login.aspx 的简单页面(您可以在 <forms> 配置区段中改写登录页面的路径)。新创建的页面只使用一个 Login 控件:
<html>
  <head>
    <title>Login to my blog</title>
  </head>
  <body>
    <form runat="server">
      <asp:Login runat="server" />
    </form>
  </body>
</html>
该 Login 控件会自动显示登录表单,在表单提交之后验证用户的凭据,并颁发表单身份验证票证。然后,此票证就会用于自动验证您在余下时间对网站的访问,直到您关闭浏览器窗口或处于非活动状态而导致票证过期为止。
图 4 显示了一个简洁版本的登录页面;为了提供更加便捷的用户体验,该页面还使用几个与登录相关的其他控件。LoginView 控件可让您在匿名用户视图和已验证用户视图之间选择,LoginName 控件提供已验证用户的名称,LoginStatus 控件为已验证用户提供一个注销链接。您可以指定登录控件在客户端的显示方式,具体做法是设置一个格式属性(有很多),或者使用自定义 CSS 应用主题或外观。
<html>
  <head>
    <title>Login to view the gallery</title>
  </head>
  <body>
    <form runat="server">
      <asp:LoginView runat="server">
        <AnonymousTemplate>
          Please log in to proceed.
          <br /><br />
          <asp:Login DestinationPageUrl="/"  runat="server" />        
        </AnonymousTemplate>
        <LoggedInTemplate>
          Welcome <asp:LoginName runat="server" />!
          <br /><br />
          <asp:LoginStatus runat="server" />
        </LoggedInTemplate>
      </asp:LoginView>
    </form>
  </body>
</html>

这样就完成了。现在,当您访问图库时,就会自动重定向到登录页面,如图 5 所示。登录时,您需要输入先前创建的任一用户的用户凭据,然后就可以随意访问了!
Figure 5 The Completed Login Page (单击该图像获得较大视图)

搜索引擎友好的 URL
默认情况下,Qdig 图库程序使用难看但可行的查询字符串 URL 在您的图库中导航。以下是我的图库中的 Qdig URL 示例:
http://myphpgallery/index.php?Qwd=./Mike&Qif=Flower.jpg
不太雅观,也不太好记。更重要的是,它使搜索引擎很难正确地索引图库的内容。
我希望让 Qdig 使用搜索引擎友好的 URL,Web 2.0 应用程序目前很流行此类 URL,因为它们不仅为用户带来了非常直观的浏览体验,而且还能更有效地让您的网站被搜索引擎索引。例如,我先前展示的友好 URL 版本如下:
http://myphpgallery/index.php/Mike/Flower.jpg
这会使子图库和图像名称置于 URL 路径中,不仅看起来更直观,而且重要的关键字放在 URL 的绝对路径中,使它更容易被搜索引擎索引。
非常有意思的是,服务器在默认情况下会将此 URL 正确地传送到 index.php,而将余下的“/Mike/Flower.jpg”识别为 PATH INFO 段。但是,随之而来的问题是,此 URL 并不遵循 Qdig 应用程序脚本 index.php 所期望的 URL 格式。即便我们可以让该 URL 调用 index.php 脚本,Qdig 仍找不到所需的查询字符串参数,所以不知道如何正确处理。
幸运的是,ASP.NET 集成管道再次来帮助解除困境,这一次,我们用它来编写一个小型 .NET 模块,该模块可以动态地重写传入的 URL,将比较友好的格式重写为 Qdig 的本机 URL,从而将正确的图库视图返回给客户端。借助 ASP.NET 集成管道,此模块可采用与处理 ASP.NET 内容请求相同的方式处理 PHP 请求。
为此,我将使用 ASP.NET IHttpModule 模式实现一个托管模块。在位于 mvolo.com/blogs/serverside/archive/2007/08/15/Developing-IIS7-web-server-features-with-the-.NET-framework.aspx 的“使用 .NET Framework 开发 IIS7 模块和处理程序”一文中,您可以阅读有关构建此类 IIS 7.0 模块的更多内容。
该模块将完成下列功能:
  1. 订阅请求处理的 BeginRequest 阶段。
  2. 截获 URL 为友好格式的请求。
  3. 从 URL 中提取子图库路径和图像文件名。
  4. 重写对 Qdig index.php 的请求,传递 Qdig 所期望的查询字符串参数中的子图库路径和图像文件名。
编写此模块其实非常简单。首先,需要创建一个 C# 类以实现 System.Web.IHttpModule 接口,并在 Init 方法内将一个事件处理程序绑定到 BeginRequest 事件:
public class QdigSEFModule : IHttpModule
{
    public void Init(HttpApplication application)
    {
        application.BeginRequest += new EventHandler(OnBeginRequest);
    }
    ...
}
然后,在 OnBeginRequest 方法中实现重写功能,如图 6 所示。
public void OnBeginRequest(Object source, EventArgs e)
{
    HttpApplication app = (HttpApplication)source;
    HttpContext context = app.Context;

    // Extract the gallery path and image filename from the url
    // code omitted for clarity ...

    // Build the rewritten url
    String rewrittenUrl = String.Format("~/index.php?Qwd=.{0}&Qif={1}",
                                        galleryPath,
                                        imageFileName);

    String originalQueryString =      
             context.Request.ServerVariables["QUERY_STRING"];
    if (!String.IsNullOrEmpty(originalQueryString))
    {
        rewrittenUrl += "&" + originalQueryString;
    }

    // Rewrite the url
    context.Server.TransferRequest(rewrittenUrl);
}

请注意 HttpServerUtility.TransferRequest 的使用,这是 .NET Framework 3.5 中的新 ASP.NET API,用于保证 IIS 7.0 ASP.NET 集成管道中的正确重写。此 API 强制停止针对当前请求的请求处理,并执行对目标 URL 新的子请求。这使托管模块可以向另一个 URL 完整地传输请求处理,而不管目标内容类型为何。您可以在本期的下载中找到此模块的完整源代码。
编写完该模块后,我需要将它部署到应用程序中。有多种部署方法,包括将该模块编译到一个 .NET 程序集,然后将该程序集部署到应用程序的 BIN 目录中。不过,我宁愿采用最简便的方法:只复制该模块的源代码,并将其作为 QdigSEFRewriter.cs 保存到应用程序 App_Code 子目录中。在应用程序启动过程中,ASP.NET 编译系统会自动编译该代码,使 ASP.NET 运行时能够使用它。
最后一步是使该模块能够在应用程序中运行,具体方法是在应用程序的 web.config 文件的 <modules> 配置区段中注册该模块,默认情况下,该区段中已经注册了许多内置的 ASP.NET 模块,比如表单身份验证等。在为访问控制而创建的 web.config 文件上,我可以添加新模块,如下所示:
<system.webServer>
    <modules>
        <remove name="FormsAuthentication" />
        <add name="FormsAuthentication" type=
            "System.Web.Security.FormsAuthenticationModule" />
        <!-- Add the SEF url rewriting module -->
        <add name="QdigSEFUrlsModule" type="QdigModules.QdigSEFModule" />
    </modules>
</system.webServer>
从现在开始,您可以使用友好的 URL 替代 Qdig 本机查询字符串 URL 来访问 Qdig 图库了。若要访问图库 Mike 子目录中的 Flower.jpg 文件,只需按如下所示请求该文件即可:
http://myphpgallery/Mike/Flower.jpg/show
请注意,我为友好的 URL 选用了 /show 后缀,以便与指向图像文件的直接链接区分开来,并防止我的模块重写它并未打算重写的 URL。当然,您也可以按照适合您需要的方式修改重写方案。

生成友好超链接
不幸的是,这还没有全部完成。虽然现在可以使用友好的 URL 访问图库,但图库页面本身仍包含旧格式的超链接:
<a href="http://index.php?Qwd=./Mike&amp;Qif=Arctica.jpg&amp;Qiv=thumbs&amp;
Qis=M" title="First Image"> ... </a>
用户单击这些链接时仍将看到不雅观的查询字符串 URL,搜索引擎索引内容时也不能将父页面的友好 URL 与页面正文中的链接关联起来。
让 Qdig 生成的超链接采用我在前面步骤中使用的同样友好的 URL,就会真正实现便利性。但为此而需要重写 PHP 脚本来生成不同的超链接,这是我不愿意做的。
实际上我不需要这么做,这多亏了 ASP.NET 响应筛选功能。我可以编写另一个托管模块,该模块动态地筛选 Qdig 响应,将旧格式的超链接替换为友好的新格式链接。
这一次,我的模块将完成下列功能:
  1. 订阅请求处理的 PreRequestHandlerExecute 阶段(刚好在处理程序执行前)。
  2. 检查 URL 是否适用于 Qdig index.php 脚本。
  3. 在响应上设置响应筛选流,这将使用正则表达式替换所有友好的超链接。
该模块类相当简单,因为它的全部工作就是有选择性地绑定筛选流(请参见图 7)。真正的工作是在 QdigSEFFilter 类中完成的,该类负责将所有的 Qdig 超链接替换为采用新格式的超链接。筛选器类会实现抽象的 System.IO.Stream 类,ASP.NET 运行时用它来在请求处理结束时筛选响应。ASP.NET 集成管道引擎允许响应筛选器处理任何响应,甚至那些不是由 ASP.NET 生成的响应,所以,只要缓存了响应,就可用它来处理 PHP 响应以及静态文件和 ASP 页面。IIS 7.0 在默认情况下缓存所有响应(直至达到配置的极限),从而确保该机制可以正常工作。
我的筛选器实现将缓存通过响应流推送的所有传入响应字节,使用响应的字符集将它们转换为一个字符串,然后对 URL 执行正则表达式替换。随后,它将该字符串重新编码为原始字符集,并将响应数据推送回运行时。您可以在代码下载中找到该模块和响应筛选器的完整实现。
我再次将模块代码作为单独的源文件 QdigSEFFilter.cs 部署到 App_Code 目录中,并在应用程序的 web.config 文件的 <modules> 配置区段中注册该模块。此时的 modules 区段如下所示:
<system.webServer>
    <modules>
        <remove name="FormsAuthentication" />
        <add name="FormsAuthentication" type=
            "System.Web.Security.FormsAuthenticationModule" />
        <!-- Add the SEF url rewriting module -->
        <add name="QdigSEFUrlsModule" type="QdigModules.QdigSEFModule" />
        <!-- Add the response filter module -->
        <add name="QdigSEFFilterModule" type=
            "QdigModules.QdigSEFFilterModule" /> 
    </modules>
</system.webServer>
在刷新 http://myphpgallery/ 上的 Qdig 页面并登录后,页面上的所有超链接已更换为使用友好的 URL(请参见图 8)。到目前为止,我并不需要更改原始应用程序的任何一行代码,只是使用 ASP.NET 的现有功能和针对 ASP.NET 集成管道开发的新功能对原始程序的功能进行了一些重要升级。
Figure 8 The Revised Gallery with Friendly URLs (单击该图像获得较大视图)

测试性能
至此,虽然我没有更改 Qdig 的任何源代码,但我向该应用程序添加了诸多功能。所有这些新功能对应用程序的性能有多大影响呢?由于 Windows 上 CGI 机制的进程启动开销,性能历来是 IIS 平台上 PHP 应用程序的重要问题。通过消除此开销,FastCGI 有望显著改进应用程序的性能,从而使 IIS 成为承载 PHP 和其他 FastCGI 兼容的应用程序的更具吸引力的平台。然而,如果我刚才引入的应用程序增强又使此类开销攀升回去的话,那么,考虑在 IIS 上部署 PHP 应用程序便不会有如此大的吸引力。
不过,我添加的功能增强对 Qdig 的性能其实影响很小。图 9 显示了我对该应用程序执行的性能测试的概要。
测试采用 Microsoft Web Capacity Analysis Tool (WCAT) 6.3 执行,服务器上 CPU 利用率达到满负荷,同时运行了 100 个虚拟客户端,请求了图库内的一系列 Qdig URL。最后的测试使用了友好的 URL 取代 Qdig URL,以便利用转换功能。为了方便测试,禁用了身份验证。
您可以看到,从 CGI 转移到 FastCGI,吞吐量几乎提高了三倍。在添加两个友好 URL 模块后,吞吐量只下降了 5%,我看这微不足道。
遗憾的是,Qdig 与 CPU 性能联系紧密,因此不可能获得由 FastCGI 提供的大幅度性能提升。相比而言,对于简单的 hello.php 脚本,当从 CGI 转移到 FastCGI 时,我看到吞吐量提高了 43 倍。即使在添加响应筛选器(一项代价很高的操作)之后,对总吞吐量的影响与应用程序自身的开销相比也是微不足道的。
只要涉及服务器性能调优,这种情形就变得相当糟糕,因为除了应用程序改进自身性能以外,您也无能为力。除非您是应用程序的开发者,否则,更改应用程序自身的代码通常是不可能的,况且也很难更改。对于我来说当然不予考虑,因为我一开始就决定不修改应用程序本身。

ASP.NET 输出缓存
输出缓存是 ASP.NET 中的一项功能,利用此功能,对相同资源的后续请求可以重用 Web 应用程序所产生的响应。实际上,ASP.NET 提供了一组非常丰富的缓存功能,利用其缓存 API 可以缓存 ASP.NET 应用程序的内部数据,利用输出缓存则可以缓存应用程序的整个响应。这两种功能通常可以增强应用程序的性能,并显著降低后端资源的负载。IIS 7.0 中的新增功能可以让非 ASP.NET 内容类型使用 ASP.NET 输出缓存,这正是我要使用的功能,以便为图库带来一些性能提升。
应用程序性能调优比较琐碎,通常需要大量的编码工作,成块地消除应用程序的处理开销,相对而言,输出缓存则倾向于按一定比例彻底消除请求负载的整体开销。其有效性取决于应用程序的使用位置:应用程序的用户请求访问相同内容的频率。对于典型的应用程序而言,通常适用 90/10 规则,其基本含义是:90% 的请求总是对应 10% 的内容。如果您能将这些内容进行输出缓存处理,则意味着您的应用程序可以提高几乎 90% 的吞吐量和能力。
当然,如果这么容易做到,人人都会去做了。输出缓存确实具有一系列限制,尤其当它面对动态内容时,这些限制通常使缓存解决方案难以部署。主要的限制是由于内容的动态性造成的。例如,网站的页面可能基于查询字符串参数、具体访问时间或数据库更新而产生不同的响应。如果忽视这些因素,就可能向用户返回不正确的缓存响应,这可不是什么好事。幸运的是,ASP.NET 输出缓存考虑了所有这些限制,从而为您的应用程序部署输出缓存解决方案提供了一个丰富的平台。
ASP.NET 页面解析器提供对 <%@ OutputCache %> 指令的支持,从而使许多输出缓存设置都可直接在 ASPX 页面配置。实际上,这是大多数开发人员为其 ASP.NET 应用程序启用输出缓存的方式。因为 Qdig 是用 PHP 编写的,所以它不能利用此项功能。因此,我将编写另一个模块,使用 ASP.NET HttpCachePolicy API 动态地配置输出缓存,让输出缓存模块能够正确地缓存 Qdig 的响应:
public class QdigOutputCacheModule : IHttpModule
{
    public void Init(HttpApplication application)
    {
        application.PostAuthorizeRequest += 
            new EventHandler(OnPostAuthorizeRequest);
        application.PostRequestHandlerExecute += 
            new EventHandler(OnPostRequestHandlerExecute);
    }

    public void Dispose(){}
}
这一次,此模块将订阅两个管道事件:PostAuthorizeRequest 和 PostRequestHandlerExecute。第一个事件 PostAuthorizeRequest 恰好在输出缓存模块试图查找对请求的现有响应之前发生,此时不作进一步处理即返回响应。PostRequestHandlerExecute 事件恰好在处理程序 (PHP) 产生响应之后、输出缓存模块试图保存响应以供后续使用之前发生。
在 PostAuthorizeRequest 事件中,我需要告诉输出缓存模块 Qdig 如何依据查询字符串作出不同的响应,以便该模块能找到正确的响应(请参见图 10)。依据查询字符串参数对缓存进行不同的配置,通过响应用户输入(比如更改导航模式或单击下一个链接转到下一幅图像),Qdig 图库可以正确地进行操作。如果不这么做的话,第一个请求最终会缓存它的响应,并为所有对图库的后续请求返回该响应,而不管请求的是哪幅图像。另外,我还依据验证的用户进行不同的配置(参见 GetVaryByCustomString 实现的详情),从而使缓存正确地为每个验证用户提供自己的缓存副本。
public void OnPostAuthorizeRequest(Object source, EventArgs e)
{
    HttpApplication app = (HttpApplication)source;
    HttpContext context = app.Context;

    // Make sure we only process requests to QDIG's index.php.
    String requestPath = context.Request.Path;
    if (!requestPath.Equals(VirtualPathUtility.ToAbsolute
        ("~/index.php")))
        return;

    //  Set up the vary by querystring information 
    HttpCacheVaryByParams varyByParams = 
      context.Response.Cache.VaryByParams;
    varyByParams["Qwd"] = true; //  gallery path
    varyByParams["Qif"] = true; //  image to display
    varyByParams["Qiv"] = true; //  navigation mode
    varyByParams["Qis"] = true; //  image size
    varyByParams["Qtmp"] = true;//  other control information

    // if user is set, also vary by user (See global.asax)
    context.Response.Cache.SetVaryByCustom("AuthenticatedUser");
}

在 PostRequestHandlerExecute 事件中,我需要确定是否应当缓存响应,并配置适当的设置来允许缓存响应(请参见图 11)。请注意 AddFileDependency 调用。在省略的代码中,我确定图库目录和图像的物理路径(如果已指定),并在响应中添加文件依赖关系。这采用输出缓存支持的 CacheDependency 机制,用以监视这些资源的变化,当资源被修改时使缓存的响应无效。这样可以使缓存的响应保留很长时间,只有当底层数据更改时才使它无效。当有人放入新图像或创建新的子图库时,缓存功能会自动删除受影响的缓存响应,任何新请求都会得到更新的页面。
public void OnPostRequestHandlerExecute(Object source, EventArgs e)
{
    HttpApplication app = (HttpApplication)source;
    HttpContext context = app.Context;

    // Make sure we only process requests to QDIG's index.php.
    String requestPath = context.Request.Path;
    if (!requestPath.Equals(VirtualPathUtility.ToAbsolute
        ("~/index.php")))
        return;

    // Enable this response to be output cached
    context.Response.Cache.SetCacheability(HttpCacheability.Public);
    context.Response.Cache.SetExpires(DateTime.Now + 
      TimeSpan.FromMinutes(5));

    // code omitted for clarity ...

    // add the dependencies to the response
    context.Response.AddFileDependency(physicalDirectoryPath);
    context.Response.AddFileDependency(physicalFileName);
}

CacheDependency 机制是一个功能强大的抽象,可用来创建您自己的、可监视任何种类底层数据的 CacheDependency 实现,例如,调用 Web 服务或检查注册表项。内置的 CacheDependency 实现包含对监视文件、目录和 SQL Server 数据库表的支持。

部署和测试
我部署最后一个模块的方法与前面采用的方法相同,就是将模块的源文件 QdigOutputCache.cs 放到 App_Code 目录中,并在 web.config 中注册该模块:
<system.webServer>
    <modules>
      ... 
      <!-- Let the OutputCache module run for all requests -->
      <remove name="OutputCacheModule" />
      <add name="OutputCacheModule" type=
          "System.Web.Caching.OutputCacheModule" /> 
      <!-- Add the custom output caching module --> 
      <add name="QdigOutputCacheModule" type=
          "QdigModules.QdigOutputCacheModule" /> 
    </modules>
</system.webServer>
除了添加 QdigOutputCacheModule,我还重新添加了 OutputCacheModule,目的是删除其 managedHandler 先决条件,这与我之前针对表单身份验证的做法相同。这样可以使 ASP.NET OutputCacheModule 在发生对 PHP 内容的请求时运行,从而提供输出缓存服务。
最后,我创建了一个 global.asax 文件(请参见图 12),该文件包含 GetVaryByCustomString 的实现,GetVaryByCustomString 这个函数允许我的模块依据验证的用户采取不同的缓存响应。这十分重要,因为它为每个验证的用户保留各自的缓存副本,从而防止某个用户偶然获得其他用户的个性化视图。既然 Qdig 不提供个性化视图,这就不算问题,不过我还是将其作为最佳实践展示一下。
<%@ Application language="C#" %>
<script runat="server" language="c#">
public override string GetVaryByCustomString(HttpContext context, 
    string s)
{
    if ("AuthenticatedUser".Equals(s))
    { 
        // vary by authenticated user
        String currentUser = String.Empty;
        if (context.User != null)    
        {
            currentUser = context.User.Identity.Name;
        }

        return currentUser;
    }
    else
    { 
        // unknown vary string
        return null;
    }
}
</script>

现在,我们检查一下性能是否得到了进一步增强。图 13 中的图表显示了完整的结果,其中包含以前的测试。
Figure 13 Performance Results (单击该图像获得较大视图)
意料之中,输出缓存没有让人失望,它将 Qdig 的性能从基线 FastCGI 加上搜索引擎友好 (SEF) URL 基准下的每秒 88 个请求,提高到服务器完全利用情况下的每秒 1386 个请求。
总之,本机 PHP 应用程序从 CGI 转移到 FastCGI,并采用友好的 URL 和输出缓存对其进行升级之后,最终结果显示了超过原始
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics