nodejs-post文件上传原理详解

标签: 代码 node node.js nodejs post | 发表时间:2011-08-25 20:59 | 作者:neekey never-online
出处:http://cnodejs.org/blog

基础知识

简单介绍下,如下图:

其中请求报文中的开始行和首部行包含了常见的各种信息,比如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中获取1):

  • 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数据的接受过程如下:

    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数据的解析(multipart/form-data)

在解析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数据的规则

  1. 不同字段数据之间以边界字符串分隔:
     --boundary\r\n // 注意,如上面的headers的例子,分割字符串应该是 ------WebKitFormBoundaryuP1WvwP2LyvHpNCi\r\n
  2. 每一行数据用”CR LF”(\r\n)分隔
  3. 数据以 边界分割符 后面加上 –结尾,如:
     ------WebKitFormBoundaryuP1WvwP2LyvHpNCi--\r\n
  4. 每个字段数据的header信息(content-disposition/content-type)和字段数据以一个空行分隔:
    \r\n\r\n

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

If the user selected a second (image) file "file2.gif", the user agent might construct the parts as follows:

   Content-Type: multipart/form-data; boundary=AaB03x

   --AaB03x
   Content-Disposition: form-data; name="submit-name"

   Larry
   --AaB03x
   Content-Disposition: form-data; name="files"
   Content-Type: multipart/mixed; boundary=BbC04y

   --BbC04y
   Content-Disposition: file; filename="file1.txt"
   Content-Type: text/plain

   ... contents of file1.txt ...
   --BbC04y
   Content-Disposition: file; filename="file2.gif"
   Content-Type: image/gif
   Content-Transfer-Encoding: binary

   ...contents of file2.gif...
   --BbC04y--
   --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的文件处理,我很弱很弱….)

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

node-formidable模块源码分析

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

项目中的lib目录

lib
  |-file.js
  |-incoming_form.js
  |-index.js
  |-multipart_parser.js
  |-querystring_parser.js
  |-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数据格式中提到的 二级边界字符串 类型的数据处理。我没有深究,有兴趣的可以再研究下。

var Buffer = require('buffer').Buffer,
    s = 0,
    S =
    { PARSER_UNINITIALIZED: s++, // 解析尚未初始化
      START: s++,  // 开始解析
      START_BOUNDARY: s++, // 开始找到边界字符串
      HEADER_FIELD_START: s++, // 开始解析到header field
      HEADER_FIELD: s++,
      HEADER_VALUE_START: s++, // 开始解析到header value
      HEADER_VALUE: s++,
      HEADER_VALUE_ALMOST_DONE: s++, // header value 解析完毕
      HEADERS_ALMOST_DONE: s++, // header 部分 解析完毕
      PART_DATA_START: s++, // 开始解析 数据段
      PART_DATA: s++,
      PART_END: s++,
      END: s++,
    },

    f = 1,
    F =
    { PART_BOUNDARY: f,
      LAST_BOUNDARY: f *= 2,
    },

    /* 一些字符的ASCII值 */
    LF = 10,
    CR = 13,
    SPACE = 32,
    HYPHEN = 45,
    COLON = 58,
    A = 97,
    Z = 122,

    /* 将所有大写小写字母的ascii一律转化为小写的ascii值 */
    lower = function(c) {
      return c | 0x20;
    };

for (var s in S) {
  exports[s] = S[s];
}

/* 构造函数 */
function MultipartParser() {
  this.boundary = null;
  this.boundaryChars = null;
  this.lookbehind = null;
  this.state = S.PARSER_UNINITIALIZED;

  this.index = null;
  this.flags = 0;
};
exports.MultipartParser = MultipartParser;

/* 给定边界字符串以初始化 */
MultipartParser.prototype.initWithBoundary = function(str) {
  this.boundary = new Buffer(str.length+4);
  this.boundary.write('\r\n--', 'ascii', 0);
  this.boundary.write(str, 'ascii', 4);
  this.lookbehind = new Buffer(this.boundary.length+8);
  this.state = S.START;

  this.boundaryChars = {};
  for (var i = 0; i < this.boundary.length; i++) {
    this.boundaryChars[this.boundary[i]] = true;
  }
};

