Изображение из HttpHandler не кешируется в браузере

Я обслуживаю изображение из базы данных с помощью IHttpHandler. Соответствующий код находится здесь:

public void ProcessRequest(HttpContext context)
{
    context.Response.ContentType = "image/jpeg";
    int imageID;
    if (int.TryParse(context.Request.QueryString["id"], out imageID))
    {
        var photo = new CoasterPhoto(imageID);
        if (photo.CoasterPhotoID == 0)
            context.Response.StatusCode = 404;
        else
        {
            byte[] imageData = GetImageData(photo);
            context.Response.OutputStream.Write(imageData, 0, imageData.Length);
            context.Response.Cache.SetCacheability(HttpCacheability.Public);
            context.Response.Cache.SetExpires(DateTime.Now.AddMinutes(5));
            context.Response.Cache.SetLastModified(photo.SubmitDate);
        }
    }
    else
        context.Response.StatusCode = 404;
}

Проблема в том, что браузер не кэширует изображение, предположительно потому, что я не указываю правильные вещи в заголовках ответов. Я думал, что методы вызова части свойства HttpCachePolicy заставят браузер удерживать изображение, но это не так. Я думаю, что «правильно», если обработчик должен вернуть код состояния 304 без изображения, не так ли? Как мне добиться этого с помощью IHttpHandler?

РЕДАКТИРОВАТЬ:

Согласно лучшему ответу, я запустил этот код, и он полностью решает проблему. Да, он требует некоторого рефакторинга, но в целом он демонстрирует то, что я искал. Соответствующие части:

if (!String.IsNullOrEmpty(context.Request.Headers["If-Modified-Since"]))
{
    CultureInfo provider = CultureInfo.InvariantCulture;
    var lastMod = DateTime.ParseExact(context.Request.Headers["If-Modified-Since"], "r", provider).ToLocalTime();
    if (lastMod == photo.SubmitDate)
    {
        context.Response.StatusCode = 304;
        context.Response.StatusDescription = "Not Modified";
        return;
    }
}
byte[] imageData = GetImageData(photo);
context.Response.OutputStream.Write(imageData, 0, imageData.Length);
context.Response.Cache.SetCacheability(HttpCacheability.Public);
context.Response.Cache.SetLastModified(photo.SubmitDate);

person Jeff Putz    schedule 15.06.2009    source источник
comment
Похоже, браузер должен кэшировать его на 5 минут. Кеширует ли он это даже так долго? Это то, что вы хотите?   -  person Matthew Flaschen    schedule 15.06.2009
comment
Браузер вообще не кеширует. Этот код выполняется каждый раз, когда делается запрос.   -  person Jeff Putz    schedule 15.06.2009
comment
Совершенно не связано, но есть ли у вас какие-либо последствия для производительности при использовании этого подхода для обслуживания изображений? Особенно, когда вы обслуживаете несколько изображений на веб-странице с помощью IHttpHandler?   -  person jishi    schedule 22.06.2009
comment
Ничего подобного я не встречал. Имейте в виду, что я использую асинхронную версию httphandler, на всякий случай, если какие-то потоки работают.   -  person Jeff Putz    schedule 24.06.2009
comment
Я знаю, что эта ветка старая, но не могли бы вы объяснить свой выбор в пользу использования OutputStream.Write поверх WriteFile? Я пытаюсь решить, что использовать и почему. Спасибо.   -  person mkelley33    schedule 20.08.2009
comment
Потому что я пишу в поток, а не в файл?   -  person Jeff Putz    schedule 20.08.2009
comment
будьте осторожны со строкой (lastMod == photo.SubmitDate), как я видел, параметр lastMod можно анализировать только до секунд, а не до миллисекунд - в моем случае это не удалось при сравнении с File.CreatonDateTime () из-за этого :)   -  person balint    schedule 14.03.2012


Ответы (4)


