<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
<channel>
<title><![CDATA[向东博客 专注WEB应用 构架之美 --- 构架之美，在于尽态极妍 | 应用之美，在于药到病除]]></title> 
<link>http://jackxiang.com/index.php</link> 
<description><![CDATA[赢在IT，Playin' with IT,Focus on Killer Application,Marketing Meets Technology.]]></description> 
<language>zh-cn</language> 
<copyright><![CDATA[向东博客 专注WEB应用 构架之美 --- 构架之美，在于尽态极妍 | 应用之美，在于药到病除]]></copyright>
<item>
<link>http://jackxiang.com/post//</link>
<title><![CDATA[模拟HTML表单上传文件（RFC 1867）]]></title> 
<author>jack &lt;xdy108@126.com&gt;</author>
<category><![CDATA[WEB2.0]]></category>
<pubDate>Mon, 20 Jun 2011 04:54:58 +0000</pubDate> 
<guid>http://jackxiang.com/post//</guid> 
<description>
<![CDATA[ 
	　如今使用HTTP协议定制API已经是十分常见的事情，在普通的GET和POST请求中传递些参数估计人人都会，但是如果我们需要上传文件呢？如果只是传递单个文件，那么将数据流POST给服务器端即可。但如果需要上传多个文件，或是在文件之外需要附带一些信息，那么又该怎么做呢？之前我遇到过一些朋友是这么打算的，他们说，不如就把文件流转化为文本，然后把它当作一个普通的字段传递。这么做自然可以“实现功能”，但缺点也很多。首先，将二进制流转化为文本会增大体积（例如最常见的BASE64编码会增大1/3的数据量）；其次，既然互联网上存在相关的协议，又为何要自定义一套规则呢？其实这便是《RFC 1867 - Form-based File Upload in HTML》，它是我们用HTML表单上传文件时使用的传输协议，虽然十分常用，但似乎了解它的人并不多。<br/><br/>　　普通POST操作<br/>　　说起HTML表单，大家绝对不会陌生。例如下面这样的HTML表单：<br/><br/><div class="code">&lt;form action=&quot;http://www.baidu.com/&quot; method=&quot;post&quot;&gt; <br/>&lt;input type=&quot;text&quot; name=&quot;myText1&quot; /&gt;&lt;br /&gt;&nbsp;&nbsp;<br/>&lt;input type=&quot;text&quot; name=&quot;myText2&quot; /&gt;&lt;br /&gt;<br/>&lt;input type=&quot;submit&quot; /&gt;&lt;/form&gt;</div><br/>提交时会向服务器端发出这样的数据（已经去除部分不相关的头信息）：<br/><div class="code"><br/>POST http://www.baidu.com/ HTTP/1.1<br/>Host: www.baidu.com<br/>Content-Length: 74<br/>Content-Type: application/x-www-form-urlencoded<br/><br/>myText1=hello+world&amp;myText2=%E4%BD%A0%E5%A5%BD%E4%B8%96%E7%95%8C<br/><br/><br/></div><br/>&nbsp;&nbsp;对于普通的HTML POST表单，它会在头信息里使用Content-Length注明内容长度。头信息每行一条，空行之后便是Body，即“内容”。此外，我们可以发现它的Content-Type是application/x-www-form-urlencoded，这意味着消息内容会经过URL编码，就像在GET请求时URL里的Query String那样。在上面的例子中，myText1里的空格被编码为加号，而myText2，您看得出这是“你好世界”这四个汉字吗？<br/>　使用POST上传文件<br/>　　不过之前的HTML表单是无法上传文件的，因此RFC 1867应运而生，它的目的便是让HTML表单可以提交文件。它对HTML表单的扩展主要是：<br/><br/>•为input标记的type属性增加一个file选项。 <br/>•在POST情况下，为form标记的enctype属性定义默认值为application/x-www-form-urlencoded。 <br/>•为form标记的enctype属性增加multipart/form-data选项。 <br/>　　于是，如果我们要使用HTML表单提交文件，则可以使用如下定义：<br/><div class="code"><br/>&lt;form action=&quot;http://www.baidu.com/&quot; method=&quot;post&quot; enctype=&quot;multipart/form-data&quot;&gt;<br/>&lt;input type=&quot;text&quot; name=&quot;myText&quot; /&gt;&lt;br /&gt;<br/>&lt;input type=&quot;file&quot; name=&quot;upload1&quot; /&gt;&lt;br /&gt;<br/>&lt;input type=&quot;file&quot; name=&quot;upload2&quot; /&gt;&lt;br /&gt;<br/>&lt;input type=&quot;submit&quot; /&gt;<br/>&lt;/form&gt;<br/><br/><br/></div><br/>　　为了实验所需，我们创建两个文件file1.txt和file2.txt，内容分别为“This is file1.”及“This is file2, it&#039;s bigger.”。在文本框里写上“hello world”，并选择这两个文件，提交，则会看到浏览器传递了如下数据：<br/><br/><br/><div class="code"><br/>POST http://www.baidu.com/ HTTP/1.1<br/>Host: www.baidu.com<br/>Content-Length: 495<br/>Content-Type: multipart/form-data; boundary=---------------------------7db2d1bcc50e6e<br/><br/>-----------------------------7db2d1bcc50e6e<br/>Content-Disposition: form-data; name=&quot;myText&quot;<br/><br/>hello world<br/>-----------------------------7db2d1bcc50e6e<br/>Content-Disposition: form-data; name=&quot;upload1&quot;; filename=&quot;C:&#92;file1.txt&quot;<br/>Content-Type: text/plain<br/><br/>This is file1.<br/>-----------------------------7db2d1bcc50e6e<br/>Content-Disposition: form-data; name=&quot;upload2&quot;; filename=&quot;C:&#92;file2.txt&quot;<br/>Content-Type: text/plain<br/><br/>This is file2, it&#039;s longer.<br/>-----------------------------7db2d1bcc50e6e--<br/><br/><br/></div><br/><br/>　这段内容比较有趣，值得细细观察。首先，第一个空行之前自然还是HTTP头，之后则是Body，而此时的Body也比之前要复杂一些。根据RFC 1867定义，我们需要选择一段数据作为“分割边界”，这个“边界数据”不能在内容其他地方出现，一般来说使用一段从概率上说“几乎不可能”的数据即可。例如，上面这段数据使用的是IE 9，而我在Chrome下则是这样的：<br/><br/><br/><div class="code">POST http://www.baidu.com/ HTTP/1.1<br/>Host: www.baidu.com<br/>Content-Length: 473<br/>Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryW49oa00LU29E4c5U<br/><br/>------WebKitFormBoundaryW49oa00LU29E4c5U<br/>Content-Disposition: form-data; name=&quot;myText&quot;<br/><br/>hello world<br/>------WebKitFormBoundaryW49oa00LU29E4c5U<br/>Content-Disposition: form-data; name=&quot;upload1&quot;; filename=&quot;file1.txt&quot;<br/>Content-Type: text/plain<br/><br/>This is file1.<br/>------WebKitFormBoundaryW49oa00LU29E4c5U<br/>Content-Disposition: form-data; name=&quot;upload2&quot;; filename=&quot;file2.txt&quot;<br/>Content-Type: text/plain<br/><br/>This is file2, it&#039;s bigger.<br/>------WebKitFormBoundaryW49oa00LU29E4c5U--<br/></div><br/><br/>　　很显然它们两个选择了不同的数据“模式”作为边界——事实上，浏览器提交两次数据时，使用的边界也可能不会相同，这都没有问题。<br/><br/>　　选择了边界之后，便会将它放在头部的Content-Type里传递给服务器端，实际需要传递的数据便可以分割为“段”，每段便是“一项”数据。从上面的内容中大家应该都能看出数据传输的规范，因此便不做细谈了。只强调几点：<br/><br/>&nbsp;&nbsp;&nbsp;&nbsp;数据均无需额外编码，直接传递即可，例如您可以看出上面的示例中的“空格”均没有变成加号。至于这里您可以看到清晰地文字内容，是因为我们上传了仅仅包含可视ASCII码的文本文件，如果您上传一个普通的文件，例如图片，捕获到的数据则几乎完全不可读了。<br/>&nbsp;&nbsp;&nbsp;&nbsp;IE和Chrome在filename的选择策略上有所不同，前者是文件的完整路径，而后者则仅仅是文件名。<br/>&nbsp;&nbsp;&nbsp;&nbsp;数据内容以两条横线结尾，并同样以一个换行结束。在网络协议中一般都以连续的CR、LF（即&#92;r、&#92;n，或0x0D、Ox0A）字符作为换行，这与Windows的标准一致。如果您使用其他操作系统，则需要考虑它们的换行符。<br/><br/>　　实现<br/><br/>　　了解上述策略之后，使用编程来实现文件上传也是顺理成章的事情，例如我这里便编写了一段简单的代码实现这一功能。<br/><br/>　　首先，我们定义一个Part类，表示每“段”，它的Write方法会写入整段数据。每段数据分为Header和Body两部分，使用WriteHeader和WriteBody两个抽象方法写入：<br/><br/><br/><div class="code">public abstract class Part<br/>&#123;<br/>protected abstract void WriteHeader(StreamWriter writer);<br/>protected abstract void WriteBody(StreamWriter writer);<br/><br/>public void Write(StreamWriter writer)<br/>&#123;<br/>this.WriteHeader(writer);<br/>writer.WriteLine();<br/>this.WriteBody(writer);<br/>&#125;<br/>&#125;</div><br/><br/>接着便是表示普通字段的NormalPart和文件上传得FilePart：<br/><br/><br/><div class="code">public class FilePart : Part<br/>&#123;<br/>public string Name &#123; get; set; &#125;<br/>public string FilePath &#123; get; set; &#125;<br/><br/>protected override void WriteHeader(StreamWriter writer)<br/>&#123;<br/>writer.WriteLine(<br/>&quot;Content-Disposition: form-data; name=&#92;&quot;&#123;0&#125;&#92;&quot;; filename=&#92;&quot;&#123;1&#125;&#92;&quot;&quot;,<br/>this.Name,<br/>Path.GetFileName(this.FilePath));<br/><br/>writer.WriteLine(&quot;Content-Type: application/octet-stream&quot;);<br/>&#125;<br/><br/>protected override void WriteBody(StreamWriter writer)<br/>&#123;<br/>var data = File.ReadAllBytes(this.FilePath);<br/>writer.Flush();<br/>writer.BaseStream.Write(data, 0, data.Length);<br/>writer.WriteLine();<br/>&#125;<br/>&#125;</div><br/><br/><br/>最后便是统一写入各段的Write方法，我在这里使用新建的GUID作为“边界”：<br/><br/><div class="code">static void Write(StreamWriter writer, IEnumerable&lt;Part&gt; parts)<br/>&#123;<br/>var guidBytes = Guid.NewGuid().ToByteArray();<br/>var boundary = &quot;----------------&quot; + Convert.ToBase64String(guidBytes);<br/><br/>　　foreach (var p in parts)<br/>　　&#123;<br/>　　　　writer.WriteLine(boundary);<br/>　　　　p.Write(writer);<br/>　　&#125;<br/><br/>　　writer.WriteLine(boundary + &quot;--&quot;);<br/>&#125;</div><br/><br/><br/>　其实就是这么简单。不过在实际情况中可能会复杂一些。例如，由于HTTP协议需要先发送头信息，因此我们需要提前计算出Content-Length再传输所有内容，不过我相信这对您来说也不会是件难事。<br/>　　其他<br/><br/>　　世界上已经有了足够多的协议，在我看来在绝大部分情况下都无所谓使用自定义的协议。协议在制定时，往往也会考虑到安全、性能等诸多方面，有时候我们自己所谓的“顾虑”其理由也并不充分。更重要的是，使用现成的协议，我们往往都有现成的实现，对于开发和测试都会有很大帮助。<br/><br/>　　RFC 1867是一个很简单的协议，当然再简单也不是我这短短一篇文章可以完整描述的，其中很多细节（例如在同一个“段”中上传多个文件）就要靠您自己去挖掘了。<br/><br/>来源：http://kb.cnblogs.com/page/95545/<br/>来源：http://blog.zhaojie.me/2011/03/html-form-file-uploading-programming.html<br/>参考：《RFC 1867 - Form-based File Upload in HTML》，http://www.faqs.org/rfcs/rfc1867.html<br/>换行符，http://en.wikipedia.org/wiki/Newline<br/>RFC 1867应运而生，http://www.faqs.org/rfcs/rfc1867.html<br/>Tags - <a href="http://jackxiang.com/tags/%25E4%25B8%258A%25E4%25BC%25A0%25E6%2596%2587%25E4%25BB%25B6/" rel="tag">上传文件</a>
]]>
</description>
</item><item>
<link>http://jackxiang.com/post//#blogcomment</link>
<title><![CDATA[[评论] 模拟HTML表单上传文件（RFC 1867）]]></title> 
<author> &lt;user@domain.com&gt;</author>
<category><![CDATA[评论]]></category>
<pubDate>Thu, 01 Jan 1970 00:00:00 +0000</pubDate> 
<guid>http://jackxiang.com/post//#blogcomment</guid> 
<description>
<![CDATA[ 
	
]]>
</description>
</item>
</channel>
</rss>