/* 每个数据段到达时的处理函数 */
MultipartParser.prototype.write = function(buffer) {
  var self = this,
      i = 0,
      len = buffer.length,
      prevIndex = this.index,
      index = this.index,
      state = this.state,
      flags = this.flags,
      lookbehind = this.lookbehind,
      boundary = this.boundary,
      boundaryChars = this.boundaryChars,
      boundaryLength = this.boundary.length,
      boundaryEnd = boundaryLength - 1,
      bufferLength = buffer.length,
      c,
      cl,

      /* 标记了name这个标记点的buffer偏移 */
      mark = function(name) {
        self[name+'Mark'] = i;
      },
      /* 清除标记 */
      clear = function(name) {
        delete self[name+'Mark'];
      },
      /* 回调函数,将调用onName,并传入对应的buffer和对应的offset区间,可知这些回调都在 incoming_form 模块中被具体实现 */
      callback = function(name, buffer, start, end) {
        if (start !== undefined && start === end) {
          return;
        }

        var callbackSymbol = 'on'+name.substr(0, 1).toUpperCase()+name.substr(1);
        if (callbackSymbol in self) {
          self[callbackSymbol](buffer, start, end);
        }
      },
      /* 数据回调 */
      dataCallback = function(name, clear) {
        var markSymbol = name+'Mark';
        if (!(markSymbol in self)) {
          return;
        }

        if (!clear) {
	  /* 传入回调的名称,buffer,buffer的开始位置(可见mark方法就是用来存储offset的),end为数据的重点 */
          callback(name, buffer, self[markSymbol], buffer.length);
          self[markSymbol] = 0;
        } else {
	  /* 区别是 end 的值为i,在一个数据已经判断到达其结束位置时,就删除这个mark点,因为这个时候已经知道了这个数据的起始位置 */
          callback(name, buffer, self[markSymbol], i);
          delete self[markSymbol];
        }
      };

  /* 对buffer逐个字节遍历,进行解析判断,并触发相应事件 */
  for (i = 0; i < len; i++) {
    c = buffer[i];
    switch (state) {
      case S.PARSER_UNINITIALIZED:
        return i;
      case S.START:
        index = 0;
        state = S.START_BOUNDARY;
      case S.START_BOUNDARY:
	/**
	 * 对于边界的判断 ====
	 * 这里看了很多次,一直很迷惑
	 * 因为首先:除了最后一个边界字符串,其他(包括第一个边界字符串)都是这个样子:
	 * --boundary\r\t
	 * 但是在初始化函数中,对于this.boundary的赋值是这样的:
	 * \r\n--boundary
	 *
	 * 但是仔细看下面部分的代码,作者只是为了初始化方便,或者出于其他的考虑
	 */

	/**
	 * 判断是否到了边界字符串的结尾\r处
	 * 如果当前字符与 \r不匹配,则return ,算是解析出错了
	 * 注意,虽然this.boundary != --boundary\r\n 但是长度是一致的,因此这里的判断是没有问题的
	 */
        if (index == boundary.length - 2) {
          if (c != CR) {
            return i;
          }
          index++;
          break;
        }
	/**
	 * 判断是否到了边界字符串的结尾\n处
	 * 如果是,则置index = 0
	 * 回调 partBegin,并将状态设置为还是header 的 field 信息的读取状态
	 */
	else if (index - 1 == boundary.length - 2) {
          if (c != LF) {
            return i;
          }
          index = 0;
          callback('partBegin');
          state = S.HEADER_FIELD_START;
          break;
        }

	/**
	 * 除了boundary的最后的\r\n外,其他字符都要进行检查
	 * 注意这里用的是 index+2 进行匹配,证实了
	 * 作者是因为某种意图将 boundary设置成\r\n--boundary的形式
	 * (其实这种形式也没有错,对处第一个边界字符串外的其他边界字符串,这个形式都是适用的)
	 */
        if (c != boundary[index+2]) {
          return i;
        }
        index++;
        break;
      /* 对于header field的扫描开始,这里记录了标记了开始点在buffer中的位置 */
      case S.HEADER_FIELD_START:
        state = S.HEADER_FIELD;
        mark('headerField');
        index = 0;
      /* header field的扫描过程 */
      case S.HEADER_FIELD:
	/* 这里是header和data中间的那个空行,所以第一个字符就是\r */
        if (c == CR) {
          clear('headerField');
          state = S.HEADERS_ALMOST_DONE;
          break;
        }

        index++;
	/* 如果是小横线 '-' 比如在 Content-Disposition */
        if (c == HYPHEN) {
          break;
        }
	/**
	 *  如果是冒号,那么说明 field结束了
	 *  dataCallback,注意第二个参数为true,则它将调用this.onHeaderField,并且将buffer,start(之前mark(headerField)记录的位置),end(当前的i)传递过去,最后将这个mark清理掉
	 *  之后进入 header value 的开始阶段
	 */
        if (c == COLON) {
          if (index == 1) {
            // empty header field
            return i;
          }
          dataCallback('headerField', true);
          state = S.HEADER_VALUE_START;
          break;
        }

	/**
	 * 对于所有其他不是冒号和小横线的字符,必须为字母,否则解析结束
	 */
        cl = lower(c);
        if (cl < A || cl > Z) {
          return i;
        }
        break;
      /**
       * value 的读取开始
       * 做记号,设置state
       */
      case S.HEADER_VALUE_START:
        if (c == SPACE) {
          break;
        }

        mark('headerValue');
        state = S.HEADER_VALUE;
      /**
       * value 的分析阶段
       */
      case S.HEADER_VALUE:
	/**
	 * 如果是 \r,则value结束
	 * 同样是调用 dataCallback,参数为true
	 * 注意这里还 callback了 headerEnd,没有给定任何参数,这里是作为一个trigger抛出一行header结束的事件
	 */
        if (c == CR) {
          dataCallback('headerValue', true);
          callback('headerEnd');
          state = S.HEADER_VALUE_ALMOST_DONE;
        }
        break;
      /**
       * value 结束的检查,之前是检查到了 \r ,如果下一个字符不是 \n 肯定问题
       * 一个value结束,可能下面还是一行header,所以不会直接 header done,而是重新进入扫描 fields的阶段
       */
      case S.HEADER_VALUE_ALMOST_DONE:
        if (c != LF) {
          return i;
        }
        state = S.HEADER_FIELD_START;
        break;
      /**
       * 同样是先检查一下字符是否有误(看一下 case S.HEADER_FIELD 的第一个if )
       * 抛出headers解析完毕的事件
       */
      case S.HEADERS_ALMOST_DONE:
        if (c != LF) {
          return i;
        }

        callback('headersEnd');
        state = S.PART_DATA_START;
        break;
      /**
       * 开始解析post数据
       * 设置状态,做记号
       */
      case S.PART_DATA_START:
        state = S.PART_DATA
        mark('partData');
      /**
       * 进入post数据的解析状态
       * 上一次设置index是在 HEADER_FIELD_START中设置为0
       */
      case S.PART_DATA:

        prevIndex = index;

        if (index == 0) {
          // boyer-moore derrived algorithm to safely skip non-boundary data
          i += boundaryEnd;
          while (i < bufferLength && !(buffer[i] in boundaryChars)) {
            i += boundaryLength;
          }
          i -= boundaryEnd;
          c = buffer[i];
        }

        if (index < boundary.length) {
          if (boundary[index] == c) {
            if (index == 0) {
              dataCallback('partData', true);
            }
            index++;
          } else {
            index = 0;
          }
        } else if (index == boundary.length) {
          index++;
          if (c == CR) {
            // CR = part boundary
            flags |= F.PART_BOUNDARY;
          } else if (c == HYPHEN) {
            // HYPHEN = end boundary
            flags |= F.LAST_BOUNDARY;
          } else {
            index = 0;
          }
        } else if (index - 1 == boundary.length)  {
          if (flags & F.PART_BOUNDARY) {
            index = 0;
            if (c == LF) {
              // unset the PART_BOUNDARY flag
              flags &= ~F.PART_BOUNDARY;
              callback('partEnd');
              callback('partBegin');
              state = S.HEADER_FIELD_START;
              break;
            }
          } else if (flags & F.LAST_BOUNDARY) {
            if (c == HYPHEN) {
              callback('partEnd');
              callback('end');
              state = S.END;
            } else {
              index = 0;
            }
          } else {
            index = 0;
          }
        }

        if (index > 0) {
          // when matching a possible boundary, keep a lookbehind reference
          // in case it turns out to be a false lead
          lookbehind[index-1] = c;
        } else if (prevIndex > 0) {
          // if our boundary turned out to be rubbish, the captured lookbehind
          // belongs to partData
          callback('partData', lookbehind, 0, prevIndex);
          prevIndex = 0;
          mark('partData');

          // reconsider the current character even so it interrupted the sequence
          // it could be the beginning of a new sequence
          i--;
        }

        break;
      case S.END:
        break;
      default:
        return i;
    }
  }
  /**
   * 下面这三个是在当前数据段解析完全后调用的。
   * 如果一个数据部分(比如如field信息)已经在上面的解析过程中解析完毕,那么自然已经调用过clear方法,那下面的dataCallback将什么也不做
   * 否则,下面的调用将会把这次数据段中的数据部分传递到回调函数中
   */
  dataCallback('headerField');
  dataCallback('headerValue');
  dataCallback('partData');

  this.index = index;
  this.state = state;
  this.flags = flags;

  return len;
};