AFAIK, вы несете ответственность за отправку 304 Not Modified, то есть мне ничего не известно в платформе .Net, что делает это за вас в этом случае использования вами отправки «динамических» данных изображения. Что вам нужно будет сделать (в псевдокоде):

  • Проверьте заголовок If-Modified-Since в запросе и проанализируйте дату (если она существует).
  • Сравните его с датой последнего изменения исходного изображения (динамически созданного) изображения. Отслеживание этого, вероятно, самая сложная часть решения этой проблемы. В вашей текущей ситуации вы воссоздаете изображение по каждому запросу; вы не хотите этого делать, если в этом нет крайней необходимости.
  • Если дата файла, хранящегося в браузере, новее или равна дате, установленной для изображения, отправьте 304 Not Modified.
  • В противном случае продолжайте свою текущую реализацию

Простой способ отслеживать время последнего изменения на вашей стороне - кэшировать вновь сгенерированные изображения в файловой системе и хранить в памяти словарь, который сопоставляет идентификатор изображения со структурой, содержащей имя файла на диске и дату последней модификации. Используйте Response.WriteFile для отправки данных с диска. Конечно, каждый раз, когда вы перезапускаете рабочий процесс, словарь будет пустым, но вы получаете хоть какое-то преимущество кеширования без необходимости иметь дело с сохраняющейся где-то информацией кеширования.

Вы можете поддержать этот подход, разделив задачи «Генерация изображений» и «Отправка изображений через HTTP» на разные классы. Прямо сейчас вы делаете две очень разные вещи в одном и том же месте.

Я знаю, это может показаться немного сложным, но оно того стоит. Я недавно реализовал этот подход, и экономия времени обработки и использования полосы пропускания была невероятной.

person Thomas Jung    schedule 15.06.2009

Если у вас есть исходный файл на диске, вы можете использовать этот код:

context.Response.AddFileDependency(pathImageSource);
context.Response.Cache.SetETagFromFileDependencies();
context.Response.Cache.SetLastModifiedFromFileDependencies();
context.Response.Cache.SetCacheability(HttpCacheability.Public);

Кроме того, убедитесь, что вы тестируете с помощью IIS, а не Visual Studio. Сервер разработки ASP.NET (он же Cassini) всегда устанавливает для Cache-Control значение private.

См. Также: Руководство по кэшированию для авторов и веб-мастеров

person Pavel Chuchuva    schedule 15.06.2009

Вот как это делается в Roadkill (.NET wiki) обработчик файлов:

FileInfo info = new FileInfo(fullPath);
TimeSpan expires = TimeSpan.FromDays(28);
context.Response.Cache.SetLastModifiedFromFileDependencies();
context.Response.Cache.SetETagFromFileDependencies();
context.Response.Cache.SetCacheability(HttpCacheability.Public);

int status = 200;
if (context.Request.Headers["If-Modified-Since"] != null)
{
    status = 304;
    DateTime modifiedSinceDate = DateTime.UtcNow;
    if (DateTime.TryParse(context.Request.Headers["If-Modified-Since"], out modifiedSinceDate))
    {
        modifiedSinceDate = modifiedSinceDate.ToUniversalTime();
        DateTime fileDate = info.LastWriteTimeUtc;
        DateTime lastWriteTime = new DateTime(fileDate.Year, fileDate.Month, fileDate.Day, fileDate.Hour, fileDate.Minute, fileDate.Second, 0, DateTimeKind.Utc);
        if (lastWriteTime != modifiedSinceDate)
            status = 200;
    }
}

context.Response.StatusCode = status;

Ответ Томаса о том, что IIS не предоставляет код состояния, является ключевым моментом, без него вы просто получаете обратно 200 секунд каждый раз.

Браузер просто отправит вам дату и время, когда он считает, что файл был последний раз изменен (без заголовка вообще), поэтому, если он отличается, вы просто верните 200. Вам необходимо нормализовать дату вашего файла, чтобы удалить миллисекунды и убедиться, что это дата в формате UTC.

Я выбрал значение по умолчанию 304, если есть действительное измененное время, но при необходимости его можно настроить.

person Chris S    schedule 22.03.2013

Происходит ли буферизация ответа? В таком случае вы можете захотеть установить заголовки перед записью в выходной поток. т.е. попробуйте переместить строку Response.OutputStream.Write() ниже строк настройки кэша.

person David McEwing    schedule 15.06.2009