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

ASP.NET 中的图片缓存

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.77/5 (56投票s)

2008 年 1 月 13 日

CPOL

9分钟阅读

viewsIcon

424718

downloadIcon

3933

提高 Web 应用程序性能最简单但最有效的方法之一是在客户端缓存图片。

引言

提高 Web 应用程序性能的方法有很多。最简单但最有效的方法之一是在客户端缓存图片。在本文中,我想介绍我们如何为我们的 DotNetNuke 网站实现了图片缓存。

问题

在我构建网站 http://www.software-architects.com 时,我在 CSS 样式表中使用了大量图片来显示菜单项的背景图片。将文件传输到我们的 Web 服务器后,我使用 Microsoft Network Monitor 测试了对我们主页的请求会产生多少流量。这是一个允许捕获和协议分析网络流量的工具。您可以从 Microsoft 下载中心下载它。

使用 Microsoft Network Monitor 3.1,我记录了一个对 http://www.software-architects.com 的调用。结果是,显示单个页面需要 20 次对 20 个不同文件的请求。Microsoft Network Monitor 显示,大约一半的请求是用于菜单图片的。

CachingImagesInASPNET/MNM_withoutCaching.png

有两种不同的方法可以避免这个问题。一方面,您可以告诉 IIS 在客户端缓存图片,另一方面,您可以直接在 ASP.NET 中完成(这会稍微复杂一些)。

在 IIS 中缓存图片

IIS 中的缓存非常简单。在左侧窗格中选择一个文件夹,或在右侧窗格中选择单个文件,然后打开“属性”对话框。

CachingImagesInASPNET/IIS_Images.png

选中“启用内容过期”,然后选择您的内容何时过期。

CachingImagesInASPNET/IIS_CachingProperties.png

就是这样!IIS 会通过“Cache-Control”标头告诉客户端该内容可以在客户端缓存。“Expires”标头包含过期日期。因此,客户端知道在此日期之后它必须向服务器请求新内容。

这种方法在以下情况下效果很好:

  • 您可以将所有图片和其他可缓存文件放在一个或几个文件夹中,
  • 最重要的是,您可以访问 IIS。

在我们的情况下,这两个条件都不满足。在我们的 DotNetNuke 项目中,图片分布在多个文件夹中,因此配置 IIS 会非常复杂。更重要的是,我们的托管提供商不向我们提供 IIS 访问权限。因此,我不得不寻找其他解决方案。

使用自定义 HttpHandler 缓存图片

我首先要解决的问题是绕过 IIS,让请求到达 ASP.NET。我决定编写一个自定义 HTTP 处理程序,该处理程序监听路径为 *.gif.ashx*.jpg.ashx*.png.ashx 的文件。您可以在 APress 网站上找到一篇关于 IHttpHandler 的好文章:使用局部范围提高性能

我在 Visual Studio 中构建了一个新的类库项目,其中包含一个名为 CachingHandler 的类,该类负责处理对图片的请求。CachingHandler 实现了 IHttpHandler 接口,就像 Page 类一样。该接口提供了 IsReusable 属性和 ProcessRequest 方法。

IsResuable 指示另一个请求是否可以重用 HTTP 处理程序。这意味着我们必须确保 ProcessRequest 方法是线程安全的。

ProcessRequest 执行实际工作。它获取当前上下文并负责将结果发送给客户端。

namespace SoftwareArchitects.Web
{
  public class CachingHandler : IHttpHandler
  {
    public bool IsReusable
    {
      get { return true; }
    }
  
    public void ProcessRequest(HttpContext context)
    {
      ...
    }
  }
}

我们希望我们的 http 处理程序向客户端发送文件。由于我们监听的路径为 *.gif.ashx*.jpg.ashx*.png.ashx 的文件,因此我们只需从请求路径中删除“.ashx”即可获得要发送给客户端的文件。此外,我们还从文件中提取文件名和扩展名。

