nodejs-post文件上传原理详解

来源:互联网 发布:linux 复制 当前目录 编辑:程序博客网 时间:2024/04/30 07:56

基础知识

  • 浅谈HTTP中Get与Post的区别
  • HTTP请求报文格式:

简单介绍下,如下图:

其中请求报文中的开始行和首部行包含了常见的各种信息,比如http协议版本,方法(GET/POST),accept-language,cookie等等。 而’实体主体’一般在post中使用,比如我们用表单上传文件,文件数据就是在这个’实体主体’当中。

引子

写这篇教程的起因是因为在学习nodejs的过程中,想要自己实现一些文件上传的功能,于是不得不去研究POST。

如果你写过一点PHP,那么你肯定记得,在PHP里面,进行文件上传的时候,我们可以直接使用全局变量 $_FILE['name' ]来获取已经被临时存储的文件信息。

但是实际上,POST数据实体,会根据数据量的大小进行分包传送,然后再从这些数据包里面分析出哪些是文件的元数据,那些是文件本身的数据。

PHP是底层做了封装,但是在nodejs里面,这个看似常见的功能却是需要自己来实现的。这篇文章主要就是介绍如何使用nodejs来解析post数据。

正文

总体上来说,对于post文件上传这样的过程,主要有以下几个部分:

  • 获取http请求报文肿的头部信息,我们可以从中获得是否为POST方法,实体主体的总大小,边界字符串等,这些对于实体主体数据的解析都是非常重要的
  • 获取POST数据(实体主体)
  • 对POST数据进行解析
  • 将数据写入文件

获取http请求报文头部信息

利用nodejs中的 http.ServerRequest中获取 :

  • request.method

用来标识请求类型

  • request.headers

其中我们关心两个字段:

  • content-type

包含了表单类型和边界字符串(下面会介绍)信息。

  • content-length

post数据的长度

关于content-type

  • get请求的headers中没有content-type这个字段
  • post 的 content-type 有两种
    1. application/x-www-form-urlencoded
      这种就是一般的文本表单用post传地数据,只要将得到的data用querystring解析下就可以了
    2. multipart/form-data
      文件表单的传输,也是本文介绍的重点

获取POST数据

前面已经说过,post数据的传输是可能分包的,因此必然是异步的。post数据的接受过程如下:

  1. var postData = '';
  2. request.addListener("data", function(postDataChunk) { // 有新的数据包到达就执行
  3. postData += postDataChunk;
  4. console.log("Received POST data chunk '"+
  5. postDataChunk + "'.");
  6. });
  7.  
  8. request.addListener("end", function() { // 数据传输完毕
  9. console.log('post data finish receiving: ' + postData );
  10. });

注意,对于非文件post数据,上面以字符串接收是没问题的,但其实 postDataChunk 是一个 buffer 类型数据,在遇到二进制时,这样的接受方式存在问题。

POST数据的解析(multipart/form-data)

在解析POST数据之前,先介绍一下post数据的格式:

multipart/form-data类型的post数据

例如我们有表单如下

  1. <FORM action="http://server.com/cgi/handle"
  2. enctype="multipart/form-data"
  3. method="post">
  4. <P>
  5. What is your name? <INPUT type="text" name="submit-name"><BR>
  6. What files are you sending? <INPUT type="file" name="files"><BR>
  7. <INPUT type="submit" value="Send"> <INPUT type="reset">
  8. </FORM>

若用户在text字段中输入‘Neekey’,并且在file字段中选择文件‘text.txt’,那么服务器端收到的post数据如下:

  1. --AaB03x
  2. Content-Disposition: form-data; name="submit-name"
  3.  
  4. Neekey
  5. --AaB03x
  6. Content-Disposition: form-data; name="files"; filename="file1.txt"
  7. Content-Type: text/plain
  8.  
  9. ... contents of file1.txt ...
  10. --AaB03x--

若file字段为空:

  1. --AaB03x
  2. Content-Disposition: form-data; name="submit-name"
  3.  
  4. Neekey
  5. --AaB03x
  6. Content-Disposition: form-data; name="files"; filename=""
  7. Content-Type: text/plain
  8.  
  9. --AaB03x--

若将file 的 input修改为可以多个文件一起上传:

  1. <FORM action="http://server.com/cgi/handle"
  2. enctype="multipart/form-data"
  3. method="post">
  4. <P >
  5. What is your name? <INPUT type="text" name="submit-name"><BR>
  6. What files are you sending? <INPUT type="file" name="files" multiple="multiple"><BR>
  7. <INPUT type="submit" value="Send"> <INPUT type="reset">
  8. </FORM>

