深刻淺出nodeJS - 2 - (內存控制、理解Buffer、網絡編程)

內容

5.內存控制
6.理解Buffer
7.網絡編程

1、內存控制

1.V8的垃圾回收機制與內存限制

Node基於垃圾回收機制進行內存的自動管理。這種機制,在瀏覽器環境下,幾乎是完美的,可是同java同樣,在後端運行的node,若是想要更完美的運行,依然須要判斷和管理內存,內存管理的好壞、垃圾回收情況是否優良,都會直接影響服務器的性能。java

1)node 與 V8node

Node在JavaScript的執行上直接受益於V8,能夠隨着V8的升級就能享受到更好的性能或新的語言特性(如ES5和ES6)等,
同時也受到V8的限制,尤爲是內存顯示。c++

2)V8的內存限制git

通常的後臺開發語言中,內存使用的大小几乎沒有限制。可是,V8最初是爲瀏覽器打造的,在V8下,64位系統能夠操縱1.4GB內存,32位系統能夠操縱0.7GB內存。在這樣的限制下,node幾乎不能直接操縱大內存。github

3)V8的對象分配web

在V8中全部的js對象都是經過堆來進行分配的,可使用node提供的V8內存使用量的查看方式查看內存分配及使用情況:redis

// 1.rss:resident set size,進程的常駐內存
// 2.heapTotal: 已經申請到的堆內存。
// 3.heapUsed: 當前堆內存使用量。
$ node
> process.memoryUsage();
{ rss: 14958592,
heapTotal: 7195904,
heapUsed: 2821496 }

clipboard.png

調整內存限制大小:算法

node --max-old-space-size=1700 test.js // 設置老生代內存空間的最大值,單位爲MB
// 或者
node --max-new-space-size=1024 test.js //  設置新生代內存空間的最大值,單位爲KB

限制堆內存緣由:v8垃圾回收機制的限制,以1.5G垃圾回收隊內存爲例,v8作一次小垃圾回收須要50毫秒以上,作一次非增量的垃圾回收甚至要1秒以上,這是垃圾回收引發JavaScipt線程暫停執行的時間,這使得應用的性能和響應能力都會直接降低。數據庫

4)V8的垃圾回收機制
v8的垃圾回收策略主要基於分代式垃圾回收機制。按照對象的存活時間將內存的垃圾回收進行不一樣的分代,而後,分別對不一樣的分代的內存再進行高效的垃圾回收算法。
1.v8的內存分代
在V8中,主要將內存分爲新生代和老生代兩代。新內存中的對象存活時間短,老內存中的對象存活時間長或常駐內存對象。
clipboard.png編程

2.新生代垃圾回收算法scavenge算法
新生代中的對象主要經過scavenge算法進行垃圾回收,其主要是採用cheney算法進行具體處理。

cheney算法採用一種複製方式的垃圾回收算法,將堆內存一分爲二,只有一部分空間被使用稱爲From空間,另外一個處於閒置稱爲To空間。當進行分配對象的時候先在from空間分配,當進行垃圾回收時,會檢查from空間中的存活對象,將這些存活對象複製到to空間中,複製完成後From和to空間角色互換,清空to空間,在垃圾回收過程當中就是經過將存活對象在兩個空間中進行復制。

  • 缺點: 只能使用一半的內存
  • 優勢: 只複製存活的對象,對於生命週期短的場景存活對象只佔小部分,因此時間效率高

當一個對象通過屢次複製依然存活時,就會被認爲是生命週期較長的對象,會被移入老生代內存中。

對於移入老生代內存有兩個條件:

  • 對象已經通過新生代內存回收機制的回收依然存活
  • 複製到To空間的對象超過25%(爲何是25%?這個To空間接下來會成爲From空間並接受內存分配,若是佔比太高影響後續分配)

老內存垃圾回收算法Mark-Sweep & Mark-Compact

採用標記清除,它分爲標記清除兩個階段
在標記階段遍歷全部的對象並標記活着的對象,在清除階段只清除死亡的對象,死亡對象在老生代內存只佔一小部分。老生代內存進行一次清除後,內存空間會出現不連續的狀態,因此清理完成須要進行一步標記整理。、
Incremental Marking
爲了不出現javaScript應用邏輯與垃圾回收器看到不一致的狀況,垃圾回收都要將應用邏輯停下來,這種行爲會形成停頓,在新生代垃圾回收過程當中由於存活對象比較少,即便停頓基本影響不大。在老生代垃圾回收中,一般存活對象較多,全堆垃圾回收的標記、清除、整理影響較大。
解決辦法:分批次進行,拆分紅許多小步,每進行一小步就讓邏輯運行一會

clipboard.png

5)查看垃圾回收日誌
在啓動時時加入 參數 --trace_gc。
還能夠在啓動時增長--prof參數,來獲得v8執行時的性能分析數據,其中包含了垃圾回收執行時佔用的時間等。

2.高效使用內存

1)做用域
只被局部變量引用的對象存活週期較短。將會分配到新生代中的From空間中,在做用域釋放後,局部變量失效,其引用的對象將會在下次垃圾回收時被釋放。
1.標識符查找

js在執行時會先查找該變量定義在哪裏。它最早查找的是當前做用域,若是在當前做用域中沒法找到該變量的聲明,將會向上級的做用域裏查找,直到查到爲止。

2.做用域鏈
這個查找過程,就是一個做用域鏈的查找過程

var foo = function () {
    var local = 'local var';
    var bar = function () {
        var local = 'another var';
        var baz = function () {
            console.log(local);
        };
        baz();
    };
    bar();
};
foo();

local變量在baz()函數造成的做用域中查找不到,會到bar造成的做用域中尋找,以此類推,逐漸向上尋找,一直查到全局做用域。因爲標識符查找的方向是自內而外的,也就是向上的,所以,變量只能向外訪問,不能向內訪問。

3.變量的主動釋放
若是變量是全局變量,那麼,全局做用域要等進程所有退出纔會釋放,此時將會致使引用的對象常駐內存。若是須要釋放常駐內存的對象,可使用delete來刪除,或者將變量從新賦值讓舊的對象脫離引用關係。咱們來看一下主動清除和整理老內存的一段代碼:

global.foo = "I am global object";
console.log(global.foo); // => "I am global object"
delete global.foo;
// 或者從新賦值
global.foo = undefined; // or null
console.log(global.foo); // => undefined

其餘的變量主動釋放均可以用這個方法,同時因爲delete會干擾v8的優化,所以,採用賦空值的方式,比較穩妥。

2)閉包
在js中實現外部做用域訪問內部做用域的方法叫作閉包。

var foo = function () {
    var bar = function () {
        var local = "局部變量";
        return function () {
            return local;
        };
    };
    var baz = bar();
    console.log(baz());
};

