Jak działa przesyłanie plików HTTP?

527

Kiedy przesyłam prosty formularz taki jak ten z załączonym plikiem:

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

Jak wysyła plik wewnętrznie? Czy plik jest wysyłany jako część treści HTTP jako dane? W nagłówkach tego żądania nie widzę nic związanego z nazwą pliku.

Chciałbym tylko znać wewnętrzne działanie HTTP podczas wysyłania pliku.

0xSina
źródło
Od jakiegoś czasu nie używałem sniffera, ale jeśli chcesz zobaczyć, co jest wysyłane w twoim żądaniu (ponieważ jest to serwer, jest to żądanie), wąchaj je. To pytanie jest zbyt ogólne. SO dotyczy bardziej konkretnych pytań programowych.
paparazzo
... gdy sniffery idą, skrzypek jest moją bronią z wyboru. Możesz nawet tworzyć własne żądania testowe, aby zobaczyć, jak publikują.
Phil Cooper
Dla zainteresowanych zobacz także „ MAX_FILE_SIZEw PHP - o co chodzi” na stackoverflow.com/q/1381364/632951
Pacerier
Uważam, że MAX_FILE_SIZE jest dziwny. ponieważ mogę zmodyfikować mój HTML w chrome do 100000000 przed opublikowaniem, aby opublikował lepszą wartość. Albo 1. umieść go w pliku cookie z bezpiecznym hashem za pośrednictwem soli, więc plik cookie, jeśli zostanie zmodyfikowany, serwer może zweryfikować i zgłosić wyjątek (jak robią to elementy strony lub playframework) lub jakiś rodzaj sprawdzania poprawności formy, że nic się nie zmieniło. @ 0xSina
Dean Hiller

Odpowiedzi:

320

Rzućmy okiem na to, co się dzieje, gdy wybierzesz plik i prześlesz formularz (skróciłem nagłówki dla zwięzłości):

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

UWAGA: każdy ciąg graniczny musi być poprzedzony dodatkowym --, tak jak na końcu ostatniego ciągu granicznego. Powyższy przykład już to obejmuje, ale łatwo go przeoczyć. Zobacz komentarz @Andreas poniżej.

Zamiast adresu URL kodującego parametry formularza, parametry formularza (w tym dane pliku) są wysyłane jako sekcje w dokumencie wieloczęściowym w treści żądania.

W powyższym przykładzie można zobaczyć dane wejściowe MAX_FILE_SIZEo wartości ustawionej w formularzu, a także sekcję zawierającą dane pliku. Nazwa pliku jest częścią Content-Dispositionnagłówka.

Pełne szczegóły są tutaj .

toddsundsted
źródło
7
@ source.rar: Nie. Serwery WWW są (prawie?) zawsze wątkowane, aby mogły obsługiwać równoczesne połączenia. Zasadniczo proces demona nasłuchujący na porcie 80 natychmiast przekazuje zadanie obsługi innego wątku / procesu, aby mógł powrócić do nasłuchiwania dla innego połączenia; nawet jeśli dwa połączenia przychodzące dotrą dokładnie w tym samym momencie, będą po prostu siedzieć w buforze sieci, dopóki demon nie będzie gotowy do ich odczytania.
eggyal
10
Wyjaśnienie wątków jest nieco niepoprawne, ponieważ istnieją serwery o wysokiej wydajności, które są zaprojektowane jako jednowątkowe i używają automatu stanów do szybkiego pobierania na zmianę pakietów danych z połączeń. Zamiast tego w TCP / IP port 80 jest portem nasłuchującym, a nie portem, na który przesyłane są dane.
slebetman
9
Kiedy gniazdo nasłuchujące IP (port 80) odbiera połączenie, inne gniazdo jest tworzone na innym porcie, zwykle o losowej liczbie powyżej 1000. To gniazdo jest następnie podłączane do gniazda zdalnego, pozostawiając port 80 swobodnie nasłuchując nowych połączeń.
slebetman
11
@slebetman Przede wszystkim chodzi o HTTP. Tryb aktywny FTP nie ma tutaj zastosowania. Po drugie, gniazdo nasłuchowe nie jest blokowane przy każdym połączeniu. Możesz mieć tyle połączeń z jednym portem, ile drugiej strony ma porty do wiązania własnego końca.
Slotos,
33
Zauważ, że ciąg granicy, który jest przekazywany jako część pola nagłówka Content-Type, jest o 2 znaki krótszy niż ciąg granicy dla poszczególnych części poniżej. Właśnie spędziłem godzinę próbując dowiedzieć się, dlaczego mój program do przesyłania nie działa, ponieważ dość trudno jest zauważyć, że w rzeczywistości są tylko 4 kreski w pierwszym ciągu granicznym, ale 6 myślników w pozostałych ciągach granicznych. Innymi słowy: w przypadku użycia ciągu granicznego do oddzielenia danych w formularzu należy poprzedzić go dwoma myślnikami: - Jest to oczywiście opisane w RFC1867, ale myślę, że należy o tym również wspomnieć
Andreas
279

