UML软件工程组织

从 .NET 程序集提供图像
转自:www.microsoft.com 作者:Morgan Skinner
摘要:大多数 Web 站点的用户界面都需要有图像,这些图像通常存储在磁盘上。本文介绍如何从程序集提供图像。从程序集提供图像可以避免众多文件散布在磁盘上,简化 Web 服务器的安装和配置,以及提高图像的安全性。(本文包含一些指向英文站点的链接)

适用于:
   Microsoft® .NET Framework 版本 1.0 和 1.1
   Microsoft® Visual C#®
   Microsoft® ASP.NET

下载 MFRImages.exe 示例文件。下载内容中包括 readme.htm,用于说明如何配置示例。

目录

简介

经常会听到那些编写过 Windows 控件,后来又转向 Web 的程序员抱怨:“为什么不能像控件那样将图像存储在同一个程序集内呢?” 答案是可以,您只需要知道如何做到这一点。本文介绍如何从一个程序集内提供图像,并提供了两种检索图像的方法。下载本页顶端的 MFRImages.exe,以便完整地了解下面讨论的示例代码。

问题概述

Web 站点上的图像通常通过 URL 来引用,例如 http://www.morganskinner.com/images/morganskinner.gif。它告诉 Web 客户端到哪里去查找图像。在 Web 页中,图像与文字是分别下载的。图像通常存储在 Web 服务器中命名为 /images 等名称的子目录中,页面只是提供到这些图像的引用,以使它们能够显示在客户端的浏览器中。

当创建自定义控件时,情况也是如此 - 图像被用户下载之前通常需要存储在磁盘上。作为控件的编写者,您可能希望能够提供一个程序集,其中不仅包括控件,而且还包括相应的默认图像。控件保持独立完整很有益,因为它只需要用户进行较少的配置。(负责配置的用户可能会忘记将大量图像文件复制到生产服务器上,从而导致其他用户的不满。)

希望将资源绑定到程序集的另外一个原因可能是为了确保用户不能更改这些资源 - 例如对于公司商标,应始终使用特定的图像,而不能由某个用户将错误的图像(如不正确的字样或颜色)误保存到磁盘中。如果已对您的程序集进行了严格命名,当加载该程序集时,可以进行测试以确保该程序集未被篡改。

从程序集提供图像的主要问题是 HTTP 需要通过 URL 获得图像 - 您不能只将直接插入 HTML 的一串字节返回给用户,就期望能够正确显示图像。设法将图像请求重定向到程序集内的某个资源是很有必要的,在本文中我将介绍两种方法。

在继续下面的内容之前,还要注意一个问题。如果在 Web 服务器上没有进行其他一些配置,想提供一个能够正确呈现图像的完全独立控件是不可能的。您还需要在服务器上创建其他的文件(至少一个),或对 IIS 配置数据库做一些更改,以将图像提供给客户端。不过,完成这些简单的更改后,您就可以轻松地从任何程序集提供图像了。

提供图像

既然问题的关键是要提供图像,那么我们现在就进入正题。当从调用方传来信息后,我们需要找到一种方法来加载图像并在响应流中返回图像。请注意,此示例中提供的图像只有 .gif 一种类型 - 要提供其他类型的图像,则需要通过图像扩展名来推断内容类型和图像格式。

以下函数显示如何从给定的程序集加载图像,并通过 HttpResponse 流返回给客户端。我们将把此函数作为后面代码的基础,并在接下来的内容中添加更有用的功能,例如异常处理和图像缓存。我将把此函数定义为 ManifestImageLoader 类中的静态函数。

public class ManifestImageLoader
{
public static void RenderImage ( string assembly , string image , HttpContext context )
{
Assembly   resourceAssem = Assembly.Load ( assembly ) ;

// 获取资源 
using ( Stream   imageStream = resourceAssem.GetManifestResourceStream ( image ) )
{
   // 如果可以,将其写出 
   using ( System.Drawing.Image theImage =
   System.Drawing.Image.FromStream ( imageStream ) )
response.ContentType = "image/gif" ;
theImage.Save ( context.Response.OutputStream , ImageFormat.Gif ) ;
}
}
}

