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的内部工作原理。

803472 次浏览

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

注意:每个边界字符串必须以额外的--为前缀,就像最后一个边界字符串的末尾一样。上面的示例已经包含了这一点,但很容易错过。请参阅下面@Andreas的评论。

代替URL编码表单参数,表单参数(包括文件数据)作为请求正文中的多部分文档中的部分发送。

在上面的示例中,您可以看到输入MAX_FILE_SIZE,其值设置在表单中,以及包含文件数据的部分。文件名是Content-Disposition标头的一部分。

完整的详细信息是这里

它如何在内部发送文件?

该格式称为multipart/form-data,如enctype='multipart/form-data'是什么意思?所问

我要去:

  • 添加更多HTML5参考
  • 解释为什么他用表单提交示例是正确的

HTML5参考

enctype三种可能性

如何生成示例

一旦你看到每种方法的例子,它们是如何工作的,以及何时应该使用每种方法就变得很明显了。

您可以使用以下示例生成:

将表单保存到最小的.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

运行我们的小回声服务器:

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

在浏览器上打开超文本标记语言,选择文件并单击提交并检查终端。

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(UTF-8中的aωb)按字面意思发送。您可以使用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构造,"--",以及 “边界”参数。

  • 每个字段在其数据之前都有一些子标头:Content-Disposition: form-data;、字段namefilename,然后是数据。

    服务器读取数据直到下一个边界字符串。浏览器必须选择一个不会出现在任何字段中的边界,这就是为什么边界可能因请求而异。

    因为我们有唯一的边界,所以不需要对数据进行编码:二进制数据按原样发送。

    待办事项:最佳边界大小(我打赌是log(N))是多少,以及找到它的算法的名称/运行时间?

  • Content-Type由浏览器自动确定。

    如何准确确定被问到:浏览器如何确定上传文件的mime类型?

应用程序/x-www-表单-urlencode d

现在将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

显然文件数据没有发送,只有基本名称。所以这不能用于文件。

至于文本字段,我们看到像ab这样的可打印字符以一个字节的形式发送,而像0xCF0x89这样的不可打印字符则分别占用了3字节%CF%89

比较

文件上传通常包含大量不可打印的字符(例如图像),而文本表单几乎从不这样做。

从这些例子中我们可以看出:

  • multipart/form-data:为消息添加了几个字节的边界开销,并且必须花费一些时间来计算它,但在一个字节中发送每个字节。

  • application/x-www-form-urlencoded:每个字段有一个单字节边界(&),但为每个不可打印字符添加线性开销因子3x

因此,即使我们可以使用application/x-www-form-urlencoded发送文件,我们也不想这样做,因为它效率低下。

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

以二进制内容发送文件(无表单或FormData上传)

在给出的答案/示例中,文件(很可能)以超文本标记语言形式或使用表单数据接口上传。该文件只是请求中发送的数据的一部分,因此是multipart/form-dataContent-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 {
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主体,在这里可以看到我们上传的文件的元和内容。