Ноябрь 2015

Том 30, номер 12

ASP.NET - Применение ASP.NET в качестве высокопроизводительного средства для скачивания файлов

By Дуг Дьюрнер

Продукты и технологии:

ASP.NET, IIS Web Server, протокол HTTP

В статье рассматриваются:

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

Исходный код можно скачать по ссылке

Медленные и сбойные соединения давно являются проблемой для скачивания больших файлов. Вы можете находиться в главном вестибюле аэропорта, собирая медийную информацию по нестабильному соединению через Wi-Fi, чтобы потом поработать над какой-нибудь презентацией в течение долгого перелета, или в африканской саванне, пытаясь скачать большой установочный файл по спутниковой связи для водяного насоса, работающего на солнечной энергии. В любом из этих примеров цена неудачи при скачивании большого файла одна и та же: потеря времени, неэффективный труд и риск для успешной работы.

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

Обзор

Мы хотели создать несложную утилиту для скачивания файлов, которую можно было бы легко добавить на ваш существующий IIS Web Server с крайне простой в использовании клиентской программой (или возможностью использования веб-браузера в качестве клиента).

IIS Web Server уже доказал, что является веб-сервером корпоративного класса с высокой масштабируемостью, годами обслуживающим файлы для браузеров. Мы хотели в основном задействовать преимущества способности IIS Web Server одновременно (параллельно) обрабатывать множество HTTP-запросов и применить это к скачиванию (копированию) файлов.

По сути, нам нужна утилита для скачивания файлов (file downloader utility), которая могла бы скачивать огромные файлы для пользователей по всему миру, иногда находящихся в удаленных уголках планеты с медленными и часто сбоящими сетевыми соединениями. Из-за того, что некоторые удаленные пользователи все еще используют модемные соединения или сбойную спутниковую связь, которая может переходить в офлайн в произвольные моменты или периодически переключаться между онлайн и офлайн, этой утилите нужно быть крайне устойчивой с возможностью повторных попыток заново скачивать только те части файла, которые не удалось скачать. Мы не хотели, чтобы пользователь провел всю ночь, скачивая огромный файл по медленному соединению, и при малейшем сбое в сетевом соединении заново начинал весь процесс скачивания. Нам также требовалось обеспечить, чтобы эти скачиваемые огромные файлы не буферизовались в памяти сервера и чтобы нагрузка на нее была минимальной и не росла, когда множество пользователей одновременно скачивают файлы.

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

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

Если в двух словах, то мы создали простую, многопоточную, параллельную, мало нагружающую память утилиту для скачивания файлов, способную разбивать файл на порции (части), скачивать индивидуальные порции в раздельных потоках и разрешать повторные попытки скачивания только тех порций, которые не удалось скачать.

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

Обзор проекта-примера

По сути, DownloadHandler.dll преобразует существующий IIS Web Server в многопоточное средство для скачивания файлов, которое позволяет скачивать файл параллельными порциями, используя простой URL из автономного исполняемого клиента (FileDownloader.exe), как показано на рис. 1. Заметьте, что параметр (chunksize=5242880) не обязателен, и, если он не включен, весь файл будет скачиваться одной порцией. На рис. 2 и 3 демонстрируется, как это позволяет повторно запрашивать только те порции файла, которые не удалось скачать, без необходимости заново начинать весь процесс скачивания. Этим наша утилита выгодно отличается от другого программного обеспечения для скачивания файлов.

Рис. 1. Высокоуровневая схема потока обработки для DownloadHandler.dll (FileDownloader.exe используется как клиент)

{Для верстки: прошу обратить внимание на необходимость замены диапазонов вида 10-15MB на наш стандарт вида 10–15 Мб}

