其中请求报文中的开始行和首部行包含了常见的各类信息,好比http协议版本,方法(GET/POST),accept-language,cookie等等。 而’实体主体’通常在post中使用,好比咱们用表单上传文件,文件数据就是在这个’实体主体’当中.php
整体上来讲,对于post文件上传这样的过程,主要有如下几个部分:java
获取http请求报文肿的头部信息,咱们能够从中得到是否为POST方法,实体主体的总大小,边界字符串等,这些对于实体主体数据的解析都是很是重要的node
获取POST数据(实体主体)服务器
对POST数据进行解析cookie
将数据写入文件数据结构
获取http请求报文头部信息闭包
利用nodejs中的 http.ServerRequest中获取1):app
request.method异步
用来标识请求类型函数
request.headers
其中咱们关心两个字段:
content-type 包含了表单类型和边界字符串(下面会介绍)信息。
content-length post数据的长度
get请求的headers中没有content-type这个字段
post 的 content-type 有两种
application/x-www-form-urlencoded
这种就是通常的文本表单用post传地数据,只要将获得的data用querystring解析下就能够了
multipart/form-data
文件表单的传输,也是本文介绍的重点
前面已经说过,post数据的传输是可能分包的,所以必然是异步的。post数据的接受过程以下:
var postData = ''; request.addListener("data", function(postDataChunk) { // 有新的数据包到达就执行 postData += postDataChunk; console.log("Received POST data chunk '"+ postDataChunk + "'."); }); request.addListener("end", function() { // 数据传输完毕 console.log('post data finish receiving: ' + postData ); });
注意,对于非文件post数据,上面以字符串接收是没问题的,但其实 postDataChunk 是一个 buffer 类型数据,在遇到二进制时,这样的接受方式存在问题。
在解析POST数据以前,先介绍一下post数据的格式:
multipart/form-data类型的post数据
例如咱们有表单以下
<FORM action="http://server.com/cgi/handle" enctype="multipart/form-data" method="post"> <P> What is your name? <INPUT type="text" name="submit-name"><BR> What files are you sending? <INPUT type="file" name="files"><BR> <INPUT type="submit" value="Send"> <INPUT type="reset"> </FORM>
若用户在text字段中输入‘Neekey’,而且在file字段中选择文件‘text.txt’,那么服务器端收到的post数据以下:
--AaB03x Content-Disposition: form-data; name="submit-name" Neekey --AaB03x Content-Disposition: form-data; name="files"; filename="file1.txt" Content-Type: text/plain ... contents of file1.txt ... --AaB03x--
若file字段为空:
--AaB03x Content-Disposition: form-data; name="submit-name" Neekey --AaB03x Content-Disposition: form-data; name="files"; filename="" Content-Type: text/plain --AaB03x--
若将file 的 input修改成能够多个文件一块儿上传:
<FORM action="http://server.com/cgi/handle" enctype="multipart/form-data" method="post"> <P> What is your name? <INPUT type="text" name="submit-name"><BR> What files are you sending? <INPUT type="file" name="files" multiple="multiple"><BR> <INPUT type="submit" value="Send"> <INPUT type="reset"> </FORM>
那么在text中输入‘Neekey’,并在file字段中选中两个文件’a.jpg’和’b.jpg’后:
--AaB03x Content-Disposition: form-data; name="submit-name" Neekey --AaB03x Content-Disposition: form-data; name="files"; filename="a.jpg" Content-Type: image/jpeg /* data of a.jpg */ --AaB03x Content-Disposition: form-data; name="files"; filename="b.jpg" Content-Type: image/jpeg /* data of b.jpg */ --AaB03x--// 能够发现 两个文件数据部分,他们的name值是同样的
简单总结下post数据的规则
不一样字段数据之间以边界字符串分隔;
每一行数据用”CR LF”(\r\n)分隔;
数据以 边界分割符 后面加上 –结尾,如:
每一个字段数据的header信息(content-disposition/content-type)和字段数据以一个空行分隔:
--boundary\r\n // 注意,如上面的headers的例子,分割字符串应该是 ------WebKitFormBoundaryuP1WvwP2LyvHpNCi\r\n
必须使用buffer来进行post数据的解析
利用文章一开始的方法(data += chunk, data为字符串 ),能够利用字符串的操做,轻易地解析出各自端的信息,可是这样有两个问题:
文件的写入须要buffer类型的数据
二进制buffer转化为string,并作字符串操做后,起索引和字符串是不一致的(若原始数据就是字符串,一致),所以是先将不总的buffer数据的toString()复制给一个字符串,再利用字符串解析出个数据的start,end位置这样的方案也是不可取的。
利用边界字符串来分割各字段数据
每一个字段数据中,使用空行(\r\n\r\n)来分割字段信息和字段数据
全部的数据都是以\r\n分割
利用上面的方法,咱们以某种方式肯定了数据在buffer中的start和end,利用buffer.splice( start, end ) 即可以进行文件写入了
比较简单,使用 File System 模块.
var fs = new require( 'fs' ).writeStream, file = new fs( filename ); fs.write( buffer, function(){ fs.end(); });
file.js主要是封装了文件的写操做
模块的主体部分
封装了对于POST数据的分段读取与解析的方法
封装了对于GET数据的解析
与我上面提到的思路不同,node-formidable是边接受数据边进行解析。
上面那种方式是每一次有数据包到达后, 添加到buffer中,等全部数据都到齐后,再对数据进行解析.这种方式,在每次数据包到达的间隙是空闲的.
第二种方式使用边接收边解析的方式,对于大文件来讲,能大大提高效率.
模块的核心文件主要是 multipart_parser.js 和 incoming_from.js 两个文件, 宏观上, multipartParser 用于解析数据, 好比给定一个buffer, 将在解析的过程当中调用相应的回调函数.好比解析到字段数据的元信息(文件名,文件类型等), 将会使用 this.onHeaderField( buffer, start, end ) 这样的形式来传输信息. 而这些方法的具体实现则是在 incoming_form.js 文件中实现的. 下面着重对这两个文件的源码进行分析
这个模块是POST数据接受的核心。起核心思想是对每一个接受到的partData进行解析,并触发相应时间,因为每次write方法的调用都将产生内部私有方法,因此partData将会被传送到各个触发事件当中,而触发事件(即对于partData的具体处理)的具体实现则是在incoming_form中实现,从这一点来讲,两个模块是高度耦合的。
multipart_form 的源码读起来会比较吃力。必须在对post数据结构比较清楚的状况下,在看源码。
源码主要是四个部分:
全局变量(闭包内)
构造函数
初始化函数(initWithBoundary)
解析函数(write)
其中全局变量,构造函数比较简单。
初始化函数用 用传进的 边界字符串 构造boundary的buffer,主要用于在解析函数中作比较。 下面主要介绍下解析函数
make( name )
将当前索引(对buffer的遍历)复制给 this[ name ]. 这个方法就是作标记,用于记录一个数据段在buffer中的开始位置
callback( name , buffer, start, end )
调用this的onName方法,并传入buffer和start以及end三个参数。 好比当文件post数据中的文件部分的数据解析完毕,则经过callback( ‘partData’, buffer, start, end ) 将该数据段的首尾位置和buffer传递给 this.onPartData 方法,作进一步处理。
dataCallback( name, clear )
前面的callback,若是不看后面的三个参数,其本质不过是一个调用某个方法的桥接函数。而dataCallback则是对callback的一个封装,他将start和end传递给callback。
从源码中能够看到,start是经过mark(name)的返回值得到,而end则多是当前遍历到的索引或者是buffer的末尾。
所以dataCallback被调用有二种状况:
在解析的数据部分的末尾在当前buffer的内部,这个时候mark记录的开始点和当前遍历到的i这个区段就是须要的数据,所以start = mark(name), end = i, 而且因为解析结束,须要将mark清除掉。
在当前buffer内,解析的数据部分还没有解析完毕(剩下的内容在下一个buffer里),所以start = mark(name), end = buffer.length
解析的主要部分是对buffer进行遍历,而后对于每个字符,都根据当前的状态进行switch的各个case进行操做。
switch的每个case都是一个解析状态。
具体看源码和注释,而后对照post的数据结构就会比较清楚。
其中 在状态:S.PART_DATA 这边,node-formidable作了一些处理,应该是针对 文章一开始介绍post数据格式中提到的 二级边界字符串 类型的数据处理。我没有深究,有兴趣的能够再研究下。