那么在text中输入‘Neekey’,并在file字段中选中两个文件’a.jpg’和’b.jpg’后:

  1. --AaB03x
  2. Content-Disposition: form-data; name="submit-name"
  3.  
  4. Neekey
  5. --AaB03x
  6. Content-Disposition: form-data; name="files"; filename="a.jpg"
  7. Content-Type: image/jpeg
  8.  
  9. /* data of a.jpg */
  10. --AaB03x
  11. Content-Disposition: form-data; name="files"; filename="b.jpg"
  12. Content-Type: image/jpeg
  13.  
  14. /* data of b.jpg */
  15. --AaB03x--
  16.  
  17. // 可以发现 两个文件数据部分,他们的name值是一样的
数据规则

简单总结下post数据的规则

  1. 不同字段数据之间以边界字符串分隔:

    1. --boundary\r\n // 注意,如上面的headers的例子,分割字符串应该是 ------WebKitFormBoundaryuP1WvwP2LyvHpNCi\r\n
  2. 每一行数据用”CR LF”(\r\n)分隔
  3. 数据以 边界分割符 后面加上 –结尾,如:

    1. ------WebKitFormBoundaryuP1WvwP2LyvHpNCi--\r\n
  4. 每个字段数据的header信息(content-disposition/content-type)和字段数据以一个空行分隔:

更加详细的信息可以参考W3C的文档Forms,不过文档中对于 multiple=“multiple” 的文件表单的post数据格式使用了二级边界字符串,但是在实际测试中,multiple类型的表单和多个单文件表单上传数据的格式一致,有更加清楚的可以交流下:

  1. If the user selected a second (image) file "file2.gif", the user agent might construct the parts as follows:
  2.  
  3. Content-Type: multipart/form-data; boundary=AaB03x
  4.  
  5. --AaB03x
  6. Content-Disposition: form-data; name="submit-name"
  7.  
  8. Larry
  9. --AaB03x
  10. Content-Disposition: form-data; name="files"
  11. Content-Type: multipart/mixed; boundary=BbC04y
  12.  
  13. --BbC04y
  14. Content-Disposition: file; filename="file1.txt"
  15. Content-Type: text/plain
  16.  
  17. ... contents of file1.txt ...
  18. --BbC04y
  19. Content-Disposition: file; filename="file2.gif"
  20. Content-Type: image/gif
  21. Content-Transfer-Encoding: binary
  22.  
  23. ...contents of file2.gif...
  24. --BbC04y--
  25. --AaB03x--

数据解析基本思路

  • 必须使用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 模块(nodejs的文件处理,我很弱很弱….)

  1. var fs = new require( 'fs' ).writeStream,
  2. file = new fs( filename );
  3. fs.write( buffer, function(){
  4. fs.end();
  5. });

node-formidable模块源码分析

node-formidable是比较流行的处理表单的nodejs模块。github主页

项目中的lib目录

  1. lib
  2. |-file.js
  3. |-incoming_form.js
  4. |-index.js
  5. |-multipart_parser.js
  6. |-querystring_parser.js
  7. |-util.js

各文件说明

file.js

file.js主要是封装了文件的写操作

incoming_from.js

模块的主体部分

multipart_parser.js

封装了对于POST数据的分段读取与解析的方法

querystring_parser.js

封装了对于GET数据的解析

总体思路

与我上面提到的思路不一样,node-formidable是边接受数据边进行解析。

上面那种方式是每一次有数据包到达后, 添加到buffer中,等所有数据都到齐后,再对数据进行解析.这种方式,在每次数据包到达的间隙是空闲的.

第二种方式使用边接收边解析的方式,对于大文件来说,能大大提升效率.

模块的核心文件主要是 multipart_parser.js 和 incoming_from.js 两个文件, 宏观上, multipartParser 用于解析数据, 比如给定一个buffer, 将在解析的过程中调用相应的回调函数.比如解析到字段数据的元信息(文件名,文件类型等), 将会使用 this.onHeaderField( buffer, start, end ) 这样的形式来传输信息. 而这些方法的具体实现则是在 incoming_form.js 文件中实现的. 下面着重对这两个文件的源码进行分析 

multipart_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被调用有二种情况:

  1. 在解析的数据部分的末尾在当前buffer的内部,这个时候mark记录的开始点和当前遍历到的i这个区段就是需要的数据,因此start = mark(name), end = i, 并且由于解析结束,需要将mark清除掉。
  2. 在当前buffer内,解析的数据部分尚未解析完毕(剩下的内容在下一个buffer里),因此start = mark(name), end = buffer.length

解析的主要部分

