Come funziona il caricamento di file HTTP?


528

Quando invio un semplice modulo come questo con un file allegato:

<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>

Come invia il file internamente? Il file viene inviato come parte del corpo HTTP come dati? Nelle intestazioni di questa richiesta, non vedo nulla correlato al nome del file.

Vorrei solo sapere il funzionamento interno dell'HTTP durante l'invio di un file.


Non uso uno sniffer da un po 'ma se vuoi vedere cosa viene inviato nella tua richiesta (dato che è al server è una richiesta) lo annusi. Questa domanda è troppo ampia. SO è più per domande di programmazione specifiche.
paparazzo,

... man mano che gli sniffer, il violinista è la mia arma preferita. Puoi persino creare le tue richieste di test per vedere come pubblicano.
Phil Cooper,

Per chi fosse interessato, vedere anche " MAX_FILE_SIZEin PHP - qual è il punto" su stackoverflow.com/q/1381364/632951
Pacerier

Trovo MAX_FILE_SIZE strano. come posso modificare il mio HTML in Chrome a 100000000 prima di pubblicarlo in modo da pubblicare un valore migliore. O 1. lo hanno in un cookie con un hash sicuro tramite salt, quindi se il cookie viene modificato, il server può convalidare e generare un'eccezione (come fanno i webpieces o il playframe) o una sorta di convalida del modulo che le cose non sono cambiate. @ 0xSina
Dean Hiller

Risposte:


320

Diamo un'occhiata a cosa succede quando selezioni un file e invii il modulo (per brevità ho troncato le intestazioni):

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: ogni stringa di limite deve essere preceduta da un ulteriore --, proprio come alla fine dell'ultima stringa di limite. L'esempio sopra include già questo, ma può essere facile perdere. Vedi il commento di @Andreas di seguito.

Invece della codifica URL dei parametri del modulo, i parametri del modulo (inclusi i dati del file) vengono inviati come sezioni in un documento multipart nel corpo della richiesta.

Nell'esempio sopra, puoi vedere l'input MAX_FILE_SIZEcon il valore impostato nel modulo, così come una sezione contenente i dati del file. Il nome del file fa parte diContent-Disposition dell'intestazione.

I dettagli completi sono qui .


7
@ source.rar: No. I server web sono (quasi?) sempre sottoposti a thread in modo da poter gestire connessioni simultanee. In sostanza, il processo daemon che è in ascolto sulla porta 80 passa immediatamente il compito di servire un altro thread / processo in modo che possa tornare in ascolto per un'altra connessione; anche se due connessioni in arrivo arrivano esattamente nello stesso momento, rimarranno semplicemente nel buffer di rete fino a quando il demone non sarà pronto a leggerle.
Eggyal

10
La spiegazione del threading è un po 'errata in quanto esistono server ad alte prestazioni progettati come single threading e che utilizzano una macchina a stati per trasferire rapidamente i pacchetti di dati dalle connessioni. Piuttosto, in TCP / IP, la porta 80 è una porta di ascolto, non la porta su cui vengono trasferiti i dati.
slebetman,

9
Quando un socket di ascolto IP (porta 80) riceve una connessione, viene creato un altro socket su un'altra porta, di solito con un numero casuale superiore a 1000. Questo socket viene quindi collegato alla presa remota lasciando la porta 80 libera di ascoltare nuove connessioni.
slebetman,

11
@slebetman Innanzitutto, si tratta di HTTP. La modalità FTP attiva non si applica qui. In secondo luogo, la presa di ascolto non viene bloccata su ogni connessione. Puoi avere quante più connessioni a una porta, come l'altra parte ha porte a cui legare la propria estremità.
Slotos,

33
Si noti che la stringa di limite che viene passata come parte del campo di intestazione Content-Type è più corta di 2 caratteri rispetto alle stringhe di confine per le singole parti sottostanti. Ho appena trascorso un'ora a cercare di capire perché il mio uploader non funziona perché è abbastanza difficile notare che in realtà ci sono solo 4 trattini nella prima stringa di contorno ma 6 trattini nelle altre stringhe di confine. In altre parole: quando si utilizza la stringa di confine per separare i singoli dati del modulo, deve essere preceduto da due trattini: - È descritto in RFC1867 ovviamente ma penso che dovrebbe essere indicato anche qui
Andreas

279

Come invia il file internamente?