函数 RenderImage 接受程序集名称、图像名称和响应流。有了有效的程序集名称和图像名称,加载图像并将其返回到输出流就轻而易举了。首先加载程序集,然后使用 Assembly.GetManifestResourceStream 函数返回已命名资源(在此实例中为图像)的数据流。您需要为 System.ReflectionSystem.IO 添加 using 子句,使其能够通过编译,并引用 System.Drawing 程序集。

有了图像数据流之后,可以使用 Image.FromStream() 方法从该字节流构造图像 - 请注意,我们使用的是 System.Drawing 中的图像类,而不是 System.Web.UI.WebControls 中具有类似名称的类,因为前者具有访问 Win32 图像函数的权限,而后者在 Web 控件中包含了 <img> 标记。

您或许不熟悉 C# using 语法,该语法在 Try/Finally 块中包含了代码,可以确保对括号中的项调用 Dispose

现在我们可以从程序集提供图像了,首先需要能够为该图像创建 URL。第一种方法是使用 .ASPX 页面。

从 ASPX 页面提供图像

第一种提供图像的方法需要使用 .ASPX 页面,该页面通常要驻留在服务器上的某个位置。页面本身不包含内容 - 它的主要功能是从 URL 检索参数,并使用这些参数来检索图像。

例如,有一个名为 ImageFromASPX.aspx 的页面位于 Web 站点的根目录下。然后可以在 HTML 中使用以下 URL 编码语法来定义从此页面提供的所有图像。这里我们提供的是名为 winxp.gif 的标题图像。

<img src="/imagefromASPX.aspx?assem=ImageServer&amp;image=winxp.gif" />

在 URL 中,我们定义了 ASPX 页面的路径,然后定义了两个参数,一个是程序集名称(在此实例中为 ImageServer),另一个是图像名称。这在安全性方面可能存在风险,因此在本文的后面我将介绍一种可用于此数据的加密方法。

在 ASPX 页面的代码中,可以写入以下内容:

private void Page_Load ( object sender, System.EventArgs e )
{
   // 检索参数
   string assembly = Request.QueryString["assem"] ;
   string image = Request.QueryString["image"] ;

   // 并加载图像
   ManifestImageLoader.RenderImage ( assembly , image ) ;
}

代码所做的就是根据请求分析参数,然后调用我们前面编写的 RenderImage 函数。正如您看到的,这并不难实现。但它有一个缺点,即所有对图像的请求都需要通过同一个 URL。也就是说,每个自定义控件都必须知道 imageserver.aspx 文件的位置和名称,才能提供图像。如何避免这个限制是下一节的主题。

从自定义处理程序提供图像

如果您以前从未接触过处理程序,我将在这里大概介绍一下。处理程序是用于实现 IHttpHandler 接口的对象。当包含给定文件扩展名的请求通过 ASP.NET 管道时,将为特定动词(如 POST、GET 等)或一组动词调用处理程序。

通常,ASP.NET 会检查请求的文件扩展名,并将请求传送给与该扩展名相关联的处理程序。

了解了这些知识,我们就可以创建处理程序,将其与自己的文件扩展名相关联(这样,ASP.NET 就会知道我们要调用的是处理程序,而不是其他内容),并且以该方式提供图像。

下面的代码显示的是一个简单的处理程序,该处理程序使用了上面声明的 RenderImage 函数。

public class ManifestResourceHandler : IHttpHandler
{
   /// <summary>
   /// 处理图像请求
   /// </summary>
   /// <param name="context">The current HTTP context</param>
   void IHttpHandler.ProcessRequest ( System.Web.HttpContext context )
   {
// 从请求中获取程序集名称和资源名称
string assembly = context.Request.QueryString["assem"] ;
string image = context.Request .QueryString["image"] ;

// 然后加载图像并返回给调用方 
ManifestImageLoader.RenderImage ( assembly , image ) ;
   }

   /// <summary>
   /// 此处理程序可以重复使用,不需要循环 
   /// </summary>
   bool IHttpHandler.IsReusable
   {
get { return true; }
   }
}

