HTTPfile upload如何工作?
当我用附件提交一个简单的表格时,
<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的内部工作。
让我们来看看当你select一个文件并提交你的表单时(为了简洁起见,我已经截断了这些标题):
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编码表单参数,而是作为请求主体中的多部分文档中的部分发送。
在上面的例子中,您可以看到inputMAX_FILE_SIZE
,其中包含表单中MAX_FILE_SIZE
的值以及包含文件数据的部分。 文件名称是Content-Disposition
标题的一部分。
完整的细节在这里 。
它如何在内部发送文件?
这个格式被称为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ω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
。
创build要上传的文件:
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,select文件并点击提交并检查terminal。
nc
打印收到的请求。
经testing: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
(UTF-8中的aωb
)是逐字发送的。 你可以使用nc -l localhost 8000 | hd
来validation nc -l localhost 8000 | hd
,这就是说,字节:
61 CF 89 62
( 61
=='a'和62
=='b')。
所以很清楚:
-
Content-Type: multipart/form-data; boundary=---------------------------9051914041544843365972754266
Content-Type: multipart/form-data; boundary=---------------------------9051914041544843365972754266
将内容types设置为multipart/form-data
并表示字段由给定的boundary
string。 -
每个字段在其数据之前获得一些子标题:
Content-Disposition: form-data;
,字段name
,filename
,后面跟着数据。服务器读取数据直到下一个边界string。 浏览器必须select一个不会出现在任何字段中的边界,所以这就是为什么边界可能因请求而不同。
由于我们有独特的边界,因此不需要编码数据:二进制数据按原样发送。
TODO:什么是最佳的边界大小(
log(N)
我打赌)和algorithm的名字/运行时间find它? 问在: https : //cs.stackexchange.com/questions/39687/find-the-shortest-sequence-that-is-not-a-sub-sequence-of-a-set-of-sequences -
Content-Type
由浏览器自动确定。如何确切地被问到: 如何由浏览器确定上传文件的MIMEtypes?
应用程序/ x-WWW窗体-urlencoded
现在将enctype
更改为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
如0xCF
和0x89
分别占用3个字节 : %CF%89
!
对照
file upload通常包含大量不可打印的字符(例如图像),而文本表单几乎从不做。
从我们看到的例子来看:
-
multipart/form-data
:将几个字节的边界开销添加到消息中,并且必须花费一些时间来计算它,但是将每个字节发送到一个字节中。 -
application/x-www-form-urlencoded
:每个字段(&
)有一个单字节边界,但为每个不可打印的字符添加3x的线性开销因子。
因此,即使我们可以用application/x-www-form-urlencoded
发送文件,我们也不想,因为效率太低了。
但是对于在文本字段中find的可打印字符,这并不重要,生成的开销较小,所以我们只是使用它。
将文件作为二进制内容发送(无需Form或FormData即可上传)
在给定的答案/例子中,文件(最有可能)是用HTML表单或使用FormData API上传的。 该文件只是请求中发送的数据的一部分,因此是multipart/form-data
Content-Type
头。
如果你想发送文件作为唯一的内容,那么你可以直接将它添加为请求正文,并将Content-Type
头设置为您正在发送的文件的MIMEtypes。 文件名称可以添加到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消息可能会在标题行之后发送一个数据体。 在一个响应中,这就是被请求的资源返回给客户端的地方(消息主体最常见的用途),或者如果有错误的话可能是解释性文本。 在请求中,这是用户input的数据或上传的文件被发送到服务器的地方。
我有这个示例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>
最后是我将用于testing目的的文件,名为a.dat的文件具有以下内容:
0x39 0x69 0x65
如果你将上面的字节解释为ASCII或UTF-8字符,他们实际上将代表:
9ie
所以让我们运行我们的Java代码,打开chrome中的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字符。 你也可以select以原始字节读取它们。
Cookie: JSESSIONID=27D0A0637A0449CF65B3CB20F40048AF
实际上是这里的最后一个HTTP头。 之后是HTTP Body,其中可以看到我们上传的文件的元数据和内容。