七天學會NodeJS(網路操作)
網路操作
不瞭解網路程式設計的程式設計師不是好前端,而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'模組提供兩種使用方式:
-
作為服務端使用時,建立一個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' }
<!DOCTYPE html>
...
HTTPS
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請求時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:[email protected]: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:[email protected]: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
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
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
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協議做底層操作。 -
小心踩坑。