5.內存控制
6.理解Buffer
7.網絡編程
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 }
調整內存限制大小:算法
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中,主要將內存分爲新生代和老生代兩代。新內存中的對象存活時間短,老內存中的對象存活時間長或常駐內存對象。編程
2.新生代垃圾回收算法scavenge算法
新生代中的對象主要經過scavenge算法進行垃圾回收,其主要是採用cheney算法進行具體處理。
cheney算法採用一種複製方式的垃圾回收算法,將堆內存一分爲二,只有一部分空間被使用稱爲From空間,另外一個處於閒置稱爲To空間。當進行分配對象的時候先在from空間分配,當進行垃圾回收時,會檢查from空間中的存活對象,將這些存活對象複製到to空間中,複製完成後From和to空間角色互換,清空to空間,在垃圾回收過程當中就是經過將存活對象在兩個空間中進行復制。
當一個對象通過屢次複製依然存活時,就會被認爲是生命週期較長的對象,會被移入老生代內存中。
對於移入老生代內存有兩個條件:
老內存垃圾回收算法Mark-Sweep & Mark-Compact
採用標記清除,它分爲標記清除兩個階段
在標記階段遍歷全部的對象並標記活着的對象,在清除階段只清除死亡的對象,死亡對象在老生代內存只佔一小部分。老生代內存進行一次清除後,內存空間會出現不連續的狀態,因此清理完成須要進行一步標記整理。、
Incremental Marking
爲了不出現javaScript應用邏輯與垃圾回收器看到不一致的狀況,垃圾回收都要將應用邏輯停下來,這種行爲會形成停頓,在新生代垃圾回收過程當中由於存活對象比較少,即便停頓基本影響不大。在老生代垃圾回收中,一般存活對象較多,全堆垃圾回收的標記、清除、整理影響較大。
解決辦法:分批次進行,拆分紅許多小步,每進行一小步就讓邏輯運行一會
5)查看垃圾回收日誌
在啓動時時加入 參數 --trace_gc。
還能夠在啓動時增長--prof參數,來獲得v8執行時的性能分析數據,其中包含了垃圾回收執行時佔用的時間等。
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)*小結
能夠利用閉包和垃圾回收的機制,來存儲一些須要存活時間長一些的對象,並將其做爲公共訪問的數據區域來使用。可是,閉包和全局變量的使用仍是要當心,因爲沒法及時回收內存,這會增長常駐內存的產生,會致使老生代中的對象增多。
1)查看內存使用狀況
2)堆外內存
堆中的內存用量老是小於進程的常駐內存用量,這意味着Node中的內存使用並不是都是經過V8進行分配的,這些不經過V8分配的內存,稱爲堆外內存。
例如:buffer對象不通過v8內存分配,所以,也不會有堆內存的大小限制。
3)小結
Node的內存主要由經過V8進行分配的部分和Node 自行分配的部分,受V8的垃圾回收限制的主要是V8的堆內存。
內存泄漏的實質就是應當回收的對象由於意外沒有被回收,變成了常駐在老生代中的對象。
形成內存泄漏的主要緣由有:緩存、隊列消費不及時、做用域未釋放。
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的超時模式和拒絕模式。
定位Node應用的內存泄漏經常使用工具以下:
工具 | 說明 |
---|---|
v8-profiler | 能夠對v8堆內存抓取快照,並對cpu進行分析 |
node-heapdump | 能夠對v8堆內存抓取快照,用於過後分析 |
node-mtrace | 使用gcc的mtrace工具來分析堆的使用 |
dtrace | 在smartos上使用的內存分析工具 |
node-memwatch | 採用wtfpl許可發佈的內存分析工具 |
使用流的方式操做大內存,也就是使用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內存的限制。可是,物理內存依然有限制。
由於在node中須要處理網絡協議、操做數據庫、處理圖片、接受上傳文件等,在網絡流和文件操做中,還要處理大量二進制數據,js自有的字符串遠遠不能知足這些需求,因而Buffer對象應運而生。
Buffer是一個像Array的對象,可是它主要用於操做字節。
1)模塊結構
buffer是一個典型的js與c++結合的模塊,將性能相關的部分用c++實現,將非性能相關的部分用js實現。同時buffer也是node的核心模塊,能夠直接使用,而且,buffer屬於堆外內存,能夠經過本身管理其垃圾回收。固然,buffer對象的管理仍是在堆內,再由這個對象去管理堆外的內存。
因爲Buffer太常見,Node在進程啓動時已經加載了它,並將其放在全局對象上,因此在使用Buffer時,無須經過require()便可直接使用。
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種狀態:
分配指定大小的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++層面提供的內存,無需頻繁的分配操做。
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來解決。
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); });
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的大小對性能的影響:
Node提供了net、dgrm、http、https這四個模塊,分別用於處理TCP、UDP、HTTP、HTTPS,適用於服務器端和客戶端。
1)TCP
TCP全稱爲傳輸控制協議,在OSI模型上屬於傳輸層協議。
七層協議示意圖以下:
TCP是面向鏈接的協議,其顯著特徵是在傳輸以前須要3次握手造成會話。
只有會話造成後,服務器端和客戶端才能相互發送數據。在建立會話的過程當中,服務器端和客戶端分別提供一個套接字,
這兩個套接字共同造成一個鏈接。服務器端和客戶端則經過套接字實現二者之間鏈接的操做。
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實例,有以下事件:
2.鏈接事件
服務器能夠同時與多個客戶端保持鏈接,對於每一個鏈接而言是可寫可讀Stream對象。
Stream對象能夠用於服務器端和客戶端之間的通訊,能夠經過data事件從一端讀取另外一端發來的數據,也能夠經過write()從一端向另外一端發送數據。
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事件仍是要進行小包合併後觸發的,這個須要注意。
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實例,事件以下:
咱們將會使用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的過程進行了封裝。
http模塊將鏈接所用的套接字的讀寫抽象爲ServerRequest和ServerResponse對象,在請求產生的過程當中,http模塊拿到鏈接中傳來的數據,調用二進制模塊http_parser進行解析,在解析完請求報文的報文頭後,觸發request事件,以後調用用戶的業務邏輯。
處理程序對應的代碼就是響應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實例。
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個鏈接,它的實質是一個鏈接池。
自行構造代理對象:
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客戶端事件
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的數據幀協議,實現客戶端與服務器的數據交換。
協議升級的過程以下:
當客戶端調用send()發送數據時,服務器端觸發onmessage(),當服務器端調用send()發送數據時,客戶端的onmessage()觸發,當咱們調用send()發送一條數據時,協議可能將這個數據封裝爲一幀或多幀數據,而後逐幀發送。
爲了安全考慮,客戶端須要發送的數據幀進行掩碼處理,服務器一旦收到無掩碼幀,好比中間攔截破壞,鏈接將會關閉。服務器發送到客戶端的數據幀無需作掩碼,若是客戶端收到了帶掩碼的數據幀,鏈接也將關閉。
在websocket中的數據幀的定義,每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的ܾ二進制)
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是一個公鑰/私鑰的結構,這也是一個非對稱的結構,每一個服務器和客戶端都有本身的公鑰和私鑰。公鑰用來加密要傳輸的數據,私鑰用來解密接收到的數據。公鑰和私鑰是配對的,經過公鑰加密的數據,只有經過私鑰才能解密,因此在創建安全傳輸以前,客戶端和服務器端之間須要互換公鑰。客戶端發送數據時要經過服務器端的公鑰進行加密,服務器端發送數據時則須要客戶端的公鑰進行加密,如此才能完成加密解密的過程:
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
公鑰和私鑰的非對稱性加密雖然很好,可是網絡中依然可能存在竊聽的狀況,典型的例子就是中間人攻擊。客戶端和服務器端在交換公鑰的過程當中,中間人對客戶端扮演服務器端的角色,對服務器端扮演客戶端的角色,所以客戶端和服務器端幾乎感覺不到中間人的存在,爲了解決這個問題,數據傳輸過程當中還須要對獲得的公鑰進行認證,以確認獲得的公鑰是出自目標服務器的,若是不能保證這種認證,中間人可能會將僞造的站點響應給用戶,從而形成經濟損失。
爲了解決中間人攻擊的問題,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
這樣就生成了本身的簽名證書,而後再次回到服務器端,服務器須要向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地址等進行檢驗的過程:
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效果一致