Text/Tao Gang
Manejar descargas de archivos grandes en aplicaciones web siempre ha sido notoriamente difícil, por lo que para la mayoría de los sitios, el usuario se aflige si se interrumpe la descarga. Pero no tenemos que hacer eso ahora, porque puede hacer que su aplicación ASP.NET sea capaz de admitir descargas reanudables (continuables) de archivos grandes. Con el método proporcionado en este artículo, puede realizar un seguimiento del proceso de descarga, de modo que pueda manejar archivos creados dinámicamente, y hacerlo sin la necesidad de bibliotecas de vínculos dinámicos ISAPI de la vieja escuela ni código C++ no administrado.
Es más fácil ofrecer un servicio para que los clientes descarguen archivos de Internet, ¿verdad? Simplemente copie el archivo descargable en el directorio de su aplicación web, publique el enlace y deje que IIS haga todo el trabajo relacionado. Sin embargo, el servicio de archivos no debería ser más que un dolor de cabeza, no quieres que todo el mundo tenga acceso a tus datos, no quieres que tu servidor esté obstruido con cientos de archivos estáticos, no Ni siquiera quiero descargar archivos temporales: estos archivos solo se crean durante el tiempo de inactividad después de que el cliente comienza a descargar.
Lamentablemente, no es posible lograr estos efectos utilizando la respuesta predeterminada de IIS para las solicitudes de descarga. Entonces, en general, para obtener control sobre el proceso de descarga, los desarrolladores deben vincularse a una página .aspx personalizada donde verifican las credenciales del usuario, crean un archivo descargable y usan el siguiente código para enviar el archivo al cliente:
Response.WriteFile
Response.End()
Y aquí es donde surge el verdadero problema.
¿Cuál es el problema?
El método WriteFile parece perfecto, hace que los datos binarios del archivo fluyan al cliente. Pero lo que no sabíamos hasta hace poco es que el método WriteFile consume mucha memoria, ya que carga el archivo completo en la RAM del servidor para servirlo (de hecho, ocupa el doble del tamaño del archivo). Para archivos grandes, esto puede causar problemas de memoria de servicio y posiblemente duplicar procesos ASP.NET. Pero en junio de 2004, Microsoft lanzó un parche que resolvió el problema. Este parche ahora forma parte del Service Pack (SP1) de .NET Framework 1.1.
Este parche introduce el método TransmitFile, que lee un archivo de disco en un búfer de memoria más pequeño y luego comienza a transferir el archivo. Aunque esta solución resuelve los problemas de memoria y bucle, sigue siendo insatisfactoria. No tienes control sobre el ciclo de vida de la respuesta. No tiene forma de saber si la descarga se completó correctamente, no tiene forma de saber si la descarga se interrumpió y (si creó archivos temporales) no tiene forma de saber si debe eliminar los archivos y cuándo. Para empeorar las cosas, si la descarga falla, el método TransmitFile comienza a descargar desde el principio del archivo que el cliente intenta a continuación.
Una posible solución, implementar un Servicio de transferencia inteligente en segundo plano (BITS), no es factible para la mayoría de los sitios porque frustraría el propósito de mantener la independencia del navegador del cliente y del sistema operativo.
La base para una solución satisfactoria proviene del primer intento de Microsoft de resolver el problema de confusión de memoria causado por WriteFile (consulte el artículo 812406 de la base de conocimientos). Ese artículo demostró un proceso inteligente de descarga de datos fragmentados que lee datos de un flujo de archivos. Antes de que el servidor envíe el fragmento de bytes al cliente, utiliza la propiedad Response.IsClientConnected para verificar si el cliente todavía está conectado. Si la conexión aún está abierta, continúa enviando bytes de flujo; de lo contrario, se detiene para evitar que el servidor envíe datos innecesarios.
Este es el enfoque que adoptamos, especialmente al descargar archivos temporales. En el caso de que IsClientConnected devuelva False, usted sabe que el proceso de descarga se interrumpió y debe guardar el archivo; de lo contrario, cuando el proceso se complete exitosamente, eliminará el archivo temporal; Además, para reanudar una descarga interrumpida, todo lo que necesita hacer es iniciar la descarga desde el punto donde falló la conexión del cliente durante el último intento de descarga.
Compatibilidad con el protocolo HTTP y la información del encabezado (encabezado) La compatibilidad con
el protocolo HTTP se puede utilizar para manejar la información del encabezado para descargas interrumpidas. Utilizando una pequeña cantidad de encabezados HTTP, puede mejorar su proceso de descarga para cumplir completamente con la especificación del protocolo HTTP. Esta especificación, junto con los rangos, proporciona toda la información necesaria para reanudar una descarga interrumpida.
Así es como funciona. Primero, si el servidor admite descargas reanudables del lado del cliente, envía el encabezado Accept-Ranges en la respuesta inicial. El servidor también envía un encabezado de etiqueta de entidad (ETag), que contiene una cadena de identificación única.
El siguiente código muestra algunos de los encabezados que IIS envía al cliente en respuesta a una solicitud de descarga inicial, que pasa los detalles del archivo solicitado al cliente.
HTTP/1.1 200 correcto
Conexión: cerrar
Fecha: martes 19 de octubre de 2004 15:11:23 GMT
Rangos de aceptación: bytes
Última modificación: domingo 26 de septiembre de 2004, 15:52:45 GMT
Etiqueta ET: "47febb2cfd76c41:2062"
Control de caché: privado
Tipo de contenido: aplicación/x-zip-comprimido
Longitud del contenido: 2844011
Después de recibir esta información del encabezado, si se interrumpe la descarga, el navegador IE enviará el valor de Etag y la información del encabezado de rango al servidor en solicitudes de descarga posteriores. El siguiente código muestra algunos de los encabezados que IE envía al servidor cuando intenta reanudar una descarga interrumpida.
GET
Estos encabezados indican que IE almacenó en caché la etiqueta de entidad proporcionada por IIS y la envió de regreso al servidor en el encabezado If-Range. Esta es una forma de garantizar que la descarga se reanude exactamente desde el mismo archivo. Lamentablemente, no todos los navegadores funcionan de la misma manera. Otros encabezados HTTP enviados por el cliente para verificar el archivo pueden ser If-Match, If-Unmodified-Since o Unless-Modified-Since. Obviamente, la especificación no es explícita sobre qué encabezados debe admitir el software cliente o qué encabezados deben usarse. Por lo tanto, algunos clientes no utilizan ninguna información de encabezado, mientras que IE solo utiliza If-Range y Unless-Modified-Since. Será mejor que verifiques esta información con el código. Al adoptar este enfoque, su aplicación puede cumplir con la especificación HTTP a un nivel muy alto y funcionar con una variedad de navegadores. El encabezado Range especifica el rango de bytes solicitado; en este caso, es el punto de partida desde el cual el servidor debe reanudar la secuencia del archivo.
Cuando IIS recibe un tipo de solicitud de descarga de currículum, devuelve una respuesta que contiene la siguiente información de encabezado:
HTTP/1.1 206 Contenido parcial
Rango de contenido: bytes 822603-2844010/2844011
Rangos de aceptación: bytes
Última modificación: domingo 26 de septiembre de 2004, 15:52:45 GMT
Etiqueta ET: "47febb2cfd76c41:2062"
Control de caché: privado
Tipo de contenido: aplicación/x-zip-comprimido
Longitud del contenido: 2021408
Tenga en cuenta que el código anterior tiene una respuesta HTTP ligeramente diferente a la solicitud de descarga original: la solicitud para reanudar la descarga es 206, mientras que la solicitud de descarga original era 200. Esto indica que lo que se pasa por el cable es una lima parcial. Esta vez, el encabezado Content-Range indica el número exacto y la ubicación de los bytes que se pasan.
IE es muy exigente con la información de estos encabezados. Si la respuesta inicial no contiene la información del encabezado de Etag, IE nunca intentará reanudar la descarga. Otros clientes que he probado no usan el encabezado ETag, simplemente confían en el nombre del archivo, el alcance de la solicitud y usan el encabezado Última modificación si intentan validar el archivo.
Una comprensión profunda del protocolo HTTP
La información del encabezado que se muestra en la sección anterior es suficiente para que la solución para reanudar las descargas funcione, pero no cubre completamente la especificación HTTP.
El encabezado Rango puede solicitar múltiples rangos en una sola solicitud, una característica llamada "rangos multiparte". No debe confundirse con la descarga segmentada, casi todas las herramientas de descarga utilizan descarga segmentada para aumentar la velocidad de descarga. Estas herramientas afirman aumentar la velocidad de descarga al abrir dos o más conexiones simultáneas, cada una de las cuales solicita un rango diferente de archivos.
La idea de rangos multiparte no abre múltiples conexiones, pero permite que el software cliente solicite los primeros diez y los últimos diez bytes de un archivo en un único ciclo de solicitud/respuesta.
Para ser honesto, nunca encontré un software que utilice esta función. Pero me niego a escribir "no es totalmente compatible con HTTP" en la declaración del código. Omitir esta característica definitivamente violará la Ley de Murphy. De todos modos, los rangos de varias partes se utilizan en las transmisiones de correo electrónico para separar la información del encabezado, el texto sin formato y los archivos adjuntos.
Código de muestra
Sabemos cómo el cliente y el servidor intercambian información de encabezado para garantizar descargas reanudables. Combinando este conocimiento con la idea de transmisión de bloques de archivos, puede agregar capacidades confiables de administración de descargas a sus aplicaciones ASP.NET.
La forma de controlar el proceso de descarga es interceptar la solicitud de descarga del cliente, leer la información del encabezado y responder adecuadamente. Antes de .NET, tenías que escribir una aplicación ISAPI (API de servidor de Internet) para implementar esta funcionalidad, pero el componente .NET Framework proporciona una interfaz IHttpHandler que, cuando se implementa en una clase, te permite hacer esto usando solo código .NET. y procesar solicitudes. Esto significa que su aplicación tiene control total y capacidad de respuesta sobre el proceso de descarga y nunca involucra ni utiliza funciones automatizadas de IIS.
El código de muestra incluye una clase HttpHandler personalizada (ZIPHandler) en el archivo HttpHandler.vb. ZipHandler implementa la interfaz IhttpHandler y maneja solicitudes para todos los archivos .zip.
Para probar el código de muestra, necesita crear un nuevo directorio virtual en IIS y copiar los archivos fuente allí. Cree un archivo llamado download.zip en este directorio (tenga en cuenta que IIS y ASP.NET no pueden manejar descargas de más de 2 GB, así que asegúrese de que su archivo no exceda este límite). Configure su directorio virtual IIS para asignar la extensión .zip a través de aspnet_isapi.dll.
Clase HttpHandler: después de que ZIPHandler
asigna la extensión .zip en ASP.NET, cada vez que el cliente solicita un archivo .zip del servidor, IIS llama al método ProcessRequest de la clase ZipHandler (consulte el código de descarga).
El método ProcessRequest primero crea una instancia de la clase FileInformation personalizada (ver código de descarga), que encapsula el estado de la descarga (como en progreso, interrumpida, etc.). El ejemplo codifica la ruta al archivo de muestra download.zip en el código. Si aplica este código a su propia aplicación, deberá modificarlo para abrir el archivo solicitado.
' Utilice objRequest para detectar qué archivo se solicitó y utilice el archivo para abrir objFile.
' Por ejemplo objFile = New Download.FileInformation(<nombre de archivo completo>)
objFile = Nueva descarga.FileInformation( _
objContext.Server.MapPath("~/descargar.zip"))
A continuación, el programa ejecuta la solicitud utilizando los encabezados HTTP descritos (si los encabezados se proporcionaron en la solicitud por primera vez bajo el sol). Si falla una verificación de validación, la respuesta finaliza inmediatamente y se envía el valor de StatusCode apropiado.
Si no es objRequest.HttpMethod.Equals(HTTP_METHOD_GET) o no
objRequest.HttpMethod.Equals(HTTP_METHOD_HEAD) Entonces
' Actualmente sólo se admiten los métodos GET y HEAD objResponse.StatusCode = 501 ' No ejecutado
De lo contrario, si no es objFile.Exists, entonces
' No se pudo encontrar el archivo solicitado objResponse.StatusCode = 404 ' No encontrado
De lo contrario, si objFile.Length > Int32.MaxValue entonces
'El archivo es demasiado grande objResponse.StatusCode = 413 'La entidad de solicitud es demasiado grande
ElseIf Not ParseRequestHeaderRange(objRequest, alRequestedRangesBegin, alRequestedRangesend, _
objFile.Length, bIsRangeRequest) Luego
' La solicitud de rango contiene entidades inútiles objResponse.StatusCode = 400 ' Solicitud inútil
De lo contrario, si no, compruebe si se modifica desde (objRequest, objFile) entonces
'La entidad no ha sido modificada objResponse.StatusCode = 304 'La entidad no ha sido modificada
ElseIf Not CheckIfUnmodifiedSince(objRequest,objFile) Entonces
' La entidad ha sido modificada desde la última fecha solicitada objResponse.StatusCode = 412 ' Falló el preprocesamiento
De lo contrario, no es CheckIfMatch(objRequest, objFile) Entonces
' La entidad no coincide con la solicitud objResponse.StatusCode = 412 ' Falló el preprocesamiento
De lo contrario, CheckIfNoneMatch(objRequest, objResponse,objFile) Entonces
' La entidad coincide con la solicitud de no coincidencia.
'El código de respuesta se encuentra en la función CheckIfNoneMatch
Demás
'Control preliminar exitoso
La función ParseRequestHeaderRange en estas comprobaciones preliminares (ver código de descarga) verifica si el cliente solicitó un rango de archivos (lo que significa una descarga parcial). Si el rango solicitado no es válido (un rango no válido es un valor de rango que excede el tamaño del archivo o contiene un número no razonable), este método establece bIsRangeRequest en True. Si se solicita un rango, el método CheckIfRange verifica la información del encabezado IfRange.
Si el rango solicitado es válido, el código calcula el tamaño del mensaje de respuesta. Si el cliente solicitó varios rangos, el valor del tamaño de la respuesta incluirá el valor de longitud del encabezado de varias partes.
Si no se puede determinar un valor de encabezado enviado, el programa manejará la solicitud de descarga como una solicitud inicial en lugar de una descarga parcial, enviando una nueva secuencia de descarga comenzando desde la parte superior del archivo.
Si bIsRangeRequest y también CheckIfRange (objRequest, objFile) entonces
'Esta es una solicitud de rango' Si la matriz Range contiene varias entidades, también es una solicitud de rango multiparte bMultipart = CBool(alRequestedRangesBegin.GetUpperBound(0)>0)
' Vaya a cada rango para obtener la longitud completa de la respuesta For iLoop = alRequestedRangesBegin.GetLowerBound(0) To alRequestedRangesBegin.GetUpperBound(0)
'La longitud del contenido (en este rango)
iResponseContentLength += Convert.ToInt32(alRequestedRangesend( _
iLoop) - alRequestedRangesBegin(iLoop)) + 1
Si bmultiparte entonces
' Si se trata de una solicitud de rango de varias partes, calcula la longitud de la información del encabezado intermedio que se enviará iResponseContentLength += MULTIPART_BOUNDARY.Length
iResponseContentLength += objFile.ContentType.Length
iResponseContentLength += alRequestedRangesBegin(iLoop).ToString.Length
iResponseContentLength += alRequestedRangesend(iLoop).ToString.Length
iResponseContentLength += objFile.Length.ToString.Length
' 49 es la longitud de los saltos de línea y otros caracteres necesarios en descargas de varias partes iResponseContentLength += 49
Terminar si
Siguiente iLoop
Si bMultipart Entonces
' Si se trata de una solicitud de rango de varias partes,
' También debemos calcular la longitud del último encabezado intermedio que se enviará iResponseContentLength +=MULTIPART_BOUNDARY.Length
' 8 es la longitud del guión y la nueva línea iResponseContentLength += 8
Demás
' No es una descarga de varias partes, por lo que debemos especificar el rango de respuesta del encabezado HTTP inicial objResponse.AppendHeader( HTTP_HEADER_CONTENT_RANGE, "bytes" & _
alRequestedRangesBegin(0).ToString & "-" & _
alRequestedRangesend(0).ToString & "/" & _
objFile.Length.ToString)
'Finalizar si
' Rango de respuesta objResponse.StatusCode = 206 ' Respuesta parcial De lo contrario
'Esta no es una solicitud de alcance, o el ID de entidad de alcance solicitado no coincide con el ID de entidad actual,
'Iniciar una nueva descarga' indica que el tamaño de la parte completa del archivo es igual a la longitud del contenido iResponseContentLength =Convert.ToInt32(objFile.Length)
'Regresar al estado normal OK objResponse.StatusCode = 200
Terminar si
' A continuación, el servidor debe enviar varios encabezados de respuesta importantes, como la longitud del contenido, la etiqueta Etag y el tipo de contenido del archivo:
'Escribe la longitud del contenido en la respuesta objResponse.AppendHeader( HTTP_HEADER_CONTENT_LENGTH,iResponseContentLength.ToString)
'Escribe la fecha de la última modificación en la respuesta objResponse.AppendHeader( HTTP_HEADER_LAST_MODIFIED,objFile.LastWriteTimeUTC.ToString("r"))
'Dígale al software cliente que aceptamos la solicitud de rango objResponse.AppendHeader( HTTP_HEADER_ACCEPT_RANGES,HTTP_HEADER_ACCEPT_RANGES_BYTES)
' Escribe la etiqueta de entidad del archivo en la respuesta (entre comillas)
objResponse.AppendHeader(HTTP_HEADER_ENTITY_TAG, """" y objFile.EntityTag y """")
'Escribe el tipo de contenido en respuestaSi bMultipart Entonces
'Los mensajes de varias partes tienen este tipo especial' En el ejemplo, el tipo mime real del archivo se escribe en la respuesta más adelante objResponse.ContentType = MULTIPART_CONTENTTYPE
Demás
'El tipo de contenido del archivo propiedad de un único mensaje parcial objResponse.ContentType = objFile.ContentType
Terminar si
Todo lo que necesitas para descargar está listo y puedes comenzar a descargar archivos. Utilizará un objeto FileStream para leer fragmentos de bytes de un archivo. Establezca la propiedad State de la instancia de FileInformation objFile en fsDownloadInProgress. Mientras el cliente permanezca conectado, el servidor lee fragmentos de bytes del archivo y los envía al cliente. Para descargas de varias partes, este código envía información de encabezado específica. Si el cliente se desconecta, el servidor establece el estado del archivo en fsDownloadBroken. Si el servidor completa el envío del rango solicitado, establece el estado en fsDownloadFinished (ver código de descarga).
Clase auxiliar FileInformation
En la sección ZIPHandler encontrará que FileInformation es una clase auxiliar que encapsula información del estado de la descarga (como descarga, interrupción, etc.).
Para crear una instancia de FileInformation, debe pasar la ruta al archivo solicitado al constructor de la clase:
Public Sub New(ByVal sPath As String)
m_objFile = Nuevo System.IO.FileInfo(sPath)
End Sub
FileInformation utiliza el objeto System.IO.FileInfo para obtener información del archivo, que se expone como propiedades del objeto (como si el archivo existe, el nombre completo del archivo, el tamaño, etc.). Esta clase también expone una enumeración DownloadState, que describe los distintos estados de la solicitud de descarga:
Enum DownloadState
' Borrar: No hay proceso de descarga, el archivo puede estar manteniendo fsClear = 1
'Bloqueado: los archivos creados dinámicamente no se pueden cambiar fsLocked = 2
'En progreso: el archivo está bloqueado y el proceso de descarga está en progreso fsDownloadInProgress = 6
'Roto: El archivo está bloqueado, el proceso de descarga estaba en progreso, pero se canceló fsDownloadBroken = 10
' Finalizado: El archivo está bloqueado y el proceso de descarga se completa fsDownloadFinished = 18
End Enum
FileInformation también proporciona el valor del atributo EntityTag. Este valor está codificado en el código de ejemplo porque el código de ejemplo solo usa un archivo de descarga y ese archivo no se cambiará, pero para una aplicación real, proporcionará varios archivos, incluso de forma dinámica. Para crear archivos, su código debe proporcionar un valor EntityTag único para cada archivo. Además, este valor debe cambiar cada vez que se cambia o modifica el archivo. Esto permite que el software del cliente verifique que los fragmentos de bytes que han descargado aún estén actualizados. Aquí está la parte del código de muestra que devuelve el valor codificado de EntityTag:
Public ReadOnly Property EntityTag() As String
' EntityTag se utiliza para la respuesta inicial (200) al cliente y para las solicitudes de recuperación del cliente. Obtener
' Crea una cadena única para el archivo.
' Tenga en cuenta que mientras el archivo no cambie, se debe conservar el código único.
' Sin embargo, si el archivo cambia o se modifica, este código debe cambiar.
Devuelve "MyExampleFileID"
Fin de obtención
Propiedad final
Una EntityTag simple y generalmente bastante segura podría consistir en el nombre del archivo y la fecha en que se modificó por última vez. Independientemente del método que utilice, debe asegurarse de que este valor sea verdaderamente único y no pueda confundirse con el EntityTag de otros archivos. Me gustaría nombrar dinámicamente los archivos creados en mi aplicación por cliente, cliente e índice de código postal, y almacenar el GUID utilizado como EntityTag en la base de datos.
La clase ZipFileHandler lee y establece la propiedad pública State. Después de completar la descarga, establece el estado en fsDownloadFinished. En este momento puedes eliminar los archivos temporales. Aquí generalmente es necesario llamar al método Save para mantener el estado.
Estado de propiedad pública () como estado de descarga
Conseguir
Devolver m_nState
Fin de obtención
Establecer (ByVal nState como estado de descarga)
m_nEstado = nEstado
' Acción opcional: Puede eliminar el archivo automáticamente en este momento.
' Si el estado se establece en Finalizado, ya no necesita este archivo.
' Si nState =DownloadState.fsDownloadFinished Entonces
'Claro()
'Demás
'Ahorrar()
'Finalizar si
Ahorrar()
Conjunto final
Propiedad final
ZipFileHandler debe llamar al método Save cada vez que cambie el estado del archivo para guardar el estado del archivo y poder mostrarlo al usuario más tarde. También puede usarlo para guardar el EntityTag que creó usted mismo. No guarde el estado del archivo ni el valor de EntityTag en la aplicación, la sesión o la caché; debe guardar la información durante todo el ciclo de vida de todos estos objetos.
SubGuardarPrivado()
'Guarde el estado de descarga del archivo en la base de datos o en el archivo XML.
' Por supuesto, si no crea el archivo dinámicamente, no necesita guardar este estado.
End Sub
Como se mencionó anteriormente, el código de ejemplo solo maneja un archivo existente (download.zip), pero puede mejorar aún más este programa para crear el archivo solicitado según sea necesario.
Al probar el código de muestra, su sistema local o LAN puede ser demasiado rápido para interrumpir el proceso de descarga, por lo que le recomiendo que utilice una conexión LAN lenta (reducir el ancho de banda del sitio en IIS es un método de simulación) o coloque el servidor en La Internet.
Descargar archivos en el cliente sigue siendo complicado. Un servidor de caché web incorrecto o mal configurado operado por un ISP puede provocar que fallen las descargas de archivos de gran tamaño, incluido un rendimiento deficiente de la descarga o la finalización anticipada de la sesión. Si el tamaño del archivo supera los 255 MB, debe alentar a los clientes a utilizar software de administración de descargas de terceros, aunque algunos navegadores recientes tienen administradores de descargas básicos integrados.
Si desea ampliar aún más el código de ejemplo, puede resultar útil consultar la especificación HTTP. Puede establecer sumas de verificación MD5 para descargas, agregándolas usando el encabezado Content-MD5 para proporcionar una forma de verificar la integridad de los archivos descargados. El código de muestra no implica otros métodos HTTP excepto GET y HEAD.