Как работает загрузка файла 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 при отправке файла.
5 ответов
Давайте посмотрим, что происходит, когда вы выбираете файл и отправляете форму (для краткости я обрезал заголовки):
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
заголовок.
Полная информация здесь.
Как это отправить файл внутри?
Формат называется multipart/form-data
, как спросили в: Что означает enctype='multipart/form-data'?
Я собираюсь:
- добавить еще несколько ссылок HTML5
- объясните, почему он прав с помощью примера
HTML5 ссылки
Есть три возможности для enctype
:
x-www-urlencoded
multipart/form-data
(спецификация указывает на RFC2388)text-plain
, Это "ненадежно интерпретируется компьютером", поэтому никогда не должно использоваться в производстве, и мы не будем вдаваться в подробности.
Как генерировать примеры
Как только вы видите пример каждого метода, становится очевидно, как они работают, и когда вы должны использовать каждый из них.
Вы можете привести примеры, используя:
nc -l
или сервер ECHO: тестовый сервер HTTP, принимающий запросы GET/POST- пользовательский агент, такой как браузер или 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ω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ω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
== 'а' и 62
== 'b').
Поэтому ясно, что:
Content-Type: multipart/form-data; boundary=---------------------------9051914041544843365972754266
устанавливает тип контента вmultipart/form-data
и говорит, что поля разделены заданнымboundary
строка.каждое поле получает несколько подзаголовков перед своими данными:
Content-Disposition: form-data;
, полеname
,filename
, а затем данные.Сервер читает данные до следующей граничной строки. Браузер должен выбрать границу, которая не будет отображаться ни в одном из полей, поэтому эта граница может варьироваться между запросами.
Поскольку у нас есть уникальная граница, кодирование данных не требуется: двоичные данные отправляются как есть.
ТОДО: каков оптимальный размер границы (
log(N)
Бьюсь об заклад), а название / время работы алгоритма, который его находит? На вопрос: https://cs.stackexchange.com/questions/39687/find-the-shortest-sequence-that-is-not-a-sub-sequence-of-a-set-of-sequencesContent-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
Мы бы не хотели, потому что это так неэффективно.
Но для печатаемых символов, найденных в текстовых полях, это не имеет значения и создает меньше накладных расходов, поэтому мы просто используем их.
Отправить файл в виде двоичного содержимого (загрузить без формы или 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);
Если вы не (не хотите) использовать формы и заинтересованы только в загрузке одного файла, это самый простой способ включить ваш файл в запрос.
У меня есть этот пример кода 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:
<!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, где на самом деле видны мета и содержимое загруженного нами файла.
Сообщение HTTP может содержать массив данных, отправленных после строк заголовка. В ответе это то место, где запрашиваемый ресурс возвращается клиенту (наиболее распространенное использование тела сообщения) или, возможно, пояснительный текст в случае ошибки. В запросе это то, где введенные пользователем данные или загруженные файлы отправляются на сервер.