MultipartParser.prototype.end = function() {
  if (this.state != S.END) {
    return new Error('MultipartParser.end(): stream ended unexpectedly');
  }
};

incoming_form.js

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

parse

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

  • writeHeaders

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

  • 为request对象添加监听事件

write

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

_initMultipart

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

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

if (global.GENTLY) require = GENTLY.hijack(require);

var util = require('./util'),
    path = require('path'),
    File = require('./file'),
    MultipartParser = require('./multipart_parser').MultipartParser,
    QuerystringParser = require('./querystring_parser').QuerystringParser,
    StringDecoder = require('string_decoder').StringDecoder,
    EventEmitter = require('events').EventEmitter;

function IncomingForm() {
  if (!(this instanceof IncomingForm)) return new IncomingForm;
  EventEmitter.call(this);

  this.error = null;
  this.ended = false;

  this.maxFieldsSize = 2 * 1024 * 1024;  // 设置最大文件限制
  this.keepExtensions = false;
  this.uploadDir = '/tmp';  // 设置文件存放目录
  this.encoding = 'utf-8';
  this.headers = null;  // post请求的headers信息
  // 收到的post数据类型(一般的字符串数据,还是文件)
  this.type = null;

  this.bytesReceived = null;  // 已经接受的字节
  this.bytesExpected = null;  // 预期接受的字节

  this._parser = null;
  this._flushing = 0;
  this._fieldsSize = 0;
};
util.inherits(IncomingForm, EventEmitter);
exports.IncomingForm = IncomingForm;