閉包是經過中間函數進行間接訪問內部變量實現的一個功能,一旦變量引用這個中間函數,這個中間函數將不會釋放,同時也會使原始的做用域不會獲得釋放,做用域中產生的內存佔用也不會獲得釋放。除非再也不有引用,纔會逐步釋放。

3)*小結
能夠利用閉包和垃圾回收的機制,來存儲一些須要存活時間長一些的對象,並將其做爲公共訪問的數據區域來使用。可是,閉包和全局變量的使用仍是要當心,因爲沒法及時回收內存,這會增長常駐內存的產生,會致使老生代中的對象增多。

3.內存指標

1)查看內存使用狀況

  • 查看進程的內存佔用:process.memoryUsage()
  • 查看系統的內存佔用:使用os模塊的totalmem()[系統的總內存],freemen()[系統的閒置內存]

2)堆外內存

堆中的內存用量老是小於進程的常駐內存用量,這意味着Node中的內存使用並不是都是經過V8進行分配的,這些不經過V8分配的內存,稱爲堆外內存。
例如:buffer對象不通過v8內存分配,所以,也不會有堆內存的大小限制。

3)小結
Node的內存主要由經過V8進行分配的部分和Node 自行分配的部分,受V8的垃圾回收限制的主要是V8的堆內存。

4.內存泄漏

內存泄漏的實質就是應當回收的對象由於意外沒有被回收,變成了常駐在老生代中的對象。
形成內存泄漏的主要緣由有:緩存、隊列消費不及時、做用域未釋放。

1)慎將內存當作緩存
緩存十分節省資源,由於它的訪問比IO效率要高,一旦命中緩存,就能夠節省一次IO時間。
可是在Node中,緩存並不是物美價廉,一旦一個對象被當作緩存來使用,那它將會常駐在老生代中,這將致使垃圾回收在進行掃描和整理時,對這些對象作無用功。
v8內存是經過垃圾回收進行處理的,沒有過時策略,而真正的緩存是存在過時策略的。

緩存限制策略
將結果記錄在數組中,一旦超過數量,就以先進先出的方式進行淘汰。若是須要更高效的緩存,能夠參與LRU算法,地址爲:https://github.com/isaacs/nod...

另外一個案例在於模塊機制,因爲模塊的緩存機制,模塊是常駐老生代的,須要添加清空隊列的相應接口,以供調用者釋放內存。

(function (exports, require, module, __filename, __dirname) {
var local = "局部變量";
exports.get = function () {
return local;
};
});

//每次調用時都會形成內存增加
var leakArray = [];
exports.leak = function () {
leakArray.push("leak" + Math.random());
};

緩存的解決方案

進程間是沒法共享內存的,所以,使用內存做爲緩存不是一個好的解決方案。最好的解決方案是使用外部緩存,例如redis等。這些緩存能夠將緩存的壓力從內存轉移到進程的外部,減小常駐內存的對象數量,讓垃圾回收更有效率,同時,還能夠實現進程間共享緩存,節約寶貴的資源。

2)關注隊列狀態

由於通常狀況下,消費的速度要遠遠高於生產的速度,所以,不容易產生內存泄漏,不過一旦發生內存泄漏,將會形成內存堆積。

例如,日誌寫入數據庫的這種狀況,由於數據庫寫入速度低於日誌的生產速度,形成了數據庫寫入請求的堆積,進而形成內存溢出。

解決方案是監控隊列的長度,一旦產生堆積,應當經過監控系統報警,同時,設置合理的超時機制,一旦超時,經過回調函數傳遞超時異常。
例如bagpipe的超時模式和拒絕模式。

5.內存泄漏排查

定位Node應用的內存泄漏經常使用工具以下:

工具 說明
v8-profiler 能夠對v8堆內存抓取快照,並對cpu進行分析
node-heapdump 能夠對v8堆內存抓取快照,用於過後分析
node-mtrace 使用gcc的mtrace工具來分析堆的使用
dtrace 在smartos上使用的內存分析工具
node-memwatch 採用wtfpl許可發佈的內存分析工具

6.大內存應用

使用流的方式操做大內存,也就是使用stream模塊。這個模塊繼承了eventemitter,具有基本的自定義事件功能,同時抽象出標準的事件和方法。它分可讀和可寫兩種。node中大多數模塊都有stream應用,例如fs的createReadStream()和createWriteStream(),process模塊的stdin和stdout。
因爲V8的內存限制沒法經過fs.readFile()和fs.writeFile()直接讀取大文件,而要使用fs的createReadStream()和createWriteStream()來讀取,咱們看個例子:

var reader = fs.createReadStream('in.txt');
var writer = fs.createWriteStream('out.txt');
reader.on('data', function (chunk) {
    writer.write(chunk);
});
reader.on('end', function () {
    writer.end();
});

//或者

var reader = fs.createReadStream('in.txt');
var writer = fs.createWriteStream('out.txt');
reader.pipe(writer);

由於流使用了buffer做爲讀寫的編碼方式,所以,不受v8內存的限制。可是,物理內存依然有限制。

2、理解Buffer

由於在node中須要處理網絡協議、操做數據庫、處理圖片、接受上傳文件等,在網絡流和文件操做中,還要處理大量二進制數據,js自有的字符串遠遠不能知足這些需求,因而Buffer對象應運而生。

1.Buffer結構

Buffer是一個像Array的對象,可是它主要用於操做字節。

1)模塊結構
buffer是一個典型的js與c++結合的模塊,將性能相關的部分用c++實現,將非性能相關的部分用js實現。同時buffer也是node的核心模塊,能夠直接使用,而且,buffer屬於堆外內存,能夠經過本身管理其垃圾回收。固然,buffer對象的管理仍是在堆內,再由這個對象去管理堆外的內存。
因爲Buffer太常見,Node在進程啓動時已經加載了它,並將其放在全局對象上,因此在使用Buffer時,無須經過require()便可直接使用。

clipboard.png

2)Buffer對象
buffer對象相似於數組,他的元素都是16進制的兩位數,即0~255的數值。

//不一樣編碼的字符串,佔用的元素個數也不相同,中文字在UTF-8下佔用3個元素,字母和半角標點符號佔用1個元素。
var str = "深刻淺出node.js";
var buf = new Buffer(str, 'utf-8');
console.log(buf);
// => <Buffer e6 b7 b1 e5 85 a5 e6 b5 85 e5 87 ba 6e 6f 64 65 2e 6a 73>

咱們能夠調用length屬性,獲得buffer對象的長度,還能夠經過下標訪問元素。

var buf = new Buffer(100);
console.log(buf.length); // => 100
console.log(buf[10]);//0

//咱們給buffer元素賦值
buf[10] = 100;
console.log(buf[10]); // => 100  
buf[20] = -100;
console.log(buf[20]); // 156  -100+256=156
buf[21] = 300;
console.log(buf[21]); // 44    300-256=44
buf[22] = 3.1415;
console.log(buf[22]); // 3     捨棄小數部分

