讓請求處理程序作出響應(yīng)

2018-02-24 15:25 更新

很好。不過現(xiàn)在要是請求處理程序能夠向瀏覽器返回一些有意義的信息而并非全是“Hello World”,那就更好了。

這里要記住的是,瀏覽器發(fā)出請求后獲得并顯示的“Hello World”信息仍是來自于我們_server.js_文件中的_onRequest_函數(shù)。

其實“處理請求”說白了就是“對請求作出響應(yīng)”,因此,我們需要讓請求處理程序能夠像_onRequest_函數(shù)那樣可以和瀏覽器進行“對話”。

不好的實現(xiàn)方式

對于我們這樣擁有PHP或者Ruby技術(shù)背景的開發(fā)者來說,最直截了當(dāng)?shù)膶崿F(xiàn)方式事實上并不是非常靠譜: 看似有效,實則未必如此。

這里我指的“直截了當(dāng)?shù)膶崿F(xiàn)方式”意思是:讓請求處理程序通過_onRequest_函數(shù)直接返回(return())他們要展示給用戶的信息。

我們先就這樣去實現(xiàn),然后再來看為什么這不是一種很好的實現(xiàn)方式。

讓我們從讓請求處理程序返回需要在瀏覽器中顯示的信息開始。我們需要將_requestHandler.js_修改為如下形式:

function start()  {
    console.log("Request handler 'start' was called.");? 
    return  "Hello Start";
}

function upload()  {
    console.log("Request handler 'upload' was called.");? 
    return  "Hello Upload";
}

exports.start = start;
exports.upload = upload;

好的。同樣的,請求路由需要將請求處理程序返回給它的信息返回給服務(wù)器。因此,我們需要將_router.js_修改為如下形式:

function route(handle, pathname)  {
? console.log("About to route a request for "  + pathname);? 
    if  (typeof handle[pathname]  ===  'function')  {? ? 
        return handle[pathname]();? }  
    else  {
? ?     console.log("No request handler found for "  + pathname);? ? 
        return  "404 Not found";? 
    }
}

exports.route = route;

正如上述代碼所示,當(dāng)請求無法路由的時候,我們也返回了一些相關(guān)的錯誤信息。

最后,我們需要對我們的_server.js_進行重構(gòu)以使得它能夠?qū)⒄埱筇幚沓绦蛲ㄟ^請求路由返回的內(nèi)容響應(yīng)給瀏覽器,如下所示:

var http = require("http");
var url = require("url");

function start(route, handle)  {? 
    function onRequest(request, response)  {? ? 
        var pathname = url.parse(request.url).pathname;
    ? ? console.log("Request for "  + pathname +  " received.");

    ? ? response.writeHead(200,  {"Content-Type":  "text/plain"});? ? 
        var content = route(handle, pathname)
    ? ? response.write(content);
    ? ? response.end();? 
    }

? http.createServer(onRequest).listen(8888);
? console.log("Server has started.");
}

exports.start = start;

如果我們運行重構(gòu)后的應(yīng)用,一切都會工作的很好:請求http://localhost:8888/start,瀏覽器會輸出“Hello Start”,請求http://localhost:8888/upload會輸出“Hello Upload”,而請求http://localhost:8888/foo?會輸出“404 Not found”。

好,那么問題在哪里呢?簡單的說就是: 當(dāng)未來有請求處理程序需要進行非阻塞的操作的時候,我們的應(yīng)用就“掛”了。

沒理解?沒關(guān)系,下面就來詳細解釋下。

阻塞與非阻塞

正如此前所提到的,當(dāng)在請求處理程序中包括非阻塞操作時就會出問題。但是,在說這之前,我們先來看看什么是阻塞操作。

我不想去解釋“阻塞”和“非阻塞”的具體含義,我們直接來看,當(dāng)在請求處理程序中加入阻塞操作時會發(fā)生什么。

這里,我們來修改下_start_請求處理程序,我們讓它等待10秒以后再返回“Hello Start”。因為,JavaScript中沒有類似_sleep()_這樣的操作,所以這里只能夠來點小Hack來模擬實現(xiàn)。