Client Machine Клиентская машина
FileDownloader.exe FileDownloader.exe
Parallel.ForEach iterates over 25MB total splitting into chunksize of 5MB (each chunk iteration is on separate thread) Parallel.ForEach проходит по файлу размером 25 Мб, разбивая его на порции по 5 Мб (каждая порция помещается в отдельный поток)
25MB 25 Мб
HttpWebRequest HttpWebRequest (0–5 Мб)
waits ожидает
Hard Drive Жесткий диск
file.txt (25MB) file.txt (25 Мб)
Http Request Header HTTP-заголовок запроса
Http Response Header returns Http status code 206-PartialContent HTTP-заголовок ответа возвращает код состояния HTTP «206-PartialContent»
Range: bytes = Range: bytes =
Content-Range: bytes Content-Range: bytes
Overlapped File I/O copies byte range of file that is inside HttpResponse Stream directly to corresponding byte range inside File Stream opened for overlapped file I/O При перекрытом файловом вводе-выводе диапазон байтов файла, находящийся внутри HttpResponse Stream, копируется напрямую в соответствующий диапазон байтов в File Stream, открытый для перекрытого файлового ввода-вывода
TransmitFile sends byte range of file straight to HttpResponse Stream without buffering in server memory TransmitFile отправляет диапазон байтов файла прямо в HttpResponse Stream без буферизации в памяти сервера
Server Machine Серверная машина
IIS IIS
IHttpAsyncHandler (DownloadHandler.dll) IHttpAsyncHandler (DownloadHandler.dll)
TransmitFile TransmitFile

Высокоуровневая схема потока обработки для DownloadHandler.dll (FileDownloader.exe используется как клиент)

Автономный исполняемый файл в качестве клиента скачивания (после повторной попытки)Автономный исполняемый файл в качестве клиента скачивания (после повторной попытки)

Рис. 2. Автономный исполняемый файл в качестве клиента скачивания (со сбойными порциями)

Автономный исполняемый файл в качестве клиента скачивания (после повторной попытки)

Рис. 3. Автономный исполняемый файл в качестве клиента скачивания (после повторной попытки)

На рис. 1 дана высокоуровневая схема DownloadHandler.dll и FileDownloader.exe, показывающая поток обработки по мере того, как порции файла с жесткого диска сервера проходят через DownloadHandler.dll и FileDownloader.exe в файл на жестком диске клиентского компьютера; кроме того, иллюстрируются HTTP-заголовки, участвующие в этом процессе.

FileDownloader.exe на рис. 1 инициирует скачивание файла, вызывая сервер по простому URL, содержащий имя нужного вам файла в виде строкового параметра URL-запроса (file=file.txt) и на внутреннем уровне использует HTTP-метод (HEAD), чтобы изначально сервер вернул только свои заголовки ответа, один из которых содержит размер всего файла. Затем клиент использует конструкцию Parallel.ForEach для итерации, разбивая файл на порции (диапазоны байтов) на основе размера порций, указанного в параметре (chunksize=5242880). На каждой итерации конструкция Parallel.ForEach выполняет метод обработки в отдельном потоке, передавая ему сопоставленный диапазон байтов. Внутри метода обработки клиент выдает серверу вызов HttpWebRequest, используя тот же URL и на внутреннем уровне дописывает HTTP-заголовок запроса, содержащий диапазон байтов, который был передан этому методу обработки (т. е. Range: bytes=0-5242880, Range: bytes=5242880-10485760 и т. д.).

На серверной машине наша реализация интерфейса IHttpAsyncHandler (System.Web.IHttpAsyncHandler) обрабатывает каждый запрос в отдельном потоке, выполняя метод HttpResponse.TransmitFile, чтобы записывать диапазон байтов, затребованный из файла на сервере, напрямую в сетевой поток данных (stream) безо всякой буферизации; благодаря этому нагрузка на память сервера практически несущественна. Сервер возвращает свой ответ с HTTP-кодом состояния «206 (PartialContent)» и на внутреннем уровне дописывает HTTP-заголовок ответа, идентифицирующий возвращенный диапазон байтов (т. е. Content-Range: bytes 0-5242880/26214400, Content-Range: bytes 5242880-10485760/26214400 и т. д.). Когда каждый поток принимает HTTP-ответ на клиентской машине, он записывает возвращенные в ответе байты в соответствующую часть файла на жестком диске клиентского компьютера; эта часть (порция) определена в HTTP-заголовке ответа (Content-Range). При этом используется асинхронный перекрытый файловый ввод-вывод (чтобы Windows I/O Manager гарантированно не сериализовал запросы ввода-вывода перед отправкой пакетов запроса ввода-вывода драйверу режима ядра для выполнения операции записи в файл). Если несколько потоков пользовательского режима одновременно записывает в файл, а тот не открыт для асинхронного перекрытого ввода-вывода, запросы будут сериализоваться и драйвер режима ядра будет получать лишь по одному запросу единовременно. Подробнее об асинхронном перекрытом вводе-выводе см. статьи «Getting Your Driver to Handle More Than One I/O Request at a Time» (bit.ly/1NIaqxP) и «Supporting Asynchronous I/O» (bit.ly/1NIaKMW) на сайте Hardware Dev Center.

