Como o upload de arquivo HTTP funciona?

528

Quando envio um formulário simples como este com um arquivo anexado:

<form enctype="multipart/form-data" action="http://localhost:3000/upload?upload_progress_id=12344" method="POST">
<input type="hidden" name="MAX_FILE_SIZE" value="100000" />
Choose a file to upload: <input name="uploadedfile" type="file" /><br />
<input type="submit" value="Upload File" />
</form>

Como ele envia o arquivo internamente? O arquivo é enviado como parte do corpo HTTP como dados? Nos cabeçalhos desta solicitação, não vejo nada relacionado ao nome do arquivo.

Gostaria apenas de saber o funcionamento interno do HTTP ao enviar um arquivo.

0xSina
fonte
Eu não uso um sniffer há algum tempo, mas se você quiser ver o que está sendo enviado em sua solicitação (já que é para o servidor, é uma solicitação), cheire-o. Esta questão é muito ampla. SO é mais para questões de programação específicas.
Paparazzo
... como farejadores, violinista é a minha arma de escolha. Você pode até criar suas próprias solicitações de teste para ver como elas são postadas.
Phil Cooper
Para os interessados, ver também " MAX_FILE_SIZEem PHP - que é o ponto" na stackoverflow.com/q/1381364/632951
Pacerier
Acho MAX_FILE_SIZE estranho. como posso modificar meu html no chrome para 100000000 antes de publicá-lo, para que ele publique um valor melhor. 1. coloque-o em um cookie com um hash seguro via salt, para que, se modificado, o servidor possa validar e gerar exceções (como peças da Web ou playframework), ou algum tipo de validação de formulário que as coisas não mudaram. @ 0xSina
Dean Hiller

Respostas:

320

Vamos dar uma olhada no que acontece quando você seleciona um arquivo e envia seu formulário (truncamos os cabeçalhos por questões de brevidade):

POST /upload?upload_progress_id=12344 HTTP/1.1
Host: localhost:3000
Content-Length: 1325
Origin: http://localhost:3000
... other headers ...
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryePkpFF7tjBAqx29L

------WebKitFormBoundaryePkpFF7tjBAqx29L
Content-Disposition: form-data; name="MAX_FILE_SIZE"

100000
------WebKitFormBoundaryePkpFF7tjBAqx29L
Content-Disposition: form-data; name="uploadedfile"; filename="hello.o"
Content-Type: application/x-object

... contents of file goes here ...
------WebKitFormBoundaryePkpFF7tjBAqx29L--

NOTA: cada string de limite deve ser prefixada com um extra --, assim como no final da última string de limite. O exemplo acima já inclui isso, mas pode ser fácil perder. Veja o comentário de @Andreas abaixo.

Em vez de URL que codifica os parâmetros do formulário, os parâmetros do formulário (incluindo os dados do arquivo) são enviados como seções em um documento de várias partes no corpo da solicitação.

No exemplo acima, você pode ver a entrada MAX_FILE_SIZEcom o valor definido no formulário, bem como uma seção que contém os dados do arquivo. O nome do arquivo faz parte do Content-Dispositioncabeçalho.

Os detalhes completos estão aqui .

toddsundsted
fonte
7
@ source.rar: Não. Os servidores da Web são (quase?) sempre encadeados para que possam lidar com conexões simultâneas. Essencialmente, o processo daemon que está escutando na porta 80 imediatamente entrega a tarefa de servir para outro encadeamento / processo para que ele possa voltar a escutar para outra conexão; mesmo se duas conexões de entrada chegarem exatamente no mesmo momento, elas permanecerão no buffer de rede até que o daemon esteja pronto para lê-las.
eggyal
10
A explicação do encadeamento é um pouco incorreta, pois existem servidores de alto desempenho projetados como encadeamento único e usam uma máquina de estado para revezar-se rapidamente no download de pacotes de dados de conexões. Em vez disso, no TCP / IP, a porta 80 é uma porta de escuta, não a porta na qual os dados são transferidos.
slebetman
9
Quando um soquete de escuta IP (porta 80) recebe uma conexão, outro soquete é criado em outra porta, geralmente com um número aleatório acima de 1000. Esse soquete é então conectado ao soquete remoto, deixando a porta 80 livre para ouvir novas conexões.
slebetman
11
@slebetman Primeiro de tudo, isso é sobre HTTP. O modo ativo de FTP não se aplica aqui. Segundo, o soquete de escuta não fica bloqueado em todas as conexões. Você pode ter tantas conexões com uma porta quanto os outros lados tiverem portas às quais vincular suas próprias extremidades.
Slotos
33
Observe que a string de limite que é passada como parte do campo de cabeçalho Content-Type é 2 caracteres menor que as strings de limite das partes individuais abaixo. Passei apenas uma hora tentando descobrir por que meu remetente não funciona, porque é muito difícil perceber que, na verdade, existem apenas 4 traços na primeira string de limite, mas 6 traços nas outras strings de limite. Em outras palavras: Ao usar a string de limite para separar os dados individuais do formulário, ela deve ser prefixada por dois hífens: - É descrito no RFC1867, é claro, mas acho que deve ser apontado aqui também
Andreas
279