这段代码与上面为 ASPX 页面显示的代码非常类似 - 从传来的 URL 读取参数,然后将这些参数传递到 RenderImage 函数。

现在,要想使用处理程序来提供图像,我们需要使用不同的 URL。在此实例中,需要创建一个虚构的文件扩展名(即在 IIS 中不存在的扩展名),这样,图像请求就可以传送给正确的处理程序。在此示例中我将使用扩展名“mfr”(表示“清单资源”)。图像请求现在看起来有点像下面的描述。

<img src=".mfr?assem=MS.Resources&amp;image=winxp.gif" />

注意,我还未指定资源的路径,只是指定了文件扩展名 .mfr。

使用处理程序的主要好处是可以为所有请求调用该程序,而不用考虑它们的路径。

要使处理程序能够工作还需要另外两个步骤。首先,需要修改 web.config,以指定新的处理程序:

<configuration>
  <system.web>
    ...
    <httpHandlers>
<add verb="GET" path="*.mfr" type="ImageServer.ManifestResourceHandler, ImageServer" />
    </httpHandlers>
  </system.web>
</configuration>

上述配置文件中的类型定义了实现处理程序的类型和程序集。注意,动词属性区分大小写,因此应设置为 GET 而不是其他的大小写形式。程序集本身需要驻留在您 Web 站点的二进制目录中,或安装在全局程序集缓存 (GAC) 中。

其次,您需要在 IIS 管理中编辑 Web 服务器的配置。单击您要更改的 Web 站点的 Properties(属性),选择 Home Directory(主目录)选项卡,然后单击 Configuration(配置)。将显示与以下窗口类似的窗口。

图 1:配置 IIS

单击 Add(添加)按钮,为 .mfr 文件类型创建条目。每个扩展名都会被映射到处理资源请求的 ISAPI 过滤器。对于 ASP.NET,为 aspnet_isapi.dll 过滤器。此库驻留在磁盘中已安装的 Framework 下,因此要设置 .mfr 扩展名的所有请求以通过相应的 ISAPI dll,需要进行如下设置:

版本 路径
1.0 C:\WINDOWS\Microsoft.NET\Framework\v1.0.3705\aspnet_isapi.dll
1.1 C:\WINDOWS\Microsoft.NET\Framework\v1.1.4322\aspnet_isapi.dll

其余的设置显示在以下图像中。请注意,必须清除 Verify that file exists(检查文件是否存在)复选框,否则永远不会调用处理程序(因为磁盘中不存在实际的 .mfr 文件)。

图 2:为扩展名设置属性

现在应该可以运行处理程序了。在浏览器中键入映射到程序集中的资源的 URL:

图 3:从程序集提供图像

如果您接收到的不是请求的图像,而是一个异常(如“‘null’不是‘stream’的有效值”),那么您可能遇到了目前我们还没有在代码中进行处理的一些小问题 - 如果图像有错误怎么办? 我们将在下一节对这个问题及其他一些小问题进行纠正。

增强代码

代码中首先要处理的是大小写形式。HTTP 认为以下所有 URL 都相同,因为 URL 不区分大小写。

<img src=".mfr?assem=ImageServer&amp;image=winxp.gif" />
<img src=".mfr?assem=ImageServer&amp;image=WINxp.gif" />
<img src=".mfr?assem=ImageServer&amp;image=WiNxP.gif" />

我们的代码目前有一个问题,由于它不保留原 HTTP 请求不区分大小写的特征,因此必须向 LoadAndReturnImage 函数再添加一些代码。

不区分大小写

我们要做的是从程序集加载图像而不考虑大小写,但由于 Assembly.GetManifestResourceStream 是区分大小写的,因此还得另想办法。Assembly.GetManifestResourceNames() 函数将返回给定程序集内的所有资源列表,因此我们要做的就是调用此列表,与这些资源名称进行不区分大小写的比较,然后使用查找出的名称:

Assembly   resourceAssem = Assembly.Load ( assembly ) ;

// 查找缓存的名称
string[] names = HttpContext.Current.Application [ assembly ] as string[] ;

