当我提交一个简单的表单时,就像这样,附带一个文件:

<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在发送文件时的内部工作原理。


当前回答

以二进制内容发送文件(没有表单或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);

如果你不(想)使用表单,并且你只对上传一个文件感兴趣,这是在请求中包含你的文件的最简单的方法。

其他回答

以二进制内容发送文件(没有表单或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);

如果你不(想)使用表单,并且你只对上传一个文件感兴趣,这是在请求中包含你的文件的最简单的方法。

HTTP消息可以在标题行之后发送一组数据。在响应中,这是将所请求的资源返回给客户端的地方(消息体的最常见用途),或者如果出现错误,可能会返回解释性文本。在请求中,这是将用户输入的数据或上传的文件发送到服务器的地方。

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

让我们来看看当你选择一个文件并提交你的表单时会发生什么(为了简洁起见,我已经截断了标题):

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) 文字保持朴实的风格。这是“计算机无法可靠地解释的”,所以它永远不应该用于生产,我们也不会进一步研究它。

如何生成示例

一旦您看到每种方法的示例,您就会清楚地知道它们是如何工作的,以及应该在什么时候使用每种方法。

你可以使用以下方法生成例子:

nc -l或ECHO服务器:接受GET/POST请求的HTTP测试服务器 像浏览器或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,这是UTF-8中的字节61 CF 89 62。

创建文件上传:

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

运行我们的echo服务器:

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

在浏览器上打开HTML,选择文件并单击提交并检查终端。

Nc打印收到的请求。

测试:Ubuntu 14.04.3, nc BSD 1.105, Firefox 40。

多部分/格式

火狐浏览器发送:

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 sets the content type to multipart/form-data and says that the fields are separated by the given boundary string. But note that the: boundary=---------------------------735323031399963166993862150 has two less dadhes -- than the actual barrier -----------------------------735323031399963166993862150 This is because the standard requires the boundary to start with two dashes --. The other dashes appear to be just how Firefox chose to implement the arbitrary boundary. RFC 7578 clearly mentions that those two leading dashes -- are required:

4.1. multipart/form-data的“Boundary”参数

与其他多部分类型一样,各部分用 边界分隔符,使用CRLF、“——”和值构造 “boundary”参数。

every field gets some sub headers before its data: Content-Disposition: form-data;, the field name, the filename, followed by the data. The server reads the data until the next boundary string. The browser must choose a boundary that will not appear in any of the fields, so this is why the boundary may vary between requests. Because we have the unique boundary, no encoding of the data is necessary: binary data is sent as is. TODO: what is the optimal boundary size (log(N) I bet), and name / running time of the algorithm that finds it? Asked at: https://cs.stackexchange.com/questions/39687/find-the-shortest-sequence-that-is-not-a-sub-sequence-of-a-set-of-sequences Content-Type is automatically determined by the browser. How it is determined exactly was asked at: How is mime type of an uploaded file determined by browser?

应用程序/ x-www-form-urlencoded

现在将加密类型更改为application/x-www-form-urlencoded,重新加载浏览器,并重新提交。

火狐浏览器发送:

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的文件,我们也不想这样做,因为这样效率很低。

但是对于在文本字段中找到的可打印字符,这并不重要,而且产生的开销更少,所以我们只使用它。

我有这个示例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,上传。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,我们上传的文件的meta和内容实际上可以看到。