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

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

<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 при отправке файла.

Ответ 1

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

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

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

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

Подробнее... здесь.

Ответ 2

Как он отправляет файл внутри?

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

Я собираюсь:

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

Ссылки HTML5

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

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

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

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

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

  • nc -l или сервер ECHO
  • пользовательский агент, например браузер или cURL

Сохраните форму в минимальном файле .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, nc BSD 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=---------------------------9051914041544843365972754266 устанавливает тип содержимого multipart/form-data и говорит, что поля разделены данной строкой boundary.

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

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

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

    TODO: какой оптимальный размер границы (log(N) я bet), а также имя/время выполнения алгоритма, который его находит? На вопрос: https://cs.stackexchange.com/questions/39687/find-the-shortest-sequence-that-is-not-a-sub-sequence-of-a-set-of-sequences

  • 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: имеет одну байтовую границу для поля (&), но добавляет линейный накладной коэффициент 3x для каждого непечатаемого символа.

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

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

Ответ 3

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

В приведенных ответах/примерах файл (скорее всего) загружен с помощью HTML-формы или с помощью FormData API. Файл является только частью данных, отправленных в запросе, поэтому заголовок 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);

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

Ответ 4

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

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

Ответ 5

У меня есть этот пример кода Java:

<!-- language: java -->

import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
public class TestClass {
    public static void main(String[] args) throws IOException {
        final ServerSocket socket = new ServerSocket(8081);
        final Socket accept = socket.accept();
        final InputStream inputStream = accept.getInputStream();
        final 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:

<!-- language: 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 inChrome, загрузите файл 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, где мета и содержимое загружаемого файла на самом деле можно увидеть.