英文標(biāo)題:Anatomy of an HTTP Transaction(https://nodejs.org/en/docs/guides/anatomy-of-an-http-transaction/)
本教程的目的在于傳授對(duì)Node.js處理HTTP進(jìn)程的扎實(shí)的理解。在不考慮編程語(yǔ)言和編程環(huán)境的情況下,我們假設(shè)你大體上了解HTTP請(qǐng)求是怎么工作的,同樣也假設(shè)你了解nodejs的 EventEmitters(事件發(fā)射器) 和 Streams(流)是怎么一回事。如果你不太了解它們,那請(qǐng)你先快速閱讀一下它們的API文檔。
創(chuàng)建一個(gè)服務(wù)
任何node的web服務(wù)應(yīng)用在某種情況下都必須創(chuàng)建一個(gè)web服務(wù)對(duì)象(server object)。這個(gè)對(duì)象通過(guò) createServer創(chuàng)建。
const http = require('http');
const server = http.createServer((request, response) => {
// magic happens here!
});
這個(gè)傳到createServer 里的方法(function),在每一次接收針對(duì)這個(gè)服務(wù)發(fā)送的HTTP請(qǐng)求時(shí),都會(huì)被調(diào)用一次,因此我們稱(chēng)這個(gè)方法為請(qǐng)求處理者(request handler)。這個(gè)由createServer返回的服務(wù)對(duì)象實(shí)際上就是個(gè) EventEmitter,它是創(chuàng)建服務(wù)對(duì)象的簡(jiǎn)寫(xiě)方式,之后加了一個(gè)監(jiān)聽(tīng)器(listener)。
const server = http.createServer();
server.on('request', (request, response) => {
// the same kind of magic happens here!
});
當(dāng)一個(gè)HTTP請(qǐng)求到達(dá)這個(gè)服務(wù)時(shí),node就會(huì)調(diào)用這個(gè)請(qǐng)求處理者方法并用一系列便于使用的對(duì)象去處理這次業(yè)務(wù)(transaction)、請(qǐng)求(request),和回應(yīng)(respose)。我們稍后會(huì)談到它們。
為了服務(wù)請(qǐng)求,服務(wù)對(duì)象上的listen方法需要被調(diào)用。在多數(shù)情況下,你只需要將你想要監(jiān)聽(tīng)的端口數(shù)字傳給listen方法即可。不過(guò)這里也有其他參數(shù)選項(xiàng),請(qǐng)參考API reference。
方法,URL和頭(Method, URL and Headers)
當(dāng)我們處理一個(gè)請(qǐng)求時(shí),我們往往首先會(huì)看這個(gè)請(qǐng)求的方法和URL,以此來(lái)找尋合適的動(dòng)作(actions)處理它。Node在請(qǐng)求對(duì)象上(request object)上加了一些便于使用的屬性,讓我們處理起來(lái)相對(duì)輕松一些。
const { method, url } = request;
注意:這里的request是IncomingMessage的實(shí)例.
這里的method通常是HTTP的方法/動(dòng)詞(method/verb)。這個(gè)url是不包含服務(wù)、協(xié)議、端口的完整URL地址。一個(gè)典型的URL,即從包括第三條正斜線(xiàn)以?xún)?nèi)的后面所有部分(譯者按:假設(shè)一段地址是,http://www.itdecent.cn/writer#/notebooks/16260155/notes/16769868/preview,典型URL應(yīng)該就指/writer#/notebooks/16260155/notes/16769868/preview這部分)。
頭(Headers)則在request一個(gè)屬性名叫headers的對(duì)象里。
const { headers } = request;
const userAgent = headers['user-agent'];
這里需要強(qiáng)調(diào)的是,無(wú)論客戶(hù)端如何發(fā)送頭部信息,所有的頭都以小寫(xiě)形式呈現(xiàn),這簡(jiǎn)化了解析頭部信息的工作。
如果頭部信息重復(fù),那么它們的值會(huì)被重寫(xiě),或者組合成以逗號(hào)相隔的字符串。在一些情況下,這可能會(huì)導(dǎo)致問(wèn)題,所以也可以傳入rawHeaders。
請(qǐng)求主體(Request Body)
當(dāng)我們接受到一個(gè)POST或PUT請(qǐng)求,請(qǐng)求主體對(duì)我們的應(yīng)用來(lái)說(shuō)就至關(guān)重要了。獲取請(qǐng)求主體的數(shù)據(jù)比獲取請(qǐng)求頭更麻煩。傳入處理程序的請(qǐng)求對(duì)象經(jīng)過(guò)了ReadableStream的接口。我們可以監(jiān)聽(tīng)這個(gè)流,或者將它向其他流一樣傳到其他地方去。通過(guò)監(jiān)聽(tīng)流的'data'和'end'事件,我們可以抓到這個(gè)流的數(shù)據(jù)。
每一個(gè)'data'事件由釋放出來(lái)的塊都是一個(gè)緩存(Buffer)。如果這個(gè)緩存以字符串形式存在,那么最好先將其轉(zhuǎn)化成數(shù)組形式,然后在end事件里再將其字符串化。
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
});
注意:在多數(shù)情況下,這樣做看起來(lái)很繁瑣。幸運(yùn)的是,在 npm 上,我們有很多像concat-stream和 body 一樣的組件,它們可以幫助我們簡(jiǎn)化一些這樣的繁瑣邏輯。
在繼續(xù)探索之前,對(duì)事情怎么發(fā)生的有一個(gè)好的理解很重要,這也是你走到這里的原因!
關(guān)于錯(cuò)誤的一件小事(errors)
因?yàn)檎?qǐng)求對(duì)象(request object)是一個(gè)可讀的流(ReadableStream),同時(shí)也是事件發(fā)射器(EventEmitter),當(dāng)一個(gè)錯(cuò)誤發(fā)生時(shí),它們的表現(xiàn)一樣。請(qǐng)求流的錯(cuò)誤通過(guò)發(fā)送'error'事件表現(xiàn)出來(lái)。如果你沒(méi)有監(jiān)聽(tīng)這個(gè)事件,這個(gè)錯(cuò)誤將會(huì)被thrown掉,這會(huì)導(dǎo)致Node.js程序崩潰。因此,即使你記錄了這個(gè)錯(cuò)誤并讓程序繼續(xù)跑,你也需要在這個(gè)請(qǐng)求流上加一個(gè)'錯(cuò)誤'監(jiān)聽(tīng)器。(最好是發(fā)送類(lèi)似HTTP error response的響應(yīng)。我們一會(huì)再講。 )
到目前為止,我們收獲了什么
到目前為止,我們創(chuàng)建了一個(gè)服務(wù),知道了方法(method)、URL、頭(headers)和請(qǐng)求主體(body out of requests)。當(dāng)我們將它們放到一塊,將會(huì)得到以下東西:
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.
如果我們運(yùn)行這個(gè)例子,我們將能獲得請(qǐng)求(requests),但是卻沒(méi)有回應(yīng)(respond)。實(shí)際上,如果你在網(wǎng)頁(yè)上跑這個(gè)例子,我的請(qǐng)求將超時(shí),沒(méi)有東西返回給客戶(hù)端。
到目前為止,我們還沒(méi)碰過(guò)響應(yīng)對(duì)象(response object),響應(yīng)對(duì)象是 ServerResponse的一個(gè)實(shí)例,同時(shí)也是一個(gè)可寫(xiě)的流 WritableStream。它包含了很多很有用的方法,以此發(fā)回?cái)?shù)據(jù)給客戶(hù)端。我們接下來(lái)談?wù)搑esponse。
HTTP Status Code
如果你不清楚怎么設(shè)置它,記住HTTP response里的 status code 通常都是200。當(dāng)然,也不是所有HTTP response都是200,而且有些時(shí)候你也要用到其他status code值。因此,你需要設(shè)置statusCode屬性。
response.statusCode = 404; // Tell the client that the resource wasn't found.
我們也可以通過(guò)其他捷徑設(shè)置statusCode,下面來(lái)看一看。
設(shè)置響應(yīng)頭(response headers)。
通過(guò)setHeader方法,我們可以很方便的設(shè)置頭信息。
response.setHeader('Content-Type', 'application/json');
response.setHeader('X-Powered-By', 'bacon');
大小寫(xiě)對(duì)響應(yīng)頭的設(shè)置沒(méi)影響,如果你重復(fù)設(shè)置了一個(gè)頭,只有最后一句代碼生效。
顯示地發(fā)送頭部數(shù)據(jù)
我們假設(shè)你用我們上述“隱式的頭”的方式來(lái)設(shè)置頭和status code。也就是說(shuō),在你發(fā)送數(shù)據(jù)體(body data)前,你依靠node幫助你去發(fā)送頭部信息。
如果你想,你也可以顯式地將頭部信息寫(xiě)入響應(yīng)流里。你可以通過(guò) writeHead方法達(dá)到此目的(寫(xiě)入status code 和 headers到流里)。
response.writeHead(200, {
'Content-Type': 'application/json',
'X-Powered-By': 'bacon'
});
當(dāng)你設(shè)置好頭后(無(wú)論是顯式地還是隱式地),你就可以開(kāi)始發(fā)送響應(yīng)數(shù)據(jù)了。
發(fā)送響應(yīng)體(Response Body)
因?yàn)轫憫?yīng)對(duì)象是一個(gè)可寫(xiě)的流( WritableStream),我們可以用常規(guī)的流方法,寫(xiě)一個(gè)響應(yīng)體發(fā)送給客戶(hù)端。
response.write('<html>');
response.write('<body>');
response.write('<h1>Hello, World!</h1>');
response.write('</body>');
response.write('</html>');
response.end();
The end function on streams can also take in some optional data to send as the last bit of data on the stream, so we can simplify the example above as follows.
在上面代碼的end方法里,我們也可以寫(xiě)一點(diǎn)數(shù)據(jù)進(jìn)去,所以上訴代碼可以簡(jiǎn)化成:
response.end('<html><body><h1>Hello, World!</h1></body></html>');
注意:在向響應(yīng)體寫(xiě)數(shù)據(jù)塊之前,狀態(tài)(status)和頭(headers)的設(shè)置非常重要。為什么這么說(shuō),因?yàn)樵贖TTP響應(yīng)里,響應(yīng)頭都是先于響應(yīng)體的。
關(guān)于錯(cuò)誤的另一件小事
響應(yīng)流同樣能發(fā)送“error”事件,某些情況下你必須要處理這個(gè)錯(cuò)誤。對(duì)請(qǐng)求流里的錯(cuò)誤處理建議也適用于此。
將這些組合到一起
Now that we've learned about making HTTP responses, let's put it all together. Building on the earlier example, we're going to make a server that sends back all of the data that was sent to us by the user. We'll format that data as JSON using JSON.stringify.
至此,我們學(xué)習(xí)了如何生成HTTP響應(yīng),讓我們合起來(lái)看一起。在之前的例子的基礎(chǔ)上,我們加這樣一個(gè)服務(wù),它將用戶(hù)發(fā)給我們的數(shù)據(jù)又發(fā)回給用戶(hù)。我們用JSON.stringify將數(shù)據(jù)JSON化:
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);
一個(gè)回顯服務(wù)端的例子
將上一個(gè)例子簡(jiǎn)化成一個(gè)簡(jiǎn)單的回顯服務(wù),它把任何接收的數(shù)據(jù)原封不動(dòng)地發(fā)回。我們要做的就是從請(qǐng)求流里的數(shù)據(jù)抓取出來(lái),然后將其寫(xiě)入響應(yīng)流里,就像我們之前做過(guò)的一樣。
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);
調(diào)整一下,讓服務(wù)端只在以下條件下才返回?cái)?shù)據(jù):
- The request method is GET.
- The URL is /echo.
在別的情況下,我們只返回404。
const http = require('http');
http.createServer((request, response) => {
if (request.method === 'GET' && 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,我們用了“路由(routing)”的形式。就路由而言,有簡(jiǎn)單地轉(zhuǎn)換的路由,也有復(fù)雜的如express一樣框架式的路由。如果你只關(guān)心路由,那么用 router就可以了。
非常好!現(xiàn)在讓我們嘗試簡(jiǎn)化它。還記得嗎,之前說(shuō)過(guò)請(qǐng)求對(duì)象是一個(gè)可讀流ReadableStream,響應(yīng)對(duì)象是一個(gè)可寫(xiě)流 WritableStream,這意味著我們能用管道(pipe)直接將數(shù)據(jù)從一個(gè)傳給另一個(gè)。這就是所謂回顯服務(wù)要做的事。
const http = require('http');
http.createServer((request, response) => {
if (request.method === 'GET' && request.url === '/echo') {
request.pipe(response);
} else {
response.statusCode = 404;
response.end();
}
}).listen(8080);
厲害了,我的流!
還沒(méi)完。就像這份指南多次提到的,我們還得應(yīng)付錯(cuò)誤發(fā)生時(shí)的情況。
對(duì)請(qǐng)求流上的錯(cuò)誤,我們把錯(cuò)誤記錄到標(biāo)準(zhǔn)出錯(cuò)文件(stderr)里,發(fā)送錯(cuò)誤碼400標(biāo)明這個(gè)一個(gè)Bad Request。在現(xiàn)實(shí)世界的應(yīng)用中,我們檢查錯(cuò)誤,去分析正確的狀態(tài)碼和信息應(yīng)該是什么。關(guān)于錯(cuò)誤,你可以讀一讀Error documentation。
對(duì)于響應(yīng)錯(cuò)誤,我們將其記錄到標(biāo)準(zhǔn)輸出文件(stdout)里。
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 === 'GET' && request.url === '/echo') {
request.pipe(response);
} else {
response.statusCode = 404;
response.end();
}
}).listen(8080);
到目前為止,我們講解了處理HTTP請(qǐng)求的基本方法。現(xiàn)在你能夠做以下事情了:
- 用請(qǐng)求處理函數(shù)(request handler function)創(chuàng)建一個(gè)HTTP服務(wù)的實(shí)例,并監(jiān)聽(tīng)其端口。
- 從請(qǐng)求對(duì)象里得到頭部信息(headers)、URL、方法(method)及數(shù)據(jù)體(body data)。根據(jù)URL 和/或 請(qǐng)求對(duì)象里的其他數(shù)據(jù)決定路由。
- 通過(guò)響應(yīng)對(duì)象返回頭部信息、HTTP狀態(tài)碼以及數(shù)據(jù)體。
- 將數(shù)據(jù)通過(guò)流的形式從請(qǐng)求對(duì)象傳到響應(yīng)對(duì)象。
- 處理請(qǐng)求流和響應(yīng)流里的錯(cuò)誤。
From these basics, Node.js HTTP servers for many typical use cases can be constructed. There are plenty of other things these APIs provide, so be sure to read through the API docs for EventEmitters
, Streams
, and HTTP
.
通過(guò)這些基礎(chǔ),我們可以建立許多基于Node.js的HTTP典型服務(wù)。上述API還能提供許多其他功能,所以請(qǐng)通讀一下關(guān)于EventEmitters, Streams, 和 HTTP的API文檔。