Кэширование изображений в ASP.NET

ОГЛАВЛЕНИЕ

Одним из простейших, но самых эффективных методов повышения производительности веб-приложений является кэширование изображений в клиенте.

•    Скачать CachingHandler - 76 KB

Введение

Есть много способов повышения производительности веб-приложений. Одним из простейших, но самых эффективных методов является кэширование изображений в клиенте. В данной статье показано, как было реализовано кэширование изображений для сайта DotNetNuke.

Проблема

При создании сайта http://www.software-architects.com/ использовалось много изображений в стилях css для отображения фоновых изображений для элементов меню. После переноса файлов на веб-сервер было проверено с помощью сетевого монитора Microsoft, сколько трафика генерирует запрос к начальной странице. Это инструмент для сбора и анализа протоколов сетевого трафика. Его можно скачать из центра загрузок Microsoft.

С помощью сетевого монитора Microsoft 3.1 был записан вызов к http://www.software-architects.com/. В результате было получено 20 запросов к 20 разным файлам для отображения одной страницы. Сетевой монитор Microsoft показывает, что около половины запросов требуются для изображений меню.

Есть два разных способа устранения этой проблемы. С одной стороны, можно приказать IIS кэшировать изображения в клиенте, и, с другой стороны, это можно делать прямо в ASP.net (что чуть сложней).

Кэширование изображений в IIS

Кэширование в IIS очень простое. Выберите папку в левой панели или один файл в правой панели и откройте диалоговое окно свойств.

 

Отметьте "Включить истечение срока действия содержимого" и выберите, когда срок действия вашего содержимого должен истечь.

 

Все! IIS сообщает клиенту с помощью заголовка "Cache-Control(управление КЭШем)", что содержимое может кэшироваться в клиенте. Заголовок "Expires(истекает)" содержит дату истечения срока действия. Клиент знает, что после этой даты он должен запросить у сервера новое содержимое.

Данный подход работает очень хорошо, если
•    вы можете поместить все изображения и другие кэшируемые файлы в одну или несколько папок,
•    и, главное, у вас есть доступ к IIS.

В нашем случае не выполняются оба условия. В проекте DotNetNuke изображения разбросаны по множеству папок, поэтому настроить IIS было бы сложно. И главное, поставщик услуг хостинга не предоставляет доступ к IIS. Поэтому придется искать другое решение.

Кэширование изображений с помощью специального обработчика HttpHandler

Первой задачей, требующей решения, был обход IIS для получения запроса к ASP.NET. Было решено написать специальный обработчик http, слушающий файлы с путями *.gif.ashx, *.jpg.ashx и *.png.ashx. Хорошую статью об IHttpHandler читайте на сайте APress: Используйте локальную область видимости для повышения производительности.
Был создан новый проект библиотеки классов в 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);

Далее загружается конфигурация для CachingHandler из файла web.config. Поэтому был создан класс CachingSection (показан позже), содержащий свойство CachingTimeSpan(интервал времени кэширования) и коллекцию FileExtensions(расширения файлов), знающую тип содержимого для каждого расширения файла. С помощью класса config конфигурируется объект ответа HttpCachePolicy(правила кэширования http):

•    SetExpires говорит клиенту, когда истекает срок действия содержимого.
•    SetCacheability говорит клиенту, кому разрешено кэшировать содержимое. Кэшируемость задается публичная. Отсюда следует, что ответ кэшируется клиентами и общими кэшами (кэш-посредниками).
•    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>
  /// Конфигурация для кэширования
  /// </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>
  /// Список доступных расширений файлов
  /// </summary>
  public class FileExtensionCollection : ConfigurationElementCollection
  {
    ...
  }

Для каждого расширения нужен класс, хранящий свойство для расширения и свойство для типа содержимого.

  /// <summary>
  /// Конфигурация для расширения файла
  /// </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 добавляется новый sectionGroup с именем SoftwareArchitects. В эту группу добавляется раздел по имени Caching. Тип атрибута задает класс и сборку класса CachingSection. Конечно, надо добавить сборку с классом CachingSection в папку bin веб-приложения. Затем можно добавить новый тег с именем группы в тег конфигурации. Внутрь группы добавляется новый тег с именем раздела, и в этом разделе теперь доступны все свойства, определенные в классе 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, надо добавить его в раздел httpHandlers в web.config. Туда надо добавить запись для каждого расширения файла, которое надо сопоставить с обработчиком http. Было решено поддерживать изображения с расширениями .gif, .jpg и .png. Поэтому был добавлен обработчик для путей *.gif.ashx, *.jpg.ashx и *.png.ashx. В атрибуте типа был указан класс и сборка обработчика 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). При повторной проверке запроса к главной странице сайта первый запрос по-прежнему генерировал 20 запросов к веб-серверу, но второй запрос потребовал лишь 7 запросов к серверу для загрузки страницы, так как изображения были закэшированы в клиенте.

 

Его работу можно увидеть на сайте http://www.software-architects.at/TechnicalArticles/CachinginASPNET/tabid/75/language/de-AT/Default.aspx. Щелкните правой кнопкой мыши по изображению и откройте диалоговое окно свойств. Вы увидите, что URL заканчивается на .ashx. Когда вы щелкаете правой кнопкой мыши по изображению и выбираете "Сохранить изображение как...", предложенное имя файла не содержит расширение .ashx из-за заголовка content-disposition.
Разумеется, можно использовать обработчик для других типов файлов, таких как файлы javascript или файлы css. Следовательно, вновь сократится число запросов.