讓我們將_requestHandlers.js_修改成如下形式:

function start()  {
? console.log("Request handler 'start' was called.");? 
    function sleep(milliSeconds)  {? ? 
        var startTime =  new  Date().getTime();? ? 
        while  (new  Date().getTime()   startTime + milliSeconds);? 
    }

    sleep(10000);? 
    return  "Hello Start";
}

function upload()  {
    console.log("Request handler 'upload' was called.");? 
    return  "Hello Upload";
}

exports.start = start;
exports.upload = upload;

上述代碼中,當(dāng)函數(shù)_start()_被調(diào)用的時候,Node.js會先等待10秒,之后才會返回“Hello Start”。當(dāng)調(diào)用_upload()_的時候,會和此前一樣立即返回。

(當(dāng)然了,這里只是模擬休眠10秒,實際場景中,這樣的阻塞操作有很多,比方說一些長時間的計算操作等。)

接下來就讓我們來看看,我們的改動帶來了哪些變化。

如往常一樣,我們先要重啟下服務(wù)器。為了看到效果,我們要進行一些相對復(fù)雜的操作(跟著我一起做): 首先,打開兩個瀏覽器窗口或者標簽頁。在第一個瀏覽器窗口的地址欄中輸入http://localhost:8888/start, 但是先不要打開它!

在第二個瀏覽器窗口的地址欄中輸入http://localhost:8888/upload, 同樣的,先不要打開它!

接下來,做如下操作:在第一個窗口中(“/start”)按下回車,然后快速切換到第二個窗口中(“/upload”)按下回車。

注意,發(fā)生了什么: /start URL加載花了10秒,這和我們預(yù)期的一樣。但是,/upload URL居然_也_花了10秒,而它在對應(yīng)的請求處理程序中并沒有類似于_sleep()_這樣的操作!

這到底是為什么呢?原因就是_start()_包含了阻塞操作。形象的說就是“它阻塞了所有其他的處理工作”。

這顯然是個問題,因為Node一向是這樣來標榜自己的:“在node中除了代碼,所有一切都是并行執(zhí)行的”。

這句話的意思是說,Node.js可以在不新增額外線程的情況下,依然可以對任務(wù)進行并行處理 —— Node.js是單線程的。它通過事件輪詢(event loop)來實現(xiàn)并行操作,對此,我們應(yīng)該要充分利用這一點 —— 盡可能的避免阻塞操作,取而代之,多使用非阻塞操作。

然而,要用非阻塞操作,我們需要使用回調(diào),通過將函數(shù)作為參數(shù)傳遞給其他需要花時間做處理的函數(shù)(比方說,休眠10秒,或者查詢數(shù)據(jù)庫,又或者是進行大量的計算)。

對于Node.js來說,它是這樣處理的:“嘿,probablyExpensiveFunction()(譯者注:這里指的就是需要花時間處理的函數(shù)),你繼續(xù)處理你的事情,我(Node.js線程)先不等你了,我繼續(xù)去處理你后面的代碼,請你提供一個callbackFunction(),等你處理完之后我會去調(diào)用該回調(diào)函數(shù)的,謝謝!”

(如果想要了解更多關(guān)于事件輪詢細節(jié),可以閱讀Mixu的博文——理解node.js的事件輪詢。)

接下來,我們會介紹一種錯誤的使用非阻塞操作的方式。

和上次一樣,我們通過修改我們的應(yīng)用來暴露問題。

這次我們還是拿_start_請求處理程序來“開刀”。將其修改成如下形式:

var exec = require("child_process").exec;

function start()  {
? console.log("Request handler 'start' was called.");? 
    var content =  "empty";

    exec("ls -lah",  
        function  (error, stdout, stderr)  {? ? 
        content = stdout;? });? 
        return content;
    }

function upload()  {
    console.log("Request handler 'upload' was called.");? 
    return  "Hello Upload";
}

exports.start = start;
exports.upload = upload;

