解剖一個 HTTP 事務
來自專欄 前端搬運工https://nodejs.org/en/docs/guides/anatomy-of-an-http-transaction/
本指南的目的是讓你深刻理解 Node.js HTTP 的處理過程。假設你知道 HTTP 請求是如何工作的,無論是哪種編程語言或者編程環境。也假設你對 Node.js 的 EventEmitters 和 Stream 有有了解。如果你不是很熟悉他們,希望你快速通讀這兩個 API 文檔。Anatomy of an HTTP Transaction | Node.js本指南的目的是讓你深刻理解 Node.js HTTP 的處理過程。假設你知道 HTTP 請求是如何工作的,無論是哪種編程語言或者編程環境。也假設你對 Node.js 的 EventEmitters 和 Stream 有有了解。如果你不是很熟悉他們,希望你快速通讀這兩個 API 文檔。
創建一個伺服器
任何一個 node web 伺服器程序在某一個時刻不得不創建一個 web 伺服器對象。可以通過 createServer 完成。
const http = require(http);const server = http.createServer((request, response) => { // magic happens here})
一旦有 HTTP 請求,傳遞給 createServer 的函數就會被伺服器調用,因此被稱作 請求處理器。實際上,createServer 返回的 Server 對象是一個 EventEmitter 實例,這裡寫的僅僅是對 創建一個 server 對象然後添加一個監聽器 的一個簡寫。
const server = http.createServer();server.on(request, (request, response) => { // the same kind of magic happens here!});
當一個 HTTP 請求到達伺服器,node 調用帶有易得的對象參數的請求處理函數來處理事務,request 和 response。不久,就會得到。
為了服務請求,listen 方法需要在 server 上面調用。大多數情況下,你需要傳遞給 listen 的是一個你想要監聽的埠號。也有其他的配置選項,具體請查閱文檔。
方法,URL 和 Headers
當處理一個請求的時候,你可能做的第一件事就是查看請求方法和URL,以便採取合適的處理程序。Node 通過將屬性放在 request 對象上面,將這變得非常棒。
const { method, url } = request;
注意 request 對象事 IncomingMessage 的實例
這裡的 method 永遠是一個正常的 HTTP 請求方法(動詞)。url 是一個沒有伺服器域名,協議或者埠號的完整 URL。
Headers 也在 request 上面。
const { headers } = request;const userAgent = headers[user-agent];
需要注意的是所有的頭部是小寫的,無論客戶端如何發送的。簡化了無論何種目的解析 Headers 的任務。
如果頭部重複了,它們的值被重寫或者通過逗號分隔開,取決於哪種頭部信息。在某些情況下,這可能有問題的,因此 rawHeader 也是可取的。
請求體
當收到一個 post 或者 put 的請求時,請求體對於你的程序非常重要。獲取請求體比獲取請求頭要複雜些。請求對象被傳遞給一個實現了 ReadableStream 介面的處理程序。這個流可以被監聽或者像其他流那樣導入到任何地方。我們可以通過監聽流的 data 和 end 事件從流中獲取數據。
在每一個 data 事件中傳遞的塊(chunk)是 Buffer。如果你知道它是字元串數據,最好的方法是用數組收集,然後在 end 事件中,連接並且轉換為 string。
let body = [];request.on(data, (chunk) => { body.push(chunk);}).on(end, () => { body = Buffer.concat(body).toString(); // at this point, `body` has the entire request body stored in it as a string});
注意:這可能有點兒乏味,在很多例子中,確實是。幸運的是,有些在 npm 上面的模塊 concat-stream 和 body 可以幫助隱藏一些邏輯。但是理解這個過程也非常重要,這就是你為什在這兒。
快速了解下 Errors
既然 request 是一個 ReadableStream,它也是一個 EventEmitter 的實例對象,當錯誤發生的時候。
在請求流中出現一個錯誤,通過在流中觸發一個 error 事件。如果你沒有 error 事件的監聽器,錯誤會被拋出,將會使你的 Node.js 程序退出。因此你應該在你的請求流中添加 error 事件監聽器,即使你需要進行日誌記錄和按你的方式繼續運行(不過最好發送一些錯誤響應,待會談)。
request.on(error, (err) => { // This prints the error message and stack trace to `stderr`. console.error(err.stack);});
這裡有一些其他的抽象和工具解決這些錯誤的方法(鏈接),但是應該一直意識到這些錯誤可以發生,但你要處理它們。
目前為止都談了什麼
此時,我們談了 創建一個伺服器,從請求中獲取方法,URL,請求頭和請求體。當我們把它們放到一起時,像這樣:
const http = require(http);http.createServer((request, response) => { const { headers, method, url } = request; let body = []; request.on(error, (err) => { console.error(err); }).on(data, (chunk) => { body.push(chunk); }).on(end, () => { body = Buffer.concat(body).toString(); // At this point, we have the headers, method, url and body, and can now // do whatever we need to in order to respond to this request. });}).listen(8080); // Activates this server, listening on port 8080.
如果運行這個例子,我們會接受到請求但是沒有響應它們。實際上,如果你在瀏覽器中請求這個伺服器,你的請求將會超時,因為沒有給客戶端發送響應。
目前為止,還沒有接觸到 response 對象,是一個 ServerResponse 的實例,屬於 WritableStream。它包含了很多將數據發送到客戶端有用的方法。等會兒我們會講到。
HTTP 狀態碼
如果你沒有設置它,在一個響應上的 HTTP 狀態碼將永遠是 200。當然並不是每個 HTTP 都是這樣的,有時候你需要定義發送一個不同的狀態碼。你可以通過設置 statusCode 屬性來完成。
response.statusCode = 404; // Tell the client that the resource wasnt found.
設置響應頭
頭部通過一個方便的方法 setHeader 來設置的。
response.setHeader(Content-Type, application.json);response.setHeader(X-Powered-By, bacon);
當在一個響應上面設置頭部時,大小寫不敏感。如果你重複設置一個頭部信息,你設置的最後一個值將會被發送。
明確地發送頭部數據
設置頭部和狀態碼地方法我們已經討論過了,假設你使用的是「隱式頭部」。這意味著你依靠 node 在正確的時間(在你發送 body 數據之前)發送頭部。
如果你想,你可以明確地將頭部信息寫入響應流中。這裡有一個 writeHead 方法可以完成,可以將狀態碼和頭部信息寫入流中。
response.writeHead(200, { Content-Type: application/json, X-Powered-By: bacon});
一旦你設置了頭部信息(隱式 或 顯式),就已經準備發送響應數據了。
發送響應體
因為 response 對象式 WritableStream,往發送到客戶端的響應體中寫入數據跟平時一些流的方法一樣。
response.write(<html>);response.write(<body>);response.write(<h1>Hello, World!</h1>);response.write(</body>);response.write(</html>);response.end();
在流中的 end 函數可以將最後需要發送的數據作為參數,因此我們可以這樣寫。
response.end(<html><body><h1>Hello, World!</h1></body></html>);
注意:在往響應體中寫入數據塊之前設置狀態碼和頭部非常重要。很容易理解在 HTTP 響應之前頭部在響應體之前。
再快速了解下另外一個 Errors
response 流也會觸發 error 事件,在某些時候你也不得不要處理這些。所有 request 流的錯誤的建議也同樣適用於這裡。
把它們都放在一起
目前我們學習了 HTTP 響應,讓我們將它們放在一起。在前面例子的基礎上,我們創建一個伺服器並且響應用戶的請求。我們用 JSON.stringify 格式化數據。
const http = require(http);http.createServer((request, response) => { const { headers, method, url } = request; let body = []; request.on(error, (err) => { console.error(err); }).on(data, (chunk) => { body.push(chunk); }).on(end, () => { body = Buffer.concat(body).toString(); // BEGINNING OF NEW STUFF response.on(error, (err) => { console.error(err); }); response.statusCode = 200; response.setHeader(Content-Type, application/json); // Note: the 2 lines above could be replaced with this next one: // response.writeHead(200, {Content-Type: application/json}) const responseBody = { headers, method, url, body }; response.write(JSON.stringify(responseBody)); response.end(); // Note: the 2 lines above could be replaced with this next one: // response.end(JSON.stringify(responseBody)) // END OF NEW STUFF });}).listen(8080);
一個伺服器示例
讓我們簡化之前的例子製作一個簡單的伺服器,僅僅將接受的請求數據發送回去。我們需要做的就是從請求流中獲取數據和往響應流中寫入數據,就像我們之前做的。
const http = require(http);http.createServer((request, response) => { let body = []; request.on(data, (chunk) => { body.push(chunk); }).on(end, () => { body = Buffer.concat(body).toString(); response.end(body); });}).listen(8080);
現在讓我們調整下。我們僅僅像在下面的條件時,響應數據。
- 請求方法是 POST
- URL 是 /echo
其他情況下,簡單的響應 404。
const http = require(http);http.createServer((request, response) => { if(request.method === POST && request.url === /echo) { let body = []; request.on(data, chunk => { body.push(chunk); }).on(end, () => { body = Buffer.concat(body).toString(); response.end(body); }) } else { response.statusCode = 404; response.end(); }}).listen(8080);
注意:通過這種方式檢查 URL,我們做的是一種路由的形式。其他的比如 switch 簡單的路由方式,或者像 express 框架那樣的複雜的路由形式。如果僅僅是路由的功能你可以嘗試下 router。
棒極了,現在嘗試簡化一下。request 對象是 ReadableStream,response 對象是 WritableStream。這意味著我們可以使用 pipe 方法直接將數據從一個地方搬到另一個地方。這就是我們想要在示例伺服器中要做的。
const http = require(http);http.createServer((request, response) => { if (request.method === POST && request.url === /echo) { request.pipe(response); } else { response.statusCode = 404; response.end(); }}).listen(8080);
?,數據流!
還沒有完成。在本指南前面多次提到,錯誤可以發生,我們需要解決它們。
為了在請求流中處理錯誤,我們將向 stderr 列印錯誤並且發送個 400 狀態碼錶明這是個錯誤的請求。在實際應用中,我們要檢查錯誤並且找出正確的狀態碼和消息。像往常一樣,你應該查閱關於錯誤的文檔。
在響應中,僅僅將錯誤列印到 stderr。
const http = require(http);http.createServer((request, response) => { request.on(error, (err) => { console.error(err); response.statusCode = 400; response.end(); }); response.on(error, (err) => { console.error(err); }); if(request.method === POST && request.url === /echo) { request.pipe(response); } else { response.statusCode = 404; response.end(); }}).listen(8080);
目前我們涵蓋了處理 HTTP 的基礎流程。你應該學會了:
- 用請求處理函數實例化一個 HTTP 伺服器,並且將它監聽到一個埠。
- 從 request 對象中獲取頭部信息,URL,請求方法和請求體。
- 基於 URL 和 其他在 request 對象上面的數據實現路由功能。
- 通過 response 對象發送頭部信息,HTTP狀態碼,和響應體數據。
- 將數據從 request 對象裡面流入到 response 對象里。
- 在 request 和 response 流中處理錯誤。
從這些基礎知識點兒,可以構建很多典型的 HTTP 伺服器用例。還有大量的官方 API 文檔,確保你都看過一遍。
??
推薦閱讀:
※golang net/http的小問題?
※用 http 數據加密和 https 有什麼區別?
※HTTP請求的整個流程以及HTTPS可靠性的簡介。
※一個網頁的漂洋過海之旅:傳輸
※請正確使用http狀態碼,謝謝!
TAG:Nodejs | HTTP | StreamProcessing |