3)Buffer內存分配
Buffer對象的內存分配不是在V8的堆內存中,而是在Node的C++層面實現內存的申請的。
由於處理大量的字節數據不能採用須要一點內存就向操做系統申請一點內存的方式,這可能形成大量的內存申請的系統調用,對操做系統有必定壓力。
Node在內存的使用上應用的是在C++層面申請內存,在js中分配內存的策略。

node採用了slab的分配機制,slab其實就是一塊申請好的固定內存區域,它有3種狀態:

  • full:徹底分配狀態
  • partial:部分分配狀態
  • empty:沒有被分配狀態

分配指定大小的Buffer對象: new Buffer(size);
node以8KB爲界限來區分Buffer是大對象仍是小對象的:Buffer.poolSize = 8 * 1024;

1.分配小Buffer對象

若是指定的buffer的大小小於8kb,node會按照小對象的方式進行分配。
若是slab的剩餘空間不夠本次分配,則會構造一個新的slab,原slab中剩餘的空間將會形成浪費。例如:

new Buffer(1);
new Buffer(8192);

2.分配大Buffer對象
大於8kb的buffer對象,會被分配一個SlowBuffer對象做爲slab單元,這個slab單元將被這個大的Buffer對象獨佔。

// Big buffer, just alloc one
this.parent = new SlowBuffer(this.length);
this.offset = 0;

這裏的SlowBuffer類是在C++中定義的,雖然引用buffer模塊能夠訪問到它,可是不推薦直接操縱它,而是用buffer替代。上面提到的buffer對象都是js層面的,可以被v8標記回收,可是其內部的parent屬性指向的SlowBuffer對象卻來自Node的c++模塊,是c++層面的buffer對象,所用的這部份內存不在v8的堆中。

3.小結

真正的buffer內存是在node的c++層面提供的,js層面只是使用它。當進行小而頻繁的buffer操做時,採用slab的機制進行預先申請和過後分配,使得js到操做系統之間沒必要有過多的內存申請方面的系統調用。對於大塊的buffer而言,直接使用c++層面提供的內存,無需頻繁的分配操做。

2.Buffer的轉換

Buffer對象能夠和字符串進行相互轉換,支持的編碼類型有:ASCII、UTF-八、UTF-16LE/UCS-二、Base6四、Binary、Hex
1)字符串轉Buffer
new Buffer(str,[encoding]);encoding默認爲utf-8類型的編碼和存儲。
寫入的方法是:buf.write(string,[offset],[length],[encoding])

2)Buffer轉字符串
buf.toString([encoding], [start], [end]) encoding默認爲utf-8

3)Buffer不支持的編碼類型
Buffer.isEncoding(encoding) 是否支持某種編碼

對於不支持的編碼格式,可使用iconv和iconv-lite來解決。

3.Buffer的拼接

1)亂碼是如何產生的

// data事件中獲取的chunk對象其實就是buffer對象。
var fs = require('fs');
//咱們限定可讀流的每次讀取的buffer長度限制爲11
var rs = fs.createReadStream('test.js', {highWaterMark: 11});
var data = '';
rs.on("data", function (chunk){
    data += chunk;//data = data.toString() + chunk.toString();
});
rs.on("end", function () {
    console.log(data);//牀前明��光,疑���地上霜。舉頭��明月,���頭思故鄉。
});

2)setEncoding() 與 string_decoder()

爲了解決上文中的亂碼問題,咱們應該設置一些編解碼格式:setEncoding()和string_decoder()
經過這個方法,咱們傳遞的再也不是buffer對象,而是編碼後的字符串了

var fs = require('fs');
var rs = fs.createReadStream('test.js', {highWaterMark: 11});
rs.setEncoding('utf8');
var data = '';
rs.on("data", function (chunk){
data += chunk;
});
rs.on("end", function () {
console.log(data);//牀前明月光,疑是地上霜。舉頭望明月,低頭思故鄉。
});

這個過程當中,也就是調用setEncoding(),可讀流在內部設置了decoder對象,這個對象來自於string_decoder模塊的StringDecoder對象實例,
由於基於StringDecoder獲得的編碼,直到utf-8的寬字符是3個字節,所以會將前3個漢字先輸出,也就是先輸出9個字節,而後將月字的前兩個字節保留在StringDecoder實例內部,再和後續的字節進行拼接。它目前支持utf-八、base6四、ucs-二、utf-16le等,其餘的沒有支持的編解碼格式,仍是須要字節手工控制。

var StringDecoder = require('string_decoder').StringDecoder;
var decoder = new StringDecoder('utf8');
var buf1 = new Buffer([0xE5, 0xBA, 0x8A, 0xE5, 0x89, 0x8D, 0xE6, 0x98, 0x8E, 0xE6, 0x9C]);
console.log(decoder.write(buf1));
// =>牀前明
var buf2 = new Buffer([0x88, 0xE5, 0x85, 0x89, 0xEF, 0xBC, 0x8C, 0xE7, 0x96, 0x91, 0xE6]);
console.log(decoder.write(buf2));
// => 月光,疑

3)正確拼接Buffer
正確的拼接方式,是用一個數組來存儲接收到的因此buffer片斷,而後調用buffer.concat()合成一個buffer對象。concat還實現了從小對象buffer向大對象buffer複製的過程

var chunks = [];
var size = 0;
res.on('data', function (chunk) {
    chunks.push(chunk);
    size += chunk.length;
});
res.on('end', function () {
    var buf = Buffer.concat(chunks, size);
    var str = iconv.decode(buf, 'utf8');
    console.log(str);
});

4.Buffer與性能

buffer在文件io和網絡io中具備普遍應用,無論是什麼對象,一旦進入到網絡傳輸中,都須要轉換爲buffer,而後以二進制進行數據傳輸。所以,提供io效率,能夠從buffer轉換入手。
在構建web服務時,將頁面的動態內容和靜態內容進行分離,靜態內容能夠經過先轉換爲buffer的方式,提高傳輸性能。
文件讀取
文件讀取時須要設置好highWaterMark參數。也就是咱們在fs.createReadStream(path,opts)時,能夠傳入一些參數:

{
flags: 'r',
encoding: null,
fd: null,
mode: 0666,
highWaterMark: 64 * 1024
}

還能夠設置start和end來指定讀取文件的位置範圍:{start: 90, end: 99}

在理想狀態下,每次讀取的長度都是用戶指定的highWaterMark,剩餘的還可分配給下一次。pool是常駐內存的,只有當pool單元神域數量小於128(kMinPoolSpace)字節時,纔會從新分配一個buffer對象,咱們來看一下源代碼:

