当我提交一个附有文件的简单表格时,
<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的内部工作原理。
答案 0 :(得分:259)
让我们来看看当您选择文件并提交表单时会发生什么(为简洁起见我截断了标题):
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
标题的一部分。
详细信息为here。
答案 1 :(得分:216)
它如何在内部发送文件?
格式称为multipart/form-data
,如:What does enctype='multipart/form-data' mean?
我要去:
enctype
有three possibilities:
x-www-urlencoded
multipart/form-data
(规格指向RFC2388)text-plain
。这是“计算机无法可靠解释”,因此不应该在生产中使用,我们不会进一步研究它。一旦你看到每个方法的一个例子,就会明白它们是如何工作的,以及何时应该使用每个方法。
您可以使用以下方式生成示例:
nc -l
或ECHO服务器:HTTP test server accepting GET/POST requests 将表单保存到最小.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
,这是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。
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=---------------------------9051914041544843365972754266
将内容类型设置为multipart/form-data
,并说明字段由给定的boundary
字符串分隔。
每个字段在其数据之前获取一些子标题:Content-Disposition: form-data;
,字段name
,filename
,后跟数据。
服务器读取数据直到下一个边界字符串。浏览器必须选择一个不会出现在任何字段中的边界,因此这就是边界可能因请求而异的原因。
因为我们有唯一的边界,所以不需要对数据进行编码:二进制数据按原样发送。
TODO:什么是最佳边界大小(我打赌log(N)
),以及找到它的算法的名称/运行时间?询问:https://cs.stackexchange.com/questions/39687/find-the-shortest-sequence-that-is-not-a-sub-sequence-of-a-set-of-sequences
Content-Type
由浏览器自动确定。
如何确定是在How is mime type of an uploaded file determined by browser?
现在将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
的文件,我们也不会这样做,因为它效率很低。
但是对于在文本字段中找到的可打印字符,它无关紧要并且产生的开销较少,所以我们只是使用它。
答案 2 :(得分:48)
在给定的答案/示例中,文件(很可能)使用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);
如果您不想(想)使用表单而您只想上传一个文件,这是将您的文件包含在请求中的最简单方法。
答案 3 :(得分:7)
我有这个示例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
并提交表单,看看我们的服务器收到了什么:< / p>
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,其中可以看到我们实际上传的文件的元数据和内容。
答案 4 :(得分:5)
HTTP消息可能在标题行之后发送了一组数据。在响应中,这是将请求的资源返回给客户端的地方(消息体的最常见用法),或者如果出现错误则可能是解释性文本。在请求中,这是用户输入的数据或上载的文件发送到服务器的地方。