解析的主要部分是对buffer进行遍历,然后对于每一个字符,都根据当前的状态进行switch的各个case进行操作。

switch的每一个case都是一个解析状态。

具体看源码和注释,然后对照post的数据结构就会比较清楚。

其中 在状态:S.PART_DATA 这边,node-formidable做了一些处理,应该是针对 文章一开始介绍post数据格式中提到的 二级边界字符串 类型的数据处理。我没有深究,有兴趣的可以再研究下。

  1. var Buffer = require('buffer').Buffer,
  2. s = 0,
  3. S =
  4. { PARSER_UNINITIALIZED: s++, // 解析尚未初始化
  5. START: s++, // 开始解析
  6. START_BOUNDARY: s++, // 开始找到边界字符串
  7. HEADER_FIELD_START: s++, // 开始解析到header field
  8. HEADER_FIELD: s++,
  9. HEADER_VALUE_START: s++, // 开始解析到header value
  10. HEADER_VALUE: s++,
  11. HEADER_VALUE_ALMOST_DONE: s++, // header value 解析完毕
  12. HEADERS_ALMOST_DONE: s++, // header 部分 解析完毕
  13. PART_DATA_START: s++, // 开始解析 数据段
  14. PART_DATA: s++,
  15. PART_END: s++,
  16. END: s++,
  17. },
  18.  
  19. f = 1,
  20. F =
  21. { PART_BOUNDARY: f,
  22. LAST_BOUNDARY: f *= 2,
  23. },
  24.  
  25. /* 一些字符的ASCII值 */
  26. LF = 10,
  27. CR = 13,
  28. SPACE = 32,
  29. HYPHEN = 45,
  30. COLON = 58,
  31. A = 97,
  32. Z = 122,
  33.  
  34. /* 将所有大写小写字母的ascii一律转化为小写的ascii值 */
  35. lower = function(c) {
  36. return c | 0x20;
  37. };
  38.  
  39. for (var s in S) {
  40. exports[s] = S[s];
  41. }
  42.  
  43. /* 构造函数 */
  44. function MultipartParser() {
  45. this.boundary = null;
  46. this.boundaryChars = null;
  47. this.lookbehind = null;
  48. this.state = S.PARSER_UNINITIALIZED;
  49.  
  50. this.index = null;
  51. this.flags = 0;
  52. };
  53. exports.MultipartParser = MultipartParser;
  54.  
  55. /* 给定边界字符串以初始化 */
  56. MultipartParser.prototype.initWithBoundary = function(str) {
  57. this.boundary = new Buffer(str.length+4);
  58. this.boundary.write('\r\n--', 'ascii', 0);
  59. this.boundary.write(str, 'ascii', 4);
  60. this.lookbehind = new Buffer(this.boundary.length+8);
  61. this.state = S.START;
  62.  
  63. this.boundaryChars = {};
  64. for (var i = 0; i < this.boundary.length; i++) {
  65. this.boundaryChars[this.boundary[i]] = true;
  66. }
  67. };
  68.  
  69. /* 每个数据段到达时的处理函数 */
  70. MultipartParser.prototype.write = function(buffer) {
  71. var self = this,
  72. i = 0,
  73. len = buffer.length,
  74. prevIndex = this.index,
  75. index = this.index,
  76. state = this.state,
  77. flags = this.flags,
  78. lookbehind = this.lookbehind,
  79. boundary = this.boundary,
  80. boundaryChars = this.boundaryChars,
  81. boundaryLength = this.boundary.length,
  82. boundaryEnd = boundaryLength - 1,
  83. bufferLength = buffer.length,
  84. c,
  85. cl,
  86.  
  87. /* 标记了name这个标记点的buffer偏移 */
  88. mark = function(name) {
  89. self[name+'Mark'] = i;
  90. },
  91. /* 清除标记 */
  92. clear = function(name) {
  93. delete self[name+'Mark'];
  94. },
  95. /* 回调函数,将调用onName,并传入对应的buffer和对应的offset区间,可知这些回调都在 incoming_form 模块中被具体实现 */
  96. callback = function(name, buffer, start, end) {
  97. if (start !== undefined && start === end) {
  98. return;
  99. }
  100.  
  101. var callbackSymbol = 'on'+name.substr(0, 1).toUpperCase()+name.substr(1);
  102. if (callbackSymbol in self) {
  103. self[callbackSymbol](buffer, start, end);
  104. }
  105. },
  106. /* 数据回调 */
  107. dataCallback = function(name, clear) {
  108. var markSymbol = name+'Mark';
  109. if (!(markSymbol in self)) {
  110. return;
  111. }
  112.  
  113. if (!clear) {
  114. /* 传入回调的名称,buffer,buffer的开始位置(可见mark方法就是用来存储offset的),end为数据的重点 */
  115. callback(name, buffer, self[markSymbol], buffer.length);
  116. self[markSymbol] = 0;
  117. } else {
  118. /* 区别是 end 的值为i,在一个数据已经判断到达其结束位置时,就删除这个mark点,因为这个时候已经知道了这个数据的起始位置 */
  119. callback(name, buffer, self[markSymbol], i);
  120. delete self[markSymbol];
  121. }
  122. };
  123.  
  124. /* 对buffer逐个字节遍历,进行解析判断,并触发相应事件 */
  125. for (i = 0; i < len; i++) {
  126. c = buffer[i];
  127. switch (state) {
  128. case S.PARSER_UNINITIALIZED:
  129. return i;
  130. case S.START:
  131. index = 0;
  132. state = S.START_BOUNDARY;
  133. case S.START_BOUNDARY:
  134. /**
  135. * 对于边界的判断 ====
  136. * 这里看了很多次,一直很迷惑
  137. * 因为首先:除了最后一个边界字符串,其他(包括第一个边界字符串)都是这个样子:
  138. * --boundary\r\t
  139. * 但是在初始化函数中,对于this.boundary的赋值是这样的:
  140. * \r\n--boundary
  141. *
  142. * 但是仔细看下面部分的代码,作者只是为了初始化方便,或者出于其他的考虑
  143. */
  144.  
  145. /**
  146. * 判断是否到了边界字符串的结尾\r处
  147. * 如果当前字符与 \r不匹配,则return ,算是解析出错了
  148. * 注意,虽然this.boundary != --boundary\r\n 但是长度是一致的,因此这里的判断是没有问题的
  149. */
  150. if (index == boundary.length - 2) {
  151. if (c != CR) {
  152. return i;
  153. }
  154. index++;
  155. break;
  156. }
  157. /**
  158. * 判断是否到了边界字符串的结尾\n处
  159. * 如果是,则置index = 0
  160. * 回调 partBegin,并将状态设置为还是header 的 field 信息的读取状态
  161. */
  162. else if (index - 1 == boundary.length - 2) {
  163. if (c != LF) {
  164. return i;
  165. }
  166. index = 0;
  167. callback('partBegin');
  168. state = S.HEADER_FIELD_START;
  169. break;
  170. }
  171.  
  172. /**
  173. * 除了boundary的最后的\r\n外,其他字符都要进行检查
  174. * 注意这里用的是 index+2 进行匹配,证实了
  175. * 作者是因为某种意图将 boundary设置成\r\n--boundary的形式
  176. * (其实这种形式也没有错,对处第一个边界字符串外的其他边界字符串,这个形式都是适用的)
  177. */
  178. if (c != boundary[index+2]) {
  179. return i;
  180. }
  181. index++;
  182. break;
  183. /* 对于header field的扫描开始,这里记录了标记了开始点在buffer中的位置 */
  184. case S.HEADER_FIELD_START:
  185. state = S.HEADER_FIELD;
  186. mark('headerField');
  187. index = 0;
  188. /* header field的扫描过程 */
  189. case S.HEADER_FIELD:
  190. /* 这里是header和data中间的那个空行,所以第一个字符就是\r */
  191. if (c == CR) {
  192. clear('headerField');
  193. state = S.HEADERS_ALMOST_DONE;
  194. break;
  195. }
  196.  
  197. index++;
  198. /* 如果是小横线 '-' 比如在 Content-Disposition */
  199. if (c == HYPHEN) {
  200. break;
  201. }
  202. /**
  203. * 如果是冒号,那么说明 field结束了
  204. * dataCallback,注意第二个参数为true,则它将调用this.onHeaderField,并且将buffer,start(之前mark(headerField)记录的位置),end(当前的i)传递过去,最后将这个mark清理掉
  205. * 之后进入 header value 的开始阶段
  206. */
  207. if (c == COLON) {
  208. if (index == 1) {
  209. // empty header field
  210. return i;
  211. }
  212. dataCallback('headerField', true);
  213. state = S.HEADER_VALUE_START;
  214. break;
  215. }
  216.  
  217. /**
  218. * 对于所有其他不是冒号和小横线的字符,必须为字母,否则解析结束
  219. */
  220. cl = lower(c);
  221. if (cl < A || cl > Z) {
  222. return i;
  223. }
  224. break;
  225. /**
  226. * value 的读取开始
  227. * 做记号,设置state
  228. */
  229. case S.HEADER_VALUE_START:
  230. if (c == SPACE) {
  231. break;
  232. }
  233.  
  234. mark('headerValue');
  235. state = S.HEADER_VALUE;
  236. /**
  237. * value 的分析阶段
  238. */
  239. case S.HEADER_VALUE:
  240. /**
  241. * 如果是 \r,则value结束
  242. * 同样是调用 dataCallback,参数为true
  243. * 注意这里还 callback了 headerEnd,没有给定任何参数,这里是作为一个trigger抛出一行header结束的事件
  244. */
  245. if (c == CR) {
  246. dataCallback('headerValue', true);
  247. callback('headerEnd');
  248. state = S.HEADER_VALUE_ALMOST_DONE;
  249. }
  250. break;
  251. /**
  252. * value 结束的检查,之前是检查到了 \r ,如果下一个字符不是 \n 肯定问题
  253. * 一个value结束,可能下面还是一行header,所以不会直接 header done,而是重新进入扫描 fields的阶段
  254. */
  255. case S.HEADER_VALUE_ALMOST_DONE:
  256. if (c != LF) {
  257. return i;
  258. }
  259. state = S.HEADER_FIELD_START;
  260. break;
  261. /**
  262. * 同样是先检查一下字符是否有误(看一下 case S.HEADER_FIELD 的第一个if )
  263. * 抛出headers解析完毕的事件
  264. */
  265. case S.HEADERS_ALMOST_DONE:
  266. if (c != LF) {
  267. return i;
  268. }
  269.  
  270. callback('headersEnd');
  271. state = S.PART_DATA_START;
  272. break;
  273. /**
  274. * 开始解析post数据
  275. * 设置状态,做记号
  276. */
  277. case S.PART_DATA_START:
  278. state = S.PART_DATA
  279. mark('partData');
  280. /**
  281. * 进入post数据的解析状态
  282. * 上一次设置index是在 HEADER_FIELD_START中设置为0
  283. */
  284. case S.PART_DATA:
  285.  
  286. prevIndex = index;
  287.  
  288. if (index == 0) {
  289. // boyer-moore derrived algorithm to safely skip non-boundary data
  290. i += boundaryEnd;
  291. while (i < bufferLength && !(buffer[i] in boundaryChars)) {
  292. i += boundaryLength;
  293. }
  294. i -= boundaryEnd;
  295. c = buffer[i];
  296. }
  297.  
  298. if (index < boundary.length) {
  299. if (boundary[index] == c) {
  300. if (index == 0) {
  301. dataCallback('partData', true);
  302. }
  303. index++;
  304. } else {
  305. index = 0;
  306. }
  307. } else if (index == boundary.length) {
  308. index++;
  309. if (c == CR) {
  310. // CR = part boundary
  311. flags |= F.PART_BOUNDARY;
  312. } else if (c == HYPHEN) {
  313. // HYPHEN = end boundary
  314. flags |= F.LAST_BOUNDARY;
  315. } else {
  316. index = 0;
  317. }
  318. } else if (index - 1 == boundary.length) {
  319. if (flags & F.PART_BOUNDARY) {
  320. index = 0;
  321. if (c == LF) {
  322. // unset the PART_BOUNDARY flag
  323. flags &= ~F.PART_BOUNDARY;
  324. callback('partEnd');
  325. callback('partBegin');
  326. state = S.HEADER_FIELD_START;
  327. break;
  328. }
  329. } else if (flags & F.LAST_BOUNDARY) {
  330. if (c == HYPHEN) {
  331. callback('partEnd');
  332. callback('end');
  333. state = S.END;
  334. } else {
  335. index = 0;
  336. }
  337. } else {
  338. index = 0;
  339. }
  340. }
  341.  
  342. if (index > 0) {
  343. // when matching a possible boundary, keep a lookbehind reference
  344. // in case it turns out to be a false lead
  345. lookbehind[index-1] = c;
  346. } else if (prevIndex > 0) {
  347. // if our boundary turned out to be rubbish, the captured lookbehind
  348. // belongs to partData
  349. callback('partData', lookbehind, 0, prevIndex);
  350. prevIndex = 0;
  351. mark('partData');
  352.  
  353. // reconsider the current character even so it interrupted the sequence
  354. // it could be the beginning of a new sequence
  355. i--;
  356. }
  357.  
  358. break;
  359. case S.END:
  360. break;
  361. default:
  362. return i;
  363. }
  364. }
  365. /**
  366. * 下面这三个是在当前数据段解析完全后调用的。
  367. * 如果一个数据部分(比如如field信息)已经在上面的解析过程中解析完毕,那么自然已经调用过clear方法,那下面的dataCallback将什么也不做
  368. * 否则,下面的调用将会把这次数据段中的数据部分传递到回调函数中
  369. */
  370. dataCallback('headerField');
  371. dataCallback('headerValue');
  372. dataCallback('partData');
  373.  
  374. this.index = index;
  375. this.state = state;
  376. this.flags = flags;
  377.  
  378. return len;
  379. };
  380.  
  381. MultipartParser.prototype.end = function() {
  382. if (this.state != S.END) {
  383. return new Error('MultipartParser.end(): stream ended unexpectedly');
  384. }
  385. };