highWaterMark的大小對性能的影響:

  • highWaterMark設置對buffer內存的分配和使用有必定影響(文件讀取基於buffer分配,buffer基於Slowbuffer分配,若是文件太小,則可能形成slab的浪費。)
  • highWaterMark設置太小,可能致使系統調用次數過多

3、網絡編程

Node提供了net、dgrm、http、https這四個模塊,分別用於處理TCP、UDP、HTTP、HTTPS,適用於服務器端和客戶端。

1.構建TCP服務

1)TCP
TCP全稱爲傳輸控制協議,在OSI模型上屬於傳輸層協議。

七層協議示意圖以下:

clipboard.png

TCP是面向鏈接的協議,其顯著特徵是在傳輸以前須要3次握手造成會話。
clipboard.png
只有會話造成後,服務器端和客戶端才能相互發送數據。在建立會話的過程當中,服務器端和客戶端分別提供一個套接字,
這兩個套接字共同造成一個鏈接。服務器端和客戶端則經過套接字實現二者之間鏈接的操做。

2)建立TCP服務器端

//建立TCP服務器端來接受網絡請求
var net = require('net');
var server = net.createServer(function (socket) {
    // 新的鏈接
    socket.on('data', function (data) {
        socket.write("hello") ;
    });
    socket.on('end', function () {
        console.log('鏈接斷開');
    });
    socket.write("hello world\n");
});
server.listen(8124, function () {
    console.log('server bound');
});

//爲了體現listener是鏈接事件connection的監聽器,也能夠採用另一種方式進行監聽
var server = net.createServer();
server.on('connection', function (socket) {
 // 新的鏈接
});
server.listen(8124);

可使用telnet做爲客戶端,對服務進行會話交流

$ telnet 127.0.0.1 8124
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
hello world
hi
hello

除了端口外,咱們還可使用Domain Socket進行監聽。

server.listen('/tmp/echo.sock');

經過net模塊本身構建客戶端進行會話

var net = require('net');
// console.log(net)
var client = net.connect({ port: 8124 }, function () { //'connect' listener
    console.log('client connected');
    client.write('world!\r\n');
});
client.on('data', function (data) {
    console.log(data.toString());
    client.end();
});
client.on('end', function () {
    console.log('client disconnected');
});

//若是是domain socket 能夠這樣寫

var client = net.connect({path: '/tmp/echo.sock'});

3)TCP服務的事件
主要是服務器事件和鏈接事件。
1.服務器事件
經過net.createServer()建立的服務器,它是一個eventEmitter實例,也是stream實例,有以下事件:

  • listening,在調用server.listen()綁定端口或者domain socket後觸發,能夠寫爲:server.listen(port,listeningListener)
  • connection,每一個客戶端套接字鏈接到服務器端時觸發,簡介寫法爲net.createServer(),最後一個參數傳遞。
  • close,調用server.close()後會中止接收新的套接字鏈接,保持當前存在的鏈接,等待因此鏈接都斷開後,觸發該事件
  • error,服務器發生異常時,將會觸發事件,好比偵聽一個使用中的端口,將會觸發一個異常,若是不偵聽error事件,服務器將會拋出異常。

2.鏈接事件
服務器能夠同時與多個客戶端保持鏈接,對於每一個鏈接而言是可寫可讀Stream對象。
Stream對象能夠用於服務器端和客戶端之間的通訊,能夠經過data事件從一端讀取另外一端發來的數據,也能夠經過write()從一端向另外一端發送數據。

  • data,當一端調用write()發送數據時,另外一端會觸發data事件,事件傳遞的數據便是write()發送的數據。
  • end,任意一端發送FIN數據,另外一端將會觸發該事件。
  • connect,客戶端與服務器鏈接成功後,客戶端觸發該事件
  • drain,當任意一端調用write()時,當前這端會觸發該事件
  • error,異常觸發該事件
  • close,當套接字徹底關閉時,觸發該事件
  • timeout,當必定時間後,鏈接不活躍,將觸發該事件,告知當前用戶,該鏈接已經被閒置了。

3.管道操做
因爲TCP套接字是可寫可讀的Stream對象,能夠利用pipe()實現管道操做。

var net = require('net');
var server = net.createServer(function (socket) {
socket.write('Echo server\r\n');
socket.pipe(socket);
});
server.listen(1337, '127.0.0.1');

tcp針對網絡中的小數據包有優化政策,nagle算法,nagle要求網絡中緩衝區數據達到必定數量或必定時間後,纔將其觸發,小數據包會被nagle合併,來優化網絡。這個方法會帶來必定的傳輸延遲。
咱們能夠經過socket.setNoDelay(true)來去掉nagle算法,使得write()能夠當即發送數據。可是,data事件仍是要進行小包合併後觸發的,這個須要注意。

2.構建UDP服務

udp,用戶數據包協議,也是傳輸層協議。udp不是面向鏈接的,也就是說udp無需鏈接,它是面向事務的簡單不可靠信息傳輸服務,在網絡差的狀況下存在丟包嚴重的問題,因爲無需鏈接,資源消耗低,處理塊速且靈活,經常用於那種偶爾丟幾個包也不產生重大影響的場景,例如,音頻、視頻等,DNS就是基於udp實現的。另外,一個udp套接字能夠與多個udp服務進行通訊。

1)建立UDP套接字
UDP套接字建立成功後,能夠做爲客戶端發送數據,也能夠做爲服務器端接收數據。

var dgram = require('dgram');
var socket = dgram.createSocket("udp4");

2)建立UDP服務器端
若想讓UDP套接字接收網絡消息,只要調用dgram.bind(port,[address])對網卡和端口進行綁定便可。

var dgram = require("dgram");
var server = dgram.createSocket("udp4");
server.on("message", function (msg, rinfo) {
    console.log("server got: " + msg + " from " +
        rinfo.address + ":" + rinfo.port);
});
server.on("listening", function () {
    var address = server.address();
    console.log("server listening " +
        address.address + ":" + address.port);
});
server.bind(41234);

3)建立UDP客戶端
udp是無需創建鏈接的,所以,高效快速不可靠。

var dgram = require('dgram');
var message = new Buffer("hi");
var client = dgram.createSocket("udp4");

//socket.send(buf, offset, length, port, address, [callback])
//socket.send(要發送的buf, buf的偏移, buf長度, port, address, [callback])
client.send(message, 0, message.length, 41234, "localhost", function(err, bytes) {
client.close();
});

//輸出以下:

$ node server.js
server listening 0.0.0.0:41234
server got: hi from 127.0.0.1:58682

4)UDP套接字事件
udp socket只是一個eventemitter實例,不是stream實例,事件以下:

  • message,當udp套接字偵聽網卡端口後,接收到消息時觸發該事件,觸發攜帶的數據爲消息Buffer對象和一個遠程地址信息
  • listening,udp開始監聽時,觸發該事件
  • close,調用close()時觸發該事件,並再也不觸發message事件,如需再次觸發message事件,從新綁定便可
  • error,異常觸發該事件,若是不監聽,異常將直接拋出,使進程退出