if ( null == names )
{
   // 获取程序集内所有资源的名称
   names = resourceAssem.GetManifestResourceNames() ;
   Array.Sort ( names , CaseInsensitiveComparer.Default ) ;
   HttpContext.Current.Application [ assembly ] = names ;
}

// 如果此程序集内存在一些资源,
// 检查所需的资源
if ( names.Length > 0 )
{
   // 在名称数组中查找图像
   int pos = Array.BinarySearch ( names , image , 
CaseInsensitiveComparer.Default ) ;

   if ( pos > -1 )
WriteImage ( resourceAssem , names[pos] , true ) ;
}

这里我加载了包含资源的程序集,然后查找应用程序缓存以查看该程序集内的资源列表是否已被加载和缓存。如果没有,我通过调用 Assembly.GetManifestResourceNames() 来读取资源列表,然后对列表进行排序,并将其存储在应用程序状态中。

然后,我就可以使用 Array.BinarySearch() 方法对名称列表执行二进制搜索。这比按顺序搜索字符串列表要快得多,且在应用程序状态存储资源列表所需的系统开销也较小。

这样就解决了区分大小写的问题,但性能如何呢?目前,每次当图像请求到达时,我们都要调用全部代码 - 除了最小的 Web 站点之外,其余所有的请求都可能会造成严重的性能问题。下一节我们将处理这个问题。

缓存

像普通的图像和一些 ASPX 页面一样,把从程序集返回的图像进行缓存是很有用的 - 因为这些图像驻留在程序集中,一般不会频繁地更改。

如果我们在编写一个简单的 ASPX 页面,则可以添加 OutputCache 指令以缓存页面。但在我们的方案中,我们需要一种方法能够通过编程方式将缓存控件标题添加到响应流中。幸运的是,在 ASP.NET 中这很容易完成。在把图像写入输出流的函数中,只需添加以下几行:

response.Cache.SetExpires ( DateTime.Now.AddMinutes ( 60 ) ) ;
response.Cache.SetCacheability ( HttpCacheability.Public ) ;
response.Cache.VaryByParams["assem"] = true ;
response.Cache.VaryByParams["image"] = true ;
// 将图像写入响应流...

此设置使图像在一小时(这个时间显然可以延长以减少服务器负载)后过期,并定义图像可以在任意位置(客户端、代理服务器、服务器等)进行缓存。它还定义了更改缓存行为的参数。

现在,代码几乎已经完成了,但我们需要决定如何处理异常情况。

关于异常的编程

我们的代码中可能会引发很多异常。现在,用户的浏览器可能会断开链接,甚至可能仍然会遇到 ASP.NET 错误页面。我们可以推测出很多种可能发生的情况。如下所示:

  • 程序集可能不存在。
  • 程序集存在但不包含任何图像。
  • 程序集可能不包含所请求的图像。

代码也可能会造成其他错误。当找不到图像时,浏览器默认的响应是返回一个带有红十字的图像以表示一个断开的链接。

您当然希望用自己的默认图像来代替此图像。我已将一个默认的断开链接图像包含在 ImageServer 程序集中,当发生异常时,该图像将返回到浏览器。此行为可以通过在 web.config 文件的 AppConfig 部分添加一个设置来实现。

当发生错误时,如果要覆盖默认行为(返回链上的异常),请将以下内容添加到 web.config 中。

<appSettings>
   <add key="MFRShowBrokenLink" value="true" />
</appSettings>

现在,当代码中出现异常时,将向浏览器返回断开链接图像,并在跟踪日志中写入警告。

图4:链接断开时返回的图像

如果查看跟踪日志,您会看到有关图像不存在的项,该项与下面类似。

图5:无效图像请求的示例跟踪日志输出

本文讨论的所有代码都可以通过本页顶部的 MFRImages.exe 下载链接获得。此下载包括本节完成的所有增强工作。还包括一些测试页,通过这些测试页可以查看使用处理程序和 ASPX 方法来呈现图像的结果。

整理

下面要添加一种方法,以返回驻留在程序集内的图像的正确 URL,然后自定义控件编写人员(或是您)可以调用此方法来返回图像。

如果已选择了处理程序方法来提供图像,则您所需的函数如下。

