不管喜歡與否,javascript無處不在。 我們可以在客戶端的前臺應用中找到它,也可以在大量的框架、類庫中找到它,而且可以在服務器端的后臺應用中找到它。

近年來, Javascript越來越流行,這似乎是由于 Javascript 生態(tài)系統(tǒng)正在幫助提高生產(chǎn)率、減少入門所需的時間。 在我的第一篇文章中,我介紹了使用 ASP.NET Web 后端 API 實現(xiàn) G級文件上傳,發(fā)表完這篇文章后,我決定試一下使用 Node.js 能否達到同樣的效果。 這意味著我需要實現(xiàn) UploadChunk和 MergeAll方法,在 Node.js中我發(fā)表的 最后一篇文章談到了這一點。

開發(fā)環(huán)境

我們將使用 Visual Studio Express 2013 for Web 作為開發(fā)環(huán)境, 不過它還不能被用來做 Node.js 開發(fā)。為此我們需要安裝 Node.js Tools for Visual Studio。 裝好后 Visual Studio Express 2013 for Web 就會轉(zhuǎn)變成一個 Node.js IDE 環(huán)境,提供創(chuàng)建這個應用所需要的所有東西.。而基于這里提供的指導,我們需要:

  • 下載安裝 Node.js Windows 版,選擇適用你系統(tǒng)平臺的版本, Node.js (x86) 或者 Node.js (x64)。
  • 下載并安裝 Node.js 的 Visual Studio 工具。

安裝完成后我們就會運行 Visual Studio Express 2013 for Web, 并使用 Node.js 的交互窗口來驗證安裝. Node.js 的交互窗口可以再 View->Other Windows->Node.js Interactive Window 下找到. Node.js 交互窗口運行后我們要輸入一些命令檢查是否一切OK.

Figure 1 Node.js Interactive Window

現(xiàn)在我們已經(jīng)對安裝進行了驗證,我們現(xiàn)在就可以準備開始創(chuàng)建支持GB級文件上傳的Node.js后臺程序了. 開始我們先創(chuàng)建一個新的項目,并選擇一個空的 Node.js Web應用程序模板.

Figure 2 New project using the Blank Node.js Web Application template

項目創(chuàng)建好以后,我們應該會看到一個叫做 server.js 的文件,還有解決方案瀏覽器里面的Node包管理器 (npm).

圖3 解決方案管理器里面的 Node.js 應用程序

server.js 文件里面有需要使用Node.js來創(chuàng)建一個基礎的hello world應用程序的代碼.

Figure 4 The Hello World application

我現(xiàn)在繼續(xù)把這段代碼從 server.js 中刪除,然后在Node.js中穿件G級別文件上傳的后端代碼。下面我需要用npm安裝這個項目需要的一些依賴:

  • Express – Node.js網(wǎng)頁應用框架,用于構(gòu)建單頁面、多頁面以及混合網(wǎng)絡應用
  • Formidable – 用于解析表單數(shù)據(jù),特別是文件上傳的Node.js模塊
  • fs-extra – 文件系統(tǒng)交互模塊

圖5 使用npm安裝所需模塊

模塊安裝完成后,我們可以從解決方案資源管理器中看到它們。

圖6 解決方案資源管理器顯示已安裝模塊

下一步我們需要在解決方案資源管理器新建一個 ”Scripts” 文件夾并且添加 ”workeruploadchunk.js” 和 “workerprocessfile.js” 到該文件夾。我們還需要下載 jQuery 2.xSparkMD5 庫并添加到”Scripts”文件夾。 最后還需要添加 ”Default.html” 頁面。這些都在我之前的 post 中介紹過。

創(chuàng)建Node.js后臺

首先我們需要用Node.js的”require()”函數(shù)來導入在后臺上傳G級文件的模塊。注意我也導入了”path”以及”crypto” 模塊?!眕ath”模塊提供了生成上傳文件塊的文件名的方法?!眂rypto” 模塊提供了生成上傳文件的MD5校驗和的方法。

// The required modules        
var   express = require('express');      
var   formidable = require('formidable');      
var   fs = require('fs-extra');      
var   path = require('path');  
var   crypto = require('crypto');

下一行代碼就是見證奇跡的時刻。

var app = express();

這行代碼是用來創(chuàng)建express應用的。express應用是一個封裝了Node.js底層功能的中間件。如果你還記得那個由Blank Node.js Web應用模板創(chuàng)建的”Hello World” 程序,你會發(fā)現(xiàn)我導入了”http”模塊,然后調(diào)用了”http.CreateServer()”方法創(chuàng)建了 ”Hello World” web應用。我們剛剛創(chuàng)建的express應用內(nèi)建了所有的功能。