public void ProcessRequest(HttpContext context)
{
  string file = context.Server.MapPath
    (context.Request.FilePath.Replace(".ashx", ""));
  string filename = file.Substring(file.LastIndexOf('\\') + 1);
  string extension = file.Substring(file.LastIndexOf('.') + 1);

下一步,我们从 web.config 文件加载 CachingHandler 的配置。为此,我创建了一个名为 CachingSection 的类(稍后会展示),其中包含一个 CachingTimeSpan 属性和一个 FileExtensions 集合,该集合知道每个文件扩展名的内容类型。借助此配置类,我们可以配置响应的 HttpCachePolicy 对象。

  • SetExpires 告诉客户端内容应该有效多长时间。
  • SetCacheability 告诉客户端谁可以缓存内容。我们将缓存设置为 public。这意味着响应可以被客户端和共享(代理)缓存。
  • SetValidUnitExpires 指定 ASP.NET 缓存是否应忽略客户端发送的使缓存失效的 HTTP Cache-Control 标头。
  • ContentType 设置响应的 MIME 类型。
CachingSection config = (CachingSection)context.GetSection(
            "SoftwareArchitects/Caching");
if (config != null)
{
    context.Response.Cache.SetExpires
      (DateTime.Now.Add(config.CachingTimeSpan));
      context.Response.Cache.SetCacheability(HttpCacheability.Public);
      context.Response.Cache.SetValidUntilExpires(false);

    FileExtension fileExtension = config.FileExtensions[extension];
    if (fileExtension != null)
    {
      context.Response.ContentType = fileExtension.ContentType;
    }
}

最后,我们将 content-disposition 标头添加到响应中,以告诉客户端它应该在浏览器中打开文件(内联)。此外,我们将文件名设置为不带“.ashx”扩展名的名称,因为当您尝试下载文件时,会显示此名称。然后我们使用 WriteFile 将文件发送给客户端。

  context.Response.AddHeader("content-disposition", 
    "inline; filename=" + filename);
    context.Response.WriteFile(file);
}

在 web.config 中定义自定义配置节

在 HTTP 处理程序中,我们使用自定义类从 web.config 文件读取一些配置信息。为此,我创建了一个名为 CachingSection 的类,它派生自 ConfigurationSection。在此类中,我实现了一个 CachingTimeSpan 属性,该属性保存一个 TimeSpan 值,用于客户端缓存对象的时长,以及一个 FileExtensions 属性,它保存 FileExtension 对象的集合。要将这些属性映射到 web.config 中的元素,您只需为每个属性添加一个 ConfigurationProperty 特性,该特性可以在 web.config 中设置。

namespace SoftwareArchitects.Web.Configuration
{
  /// <summary>
  /// Configuration for caching
  /// </summary>
  public class CachingSection : ConfigurationSection
  {
    [ConfigurationProperty("CachingTimeSpan", IsRequired = true)]
    public TimeSpan CachingTimeSpan 
    {
      get { return (TimeSpan)base["CachingTimeSpan"]; }
      set { base["CachingTimeSpan"] = value; }
    }

    [ConfigurationProperty("FileExtensions", IsDefaultCollection = true, 
      IsRequired = true)]
    public FileExtensionCollection FileExtensions 
    {
      get { return ((FileExtensionCollection)base["FileExtensions"]); }
    }
  }

为了支持集合而不仅仅是单个值,我们必须实现一个派生自 ConfigurationElementCollection 的类。在我们的示例中,我们需要一个集合来配置有效扩展名及其相应内容类型的列表。

您可以下载文件 CachingSection.cs 的完整代码。

/// <summary>
/// List of available file extensions
/// </summary>
public class FileExtensionCollection : ConfigurationElementCollection
{
    ...
}

最后,我们需要一个用于每个扩展名的类,它保存一个用于扩展名的属性和一个用于内容类型的属性。

/// <summary>
/// Configuration for a file extension
/// </summary>
public class FileExtension : ConfigurationElement
{
    [ConfigurationProperty("Extension", IsRequired = true)]
    public string Extension
    {
      get { return (string)base["Extension"]; }
      set { base["Extension"] = value.Replace(".", ""); }
    }
  
    [ConfigurationProperty("ContentType", IsRequired = true)]
    public string ContentType
    {
      get { return (string)base["ContentType"]; }
      set { base["ContentType"] = value; }
    }
}

我们现在只需要做的是在我们的 web.config 中添加一个配置节。在 configSections 标签中,我们添加一个名为 SoftwareArchitects 的新 sectionGroup。在此组中,我们添加一个名为 Caching 的节。type 属性指定了 CachingSection 类的类和程序集。当然,我们必须将包含 CachingSection 类的程序集添加到 Web 应用程序的 bin 文件夹中。然后,我们可以向 configuration 标签添加一个名为组名的标签。在组内,我们添加一个名为节名的标签,在此节中,我们在 CachingSection 类中定义的所有属性现在都可用。

<configuration>
  <configSections>
    <sectionGroup name="SoftwareArchitects">
      <section name="Caching" requirePermission="false" 
        type="SoftwareArchitects.Web.Configuration.CachingSection, 
        SoftwareArchitects.Web.CachingHandler" />
    </sectionGroup>
  </configSections>

  <SoftwareArchitects>
    <Caching CachingTimeSpan="1">
      <FileExtensions>
        <add Extension="gif" ContentType="image\gif" />
        <add Extension="jpg" ContentType="image\jpeg" />
        <add Extension="png" ContentType="image\png" />
      </FileExtensions>
    </Caching>
  </SoftwareArchitects>