Чтобы реализовать асинхронность в нашем IHttpAsyncHandler, мы вручную передаем структуру перекрытого ввода-вывода порту завершения ввода-вывода, и CLR ThreadPool выполняет в потоке порта завершения делегат завершения, предоставленный в структуре перекрытого ввода-вывода. Это те же потоки порта завершения, используемые большинством встроенных асинхронных методов. В целом, для большей части работы по вводу-выводу лучше всего использовать новые встроенные асинхронные методы, но в данном случае мы хотели задействовать функцию HttpResponse.TransmitFile из-за ее исключительной способности передавать огромные файлы безо всякой буферизации в серверной памяти.

Конструкция Parallel.ForEach изначально предназначена для работы, требующей интенсивного использования процессора, и никогда не должна применяться в серверной реализации из-за ее блокирующей природы. Мы перекладываем эту работу на поток порта завершения из CLR ThreadPool вместо обычного рабочего потока из того же пула, чтобы предотвратить исчерпание тех же потоков, используемых IIS для обслуживания входящих запросов. Кроме того, более высокая эффективность выполнения этой работы портом завершения в какой-то мере ограничивает потребление потоков на сервере. В разделе comment в начале класса IOThread (из пакета сопутствующего исходного кода) присутствует схема с более подробным пояснением, где выделены различия между потоками порта завершения и рабочими потоками в CLR ThreadPool.

Поскольку масштабирование до миллионов пользователей не является основной целью нашей утилиты, мы можем позволить расширить количество дополнительных потоков на сервере, необходимых для выполнения функции HttpResponse.TransmitFile, чтобы достичь соответствующей экономии памяти на сервере при передаче массивных файлов. По сути, мы жертвуем масштабируемостью из-за дополнительных потоков на сервере (вместо использования встроенных асинхронных методов, не требующих потоков) в пользу применения функции HttpResponse.TransmitFile, которая расходует невероятно малый объем памяти на сервере. Хотя это выходит за рамки нашей статьи, можно было бы задействовать встроенные асинхронные методы в сочетании с не буферизованным файловым вводом-выводом для достижения сходной экономии памяти без дополнительных потоков, но, как мы понимаем, все должно быть выровнено по секторам (sector aligned), а это довольно трудно реализовать должным образом. Ко всему прочему, складывается впечатление, что Microsoft намеренно удалила элемент NoBuffering из перечисления FileOptions, чтобы фактически исключить не буферизованный файловый ввод-вывод, и для его использования пришлось бы придумывать замысловатые обходные пути. У нас были серьезные опасения рисков, связанных с некорректной реализацией этого, и мы решили выбрать менее рискованный вариант с HttpResponse.TransmitFile, который был полностью протестирован.

FileDownloader.exe может запускать несколько потоков, каждый из которых выдает отдельный вызов HttpWebRequest, соответствующий его диапазону байтов скачиваемого файла, поделенного на порции на основе его общего размера, как показывает параметр Chunk Bytes, показанный на рис. 2.

Любой поток, которому не удалось скачать свою порцию файла (диапазон байтов), заданный в его вызове HttpWebRequest, может выполнить повторную попытку, просто выдавая тот же вызов HttpWebRequest (только для этого диапазона байтов), пока он не увенчается успехом (рис. 3). Вы не потеряете уже скачанные порции файла, что в условиях медленного соединения может подразумевать экономию многих часов процесса скачивания. Вы можете практически исключить негативное влияние сбойного соединения, которое постоянно переходит в офлайн. А благодаря схеме, при которой несколько потоков скачивает разные порции файла параллельно (напрямую в сетевой поток данных без буферизации) и с применением асинхронного перекрытого файлового ввода-вывода, можно максимизировать объем скачиваемых данных в те временные окна, когда нестабильное соединение реально находится в режиме онлайн. Утилита продолжит скачивание оставшихся частей файла всякий раз, когда сетевое соединение будет возвращаться в онлайн, не теряя уже проделанной работы. Мы предпочитаем называть эту утилиту загрузчиком файлов с повторением, а не возобновлением.