Jak wysyła plik wewnętrznie?

Format jest wywoływany multipart/form-datazgodnie z pytaniem: Co oznacza enctype = 'multipart / form-data'?

Zamierzam:

  • dodaj więcej odniesień do HTML5
  • wyjaśnij, dlaczego ma rację, korzystając z formularza przesłania przykładu

Odnośniki HTML5

Istnieją trzy możliwości dla enctype:

Jak wygenerować przykłady

Gdy zobaczysz przykład każdej metody, staje się oczywiste, jak działają i kiedy powinieneś użyć każdej z nich.

Możesz tworzyć przykłady, używając:

Zapisz formularz do minimalnego .htmlpliku:

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

Ustawiamy domyślną wartość tekstową na a&#x03C9;b, co oznacza, aωbże ωjest U+03C9, czyli bajtów 61 CF 89 62w UTF-8.

Utwórz pliki do przesłania:

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

Uruchom nasz mały serwer echa:

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

Otwórz HTML w przeglądarce, wybierz pliki, kliknij prześlij i sprawdź terminal.

nc drukuje otrzymane żądanie.

Testowane na: Ubuntu 14.04.3, ncBSD 1.105, Firefox 40.

multipart / form-data

Firefox wysłał:

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

W przypadku pliku binarnego i pola tekstowego bajty 61 CF 89 62( aωbw UTF-8) są wysyłane dosłownie. Możesz to sprawdzić za pomocą nc -l localhost 8000 | hd, co oznacza, że ​​bajty:

61 CF 89 62

zostały wysłane ( 61== 'a' i 62== 'b').

Dlatego jasne jest, że:

  • Content-Type: multipart/form-data; boundary=---------------------------735323031399963166993862150ustawia typ zawartości na multipart/form-datai mówi, że pola są oddzielone danym boundaryciągiem.

    Pamiętaj jednak, że:

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

    ma dwa mniej ojców --niż rzeczywista bariera

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

    Wynika to z faktu, że norma wymaga, aby granica zaczynała się od dwóch myślników --. Inne myślniki wydają się być tym, w jaki sposób Firefox wybrał implementację arbitralnej granicy. RFC 7578 wyraźnie wspomina, że ​​te dwa wiodące myślniki --są wymagane:

    4.1 „Boundary” Parametr danych wieloczęściowych / formularzy

    Podobnie jak w przypadku innych typów wieloczęściowych, części są rozdzielane ogranicznikiem granicy, konstruowanym przy użyciu CRLF, „-” i wartości parametru „granicy”.

  • Każde pole dostaje kilka nagłówków podrzędnych przed jego danych: Content-Disposition: form-data;pole name, tym filename, po której następuje danych.

    Serwer odczytuje dane do następnego ciągu granicznego. Przeglądarka musi wybrać granicę, która nie pojawi się w żadnym z pól, dlatego granica może się różnić w zależności od żądania.

    Ponieważ mamy unikalną granicę, kodowanie danych nie jest konieczne: dane binarne są wysyłane w niezmienionej postaci.

    DO ZROBIENIA: jaki jest optymalny rozmiar granicy ( log(N)założę się) i nazwa / czas działania algorytmu, który ją znajduje? Pytanie zadane na: /cs/39687/find-the-shortest-sequence-that-is-not-a-sub-sequence-of-a-set-of-sequences

  • Content-Type jest automatycznie określany przez przeglądarkę.

    Jak to dokładnie ustalić, zapytano na stronie: W jaki sposób typ MIME przesłanego pliku jest określany przez przeglądarkę?

application / x-www-form-urlencoded

Teraz zmienić enctypesię application/x-www-form-urlencoded, załaduj przeglądarkę, i ponownie.

Firefox wysłał:

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

Najwyraźniej dane pliku nie zostały wysłane, tylko nazwy basename. Nie można tego użyć do plików.

Co do pola tekstowego, widzimy, że zwykłe znaki druku jak ai bwysłano w jednym bajcie, a niedrukowalne te, jak 0xCFi 0x89zajął 3 bajty każda: %CF%89!

Porównanie

Przesyłane pliki często zawierają wiele znaków niedrukowalnych (np. Obrazy), podczas gdy formularze tekstowe prawie nigdy tego nie robią.

Z przykładów, które widzieliśmy, że:

  • multipart/form-data: dodaje do wiadomości kilka bajtów narzutu granicznego i musi poświęcić trochę czasu na jego obliczenie, ale wysyła każdy bajt w jednym bajcie.

  • application/x-www-form-urlencoded: ma granicę jednego bajtu na pole ( &), ale dodaje liniowy współczynnik obciążenia 3x dla każdego znaku, który nie może być wydrukowany.

Dlatego nawet gdybyśmy mogli przesyłać pliki application/x-www-form-urlencoded, nie chcielibyśmy, ponieważ jest to tak nieefektywne.