上述代碼中,我們引入了一個新的Node.js模塊,_childprocess。之所以用它,是為了實現(xiàn)一個既簡單又實用的非阻塞操作:exec()。

_exec()做了什么呢?它從Node.js來執(zhí)行一個shell命令。在上述例子中,我們用它來獲取當(dāng)前目錄下所有的文件(“l(fā)s -lah”),然后,當(dāng)/start_URL請求的時候?qū)⑽募畔⑤敵龅綖g覽器中。

上述代碼是非常直觀的: 創(chuàng)建了一個新的變量content(初始值為“empty”),執(zhí)行“l(fā)s -lah”命令,將結(jié)果賦值給content,最后將content返回。

和往常一樣,我們啟動服務(wù)器,然后訪問“http://localhost:8888/start” 。

之后會載入一個漂亮的web頁面,其內(nèi)容為“empty”。怎么回事?

這個時候,你可能大致已經(jīng)猜到了,_exec()_在非阻塞這塊發(fā)揮了神奇的功效。它其實是個很好的東西,有了它,我們可以執(zhí)行非常耗時的shell操作而無需迫使我們的應(yīng)用停下來等待該操作。

(如果想要證明這一點,可以將“l(fā)s -lah”換成比如“find /”這樣更耗時的操作來效果)。

然而,針對瀏覽器顯示的結(jié)果來看,我們并不滿意我們的非阻塞操作,對吧?

好,接下來,我們來修正這個問題。在這過程中,讓我們先來看看為什么當(dāng)前的這種方式不起作用。

問題就在于,為了進行非阻塞工作,_exec()_使用了回調(diào)函數(shù)。

在我們的例子中,該回調(diào)函數(shù)就是作為第二個參數(shù)傳遞給_exec()_的匿名函數(shù):

function  (error, stdout, stderr)  {? 
    content = stdout;
}

現(xiàn)在就到了問題根源所在了:我們的代碼是同步執(zhí)行的,這就意味著在調(diào)用_exec()_之后,Node.js會立即執(zhí)行?return content?;在這個時候,_content_仍然是“empty”,因為傳遞給_exec()_的回調(diào)函數(shù)還未執(zhí)行到——因為_exec()_的操作是異步的。

我們這里“l(fā)s -lah”的操作其實是非常快的(除非當(dāng)前目錄下有上百萬個文件)。這也是為什么回調(diào)函數(shù)也會很快的執(zhí)行到 —— 不過,不管怎么說它還是異步的。

為了讓效果更加明顯,我們想象一個更耗時的命令: “find /”,它在我機器上需要執(zhí)行1分鐘左右的時間,然而,盡管在請求處理程序中,我把“l(fā)s -lah”換成“find /”,當(dāng)打開/start URL的時候,依然能夠立即獲得HTTP響應(yīng) —— 很明顯,當(dāng)_exec()_在后臺執(zhí)行的時候,Node.js自身會繼續(xù)執(zhí)行后面的代碼。并且我們這里假設(shè)傳遞給_exec()_的回調(diào)函數(shù),只會在“find /”命令執(zhí)行完成之后才會被調(diào)用。

那究竟我們要如何才能實現(xiàn)將當(dāng)前目錄下的文件列表顯示給用戶呢?

好,了解了這種不好的實現(xiàn)方式之后,我們接下來來介紹如何以正確的方式讓請求處理程序?qū)g覽器請求作出響應(yīng)。

以非阻塞操作進行請求響應(yīng)

我剛剛提到了這樣一個短語 —— “正確的方式”。而事實上通常“正確的方式”一般都不簡單。

不過,用Node.js就有這樣一種實現(xiàn)方案: 函數(shù)傳遞。下面就讓我們來具體看看如何實現(xiàn)。

到目前為止,我們的應(yīng)用已經(jīng)可以通過應(yīng)用各層之間傳遞值的方式(請求處理程序 -> 請求路由 -> 服務(wù)器)將請求處理程序返回的內(nèi)容(請求處理程序最終要顯示給用戶的內(nèi)容)傳遞給HTTP服務(wù)器。