Como ele envia o arquivo internamente?

O formato é chamado multipart/form-data, conforme solicitado em: O que significa enctype = 'multipart / form-data'?

Eu vou:

  • adicione mais algumas referências HTML5
  • explicar por que ele está certo com um exemplo de envio de formulário

Referências HTML5

Existem três possibilidades para enctype:

Como gerar os exemplos

Depois de ver um exemplo de cada método, fica óbvio como eles funcionam e quando você deve usar cada um.

Você pode produzir exemplos usando:

Salve o formulário em um .htmlarquivo mínimo :

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8"/>
  <title>upload</title>
</head>
<body>
  <form action="http://localhost:8000" method="post" enctype="multipart/form-data">
  <p><input type="text" name="text1" value="text default">
  <p><input type="text" name="text2" value="a&#x03C9;b">
  <p><input type="file" name="file1">
  <p><input type="file" name="file2">
  <p><input type="file" name="file3">
  <p><button type="submit">Submit</button>
</form>
</body>
</html>

Definimos o valor de texto padrão como a&#x03C9;b, o que significa aωbporque ωé U+03C9, quais são os bytes 61 CF 89 62em UTF-8.

Crie arquivos para upload:

echo 'Content of a.txt.' > a.txt

echo '<!DOCTYPE html><title>Content of a.html.</title>' > a.html

# Binary file containing 4 bytes: 'a', 1, 2 and 'b'.
printf 'a\xCF\x89b' > binary

Execute nosso pequeno servidor de eco:

while true; do printf '' | nc -l 8000 localhost; done

Abra o HTML no seu navegador, selecione os arquivos, clique em enviar e verifique o terminal.

nc imprime a solicitação recebida.

Testado em: Ubuntu 14.04.3, ncBSD 1.105, Firefox 40.

multipart / form-data

Firefox enviado:

POST / HTTP/1.1
[[ Less interesting headers ... ]]
Content-Type: multipart/form-data; boundary=---------------------------735323031399963166993862150
Content-Length: 834

-----------------------------735323031399963166993862150
Content-Disposition: form-data; name="text1"

text default
-----------------------------735323031399963166993862150
Content-Disposition: form-data; name="text2"

aωb
-----------------------------735323031399963166993862150
Content-Disposition: form-data; name="file1"; filename="a.txt"
Content-Type: text/plain

Content of a.txt.

-----------------------------735323031399963166993862150
Content-Disposition: form-data; name="file2"; filename="a.html"
Content-Type: text/html

<!DOCTYPE html><title>Content of a.html.</title>

-----------------------------735323031399963166993862150
Content-Disposition: form-data; name="file3"; filename="binary"
Content-Type: application/octet-stream

aωb
-----------------------------735323031399963166993862150--

Para o arquivo binário e o campo de texto, os bytes 61 CF 89 62( aωbem UTF-8) são enviados literalmente. Você pode verificar isso com nc -l localhost 8000 | hd, que diz que os bytes:

61 CF 89 62

foram enviados ( 61== 'a' e 62== 'b').