  ...

现在,在我们能够使用 CachingHandler 之前,只剩最后一件事了。我们必须将其添加到 web.confighttpHandlers 节中。在那里,我们必须为每个要映射到我们的 HTTP 处理程序的扩展名添加一个条目。我决定支持具有 .gif.jpg.png 扩展名的图片。因此,我为路径 *.gif.ashx*.jpg.ashx*.png.ashx 添加了一个处理程序。在 type 属性中,我指定了 HTTP 处理程序的类和程序集。当然,程序集也必须放在 bin 文件夹中。

<httpHandlers>
    <add verb="*" path="*.gif.ashx" 
      type="SoftwareArchitects.Web.CachingHandler, 
      SoftwareArchitects.Web.CachingHandler"/>
    <add verb="*" path="*.jpg.ashx" 
      type="SoftwareArchitects.Web.CachingHandler, 
      SoftwareArchitects.Web.CachingHandler"/>
    <add verb="*" path="*.png.ashx" 
      type="SoftwareArchitects.Web.CachingHandler, 
      SoftwareArchitects.Web.CachingHandler"/>
  </httpHandlers>
</configuration>

您也可以使用其他文件扩展名,如 *.gifx。但要这样做,您需要访问 IIS 来配置新的扩展名以由 aspnet_isapi.dll 处理。由于我无法访问我们托管提供商的 IIS,因此我不得不使用 *.ashx,因为它已经映射到 aspnet_isapi.dll

最后,我将 .ashx 扩展名添加到网站中的所有图片(在 .css 文件和 .aspx 文件中)。当我再次监控对 http://www.software-architects.com 主页的请求时,第一次请求仍然生成了 20 个到 Web 服务器的请求,但从第二次请求开始,加载页面只需要 7 个请求,因为图片已在客户端缓存。

CachingImagesInASPNET/MNM_withCaching.png

您可以在我们的网站 http://www.software-architects.com/devblog/2008/01/19/Caching-in-ASPNET 上看到它是如何工作的。右键单击图片并打开“属性”对话框。您会看到 URL 以 .ashx 结尾。当您右键单击图片并选择“另存为...”时,建议的文件名不包含 .ashx 扩展名,因为 content-disposition 标头。

当然,您也可以将此处理程序用于其他文件类型,如 JavaScript 文件或 CSS 文件。这样,您可以再次减少请求的数量。

测试 CachingHandler

您可以使用一个简单的网站轻松测试图片的缓存。我在 Visual Studio Solution 中添加了一个名为 CachingWebSite 的网站项目,您可以使用它来尝试它是如何工作的(下载完整解决方案)。一方面,网站包含一个名为 Default.aspx 的页面,其中包含一个图像标签。您可以看到图像源以 .ashx 结尾。

<img src="/Portals/1/App_Themes/Standard/Images/LogoWithMenuBackground.png.ashx" />

另一方面,网站包含一个名为 Standard 的主题和一个名为 Style.css 的样式表。在样式表中,我使用了一个背景图片。同样,图像源以 .ashx 结尾。

body 
{
  margin: 0px;
  padding: 0px;
  background-image: url(Images/MenuBackground.png.ashx);
  background-repeat: repeat-x;
}

在网站的 web.config 中,我插入了一个自定义节来配置 CachingHandler,并为每个扩展名添加了一个 HTTP 处理程序,就像上面解释的那样 。此外,我还将 trace 标签添加到 system.web 节中,以跟踪对文件的每个请求。

<trace enabled="true" pageOutput="false" requestLimit="50" mostRecent="true" />

当我启动我的网站项目时,我看到 Default.aspx 页面,其中包含在 Default.aspx 中定义的 logo,以及在样式表中定义的背景图片。

CachingImagesInASPNET/CachingWebSite.png

要查看跟踪信息,我在 IE 中打开了一个新选项卡,并在 URL 中将 Default.aspx 替换为 Trace.axd。跟踪显示,显示 Default.aspx 页面需要四次请求。对于第一次请求以及用户每次按 F5 时,所有文件都会发送到客户端。

CachingImagesInASPNET/Trace_withoutCaching.png

当我切换回第一个选项卡时,我有三种重新加载页面的可能性。我可以

  • 按 F5
  • 点击“重新加载页面...”链接
  • 点击“重新加载页面...”按钮

按 F5 会重新加载所有内容,而点击链接或按钮只会重新加载未在客户端缓存的内容。我为以下屏幕截图点击了链接和按钮。如您所见,跟踪中只添加了对 Default.aspxStyle.css 的请求。

CachingImagesInASPNET/Trace_withCaching.png

如果用户通过超链接导航到页面或仅执行回发,则会从服务器请求未在客户端缓存的文件。请求 5 和 6 是由于点击链接引起的,而请求 7 和 8 是由于点击按钮引起的。

© . All rights reserved.