IncomingForm.prototype.parse = function(req, cb) {
  // 每次调用都重新建立方法,用于对req和cb的闭包使用
  this.pause = function() {
    try {
      req.pause();
    } catch (err) {
      // the stream was destroyed
      if (!this.ended) {
        // before it was completed, crash & burn
        this._error(err);
      }
      return false;
    }
    return true;
  };

  this.resume = function() {
    try {
      req.resume();
    } catch (err) {
      // the stream was destroyed
      if (!this.ended) {
        // before it was completed, crash & burn
        this._error(err);
      }
      return false;
    }

    return true;
  };

  // 记录下headers信息
  this.writeHeaders(req.headers);

  var self = this;
  req
    .on('error', function(err) {
      self._error(err);
    })
    .on('aborted', function() {
      self.emit('aborted');
    })
	// 接受数据
    .on('data', function(buffer) {
      self.write(buffer);
    })
	// 数据传送结束
    .on('end', function() {
      if (self.error) {
        return;
      }

      var err = self._parser.end();
      if (err) {
        self._error(err);
      }
    });

  // 若回调函数存在
  if (cb) {
    var fields = {}, files = {};
    this
	  // 一个字段解析完毕,触发事件
      .on('field', function(name, value) {
        fields[name] = value;
      })
	  // 一个文件解析完毕,处罚事件
      .on('file', function(name, file) {
        files[name] = file;
      })
      .on('error', function(err) {
        cb(err, fields, files);
      })
	  // 所有数据接收完毕,执行回调函数
      .on('end', function() {
        cb(null, fields, files);
      });
  }

  return this;
};