Разницу можно проиллюстрировать на гипотетическом примере. Допустим, вы собираетесь скачать большой файл, что займет всю ночь. Располагая загрузчиком файлов с возобновлением, вы оставляете его работать. Придя утром на работу, вы видите, что этот загрузчик потерпел неудачу на 10% и готов к возобновлению процесса скачивания. Но, когда он возобновляет скачивание, ему все равно снова понадобится вся ночь, чтобы скачать оставшиеся 90%.

И напротив, вы запускаете наш загрузчик файлов с повторением, уходя с работы и оставляя его работать на ночь. Придя утром на работу, вы видите, что скачивание оказалось неудачным только для одной порции файла на 10%, но остальные порции файла продолжали скачиваться. Теперь вам остается лишь повторить скачивание всего одной порции, и все готово. Встретив ту сбойную порцию из-за временного прекращения работы сетевого соединения, утилита продолжила и закончила скачивание остальных 90% за остальную часть ночи, когда сетевое соединение вернулось в онлайн.

Встроенный в веб-браузер клиент скачивания по умолчанию тоже можно использовать, указав URL вида https://localhost/DownloadPortal/Download?file=test.txt&chunksize=5242880.

Заметьте, что параметр (chunksize=5242880) также является не обязательным, когда вы используете веб-браузер в качестве клиента скачивания. Если этот параметр не задан, сервер будет передавать весь файл одной порцией с помощью той же HttpResponse.TransmitFile. А если параметр включен, сервер будет выполнять отдельный вызов HttpResponse.TransmitFile для каждой порции.

На рис. 4 дана высокоуровневая схема при использовании DownloadHandler.dll с веб-браузером, который не поддерживает частичный контент при скачивании. Она иллюстрирует поток обработки при прохождении порций файла с жесткого диска серверной машины через DownloadHandler.dll и веб-браузер в файл на жестком диске клиентской машины с этим браузером.

Рис. 4. Высокоуровневая схема потока обработки для DownloadHandler.dll (при использовании веб-браузера, который не поддерживает частичный контент как клиент)

{Для верстки: и вновь не забудьте о диапазонах}

Client Machine Клиентская машина
Web Browser Веб-браузер
Hard Drive Жесткий диск
file.txt (25MB) file.txt (25 Мб)
BROWSER URL (INCLUDES CHUNKSIZE) URL браузера (включает chunksize)
TransmitFile sends byte range of file straight to HttpResponse Stream without buffering in server memory TransmitFile посылает диапазон байтов прямо в HttpResponse Stream без буферизации в памяти сервера
BROWSER URL (DOESN’T INCLUDE CHUNKSIZE) URL браузера (не включает chunksize)
Server Machine Серверная машина
IIS IIS
IHttpAsyncHandler (DownloadHandler.dll) IHttpAsyncHandler (DownloadHandler.dll)
For each loop parses total file size into chunks, calling TransmitFile on each chunk Цикл for each разбивает файл на порции, вызывая TransmitFile для каждой порции
TransmitFile (entire 25MB file) TransmitFile (общий размер файла — 25 Мб)

Интересная особенность нашей реализации интерфейса IHttpAsyncHandler в IIS Web Server — поддержка «обслуживания байтов» («byte serving»). Для этого сервер передает HTTP-заголовок Accept-Ranges в своем HTTP-ответе (Accept-Ranges: bytes), сообщая клиенту, что он будет обслуживать порции файла (диапазон частичного контента). Если клиент скачивания по умолчанию в веб-браузере поддерживает частичный контент, он может отправить серверу в своем HTTP-запросе HTTP-заголовок Range (Range: bytes=5242880-10485760). Когда сервер возвращает такому клиенту частичный контент, он отправляет обратно HTTP-заголовок Content-Range в HTTP-ответе (Content-Range: bytes 5242880-10485760/26214400). Поэтому в зависимости от того, какой веб-браузер вы используете и какой клиент скачивания по умолчанию встроен в этот браузер, вы можете получать некоторые из тех же преимуществ, что и при работе с автономным исполняемым клиентом. В любом случае большинство веб-браузеров позволяет вам создавать собственный клиент скачивания и подключать его к браузеру, заменяя встроенный по умолчанию.

Конфигурация проекта-примера

Для проекта-примера просто скопируйте DownloadHandler.dll и IOThreads.dll в каталог \bin в виртуальном каталоге и поместите следующую запись в разделы handlers и modules файла web.config:

<handlers>
  <add name="Download" verb="*" path="Download"
    type="DownloaderHandlers.DownloadHandler" />
</handlers>