incoming_form.js

上图是incoming_form解析的主要过程(文件类型),其中

parse

根据传入的requeset对象开始启动整个解析的过程

  • writeHeaders

从request对象中获取post数据长度,解析出边界字符串,用来初始化multipartParser

  • 为request对象添加监听事件

write

request对象的 ‘data’时间到达会调用该方法,而write方法实质上是调用multipartParser.write

_initMultipart

利用边界字符串初始化multipartParser,并实现在multipart_form.js中write解析方法中会触发的事件回调函数

具体细节看源码会比较清楚。

  1. if (global.GENTLY) require = GENTLY.hijack(require);
  2.  
  3. var util = require('./util'),
  4. path = require('path'),
  5. File = require('./file'),
  6. MultipartParser = require('./multipart_parser').MultipartParser,
  7. QuerystringParser = require('./querystring_parser').QuerystringParser,
  8. StringDecoder = require('string_decoder').StringDecoder,
  9. EventEmitter = require('events').EventEmitter;
  10.  
  11. function IncomingForm() {
  12. if (!(this instanceof IncomingForm)) return new IncomingForm;
  13. EventEmitter.call(this);
  14.  
  15. this.error = null;
  16. this.ended = false;
  17.  
  18. this.maxFieldsSize = 2 * 1024 * 1024; // 设置最大文件限制
  19. this.keepExtensions = false;
  20. this.uploadDir = '/tmp'; // 设置文件存放目录
  21. this.encoding = 'utf-8';
  22. this.headers = null; // post请求的headers信息
  23. // 收到的post数据类型(一般的字符串数据,还是文件)
  24. this.type = null;
  25.  
  26. this.bytesReceived = null; // 已经接受的字节
  27. this.bytesExpected = null; // 预期接受的字节
  28.  
  29. this._parser = null;
  30. this._flushing = 0;
  31. this._fieldsSize = 0;
  32. };
  33. util.inherits(IncomingForm, EventEmitter);
  34. exports.IncomingForm = IncomingForm;
  35.  
  36. IncomingForm.prototype.parse = function(req, cb) {
  37. // 每次调用都重新建立方法,用于对req和cb的闭包使用
  38. this.pause = function() {
  39. try {
  40. req.pause();
  41. } catch (err) {
  42. // the stream was destroyed
  43. if (!this.ended) {
  44. // before it was completed, crash & burn
  45. this._error(err);
  46. }
  47. return false;
  48. }
  49. return true;
  50. };
  51.  
  52. this.resume = function() {
  53. try {
  54. req.resume();
  55. } catch (err) {
  56. // the stream was destroyed
  57. if (!this.ended) {
  58. // before it was completed, crash & burn
  59. this._error(err);
  60. }
  61. return false;
  62. }
  63.  
  64. return true;
  65. };
  66.  
  67. // 记录下headers信息
  68. this.writeHeaders(req.headers);
  69.  
  70. var self = this;
  71. req
  72. .on('error', function(err) {
  73. self._error(err);
  74. })
  75. .on('aborted', function() {
  76. self.emit('aborted');
  77. })
  78. // 接受数据
  79. .on('data', function(buffer) {
  80. self.write(buffer);
  81. })
  82. // 数据传送结束
  83. .on('end', function() {
  84. if (self.error) {
  85. return;
  86. }
  87.  
  88. var err = self._parser.end();
  89. if (err) {
  90. self._error(err);
  91. }
  92. });
  93.  
  94. // 若回调函数存在
  95. if (cb) {
  96. var fields = {}, files = {};
  97. this
  98. // 一个字段解析完毕,触发事件
  99. .on('field', function(name, value) {
  100. fields[name] = value;
  101. })
  102. // 一个文件解析完毕,处罚事件
  103. .on('file', function(name, file) {
  104. files[name] = file;
  105. })
  106. .on('error', function(err) {
  107. cb(err, fields, files);
  108. })
  109. // 所有数据接收完毕,执行回调函数
  110. .on('end', function() {
  111. cb(null, fields, files);
  112. });
  113. }
  114.  
  115. return this;
  116. };
  117.  
  118. // 保存header信息
  119. IncomingForm.prototype.writeHeaders = function(headers) {
  120. this.headers = headers;
  121. // 从头部中解析数据的长度和form类型
  122. this._parseContentLength();
  123. this._parseContentType();
  124. };
  125.  
  126. IncomingForm.prototype.write = function(buffer) {
  127. if (!this._parser) {
  128. this._error(new Error('unintialized parser'));
  129. return;
  130. }
  131.  
  132. /* 累加接收到的信息 */
  133. this.bytesReceived += buffer.length;
  134. this.emit('progress', this.bytesReceived, this.bytesExpected);
  135.  
  136. // 解析数据
  137. var bytesParsed = this._parser.write(buffer);
  138. if (bytesParsed !== buffer.length) {
  139. this._error(new Error('parser error, '+bytesParsed+' of '+buffer.length+' bytes parsed'));
  140. }
  141.  
  142. return bytesParsed;
  143. };
  144.  
  145. IncomingForm.prototype.pause = function() {
  146. // this does nothing, unless overwritten in IncomingForm.parse
  147. return false;
  148. };
  149.  
  150. IncomingForm.prototype.resume = function() {
  151. // this does nothing, unless overwritten in IncomingForm.parse
  152. return false;
  153. };
  154.  
  155. /**
  156. * 开始接受数据(这个函数在headers被分析完成后调用,这个时候剩下的data还没有解析过来
  157. */
  158. IncomingForm.prototype.onPart = function(part) {
  159. // this method can be overwritten by the user
  160. this.handlePart(part);
  161. };
  162.  
  163. IncomingForm.prototype.handlePart = function(part) {
  164. var self = this;
  165.  
  166. /* post数据不是文件的情况 */
  167. if (!part.filename) {
  168. var value = ''
  169. , decoder = new StringDecoder(this.encoding);
  170. /* 有数据过来时 */
  171. part.on('data', function(buffer) {
  172. self._fieldsSize += buffer.length;
  173. if (self._fieldsSize > self.maxFieldsSize) {
  174. self._error(new Error('maxFieldsSize exceeded, received '+self._fieldsSize+' bytes of field data'));
  175. return;
  176. }
  177. value += decoder.write(buffer);
  178. });
  179.  
  180. part.on('end', function() {
  181. self.emit('field', part.name, value);
  182. });
  183. return;
  184. }
  185.  
  186. this._flushing++;
  187.  
  188. // 创建新的file实例
  189. var file = new File({
  190. path: this._uploadPath(part.filename),
  191. name: part.filename,
  192. type: part.mime,
  193. });
  194.  
  195. this.emit('fileBegin', part.name, file);
  196.  
  197. file.open();
  198.  
  199. /* 当文件数据达到,一点一点写入文件 */
  200. part.on('data', function(buffer) {
  201. self.pause();
  202. file.write(buffer, function() {
  203. self.resume();
  204. });
  205. });
  206.  
  207. // 一个文件的数据解析完毕,出发事件
  208. part.on('end', function() {
  209. file.end(function() {
  210. self._flushing--;
  211. self.emit('file', part.name, file);
  212. self._maybeEnd();
  213. });
  214. });
  215. };
  216. /**
  217. * 解析表单类型
  218. * 如果为文件表单,则解析出边界字串,初始化multipartParser
  219. */
  220. IncomingForm.prototype._parseContentType = function() {
  221. if (!this.headers['content-type']) {
  222. this._error(new Error('bad content-type header, no content-type'));
  223. return;
  224. }
  225.  
  226. // 如果是一般的post数据
  227. if (this.headers['content-type'].match(/urlencoded/i)) {
  228. this._initUrlencoded();
  229. return;
  230. }
  231.  
  232. // 如果为文件类型
  233. if (this.headers['content-type'].match(/multipart/i)) {
  234. var m;
  235. if (m = this.headers['content-type'].match(/boundary=(?:"([^"]+)"|([^;]+))/i)) {
  236. // 解析出边界字符串,并利用边界字符串初始化multipart组件
  237. this._initMultipart(m[1] || m[2]);
  238. } else {
  239. this._error(new Error('bad content-type header, no multipart boundary'));
  240. }
  241. return;
  242. }
  243.  
  244. this._error(new Error('bad content-type header, unknown content-type: '+this.headers['content-type']));
  245. };
  246.  
  247. IncomingForm.prototype._error = function(err) {
  248. if (this.error) {
  249. return;
  250. }
  251.  
  252. this.error = err;
  253. this.pause();
  254. this.emit('error', err);
  255. };
  256.  
  257. // 从 this.headers 中获取数据总长度
  258. IncomingForm.prototype._parseContentLength = function() {
  259. if (this.headers['content-length']) {
  260. this.bytesReceived = 0;
  261. this.bytesExpected = parseInt(this.headers['content-length'], 10);
  262. }
  263. };
  264.  
  265. IncomingForm.prototype._newParser = function() {
  266. return new MultipartParser();
  267. };
  268. // 初始化multipartParset 组件
  269. IncomingForm.prototype._initMultipart = function(boundary) {
  270. this.type = 'multipart';
  271.  
  272. // 实例化组件
  273. var parser = new MultipartParser(),
  274. self = this,
  275. headerField,
  276. headerValue,
  277. part;
  278.  
  279. parser.initWithBoundary(boundary);
  280. /**
  281. * 下面这些方法便是multipartParser中的callback以及dataCallback调用的函书
  282. * 当开始解析一个数据段(比如一个文件..)
  283. * 并重置相关信息
  284. */
  285. parser.onPartBegin = function() {
  286. part = new EventEmitter();
  287. part.headers = {};
  288. part.name = null;
  289. part.filename = null;
  290. part.mime = null;
  291. headerField = '';
  292. headerValue = '';
  293. };
  294. /**
  295. * 数据段的头部信息解析完毕(或者数据段的头部信息在当前接受到的数据段的尾部,并且尚未结束)
  296. * 下面的onHeaderValue和onPartData也是一样的道理
  297. */
  298. parser.onHeaderField = function(b, start, end) {
  299. headerField += b.toString(self.encoding, start, end);
  300. };
  301.  
  302. /* 数据段的头部信息value的解析过程 */
  303. parser.onHeaderValue = function(b, start, end) {
  304. headerValue += b.toString(self.encoding, start, end);
  305. };
  306.  
  307. /* header信息(一行)解析完毕,并储存起来 */
  308. parser.onHeaderEnd = function() {
  309. headerField = headerField.toLowerCase();
  310. part.headers[headerField] = headerValue;
  311.  
  312. var m;
  313. if (headerField == 'content-disposition') {
  314. if (m = headerValue.match(/name="([^"]+)"/i)) {
  315. part.name = m[1];
  316. }
  317.  
  318. if (m = headerValue.match(/filename="([^;]+)"/i)) {
  319. part.filename = m[1].substr(m[1].lastIndexOf('\\') + 1);
  320. }
  321. } else if (headerField == 'content-type') {
  322. part.mime = headerValue;
  323. }
  324.  
  325. /* 重置,准备解析下一个header信息 */
  326. headerField = '';
  327. headerValue = '';
  328. };
  329.  
  330. /* 整个headers信息解析完毕 */
  331. parser.onHeadersEnd = function() {
  332. self.onPart(part);
  333. };
  334.  
  335. /* 数据部分的解析 */
  336. parser.onPartData = function(b, start, end) {
  337. part.emit('data', b.slice(start, end));
  338. };
  339.  
  340. /* 数据段解析完毕 */
  341. parser.onPartEnd = function() {
  342. part.emit('end');
  343. };
  344.  
  345. parser.onEnd = function() {
  346. self.ended = true;
  347. self._maybeEnd();
  348. };
  349.  
  350. this._parser = parser;
  351. };
  352.  
  353. /* 初始化,处理application/x-www-form-urlencoded类型的表单 */
  354. IncomingForm.prototype._initUrlencoded = function() {
  355. this.type = 'urlencoded';
  356.  
  357. var parser = new QuerystringParser()
  358. , self = this;
  359.  
  360. parser.onField = function(key, val) {
  361. self.emit('field', key, val);
  362. };
  363.  
  364. parser.onEnd = function() {
  365. self.ended = true;
  366. self._maybeEnd();
  367. };
  368.  
  369. this._parser = parser;
  370. };
  371.  
  372. /**
  373. * 根据给定的文件名,构造出path
  374. */
  375. IncomingForm.prototype._uploadPath = function(filename) {
  376. var name = '';
  377. for (var i = 0; i < 32; i++) {
  378. name += Math.floor(Math.random() * 16).toString(16);
  379. }
  380.  
  381. if (this.keepExtensions) {
  382. name += path.extname(filename);
  383. }
  384.  
  385. return path.join(this.uploadDir, name);
  386. };
  387.  
  388. IncomingForm.prototype._maybeEnd = function() {
  389. if (!this.ended || this._flushing) {
  390. return;
  391. }
  392.  
  393. this.emit('end');
  394. };
0 0
原创粉丝点击