Portanto, é claro que:

  • Content-Type: multipart/form-data; boundary=---------------------------735323031399963166993862150define o tipo de conteúdo como multipart/form-datae diz que os campos são separados pela boundarysequência especificada .

    Mas observe que o:

    boundary=---------------------------735323031399963166993862150
    

    tem dois paizinhos a menos do --que a barreira real

    -----------------------------735323031399963166993862150
    

    Isso ocorre porque o padrão requer que o limite comece com dois traços --. Os outros traços parecem ser exatamente como o Firefox escolheu implementar o limite arbitrário. A RFC 7578 menciona claramente que esses dois traços principais --são necessários:

    4.1 Parâmetro "Limite" de dados de várias partes / formulário

    Como com outros tipos de várias partes, as partes são delimitadas com um delimitador de limite, construído usando CRLF, "-" e o valor do parâmetro "limite".

  • todo campo recebe alguns subtítulos antes de seus dados:, Content-Disposition: form-data;o campo name, o filename, seguido pelos dados.

    O servidor lê os dados até a próxima sequência de limites. O navegador deve escolher um limite que não apareça em nenhum dos campos; é por isso que o limite pode variar entre solicitações.

    Como temos um limite único, nenhuma codificação dos dados é necessária: dados binários são enviados como estão.

    TODO: qual é o tamanho ideal do limite ( log(N)aposto) e o nome / tempo de execução do algoritmo que o encontra? Perguntado em: /cs/39687/find-the-shortest-sequence-that-is-not-a-sub-sequence-of-a-set-of-sequences

  • Content-Type é determinado automaticamente pelo navegador.

    Como foi determinado exatamente foi perguntado em: Como o tipo MIME de um arquivo carregado é determinado pelo navegador?

application / x-www-form-urlencoded

Agora mude enctypepara application/x-www-form-urlencoded, recarregue o navegador e reenvie.

Firefox enviado:

POST / HTTP/1.1
[[ Less interesting headers ... ]]
Content-Type: application/x-www-form-urlencoded
Content-Length: 51

text1=text+default&text2=a%CF%89b&file1=a.txt&file2=a.html&file3=binary

Claramente, os dados do arquivo não foram enviados, apenas os nomes de base. Portanto, isso não pode ser usado para arquivos.

Como para o campo de texto, vemos que caracteres imprimíveis habituais, como ae bforam enviados em um byte, enquanto os não-imprimíveis, como 0xCFe 0x89pegou 3 bytes cada um: %CF%89!

Comparação

O upload de arquivos geralmente contém muitos caracteres não imprimíveis (por exemplo, imagens), enquanto os formulários de texto quase nunca o fazem.

A partir dos exemplos, vimos que:

  • multipart/form-data: adiciona alguns bytes de sobrecarga de limite à mensagem e deve passar algum tempo calculando-a, mas envia cada byte em um byte.

  • application/x-www-form-urlencoded: possui um limite de byte único por campo ( &), mas adiciona um fator de sobrecarga linear de 3x para cada caractere não imprimível.

Portanto, mesmo se pudéssemos enviar arquivos application/x-www-form-urlencoded, não desejaríamos, porque é muito ineficiente.

Porém, para caracteres imprimíveis encontrados em campos de texto, isso não importa e gera menos sobrecarga; portanto, apenas o usamos.

Ciro Santilli adicionou uma nova foto
fonte
1
Como você adicionaria um anexo binário? (ou seja, uma imagem pequena) - Posso ver a alteração dos valores dos atributos Content-Dispositione, Content-Typemas como lidar com o 'conteúdo'?
Bloqueus #
3
@ianbeks O navegador faz isso automaticamente antes de enviar a solicitação. Não sei quais heurísticas são usadas, mas provavelmente a extensão do arquivo está entre elas. Isso pode responder à pergunta: stackoverflow.com/questions/1201945/...
Ciro Santilli郝海东冠状病六四事件法轮功
3
@CiroSantilli 视 事件 法轮功 纳米比亚 威 视 Acho que essa resposta é muito melhor que a escolhida. Mas remova o conteúdo irrelevante do seu perfil. É contra o espírito de SO.
smwikipedia
2
@smwikipedia obrigado pela cotação rfc e por gostar desta resposta! Sobre o nome de usuário: para mim, o espírito da SO é que todos tenham sempre as melhores informações. ~~ Vamos manter essa discussão no twitter ou meta. Paz.
Ciro Santilli escreveu
1
@KumarHarsh não há detalhes suficientes para responder, eu acho. Por favor, abra uma nova pergunta super detalhada.
Ciro Santilli escreveu:
62

Enviar arquivo como conteúdo binário (upload sem formulário ou FormData)

Nas respostas / exemplos fornecidos, o arquivo é (provavelmente) carregado com um formulário HTML ou usando a API FormData . O arquivo é apenas uma parte dos dados enviados na solicitação, portanto, o multipart/form-data Content-Typecabeçalho.

