Text/Tao Gang
Обработка загрузок больших файлов в веб-приложениях всегда была чрезвычайно сложной, поэтому для большинства сайтов горе постигает пользователя, если их загрузка прерывается. Но нам не обязательно делать это сейчас, потому что вы можете сделать свое приложение ASP.NET способным поддерживать возобновляемые (продолжаемые) загрузки больших файлов. Используя метод, представленный в этой статье, вы можете отслеживать процесс загрузки, чтобы иметь возможность обрабатывать динамически создаваемые файлы — и делать это без необходимости использования динамических библиотек старой школы ISAPI и неуправляемого кода C++.
Проще всего предоставить клиентам услугу по загрузке файлов из Интернета, не так ли? Просто скопируйте загружаемый файл в каталог вашего веб-приложения, опубликуйте ссылку и позвольте IIS выполнить всю соответствующую работу. Однако обслуживание файлов не должно быть чем-то большим, чем головная боль: вы не хотите, чтобы весь мир имел доступ к вашим данным, вы не хотите, чтобы ваш сервер был забит сотнями статических файлов, вы не хотите Даже не хочу Загружать временные файлы. Эти файлы создаются только во время простоя после того, как клиент начинает загрузку.
К сожалению, невозможно добиться этих эффектов, используя стандартный ответ IIS на запросы загрузки. Таким образом, в целом, чтобы получить контроль над процессом загрузки, разработчикам необходимо создать ссылку на пользовательскую страницу .aspx, где они проверяют учетные данные пользователя, создают загружаемый файл и используют следующий код для передачи файла клиенту:
Response.WriteFile
Response.End()
И вот здесь возникает настоящая проблема.
В чем проблема?
Метод WriteFile выглядит идеально, он передает двоичные данные файла клиенту. Но до недавнего времени мы не знали, что метод WriteFile является печально известным пожирателем памяти, загружая весь файл в оперативную память сервера для обслуживания (на самом деле он занимает вдвое больший размер файла). Для больших файлов это может вызвать проблемы с служебной памятью и возможное дублирование процессов ASP.NET. Но в июне 2004 года Microsoft выпустила патч, решивший проблему. Это исправление теперь является частью пакета обновления (SP1) для .NET Framework 1.1.
В этом патче представлен метод TransmitFile, который считывает файл с диска в меньший буфер памяти, а затем начинает передачу файла. Хотя это решение решает проблемы с памятью и циклами, оно по-прежнему неудовлетворительно. Вы не можете контролировать жизненный цикл ответа. У вас нет возможности узнать, правильно ли завершилась загрузка, у вас нет возможности узнать, была ли загрузка прервана, и (если вы создали временные файлы) у вас нет возможности узнать, следует ли и когда удалять файлы. Что еще хуже, если загрузка все же не удалась, метод TransmitFile начнет загрузку с начала файла, который клиент попытается в следующий раз.
Одно из возможных решений — реализация фоновой интеллектуальной службы передачи (BITS) — неприемлемо для большинства сайтов, поскольку оно противоречит цели поддержания независимости клиентского браузера и операционной системы.
В основе удовлетворительного решения лежит первая попытка Microsoft решить проблему путаницы в памяти, вызванную WriteFile (см. статью 812406 базы знаний). В этой статье был продемонстрирован интеллектуальный процесс загрузки фрагментов данных, который считывает данные из файлового потока. Прежде чем сервер отправит фрагмент байта клиенту, он использует свойство Response.IsClientConnected, чтобы проверить, подключен ли клиент. Если соединение все еще открыто, оно продолжает отправлять байты потока, в противном случае оно останавливается, чтобы предотвратить отправку сервером ненужных данных.
Именно такого подхода мы придерживаемся, особенно при загрузке временных файлов. В случае, когда IsClientConnected возвращает False, вы знаете, что процесс загрузки был прерван, и вам следует сохранить файл, в противном случае, когда процесс завершится успешно, вы удалите временный файл; Кроме того, чтобы возобновить прерванную загрузку, все, что вам нужно сделать, — это начать загрузку с того места, где при последней попытке загрузки произошел сбой соединения клиента.
Поддержка протокола HTTP и информации заголовка (заголовок).
Поддержка протокола HTTP может использоваться для обработки информации заголовка при прерванных загрузках. Используя небольшое количество заголовков HTTP, вы можете улучшить процесс загрузки, чтобы он полностью соответствовал спецификации протокола HTTP. Эта спецификация вместе с диапазонами предоставляет всю информацию, необходимую для возобновления прерванной загрузки.
Вот как это работает. Во-первых, если сервер поддерживает возобновляемые загрузки на стороне клиента, он отправляет заголовок Accept-Ranges в первоначальном ответе. Сервер также отправляет заголовок тега объекта (ETag), который содержит уникальную идентифицирующую строку.
В приведенном ниже коде показаны некоторые заголовки, которые IIS отправляет клиенту в ответ на первоначальный запрос на загрузку, который передает клиенту сведения о запрошенном файле.
HTTP/1.1 200 ОК
Соединение: закрыть
Дата: вторник, 19 октября 2004 г., 15:11:23 по Гринвичу.
Accept-Ranges: байты
Последнее изменение: воскресенье, 26 сентября 2004 г., 15:52:45 GMT.
ETag: "47febb2cfd76c41:2062"
Контроль кэша: частный
Тип контента: приложение/x-zip-сжатый
Content-Length: 2844011
После получения этой информации заголовка, если загрузка прервана, браузер IE отправит значение Etag и информацию заголовка Range обратно на сервер в последующих запросах на загрузку. В приведенном ниже коде показаны некоторые заголовки, которые IE отправляет на сервер при попытке возобновить прерванную загрузку.
GET
Эти заголовки указывают, что IE кэшировал тег объекта, предоставленный IIS, и отправил его обратно на сервер в заголовке If-Range. Это способ гарантировать возобновление загрузки из того же файла. К сожалению, не все браузеры работают одинаково. Другими заголовками HTTP, отправляемыми клиентом для проверки файла, могут быть If-Match, If-Unmodified-Since или Unless-Modified-Since. Очевидно, что в спецификации не указано, какие заголовки клиентское программное обеспечение должно поддерживать или какие заголовки следует использовать. Поэтому некоторые клиенты вообще не используют информацию заголовка, тогда как IE использует только If-Range и Unless-Modified-Since. Эту информацию лучше проверить с помощью кода. При таком подходе ваше приложение может соответствовать спецификации HTTP на очень высоком уровне и работать с различными браузерами. Заголовок Range указывает запрошенный диапазон байтов — в данном случае это отправная точка, с которой сервер должен возобновить поток файлов.
Когда IIS получает тип запроса на загрузку возобновления, он отправляет обратно ответ, содержащий следующую информацию заголовка:
HTTP/1.1 206 Частичное содержимое
Диапазон содержимого: 822603-2844010/2844011 байт.
Accept-Ranges: байты
Последнее изменение: воскресенье, 26 сентября 2004 г., 15:52:45 GMT.
ETag: "47febb2cfd76c41:2062"
Контроль кэша: частный
Тип контента: приложение/x-zip-сжатый
Длина контента: 2021408
Обратите внимание, что приведенный выше код имеет немного другой HTTP-ответ, чем исходный запрос на загрузку: запрос на возобновление загрузки имеет номер 206, а исходный запрос на загрузку — 200. Это указывает на то, что передаваемый по сети файл является частичным. На этот раз заголовок Content-Range указывает точное количество и расположение передаваемых байтов.
IE очень требователен к этой информации в заголовках. Если первоначальный ответ не содержит информации заголовка Etag, IE никогда не попытается возобновить загрузку. Другие клиенты, которые я тестировал, не используют заголовок ETag, они просто полагаются на имя файла, область запроса и используют заголовок Last-Modified, если пытаются проверить файл.
Углубленное понимание протокола HTTP.
Информация заголовка, показанная в предыдущем разделе, достаточна для того, чтобы решение по возобновлению загрузок работало, но она не полностью охватывает спецификацию HTTP.
Заголовок Range может запрашивать несколько диапазонов в одном запросе — функция, называемая «составные диапазоны». Не путать с сегментированной загрузкой: почти все инструменты загрузки используют сегментированную загрузку для увеличения скорости загрузки. Эти инструменты утверждают, что увеличивают скорость загрузки за счет открытия двух или более одновременных соединений, каждое из которых запрашивает различный диапазон файлов.
Идея составных диапазонов не открывает несколько соединений, но позволяет клиентскому программному обеспечению запрашивать первые десять и последние десять байтов файла за один цикл запроса/ответа.
Честно говоря, я никогда не нашел программы, использующей эту функцию. Но я отказываюсь писать «он не полностью HTTP-совместим» в объявлении кода. Отсутствие этой функции определенно нарушит закон Мерфи. Тем не менее, составные диапазоны используются при передаче электронной почты для разделения информации заголовка, обычного текста и вложений.
Пример кода
Мы знаем, как клиент и сервер обмениваются информацией заголовка, чтобы обеспечить возобновление загрузок. Объединив эти знания с идеей потоковой передачи файлов, вы можете добавить в свои приложения ASP.NET надежные возможности управления загрузками.
Чтобы получить контроль над процессом загрузки, необходимо перехватить запрос на загрузку от клиента, прочитать информацию заголовка и ответить соответствующим образом. До появления .NET вам приходилось писать приложение ISAPI (Internet Server API) для реализации этой функциональности, но компонент .NET Framework предоставляет интерфейс IHttpHandler, который при реализации в классе позволяет вам делать это, используя только код .NET Intercept. и обрабатывать запросы. Это означает, что ваше приложение имеет полный контроль и оперативность процесса загрузки и никогда не задействует и не использует автоматизированные функции IIS.
Пример кода включает пользовательский класс HttpHandler (ZIPHandler) в файле HttpHandler.vb. ZipHandler реализует интерфейс IhttpHandler и обрабатывает запросы для всех ZIP-файлов.
Чтобы протестировать пример кода, вам необходимо создать новый виртуальный каталог в IIS и скопировать туда исходные файлы. Создайте в этом каталоге файл с именем download.zip (обратите внимание, что IIS и ASP.NET не могут обрабатывать загрузки размером более 2 ГБ, поэтому убедитесь, что ваш файл не превышает этот предел). Настройте виртуальный каталог IIS для сопоставления расширения .zip через aspnet_isapi.dll.
Класс HttpHandler: после того как ZIPHandler
сопоставляет расширение .zip в ASP.NET, каждый раз, когда клиент запрашивает ZIP-файл с сервера, IIS вызывает метод ProcessRequest класса ZipHandler (см. код загрузки).
Метод ProcessRequest сначала создает экземпляр пользовательского класса FileInformation (см. код загрузки), который инкапсулирует статус загрузки (например, выполняется, прервано и т. д.). В примере путь к образцу файла download.zip жестко закодирован в коде. Если вы примените этот код к своему приложению, вам нужно будет изменить его, чтобы открыть запрошенный файл.
' Используйте objRequest, чтобы определить, какой файл был запрошен, и используйте файл для открытия objFile.
' Например, objFile = New Download.FileInformation(<полное имя файла>)
objFile = Новая загрузка.FileInformation( _
objContext.Server.MapPath("~/download.zip"))
Далее программа выполняет запрос, используя описанные HTTP-заголовки (если заголовки были предоставлены в запросе, я молился впервые под солнцем). Если проверка не удалась, ответ немедленно прекращается и отправляется соответствующее значение StatusCode.
Если нет, objRequest.HttpMethod.Equals(HTTP_METHOD_GET) или нет
objRequest.HttpMethod.Equals(HTTP_METHOD_HEAD) Тогда
' В настоящее время поддерживаются только методы GET и HEAD objResponse.StatusCode = 501 ' Не выполняется
ИначеЕсли Не objFile.Существует Тогда
' Запрошенный файл не найден objResponse.StatusCode = 404 ' Не найден
ИначеЕсли objFile.Length > Int32.MaxValue Тогда
'Файл слишком велик objResponse.StatusCode = 413 'Сущность запроса слишком велика
ElseIf Not ParseRequestHeaderRange(objRequest, alRequestedRangesBegin, alRequestedRangesend, _
objFile.Length, bisRangeRequest) Тогда
' Запрос Range содержит бесполезные объекты objResponse.StatusCode = 400 ' Бесполезный запрос
ИначеЕсли Не CheckIfModifiedSince(objRequest,objFile) Тогда
'Субъект не был изменен objResponse.StatusCode = 304 'Субъект не был изменен
ИначеЕсли Не CheckIfUnmodifiedSince(objRequest,objFile) Тогда
' Объект был изменен с момента последнего запроса objResponse.StatusCode = 412 ' Не удалось выполнить предварительную обработку
ИначеЕсли Не CheckIfMatch(objRequest, objFile) Тогда
' Объект не соответствует запросу objResponse.StatusCode = 412 ' Не удалось выполнить предварительную обработку
ИначеЕсли Не CheckIfNoneMatch(objRequest, objResponse,objFile) Тогда
' Объект соответствует запросу, не имеющему соответствия.
'Код ответа находится в функции CheckIfNoneMatch
Еще
'Предварительная проверка прошла успешно
Функция ParseRequestHeaderRange в этих предварительных проверках (см. код загрузки) проверяет, запросил ли клиент диапазон файлов (что означает частичную загрузку). Если запрошенный диапазон недействителен (недопустимый диапазон — это значение диапазона, которое превышает размер файла или содержит необоснованное число), этот метод устанавливает для bIsRangeRequest значение True. Если запрашивается диапазон, метод CheckIfRange проверяет информацию заголовка IfRange.
Если запрошенный диапазон действителен, код вычисляет размер ответного сообщения. Если клиент запросил несколько диапазонов, значение размера ответа будет включать значение длины составного заголовка.
Если значение отправленного заголовка невозможно определить, программа будет обрабатывать запрос на загрузку как первоначальный запрос, а не как частичную загрузку, отправляя новый поток загрузки, начиная с начала файла.
Если bIsRangeRequest AndAlso CheckIfRange(objRequest, objFile) Тогда
«Это запрос диапазона». Если массив Range содержит несколько объектов, это также запрос диапазона, состоящий из нескольких частей. bMultipart = CBool(alRequestedRangesBegin.GetUpperBound(0)>0)
' Переходим в каждый диапазон, чтобы получить всю длину ответа. For iLoop = alRequestedRangesBegin.GetLowerBound(0) To alRequestedRangesBegin.GetUpperBound(0)
'Длина контента (в этом диапазоне)
iResponseContentLength += Convert.ToInt32(alRequestedRangesend( _
iLoop) — alRequestedRangesBegin(iLoop)) + 1
Если bMultipart Тогда
' Если это запрос диапазона, состоящий из нескольких частей, вычисляется длина передаваемой информации промежуточного заголовка iResponseContentLength += MULTIPART_BOUNDARY.Length
iResponseContentLength += objFile.ContentType.Length
iResponseContentLength += alRequestedRangesBegin(iLoop).ToString.Length
iResponseContentLength += alRequestedRangesend(iLoop).ToString.Length
iResponseContentLength += objFile.Length.ToString.Length
' 49 — длина разрывов строк и других необходимых символов при загрузке, состоящей из нескольких частей. iResponseContentLength += 49
Конец, если
Следующий iLoop
Если bMultipart Тогда
' Если это запрос диапазона, состоящий из нескольких частей,
' Мы также должны вычислить длину последнего промежуточного заголовка, который будет отправлен iResponseContentLength +=MULTIPART_BOUNDARY.Length
' 8 — длина тире и новой строки iResponseContentLength += 8
Еще
' Это не загрузка из нескольких частей, поэтому мы должны указать диапазон ответа исходного HTTP-заголовка objResponse.AppendHeader( HTTP_HEADER_CONTENT_RANGE, "bytes" & _
alRequestedRangesBegin(0).ToString & "-" & _
alRequestedRangesend(0).ToString & "/" & _
objFile.Length.ToString)
'Конец, если
' Ответ диапазона objResponse.StatusCode = 206 ' Частичный ответ Else
' Это не запрос области, или запрошенный идентификатор объекта области не соответствует текущему идентификатору объекта,
«Итак, начните новую загрузку» означает, что размер завершенной части файла равен длине содержимого iResponseContentLength =Convert.ToInt32(objFile.Length)
'Возврат к нормальному состоянию ОК objResponse.StatusCode = 200
Конец, если
' Затем сервер должен отправить несколько важных заголовков ответа, таких как длина содержимого, Etag и тип содержимого файла:
' Записываем длину содержимого в ответ objResponse.AppendHeader( HTTP_HEADER_CONTENT_LENGTH,iResponseContentLength.ToString)
' Записываем дату последнего изменения в ответ objResponse.AppendHeader( HTTP_HEADER_LAST_MODIFIED,objFile.LastWriteTimeUTC.ToString("r"))
' Сообщаем клиентскому программному обеспечению, что мы приняли запрос диапазона objResponse.AppendHeader( HTTP_HEADER_ACCEPT_RANGES,HTTP_HEADER_ACCEPT_RANGES_BYTES)
' Записываем тег объекта файла в ответ (заключенный в кавычки)
objResponse.AppendHeader(HTTP_HEADER_ENTITY_TAG, """" & objFile.EntityTag & """")
'Записываем тип контента в responseIf bMultipart then
«Сообщения, состоящие из нескольких частей, имеют этот специальный тип». В этом примере фактический тип mime файла записывается в ответ позже objResponse.ContentType = MULTIPART_CONTENTTYPE
Еще
'Тип содержимого файла, принадлежащий одному частичному сообщению objResponse.ContentType = objFile.ContentType
Конец, если
Все необходимое для скачивания готово и можно приступать к скачиванию файлов. Вы будете использовать объект FileStream для чтения фрагментов байтов из файла. Установите для свойства State экземпляра FileInformation objFile значение fsDownloadInProgress. Пока клиент остается подключенным, сервер считывает фрагменты байтов из файла и отправляет их клиенту. Для многочастных загрузок этот код отправляет конкретную информацию заголовка. Если клиент отключается, сервер устанавливает статус файла fsDownloadBroken. Если сервер завершает отправку запрошенного диапазона, он устанавливает статус fsDownloadFinished (см. код загрузки).
Вспомогательный класс FileInformation
В разделе ZIPHandler вы обнаружите, что FileInformation — это вспомогательный класс, который инкапсулирует информацию о состоянии загрузки (например, загрузка, прерывание и т. д.).
Чтобы создать экземпляр FileInformation, вам необходимо передать путь к запрошенному файлу конструктору класса:
Public Sub New(ByVal sPath As String)
m_objFile = Новый System.IO.FileInfo(sPath)
End Sub
FileInformation использует объект System.IO.FileInfo для получения информации о файле, которая отображается как свойства объекта (например, существует ли файл, полное имя файла, размер и т. д.). Этот класс также предоставляет перечисление DownloadState, которое описывает различные состояния запроса на загрузку:
Enum DownloadState
' Очистить: нет процесса загрузки, файл может поддерживать fsClear = 1
'Заблокировано: динамически созданные файлы не могут быть изменены fsLocked = 2
'В процессе: файл заблокирован и идет процесс загрузки fsDownloadInProgress = 6
'Broken: файл заблокирован, процесс загрузки шел, но был отменен fsDownloadBroken = 10
' Завершено: файл заблокирован и процесс загрузки завершен fsDownloadFinished = 18
End Enum
FileInformation также предоставляет значение атрибута EntityTag. Это значение жестко запрограммировано в примере кода, поскольку в примере кода используется только один загружаемый файл, и этот файл не будет изменен, но для реального приложения вы предоставите несколько файлов, даже динамически. Для создания файлов ваш код должен предоставить уникальное значение EntityTag для каждого файла. Кроме того, это значение должно меняться каждый раз, когда файл изменяется или модифицируется. Это позволяет клиентскому программному обеспечению проверять актуальность загруженных фрагментов байтов. Вот часть примера кода, который возвращает жестко закодированное значение EntityTag:
Public ReadOnly Property EntityTag() As String
' EntityTag используется для начального (200) ответа клиенту и для запросов на восстановление от клиента.
' Создаем уникальную строку для файла.
' Обратите внимание, что до тех пор, пока файл не изменится, уникальный код должен сохраняться.
' Однако, если файл изменяется или модифицируется, этот код должен измениться.
Вернуть «MyExampleFileID»
Конец Получить
Свойство End
Простой и, как правило, достаточно безопасный EntityTag может состоять из имени файла и даты последнего изменения файла. Независимо от того, какой метод вы используете, вы должны убедиться, что это значение действительно уникально и его нельзя спутать с EntityTag других файлов. Я хотел бы динамически называть созданные файлы в моем приложении по индексу клиента, клиента и почтового индекса и сохранять GUID, используемый в качестве EntityTag, в базе данных.
Класс ZipFileHandler считывает и устанавливает свойство public State. После завершения загрузки он устанавливает состояние fsDownloadFinished. В это время вы можете удалить временные файлы. Здесь обычно нужно вызвать метод Save для сохранения состояния.
Государство публичной собственности() как DownloadState
Получать
Вернуть m_nState
Конец Получить
Установить (ByVal nState As DownloadState)
m_nState = nState
' Необязательное действие: в это время вы можете автоматически удалить файл.
' Если статус установлен на «Завершено», этот файл вам больше не нужен.
' Если nState =DownloadState.fsDownloadFinished Тогда
'Прозрачный()
'Еще
'Сохранять()
'Конец, если
Сохранять()
Конечный набор
Конечное свойство
ZipFileHandler должно вызывать метод Save каждый раз при изменении статуса файла, чтобы сохранить статус файла, чтобы его можно было отобразить пользователю позже. Вы также можете использовать его для сохранения созданного вами EntityTag. Не сохраняйте состояние файла и значение EntityTag в приложении, сеансе или кеше — вы должны сохранять информацию на протяжении всего жизненного цикла всех этих объектов.
ПриватСубСэйв()
'Сохраните статус загрузки файла в базе данных или XML-файле.
' Конечно, если вы не создаете файл динамически, вам не нужно сохранять это состояние.
End Sub
Как упоминалось ранее, пример кода обрабатывает только существующий файл (download.zip), но вы можете дополнительно улучшить эту программу, чтобы при необходимости создавать запрошенный файл.
При тестировании примера кода ваша локальная система или локальная сеть могут быть слишком быстрыми, чтобы прервать процесс загрузки, поэтому я рекомендую вам использовать медленное LAN-соединение (уменьшение пропускной способности сайта в IIS — это метод моделирования) или поставить сервер Интернет.
Загрузка файлов на клиенте по-прежнему является проблемой. Неправильный или неправильно настроенный сервер веб-кеша, управляемый интернет-провайдером, может привести к сбою загрузки больших файлов, включая низкую производительность загрузки или досрочное завершение сеанса. Если размер файла превышает 255 МБ, вам следует рекомендовать клиентам использовать стороннее программное обеспечение для управления загрузками, хотя в некоторые современные браузеры встроены базовые менеджеры загрузок.
Если вы хотите расширить пример кода, возможно, будет полезно ознакомиться со спецификацией HTTP. Вы можете установить контрольные суммы MD5 для загрузок, добавив их с помощью заголовка Content-MD5, чтобы обеспечить возможность проверки целостности загруженных файлов. В примере кода не используются другие методы HTTP, кроме GET и HEAD.