3.構建HTTP服務

咱們將會使用node的核心模塊http和https進行構建,這兩個模塊分別對http和https協議進行了抽象和封裝,最大限度的模擬http協議和https協議的行爲。

var http = require('http');
http.createServer(function (req, res) {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('Hello World\n');
}).listen(1337, '127.0.0.1');
console.log('Server running at http://127.0.0.1:1337/');

1)HTTP
1.初識HTTP
HTTP是超文本傳輸協議,英文寫做HyperText Transfer Protocol,它是構建在TCP協議之上的。在http的兩端分別是客戶端和服務器,這就是經典的B/S模式。另外,這裏的B,就是瀏覽器的意思,瀏覽器成爲了http的代理,用戶的行爲將會經過瀏覽器轉化爲http請求報文,發送給服務器,服務器也就是S,會處理請求,而後發送響應報文給代理,也就是瀏覽器,瀏覽器解析響應報文後,將用戶界面展現給用戶。這裏咱們看到,基於http或者https的B/S模式中國,瀏覽器只負責發送報文、接收報文、解析報文、展現界面,服務器負責處理http請求和發送http響應。

2.HTTP報文

採用curl工具,查看此次網絡通訊的全部報文信息。報文分爲四部分。

$ curl -v http://127.0.0.1:1337
//第一部分:是經典的TCP三次握手,這樣就創建了鏈接
* About to connect() to 127.0.0.1 port 1337 (#0)
* Trying 127.0.0.1...
* connected
* Connected to 127.0.0.1 (127.0.0.1) port 1337 (#0)
//第二部分:在完成握手以後,客戶端向服務器端發送請求報文。
> GET / HTTP/1.1
> User-Agent: curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8r zlib/1.2.5
> Host: 127.0.0.1:1337
> Accept: */*
>
//第三部分:服務器端完成處理後,向客戶端發送的響應內容,包括響應頭和響應體。
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Date: Sat, 06 Apr 2013 08:01:44 GMT
< Connection: keep-alive
< Transfer-Encoding: chunked
<
Hello World
第四部分:結束會話的信息
* Connection #0 to host 127.0.0.1 left intact
* Closing connection #0

注意:報文的內容主要是兩部分,報文頭和報文體,上一個例子中,使用的是get請求,報文頭的部分是上邊報文信息中>和<的部分。在響應報文中,有一個報文體,是Hello World。

2)http模塊
Node 的http模塊包含對http處理的封裝。 在node中,http服務繼承tcp服務器(net模塊),它可以與多個客戶端保持鏈接,因爲採用事件驅動的方式,所以,並不爲每個鏈接建立額外的線程或進程,保持很低的內存佔用,因此能實現高併發。

http服務與tcp服務模型有區別的地方在於,在開啓keepalive以後,一個tcp會話能夠用於屢次請求和響應,tcp服務以connection爲單位進行服務,http以request爲單位進行服務。http模塊也就是將connection到request的過程進行了封裝。

clipboard.png

http模塊將鏈接所用的套接字的讀寫抽象爲ServerRequest和ServerResponse對象,在請求產生的過程當中,http模塊拿到鏈接中傳來的數據,調用二進制模塊http_parser進行解析,在解析完請求報文的報文頭後,觸發request事件,以後調用用戶的業務邏輯。
clipboard.png

處理程序對應的代碼就是響應Hello World這部分

function (req, res) {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end('Hello World\n');
}

1.HTTP請求

對於tcp鏈接的讀操做,http模塊將其封裝爲ServerRequest對象,咱們再來看看報文頭,此處報文頭會被http_parser進行解析:

> GET / HTTP/1.1
> User-Agent: curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8r zlib/1.2.5
> Host: 127.0.0.1:1337
> Accept: */*
>

第一行報文頭GET / HTTP/1.1會被解析爲,以下屬性:

屬性 說明
req.method 值爲GET,也就是req.method='GET',這個就是請求方法,咱們常見的請求方法有GET、POST、DELETE、PUT、CONNECT等
req.url 值爲/,也就是req.url='/'
req.httpVersion 值爲1.1,也就是req.httpVersion='1.1'

其他的報文頭都會被解析爲頗有規律的json,也就是key和value。這些值,被解析到req.headers屬性上。

headers:
{ 'user-agent': 'curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8r zlib/1.2.5',
host: '127.0.0.1:1337',
accept: '*/*' }

報文體部分則被抽象爲一個只讀流對象,若是業務邏輯須要讀取報文體中的數據,則要在這個數據流結束後才能進行操做:

function (req, res) {
    // console.log(req.headers);
    var buffers = [];
    req.on('data', function (trunk) {
        buffers.push(trunk);
    }).on('end', function () {
        var buffer = Buffer.concat(buffers);
        // TODO
        res.end('Hello world');
    });
}

2.HTTP響應

http響應,也就是對套接字的寫操做進行了封裝,能夠將其當作一個可寫的流對象。
影響響應報文頭部信息的API是res.setHeader()和res.writeHead()。

咱們能夠屢次調用setHeader進行屢次設置,可是隻能調用一次writeHead,而且也只有調用了writeHead後,纔會將響應報文頭寫入到鏈接中,除此以外,http模塊還會自動幫你設置一些頭信息:

< Date: Sat, 06 Apr 2013 08:01:44 GMT
< Connection: keep-alive
< Transfer-Encoding: chunked
<

報文體部分則是經過調用res.write()和res.end()實現的,res.end()會先調用write()發送數據,而後,發送信號通知服務器此次響應結束,響應的結果就是咱們以前發送的hello world。

響應結束後,http服務器可能會將當前的鏈接用於下一個請求,或者關閉鏈接。另外,報頭是在報文體發送前發送的,一旦開始了數據的發送,再次調用writeHead和setHead將再也不生效。

另外,服務器無論是完成業務,仍是發生異常,都應該調用res.end()以結束請求,不然客戶端將會一直處於等待的狀態。固然,也能夠經過延遲res.end()的方式,來實現與客戶端的長鏈接,可是結束時,務必關閉鏈接。

3.HTTP服務的事件
http服務也繼承了events模塊,所以也是一個EventEmitter實例。

  • connection : 在http請求和響應前,客戶端與服務器須要創建tcp鏈接,這個鏈接可能由於開啓了keep-alive,能夠在屢次響應和請求之間使
    用,當創建鏈接時,服務器觸發一次connection事件
  • request : 創建tcp鏈接後,http模塊底層將在數據流中抽象http請求和響應,當請求數據發送到服務器,在解析出http請求頭後,將會觸發該
    事件,在res.end()後,tcp鏈接可能用於下一次請求響應
  • close : 與tcp服務器的行爲一致,調用server.close()中止接受新的鏈接,當已有的鏈接都斷開時,觸發該事件,能夠給server.close()傳遞
    一個回調函數,來快速註冊該事件。
  • checkContinue : 客戶端發送較大的數據時,不會將數據直接發送,而是先發送一個頭部帶Expect:100-continue的請求到服務器,服務器將會 觸發checkContinue事件,若是沒有爲服務器監聽這個事件,服務器將會自動響應客戶端100 Continue的狀態碼,表示接受數據上傳,若是不接 受的數據較多時,響應客戶端400 Bad Request,拒絕客戶端繼續發送數據便可。須要注意的是,該事件發生時不會觸發request事件,兩個事件是 互斥的,當客戶端收到100 Continue後,從新發起請求時,纔會觸發request事件
  • connect : 當客戶端發起CONNECT請求時觸發,而發起CONNECT請求,一般在HTTP代理出現,若是不監聽該事件,發起該請求的鏈接將會關閉
  • upgrade : 當客戶端要求升級鏈接協議時,須要和服務器端協商,客戶端會在請求頭中帶上Upgrade字段,服務器端會在接收到這樣的請求時觸發
    該事件,這個會在websocket中詳細介紹,一樣,若是不監聽該事件發起該請求的鏈接將會關閉。
  • clientError : 鏈接的客戶端觸發error事件,這個錯誤會傳遞到服務器端,此時觸發該事件。

3)HTTP客戶端
http客戶端會產生請求報文頭和報文體,接收響應報文頭和報文體,並解析。除了瀏覽器,咱們也能夠經過http模塊提供的http.request(options,connect)來構造http客戶端。

var http = require('http');
var options = {
   host:'127.0.0.1',
    hostname: '127.0.0.1',
    port: 1334,
    path: '/',
    method: 'GET'
};
var req = http.request(options, function (res) {
    console.log('STATUS: ' + res.statusCode);
    console.log('HEADERS: ' + JSON.stringify(res.headers));
    res.setEncoding('utf8');
    res.on('data', function (chunk) {
        console.log(chunk);
    });
});
req.end();

//輸出:

$ node client.js
STATUS: 200
HEADERS: {"date":"Sat, 06 Apr 2013 11:08:01
GMT","connection":"keep-alive","transfer-encoding":"chunked"}
Hello World

options決定了http請求頭的內容,選項以下:

參數 說明
host 服務器的域名或IP地址,默認localhost
hostname 服務器名稱
port 服務器端口,默認80
localAddress 創建網絡鏈接的本地網卡
sockerPath Domain套接字路徑
method http請求方法,默認GET
path 請求路徑,默認爲/
headers 請求頭對象
auth Basic認證,這個值將被計算成請求頭中的Authorization

報文體的內容則由請求對象的wirte()和end()方法實現,經過write寫入數據,經過end告知報文結束。

1.HTTP響應
http客戶端的響應對象與服務器端較爲相似,在ClientRequest對象中,它的事件也被稱爲response,ClientRequest在解析響應報文時,解析完響應頭就會觸發response事件,同時傳遞一個響應對象以供操做ClientResponse,後續響應報文以只讀流的方式提供。

function(res) {
    console.log('STATUS: ' + res.statusCode);
    console.log('HEADERS: ' + JSON.stringify(res.headers));
    res.setEncoding('utf8');
    res.on('data', function (chunk) {
        console.log(chunk);
    });
}

2.HTTP代理
如同服務器端的實現同樣,http提供的ClientRequest對象也是基於tcp實現的,在keepalive的狀況下,一個底層會話鏈接能夠屢次用於請求,爲了重用tcp鏈接,http模塊包含一個默認的客戶端代理對象http.globalAgent,它對每一個服務器端的host+port建立的鏈接進行了管理,默認狀況下,經過ClientRequest對象對同一個服務器端發起的HTTP請求最多能夠建立5個鏈接,它的實質是一個鏈接池。

clipboard.png

自行構造代理對象:

var agent = new http.Agent({
    maxSockets: 10
});
var options = {
    hostname: '127.0.0.1',
    port: 1334,
    path: '/',
    method: 'GET',
    agent: agent
};

也能夠設置Agent選項爲false,以脫離鏈接池的管理,使得請求不受併發的限制。

Agent對象的sockets和requests屬性分別表示當前鏈接池中使用的鏈接數和處於等待狀態的請求數,在業務中監視這兩個值有助於發現業務狀態的繁忙程度。

3.HTTP客戶端事件

  • response : 處理服務器端返回的response,返回後,觸發該事件
  • socket : 當底層鏈接池中創建的鏈接分配給當前請求對象時,觸發該事件
  • connect : 當客戶端向服務器端發起CONNECT請求時,若是服務器端響應了200狀態碼,客戶端會觸發該事件
  • upgrade : 客戶端向服務器端發起Upgrade請求時,若是服務器端響應了101 Switching Protocols狀態,客戶端將會觸發該事件
  • continue : 客戶端向服務器端發起Expect: 100-continue頭信息,以試圖發送較大數據量,若是服務器端響應100 Continue狀態,客戶端將觸發該事件。

4.構建WebSocket服務

websocket與傳統HTTP有以下好處:

  • 客戶端與服務器只創建一個tcp鏈接,可使用更少的鏈接
  • websocket服務器端能夠推送數據到客戶端,這遠比HTTP請求響應模式更靈活,更高效。
  • 有更輕量級的協議頭,減小數據傳送量

    //websocket客戶端程序
    var socket = new WebSocket('ws://127.0.0.1:12010/updates');
    socket.onopen = function () {
    setInterval(function() {
    if (socket.bufferedAmount == 0)
    socket.send(getUpdateData());
    }, 50);
    };
    socket.onmessage = function (event) {
    // TODO: event.data
    };

websocket是經過tcp從新擬定的新的協議,不是在http協議的基礎上的封裝。websocket分爲握手和數據傳輸兩部分,其中握手使用了http進行。

1)WebSocket握手
客戶端創建鏈接是,經過HTTP發起請求報文。以下所示:

GET /chat HTTP/1.1
Host: server.example.com
//請求服務端升級協議爲WebSocket
Upgrade: websocket
Connection: Upgrade
//用於安全校驗
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
//指定子協議和版本號
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

Sec-WebSocket-Key的值是隨機生成的base64編碼的字符串。服務器端接收到以後,將其與字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11相連,造成字符串dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11,而後經過sha1安全散列算法計算出結果後,再進行base64編碼,最後,返回給客戶端,咱們看一下這個算法:

var crypto = require('crypto');
var val = crypto.createHash('sha1').update(key).digest('base64');

服務器端在處理完請求後,響應以下報文:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat

這段報文將告訴客戶端,正在更換協議,更新爲應用層協議websocket,並在當前的套接字上應用新的協議。
剩餘的字段分別表示服務器端基於Sec-WebSocket-Key生成的字符串和選中的子協議。客戶端將會校驗Sec-WebSocket-Accept的值,若是成功,將開始接下來的數據傳輸。
咱們使用node來模擬瀏覽器發起協議切換的行爲:

var WebSocket = function (url) {
    // 僞代碼,解析ws://127.0.0.1:12010/updates,用於請求
    this.options = parseUrl(url);
    this.connect();
};
WebSocket.prototype.onopen = function () {
    // TODO
};
WebSocket.prototype.setSocket = function (socket) {
    this.socket = socket;
};
WebSocket.prototype.connect = function () {
    var this = that;
    var key = new Buffer(this.options.protocolVersion + '-' + Date.now()).toString('base64');
    var shasum = crypto.createHash('sha1');
    var expected = shasum.update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11').digest('base64');
    var options = {
        port: this.options.port, // 12010
        host: this.options.hostname, // 127.0.0.1
        headers: {
            'Connection': 'Upgrade',
            'Upgrade': 'websocket',
            'Sec-WebSocket-Version': this.options.protocolVersion,
            'Sec-WebSocket-Key': key
        }
    };
    var req = http.request(options);
    req.end();
    req.on('upgrade', function (res, socket, upgradeHead) {
        // 鏈接成功
        that.setSocket(socket);
        //觸發open事件
        that.onopen();
    });
};

服務器端的響應代碼

var server = http.createServer(function (req, res) {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('Hello World\n');
});
server.listen(12010);
// 在收到upgrade請求後,告知客戶端容許切換協議
server.on('upgrade', function (req, socket, upgradeHead) {
    var head = new Buffer(upgradeHead.length);
    upgradeHead.copy(head);
    var key = req.headers['sec-websocket-key'];
    var shasum = crypto.createHash('sha1');
    key = shasum.update(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").digest('base64');
    var headers = [
        'HTTP/1.1 101 Switching Protocols',
        'Upgrade: websocket',
        'Connection: Upgrade',
        'Sec-WebSocket-Accept: ' + key,
        'Sec-WebSocket-Protocol: ' + protocol
    ];
    // 讓數據當即發送
    socket.setNoDelay(true);
    socket.write(headers.concat('', '').join('\r\n'));
    // 創建服務器端WebSocket鏈接
    var websocket = new WebSocket();
    websocket.setSocket(socket);
});

一旦websocket握手成功,服務器端與客戶端就將會呈現對等的效果,都能接收和發送消息。

2)WebSocket數據傳輸

在順利握手後,當前鏈接將再也不進行http交互,而是開始websocket的數據幀協議,實現客戶端與服務器的數據交換。
協議升級的過程以下:

clipboard.png

當客戶端調用send()發送數據時,服務器端觸發onmessage(),當服務器端調用send()發送數據時,客戶端的onmessage()觸發,當咱們調用send()發送一條數據時,協議可能將這個數據封裝爲一幀或多幀數據,而後逐幀發送。
爲了安全考慮,客戶端須要發送的數據幀進行掩碼處理,服務器一旦收到無掩碼幀,好比中間攔截破壞,鏈接將會關閉。服務器發送到客戶端的數據幀無需作掩碼,若是客戶端收到了帶掩碼的數據幀,鏈接也將關閉。
在websocket中的數據幀的定義,每8位爲一列,也就是一個字節,其中每一位都有它的意義:

clipboard.png

  • fin,若是這一幀是最後一幀,這個fin爲爲1,其他狀況爲0.
  • rsv一、rsv二、rsv3,都是一位長,用於標識擴展,當有已協商的擴展時,這些值可能爲1,其他狀況爲0。
  • opcode,4位長,能夠用來表示0~15的值,用於解釋當前數據幀,0表示附加數據幀,1表示文本數據幀,2表示二進制數據幀,8表示發送一個鏈接關閉的數據幀,9表示ping數據幀,10表示pong數據幀,其他值暫時沒有定義。瓶數據幀和pong數據幀用於心跳檢測,當一端發送一個ping數據幀時,另外一端必須發送pong數據幀做爲迴應,告知對方這一端仍然處於響應狀態。
  • masked,表示是否進行掩碼處理,1位長度,客戶端發送給服務器時爲1,服務器發送回客戶端時爲0.
  • payload length:一個七、7+16或7+64位長的數據爲,標識數據的長度,若是值在0~125之間那麼該值就是數據的真實長度,若是是126,則後面16位的值是數據的真實長度,若是是127,則後面64位的值是數據的真實長度。
  • making key,當masked爲1時,這裏是一個32位長的數據位,用於解密數據。
  • payload data,咱們的目標數據,位數爲8的倍數。

客戶端發送消息時,須要構造一個或多個數據幀協議報文,例如咱們發送一個hello world,這個比較短,不存在分割多個數據幀的狀況,而且以文本方式發送,他的payload length長度爲96(12字節*8位/字節),二進制表示爲110000。因此報文應該是:

fin(1) + res(000) + opcode(0001) + masked(1) + payload length(1100000) + masking key(32位) + payload
data(hello world!加密後的ܾ二進制)

服務器回覆的是yakexi,這個無需掩碼,形式以下:

fin(1) + res(000) + opcode(0001) + masked(0) + payload length(1100000) + payload data(yakexi的ܾ二進制)

5.網絡服務與安全

SSL(secure socket layer)做爲一種安全協議,它在傳輸層提供對網絡鏈接的加密的功能,對於應用層它是透明的,數據在傳遞到應用層以前就已經完成了加密和解密的過程。
最開始使用這個協議的是網景的瀏覽器,而後,爲了被更多的服務器核瀏覽器支持,IETF組織將其標準化,也就是TLS = transport layer security。
node在網絡安全方面提供了crypto、tls、https三個模塊,crypto用於加密解密,例如sha一、md5等加密算法,tls用於創建一個基於TLS/SSL的tcp連接,它能夠當作是net模塊的加密升級版本。https用於提供一個加密版本的http,也是http的加密升級版本,甚至提供的接口和事件也跟http模塊同樣。

1)TLS/SSL

