下面小编给大家整理了nodejspost文件上传原理详解网页设计,本文共5篇,供大家阅读参考。本文原稿由网友“南瓜没有星”提供。
篇1:nodejspost文件上传原理详解网页设计
写这篇教程的起因是因为在学习nodejs的过程中,想要自己实现一些文件上传的功能,于是不得不去研究POST。
如果你写过一点PHP,那么你肯定记得,在PHP里面,进行文件上传的时候,我们可以直接使用全局变量 $_FILE['name' ]来获取已经被临时存储的文件信息。
但是实际上,POST数据实体,会根据数据量的大小进行分包传送,然后再从这些数据包里面分析出哪些是文件的元数据,那些是文件本身的数据。
PHP是底层做了封装,但是在nodejs里面,这个看似常见的功能却是需要自己来实现的。这篇文章主要就是介绍如何使用nodejs来解析post数据。
篇2:nodejspost文件上传原理详解网页设计
总体上来说,对于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 有两种
application/x-www-form-urlencoded
这种就是一般的文本表单用post传地数据,只要将得到的data用querystring解析下就可以了
multipart/form-data
文件表单的传输,也是本文介绍的重点
获取POST数据
前面已经说过,post数据的传输是可能分包的,因此必然是异步的。post数据的接受过程如下:
varpostData='';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数据
例如我们有表单如下
若用户在text字段中输入‘Neekey’,并且在file字段中选择文件‘text.txt’,那么服务器端收到的post数据如下:
--AaB03xContent-Disposition:form-data;name=“submit-name”Neekey--AaB03xContent-Disposition:form-data;name=“files”;filename=“file1.txt”Content-Type:text/plain...contents of file1.txt...--AaB03x--
若file字段为空:
--AaB03xContent-Disposition:form-data;name=“submit-name”Neekey--AaB03xContent-Disposition:form-data;name=“files”;filename=“”Content-Type:text/plain--AaB03x--
若将file 的 input修改为可以多个文件一起上传:
那么在text中输入‘Neekey’,并在file字段中选中两个文件’a.jpg’和’b.jpg’后:
--AaB03xContent-Disposition:form-data;name=“submit-name”Neekey--AaB03xContent-Disposition:form-data;name=“files”;filename=“a.jpg”Content-Type:image/jpeg/* data of a.jpg */--AaB03xContent-Disposition:form-data;name=“files”;filename=“b.jpg”Content-Type:image/jpeg/* data of b.jpg */--AaB03x--// 可以发现 两个文件数据部分,他们的name值是一样的
数据规则
简单总结下post数据的规则
不同字段数据之间以边界字符串分隔:
--boundary\\r\\n// 注意,如上面的headers的例子,分割字符串应该是 ------WebKitFormBoundaryuP1WvwP2LyvHpNCi\\r\\n
每一行数据用”CR LF”(\\r\\n)分隔
数据以 边界分割符 后面加上 –结尾,如:
------WebKitFormBoundaryuP1WvwP2LyvHpNCi--\\r\\n
每个字段数据的header信息(content-disposition/content-type)和字段数据以一个空行分隔:
\\r\\n\\r\\n
更加详细的信息可以参考W3C的文档Forms,不过文档中对于 multiple=“multiple” 的文件表单的post数据格式使用了二级边界字符串,但是在实际测试中,multiple类型的表单和多个单文件表单上传数据的格式一致,有更加清楚的可以交流下:
Ifthe user selected a second(image)file“file2.gif”,the user agent might construct the partsasfollows:Content-Type:multipart/form-data;boundary=AaB03x--AaB03xContent-Disposition:form-data;name=“submit-name”Larry--AaB03xContent-Disposition:form-data;name=“files”Content-Type:multipart/mixed;boundary=BbC04y--BbC04yContent-Disposition:file;filename=“file1.txt”Content-Type:text/plain...contents of file1.txt...--BbC04yContent-Disposition:file;filename=“file2.gif”Content-Type:image/gifContent-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的文件处理,我很弱很弱….)
varfs=newrequire('fs').writeStream,file=newfs(filename);fs.write(buffer,function(){fs.end();});
篇3:nodejspost文件上传原理详解网页设计
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被调用有二种情况:
在解析的数据部分的末尾在当前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数据格式中提到的 二级边界字符串 类型的数据处理。我没有深究,有兴趣的可以再研究下。
varBuffer=require('buffer').Buffer,s=0,S={PARSER_UNINITIALIZED:s++,// 解析尚未初始化START:s++,// 开始解析START_BOUNDARY:s++,// 开始找到边界字符串HEADER_FIELD_START:s++,// 开始解析到header fieldHEADER_FIELD:s++,HEADER_VALUE_START:s++,// 开始解析到header valueHEADER_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){returnc|0x20;};for(varsinS){exports[s]=S[s];}/* 构造函数 */functionMultipartParser(){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=newBuffer(str.length+4);this.boundary.write('\\r\\n--','ascii',0);this.boundary.write(str,'ascii',4);this.lookbehind=newBuffer(this.boundary.length+8);this.state=S.START;this.boundaryChars={};for(vari=0;i incoming_form.js 上图是incoming_form解析的主要过程(文件类型),其中 根据传入的requeset对象开始启动整个解析的过程 writeHeaders 从request对象中获取post数据长度,解析出边界字符串,用来初始化multipartParser 为request对象添加监听事件 request对象的 ‘data’时间到达会调用该方法,而write方法实质上是调用multipartParser.write 利用边界字符串初始化multipartParser,并实现在multipart_form.js中write解析方法中会触发的事件回调函数 具体细节看源码会比较清楚。 if(global.GENTLY)require=GENTLY.hijack(require);varutil=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;functionIncomingForm(){if(!(thisinstanceofIncomingForm))returnnewIncomingForm;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 destroyedif(!this.ended){// before it was completed, crash & burnthis._error(err);}returnfalse;}returntrue;};this.resume=function(){try{req.resume();}catch(err){// the stream was destroyedif(!this.ended){// before it was completed, crash & burnthis._error(err);}returnfalse;}returntrue;};// 记录下headers信息this.writeHeaders(req.headers);varself=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;}varerr=self._parser.end();if(err){self._error(err);}});// 若回调函数存在if(cb){varfields={},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);});}returnthis;};// 保存header信息IncomingForm.prototype.writeHeaders=function(headers){this.headers=headers;// 从头部中解析数据的长度和form类型this._parseContentLength();this._parseContentType();};IncomingForm.prototype.write=function(buffer){if(!this._parser){this._error(newError('unintialized parser'));return;}/* 累加接收到的信息 */this.bytesReceived+=buffer.length;this.emit('progress',this.bytesReceived,this.bytesExpected);// 解析数据varbytesParsed=this._parser.write(buffer);if(bytesParsed!==buffer.length){this._error(newError('parser error, '+bytesParsed+' of '+buffer.length+' bytes parsed'));}returnbytesParsed;};IncomingForm.prototype.pause=function(){// this does nothing, unless overwritten in IncomingForm.parsereturnfalse;};IncomingForm.prototype.resume=function(){// this does nothing, unless overwritten in IncomingForm.parsereturnfalse;};/** * 开始接受数据(这个函数在headers被分析完成后调用,这个时候剩下的data还没有解析过来 */IncomingForm.prototype.onPart=function(part){// this method can be overwritten by the userthis.handlePart(part);};IncomingForm.prototype.handlePart=function(part){varself=this;/* post数据不是文件的情况 */if(!part.filename){varvalue='',decoder=newStringDecoder(this.encoding);/* 有数据过来时 */part.on('data',function(buffer){self._fieldsSize+=buffer.length;if(self._fieldsSize>self.maxFieldsSize){self._error(newError('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实例varfile=newFile({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(newError('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)){varm;if(m=this.headers['content-type'].match(/boundary=(?:“([^”]+)“|([^;]+))/i)){// 解析出边界字符串,并利用边界字符串初始化multipart组件this._initMultipart(m[1]||m[2]);}else{this._error(newError('bad content-type header, no multipart boundary'));}return;}this._error(newError('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(){returnnewMultipartParser();};// 初始化multipartParset 组件IncomingForm.prototype._initMultipart=function(boundary){this.type='multipart';// 实例化组件varparser=newMultipartParser(),self=this,headerField,headerValue,part;parser.initWithBoundary(boundary);/** * 下面这些方法便是multipartParser中的callback以及dataCallback调用的函书 * 当开始解析一个数据段(比如一个文件..) * 并重置相关信息 */parser.onPartBegin=function(){part=newEventEmitter();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;varm;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);}}elseif(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';varparser=newQuerystringParser(),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){varname='';for(vari=0;i<32;i++){name+=Math.floor(Math.random()*16).toString(16);}if(this.keepExtensions){name+=path.extname(filename);}returnpath.join(this.uploadDir,name);};IncomingForm.prototype._maybeEnd=function(){if(!this.ended||this._flushing){return;}this.emit('end');}; 1)参考文档-http.ServerRequest:nodejs.org/docs/v0.5.4/api/http.html#http.ServerRequest 最近在研究nodejs如何实现文件上传功能,偶然读到《nodejs-post文件上传原理详解》这篇教程,感觉非常给力,教程中详细解读了post数据格式,以及通过分析node-formidable模块的源代码来解读文件上传的原理。 可是,在源码分析的过程中,作者并没有解释得很到位,从而导致在看完整篇教程后某些地方还有些云里雾里,在潜心研究一番后,终于明了,然而这也催生了我写这篇文章的想法。 由于该文章是在上文所提教程的基础之上补充的,因此,建议读之前先看以上教程。 使用post提交表单后,传递过来的数据到底是什么样的呢? 上篇教程中已经讲解过,可是,为了使之能更好地与后面的数据解析部分的代码相结合,我对Post数据进行了一个详细的标注 HTML 该html中有一个表单,其中包含了一个text文本框和一个file多文件上传控件。 POST HEADER 这是Post的header信息,请注意Content-Type后面的boundary数据,这就是边界字符串,用于分隔每个字段数据。 POST DATA 上图便是传递过来的POST数据部分,图中对各部分已经作了详细的表示,每一行其实隐含着一个\\r\\n(CR LF)。 上面提到的教程中其实已经总结过POST数据的规则,这里再总结一次: 每一部分数据包括字段部分和数据部分,比如文本框数据,字段部分是ContentPosition,数据部分是huli;字段部分和数据部分使用一个空行分隔; 每一行后面实际隐含这一个\\r\\n(CR LF) 每一部分数据以边界字符串分隔,假设post_header中定义的边界为Aa03x,那么一般的边界字符串为–Aa03x\\r\\n(程序中称这样的边界字符串为PART_BOUNDARY,表示后面还有数据需要解析),最后面的边界字符串为–Aa03x–(程序中称这样的边界字符串为LAST_BOUNDARY,表示所有的数据都解析完毕) 在每一部分数据中,数据部分(记得前面提到过的,每一部分数据包括字段部分和数据部分吗?)是和接下来的边界字符串紧挨在一起的! 更加详细的信息可参考W3C文档的FORMSparse
write
_initMultipart
篇4:nodeformidable详解网页设计
篇5:nodeformidable详解网页设计
- 网页设计的实习报告2022-12-11
- 网页设计面试作品2024-01-13
- 网页设计的代码范文2025-09-16
- 如何打包字体为swf文件,然后在Flex中使用网页设计2022-12-11
- 网页设计心得与体会2022-12-11
- 多媒体网页设计实习报告2022-12-11
- 网页设计应届生求职个人简历2024-10-05
- 网页设计实训报告总结2022-12-11
- 电子商务创意网页设计大赛策划书2025-03-16
- linux平台文件目录操作代码(1/3)linux网页制作2024-05-31