Text/Tao Gang
Lidar com downloads de arquivos grandes em aplicativos da web sempre foi notoriamente difícil, portanto, para a maioria dos sites, a desgraça recai sobre o usuário se o download for interrompido. Mas não precisamos fazer isso agora, porque você pode tornar seu aplicativo ASP.NET capaz de suportar downloads recuperáveis (contínuos) de arquivos grandes. Usando o método fornecido neste artigo, você pode acompanhar o processo de download, para poder lidar com arquivos criados dinamicamente - e fazer isso sem a necessidade de bibliotecas de link dinâmico ISAPI tradicionais e código C++ não gerenciado.
É mais fácil fornecer um serviço para os clientes baixarem arquivos da Internet, certo? Basta copiar o arquivo para download no diretório do seu aplicativo web, publicar o link e deixar o IIS fazer todo o trabalho relacionado. No entanto, o serviço de arquivos não deve ser mais do que uma dor de cabeça, você não quer que o mundo inteiro tenha acesso aos seus dados, você não quer que seu servidor fique entupido com centenas de arquivos estáticos, você não ' Não quero nem baixar arquivos temporários - Esses arquivos só são criados durante o tempo ocioso depois que o cliente inicia o download.
Infelizmente, não é possível obter esses efeitos usando a resposta padrão do IIS para solicitações de download. Portanto, em geral, para obter controle sobre o processo de download, os desenvolvedores precisam vincular a uma página .aspx personalizada onde verificam as credenciais do usuário, criam um arquivo para download e usam o seguinte código para enviar o arquivo ao cliente:
Response.WriteFile
Response.End()
E é aqui que surge o verdadeiro problema.
Qual é o problema?
O método WriteFile parece perfeito, faz com que os dados binários do arquivo fluam para o cliente. Mas o que não sabíamos até recentemente é que o método WriteFile consome muita memória, carregando o arquivo inteiro na RAM do servidor para servir (na verdade, ele ocupa o dobro do tamanho do arquivo). Para arquivos grandes, isso pode causar problemas de memória de serviço e possivelmente duplicar processos ASP.NET. Mas em junho de 2004, a Microsoft lançou um patch que resolveu o problema. Este patch agora faz parte do .NET Framework 1.1 Service Pack (SP1).
Este patch introduz o método TransmitFile, que lê um arquivo de disco em um buffer de memória menor e então começa a transferir o arquivo. Embora esta solução resolva os problemas de memória e loop, ainda é insatisfatória. Você não tem controle sobre o ciclo de vida da resposta. Você não tem como saber se o download foi concluído corretamente, não tem como saber se o download foi interrompido e (se você criou arquivos temporários) não tem como saber se e quando deve excluir os arquivos. Para piorar a situação, se o download falhar, o método TransmitFile inicia o download a partir do início do arquivo que o cliente tentará em seguida.
Uma solução possível, a implementação de um serviço de transferência inteligente em segundo plano (BITS), não é viável para a maioria dos sites porque iria contra o propósito de manter a independência do navegador do cliente e do sistema operacional.
A base para uma solução satisfatória vem da primeira tentativa da Microsoft de resolver o problema de confusão de memória causado pelo WriteFile (consulte o artigo 812406 da Base de Conhecimento). Esse artigo demonstrou um processo inteligente de download de dados em blocos que lê dados de um fluxo de arquivos. Antes de o servidor enviar o bloco de bytes ao cliente, ele usa a propriedade Response.IsClientConnected para verificar se o cliente ainda está conectado. Se a conexão ainda estiver aberta, ela continua enviando bytes de fluxo, caso contrário, ela para para evitar que o servidor envie dados desnecessários.
Esta é a abordagem que adotamos, especialmente ao baixar arquivos temporários. Caso IsClientConnected retorne False, você sabe que o processo de download foi interrompido e deve salvar o arquivo, caso contrário, quando o processo for concluído com sucesso, você exclui o arquivo temporário; Além disso, para retomar um download interrompido, basta iniciar o download a partir do ponto onde a conexão do cliente falhou durante a última tentativa de download.
Suporte ao protocolo HTTP e informações de cabeçalho (cabeçalho)
O suporte ao protocolo HTTP pode ser usado para manipular informações de cabeçalho para downloads interrompidos. Usando um pequeno número de cabeçalhos HTTP, você pode aprimorar seu processo de download para cumprir totalmente a especificação do protocolo HTTP. Esta especificação, juntamente com os intervalos, fornece todas as informações necessárias para retomar um download interrompido.
Veja como funciona. Primeiro, se o servidor suportar downloads recuperáveis do lado do cliente, ele enviará o cabeçalho Accept-Ranges na resposta inicial. O servidor também envia um cabeçalho de tag de entidade (ETag), que contém uma string de identificação exclusiva.
O código abaixo mostra alguns dos cabeçalhos que o IIS envia ao cliente em resposta a uma solicitação inicial de download, que passa os detalhes do arquivo solicitado ao cliente.
HTTP/1.1 200 OK
Conexão: fechar
Data: terça-feira, 19 de outubro de 2004, 15:11:23 GMT
Intervalos de aceitação: bytes
Última modificação: Dom, 26 de setembro de 2004 15:52:45 GMT
ETag: "47febb2cfd76c41:2062"
Controle de cache: privado
Tipo de conteúdo: aplicativo/x-zip compactado
Content-Length: 2844011
Depois de receber essas informações de cabeçalho, se o download for interrompido, o navegador IE enviará o valor Etag e as informações do cabeçalho Range de volta ao servidor em solicitações de download subsequentes. O código abaixo mostra alguns dos cabeçalhos que o IE envia ao servidor ao tentar retomar um download interrompido.
GET
Esses cabeçalhos indicam que o IE armazenou em cache a tag de entidade fornecida pelo IIS e a enviou de volta ao servidor no cabeçalho If-Range. Essa é uma forma de garantir que o download seja retomado exatamente do mesmo arquivo. Infelizmente, nem todos os navegadores funcionam da mesma maneira. Outros cabeçalhos HTTP enviados pelo cliente para verificar o arquivo podem ser If-Match, If-Unmodified-Since ou Except-Modified-Since. Obviamente, a especificação não é explícita sobre quais cabeçalhos o software cliente deve suportar ou quais cabeçalhos devem ser usados. Portanto, alguns clientes não usam informações de cabeçalho, enquanto o IE usa apenas If-Range e Except-Modified-Since. É melhor você verificar essas informações com código. Ao adotar essa abordagem, seu aplicativo pode estar em conformidade com a especificação HTTP em um nível muito alto e funcionar com diversos navegadores. O cabeçalho Range especifica o intervalo de bytes solicitado - neste caso, é o ponto inicial a partir do qual o servidor deve retomar o fluxo de arquivos.
Quando o IIS recebe uma solicitação do tipo currículo de download, ele envia de volta uma resposta contendo as seguintes informações de cabeçalho:
HTTP/1.1 206 Partial Content
Intervalo de conteúdo: bytes 822603-2844010/2844011
Intervalos de aceitação: bytes
Última modificação: Dom, 26 de setembro de 2004 15:52:45 GMT
ETag: "47febb2cfd76c41:2062"
Controle de cache: privado
Tipo de conteúdo: aplicativo/x-zip compactado
Comprimento do conteúdo: 2021408
Observe que o código acima tem uma resposta HTTP ligeiramente diferente da solicitação de download original - a solicitação para retomar o download é 206, enquanto a solicitação de download original era 200. Isso indica que o que está sendo transmitido pela rede é um arquivo parcial. Desta vez, o cabeçalho Content-Range indica o número exato e a localização dos bytes que estão sendo passados.
O IE é muito exigente com essas informações de cabeçalho. Se a resposta inicial não contiver as informações do cabeçalho Etag, o IE nunca tentará retomar o download. Outros clientes que testei não usam o cabeçalho ETag, eles simplesmente confiam no nome do arquivo, solicitam o escopo e usam o cabeçalho Last-Modified se estiverem tentando validar o arquivo.
Uma compreensão profunda do protocolo HTTP
As informações do cabeçalho mostradas na seção anterior são suficientes para fazer a solução para retomar downloads funcionar, mas não cobrem completamente a especificação HTTP.
O cabeçalho Range pode solicitar vários intervalos em uma única solicitação, um recurso chamado "intervalos multipartes". Não deve ser confundido com download segmentado, quase todas as ferramentas de download usam download segmentado para aumentar a velocidade de download. Essas ferramentas pretendem aumentar a velocidade de download abrindo duas ou mais conexões simultâneas, cada uma solicitando um intervalo diferente de arquivos.
A ideia de intervalos multipartes não abre múltiplas conexões, mas permite que o software cliente solicite os primeiros dez e os últimos dez bytes de um arquivo em um único ciclo de solicitação/resposta.
Para ser sincero, nunca encontrei um software que usasse esse recurso. Mas me recuso a escrever "não é totalmente compatível com HTTP" na declaração do código. A omissão deste recurso violará definitivamente a Lei de Murphy. Independentemente disso, intervalos multipartes são usados em transmissões de e-mail para separar informações de cabeçalho, texto simples e anexos.
Código de amostra
Sabemos como o cliente e o servidor trocam informações de cabeçalho para garantir downloads recuperáveis. Combinando esse conhecimento com a ideia de streaming de bloco de arquivos, você pode adicionar recursos confiáveis de gerenciamento de download aos seus aplicativos ASP.NET.
A maneira de obter controle do processo de download é interceptar a solicitação de download do cliente, ler as informações do cabeçalho e responder adequadamente. Antes do .NET, você tinha que escrever um aplicativo ISAPI (Internet Server API) para implementar essa funcionalidade, mas o componente .NET Framework fornece uma interface IHttpHandler que, quando implementada em uma classe, permite fazer isso usando apenas o código .NET Intercept. e processar solicitações. Isso significa que seu aplicativo tem total controle e capacidade de resposta sobre o processo de download e nunca envolve ou usa funções automatizadas do IIS.
O código de amostra inclui uma classe HttpHandler personalizada (ZIPHandler) no arquivo HttpHandler.vb. ZipHandler implementa a interface IhttpHandler e lida com solicitações para todos os arquivos .zip.
Para testar o código de exemplo, você precisa criar um novo diretório virtual no IIS e copiar os arquivos de origem lá. Crie um arquivo chamado download.zip neste diretório (observe que o IIS e o ASP.NET não podem lidar com downloads maiores que 2 GB, portanto, certifique-se de que seu arquivo não exceda esse limite). Configure seu diretório virtual do IIS para mapear a extensão .zip por meio de aspnet_isapi.dll.
Classe HttpHandler: Depois que o ZIPHandler
mapeia a extensão .zip no ASP.NET, toda vez que o cliente solicita um arquivo .zip do servidor, o IIS chama o método ProcessRequest da classe ZipHandler (consulte o código de download).
O método ProcessRequest primeiro cria uma instância da classe FileInformation personalizada (consulte o código de download), que encapsula o status do download (como em andamento, interrompido, etc.). O exemplo codifica o caminho para o arquivo de amostra download.zip no código. Se você aplicar esse código ao seu próprio aplicativo, precisará modificá-lo para abrir o arquivo solicitado.
' Use objRequest para detectar qual arquivo foi solicitado e use o arquivo para abrir objFile.
' Por exemplo objFile = New Download.FileInformation(<nome completo do arquivo>)
objFile = Novo Download.FileInformation( _
objContext.Server.MapPath("~/download.zip"))
Em seguida, o programa executa a solicitação usando os cabeçalhos HTTP descritos (se os cabeçalhos foram fornecidos na solicitação, orei pela primeira vez). Se uma verificação de validação falhar, a resposta será encerrada imediatamente e o valor StatusCode apropriado será enviado.
Se não objRequest.HttpMethod.Equals(HTTP_METHOD_GET) ou não
objRequest.HttpMethod.Equals(HTTP_METHOD_HEAD) Então
' Atualmente apenas os métodos GET e HEAD são suportados objResponse.StatusCode = 501 ' Não executado
ElseIf Não objFile.Exists Então
' O arquivo solicitado não foi encontrado objResponse.StatusCode = 404 ' Não encontrado
ElseIf objFile.Length > Int32.MaxValue Então
'O arquivo é muito grande objResponse.StatusCode = 413 'A entidade solicitada é muito grande
ElseIf Not ParseRequestHeaderRange(objRequest, alRequestedRangesBegin, alRequestedRangesend, _
objFile.Length, bIsRangeRequest) Então
' A solicitação Range contém entidades inúteis objResponse.StatusCode = 400 ' Solicitação inútil
ElseIf Not CheckIfModifiedSince(objRequest,objFile) Then
'A entidade não foi modificada objResponse.StatusCode = 304 'A entidade não foi modificada
ElseIf Not CheckIfUnmodifiedSince(objRequest,objFile) Then
' A entidade foi modificada desde a última data solicitada objResponse.StatusCode = 412 ' Falha no pré-processamento
ElseIf Not CheckIfMatch(objRequest, objFile) Então
' A entidade não corresponde à solicitação objResponse.StatusCode = 412 ' Falha no pré-processamento
ElseIf Not CheckIfNoneMatch(objRequest, objResponse,objFile) Então
'A entidade corresponde à solicitação sem correspondência.
'O código de resposta está localizado na função CheckIfNoneMatch
Outro
'Verificação preliminar bem sucedida
A função ParseRequestHeaderRange nessas verificações preliminares (ver código de download) verifica se o cliente solicitou um intervalo de arquivos (o que significa um download parcial). Se o intervalo solicitado for inválido (um intervalo inválido é um valor de intervalo que excede o tamanho do arquivo ou contém um número não razoável), esse método define bIsRangeRequest como True. Se um intervalo for solicitado, o método CheckIfRange verifica as informações do cabeçalho IfRange.
Se o intervalo solicitado for válido, o código calcula o tamanho da mensagem de resposta. Se o cliente solicitou vários intervalos, o valor do tamanho da resposta incluirá o valor do comprimento do cabeçalho multipart.
Se um valor de cabeçalho enviado não puder ser determinado, o programa tratará a solicitação de download como uma solicitação inicial em vez de um download parcial, enviando um novo fluxo de download começando no topo do arquivo.
Se bIsRangeRequest AndAlso CheckIfRange(objRequest, objFile) Então
'Esta é uma solicitação de intervalo' Se a matriz Range contiver várias entidades, também será uma solicitação de intervalo multipartes bMultipart = CBool(alRequestedRangesBegin.GetUpperBound(0)>0)
' Vá para cada intervalo para obter o comprimento total da resposta For iLoop = alRequestedRangesBegin.GetLowerBound(0) To alRequestedRangesBegin.GetUpperBound(0)
'A duração do conteúdo (neste intervalo)
iResponseContentLength += Convert.ToInt32(alRequestedRangesend( _
iLoop) -alRequestedRangesBegin(iLoop)) + 1
Se bMultipart então
' Se for uma solicitação de intervalo de várias partes, calcule o comprimento das informações do cabeçalho intermediário a serem enviadas iResponseContentLength += MULTIPART_BOUNDARY.Length
iResponseContentLength += objFile.ContentType.Length
iResponseContentLength += alRequestedRangesBegin(iLoop).ToString.Length
iResponseContentLength += alRequestedRangesend(iLoop).ToString.Length
iResponseContentLength += objFile.Length.ToString.Length
'49 é o comprimento das quebras de linha e outros caracteres necessários em downloads de várias partes iResponseContentLength += 49
Terminar se
Próximo iLoop
se bMultipart então
'Se for uma solicitação de intervalo com várias partes,
' Devemos também calcular o comprimento do último cabeçalho intermediário que será enviado iResponseContentLength +=MULTIPART_BOUNDARY.Length
'8 é o comprimento do traço e da nova linha iResponseContentLength += 8
Outro
' Não é um download de várias partes, portanto, devemos especificar o intervalo de resposta do cabeçalho HTTP inicial objResponse.AppendHeader( HTTP_HEADER_CONTENT_RANGE, "bytes" & _
alRequestedRangesBegin(0).ToString & "-" & _
alRequestedRangesend(0).ToString & "/" & _
objFile.Length.ToString)
'Fim se
' Faixa de resposta objResponse.StatusCode = 206 ' Resposta parcial Else
'Esta não é uma solicitação de escopo ou o ID da entidade de escopo solicitado não corresponde ao ID da entidade atual,
'Então inicie um novo download' indica que o tamanho da parte concluída do arquivo é igual ao comprimento do conteúdo iResponseContentLength =Convert.ToInt32(objFile.Length)
'Retornar ao status normal OK objResponse.StatusCode = 200
Terminar se
' Em seguida, o servidor deve enviar vários cabeçalhos de resposta importantes, como comprimento do conteúdo, Etag e tipo de conteúdo do arquivo:
' Escreva o comprimento do conteúdo na resposta objResponse.AppendHeader( HTTP_HEADER_CONTENT_LENGTH,iResponseContentLength.ToString)
' Escreva a data da última modificação na resposta objResponse.AppendHeader( HTTP_HEADER_LAST_MODIFIED,objFile.LastWriteTimeUTC.ToString("r"))
'Informa ao software cliente que aceitamos a solicitação de intervalo objResponse.AppendHeader( HTTP_HEADER_ACCEPT_RANGES,HTTP_HEADER_ACCEPT_RANGES_BYTES)
'Escreve a tag de entidade do arquivo na resposta (entre aspas)
objResponse.AppendHeader(HTTP_HEADER_ENTITY_TAG, """" & objFile.EntityTag & """")
'Escreve o tipo de conteúdo em responseIf bMultipart Then
'Mensagens multipartes têm este tipo especial' No exemplo, o tipo MIME real do arquivo é gravado na resposta posteriormente objResponse.ContentType = MULTIPART_CONTENTTYPE
Outro
'O tipo de conteúdo do arquivo pertencente a uma única mensagem parcial objResponse.ContentType = objFile.ContentType
Terminar se
Tudo que você precisa para baixar está pronto e você pode começar a baixar os arquivos. Você usará um objeto FileStream para ler pedaços de bytes de um arquivo. Configure a propriedade State da instância FileInformation objFile como fsDownloadInProgress. Enquanto o cliente permanecer conectado, o servidor lê pedaços de bytes do arquivo e os envia ao cliente. Para downloads multiparte, esse código envia informações de cabeçalho específicas. Se o cliente for desconectado, o servidor definirá o status do arquivo como fsDownloadBroken. Se o servidor concluir o envio do intervalo solicitado, ele definirá o status como fsDownloadFinished (consulte o código de download).
Classe Auxiliar FileInformation
Na seção ZIPHandler você descobrirá que FileInformation é uma classe auxiliar que encapsula informações de status de download (como download, interrompido, etc.).
Para criar uma instância de FileInformation, você precisa passar o caminho do arquivo solicitado para o construtor da classe:
Public Sub New(ByVal sPath As String)
m_objFile = Novo System.IO.FileInfo(sPath)
End Sub
FileInformation usa o objeto System.IO.FileInfo para obter informações do arquivo, que são expostas como propriedades do objeto (como se o arquivo existe, o nome completo do arquivo, tamanho, etc.). Esta classe também expõe uma enumeração DownloadState, que descreve os vários estados da solicitação de download:
Enum DownloadState
' Clear: Nenhum processo de download, o arquivo pode estar mantendo fsClear = 1
'Bloqueado: Arquivos criados dinamicamente não podem ser alterados fsLocked = 2
'Em Andamento: O arquivo está bloqueado e o processo de download está em andamento fsDownloadInProgress = 6
'Quebrado: O arquivo está bloqueado, o processo de download estava em andamento, mas foi cancelado fsDownloadBroken = 10
' Concluído: O arquivo está bloqueado e o processo de download foi concluído fsDownloadFinished = 18
End Enum
FileInformation também fornece o valor do atributo EntityTag. Este valor é codificado no código de exemplo porque o código de exemplo usa apenas um arquivo de download e esse arquivo não será alterado, mas para um aplicativo real, você fornecerá vários arquivos, mesmo dinamicamente. Para criar arquivos, seu código deve fornecer um valor EntityTag exclusivo para cada arquivo. Além disso, esse valor deve mudar sempre que o arquivo for alterado ou modificado. Isso permite que o software cliente verifique se os pedaços de bytes baixados ainda estão atualizados. Aqui está a parte do código de exemplo que retorna o valor EntityTag codificado:
Public ReadOnly Property EntityTag() As String
' EntityTag é usado para a resposta inicial (200) ao cliente e para solicitações de recuperação do cliente.
'Cria uma string exclusiva para o arquivo.
'Observe que, desde que o arquivo não seja alterado, o código exclusivo deve ser retido.
' No entanto, se o arquivo for alterado ou modificado, esse código deverá ser alterado.
Retornar "MyExampleFileID"
Fim
Propriedade End
Uma EntityTag simples e geralmente segura pode consistir no nome do arquivo e na data em que o arquivo foi modificado pela última vez. Não importa o método usado, você deve garantir que esse valor seja verdadeiramente único e não possa ser confundido com o EntityTag de outros arquivos. Gostaria de nomear dinamicamente os arquivos criados em minha aplicação por cliente, cliente e índice de CEP, e armazenar o GUID usado como EntityTag no banco de dados.
A classe ZipFileHandler lê e define a propriedade pública State. Após concluir o download, ele define o estado como fsDownloadFinished. Neste momento você pode excluir os arquivos temporários. Aqui você geralmente precisa chamar o método Save para manter o estado.
Estado de propriedade pública() como DownloadState
Pegar
Retornar m_nState
Fim
Definir (ByVal nState como DownloadState)
m_nEstado = nEstado
' Ação opcional: Você pode excluir o arquivo automaticamente neste momento.
' Se o status estiver definido como Concluído, você não precisará mais deste arquivo.
'Se nState =DownloadState.fsDownloadFinished Então
'Claro()
'Outro
'Salvar()
'Fim se
Salvar()
Conjunto final
End Property
ZipFileHandler deve chamar o método Save sempre que o status do arquivo for alterado para salvar o status do arquivo para que ele possa ser exibido ao usuário posteriormente. Você também pode usá-lo para salvar a EntityTag que você mesmo criou. Não salve o estado do arquivo e o valor EntityTag no Aplicativo, Sessão ou Cache - você deve salvar as informações ao longo do ciclo de vida de todos esses objetos.
PrivadoSubSave()
'Salve o status de download do arquivo no banco de dados ou arquivo XML.
' Claro, se você não criar o arquivo dinamicamente, você não precisa salvar este estado.
End Sub
Como mencionado anteriormente, o código de exemplo trata apenas de um arquivo existente (download.zip), mas você pode aprimorar ainda mais este programa para criar o arquivo solicitado conforme necessário.
Ao testar o código de exemplo, seu sistema local ou LAN pode ser muito rápido para interromper o processo de download, então recomendo que você use uma conexão LAN lenta (reduzir a largura de banda do site no IIS é um método de simulação) ou colocar o servidor em a Internet.
Baixar arquivos no cliente ainda é uma luta. Um servidor de cache da web incorreto ou mal configurado operado por um ISP pode causar falhas em downloads de arquivos grandes, incluindo baixo desempenho de download ou encerramento antecipado da sessão. Se o tamanho do arquivo exceder 255 MB, você deve incentivar os clientes a usar software de gerenciamento de download de terceiros, embora alguns navegadores recentes tenham gerenciadores de download básicos integrados.
Se você deseja estender ainda mais o código de exemplo, pode ser útil consultar a especificação HTTP. Você pode estabelecer somas de verificação MD5 para downloads, adicionando-as usando o cabeçalho Content-MD5 para fornecer uma maneira de verificar a integridade dos arquivos baixados. O código de exemplo não envolve outros métodos HTTP, exceto GET e HEAD.