不了解網絡編程的程序員不是好前端,而NodeJS恰好提供了一扇了解網絡編程的窗口。通過NodeJS,除了可以編寫一些服務端程序來協助前端開發和測試外,還能夠學習一些HTTP協議與Socket協議的相關知識,這些知識在優化前端性能和排查前端故障時說不定能派上用場。本章將介紹與之相關的NodeJS內置模塊。
開門紅
NodeJS本來的用途是編寫高性能Web服務器。我們首先在這裏重複一下官方文檔裏的例子,使用NodeJS內置的http模塊簡單實現一個HTTP服務器。
var http = require('http');
http.createServer(function (request, response) {
response.writeHead(200, { 'Content-Type': 'text-plain' });
response.end('Hello World\n');
}).listen(8124);
以上程序創建了一個HTTP服務器並監聽8124端口,打開瀏覽器訪問該端口http://127.0.0.1:8124/就能夠看到效果。
豆知識: 在Linux係統下,監聽1024以下端口需要root權限。因此,如果想監聽80或443端口的話,需要使用sudo命令啟動程序。
API走馬觀花
我們先大致看看NodeJS提供了哪些和網絡操作有關的API。這裏並不逐一介紹每個API的使用方法,官方文檔已經做得很好了。
HTTP
官方文檔: http://nodejs.org/api/http.html
‘http’模塊提供兩種使用方式:
作為服務端使用時,創建一個HTTP服務器,監聽HTTP客戶端請求並返回響應。
作為客戶端使用時,發起一個HTTP客戶端請求,獲取服務端響應。
首先我們來看看服務端模式下如何工作。如開門紅中的例子所示,首先需要使用.createServer方法創建一個服務器,然後調用.listen方法監聽端口。之後,每當來了一個客戶端請求,創建服務器時傳入的回調函數就被調用一次。可以看出,這是一種事件機製。
HTTP請求本質上是一個數據流,由請求頭(headers)和請求體(body)組成。例如以下是一個完整的HTTP請求數據內容。
POST / HTTP/1.1
User-Agent: curl/7.26.0
Host: localhost
Accept: */*
Content-Length: 11
Content-Type: application/x-www-form-urlencoded
Hello World
可以看到,空行之上是請求頭,之下是請求體。HTTP請求在發送給服務器時,可以認為是按照從頭到尾的順序一個字節一個字節地以數據流方式發送的。而http模塊創建的HTTP服務器在接收到完整的請求頭後,就會調用回調函數。在回調函數中,除了可以使用request對象訪問請求頭數據外,還能把request對象當作一個隻讀數據流來訪問請求體數據。以下是一個例子。
http.createServer(function (request, response) {
var body = [];
console.log(request.method);
console.log(request.headers);
request.on('data', function (chunk) {
body.push(chunk);
});
request.on('end', function () {
body = Buffer.concat(body);
console.log(body.toString());
});
}).listen(80);
------------------------------------
POST
{ 'user-agent': 'curl/7.26.0',
host: 'localhost',
accept: '*/*',
'content-length': '11',
'content-type': 'application/x-www-form-urlencoded' }
Hello World
HTTP響應本質上也是一個數據流,同樣由響應頭(headers)和響應體(body)組成。例如以下是一個完整的HTTP請求數據內容。
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 11
Date: Tue, 05 Nov 2013 05:31:38 GMT
Connection: keep-alive
Hello World
在回調函數中,除了可以使用response對象來寫入響應頭數據外,還能把response對象當作一個隻寫數據流來寫入響應體數據。例如在以下例子中,服務端原樣將客戶端請求的請求體數據返回給客戶端。
http.createServer(function (request, response) {
response.writeHead(200, { 'Content-Type': 'text/plain' });
request.on('data', function (chunk) {
response.write(chunk);
});
request.on('end', function () {
response.end();
});
}).listen(80);
接下來我們看看客戶端模式下如何工作。為了發起一個客戶端HTTP請求,我們需要指定目標服務器的位置並發送請求頭和請求體,以下示例演示了具體做法。
var options = {
hostname: 'www.example.com',
port: 80,
path: '/upload',
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
};
var request = http.request(options, function (response) {});
request.write('Hello World');
request.end();
可以看到,.request方法創建了一個客戶端,並指定請求目標和請求頭數據。之後,就可以把request對象當作一個隻寫數據流來寫入請求體數據和結束請求。另外,由於HTTP請求中GET請求是最常見的一種,並且不需要請求體,因此http模塊也提供了以下便捷API。
http.get('http://www.example.com/', function (response) {});
當客戶端發送請求並接收到完整的服務端響應頭時,就會調用回調函數。在回調函數中,除了可以使用response對象訪問響應頭數據外,還能把response對象當作一個隻讀數據流來訪問響應體數據。以下是一個例子。
http.get('http://www.example.com/', function (response) {
var body = [];
console.log(response.statusCode);
console.log(response.headers);
response.on('data', function (chunk) {
body.push(chunk);
});
response.on('end', function () {
body = Buffer.concat(body);
console.log(body.toString());
});
});
------------------------------------
200
{ 'content-type': 'text/html',
server: 'Apache',
'content-length': '801',
date: 'Tue, 05 Nov 2013 06:08:41 GMT',
connection: 'keep-alive' }
...
HTTPS
官方文檔: http://nodejs.org/api/https.html
https模塊與http模塊極為類似,區別在於https模塊需要額外處理SSL證書。
在服務端模式下,創建一個HTTPS服務器的示例如下。
var options = {
key: fs.readFileSync('./ssl/default.key'),
cert: fs.readFileSync('./ssl/default.cer')
};
var server = https.createServer(options, function (request, response) {
// ...
});
可以看到,與創建HTTP服務器相比,多了一個options對象,通過key和cert字段指定了HTTPS服務器使用的私鑰和公鑰。
另外,NodeJS支持SNI技術,可以根據HTTPS客戶端請求使用的域名動態使用不同的證書,因此同一個HTTPS服務器可以使用多個域名提供服務。接著上例,可以使用以下方法為HTTPS服務器添加多組證書。
server.addContext('foo.com', {
key: fs.readFileSync('./ssl/foo.com.key'),
cert: fs.readFileSync('./ssl/foo.com.cer')
});
server.addContext('bar.com', {
key: fs.readFileSync('./ssl/bar.com.key'),
cert: fs.readFileSync('./ssl/bar.com.cer')
});
在客戶端模式下,發起一個HTTPS客戶端請求與http模塊幾乎相同,示例如下。
var options = {
hostname: 'www.example.com',
port: 443,
path: '/',
method: 'GET'
};
var request = https.request(options, function (response) {});
request.end();
但如果目標服務器使用的SSL證書是自製的,不是從頒發機構購買的,默認情況下https模塊會拒絕連接,提示說有證書安全問題。在options裏加入rejectUnauthorized: false字段可以禁用對證書有效性的檢查,從而允許https模塊請求開發環境下使用自製證書的HTTPS服務器。
URL
官方文檔: http://nodejs.org/api/url.html
處理HTTP請求時url模塊使用率超高,因為該模塊允許解析URL、生成URL,以及拚接URL。首先我們來看看一個完整的URL的各組成部分。
href
-----------------------------------------------------------------
host path
--------------- ----------------------------
http: // user:pass @ host.com : 8080 /p/a/t/h ?query=string #hash
----- --------- -------- ---- -------- ------------- -----
protocol auth hostname port pathname search hash
------------
query
我們可以使用.parse方法來將一個URL字符串轉換為URL對象,示例如下。
url.parse('http://user:pass@host.com:8080/p/a/t/h?query=string#hash');
/* =>
{ protocol: 'http:',
auth: 'user:pass',
host: 'host.com:8080',
port: '8080',
hostname: 'host.com',
hash: '#hash',
search: '?query=string',
query: 'query=string',
pathname: '/p/a/t/h',
path: '/p/a/t/h?query=string',
href: 'http://user:pass@host.com:8080/p/a/t/h?query=string#hash' }
*/
傳給.parse方法的不一定要是一個完整的URL,例如在HTTP服務器回調函數中,request.url不包含協議頭和域名,但同樣可以用.parse方法解析。
http.createServer(function (request, response) {
var tmp = request.url; // => "/foo/bar?a=b"
url.parse(tmp);
/* =>
{ protocol: null,
slashes: null,
auth: null,
host: null,
port: null,
hostname: null,
hash: null,
search: '?a=b',
query: 'a=b',
pathname: '/foo/bar',
path: '/foo/bar?a=b',
href: '/foo/bar?a=b' }
*/
}).listen(80);
.parse方法還支持第二個和第三個布爾類型可選參數。第二個參數等於true時,該方法返回的URL對象中,query字段不再是一個字符串,而是一個經過querystring模塊轉換後的參數對象。第三個參數等於true時,該方法可以正確解析不帶協議頭的URL,例如//www.example.com/foo/bar。
反過來,format方法允許將一個URL對象轉換為URL字符串,示例如下。
url.format({
protocol: 'http:',
host: 'www.example.com',
pathname: '/p/a/t/h',
search: 'query=string'
});
/* =>
'http://www.example.com/p/a/t/h?query=string'
*/
另外,.resolve方法可以用於拚接URL,示例如下。
url.resolve('http://www.example.com/foo/bar', '../baz');
/* =>
http://www.example.com/baz
*/
Query String
官方文檔: http://nodejs.org/api/querystring.html
querystring模塊用於實現URL參數字符串與參數對象的互相轉換,示例如下。
querystring.parse('foo=bar&baz=qux&baz=quux&corge');
/* =>
{ foo: 'bar', baz: ['qux', 'quux'], corge: '' }
*/
querystring.stringify({ foo: 'bar', baz: ['qux', 'quux'], corge: '' });
/* =>
'foo=bar&baz=qux&baz=quux&corge='
*/
Zlib
官方文檔: http://nodejs.org/api/zlib.html
zlib模塊提供了數據壓縮和解壓的功能。當我們處理HTTP請求和響應時,可能需要用到這個模塊。
首先我們看一個使用zlib模塊壓縮HTTP響應體數據的例子。這個例子中,判斷了客戶端是否支持gzip,並在支持的情況下使用zlib模塊返回gzip之後的響應體數據。
http.createServer(function (request, response) {
var i = 1024,
data = '';
while (i--) {
data += '.';
}
if ((request.headers['accept-encoding'] || '').indexOf('gzip') !== -1) {
zlib.gzip(data, function (err, data) {
response.writeHead(200, {
'Content-Type': 'text/plain',
'Content-Encoding': 'gzip'
});
response.end(data);
});
} else {
response.writeHead(200, {
'Content-Type': 'text/plain'
});
response.end(data);
}
}).listen(80);
接著我們看一個使用zlib模塊解壓HTTP響應體數據的例子。這個例子中,判斷了服務端響應是否使用gzip壓縮,並在壓縮的情況下使用zlib模塊解壓響應體數據。
var options = {
hostname: 'www.example.com',
port: 80,
path: '/',
method: 'GET',
headers: {
'Accept-Encoding': 'gzip, deflate'
}
};
http.request(options, function (response) {
var body = [];
response.on('data', function (chunk) {
body.push(chunk);
});
response.on('end', function () {
body = Buffer.concat(body);
if (response.headers['content-encoding'] === 'gzip') {
zlib.gunzip(body, function (err, data) {
console.log(data.toString());
});
} else {
console.log(data.toString());
}
});
}).end();
Net
官方文檔: http://nodejs.org/api/net.html
net模塊可用於創建Socket服務器或Socket客戶端。由於Socket在前端領域的使用範圍還不是很廣,這裏先不涉及到WebSocket的介紹,僅僅簡單演示一下如何從Socket層麵來實現HTTP請求和響應。
首先我們來看一個使用Socket搭建一個很不嚴謹的HTTP服務器的例子。這個HTTP服務器不管收到啥請求,都固定返回相同的響應。
net.createServer(function (conn) {
conn.on('data', function (data) {
conn.write([
'HTTP/1.1 200 OK',
'Content-Type: text/plain',
'Content-Length: 11',
'',
'Hello World'
].join('\n'));
});
}).listen(80);
接著我們來看一個使用Socket發起HTTP客戶端請求的例子。這個例子中,Socket客戶端在建立連接後發送了一個HTTP GET請求,並通過data事件監聽函數來獲取服務器響應。
var options = {
port: 80,
host: 'www.example.com'
};
var client = net.connect(options, function () {
client.write([
'GET / HTTP/1.1',
'User-Agent: curl/7.26.0',
'Host: www.baidu.com',
'Accept: */*',
'',
''
].join('\n'));
});
client.on('data', function (data) {
console.log(data.toString());
client.end();
});
靈機一點
使用NodeJS操作網絡,特別是操作HTTP請求和響應時會遇到一些驚喜,這裏對一些常見問題做解答。
問: 為什麽通過headers對象訪問到的HTTP請求頭或響應頭字段不是駝峰的?
答: 從規範上講,HTTP請求頭和響應頭字段都應該是駝峰的。但現實是殘酷的,不是每個HTTP服務端或客戶端程序都嚴格遵循規範,所以NodeJS在處理從別的客戶端或服務端收到的頭字段時,都統一地轉換為了小寫字母格式,以便開發者能使用統一的方式來訪問頭字段,例如headers[‘content-length’]。
問: 為什麽http模塊創建的HTTP服務器返回的響應是chunked傳輸方式的?
答: 因為默認情況下,使用.writeHead方法寫入響應頭後,允許使用.write方法寫入任意長度的響應體數據,並使用.end方法結束一個響應。由於響應體數據長度不確定,因此NodeJS自動在響應頭裏添加了Transfer-Encoding: chunked字段,並采用chunked傳輸方式。但是當響應體數據長度確定時,可使用.writeHead方法在響應頭裏加上Content-Length字段,這樣做之後NodeJS就不會自動添加Transfer-Encoding字段和使用chunked傳輸方式。
問: 為什麽使用http模塊發起HTTP客戶端請求時,有時候會發生socket hang up錯誤?
答: 發起客戶端HTTP請求前需要先創建一個客戶端。http模塊提供了一個全局客戶端http.globalAgent,可以讓我們使用.request或.get方法時不用手動創建客戶端。但是全局客戶端默認隻允許5個並發Socket連接,當某一個時刻HTTP客戶端請求創建過多,超過這個數字時,就會發生socket hang up錯誤。解決方法也很簡單,通過http.globalAgent.maxSockets屬性把這個數字改大些即可。另外,https模塊遇到這個問題時也一樣通過https.globalAgent.maxSockets屬性來處理。
小結
-
本章介紹了使用NodeJS操作網絡時需要的API以及一些坑回避技巧,總結起來有以下幾點:
-
http和https模塊支持服務端模式和客戶端模式兩種使用方式。
-
request和response對象除了用於讀寫頭數據外,都可以當作數據流來操作。
-
url.parse方法加上request.url屬性是處理HTTP請求時的固定搭配。
-
使用zlib模塊可以減少使用HTTP協議時的數據傳輸量。
-
通過net模塊的Socket服務器與客戶端可對HTTP協議做底層操作。
-
小心踩坑。