<modules>
  <add name="CustomBasicAuthenticationModule"
    preCondition="managedHandler" type=
    "DownloaderHandlers.CustomBasicAuthenticationModule" />
</modules>

Если на IIS Server нет виртуального каталога, создайте его с каталогом \bin внутри, сделайте его Application и убедитесь, что он использует Microsoft .NET Framework 4 Application Pool.

Пользовательский модуль базовой аутентификации опирается на простой в применении AspNetSqlMembershipProvider, который сегодня работает на многих веб-сайтах ASP.NET, сохраняя имя и пароль пользователя, необходимые для скачивания файла, в базе данных aspnetdb в SQL Server. Одно из преимуществ AspNetSqlMembershipProvider — пользователю не требуется учетная запись в домене Windows. Подробные инструкции по установке AspNetSqlMembershipProvider и настройкам, нужным для IIS Server, чтобы сконфигурировать учетные записи пользователей и SSL-сертификат, перечислены в разделе comment в начале класса CustomBasicAuthenticationModule (см. пакет сопутствующего исходного кода). Другие дополнительные конфигурационные параметры, используемые для тонкой настройки IIS Server, обычно задаются IT-отделом, который управляет сервером. Их описание выходит за рамки этой статьи, но если в вашем случае дело обстоит иначе, они доступны в TechNet Library (bit.ly/1JRJjNS).

Вот и все. Ничего особо сложного.

Важные факторы

Наиболее важный фактор, определяющий проект, — не в том, чтобы его архитектура обеспечивала более высокую производительность, а в повышении надежности и отказоустойчивости при простоях сети, вызванных нестабильными сетевыми соединениями, периодически переходящими из онлайн в офлайн.

Как правило, скачивание одного файла одной порцией по одному соединению обеспечивает максимальную пропускную способность. Но бывают некоторые уникальные исключения из этого правила, такие как серверные среды с зеркалированием, где файл скачивается отдельными частями, причем каждая часть файла извлекается с другого сервера-зеркала, как показано на рис. 5. Но в целом, скачивание файла в нескольких потоках на самом деле медленнее, чем в одном потоке, потому что узким местом обычно является сеть. Однако возможность повторного запроса только тех частей файла, которые не удалось скачать, причем до тех пор, пока они не будут успешно загружены, без необходимости заново начинать весь процесс скачивания обеспечивает то, что мы предпочитаем называть псевдо-отказоустойчивостью (quasi-fault tolerance).

Рис. 5. Гипотетические будущие расширения для симуляции крайне рудиментарной инфраструктуры зеркалированияРис. 5. Гипотетические будущие расширения для симуляции крайне рудиментарной инфраструктуры зеркалирования

Mirror Server Environment Серверная среда с зеркалированием
Node Узел
IIS IIS
Client Node Клиентский узел
Exe Exe
file.txt file.txt
Gets all the individual pieces of file from mirror nodes in parallel Получаем все индивидуальные части файла параллельно с узлов-зеркал
Writes all the individual pieces of file to client using overlapped file I/O Записываем все индивидуальные части файла на клиенте, используя перекрытый файловый ввод-вывод
Web Request URL: URL веб-запроса:
(Range: bytes=0-5242880 is appended to web request) (Range: bytes=0-5242880 добавляется в веб-запрос)
(Range: bytes=5242880-10485760 is appended to web request) (Range: bytes=5242880-10485760 добавляется в веб-запрос)
Client URL: URL клиента
Verifies MD5 matches after copy complete По окончании копирования проверяем контрольную сумму MD5
Parallel.ForEach iterates string split of “node name:byte range” query string parameter and sends Web request to that mirror node requesting that byte range, instead of iterating splitting total file size into chunks Parallel.ForEach разбирает в цикле строковый параметр запроса на части вида «node name:byte range» и посылает веб-запрос указанному узлу, запрашивая от него этот диапазон байтов, а не разбивает весь файл на порции

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

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

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

Есть еще три фактора, гораздо менее важных, но, тем не менее, их стоит принять во внимание.

Во-первых, поскольку проект включает как клиент, так и сервер, у вас есть полный контроль над конфигурацией на серверной стороне. Это дает вам полную свободу в настройке конфигурационных параметров, которые зачастую сильно затрудняют процесс скачивания файлов с серверов, принадлежащих сторонним лицам и находящихся вне вашего контроля. Например, вы можете настроить ограничение по числу соединений с каждого клиентского IP-адреса в сторону увеличения лимита против обычных двух соединений. Вы также можете убавить ограничение по скорости для каждого клиентского соединения.