現(xiàn)在我們已經(jīng)創(chuàng)建了一個express應用,我們讓它呈現(xiàn)之前創(chuàng)建的”Default.html”,然后讓應用等待連接。

// Serve up the Default.html page  
app.use(express.static(__dirname, { index: 'Default.html' }));      

// Startup the express.js application  
app.listen(process.env.PORT || 1337);      

// Path to save the files  
var   uploadpath = 'C:/Uploads/CelerFT/';

express應用有app.VERB()方法,它提供了路由的功能。我們將使用app.post()方法來處理”UploadChunk” 請求。在app.post()方法里我們做的第一件事是檢查我們是否在處理POST請求。接下去檢查Content-Type是否是mutipart/form-data,然后檢查上傳的文件塊大小不能大于51MB。

// Use the post method for express.js to respond to posts to the uploadchunk urls and  
// save each file chunk as a separate file  
app.post('*/api/CelerFTFileUpload/UploadChunk*', function(request,response) {      

    if (request.method === 'POST') {      
        // Check Content-Type     
        if (!(request.is('multipart/form-data'))){      
            response.status(415).send('Unsupported media type');      
            return;      
        }      

        // Check that we have not exceeded the maximum chunk upload size  
        var maxuploadsize =51 * 1024 * 1024;      

        if (request.headers['content-length']> maxuploadsize){      
            response.status(413).send('Maximum upload chunk size exceeded');      
            return;      
        }

一旦我們成功通過了所有的檢查,我們將把上傳的文件塊作為一個單獨分開的文件并將它按順序數(shù)字命名。下面最重要的代碼是調(diào)用fs.ensureDirSync()方法,它使用來檢查臨時目錄是否存在。如果目錄不存在則創(chuàng)建一個。注意我們使用的是該方法的同步版本。

// Get the extension from the file name  
var extension =path.extname(request.param('filename'));      

// Get the base file name  
var baseFilename =path.basename(request.param('filename'), extension);      

// Create the temporary file name for the chunk  
var tempfilename =baseFilename + '.'+      
request.param('chunkNumber').toString().padLeft('0', 16) + extension + ".tmp";      

// Create the temporary directory to store the file chunk  
// The temporary directory will be based on the file name  
var tempdir =uploadpath + request.param('directoryname')+ '/' + baseFilename;      

// The path to save the file chunk  
var localfilepath =tempdir + '/'+ tempfilename;      

if (fs.ensureDirSync(tempdir)) {      
    console.log('Created directory ' +tempdir);  
}

正如我之前提出的,我們可以通過兩種方式上傳文件到后端服務器。第一種方式是在web瀏覽器中使用FormData,然后把文件塊作為二進制數(shù)據(jù)發(fā)送,另一種方式是把文件塊轉(zhuǎn)換成base64編碼的字符串,然后創(chuàng)建一個手工的multipart/form-data encoded請求,然后發(fā)送到后端服務器。

所以我們需要檢查一下是否在上傳的是一個手工multipart/form-data encoded請求,通過檢查”CelerFT-Encoded”頭部信息,如果這個頭部存在,我們創(chuàng)建一個buffer并使用request的ondata時間把數(shù)據(jù)拷貝到buffer中。

在request的onend事件中通過將buffer呈現(xiàn)為字符串并按CRLF分開,從而從 multipart/form-data encoded請求中提取base64字符串。base64編碼的文件塊可以在數(shù)組的第四個索引中找到。

通過創(chuàng)建一個新的buffer來將base64編碼的數(shù)據(jù)重現(xiàn)轉(zhuǎn)換為二進制。隨后調(diào)用fs.outputFileSync()方法將buffer寫入文件中。

// Check if we have uploaded a hand crafted multipart/form-data request  
// If we have done so then the data is sent as a base64 string  
// and we need to extract the base64 string and save it  
if (request.headers['celerft-encoded']=== 'base64') {     

    var fileSlice = newBuffer(+request.headers['content-length']);      
    var bufferOffset = 0;      

    // Get the data from the request  
    request.on('data', function (chunk) {      
        chunk.copy(fileSlice , bufferOffset);      
        bufferOffset += chunk.length;      
    }).on('end', function() {      
        // Convert the data from base64 string to binary  
        // base64 data in 4th index of the array  
        var base64data = fileSlice.toString().split('\r\n');      
        var fileData = newBuffer(base64data[4].toString(), 'base64');      

        fs.outputFileSync(localfilepath,fileData);      
        console.log('Saved file to ' +localfilepath);      

        // Send back a sucessful response with the file name  
        response.status(200).send(localfilepath);      
        response.end();      
    });  
}

二進制文件塊的上傳是通過formidable模塊來處理的。我們使用formidable.IncomingForm()方法得到multipart/form-data encoded請求。formidable模塊將把上傳的文件塊保存為一個單獨的文件并保存到臨時目錄。我們需要做的是在formidable的onend事件中將上傳的文件塊保存為里一個名字。

else {      
    // The data is uploaded as binary data.      
    // We will use formidable to extract the data and save it      
    var form = new formidable.IncomingForm();      
    form.keepExtensions = true;      
    form.uploadDir = tempdir;     

    // Parse the form and save the file chunks to the      
    // default location      
    form.parse(request, function (err, fields, files) {      
        if (err){      
            response.status(500).send(err);      
            return;      
        }      

    //console.log({ fields: fields, files: files });      
    });      

    // Use the filebegin event to save the file with the naming convention      
    /*form.on('fileBegin', function (name, file) {  
    file.path = localfilepath;  
});*/       

form.on('error', function (err) {      
        if (err){      
            response.status(500).send(err);      
            return;      
        }      
    });      

    // After the files have been saved to the temporary name      
    // move them to the to teh correct file name      
    form.on('end', function (fields,files) {      
        // Temporary location of our uploaded file             
        var temp_path = this.openedFiles[0].path;      

        fs.move(temp_path , localfilepath,function (err){      

            if (err) {      
                response.status(500).send(err);      
                return;      
            }      
            else {      
                // Send back a sucessful response with the file name      
                response.status(200).send(localfilepath);      
                response.end();      
            }     
        });     
    });      

// Send back a sucessful response with the file name      
//response.status(200).send(localfilepath);      
//response.end();      
}  
}

app.get()方法使用來處理”MergeAll”請求的。這個方法實現(xiàn)了之前描述過的功能。

// Request to merge all of the file chunks into one file  
app.get('*/api/CelerFTFileUpload/MergeAll*', function(request,response) {      

    if (request.method === 'GET') {      

        // Get the extension from the file name  
        var extension =path.extname(request.param('filename'));      

        // Get the base file name  
        var baseFilename =path.basename(request.param('filename'), extension);      

        var localFilePath =uploadpath + request.param('directoryname')+ '/' + baseFilename;      

        // Check if all of the file chunks have be uploaded  
        // Note we only wnat the files with a *.tmp extension  
        var files =getfilesWithExtensionName(localFilePath, 'tmp')      
        /*if (err) {  
            response.status(500).send(err);  
            return;  
        }*/ 

        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);      

        // Done writing the file  
        // Move it to top level directory  
        // and create MD5 hash  
        outputFile.on('finish', function (){      
            console.log('file has been written');      
            // New name for the file  
            var newfilename = uploadpath +request.param('directoryname')+ '/' + baseFilename  
            + extension;      

            // Check if file exists at top level if it does delete it  
            //if (fs.ensureFileSync(newfilename)) {  
            fs.removeSync(newfilename);      
            //} 

            // Move the file  
            fs.move(filename, newfilename ,function (err) {      
                if (err) {      
                    response.status(500).send(err);      
                    return;      
                }      
                else {      
                    // Delete the temporary directory  
                    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');      
                        // Send back a sucessful response with the file name  
                        response.status(200).send('Sucessfully merged file ' + filename + ", "     
                        + md5results.toUpperCase());      
                        response.end();      
                    });      
                }      
            });      
        });      

        // Loop through the file chunks and write them to the file  
        // files[index] retunrs the name of the file.  
        // we need to add put in the full path to the file  
        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實現(xiàn)的。

// String padding left code taken from  
// http://www.lm-tech.it/Blog/post/2012/12/01/String-Padding-in-Javascript.aspx  
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;  
}   ;

一些其它事情

其中一件事是,發(fā)表上篇文章后我繼續(xù)研究是為了通過域名碎片實現(xiàn)并行上傳到CeleFT功能。域名碎片的原理是訪問一個web站點時,讓web瀏覽器建立更多的超過正常允許范圍的并發(fā)連接。 域名碎片可以通過使用不同的域名(如web1.example.com,web2.example.com)或者不同的端口號(如8000, 8001)托管web站點的方式實現(xiàn)。

示例中,我們使用不同端口號托管web站點的方式。

我們使用 iisnode 把 Node.js集成到 IIS( Microsoft Internet Information Services)實現(xiàn)這一點。 下載兼容你操作系統(tǒng)的版本 iisnode (x86) 或者 iisnode (x64)。 下載 IIS URL重寫包。

一旦安裝完成(假定windows版Node.js已安裝),到IIS管理器中創(chuàng)建6個新網(wǎng)站。將第一個網(wǎng)站命名為CelerFTJS并且將偵聽端口配置為8000。

圖片7在IIS管理器中創(chuàng)建一個新網(wǎng)站

然后創(chuàng)建其他的網(wǎng)站。我為每一個網(wǎng)站都創(chuàng)建了一個應用池,并且給應用池“LocalSystem”級別的權(quán)限。所有網(wǎng)站的本地路徑是C:\inetpub\wwwroot\CelerFTNodeJS。

圖片8 文件夾層級

我在Release模式下編譯了Node.js應用,然后我拷貝了server.js文件、Script文件夾以及node_modules文件夾到那個目錄下。

要讓包含 iisnode 的Node.js的應用工作,我們需要創(chuàng)建一個web.config文件,并在其中添加如下得內(nèi)容。

<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重寫規(guī)則

如果你正確的做完了所有的工作,你就可以通過http://localhost:8000瀏覽網(wǎng)站,并進入CelerFT ”Default.html”頁面。

web.config文件被修改以支持如前面post中所解釋的大文件的上傳,這里我不會解釋所有的項。不過下面的web.config項可以改善 iisnode中Node.js的性能。

并行上傳

為了使用域名碎片來實現(xiàn)并行上傳,我不得不給Node.js應用做些修改。我第一個要修改的是讓Node.js應用支持跨域資源共享。我不得不這樣做是因為使用域碎片實際上是讓一個請求分到不同的域并且同源策略會限制我的這個請求。

好消息是XMLttPRequest 標準2規(guī)范允許我這么做,如果網(wǎng)站已經(jīng)把跨域資源共享打開,更好的是我不用為了實現(xiàn)這個而變更在”workeruploadchunk.js”里的上傳方法。

// 使用跨域資源共享 // Taken from http://bannockburn.io/2013/09/cross-origin-resource-sharing-cors-with-a-node-js-express-js-and-sencha-touch-app/  
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'   )   ;  

    // 攔截OPTIONS方法
    if ('OPTIONS' ==request.method){      
        response.send(204);      
    }      
    else {      
        next();      
    }      
}   ;        