// 保存header信息
IncomingForm.prototype.writeHeaders = function(headers) {
  this.headers = headers;
  // 从头部中解析数据的长度和form类型
  this._parseContentLength();
  this._parseContentType();
};

IncomingForm.prototype.write = function(buffer) {
  if (!this._parser) {
    this._error(new Error('unintialized parser'));
    return;
  }

  /* 累加接收到的信息 */
  this.bytesReceived += buffer.length;
  this.emit('progress', this.bytesReceived, this.bytesExpected);

  // 解析数据
  var bytesParsed = this._parser.write(buffer);
  if (bytesParsed !== buffer.length) {
    this._error(new Error('parser error, '+bytesParsed+' of '+buffer.length+' bytes parsed'));
  }

  return bytesParsed;
};

IncomingForm.prototype.pause = function() {
  // this does nothing, unless overwritten in IncomingForm.parse
  return false;
};

IncomingForm.prototype.resume = function() {
  // this does nothing, unless overwritten in IncomingForm.parse
  return false;
};

/**
 * 开始接受数据(这个函数在headers被分析完成后调用,这个时候剩下的data还没有解析过来
 */
IncomingForm.prototype.onPart = function(part) {
  // this method can be overwritten by the user
  this.handlePart(part);
};

IncomingForm.prototype.handlePart = function(part) {
  var self = this;

  /* post数据不是文件的情况 */
  if (!part.filename) {
    var value = ''
      , decoder = new StringDecoder(this.encoding);
	/* 有数据过来时 */
    part.on('data', function(buffer) {
      self._fieldsSize += buffer.length;
      if (self._fieldsSize > self.maxFieldsSize) {
        self._error(new Error('maxFieldsSize exceeded, received '+self._fieldsSize+' bytes of field data'));
        return;
      }
      value += decoder.write(buffer);
    });

    part.on('end', function() {
      self.emit('field', part.name, value);
    });
    return;
  }

  this._flushing++;

  // 创建新的file实例
  var file = new File({
    path: this._uploadPath(part.filename),
    name: part.filename,
    type: part.mime,
  });

  this.emit('fileBegin', part.name, file);

  file.open();

  /* 当文件数据达到,一点一点写入文件 */
  part.on('data', function(buffer) {
    self.pause();
    file.write(buffer, function() {
      self.resume();
    });
  });

  // 一个文件的数据解析完毕,出发事件
  part.on('end', function() {
    file.end(function() {
      self._flushing--;
      self.emit('file', part.name, file);
      self._maybeEnd();
    });
  });
};
/**
 * 解析表单类型
 * 如果为文件表单,则解析出边界字串,初始化multipartParser
 */
IncomingForm.prototype._parseContentType = function() {
  if (!this.headers['content-type']) {
    this._error(new Error('bad content-type header, no content-type'));
    return;
  }

  // 如果是一般的post数据
  if (this.headers['content-type'].match(/urlencoded/i)) {
    this._initUrlencoded();
    return;
  }

  // 如果为文件类型
  if (this.headers['content-type'].match(/multipart/i)) {
    var m;
    if (m = this.headers['content-type'].match(/boundary=(?:"([^"]+)"|([^;]+))/i)) {
	  // 解析出边界字符串,并利用边界字符串初始化multipart组件
      this._initMultipart(m[1] || m[2]);
    } else {
      this._error(new Error('bad content-type header, no multipart boundary'));
    }
    return;
  }

  this._error(new Error('bad content-type header, unknown content-type: '+this.headers['content-type']));
};

IncomingForm.prototype._error = function(err) {
  if (this.error) {
    return;
  }

  this.error = err;
  this.pause();
  this.emit('error', err);
};