Во-вторых, код проекта-примера в нашем клиенте (FileDownloader.exe) и сервере (DownloadHandler.dll) может послужить в качестве простых и понятных блоков, демонстрирующих применение HTTP-заголовков запросов и ответов, необходимых для передачи диапазонов байтов частичного контента по протоколу HTTP. Легко увидеть, какие HTTP-заголовки запроса должен отправлять клиент, чтобы запрашивать диапазоны байтов, и какие HTTP-заголовки ответов должен передавать сервер, чтобы возвращать диапазоны байтов как частичный контент. Модифицировать этот код для реализации более высокоуровневой функциональности поверх нашей базовой функциональности или реализовать какую-то более сложную функциональность, доступную в пакетах более серьезных приложений, должно быть сравнительно легко. Кроме того, этот проект можно использовать как отправной шаблон, который относительно упрощает добавление поддержки дополнительных HTTP-заголовков вроде Content-Type: multipart/byteranges, Content-MD5: md5-digest, If-Match: entity-tag и т. д.

Короткие прерывания в работе вашего сетевого соединения не означают, что вам нужно начинать все с начала; вместо этого вы скачиваете только порции, которые не удалось скачать.

В-третьих, так как в проекте используется IIS Web Server, вы автоматом выигрываете от некоторой встроенной функциональности, предоставляемой сервером. Например, коммуникации можно автоматически шифровать (используя HTTPS с SSL-сертификатом) и сжимать (с помощью компрессии gzip). Однако применять компрессию gzip не рекомендуется в случае очень больших файлов, если это слишком сильно нагружает процессоры вашего сервера. Но, если процессоры вашего сервера вполне справляются с дополнительной нагрузкой, эффективность передачи гораздо меньших сжатых данных иногда дает большую разницу в общей пропускной способности системы.

Будущие расширения

Код проекта-примера обеспечивает лишь минимальную базовую функциональность, необходимую для работы утилиты для скачивания файлов. Нашей целью было поддержание простоты проекта и легкости его понимания, чтобы его можно было относительно легко использовать как базис для дальнейшего расширения. Это просто отправная точка и базовый шаблон. Перед его коммерческим использованием абсолютно необходимо внести много расширений. Добавление более высокоуровневых абстракций, обеспечивающих дополнительную, более продвинутую функциональность, мы оставляем читателю в качестве упражнения. However, мы разъясним некоторые из критически важных улучшений.

Код проекта-примера в настоящее время не включает контрольную сумму MD5 для файла. На практике важно применять какую-то стратегию вычисления контрольной суммы файла, чтобы гарантировать соответствие файла, скачанного на клиентскую машину, с файлом на сервере и то, что его не пытались как-либо изменить. HTTP-заголовок (Content-MD5: md5-digest) упрощает эту задачу. По сути, один из первых наших прототипов включал вычисление контрольной суммы MD5 для файла всякий раз, когда он запрашивался, и помещал хеш-сумму (digest) в заголовок (Content-MD5: md5-digest) до передачи файла с сервера. Затем клиент выполнял тот же подсчет контрольной суммы MD5 для полученного файла и проверял полученную хеш-сумму с таковой в заголовке (Content-MD5: md5-digest), возвращенным сервером. Если они не совпадали, файл был взломан или поврежден. Хотя это решало задачу, вычисления с большими файлами приводили к слишком интенсивной нагрузке на процессор на сервере и занимали чрезмерно много времени.

На практике это, по-видимому, потребует создать тот или иной уровень кеширования, который в фоновом режиме обработает файл для вычисления контрольной суммы MD5 только один раз и сохранит полученную хеш-сумму в словаре, где имя файла будет использоваться как ключ. Тогда для получения хеш-суммы файла серверу потребуется лишь простой поиск по словарю, и эту хеш-сумму можно будет моментально добавлять в заголовок при передаче файла с минимальной нагрузкой на процессоры сервера.

Мы хотели предоставить вам простую и понятную базу, включающую лишь необходимый минимум.

Код проекта примера в настоящее время также не ограничивает клиент от использования гигантского количества потоков и может разбивать файл на слишком много порций, если размер файла велик. Это, конечно, позволяет клиенту делать то, что ему нужно для гарантированного скачивания файла. Но на практике понадобится какая-то инфраструктура, способная накладывать ограничения для клиента, чтобы один клиент не умудрился «положить» сервер и заставить ждать все остальные клиенты.