// 在表達式中使用跨域資源共享
app.   use   (   enableCORS   )   ;

為了使server.js文件中得CORS可用,我創(chuàng)建了一個函數(shù),該函數(shù)會創(chuàng)建必要的頭以表明Node.js應用支持CORS。另一件事是我還需要表明CORS支持兩種請求,他們是:

  • 簡單請求:

1、只用GET,HEAD或POST。如果使用POST向服務器發(fā)送數(shù)據(jù),那么發(fā)送給服務器的HTTP POST請求的Content-Type應是application/x-www-form-urlencoded, multipart/form-data, 或 text/plain其中的一個。

2、HTTP請求中不要設置自定義的頭(例如X-Modified等)

  • 預檢請求:

1、使用GET,HEAD或POST以外的方法。假設使用POST發(fā)送請求,那么Content-Type不能是application/x-www-form-urlencoded, multipart/form-data, or text/plain,例如假設POST請求向服務器發(fā)送了XML有效載荷使用了application/xml or text/xml,那么這個請求就是預檢的。

2、在請求中設置自定義頭(比如請求使用X-PINGOTHER頭)。

在我們的例子中,我們用的是簡單請求,所以我們不需要做其他得工作以使例子能夠工作。

在 ”workeruploadchunk.js” 文件中,我向 self.onmessage 事件添加了對進行并行文件數(shù)據(jù)塊上傳的支持.