Ale w przypadku znaków drukowalnych znalezionych w polach tekstowych nie ma to znaczenia i generuje mniejszy narzut, więc po prostu go używamy.

Ciro Santilli
źródło
1
Jak dodać załącznik binarny? (tj. mały obraz) - Widzę zmianę wartości atrybutów Content-Dispositioni, Content-Typeale jak obsługiwać „treść”?
blurfus
3
@ianbeks Przeglądarka robi to automatycznie przed wysłaniem żądania. Nie wiem, z jakiej heurystyki korzysta, ale najprawdopodobniej jest wśród nich rozszerzenie pliku. To może odpowiedzieć na pytanie: stackoverflow.com/questions/1201945/...
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
3
@CiroSantilli 六四 事件 法轮功 纳米比亚 威 视 Myślę, że ta odpowiedź jest znacznie lepsza niż wybrana. Ale usuń niepotrzebne treści ze swojego profilu. Jest to sprzeczne z duchem SO.
smwikipedia,
2
@smwikipedia dzięki za wycenę RCF i za polubienie tej odpowiedzi! O nazwie użytkownika: dla mnie duchem SO jest to, że każdy powinien mieć zawsze najlepszą informację. ~~ Trzymajmy tę dyskusję na Twitterze lub meta. Pokój.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
1
@KumarHarsh za mało szczegółów, by odpowiedzieć. Otwórz nowe bardzo szczegółowe pytania.
Ciro Santilli 19 冠状 病 六四 事件 法轮功
62

Wyślij plik jako treść binarną (prześlij bez formularza lub FormData)

W podanych odpowiedziach / przykładach plik jest (najprawdopodobniej) przesłany za pomocą formularza HTML lub interfejsu API FormData . Plik jest tylko częścią danych wysłanych w żądaniu, stąd multipart/form-data Content-Typenagłówek.

Jeśli chcesz wysłać plik jako jedyną treść, możesz bezpośrednio dodać go jako treść żądania i ustawić Content-Typenagłówek na typ MIME wysyłanego pliku. Nazwę pliku można dodać w Content-Dispositionnagłówku. Możesz przesłać w ten sposób:

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

Jeśli nie chcesz (używasz) formularzy i jesteś zainteresowany przesłaniem tylko jednego pliku, jest to najprostszy sposób na dołączenie pliku do żądania.

Więdnąć
źródło
Jak skonfigurować do tego usługę po stronie serwera za pomocą Asp.Net 4.0? Czy obsłuży także wiele parametrów wejściowych, takich jak userId, ścieżka, captionText itp.?
Asle G
1
@AsleG Nie, służy tylko do wysłania jednego pliku jako treści twojego żądania. Nie jestem ekspertem od Asp.Net, ale powinieneś po prostu wyciągnąć zawartość (obiekt blob) z żądania i zapisać ją w pliku przy użyciu Content-Typenagłówka.
Wilt,
@AsleG Może ten link może pomóc
Wilt
@wilt Jeśli nie używam formularza, ale chcę korzystać z interfejsu API formdata, czy mogę to zrobić w ten sposób?
zły kiwi
1
@AnkitKhettry Wygląda na to, że przesłano go za pomocą formularza lub przy użyciu interfejsu API formularza. Te „dziwne ciągi”, o których mówisz, to granice formularzy zwykle używane do rozdzielania danych formularza na części na serwerze.
Wilt
9

Mam ten przykładowy kod Java:

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);
    }
}

i mam ten plik 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>

i wreszcie plik, którego będę używać do celów testowych, o nazwie a.dat, ma następującą zawartość:

0x39 0x69 0x65

jeśli interpretujesz powyższe bajty jako znaki ASCII lub UTF-8, będą one w rzeczywistości reprezentować:

9ie

Więc uruchommy nasz kod Java, otwórzmy Uruchommy test.html w naszej ulubionej przeglądarce, prześlij a.dati prześlij formularz i zobacz, co otrzymuje nasz serwer:

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

Nie jestem zaskoczony, widząc bohaterów 9ie, ponieważ powiedzieliśmy Javie, aby je wydrukowało, traktując je jak znaki UTF-8. Równie dobrze możesz przeczytać je jako surowe bajty ..

Cookie: JSESSIONID=27D0A0637A0449CF65B3CB20F40048AF 

jest tak naprawdę ostatnim nagłówkiem HTTP tutaj. Następnie pojawia się treść HTTP, w której można zobaczyć meta i zawartość przesłanego pliku.

Koray Tugay
źródło
6

Wiadomość HTTP może zawierać zbiór danych przesłany za wierszami nagłówka. W odpowiedzi jest to miejsce, w którym żądany zasób jest zwracany do klienta (najczęstsze użycie treści komunikatu) lub tekst wyjaśniający, jeśli wystąpi błąd. W żądaniu jest to miejsce, w którym dane wprowadzone przez użytkownika lub przesłane pliki są wysyłane na serwer.

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

flagg19
źródło