server.js 文件里面有需要使用Node.js来创建一个基础的hello world应用程序的代码.
我现在继续把这段代码从 server.js 中删除,然后在Node.js中穿件G级别文件上传的后端代码。下面我需要用npm安装这个项目需要的一些依赖:
图5 使用npm安装所需模块
模块安装完成后,我们可以从解决方案资源管理器中看到它们。
图6 解决方案资源管理器显示已安装模块
下一步我们需要在解决方案资源管理器新建一个 "Scripts" 文件夹并且添加 "workeruploadchunk.js" 和 "workerprocessfile.js" 到该文件夹。我们还需要下载 jQuery 2.x 和 SparkMD5 库并添加到"Scripts"文件夹。 最后还需要添加 "Default.html" 页面。这些都在我之前的 post 中介绍过。
创建Node.js后台
首先我们需要用Node.js的"require()"函数来导入在后台上传G级文件的模块。注意我也导入了"path"以及"crypto" 模块。"path"模块提供了生成上传文件块的文件名的方法。"crypto" 模块提供了生成上传文件的MD5校验和的方法。
1 2 3 4 5 6 | var express = require( 'express' );
var formidable = require( 'formidable' );
var fs = require( 'fs-extra' );
var path = require( 'path' );
var crypto = require( 'crypto' );
|
下一行代码就是见证奇迹的时刻。
1 | <span style= "background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-removed: initial; background-repeat: initial; background-size: initial; color: #000066; font-family: Consolas; font-size: 9pt;" > var </span><span style= "background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-removed: initial; background-repeat: initial; background-size: initial; font-family: Consolas; font-size: 9pt;" > app <span style= "color: #339933;" >=</span> express<span style= "color: #009900;" >()</span><span style= "color: #339933;" >;</span></span>
|
这行代码是用来创建express应用的。express应用是一个封装了Node.js底层功能的中间件。如果你还记得那个由Blank Node.js Web应用模板创建的"Hello World" 程序,你会发现我导入了"http"模块,然后调用了"http.CreateServer()"方法创建了 "Hello World" web应用。我们刚刚创建的express应用内建了所有的功能。
现在我们已经创建了一个express应用,我们让它呈现之前创建的"Default.html",然后让应用等待连接。
1 2 3 4 5 6 7 8 | app.use(express. static (__dirname, { index: 'Default.html' }));
app.listen(process.env.PORT || 1337);
var uploadpath = 'C:/Uploads/CelerFT/' ;
|
express应用有app.VERB()方法,它提供了路由的功能。我们将使用app.post()方法来处理"UploadChunk" 请求。在app.post()方法里我们做的第一件事是检查我们是否在处理POST请求。接下去检查Content-Type是否是mutipart/form-data,然后检查上传的文件块大小不能大于51MB。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | app.post( '*/api/CelerFTFileUpload/UploadChunk*' , function (request,response) {
if (request.method === 'POST' ) {
if (!(request.is( 'multipart/form-data' ))){
response.status(415).send( 'Unsupported media type' );
return ;
}
var maxuploadsize =51 * 1024 * 1024;
if (request.headers[ 'content-length' ]> maxuploadsize){
response.status(413).send( 'Maximum upload chunk size exceeded' );
return ;
}
|
一旦我们成功通过了所有的检查,我们将把上传的文件块作为一个单独分开的文件并将它按顺序数字命名。下面最重要的代码是调用fs.ensureDirSync()方法,它使用来检查临时目录是否存在。如果目录不存在则创建一个。注意我们使用的是该方法的同步版本。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | var extension =path.extname(request.param( 'filename' ));
var baseFilename =path.basename(request.param( 'filename' ), extension);
var tempfilename =baseFilename + '.' +
request.param( 'chunkNumber' ).toString().padLeft( '0' , 16) + extension + ".tmp" ;
var tempdir =uploadpath + request.param( 'directoryname' )+ '/' + baseFilename;
var localfilepath =tempdir + '/' + tempfilename;
if (fs.ensureDirSync(tempdir)) {
console.log( 'Created directory ' +tempdir);
}
|
正如我之前提出的,我们可以通过两种方式上传文件到后端服务器。第一种方式是在web浏览器中使用FormData,然后把文件块作为二进制数据发送,另一种方式是把文件块转换成base64编码的字符串,然后创建一个手工的multipart/form-data encoded请求,然后发送到后端服务器。
所以我们需要检查一下是否在上传的是一个手工multipart/form-data encoded请求,通过检查"CelerFT-Encoded"头部信息,如果这个头部存在,我们创建一个buffer并使用request的ondata时间把数据拷贝到buffer中。
在request的onend事件中通过将buffer呈现为字符串并按CRLF分开,从而从 multipart/form-data encoded请求中提取base64字符串。base64编码的文件块可以在数组的第四个索引中找到。
通过创建一个新的buffer来将base64编码的数据重现转换为二进制。随后调用fs.outputFileSync()方法将buffer写入文件中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | if (request.headers[ 'celerft-encoded' ]=== 'base64' ) {
var fileSlice = newBuffer(+request.headers[ 'content-length' ]);
var bufferOffset = 0;
request.on( 'data' , function (chunk) {
chunk.copy(fileSlice , bufferOffset);
bufferOffset += chunk.length;
}).on( 'end' , function () {
var base64data = fileSlice.toString().split( '\r\n' );
var fileData = newBuffer(base64data[4].toString(), 'base64' );
fs.outputFileSync(localfilepath,fileData);
console.log( 'Saved file to ' +localfilepath);
response.status(200).send(localfilepath);
response.end();
});
}
|
二进制文件块的上传是通过formidable模块来处理的。我们使用formidable.IncomingForm()方法得到multipart/form-data encoded请求。formidable模块将把上传的文件块保存为一个单独的文件并保存到临时目录。我们需要做的是在formidable的onend事件中将上传的文件块保存为里一个名字。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | else {
var form = new formidable.IncomingForm();
form.keepExtensions = true ;
form.uploadDir = tempdir;
form.parse(request, function (err, fields, files) {
if (err){
response.status(500).send(err);
return ;
}
});
form.on( 'error' , function (err) {
if (err){
response.status(500).send(err);
return ;
}
});
form.on( 'end' , function (fields,files) {
var temp_path = this .openedFiles[0].path;
fs.move(temp_path , localfilepath, function (err){
if (err) {
response.status(500).send(err);
return ;
}
else {
response.status(200).send(localfilepath);
response.end();
}
});
});
}
}
|
app.get()方法使用来处理"MergeAll"请求的。这个方法实现了之前描述过的功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | app.get( '*/api/CelerFTFileUpload/MergeAll*' , function (request,response) {
if (request.method === 'GET' ) {
var extension =path.extname(request.param( 'filename' ));
var baseFilename =path.basename(request.param( 'filename' ), extension);
var localFilePath =uploadpath + request.param( 'directoryname' )+ '/' + baseFilename;
var files =getfilesWithExtensionName(localFilePath, 'tmp' )
if (files.length !=request.param( 'numberOfChunks' )){
response.status(400).send( 'Number of file chunks less than total count' );
return ;
}
var filename =localFilePath + '/' + baseFilename +extension;
var outputFile =fs.createWriteStream(filename);
outputFile.on( 'finish' , function (){
console.log( 'file has been written' );
var newfilename = uploadpath +request.param( 'directoryname' )+ '/' + baseFilename
+ extension;
fs.removeSync(newfilename);
fs.move(filename, newfilename , function (err) {
if (err) {
response.status(500).send(err);
return ;
}
else {
fs.removeSync(localFilePath);
varhash = crypto.createHash( 'md5' ),
hashstream = fs.createReadStream(newfilename);
hashstream.on( 'data' , function (data) {
hash.update(data)
});
hashstream.on( 'end' , function (){
var md5results =hash.digest( 'hex' );
response.status(200).send( 'Sucessfully merged file ' + filename + ", "
+ md5results.toUpperCase());
response.end();
});
}
});
});
for ( var index infiles) {
console.log(files[index]);
var data = fs.readFileSync(localFilePath + '/' +files[index]);
outputFile.write(data);
fs.removeSync(localFilePath + '/' + files[index]);
}
outputFile.end();
}
}) ;
|
注意Node.js并没有提供String.padLeft()方法,这是通过扩展String实现的。
1 2 3 4 5 6 7 8 9 10 11 | String.prototype.padLeft = function (paddingChar, length) {
var s = new String( this );
if (( this .length< length)&& (paddingChar.toString().length > 0)) {
for ( var i = 0; i < (length - this .length) ; i++) {
s = paddingChar.toString().charAt(0).concat(s);
}
}
return s;
} ;
|
一些其它事情
其中一件事是,发表上篇文章后我继续研究是为了通过域名碎片实现并行上传到CeleFT功能。域名碎片的原理是访问一个web站点时,让web浏览器建立更多的超过正常允许范围的并发连接。 域名碎片可以通过使用不同的域名(如web1.example.com,web2.example.com)或者不同的端口号(如8000, 8001)托管web站点的方式实现。
示例中,我们使用不同端口号托管web站点的方式。
我们使用 iisnode 把 Node.js集成到 IIS( Microsoft Internet Information Services)实现这一点。 下载兼容你操作系统的版本 iisnode (x86) 或者 iisnode (x64)。 下载 IIS URL重写包。
一旦安装完成(假定windows版Node.js已安装),到IIS管理器中创建6个新网站。将第一个网站命名为CelerFTJS并且将侦听端口配置为8000。

图片7在IIS管理器中创建一个新网站
然后创建其他的网站。我为每一个网站都创建了一个应用池,并且给应用池“LocalSystem”级别的权限。所有网站的本地路径是C:\inetpub\wwwroot\CelerFTNodeJS。

图片8 文件夹层级
我在Release模式下编译了Node.js应用,然后我拷贝了server.js文件、Script文件夹以及node_modules文件夹到那个目录下。
要让包含 iisnode 的Node.js的应用工作,我们需要创建一个web.config文件,并在其中添加如下得内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | <defaultDocument>
<files>
<add value= "server.js" />
</files>
</defaultDocument>
<handlers>
<!-- indicates that the server.js file is a node.js application to be handled by the
iisnode module -->
<add name= "iisnode" path= "*.js" verb= "*" modules= "iisnode" />
</handlers>
<rewrite>
<rules>
<rule name= "CelerFTJS" >
<match url= "/*" />
<action type= "Rewrite" url= "server.js" />
</rule>
<!-- Don't interfere with requests for node-inspector debugging -->
<rule name= "NodeInspector" patternSyntax= "ECMAScript" stopProcessing= "true" >
<match url= "^server.js\/debug[\/]?" />
</rule>
</rules>
</rewrite>
|
web.config中各项的意思是让iisnode处理所有得*.js文件,由server.js 处理任何匹配"/*"的URL。

图片9 URL重写规则
如果你正确的做完了所有的工作,你就可以通过http://localhost:8000浏览网站,并进入CelerFT "Default.html"页面。
web.config文件被修改以支持如前面post中所解释的大文件的上传,这里我不会解释所有的项。不过下面的web.config项可以改善 iisnode中Node.js的性能。
1 | < span style = "font-family: Consolas; font-size: 9pt;" >< iisnode </span>< span style = "color:
|
并行上传
为了使用域名碎片来实现并行上传,我不得不给Node.js应用做些修改。我第一个要修改的是让Node.js应用支持跨域资源共享。我不得不这样做是因为使用域碎片实际上是让一个请求分到不同的域并且同源策略会限制我的这个请求。
好消息是XMLttPRequest 标准2规范允许我这么做,如果网站已经把跨域资源共享打开,更好的是我不用为了实现这个而变更在"workeruploadchunk.js"里的上传方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | var enableCORS = function (request,response, next){
response.header( 'Access-Control-Allow-Origin' , '*' );
response.header( 'Access-Control-Allow-Methods' , 'GET,POST,OPTIONS' );
response.header( 'Access-Control-Allow-Headers' , 'Content-Type, Authorization, Content-
Length, X-Requested-With' ) ;
if ( 'OPTIONS' ==request.method){
response.send(204);
}
else {
next();
}
} ;
app. use ( enableCORS ) ;
|
为了使server.js文件中得CORS可用,我创建了一个函数,该函数会创建必要的头以表明Node.js应用支持CORS。另一件事是我还需要表明CORS支持两种请求,他们是:
1、只用GET,HEAD或POST。如果使用POST向服务器发送数据,那么发送给服务器的HTTP POST请求的Content-Type应是application/x-www-form-urlencoded, multipart/form-data, 或 text/plain其中的一个。
2、HTTP请求中不要设置自定义的头(例如X-Modified等)
1、使用GET,HEAD或POST以外的方法。假设使用POST发送请求,那么Content-Type不能是application/x-www-form-urlencoded, multipart/form-data, or text/plain,例如假设POST请求向服务器发送了XML有效载荷使用了application/xml or text/xml,那么这个请求就是预检的。
2、在请求中设置自定义头(比如请求使用X-PINGOTHER头)。
在我们的例子中,我们用的是简单请求,所以我们不需要做其他得工作以使例子能够工作。
在 "workeruploadchunk.js" 文件中,我向 self.onmessage 事件添加了对进行并行文件数据块上传的支持.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | else if (workerdata.chunk!= null && workerdata.paralleluploads == true ){
if (urlnumber >= 6) {
urlnumber = 0;
}
if (urlcount >= 6) {
urlcount = 0;
}
if (urlcount == 0) {
uploadurl = workerdata.currentlocation +webapiUrl + urlnumber;
}
else {
uploadurl = workerdata.currentlocation.slice(0, -1) + urlcount +webapiUrl +
urlnumber;
}
upload(workerdata.chunk,workerdata.filename,workerdata.chunkCount, uploadurl,
workerdata.asyncstate);
urlcount++;
urlnumber++;
}
|
在 Default.html 页面我对当前的URL进行了保存,因为我准备把这些信息发送给文件上传的工作程序. 只所以这样做是因为:
1 2 3 4 5 6 7 8 | < div class = "MsoNoSpacing" style = "background: #FFFF99;" >
< span style = "font-family: " Lucida Console"; font-size: 8.0pt;">// Save current protocol and host for parallel uploads</ span ></ div >
< div class = "MsoNoSpacing" style = "background: #FFFF99;" >
"font-family: 'Lucida Console'; font-size: 8pt;">< span style = "color: #000066;" >var</ span >< span style = "font-family: 'Lucida Console'; font-size: 8pt;" > currentProtocol </ span >< span style = "color: #339933; font-family: 'Lucida Console'; font-size: 8pt;" >= </ span >< span style = "font-family: 'Lucida Console'; font-size: 8pt;" >window.</ span >< span style = "color: #660066; font-family: 'Lucida Console'; font-size: 8pt;" >location</ span >< span style = "font-family: 'Lucida Console'; font-size: 8pt;" >.</ span >< span style = "color: #660066; font-family: 'Lucida Console'; font-size: 8pt;" >protocol</ span >< span style = "color: #339933; font-family: 'Lucida Console'; font-size: 8pt;" >;</ span ></ div >
< div class = "MsoNoSpacing" style = "background: #FFFF99;" >
"font-family: 'Lucida Console'; font-size: 8pt;">< span style = "color: #000066;" >var</ span >< span style = "font-family: 'Lucida Console'; font-size: 8pt;" > currentHostandPort </ span >< span style = "color: #339933; font-family: 'Lucida Console'; font-size: 8pt;" >=</ span >< span style = "font-family: 'Lucida Console'; font-size: 8pt;" > window.</ span >< span style = "color: #660066; font-family: 'Lucida Console'; font-size: 8pt;" >location</ span >< span style = "font-family: 'Lucida Console'; font-size: 8pt;" >.</ span >< span style = "color: #660066; font-family: 'Lucida Console'; font-size: 8pt;" >host</ span >< span style = "color: #339933; font-family: 'Lucida Console'; font-size: 8pt;" >;</ span ></ div >
< div class = "MsoNoSpacing" style = "background: #FFFF99;" >
"font-family: 'Lucida Console'; font-size: 8pt;">< span style = "color: #000066;" >var</ span >< span style = "font-family: 'Lucida Console'; font-size: 8pt;" > currentLocation </ span >< span style = "color: #339933; font-family: 'Lucida Console'; font-size: 8pt;" >= </ span >< span style = "font-family: 'Lucida Console'; font-size: 8pt;" >currentProtocol </ span >< span style = "color: #339933; font-family: 'Lucida Console'; font-size: 8pt;" >+</ span >< span style = "font-family: 'Lucida Console'; font-size: 8pt;" > </ span >< span style = "color: #3366cc; font-family: 'Lucida Console'; font-size: 8pt;" >"//"</ span >< span style = "font-family: 'Lucida Console'; font-size: 8pt;" > </ span >< span style = "color: #339933; font-family: 'Lucida Console'; font-size: 8pt;" >+</ span >< span style = "font-family: 'Lucida Console'; font-size: 8pt;" > currentHostandPort</ span >< span style = "color: #339933; font-family: 'Lucida Console'; font-size: 8pt;" >;</ span ></ div >
|
1 2 | < span style = "font-family: " Calibri","sans-serif"; font-size: 11.0pt; mso-ascii-theme-font: minor-latin; mso-bidi-font-family: "Times New Roman"; mso-bidi-theme-font: minor-bidi; mso-fareast-font-family: Calibri; mso-fareast-theme-font: minor-latin; mso-hansi-theme-font: minor-latin;">
</ span >
|
1 | < span style = "font-family: " Calibri","sans-serif"; font-size: 11.0pt; mso-ascii-theme-font: minor-latin; mso-bidi-font-family: "Times New Roman"; mso-bidi-theme-font: minor-bidi; mso-fareast-font-family: Calibri; mso-fareast-theme-font: minor-latin; mso-hansi-theme-font: minor-latin;">The code below shows the modification made to the upload message.</ span >< span style = "color: #006600; mso-bidi-font-style: italic;" >< o:p ></ o:p ></ span >
|
1 2 | < span style = "font-family: " Calibri","sans-serif"; font-size: 11.0pt; mso-ascii-theme-font: minor-latin; mso-bidi-font-family: "Times New Roman"; mso-bidi-theme-font: minor-bidi; mso-fareast-font-family: Calibri; mso-fareast-theme-font: minor-latin; mso-hansi-theme-font: minor-latin;">
</ span >
|
1 | < span style = "background-color: #ffff99; font-family: 'Lucida Console'; font-size: 8pt;" >// Send and upload message to the webworker</ span >
|
1 | "background-color: #ffff99; font-family: 'Lucida Console'; font-size: 8pt;">< span style = "color: #000066;" >case</ span >< span style = "background-color: #ffff99; font-family: 'Lucida Console'; font-size: 8pt;" > </ span >< span style = "background-color: #ffff99; color: #3366cc; font-family: 'Lucida Console'; font-size: 8pt;" >'upload'</ span >< span style = "background-color: #ffff99; color: #339933; font-family: 'Lucida Console'; font-size: 8pt;" >:</ span >
|
1 | < span style = "background-color: #ffff99; font-family: 'Lucida Console'; font-size: 8pt;" >// </ span >< span style = "background-color: #ffff99; font-family: 'Lucida Console'; font-size: 8pt;" >Check to see if backend supports parallel uploads</ span >
|
1 2 3 4 5 6 7 8 9 10 | var paralleluploads = false ;
if ($( '#select_parallelupload' ).prop( 'checked' )) {
paralleluploads = true ;
}
uploadworkers[data.id].postMessage({ 'chunk' : data.blob, 'filename' :data.filename,
'directory' : $( "#select_directory" ).val(), 'chunkCount' :data.chunkCount,
'asyncstate' :data.asyncstate, 'paralleluploads' :paralleluploads, 'currentlocation' :
currentLocation, 'id' : data.id });
break ;
|