ASP.NET 中的图片缓存






4.77/5 (56投票s)
提高 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 显示,大约一半的请求是用于菜单图片的。
有两种不同的方法可以避免这个问题。一方面,您可以告诉 IIS 在客户端缓存图片,另一方面,您可以直接在 ASP.NET 中完成(这会稍微复杂一些)。
在 IIS 中缓存图片
IIS 中的缓存非常简单。在左侧窗格中选择一个文件夹,或在右侧窗格中选择单个文件,然后打开“属性”对话框。
选中“启用内容过期”,然后选择您的内容何时过期。
就是这样!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.config 的 httpHandlers
节中。在那里,我们必须为每个要映射到我们的 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 个请求,因为图片已在客户端缓存。
您可以在我们的网站 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,以及在样式表中定义的背景图片。
要查看跟踪信息,我在 IE 中打开了一个新选项卡,并在 URL 中将 Default.aspx 替换为 Trace.axd。跟踪显示,显示 Default.aspx 页面需要四次请求。对于第一次请求以及用户每次按 F5 时,所有文件都会发送到客户端。
当我切换回第一个选项卡时,我有三种重新加载页面的可能性。我可以
- 按 F5
- 点击“重新加载页面...”链接
- 点击“重新加载页面...”按钮
按 F5 会重新加载所有内容,而点击链接或按钮只会重新加载未在客户端缓存的内容。我为以下屏幕截图点击了链接和按钮。如您所见,跟踪中只添加了对 Default.aspx 和 Style.css 的请求。
如果用户通过超链接导航到页面或仅执行回发,则会从服务器请求未在客户端缓存的文件。请求 5 和 6 是由于点击链接引起的,而请求 7 和 8 是由于点击按钮引起的。