英文標(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)用一次,因此我們稱這個(gè)方法為請(qǐng)求處理者(request handler)观蜗。這個(gè)由createServer返回的服務(wù)對(duì)象實(shí)際上就是個(gè) EventEmitter臊恋,它是創(chuàng)建服務(wù)對(duì)象的簡(jiǎn)寫方式,之后加了一個(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唆阿,即從包括第三條正斜線以內(nèi)的后面所有部分(譯者按:假設(shè)一段地址是益涧,http://www.reibang.com/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ú)論客戶端如何發(fā)送頭部信息浅辙,所有的頭都以小寫形式呈現(xiàn)扭弧,這簡(jiǎn)化了解析頭部信息的工作。
如果頭部信息重復(fù)记舆,那么它們的值會(huì)被重寫鸽捻,或者組合成以逗號(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ā)送類似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)有東西返回給客戶端拍屑。
到目前為止,我們還沒(méi)碰過(guò)響應(yīng)對(duì)象(response object)坑傅,響應(yīng)對(duì)象是 ServerResponse的一個(gè)實(shí)例僵驰,同時(shí)也是一個(gè)可寫的流 WritableStream。它包含了很多很有用的方法唁毒,以此發(fā)回?cái)?shù)據(jù)給客戶端蒜茴。我們接下來(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');
大小寫對(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ā)送頭部信息肮帐。
如果你想咖驮,你也可以顯式地將頭部信息寫入響應(yīng)流里。你可以通過(guò) writeHead方法達(dá)到此目的(寫入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è)可寫的流( WritableStream)恒界,我們可以用常規(guī)的流方法睦刃,寫一個(gè)響應(yīng)體發(fā)送給客戶端。
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方法里十酣,我們也可以寫一點(diǎn)數(shù)據(jù)進(jìn)去涩拙,所以上訴代碼可以簡(jiǎn)化成:
response.end('<html><body><h1>Hello, World!</h1></body></html>');
注意:在向響應(yīng)體寫數(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ù),它將用戶發(fā)給我們的數(shù)據(jù)又發(fā)回給用戶森书。我們用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)凛膏,然后將其寫入響應(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è)可寫流 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文檔癌瘾。