現(xiàn)在我們采用如下這種新的實現(xiàn)方式:相對采用將內(nèi)容傳遞給服務(wù)器的方式,我們這次采用將服務(wù)器“傳遞”給內(nèi)容的方式。 從實踐角度來說,就是將_response_對象(從服務(wù)器的回調(diào)函數(shù)_onRequest()_獲?。┩ㄟ^請求路由傳遞給請求處理程序。 隨后,處理程序就可以采用該對象上的函數(shù)來對請求作出響應(yīng)。

原理就是如此,接下來讓我們來一步步實現(xiàn)這種方案。

先從_server.js_開始:

var http = require("http");
var url = require("url");

function start(route, handle)  {? 
    function onRequest(request, response)  {? ? 
        var pathname = url.parse(request.url).pathname;
    ? ? console.log("Request for "  + pathname +  " received.");

    ? ? route(handle, pathname, response);? 
    }

    http.createServer(onRequest).listen(8888);
    console.log("Server has started.");
}

exports.start = start;

相對此前從_route()_函數(shù)獲取返回值的做法,這次我們將response對象作為第三個參數(shù)傳遞給_route()_函數(shù),并且,我們將_onRequest()_處理程序中所有有關(guān)_response_的函數(shù)調(diào)都移除,因為我們希望這部分工作讓_route()_函數(shù)來完成。

下面就來看看我們的router.js:

function route(handle, pathname, response)  {
?     console.log("About to route a request for "  + pathname);? 
    if  (typeof handle[pathname]  ===  'function')  {
? ?     handle[pathname](response);? }  
    else  {
    ? ? console.log("No request handler found for "  + pathname);
    ? ? response.writeHead(404,  {"Content-Type":  "text/plain"});
    ? ? response.write("404 Not found");
    ? ? response.end();? 
    }
}

exports.route = route;

同樣的模式:相對此前從請求處理程序中獲取返回值,這次取而代之的是直接傳遞_response_對象。

如果沒有對應(yīng)的請求處理器處理,我們就直接返回“404”錯誤。

最后,我們將_requestHandler.js_修改為如下形式:

var exec = require("child_process").exec;

function start(response)  {
? console.log("Request handler 'start' was called.");

? exec("ls -lah",  function  (error, stdout, stderr)  {
? ? response.writeHead(200,  {"Content-Type":  "text/plain"});
? ? response.write(stdout);
? ? response.end();? });
}

function upload(response)  {
? console.log("Request handler 'upload' was called.");
? response.writeHead(200,  {"Content-Type":  "text/plain"});
? response.write("Hello Upload");
? response.end();
}

exports.start = start;
exports.upload = upload;

我們的處理程序函數(shù)需要接收response參數(shù),為了對請求作出直接的響應(yīng)。

_start_處理程序在_exec()_的匿名回調(diào)函數(shù)中做請求響應(yīng)的操作,而_upload_處理程序仍然是簡單的回復(fù)“Hello World”,只是這次是使用_response_對象而已。

這時再次我們啟動應(yīng)用(node index.js),一切都會工作的很好。

如果想要證明_/start處理程序中耗時的操作不會阻塞對/upload_請求作出立即響應(yīng)的話,可以將_requestHandlers.js_修改為如下形式:

var exec = require("child_process").exec;

function start(response)  {
? console.log("Request handler 'start' was called.");

? exec("find /",? ? { timeout:  10000, maxBuffer:  20000*1024  },? ? function  (error, stdout, stderr)  {
? ? ? response.writeHead(200,  {"Content-Type":  "text/plain"});
? ? ? response.write(stdout);
? ? ? response.end();? ? });
}

function upload(response)  {
? console.log("Request handler 'upload' was called.");
? response.writeHead(200,  {"Content-Type":  "text/plain"});
? response.write("Hello Upload");
? response.end();
}

exports.start = start;
exports.upload = upload;

這樣一來,當(dāng)請求http://localhost:8888/start的時候,會花10秒鐘的時間才載入,而當(dāng)請求http://localhost:8888/upload的時候,會立即響應(yīng),縱然這個時候/start響應(yīng)還在處理中。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號