1. 程式人生 > >七天學會NodeJS(網路操作)

七天學會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://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' }
<!DOCTYPE html>
...

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物件,通過keycert欄位指定了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:[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

官方文件: 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以及一些坑迴避技巧,總結起來有以下幾點:

  • httphttps模組支援服務端模式和客戶端模式兩種使用方式。

  • requestresponse物件除了用於讀寫頭資料外,都可以當作資料流來操作。

  • url.parse方法加上request.url屬性是處理HTTP請求時的固定搭配。

  • 使用zlib模組可以減少使用HTTP協議時的資料傳輸量。

  • 通過net模組的Socket伺服器與客戶端可對HTTP協議做底層操作。

  • 小心踩坑。