1.密鑰
TLS/SSL是一個公鑰/私鑰的結構,這也是一個非對稱的結構,每一個服務器和客戶端都有本身的公鑰和私鑰。公鑰用來加密要傳輸的數據,私鑰用來解密接收到的數據。公鑰和私鑰是配對的,經過公鑰加密的數據,只有經過私鑰才能解密,因此在創建安全傳輸以前,客戶端和服務器端之間須要互換公鑰。客戶端發送數據時要經過服務器端的公鑰進行加密,服務器端發送數據時則須要客戶端的公鑰進行加密,如此才能完成加密解密的過程:

clipboard.png

node在底層採用openssl來實現TLS/SSL,爲此要生成公鑰和私鑰須要經過openssl來完成,咱們分別爲服務器端和客戶端生成私鑰:

// 生成服務器端私鑰
$ openssl genrsa -out server.key 1024
// 生成客戶端私鑰
$ openssl genrsa -out client.key 1024

上述命令生成了兩個1024位長的RSA私鑰文件,咱們繼續經過它生成公鑰:

$ openssl rsa -in server.key -pubout -out server.pem
$ openssl rsa -in client.key -pubout -out client.pem

公鑰和私鑰的非對稱性加密雖然很好,可是網絡中依然可能存在竊聽的狀況,典型的例子就是中間人攻擊。客戶端和服務器端在交換公鑰的過程當中,中間人對客戶端扮演服務器端的角色,對服務器端扮演客戶端的角色,所以客戶端和服務器端幾乎感覺不到中間人的存在,爲了解決這個問題,數據傳輸過程當中還須要對獲得的公鑰進行認證,以確認獲得的公鑰是出自目標服務器的,若是不能保證這種認證,中間人可能會將僞造的站點響應給用戶,從而形成經濟損失。