// 从 this.headers 中获取数据总长度
IncomingForm.prototype._parseContentLength = function() {
  if (this.headers['content-length']) {
    this.bytesReceived = 0;
    this.bytesExpected = parseInt(this.headers['content-length'], 10);
  }
};

IncomingForm.prototype._newParser = function() {
  return new MultipartParser();
};
// 初始化multipartParset 组件
IncomingForm.prototype._initMultipart = function(boundary) {
  this.type = 'multipart';

  // 实例化组件
  var parser = new MultipartParser(),
      self = this,
      headerField,
      headerValue,
      part;

  parser.initWithBoundary(boundary);
  /**
   * 下面这些方法便是multipartParser中的callback以及dataCallback调用的函书
   * 当开始解析一个数据段(比如一个文件..)
   * 并重置相关信息
   */
  parser.onPartBegin = function() {
    part = new EventEmitter();
    part.headers = {};
    part.name = null;
    part.filename = null;
    part.mime = null;
    headerField = '';
    headerValue = '';
  };
  /**
   * 数据段的头部信息解析完毕(或者数据段的头部信息在当前接受到的数据段的尾部,并且尚未结束)
   * 下面的onHeaderValue和onPartData也是一样的道理
   */
  parser.onHeaderField = function(b, start, end) {
    headerField += b.toString(self.encoding, start, end);
  };

  /* 数据段的头部信息value的解析过程 */
  parser.onHeaderValue = function(b, start, end) {
    headerValue += b.toString(self.encoding, start, end);
  };

  /* header信息(一行)解析完毕,并储存起来 */
  parser.onHeaderEnd = function() {
    headerField = headerField.toLowerCase();
    part.headers[headerField] = headerValue;

    var m;
    if (headerField == 'content-disposition') {
      if (m = headerValue.match(/name="([^"]+)"/i)) {
        part.name = m[1];
      }

      if (m = headerValue.match(/filename="([^;]+)"/i)) {
        part.filename = m[1].substr(m[1].lastIndexOf('\\') + 1);
      }
    } else if (headerField == 'content-type') {
      part.mime = headerValue;
    }

	/* 重置,准备解析下一个header信息 */
    headerField = '';
    headerValue = '';
  };

  /* 整个headers信息解析完毕 */
  parser.onHeadersEnd = function() {
    self.onPart(part);
  };

  /* 数据部分的解析 */
  parser.onPartData = function(b, start, end) {
    part.emit('data', b.slice(start, end));
  };

  /* 数据段解析完毕 */
  parser.onPartEnd = function() {
    part.emit('end');
  };

  parser.onEnd = function() {
    self.ended = true;
    self._maybeEnd();
  };

  this._parser = parser;
};

/* 初始化,处理application/x-www-form-urlencoded类型的表单 */
IncomingForm.prototype._initUrlencoded = function() {
  this.type = 'urlencoded';

  var parser = new QuerystringParser()
    , self = this;

  parser.onField = function(key, val) {
    self.emit('field', key, val);
  };

  parser.onEnd = function() {
    self.ended = true;
    self._maybeEnd();
  };

  this._parser = parser;
};

/**
 * 根据给定的文件名,构造出path
 */
IncomingForm.prototype._uploadPath = function(filename) {
  var name = '';
  for (var i = 0; i < 32; i++) {
    name += Math.floor(Math.random() * 16).toString(16);
  }

  if (this.keepExtensions) {
    name += path.extname(filename);
  }

  return path.join(this.uploadDir, name);
};

IncomingForm.prototype._maybeEnd = function() {
  if (!this.ended || this._flushing) {
    return;
  }

  this.emit('end');
};
关于作者
倪云建, 淘宝UED, 前端研发工程师
您可能也喜欢:

nodejs快速入门

几个常用字符串hash算法的node封装

用Eclipse调试Node.js代码

nodejs下mysql性能测试

Node 下 Http Streaming 的跨浏览器实现
无觅

相关 [nodejs post 文件] 推荐:

nodejs-post文件上传原理详解

