HTTP文件上传如何工作?


527

当我提交带有附件的简单表格时:

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


我已经有一段时间没有使用嗅探器了,但是如果您想查看请求中发送的内容(因为它是发送到服务器的请求),则可以进行嗅探。这个问题太广泛了。SO更适合特定的编程问题。
狗仔队

...随着嗅探者的流逝,提琴手是我的首选武器。您甚至可以建立自己的测试请求以查看其发布方式。
2014年

对于那些感兴趣的人,也请参见stackoverflow.com/q/1381364/632951MAX_FILE_SIZE上的“
in PHP-

我发现MAX_FILE_SIZE很奇怪。因为我可以在发布之前将chrome中的html修改为100000000,以便发布更好的值。1.将其放入带有盐的安全哈希的cookie中,以便对cookie进行修改(如果修改了该cookie),服务器可以验证并抛出异常(就像Webpiece或playframework都可以)或某种形式的验证,而事情没有改变。@ 0xSina
院长希勒

Answers:


320

让我们看一下选择文件并提交表单时发生的情况(为简洁起见,我已将标题删节了):

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

注意:每个边界字符串都必须以extra开头--,就像最后一个边界字符串的末尾一样。上面的示例已经包含了这一点,但是很容易遗漏。请参阅下面@Andreas的评论。

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

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

详细信息在这里


7
@ source.rar:否。Web服务器总是(几乎是?)是线程化的,以便它们可以处理并发连接。本质上,正在监听端口80的守护进程立即将服务任务移交给另一个线程/进程,以便它可以返回监听另一个连接。即使两个传入连接恰好在同一时刻到达,它们也将坐在网络缓冲区中,直到守护程序准备读取它们为止。
eggyal 2014年

10
线程解释有点不正确,因为有些高性能服务器被设计为单线程,并使用状态机来快速轮流从连接中下载数据包。而是,在TCP / IP中,端口80是侦听端口,而不是数据在其上传输的端口。
slebetman

9
当IP侦听套接字(端口80)接收到一个连接时,通常会在另一个端口上创建另一个套接字,通常其随机数大于1000。然后将该套接字连接到远程套接字,从而使端口80可以自由侦听新的连接。
slebetman 2014年

11
@slebetman首先,这是关于HTTP的。FTP活动模式不适用于此处。其次,监听套接字不会在每个连接上都被阻塞。您可以与一个端口建立尽可能多的连接,而另一端则具有将其自己的一端绑定到的端口。
Slotos 2014年

33
请注意,作为Content-Type标头字段的一部分传递的边界字符串比下面各个部分的边界字符串短2个字符。我花了一个小时试图弄清楚为什么我的上传器无法正常工作,因为很难注意到实际上第一个边界字符串中只有4个破折号,而其他边界字符串中只有6个破折号。换句话说:当使用边界字符串来分隔单个表单数据时,必须在其前面加上两个破折号:-当然在RFC1867中对此进行了描述,但我想在这里也应指出这一点
Andreas

279

它如何在内部发送文件?

该格式称为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,这是61 CF 89 62UTF-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,选择文件,然后单击Submit并检查终端。

nc 打印收到的请求。

经过测试:Ubuntu 14.04.3,ncBSD 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 62aωb在UTF-8中)按字面意义发送。您可以使用进行验证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。多部分/表单数据的“边界”参数

    与其他多部分类型一样,这些部分使用边界定界符定界,该定界符使用CRLF,“-”和“ boundary”参数的值构造。

  • 每场得到一些子报头的数据之前:Content-Disposition: form-data;,外地namefilename,其次是数据。

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

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

    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 由浏览器自动确定。

    有人问如何确定它:浏览器如何确定上载文件的mime类型?

应用程序/ x-www-form-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

显然,没有发送文件数据,仅发送了基名。因此,这不能用于文件。

至于文本字段中,我们可以看到,平时打印的字符像ab一个字节被送往,而不可打印的像0xCF,并0x89讨论了3个字节的每个:%CF%89

比较方式

文件上载通常包含许多不可打印的字符(例如图像),而文本形式几乎从来没有。

从示例中我们可以看到:

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

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

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

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


1
您将如何添加二进制附件?(即一幅小图像)-我可以看到更改了Content-DispositionContent-Type属性的值,但如何处理“内容”?
blurfus

3
@ianbeks发送请求之前,浏览器会自动执行。我不知道它使用哪种试探法,但是很可能文件扩展名就在其中。这可以回答这个问题:stackoverflow.com/questions/1201945/...
西罗桑蒂利郝海东冠状病六四事件法轮功

3
@CiroSantilli六四事件法轮功纳米比亚威视我认为此答案比所选答案要好得多。但是,请从您的个人资料中删除不相关的内容。这违反了SO的精神。
smwikipedia

2
@smwikipedia感谢RFC报价和喜欢这个答案!关于用户名:对我而言,SO的精神是每个人都应始终拥有最佳信息。~~让我们继续讨论这个话题到twitter或meta。和平。
西罗Santilli郝海东冠状病六四事件法轮功2015年

1
@KumarHarsh没有足够的细节来回答我的想法。请打开一个新的超级详细问题。
西罗Santilli郝海东冠状病六四事件法轮功

62

将文件作为二进制内容发送(不带表单或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);

如果您不(不想)使用表格,而只对上传单个文件感兴趣,这是在请求中包括文件的最简单方法。


如何使用Asp.Net 4.0为此配置服务器端服务?它也会处理多个输入参数,例如userId,path,captionText等吗?
Asle G

1
@AsleG不,它仅用于发送单个文件作为您请求的内容。我不是Asp.Net专家,但是您应该简单地从请求中拉出内容(斑点),然后使用Content-Type头文件中的将其保存到文件中。
2015年

@AsleG也许该链接会有所帮助
Wilt

@wilt如果我不使用表单,但我想使用formdata API,可以这样做吗?
愤怒的猕猴桃

1
@AnkitKhettry听起来像是使用表单或使用表单API上传的。您引用的这些“怪异字符串”是表单边界,通常用于将表单数据分为服务器上的各个部分。
威尔特

9

我有以下示例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正文,实际上可以看到我们上传的文件的元数据和内容。


6

HTTP消息可能具有在标头行之后发送的数据主体。在响应中,这是将请求的资源返回给客户端的地方(消息正文的最常用用法),如果有错误,也可能是解释性文本。在请求中,将用户输入的数据或上载的文件发送到服务器。

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.