Рис. 5 иллюстрирует гипотетические будущие улучшения для симуляции крайне рудиментарной инфраструктуры зеркалирования, модифицируя проект так, чтобы он предоставлял список пар «имя узла — диапазон байтов» в качестве строкового параметра запроса в URL вместо текущего параметра chunksize. Существующий проект можно было бы относительно легко изменить так, чтобы получать каждую порцию файла от другого сервера простым перебором пар «имя узла — диапазон байтов», вызовом HttpWebRequest для каждой пары вместо разбиения размера всего файла на порции в соответствии с параметром chunksize и вызовом HttpWebRequest для каждой порции.

Вы могли бы сформировать URL для HttpWebRequest простой заменой имени сервера именем сопоставленного узла из списка пар «имя узла — диапазон байтов», добавлением связанного диапазона байтов в HTTP-заголовок Range (т. е. Range: bytes=0-5242880), а затем полным удалением этого списка из URL. Подходящий файл метаданных помогал бы идентифицировать, на каких серверах находятся части файла, а впоследствии машина, выдавшая запрос, могла бы собрать файл из отдельных частей, распределенных по разным серверам.

Если файл зеркалируется на 10 серверах, проект можно было бы модифицировать для получения части 1 файла с зеркальной копии сервера 1, части 2 с зеркальной копии сервера 2 и т. д. И вновь важно вычислять контрольную сумму MD5 файла после получения всех его частей и их сборки на клиенте, чтобы быть уверенным в том, что ни одна из частей не повреждена и что вы действительно получили весь файл. Можно было бы даже выйти на новый уровень и использовать территориально распределенные серверы с определением наименее загруженных из них в данный момент.

Заключение

Мы не ставили целью нашего проекта создание более быстрой и масштабируемой утилиты для скачивания файлов, а хотели разработать такую программу, которая чрезвычайно устойчива к периодическим отказам сети.

Мы предприняли большие усилия, чтобы наш проект был очень прост и четко демонстрировал, как использовать HTTP-заголовки для обслуживания диапазонов байтов и частичного контента.

В нашем исследовании мы обнаружили, что довольно трудно найти хороший ясный пример того, как осуществляется простое «обслуживание байтов» по HTTP и как корректно использовать HTTP-заголовки для диапазонов байтов. Большинство примеров было либо излишне сложными, либо использовали множество других заголовков для реализации гораздо более продвинутой функциональности протокола HTTP, затрудняя понимание, не говоря уже о расширении или улучшении таких примеров.

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


Дуг Дьюрнер (Doug Duerner) — старший инженер ПО с более чем 15-летним опытом проектирования и реализации крупномасштабных систем на основе технологий Microsoft. Работал в нескольких банковских учреждениях из списка «Fortune 500» и на компанию, которая проектировала и создавала систему управления крупномасштабными распределенными сетями, использовавшимися Агентством оборонных информационных систем Defense Information Systems Agency (DISA) Министерства обороны для своей «глобальной информационной сети» и Государственным департаментом США. В глубине души помешан на компьютерах, но получает удовольствие от наиболее комплексных и трудных технических задач, особенно тех, которые всеми считаются невыполнимыми. С ним можно связаться по адресу coding.innovation@gmail.com.

Юн-Чанг Ванг (Yeon-Chang Wang) — старший инженер ПО с более чем 15-летним опытом проектирования и реализации крупномасштабных систем на основе технологий Microsoft. Тоже работал в нескольких банковских учреждениях из списка «Fortune 500» и на компанию, которая проектировала и создавала систему управления крупномасштабными распределенными сетями, использовавшимися агентством оборонных информационных систем Defense Information Systems Agency (DISA) Министерства обороны для своей «глобальной информационной сети» и Государственным департаментом США. Кроме того, занимался проектированием и реализацией крупномасштабной системы сертификации драйверов для одного из крупнейшего в мире производителя чипов. Имеет степень магистра в области компьютерных наук. Сложнейшие проблемы щелкает как орешки. С ним можно связаться по адресу yeon_wang@yahoo.com.

Выражаем благодарность за рецензирование статьи экспертамMicrosoft Стивену Клири (StephenCleary) и Джеймсу Маккафри (JamesMcCaffrey).