public static string ConstructImageURL ( Assembly assembly, string image )
{
   return string.Concat ( ".mfr?assem=" , 
   HttpUtility.UrlEncode ( assembly.FullName.ToString ( ) ) , 
   "&image=" ,
   HttpUtility.UrlEncode ( image ) ) ;
}

对于这段代码,我使用的是 string.Concat(),因为它比 string.Format() 大约快 4 倍。每个小技巧都会有所帮助! 然后可以用它来设置您在自定义控件中创建的所有图像的 ImageURL 属性。

安全性

到目前为止的讨论中,我们一直基于程序集名称和资源名称提供图像。这没什么不好,但这意味着任何人都可以得到磁盘上的程序集名称,并可以尝试通过将其他程序集名称传递给处理程序来进行攻击。

为了避免这个潜在的问题,最好用某种方法对返回的值进行加密。我们可以提供一些从程序集名称和图像名称生成的散列码,或使用程序集名称和图像名称的加密格式,然后在接收到请求后再进行解密。

前一种方法(使用散列码)需要服务器中有查找表,并且表中为每个提供的图像填充了内容。这就给 Web 领域带来一个潜在的问题。在 Web 领域,可能一个服务器提供初始图像请求(并缓存散列码),而另一个服务器实际响应图像。

因此,我选择了第二种方法,即在返回到用户的 URL 中包含加密的程序集名称和图像名称。这样就不会遇到 Web 领域中存在的问题,但却意味着需要从浏览器多传送一些数据到服务器,因为图像 URL 要长一些。

示例代码包含一个类,它使用 Triple-DES(数据加密标准)算法加密和解密字符串。通常,程序集名称和图像名称在传递到客户端之前已进行了加密。当请求图像时,这些值被解密,并调用与原来相同的代码。

我已将这些内容以可配置的方式添加到解决方案中。在 web.config 中仅有一个标志,如果设置为“true”,则会在向客户端提供资源名称时对其进行加密:

   <appSettings>
<add key="MFRSecure" value="true" />
   </appSettings>

在处理程序的 ProcessRequest 方法中,我对此标记进行检查:

bool   secure = false ;
string   shouldSecure = ConfigurationSettings.AppSettings["MFRSecure"] ;

if ( null != shouldSecure )
   secure = Convert.ToBoolean ( shouldSecure ) ;

string assembly = context.Request.QueryString["assem"] ;
string image = context.Request.QueryString["image"] ;

if ( secure )
{
   assembly = Crypto.Decrypt ( assembly ) ;
   image = Crypto.Decrypt ( image ) ;
}

ManifestImageLoader.RenderImage ( assembly , image ) ;

类似地,在前面介绍的 ConstructImageURL 方法中,在程序集名称和图像名称被传递给客户端之前,我对它们进行了加密。

代码的很多部分都可以进行扩展或改进。下面是我的几点建议。

  • 当无法找到资源时,配置项不对使用的图像进行硬编码,而是指定图像的 URL。这样在出现异常时,您就可以从磁盘(或从其他程序集)加载特定的图像并将其返回到浏览器。
  • 图像的缓存超时也可以定义为配置项。
  • 可以扩展代码,以允许从程序集提供任何类型的图像 - 目前,mime 类型被硬编码为 image/GIF。
  • 对于为何此示例中的代码不能提供程序集内的其他资源,没有什么原因 - 您完全可以提供 TXT 文件、WAV 文件等。

小结

本文介绍了两种方法,用于从程序集检索格式适合包含在 Web 站点中的图像。第一种方法是从 ASPX 页面提供图像,这种方法简单而且不需要修改 Web 服务器配置,但是提供图像的 ASPX 页面的路径必须正确,以使图像能够正确显示。

另一种方法是从自定义处理程序提供图像。这种方法克服了基于路径的限制,但需要更改 IIS 配置数据库,以允许由 aspnet_isapi.dll 扩展程序提供 .mfr 扩展名。而且还要为给定的应用程序修改 web.config。

我个人建议使用处理程序方法而不要使用 ASPX 方法,因为在 Web 服务器中配置处理程序方法后,使用起来会更容易(而且不需要路径)。

 

 

版权所有:UML软件工程组织