Il formato viene chiamato multipart/form-data, come richiesto in: Cosa significa enctype = 'multipart / form-data'?

Sto andando a:

  • aggiungi altri riferimenti HTML5
  • spiega perché ha ragione con un modulo invia un esempio

Riferimenti HTML5

Esistono tre possibilità per enctype:

Come generare gli esempi

Una volta che vedi un esempio di ciascun metodo, diventa ovvio come funzionano e quando dovresti usarli.

Puoi produrre esempi usando:

Salvare il modulo in un .htmlfile minimo :

<!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>

Impostiamo il valore di testo predefinito su a&#x03C9;b, il che significa aωbperché ωè U+03C9, che sono i byte 61 CF 89 62in UTF-8.

Crea file da caricare:

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

Esegui il nostro piccolo server echo:

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

Apri l'HTML sul tuo browser, seleziona i file e fai clic su Invia e controlla il terminale.

nc stampa la richiesta ricevuta.

Testato su: Ubuntu 14.04.3, ncBSD 1.105, Firefox 40.

multipart / form-data

Firefox inviato:

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--

Per il file binario e il campo di testo, i byte 61 CF 89 62( aωbin UTF-8) vengono inviati letteralmente. Puoi verificarlo con nc -l localhost 8000 | hd, che dice che i byte:

61 CF 89 62

sono stati inviati ( 61== 'a' e 62== 'b').

Pertanto è chiaro che:

  • Content-Type: multipart/form-data; boundary=---------------------------735323031399963166993862150imposta il tipo di contenuto su multipart/form-datae dice che i campi sono separati dal datoboundary stringa .

    Ma nota che:

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

    ha due paphes in meno --rispetto alla barriera reale

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

    Questo perché lo standard richiede che il limite inizi con due trattini --. Gli altri trattini sembrano essere proprio come Firefox ha scelto di implementare il confine arbitrario. RFC 7578 menziona chiaramente che --sono necessari quei due trattini principali :

    4.1. Parametro "Boundary" di multipart / form-data

    Come con altri tipi multipart, le parti sono delimitate da un delimitatore di limite, costruito usando CRLF, "-" e il valore del parametro "limite".

  • ogni campo ottiene alcuni sottotitoli prima dei suoi dati:, Content-Disposition: form-data;il campo name, il filename, seguito dai dati.

    Il server legge i dati fino alla stringa di limite successiva. Il browser deve scegliere un limite che non verrà visualizzato in nessuno dei campi, quindi è per questo che il limite può variare tra le richieste.

    Poiché abbiamo il limite univoco, non è necessaria alcuna codifica dei dati: i dati binari vengono inviati così come sono.

    TODO: qual è la dimensione del limite ottimale ( log(N)scommetto) e il nome / tempo di esecuzione dell'algoritmo che lo trova? Chiesto a: /cs/39687/find-the-shortest-sequence-that-is-not-a-sub-sequence-of-a-set-of-sequences

  • Content-Type viene determinato automaticamente dal browser.

    Come viene determinato esattamente è stato chiesto a: Come viene determinato il tipo mime di un file caricato dal browser?

application / x-www-form-urlencoded

Ora modifica il enctypein application/x-www-form-urlencoded, ricarica il browser e reinvia.

Firefox inviato:

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

Chiaramente i dati del file non sono stati inviati, solo i nomi di base. Quindi questo non può essere usato per i file.

Per quanto riguarda il campo di testo, vediamo che al solito i caratteri stampabili come ae bsono stati inviati in un byte, mentre quelli non stampabili come 0xCFe 0x89presero 3 byte ciascuno: %CF%89!

Confronto

I caricamenti di file contengono spesso molti caratteri non stampabili (ad esempio immagini), mentre i moduli di testo non lo fanno quasi mai.

Dagli esempi abbiamo visto che:

  • multipart/form-data: aggiunge alcuni byte di sovraccarico al confine al messaggio e deve impiegare del tempo per calcolarlo, ma invia ogni byte in un byte.

  • application/x-www-form-urlencoded: ha un limite di byte singolo per campo ( &), ma aggiunge un fattore di sovraccarico lineare di 3x per ogni carattere non stampabile.

Pertanto, anche se potessimo inviare file con application/x-www-form-urlencoded , non vorremmo, perché è così inefficiente.

Ma per i caratteri stampabili che si trovano nei campi di testo, non importa e genera meno sovraccarico, quindi lo usiamo e basta.


