Как работает загрузка HTTP файла?

528

Когда я отправляю простую форму вроде этого с прикрепленным файлом:

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

Как это отправить файл внутри? Файл отправляется как часть тела HTTP как данные? В заголовках этого запроса я не вижу ничего, связанного с именем файла.

Я просто хотел бы знать внутреннюю работу HTTP при отправке файла.

0xSina
источник
Некоторое время я не использовал анализатор, но если вы хотите увидеть, что отправляется в вашем запросе (поскольку он направлен на сервер, это запрос), прослушайте его. Этот вопрос слишком широк. ТАК больше для конкретных вопросов программирования.
Папараццо
... как снифферы, мой выбор - скрипач . Вы даже можете создавать свои собственные тестовые запросы, чтобы увидеть, как они публикуются.
Фил Купер
Для тех, кто заинтересован, также посмотрите « MAX_FILE_SIZEв PHP - какой смысл» на stackoverflow.com/q/1381364/632951
Pacerier
Я нахожу MAX_FILE_SIZE странным. как я могу изменить свой HTML-код в Chrome до 100000000, прежде чем публиковать его, так что он публикует лучшее значение. Либо 1. поместите его в файл cookie с безопасным хешем через соль, чтобы при изменении файла cookie сервер мог проверять и выдавать исключения (как это делают веб-части или игровые рамки), либо проверять какую-либо форму, чтобы ничего не изменилось. @ 0xSina
Дин Хиллер

Ответы:

320

Давайте посмотрим, что происходит, когда вы выбираете файл и отправляете форму (для краткости я обрезал заголовки):

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

ПРИМЕЧАНИЕ. Каждой граничной строке должен предшествовать дополнительный символ --, как в конце последней граничной строки. Пример выше уже включает это, но это может быть легко пропустить. Смотрите комментарий @Andreas ниже.

Вместо URL, кодирующего параметры формы, параметры формы (включая данные файла) отправляются в виде разделов в многочастном документе в теле запроса.

В приведенном выше примере вы можете увидеть входные данные MAX_FILE_SIZEсо значением, заданным в форме, а также раздел, содержащий данные файла. Имя файла является частью Content-Dispositionзаголовка.

Полная информация здесь .

toddsundsted
источник
7
@ source.rar: Нет. Веб-серверы (почти?) всегда имеют многопоточность, чтобы они могли обрабатывать параллельные соединения. По сути, процесс-демон, который прослушивает порт 80, немедленно выполняет задачу обслуживания другому потоку / процессу, чтобы он мог вернуться к прослушиванию другого соединения; даже если два входящих соединения приходят в один и тот же момент, они просто будут находиться в сетевом буфере, пока демон не будет готов их прочитать.
eggyal
10
Объяснение многопоточности немного неверно, поскольку существуют высокопроизводительные серверы, которые спроектированы как однопоточные и используют конечный автомат для быстрой загрузки пакетов данных из соединений по очереди. Скорее, в TCP / IP порт 80 является портом прослушивания, а не портом, на который передаются данные.
Slebetman
9
Когда прослушивающий сокет IP (порт 80) получает соединение, на другом порту создается другой сокет, обычно со случайным числом выше 1000. Затем этот сокет подключается к удаленному сокету, оставляя порт 80 свободным для прослушивания новых соединений.
Slebetman
11
@slebetman Прежде всего, это о HTTP. Активный режим FTP здесь не применяется. Во-вторых, сокет прослушивания не блокируется при каждом подключении. У вас может быть столько соединений с одним портом, сколько у других сторон есть порты, к которым они могут привязать свой конец.
Slotos
33
Обратите внимание, что строка границы, которая передается как часть поля заголовка Content-Type, на 2 символа короче, чем строка границы для отдельных частей ниже. Я только что потратил час, пытаясь выяснить, почему мой загрузчик не работает, потому что довольно трудно заметить, что на самом деле в первой граничной строке есть только 4 черты, а в других граничных строках - 6. Другими словами: при использовании граничной строки для разделения отдельных данных формы перед ней должны стоять две черточки: - Конечно, это описано в RFC1867, но я думаю, что и здесь следует указать
Андреас
280

Как это отправить файл внутри?

Формат называется multipart/form-data, как спрашивается в: Что означает enctype = 'multipart / form-data'?