Se você quiser enviar o arquivo como o único conteúdo, poderá adicioná-lo diretamente como o corpo da solicitação e definir o Content-Typecabeçalho para o tipo MIME do arquivo que está enviando. O nome do arquivo pode ser adicionado no Content-Dispositioncabeçalho. Você pode fazer o upload assim:

var xmlHttpRequest = new XMLHttpRequest();

var file = ...file handle...
var fileName = ...file name...
var target = ...target...
var mimeType = ...mime type...

xmlHttpRequest.open('POST', target, true);
xmlHttpRequest.setRequestHeader('Content-Type', mimeType);
xmlHttpRequest.setRequestHeader('Content-Disposition', 'attachment; filename="' + fileName + '"');
xmlHttpRequest.send(file);

Se você (não quiser) usar formulários e estiver interessado apenas em fazer upload de um único arquivo, é a maneira mais fácil de incluir seu arquivo na solicitação.

Wilt
fonte
Como você configura um serviço do lado do servidor para isso com o Asp.Net 4.0? Ele também manipulará vários parâmetros de entrada, como userId, path, captionText etc?
Asle G
1
@AsleG Não, é apenas para enviar um único arquivo como o conteúdo da sua solicitação. Eu não sou especialista em Asp.Net, mas você deve simplesmente extrair o conteúdo (um blob) da solicitação e salvá-lo em um arquivo usando Content-Typeo cabeçalho.
Wilt
@AsleG Talvez este link pode ajudar
Wilt
@ wilt Se eu não uso o formulário, mas quero usar a API formdata, posso fazê-lo dessa maneira?
angry kiwi
1
@AnkitKhettry Parece que ele foi enviado com um formulário ou usando a API do formulário. Essas 'strings estranhas' a que você se refere são os limites do formulário normalmente usados ​​para separar os dados do formulário em partes no servidor.
Wilt
9

Eu tenho este código Java de exemplo:

import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;

public class TestClass {
    public static void main(String[] args) throws IOException {
        ServerSocket socket = new ServerSocket(8081);
        Socket accept = socket.accept();
        InputStream inputStream = accept.getInputStream();

        InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
        char readChar;
        while ((readChar = (char) inputStreamReader.read()) != -1) {
            System.out.print(readChar);
        }

        inputStream.close();
        accept.close();
        System.exit(1);
    }
}

e eu tenho esse arquivo test.html:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>File Upload!</title>
</head>
<body>
<form method="post" action="http://localhost:8081" enctype="multipart/form-data">
    <input type="file" name="file" id="file">
    <input type="submit">
</form>
</body>
</html>

e, finalmente, o arquivo que usarei para fins de teste, chamado a.dat, tem o seguinte conteúdo:

0x39 0x69 0x65

se você interpretar os bytes acima como caracteres ASCII ou UTF-8, eles representarão:

9ie

Então, vamos executar nosso código Java, abrir test.html em nosso navegador favorito, fazer upload a.date enviar o formulário e ver o que nosso servidor recebe:

POST / HTTP/1.1
Host: localhost:8081
Connection: keep-alive
Content-Length: 196
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Origin: null
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.97 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary06f6g54NVbSieT6y
DNT: 1
Accept-Encoding: gzip, deflate
Accept-Language: en,en-US;q=0.8,tr;q=0.6
Cookie: JSESSIONID=27D0A0637A0449CF65B3CB20F40048AF

------WebKitFormBoundary06f6g54NVbSieT6y
Content-Disposition: form-data; name="file"; filename="a.dat"
Content-Type: application/octet-stream

9ie
------WebKitFormBoundary06f6g54NVbSieT6y--

Bem, não estou surpreso ao ver os caracteres 9ie porque pedimos ao Java para imprimi-los tratando-os como caracteres UTF-8. Você também pode optar por lê-los como bytes brutos.

Cookie: JSESSIONID=27D0A0637A0449CF65B3CB20F40048AF 

é realmente o último cabeçalho HTTP aqui. Depois disso, chega o Corpo HTTP, onde a meta e o conteúdo do arquivo que carregamos realmente podem ser vistos.

Koray Tugay
fonte
6

Uma mensagem HTTP pode ter um corpo de dados enviado após as linhas do cabeçalho. Em uma resposta, é aqui que o recurso solicitado é retornado ao cliente (o uso mais comum do corpo da mensagem), ou talvez texto explicativo, se houver um erro. Em uma solicitação, é aqui que os dados inseridos pelo usuário ou os arquivos carregados são enviados ao servidor.

http://www.tutorialspoint.com/http/http_messages.htm

flagg19
fonte