// We are going to upload to a backend that supports parallel uploads.  
// Parallel uploads is supported by publishng the web site on different ports  
// The backen must implement CORS for this to work  
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 {      
        // Increment the port numbers, e.g 8000, 8001, 8002, 8003, 8004, 8005  
        uploadurl = workerdata.currentlocation.slice(0, -1) + urlcount +webapiUrl +      
        urlnumber;      
    }      

    upload(workerdata.chunk,workerdata.filename,workerdata.chunkCount, uploadurl,      
    workerdata.asyncstate);      
    urlcount++;      
    urlnumber++;  
  }

在 Default.html 頁面我對當前的URL進行了保存,因為我準備把這些信息發(fā)送給文件上傳的工作程序. 只所以這樣做是因為:

  • 我想要利用這個信息增加端口數(shù)量
  • 做了 CORS 請求,我需要把完整的 URL 發(fā)送給 XMLHttpRequest 對象.

最后修改了 CelerFT 接口來支持并行上傳.

帶有并行上傳的CelerFT

  哈爾濱品用軟件有限公司致力于為哈爾濱的中小企業(yè)制作大氣、美觀的優(yōu)秀網(wǎng)站,并且能夠搭建符合百度排名規(guī)范的網(wǎng)站基底,使您的網(wǎng)站無需額外費用,即可穩(wěn)步提升排名至首頁。歡迎體驗最佳的哈爾濱網(wǎng)站建設