Я собираюсь:

  • добавить еще несколько ссылок HTML5
  • объясните, почему он прав с помощью примера

HTML5 ссылки

Есть три возможности для enctype:

  • x-www-urlencoded
  • multipart/form-data(спецификация указывает на RFC2388 )
  • text-plain, Это «ненадежно интерпретируется компьютером», поэтому никогда не должно использоваться в производстве, и мы не будем вдаваться в подробности.

Как генерировать примеры

Как только вы видите пример каждого метода, становится очевидным, как они работают, и когда вы должны использовать каждый из них.

Вы можете привести примеры, используя:

Сохраните форму в минимальный .htmlфайл:

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

Мы устанавливаем текстовое значение по умолчанию a&#x03C9;b, что означает, aωbпотому что ωесть U+03C9, которые являются байтами 61 CF 89 62в UTF-8.

Создайте файлы для загрузки:

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

Запустите наш маленький эхо-сервер:

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

Откройте HTML в вашем браузере, выберите файлы, нажмите «Отправить» и проверьте терминал.

nc печатает полученный запрос

Проверено на: Ubuntu 14.04.3, ncBSD 1.105, Firefox 40.

многочастному / форм-данных,

Firefox отправил:

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

Для двоичного файла и текстового поля байты 61 CF 89 62( aωbв UTF-8) отправляются буквально. Вы можете проверить это с помощью nc -l localhost 8000 | hd, который говорит, что байты:

61 CF 89 62

были отправлены ( 61== 'a' и 62== 'b').

Поэтому ясно, что:

  • Content-Type: multipart/form-data; boundary=---------------------------735323031399963166993862150устанавливает тип содержимого multipart/form-dataи говорит, что поля разделены заданной boundaryстрокой.

    Но обратите внимание, что:

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

    имеет на два меньше черт, --чем фактический барьер

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

    Это потому, что стандарт требует, чтобы граница начиналась с двух штрихов --. Похоже, что другие черты - это то, как Firefox решил реализовать произвольную границу. RFC 7578 четко упоминает, что эти две лидирующие черты --необходимы:

    4.1. «Граничный» параметр multipart / form-data

    Как и в случае других составных типов, части разделяются граничным разделителем, созданным с использованием CRLF, "-" и значения параметра "border".

  • каждое поле получает некоторые вложенные заголовки перед своими данными: Content-Disposition: form-data;, поле name, тем filename, за которым следуют данные.

    Сервер читает данные до следующей граничной строки. Браузер должен выбрать границу, которая не будет отображаться ни в одном из полей, поэтому эта граница может варьироваться между запросами.

    Поскольку у нас есть уникальная граница, кодирование данных не требуется: двоичные данные отправляются как есть.

    ТОДО: каков оптимальный размер границы ( log(N)бьюсь об заклад) и название / время выполнения алгоритма, который его находит? На вопрос: /cs/39687/find-the-shortest-sequence-that-is-not-a-sub-sequence-of-a-set-of-sequence

  • Content-Type определяется автоматически браузером.

    Как именно это определяется, было задано по адресу: Как браузер определяет тип mime загруженного файла?

применение / х-WWW-форм-urlencoded

Теперь измените enctypeк application/x-www-form-urlencoded, перезагрузите браузер и повторите.

Firefox отправил:

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

Ясно, что данные файла не были отправлены, только базовые имена. Так что это не может быть использовано для файлов.

Что касается текстового поля, мы видим, что обычные печатаемые символы, такие как aи bбыли отправлены в один байт, а непечатные символы, такие как 0xCFи 0x89занимают 3 байта каждый %CF%89:!

сравнение

Загрузки файлов часто содержат много непечатных символов (например, изображений), в то время как текстовые формы почти никогда не делают.

Из примеров мы видели, что:

  • multipart/form-data: добавляет к сообщению несколько байтов служебных данных границы и должен потратить некоторое время на его вычисление, но отправляет каждый байт по одному байту.

  • application/x-www-form-urlencoded: имеет одну байтовую границу для каждого поля ( &), но добавляет линейный коэффициент издержек в 3 раза для каждого непечатаемого символа.

Поэтому, даже если бы мы могли отправлять файлы с помощью application/x-www-form-urlencoded, мы бы этого не хотели, потому что это так неэффективно.