- never-online - CNode社区
浅谈HTTP中Get与Post的区别. 其中请求报文中的开始行和首部行包含了常见的各种信息,比如http协议版本,方法(GET/POST),accept-language,cookie等等. 而’实体主体’一般在post中使用,比如我们用表单上传文件,文件数据就是在这个’实体主体’当中. 写这篇教程的起因是因为在学习nodejs的过程中,想要自己实现一些文件上传的功能,于是不得不去研究POST.

fred: 使用 NodeJS + Express 從 GET/POST Request 取值

- - Planet DebianTW
過去無論哪一種網站應用程式的開發語言,初學者教學中第一次會提到的起手式,八九不離十就是 GET/POST Request 的取值. 但是,在 Node.js + Express 的世界中,彷彿人人是高手,天生就會使用,從不曾看到有人撰文說明. 這應該算是開發 Web Service 的入門,在 Client 與 Server 的互動中,瀏覽器發出 GET/POST Request 時會傳值給 Server-side,常見應用就是網頁上以 POST method 送出的表單內容,或是網址列上的 Query Strings (ex: page?page=3&id=5).

nodejs快速入门

- AreYouOK? - 淘宝数据平台与产品部官方博客 tbdata.org
主要介绍了一下node.js的发展, 现状, 安装, 使用.

NodeJS学习笔记

- - Web前端 - ITeye博客
今天开始学习NodeJS,在这里做个笔记,记录一下我的学习历程,也方便以后参考. Node.js® 是一个基于  Chrome V8 引擎 的 JavaScript 运行时. 简单的说 Node.js 就是运行在服务端的 JavaScript. Node.js 是一个基于Chrome JavaScript 运行时建立的一个平台.

iframe post提交数据

- - Web前端 - ITeye博客
对于iframe默认get提交 需要进行post数据的大量提交,给找到外国网站的简单例子如:
. 因为我项目用到的是 页面用的tab 样式(如何实现tab样式切换这里不做说明),嵌入的是三个页面iframe,需要在切换的时候更改target指定iframe名字,为了验证是否可行,只好试下,不然无别的办法.

NodeJS与Mysql的交互

- - CSDN博客推荐文章
把Mysql Module装到 NodeJS中.   JS脚本 mysqlTest.js. //加载mysql Module  .   //要创建的数据库名  .     //要创建的表名  . 作者:qxs965266509 发表于2013-8-17 9:47:35 原文链接. 阅读:0 评论:0 查看评论.

[译]你不知道的NodeJS

- - 掘金前端
更新:这篇文章现在是我的书《Node.js进阶》的一部分. 在 jscomplete.com/node-beyond…中阅读此内容的更新版本以及有关Node.js的更多信息. 在今年的Forward.js会议(关于JavaScript的会议)上,我分享了题为“你不知道的NodeJS”的演讲. 在那次演讲中,我向观众提出了一系列有关Nodejs运行时的问题,大多数有技术背景的观众无法回答其中大多数问题.

nodejs web开发入门: Simple-TODO Nodejs 实现版

- Aleafs - CNode社区
看到simple todo的各种python版本实现, 我也来凑凑热闹…. 既然已经有这么多python版本了, 我就对比实现了一个Simple-TODO的nodejs版本: Node TODO, 模版和樣式全部copy自web.py版本.. 源代码: https://github.com/fengmk2/todo.

从HTTP GET和POST的区别说起

- jin - Yining.write()
在推特上抱怨面试时问HTTP GETE和POST的区别得到回答都不满意,有人不清楚,当时只回复了看 RFC2616. 面试时得到的回答大多是:POST是安全的,因为被提交的数据看不到,或者被加密的,其它的还有GET的时候中文出现乱码(在地址栏里),数据最大长度限制等等. 说 POST 比 GET 安全肯定是错的,POST跟GET都是明文传输,用httpfox等插件,或者像WireShark 等类似工具就能观察到.

也谈 GET 和 POST 的区别

- - 博客园_首页
上个月,博客园精华区有篇文章《 GET 和 POST 有什么区别. 及为什么网上的多数答案都是错的 》,文中和回复多是对以下两个问题进行了深究:. 在我看来这两者都不是重点,特写此文予以讨论. GET、POST 专业名称是 HTTP Request Methods. 但 HTTP Request Methods 不只是 GET 和 POST,完整列表如下:.