clipboard.png

爲了解決中間人攻擊的問題,TLS/SSL引入了數字證書來進行認證,與直接公鑰不一樣,數字證書中包含了服務器的名稱和主機名稱、服務器的公鑰、簽名頒發機構的名稱、來自簽名頒發機構的簽名。在創建鏈接前,會經過證書中的簽名確認收到的公鑰是來自目標服務器的,從而產生信任關係。

2.數字證書
CA (Certificate Authority,數字證書認證中心)是數字證書的頒發機構,這個證書具備ca經過本身的公鑰和私鑰實現的簽名。
爲了獲得ca的簽名證書,服務器端須要經過本身的私鑰生成CSR = certificate signing request文件,ca機構將經過這個文件頒發屬於該服務器的簽名證書,只要經過ca機構就能驗證證書是否合法。
經過ca機構頒發證書一般是一個繁瑣的過程,須要付出必定的精力和費用,對於中小企業來講,能夠採用自簽名證書來構建安全的網絡,也就是本身給本身的服務器扮演ca機構,給本身的服務器頒發本身的ca生成的簽名證書。咱們仍是使用openssl來實現這一過程

//生成服務器私鑰
$ openssl genrsa -out ca.key 1024
//生成csr文件
$ openssl req -new -key ca.key -out ca.csr
//經過私鑰自簽名生成證書,此時尚未業務服務器的簽名
$ openssl x509 -req -in ca.csr -signkey ca.key -out ca.crt