Но для печатных символов, найденных в текстовых полях, это не имеет значения и создает меньше накладных расходов, поэтому мы просто используем их.

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
источник
1
Как бы вы добавили двоичное вложение? (то есть небольшое изображение) - я могу видеть изменения значения для Content-Dispositionи Content-Typeатрибутов , но как справиться с «содержанием»?
blurfus
3
@ianbeks Браузер делает это автоматически перед отправкой запроса. Я не знаю, какую эвристику он использует, но, скорее всего, среди них расширение файла. Это может ответить на вопрос: stackoverflow.com/questions/1201945/…
Сиро Сантилли 法轮功 冠状 病 六四 事件 法轮功
3
@CiroSantilli 六四 事件 法轮功 纳米比亚 威 视 Я думаю, что этот ответ намного лучше, чем выбранный. Но, пожалуйста, удалите ненужный контент из своего профиля. Это против духа ТАК.
smwikipedia
2
@smwikipedia спасибо за цитату RFC и за этот ответ! Об имени пользователя: для меня дух SO заключается в том, что каждый должен всегда иметь самую лучшую информацию. ~~ Давайте сохраним это обсуждение в твиттере или мета. Мир.
Сиро Сантилли 郝海东 冠状 病 六四 事件 法轮功
1
@KumarHarsh недостаточно подробно, чтобы ответить, я думаю. Пожалуйста, откройте новые супер подробные вопросы.
Сиро Сантилли 郝海东 冠状 病 六四 事件 法轮功
62

Отправить файл как двоичное содержимое (загрузить без формы или FormData)

В приведенных ответах / примерах файл (скорее всего) загружен с HTML-формой или с использованием API-интерфейса FormData . Файл является только частью данных, отправляемых в запросе, отсюда и multipart/form-data Content-Typeзаголовок.

Если вы хотите отправить файл в качестве единственного содержимого, вы можете напрямую добавить его в качестве тела запроса и установить в Content-Typeзаголовке тип MIME отправляемого файла. Имя файла можно добавить в Content-Dispositionшапку. Вы можете загрузить как это:

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

Если вы не (не хотите) использовать формы и заинтересованы только в загрузке одного файла, это самый простой способ включить ваш файл в запрос.

Уилт
источник
Как настроить серверную службу для этого с Asp.Net 4.0? Будет ли он обрабатывать несколько входных параметров, таких как userId, path, captionText и т. Д.?
Asle G
1
@ AsleG Нет, это только для отправки одного файла в качестве содержания вашего запроса. Я не эксперт Asp.Net, но вы должны просто извлечь содержимое (блоб) из запроса и сохранить его в файл, используя Content-Typeзаголовок.
Уилт
@AsleG Может быть, эта ссылка может помочь
Уилт
@wilt Если я не использую форму, но я хочу использовать API формданных, могу ли я сделать это таким образом?
злой киви
1
@AnkitKhettry Похоже, что он загружен с формой или с помощью API формы. Эти «странные строки», на которые вы ссылаетесь, являются границами формы, обычно используемыми для разделения данных формы на части на сервере.
Уилт
9

У меня есть этот пример кода 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);
    }
}

и у меня есть этот файл 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>

и, наконец, файл, который я буду использовать для тестирования с именем a.dat, имеет следующее содержимое:

0x39 0x69 0x65

если вы интерпретируете байты выше как символы ASCII или UTF-8, они фактически будут представлять:

9ie

Итак, давайте запустим наш Java-код, откроем test.html в нашем любимом браузере, загрузим a.datи отправим форму и посмотрим, что получит наш сервер:

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

Ну, я не удивлен, увидев символы 9ie, потому что мы сказали Java печатать их, считая их символами UTF-8. Вы также можете прочитать их как необработанные байты.

Cookie: JSESSIONID=27D0A0637A0449CF65B3CB20F40048AF 

на самом деле последний HTTP-заголовок здесь. После этого идет HTTP Body, где на самом деле видны мета и содержимое загруженного нами файла.

Корай Тугай
источник
6

Сообщение HTTP может содержать массив данных, отправленных после строк заголовка. В ответе именно здесь запрашиваемый ресурс возвращается клиенту (наиболее распространенное использование тела сообщения) или, возможно, пояснительный текст, если есть ошибка. В запросе это то, где введенные пользователем данные или загруженные файлы отправляются на сервер.

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

flagg19
источник