1
Come aggiungeresti un allegato binario? (ad es. una piccola immagine) - Vedo cambiare i valori per gli attributi Content-Dispositione Content-Typema come gestire il 'contenuto'?
Blurfus,

3
@ianbeks Il browser lo fa automaticamente prima di inviare la richiesta. Non so quale euristica utilizzi, ma molto probabilmente l'estensione del file è tra questi. Questo può rispondere alla domanda: stackoverflow.com/questions/1201945/...
Ciro Santilli郝海东冠状病六四事件法轮功

3
@CiroSantilli 六四 事件 法轮功 纳米比亚 威 视 Penso che questa risposta sia molto migliore di quella scelta. Ma rimuovi i contenuti non pertinenti dal tuo profilo. È contro lo spirito della SO.
smwikipedia,

2
@smwikipedia grazie per il preventivo RFC e per aver gradito questa risposta! Informazioni sul nome utente: per me, lo spirito di SO è che tutti dovrebbero avere le migliori informazioni in ogni momento. ~~ Manteniamo questa discussione su Twitter o Meta. Pace.
Ciro Santilli 6 冠状 病 六四 事件 法轮功

1
@KumarHarsh non abbastanza dettagli per rispondere, penso. Si prega di aprire una nuova domande super dettagliate.
Ciro Santilli 19 冠状 病 六四 事件 法轮功

62

Invia file come contenuto binario (carica senza modulo o FormData)

Nelle risposte / esempi forniti il ​​file viene (molto probabilmente) caricato con un modulo HTML o utilizzando l' API FormData . Il file è solo una parte dei dati inviati nella richiesta, quindi l' multipart/form-data Content-Typeintestazione.

Se si desidera inviare il file come unico contenuto, è possibile aggiungerlo direttamente come corpo della richiesta e impostare l' Content-Typeintestazione sul tipo MIME del file che si sta inviando. Il nome del file può essere aggiunto Content-Dispositionnell'intestazione. Puoi caricare in questo modo:

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 non si desidera (si desidera) utilizzare i moduli e si è interessati solo a caricare un singolo file, questo è il modo più semplice per includere il file nella richiesta.


Come si configura un servizio lato server per questo con Asp.Net 4.0? Gestirà anche più parametri di input, come userId, path, captionText ecc?
Asle G

1
@AsleG No, serve solo per inviare un singolo file come contenuto della tua richiesta. Non sono un esperto di Asp.Net, ma dovresti semplicemente estrarre il contenuto (un blob) dalla richiesta e salvarlo in un file usando Content-Typel'intestazione from.
Appassisci il

@AsleG Forse questo link può aiutare
Wilt il

@wilt Se non utilizzo form, ma voglio usare l'API formdata, posso farlo in questo modo?
kiwi arrabbiato,

1
@AnkitKhettry Sembra che sia caricato con un modulo o usando l'API del modulo. Queste "strane stringhe" a cui fai riferimento sono i limiti del modulo normalmente utilizzati per separare i dati del modulo in parti sul server.
Wilt

9

Ho questo codice Java di esempio:

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 ho questo file 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 infine il file che userò a scopo di test, chiamato a.dat ha il seguente contenuto:

0x39 0x69 0x65

se interpretate i byte sopra come caratteri ASCII o UTF-8, rappresenteranno effettivamente:

9ie

Quindi eseguiamo il nostro codice Java, apriamo test.html nel nostro browser preferito, cariciamo e inviamoa.dat il modulo e vediamo cosa riceve il nostro server:

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--

Beh, non sono sorpreso di vedere i personaggi 9ie perché abbiamo detto a Java di stamparli trattandoli come personaggi UTF-8. Puoi anche scegliere di leggerli come byte grezzi ..

Cookie: JSESSIONID=27D0A0637A0449CF65B3CB20F40048AF 

è in realtà l'ultima intestazione HTTP qui. Dopodiché arriva l'HTTP Body, dove possono essere effettivamente visualizzati meta e contenuti del file che abbiamo caricato.


6

Un messaggio HTTP può avere un corpo di dati inviati dopo le righe di intestazione. In una risposta, è qui che la risorsa richiesta viene restituita al client (l'uso più comune del corpo del messaggio), o forse testo esplicativo in caso di errore. In una richiesta, è qui che i dati immessi dall'utente o i file caricati vengono inviati al server.

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.