clipboard.png

這樣就生成了本身的簽名證書,而後再次回到服務器端,服務器須要向ca申請簽名,在申請簽名以前,依然須要建立本身的csr,值得注意的是,這個過程當中的common name須要匹配服務器域名,不然在後續的認證過程當中會出錯:

//生成本身的業務服務器csr
$ openssl req -new -key server.key -out server.csr
//向本身的ca申請簽名證書,這個過程須要ca的證書和私鑰參與,最終生成帶簽名的證書
$ openssl x509 -req -CA ca.crt -CAkey ca.key -CAcreateserial -in server.csr -out server.crt

以後,客戶端發起安全鏈接前會去捕獲服務器端的證書,並經過ca的證書驗證服務器端證書的真僞。除了驗證真僞外,一般還含有對服務器名稱、IP地址等進行檢驗的過程:

clipboard.png

ca機構將證書頒發給服務器端後,證書在請求的過程當中會被髮送給客戶端,客戶端須要經過ca的證書驗證真僞。若是是知名的ca機構,他們的證書通常都會預裝在瀏覽器中,若是是本身扮演的ca,就須要讓客戶本身先去獲取這個ca而後才能進行驗證。
另外,ca的證書通常被稱爲根證書,也就是不須要上級證書參與簽名的證書。

2)TLS服務
先基於tls模塊建立服務器端程序

var tls = require('tls');
var fs = require('fs');
var options = {
    key: fs.readFileSync('./keys/server.key'),
    cert: fs.readFileSync('./keys/server.crt'),
    requestCert: true,
    ca: [fs.readFileSync('./keys/ca.crt')]
};
var server = tls.createServer(options, function (stream) {
    console.log('server connected', stream.authorized ? 'authorized' : 'unauthorized');
    stream.write("welcome!\n");
    stream.setEncoding('utf8');
    stream.pipe(stream);
});
server.listen(8000, function () {
    console.log('server bound');
})

建立客戶端程序

var tls = require('tls');
var fs = require('fs');
var options = {
    key: fs.readFileSync('./keys/client.key'),
    cert: fs.readFileSync('./keys/client.crt'),
    ca: [fs.readFileSync('./keys/ca.crt')]
};
var stream = tls.connect(8000, options, function () {
    console.log('client connected', stream.authorized ? 'authorized' : 'unauthorized');
    process.stdin.pipe(stream);
});
stream.setEncoding('utf8');
stream.on('data', function (data) {
    console.log(data);
});
stream.on('end', function () {
    server.close();
});

客戶端啓動以後,就能夠在輸入流中輸入數據了,服務器端將會迴應相同的數據。至此咱們完成了TLS的服務器端和客戶端的建立,與普通的tcp服務器和客戶端相比,TLS的服務器核客戶端僅僅只是須要配置證書,其餘基本同樣。
3)HTTPS服務

HTTPS其實就是TLS/SSL基礎上的HTTP。換句話說,net模塊對應http模塊,tls模塊對應https模塊,咱們來建立一個https服務:
1.準備證書

HTTPS服務須要用到私鑰和簽名證書。

2.建立https服務

var https = require('https');
var fs = require('fs');
var options = {
    key: fs.readFileSync('./keys/server.key'),
    cert: fs.readFileSync('./keys/server.crt')
};
https.createServer(options, function (req, res) {
    res.writeHead(200);
    res.end("hello world\n");
}).listen(8000);

3.https客戶端

var https = require('https');
var fs = require('fs');
var options = {
    hostname: 'localhost',
    port: 8000,
    path: '/',
    method: 'GET',
    key: fs.readFileSync('./keys/client.key'),
    cert: fs.readFileSync('./keys/client.crt'),
    ca: [fs.readFileSync('./keys/ca.crt')]
};
options.agent = new https.Agent(options);
var req = https.request(options, function (res) {
    res.setEncoding('utf-8');
    res.on('data', function (d) {
        console.log(d);
    });
});
req.end();
req.on('error', function (e) {
    console.log(e);
});

//輸出結果
$ node client.js
hello world

//若是不設置ca的話,會報錯
[Error: UNABLE_TO_VERIFY_LEAF_SIGNATURE]

這個異常能夠經過添加屬性
rejectUnauthorized:false解決,這個與curl -k效果一致
相關文章
相關標籤/搜索