github地址:戳這裏javascript
目標:寫一個基於事件驅動 ,非阻塞i/o 的web服務器,以達到更高的性能。構建快速,可伸縮的網絡應用平臺php
js開發性能低,事件驅動應用html
node強制不共享任何資源的 單線程 ,單進程系統,包含十分適宜網絡的庫前端
兩個readFile的操做最終時間爲最慢的那一個java
事件編程方式:輕量級,輕耦合,只關注事務點等優點node
單線程nginx
前言:c++
web 1.0 : JavaScript用於表單校驗和網頁特效,只有對bom,dom的支持git
web 2.0 : 提高了網頁的用戶體驗,bs應用展示出了比cs(須要裝客戶端)應用優越的地方。h5嶄露頭角github
此過程經歷了工具-組件-框架-應用的變遷
js的規範缺陷:
require()
同步,爲後端js指定的規範,並不徹底適合前端的應用場景
已被編譯進了二進制執行文件,node啓動時就被加載進內存,因此1.2步驟能夠省略。且加載速度最快
動態加載,速度比核心模塊慢
路徑分析
..
或者 .
相對路勁模塊/
開頭的絕對路徑模塊connect
模塊若是想加載與核心模塊標識符相同的模塊,必須選擇 不一樣的標識符 或者 換用路徑 的方法
以 .
,..
,/
開頭的標識符,會將路徑轉換成真實路徑
自定義模塊是最費時的
module.paths
模仿搜索路徑
規則以下:
文件定位
文件擴展名
.js
.node
.json
順序補齊
fs
模塊同步阻塞式的判斷文件是否存在,若是是.node
和.json
文件,帶上擴展名再配合緩存能夠加快速度
目錄和包的處理
package.json
,取出 main
屬性指定的文件名定位。package.json
, 會將 index
做爲默認文件名編譯執行
node會新建一個模塊對象,而後根據路徑載入並編譯,對應不一樣擴展名,載入方法不一樣:
.js
經過 fs
同步讀取.node
經過 dlopen()
加載.json
經過fs讀取,再 JSON.parse
.js
每個編譯成功的模塊都會被綁定在 Module._cache
上
編譯過程對文件內容進行頭尾包裝
// 經過vm原生模塊runInThisContext方法執行,不污染全局
(function (exports, require, module, __filename, __dirname) {
})
複製代碼
另外,這樣會出錯
exports = function () {
// My class
}
複製代碼
緣由在於,exports對象是經過形參的方式傳入的,直接賦值會改變形參的做用,但並不能改變做用域外的值。
c++模塊主內完成核心,js主外實現封裝
性能優於腳本語言
被編譯成二進制文件,一旦node開始執行,就直接加載進緩存
依賴關係:文件模塊 <--
核心模 塊<--
內建模塊
package.json
包描述文件
bin
存放可執行二進制文件的目錄
lib
存放js的代碼目錄
doc
存放文檔
test
存放單元測試用例
查看幫助npm help
安裝依賴包npm install --save/--save-dev express
-g是講一個包安裝到全局可用的可執行命令。它根據包描述文件中的bin字段配置,將實際腳本鏈接到與node可執行文件相同的路徑下
若是node可執行文件的位置是/usr/local/bin/node
,那麼模塊目錄就是/usr/local/lib/node_modules
。最後經過軟連接方式將bin字段配置的可執行文件連接到node的可執行目錄下
本地安裝
換源:
npm install underscore --registry=http:registry.url
npm config set registry http:registry.url
npm鉤子
發佈包
npm adduser
npm publish<folder>
npm owner ls <package_name>
npm owner add <user> <package_name>
npm owner rm <user> <package_name>
6. 分析包 npm ls
node模塊引入幾乎都是同步的,但若是前端模塊也採用同步的方式來引入,用戶體驗會形成問題
須要用define來明肯定義一個模塊,而在node實現中是隱式包裝的。
全部的依賴,經過形參傳遞到依賴模塊內容中
define(['dep1', 'dep2'], function (dep1, dep2) {
return function () {}
})
複製代碼
目的是做用域隔離
內容須要返回的方式實現導出
define(function () {
var exports = {};
exports.sayHello = function () {
...
}
return exports
})
複製代碼
更接近commonjs規範
define(function (require, exports, module) {
// ...
})
複製代碼
require,exports, module經過形參傳遞給模塊。
;(function (name, definition) {
var hasDefine = typeof define === 'function';
var hasExports = typeof module !== 'undefined' && module.exports;
if (hasDefine) { // AMD或者CMD
define(definition);
} else if(hasExports) { // 定義爲普通模塊
module.exports = definition()
} else {
this[name] = definition()
}
})('hello', function () {
var hello = function () {}
return hello
})
複製代碼
node面向網絡而設計
利用單線程,原理多線程死鎖,狀態同步問題
利用異步i/o,讓單線程原理阻塞,更好的利用cpu
內核在進行文件i/o的操做時,經過文件描述符進行管理,文件描述符相似於應用程序與系統內核之間的憑證。
阻塞i/o形成cpu等待浪費,非阻塞卻要 輪詢 去確認是否徹底完成數據獲取
理想非阻塞異步i/o:發起非阻塞調用後,能夠直接處理下一個任務,只需i/o完成後經過信號或回調將數據傳遞給應用程序
顯示的異步i/o:經過讓部分線程進行阻塞i/p或者非阻塞i/o加輪詢技術來完成數據獲取,讓一個線程進行計算處理,經過線程之間的通訊將i/o獲得的數據進行傳遞
用戶體驗
若是是同步,js執行ui渲染和響應將處於停滯狀態
採用異步,在下載資源期間,js和ui的執行都不會處於等待狀態
採用異步方式所花時間爲max(m, n)
資源分配
缺點:
單線程同步編程模型會由於阻塞i/o致使性能差,
缺點:
代價在於建立線程和執行期線程上下文切換的開銷較大
多線程常面臨鎖,狀態同步問題
優勢:
可是能有效提高cpu利用率
模型基本要素:事件循環,觀察者,請求對象,i/o線程池
node自身實際上是多線程的,只是i/o線程使用的cpu較少
每一個事件循環中有一個或者多個觀察者
異步i/o過程當中的重要中間產物,全部的狀態都保存在這個對象中,包括送入線程池等待執行以及i/o操做完畢後的回調處理
建立的定時器會被插入到定時器觀察者內部的一個紅黑樹中
每次Tick執行時,會從紅黑樹中迭代取出定時器對象,檢查是否超過定時時間。若是超過,就造成一個時間,它的回調函數將當即執行
時間複雜度O(lg(n)) 2. process.nextTick
將回調函數放入隊列,在下一輪Tick時取出執行
時間複雜度 0(1)
服務器模型:
node高性能:
var toString = Object.prototype.toString;
var isType = function (type) {
return function (obj) {
return toString.call(obj) == '[object' + type + ']'
}
}
var isFunction = isType('Function')
複製代碼
異步i/o提交請求和處理結果兩個階段中間,有事件循環的調度。異步方法則一般在提交請求後當即返回,由於一場並不必定發生在這個階段,因此try/catch在這裏無效
try/catch對於callback執行時拋出的異常無能爲力
事件發佈/訂閱模式
var events = require('events');
function Stream () {
events.EventEmitter.call(this)
}
util.inherits(Stream, events.EventEmitter)
複製代碼
利用事件隊列解決雪崩問題,once方法
多異步之間的寫做方案
Promise/Deferred
Promise/A
只有三種狀態:rejected,fullfiled, rejected
只能未完成到完成,或者失敗,不能逆反
狀態不能更改
流程控制庫
js在瀏覽器的應用場景,因爲運行時間短,隨着進程的推出,內存會釋放,幾乎沒有內存管理的額必要
內存控制正式在海量請求和長時間運行的前提下進行探討的。
在服務器端,資源寸土寸金
對於性能敏感的服務器端程序,內存管理的好壞,垃圾回收情況的優良,影響很大
在node中經過js使用內存時,只能使用部分,沒法直接操做大內存對象
64位系統下約爲1.4GB,32位系統下約爲0.7GB
node中使用js對象,都是經過V8來進行分配和管理的
js對象經過堆來分配
當在代碼中生命變量並賦值時,所使用對象的內存就分配在堆中。若是已申請的堆空閒內存不夠分配新的對象,將繼續申請堆內存,直到堆得大小超過V8的限制爲止
V8爲什麼限制堆得大小:表層緣由是起初爲瀏覽器而設計,限制值已經綽綽有餘。深層緣由是V8的垃圾回收機制的限制,作一次非增量式的垃圾回收時間花銷大
V8垃圾回收策略主要基 分代式垃圾回收機制
垃圾回收算法:
將內存分爲 新生代 和 老生代
新生代中的對象爲存活時間較短的對象,老生代的對象爲存活時間較長或常駐內存的對象
Scavenge算法
具體實現主要採用Cheney算法
採用複製的方式實現垃圾回收算法。
將堆內存一分爲二。每一份空間成爲semispace。處於閒置狀態的稱爲To空間,處於使用狀態的稱爲From空間。
當開始進行垃圾回收時,會檢查From空間的存活對象,這些存活對象會被複制到To空間。非存活對象佔用空間會被釋放
缺點:用空間換時間
當一個對象通過屢次複製依然存活時,被認爲是生命週期較長的對象。被移到老生代中。稱爲晉升
對象晉升的條件:
經過檢查它的內存地址來判斷。若是經歷過了,從From複製到老生代
缺點:1. 存活對象較多時,複製存活對象的效率低。 2. 浪費通常空間
Mark-Sweep(標記清除)
Mark-Compat(標記整理)
對象在標記爲死亡後,整理過程當中,將活着的對象往一端移動。完成後,直接清理掉邊界外的內存
在空間不足以對重新生代晉升過來的對象進行分配時才使用
Incremental Marking
延遲清理和增量清理
並行標記和並行清理
小結:
node --trace_gc -e "..."
能夠了解垃圾回收的運行情況,找出哪些階段比較費時
node --prof xx.js
會在該目錄下生成v8.log文件,獲得性能分析數據
node --prof-process isolate-0x103001200-v8.log
因爲日誌文件不具有可讀性,故這樣能夠統計日誌信息
做用域
var foo = function () {
var local = {};
}
foo();
複製代碼
內存回收過程:只被局部變量引用的對象存活週期較短,會被分配在新生代的From空間,在做用域釋放後,局部變量local失效,引用的對象會在下次垃圾回收時被釋放
標識符查找:
js在執行時回去找該變量在哪裏定義,在當前做用域沒有查到,將會向上級的做用域裏查找,直到查到爲止
做用域鏈:
根據在內部函數能夠訪問外部函數變量的這種機制,用鏈式查找決定哪些數據能被內部函數訪問。
執行環境:
js爲每個執行環境關聯了一個變量對象。環境中定義的全部變量和函數都保存在這個對象中。
變量的主動釋放:
全局變量,直到進程退出才釋放。引用的對象常駐內存(老生代)。
能夠用delete操做和從新賦值(null或者undefined)
實現外部做用域訪問內部做用域中變量的方法
做用域中產生的內存佔用不會獲得釋放。除非再也不有引用,纔會逐步釋放
進程的內存一部分是rss,其他部分在交換區或者文件系統中
$ node
> process.memoryUsage()
{
rss: // 常駐內存
heapTotal: // 總申請的內存量
heapUsed: // 使用中的內存量
}
> os.totalmem() // 總內存
> os.freemem() // 閒置內存
複製代碼
Buffer對象並不是經過V8分配,沒有堆內存的大小閒置
小結:受V8的垃圾回收限制的主要是V8堆內存
哪怕一字節的內存泄漏也會形成堆積,垃圾回收過程當中將會耗費更多時間進行對象描述,應用響應緩慢,直到進程內存溢出,應用奔潰
緣由:
緩存中存儲的鍵越多,長期存活對象也就越多,常駐在老生代
普通對象無過時策略
var cached = {};
function get (key) {
if (cached[key]) {
return cached[key]
} else {
}
}
function set (key, value) {
cached[key] = value;
}
複製代碼
解決:
緩存限制策略
超過數量,先進先出的方式進行淘汰
設計模塊時,應添加清空隊列的相應接口
緩存的解決方案
進程間沒法共享內存
隊列消費速度低於生產速度,將會造成堆積。而js相關做用域也不會獲得釋放,內存佔用不會回落,從而出現內存泄漏
解決方案:
node中大多數模塊都有stream應用。因爲V8內存限制,採用流實現對大文件的操做
若是不須要進行字符串層面的操做,則不須要V8來處理,嘗試進行純粹的Buffer操做
const fs = require('fs');
var reader = fs.createReadStream('./text.md', {highWaterMark: 11});
var data = ''
reader.on('data', function (chunk) {
data += chunk
})
reader.on('end', function () {
console.log(data)
})
複製代碼
var reader = fs.createReadStream('./text.md', {highWaterMark: 11});
render.setEncoding('utf8')
複製代碼
setEncoding的時候,可讀流對象在內部設置了一個decoder對象。每次data事件都經過該decoder對象進行Buffer到字符串的解碼。
decoder的對象會暫時存儲,buffer讀取的剩餘字節
const fs = require('fs');
var reader = fs.createReadStream('./text.md', {highWaterMark: 11});
var chunks = [];
var size = 0;
reader.on('data', function (chunk) {
chunks.push(chunk);
size += chunk.length;
})
reader.on('end', function () {
var buf = Buffer.concat(chunks, size);
console.log(buf.toString())
})
複製代碼
在web領域,大多數的編程語言須要專門的web服務器做爲容器,如ASP、ASP.NET須要IIS做爲服務器,PHP須要打在Apache或Nginx環境等,JSP須要Tomcat服務器等。但對於Node而言,只須要幾行代碼便可構建服務器,無需額外的容器。
TCP
建立TCP服務器端
const net = require('net');
let server = net.createServer();
server.on('connection', function (socket) {
console.log('connection')
})
server.listen(8000)
複製代碼
UDP不是面向鏈接的。
一個套接字能夠與多個UDP服務通訊,它雖然提供面向事務的簡單不可靠信息傳輸服務,在網絡差的狀況下存在丟包嚴重的問題
優勢:無鏈接,資源消耗低,處理快速且靈活
應用:音頻,視頻,dns服務
const dgram = require('dgram');
const server = dgram.createSocket('udp4')
server.on('error', (err) => {
console.log(`服務器異常:\n${err.stack}`);
server.close();
});
server.on('message', (msg, rinfo) => {
console.log(`服務器收到:${msg} 來自 ${rinfo.address}:${rinfo.port}`);
});
server.on('listening', () => {
const address = server.address();
console.log(`服務器監聽 ${address.address}:${address.port}`);
});
server.bind(1000)
複製代碼
特色:
res.writeHead(()
res.write() // 發送數據
res.end()
複製代碼
http服務端事件
http客戶端
示例:
var req = http.request(options, function (res) {
res.setEncoding('utf8');
res.on('data', function (chunk) {
console.log(chunk)
})
})
複製代碼
在keepalive的狀況下,一個底層會話鏈接能夠屢次用於請求。爲了重用tcp鏈接,能夠用http.globalAgent客戶端代理對象
默認狀況下,經過ClientRequest對象對同一個服務器發起的http請求最多能夠建立五個鏈接
如需改變,可在options中傳遞agent選項
var agent = new http.Agent({
maxSockets: 10
})
var options = {
hostname: '127.0.0.1',
port: 1334,
path: '/',
method: 'GET',
agent: agent
}
複製代碼
特色:
好處:
握手完成後,再也不進行http交互,客戶端的onopen將會觸發執行
當客戶端調用send發送數據時,服務端觸發onmessage事件;當服務端調用send發送數據時,客戶端觸發message事件。
當send發送一條數據時,協議可能將這個數據封裝爲一幀或多幀數據,而後逐幀發送
交換公鑰過程當中,可能遇到中間人攻擊,因此應引入數字證書來認證。
建立私鑰:
openssl genrsa -out ryans-key.pem 2048
生成csr
openssl req -new -sha256 -key ryans-key.pem -out ryans-csr.pem
生成自簽名證書
openssl x509 -req -in ryans-csr.pem -signkey ryans-key.pem -out ryans-cert.pem
驗證:
const https = require('https');
const fs = require('fs');
const options = {
key: fs.readFileSync('./ryans-key.pem'),
cert: fs.readFileSync('./ryans-cert.pem')
}
https.createServer(options, function (req, res) {
res.writeHead(200);
res.end('hello world')
}).listen(2000)
複製代碼
-k忽略掉證書的驗證
curl -k https://localhost:2000
HTTP_Parser在解析請求報文的時候,將報文頭抽取出來,設置爲req.method。有諸如:GET, POST, HEAD, PUT, DELETE, OPTIONS, TRACE, CONNECT
路徑部分存在於報文的第一行的第二部分,如:
GET /path?foo=bar HTTP/1.1
HTTP_Parser將其解析爲req.url, 通常而言,完整的url地址以下
http://user:pass@host.com:8080/p/a/t/h?query=string#hash
這裏hash部分會被丟棄,不會存在於報文的任何地方, 下列的url對象不是報文中的,故有hash
解析出來的url對象
Url {
protocol: 'https:',
slashes: true,
auth: 'user:pass',
host: 'sub.host.com:8080',
port: '8080',
hostname: 'sub.host.com',
hash: '#hash',
search: '?query=string',
query: 'query=string',
pathname: '/p/a/t/h',
path: '/p/a/t/h?query=string',
href: 'https://user:pass@sub.host.com:8080/p/a/t/h?query=string#hash' }
複製代碼
查詢字符串,若是鍵出現屢次,那麼它的值會是一個數組
foo=bar&foo=baz
複製代碼
var query = url.parse(req.url, true).query;
{
foo: ['bar', 'baz']
}
複製代碼
cookie處理:
Set-Cookie: name=vale; Path=/;Expires=Sun, 23-Apr-23 09:01:35 GMT; Domain=.domain.com;
path表示cookie影響路徑,表示服務器目錄下的子html都能訪問
expires和max-age表示過時時間,一個是絕對時間,一個是相對時間
httpOnly告知瀏覽器不能經過document.cookie獲取
secure爲true表示在https纔有效
domain:子域名訪問父域名
**性能影響:**大多數cookie並不須要每次都用上,由於這會形成帶寬的部分浪費
解決:
session的數據只保留在服務器端,客戶端沒法修改。
應用:
將口令放在cookie中,口令一旦被褚昂愛,就丟失映射關係。一般session的有效期一般短,過時就將數據刪除
一旦服務器檢查到用戶請求cookie中沒有攜帶session_id,它會爲之生成一個值,這個值是惟一且不重複的值,並設定超時時間。若是過時就從新生成,若是沒有過時,就更新超時時間
var sessions = {};
var key = 'session_id';
var EXPIRES = 20*60*1000;
var generate = function () {
var session = {};
session.id = (new Date().getTime()) + Math.random();
session.cookie = {
expire: (new Date()).getTime() + EXPIRES
}
sessions[session.id] = session
}
function (req, res) {
var id = req.cookies[key];
if (!id) {
req.session = generate();
} else {
var session = sessions[id];
if (session) {
if (session.cookie.expire > new Date().getTime()) {
session.cookie.expire = new Date().getTime() + EXPIRES;
req.session = session;
} else {
delete sessions[id];
req.session = generate();
}
} else {
req.session = generate();
}
}
}
複製代碼
原理:檢查查詢字符串,若是沒有值,會生成新的帶值的url
var getURL = function (_url, key, value) {
var obj = url.parse(_url, true);
obj.query[key] = value;
return url.format(obj);
}
function (req, res) {
var redirect = function (url) {
res.setHeader('Location', url);
res.writeHead(302);
res.end();
}
var id = req.query[key];
if (!id) {
var session = generate();
redirect(getURL(req.url), key, session.id);
} else {
var session = sessions[id];
if (session) {
if (session.cookie.expire > new Date().getTime()) {
session.cookie.expire = new Date().getTime() + EXPIRES;
req.session = session;
handle(req, res);
} else {
delete sessions[id];
var session = generate();
redirect(getURL(req.url), key, session.id)
}
} else {
var session = generate();
redirect(getURL(req.url), key, session.id)
}
}
}
複製代碼
因爲session存儲在sessions對象中,故在內存中,若數據量加大,會引發垃圾回收的頻繁掃描,引發性能問題。
爲了利用多核cpu而啓動多個進程,用戶請求的鏈接將可能隨意分配到各個進程中,node的進程與進程之間不能直接共享內存,用戶的session可能會引發錯亂
將session集中化,將可能分散在多個進程裏的數據,統一轉移到集中數據存儲中。目前經常使用工具是redis,memcached。node無需在內部維護數據對象。
問題: 會引發網絡訪問
設置last-modified
var handle = function (req, res) {
fs.stat(filename, function (err, stat) {
var lastModified = stat.mtime.toUTCString();
if (lastModified === req.headers['if-modified-since']) {
res.writeHead(304, 'Not Modified');
res.end()
} else {
fs.readFile(filename, function (err, file) {
var lastModified = stat.mtime.toUTCString();
res.setHeader('Last-modified', lastModified);
res.writeHead(200, 'ok');
res.end(file);
})
}
})
}
複製代碼
缺陷:
設置etag
var getHash = function (str) {
var shasum = crypto.createHash('sha1');
return shasum.update(str).digest('base64');
}
var handle = function (req, res) {
fs.readFile(filename, function (err, file) {
var hash = getHash(file);
var noneMatch = req['if-none-match'];
if (hash === noneMath) {
res.writeHead(304, "Not Modified");
res.end()
} else {
res.setHeader("ETag", hash);
res.writeHead(200, "ok");
res.end(file);
}
})
}
複製代碼
強制緩存
var handle = function (req, res) {
fs.readFile(filename, function (err, file) {
res.setHeader("Cache-Control", "max-age=" + 10*365*24*60*60*1000);
res.writeHead(200, "ok");
res.end(file);
})
}
複製代碼
用expires可能致使瀏覽器端與服務器端時間不一樣步帶來的不一致性問題
清除緩存
瀏覽器是根據url進行緩存,那麼一旦內容有所更新時,咱們就讓瀏覽器發起新的url請求,使得新內容可以被客戶端更新。
var hasBody = function (req) {
return 'transfer-encoding' in req.headers || 'content-length' in req.headers;
}
function (req, res) {
if (hasBody(req)) {
var buffers = [];
req.on('data', functino (chunk) {
buffers.push(chunk);
})
req.on('end', function () {
req.rawBody = Buffer.concat(buffers).toString(); // 拼接buffer
handle(req, res);
})
} else {
handle(req, res);
}
}
複製代碼
處理json格式
// application/json;charset=utf-8;
var mime = function (req) {
var str = req.headers['content-type'] || '';
return str.split(';')[0]
}
var handle = function (req, res) {
if (mime(req) === 'application/json') {
try {
req.body = JSON.parse(req.rawBody);
} catch(e) {
res.writeHead(400);
res.end("Invalid JSON");
return
}
}
todo(req, res)
}
複製代碼
處理xml文件
var xml2js = require('xml2.js');
var handle = function (req, res) {
if (mime(req) === 'appliction/xml') {
xml2js.parseString(req.rawBody, function (err, xml) {
if (err) {
res.writeHead(400);
res.end('Invalid XML');
return;
}
req.body = xml;
todo(req, res);
})
}
}
複製代碼
圖片上傳
var formidable = require('formidable'),
http = require('http'),
util = require('util'),
fs = require('fs');
http.createServer(function(req, res) {
if (req.url == '/upload' && req.method.toLowerCase() == 'post') {
// parse a file upload
var form = new formidable.IncomingForm();
form.parse(req, function(err, fields, files) {
fs.renameSync(files.upload.path,"./tmp/text.jpeg"); // 另存圖片
res.writeHead(200, {'content-type': 'text/plain'});
res.write('received upload:\n\n');
res.end(util.inspect({fields: fields, files: files}));
});
return;
}
if (req.url == '/')
// show a file upload form
res.writeHead(200, {'content-type': 'text/html'});
res.end(
'<form action="/upload" enctype="multipart/form-data" method="post">'+
'<input type="text" name="title"><br>'+
'<input type="file" name="upload" multiple="multiple"><br>'+
'<input type="submit" value="Upload">'+
'</form>'
);
}).listen(8080);
複製代碼
在解析表單,json和xml部分,咱們採起的策略是先保存用戶提交的全部數據,而後再解析處理,最後才傳遞給業務邏輯。
弊端:數據量大,佔內存
解決方案:
限制大小方案代碼:
var bytes = 1024;
function (req, res) {
var received = 0;
var len = req.headers['content-length'] ? parseInt(req.headers['content-length'], 10) : null;
if (len && len > bytes) {
res.writeHead(413);
res.end();
return;
}
req.on('data', function (chunk) {
received += chunk.length;
if (received > bytes) {
req.destroy();
}
})
handle(req, res);
}
複製代碼
var generateRandom = function (len) {
return crypto.randomBytes(Math.ceil(len*3/4)).toString('base64').slice(0, len);
}
var token = req.session._csrf || (req.session._crsf = generateRandom(24));
// 作頁面渲染的時候服務器端渲染這個_csrf
複製代碼
function (req, res) {
var token = req.session._csrf || (req.session._csrf = generateRandom(24));
var _csrf = req.body._csrf;
if (token !== _csrf) {
res.writeHead(413);
res.end("禁止訪問");
} else {
handle(req, res);
}
}
複製代碼
mvc工做模式
手工映射
自由映射,從入口程序中判斷url,而後執行對應的邏輯。
匹配的時候,可以正則匹配
天然映射
/controller/action/param1/param2/param3
按約定去找controllers目錄下的user文件,將其require出來,調用這個文件模塊的setting方法,其他的參數直接傳遞到這個方法中
RESTful(representational state transfer)
須要區分請求方法
一個地址表明了一個資源,對這個資源的操做,主要體如今http請求方法上,不是體如今url上
設計:
POST,GET,PUT,DELETE
POST /user/add?username=jack
GET /user/remove?username=jack
複製代碼
中間件
含義:指底層封裝細節,爲上層提供更方便服務的意義,爲咱們封裝全部http請求細節處理的中間件
中間件性能
緩存須要重複計算的結果,避免沒必要要的計算。
響應頭中的content-*字段十分重要。
示例
Content-Encoding:gzip
Content-Length:21170
Content-Type:text/javascript;charfset=utf-8
複製代碼
客戶端在接收到後,經過gzip來解碼報文體重的內容,用長度校驗報文體內容是否正確,而後在以字符集utf-8將解碼後的腳本插入到文檔節點中
application/json, application/xml, application/pdf
背景:不管響應的內容是什麼MIME,只須要彈出並下載它
Content-Disposition
判斷是應該將報文數據當作及時瀏覽的內容,仍是可下載的附件。
inline // 內容只需查看
attachment // 數據能夠存爲附件
複製代碼
還能指定保存時使用的文件名
Content-Disposition:attachment;filename="filename.txt"
響應附件api
res.sendfile = (filepath) => {
fs.stat(filepath, (err, stat) => {
let stream = fs.createReadStream(filepath);
res.setHeader("Content-Type", mime.lookup(filepath));
res.setHeader("Content-length", stat.size);
res.setHeader("Content-Disposition", 'attachment;filename="'+ path.basename(filepath) +'"')
res.writeHead(200);
stream.pipe(res);
})
}
複製代碼
res.json = function (json) {
res.setHeader("Content-Type", "application/json");
res.writeHead(200);
res.end(JSON.stringify(json))
}
複製代碼
res.redirect = function (url) {
res.setHeader('Location', url);
res.writeHead(200);
res.end('redirect to' + url)
}
複製代碼
res.render = function (view, data) {
res.setHeader("Content-Type", "text/html");
res.writeHead(200);
var html = render(view, data);
res.end(html)
}
複製代碼
模板要素:
function render (str, data) {
var tpl = str.replace(/<%=([\s\S]+?)%>/g, function (match, code) {
return "' + obj." + code + "+ '";
})
tpl = "var tpl = '" + tpl + "'\nreturn tpl;";
var compiled = new Function('obj', tpl);
return compiled(data);
}
複製代碼
集成文件系統
fs.readFile('file/path', 'utf8', function (err, txt) {
if(err) {
res.writeHead(500, {'Content-Type': 'text/html'});
res.end('模板文件錯誤');
return;
}
res.writeHead(200, {"Content-Type": "text/html"});
var html = render(compile(text), data);
res.end(html);
})
複製代碼
這樣作每次都須要讀取模板文件,所以可設置cache={}
模板性能
一個進程只能利用一個核,如何充分利用多核cpu服務器
單線程上拋出的異常沒有被捕獲,如何保證進程的健壯性和穩定性
一次只爲一個請求服務
經過進程的賦值同時服務更多的請求和用戶。進程賦值會致使內存浪費
一個線程服務一個請求,線程相對於進程的開銷要小,線程之間能夠共享數據,內存浪費問題獲得解決
可是線程上線文切換會產生時間消耗
解決高併發問題
單線程避免沒必要要的內存開銷和上下文切換
php爲每一個請求都簡歷獨立的上下文
master.js實現進程的複製
let fork = require('child_process').fork;
let cpus = require('os').cpus();
for (let i = 0; i < cpus.length; i++) {
fork('./worker.js');
}
複製代碼
worker.js
const http = require('http');
http.createServer((req, res) => {
res.writeHead(200, {"Content-Type": "text/plain"});
res.end('hello')
}).listen(parseInt(Math.random()*10000), '127.0.0.1')
複製代碼
ps aux | grep worker.js
查看進程的數量
lejunjie 3306 0.0 0.0 4267752 868 s001 S+ 11:18上午 0:00.00 grep worker.js
lejunjie 3171 0.0 0.3 4893888 21656 s000 S+ 11:18上午 0:00.13 /Users/lejunjie/.nvm/versions/node/v8.11.1/bin/node ./worker.js
lejunjie 3170 0.0 0.3 4893888 21632 s000 S+ 11:18上午 0:00.13 /Users/lejunjie/.nvm/versions/node/v8.11.1/bin/node ./worker.js
lejunjie 3169 0.0 0.3 4893888 21708 s000 S+ 11:18上午 0:00.13 /Users/lejunjie/.nvm/versions/node/v8.11.1/bin/node ./worker.js
lejunjie 3168 0.0 0.3 4893888 21664 s000 S+ 11:18上午 0:00.13 /Users/lejunjie/.nvm/versions/node/v8.11.1/bin/node ./worker.js
複製代碼
經過fork複製的進程都是一個獨立的進程,啓動多個進程只是爲了充分將cpu資源利用起來,而不是爲了解決併發問題
cp.spawn('node', ['worker.js']);
sp.exec('node worker.js', () => {})
啓動一個子進程來執行可執行文件
建立node子進程只須要指定要執行的javascript文件模塊
主線程與工做線程之間經過onmessage和postMessage進行通訊,子進程對象則由send方法實現主進程向子進程發送數據
parent.js
var cp = require('child_process');
var n = cp.fork('./child.js');
n.on('message', function (data) {
console.log('parent data: ' + data.name);
})
n.send({name: 'parent'})
複製代碼
child.js
process.on('message', function (data) {
console.log('child: ' + data.name);
})
process.send({name: 'child'})
複製代碼
結果
child: parent
parent data: child
複製代碼
node中實現ipc通道的是管道技術,具體由libuv提供
父進程在實際建立子進程以前,會建立ipc通道並監聽它,而後才真正建立子進程,並經過環境變量告訴子進程這個ipc通道的文件描述符。
雙向通訊,在系統內核中完成通訊,不用通過實際的網絡層
多個進程監聽經過端口會拋出EADDRINUSE異常,這是端口被佔用的狀況。能夠經過代理,在代理進程上作適當的負載均衡,使得每一個子進程能夠較爲均衡地執行任務。可是代理進程鏈接到工做進程的過程須要用掉兩個文件描述符
句柄是一種能夠用來標識資源的應用,他的內部包含了只想對象的文件描述符。好比句柄能夠用來表示一個服務器端socket對象,一個客戶端socket對象,一個udp套接字,一個管道等。
發送句柄使得主進程接收到socket請求後,將這個socket直接發給工做進程,而不是從新與工做進程之間創建新的socket鏈接來轉發數據。解決文件描述符的浪費問題
parent.js
const cp = require('child_process');
var child1 = cp.fork('./child.js');
var child2 = cp.fork('./child.js');
var server = require('net').createServer();
server.on('connection', (socket) => {
socket.end('handled by parent');
})
server.listen(1338, () => {
child1.send('server', server);
child2.send('server', server);
})
複製代碼
child.js
process.on('message', (m, server) => {
if (m === 'server') {
server.on('connection', function (socket) {
socket.end('handled by child , pid is' + process.pid);
})
}
})
複製代碼
讓請求都由子進程處理
parent
const cp = require('child_process');
var child1 = cp.fork('./child.js');
var child2 = cp.fork('./child.js');
var server = require('net').createServer();
server.on('connection', (socket) => {
socket.end('handled by parent');
})
server.listen(1338, () => {
child1.send('server', server);
child2.send('server', server);
server.close();
})
複製代碼
child
var http = require('http');
var server = http.createServer((req, res) => {
res.writeHead(200, {"Content-Type": "text/plain"});
res.end("handled by child, pid is" + process.pid);
})
process.on('message', (m, tcp) => {
if (m === 'server') {
tcp.on('connection', function (socket) {
server.emit('connection', socket);
})
}
})
複製代碼
多個子進程能夠同時監聽相同端口,再沒有EADDRINUSE異常發生
總結:
獨立啓動的進程中,tcp服務器端socket套接字的文件描述符並不相同,致使監聽到相同的端口時會拋出異常
多個應用監聽相同端口時,文件描述符同一時間只能被某一個進程所用,因此是搶佔式的
進程退出時,讓全部工做進程退出。子進程退出時從新create
const cp = require('child_process');
var server = require('net').createServer();
var cpus = require('os').cpus();
var workers = {};
function create () {
var worker = cp.fork('./child.js');
worker.on('exit', function () {
console.log('worker: ' + worker.pid + 'exited');
})
worker.send('server', server);
workers[worker.pid] = worker;
console.log('create worker pid: ' + worker.pid);
}
for (var i = 0; i < cpus.length; i++) {
create();
}
process.on('exit', function () {
for (var pid in workers) {
workers[pid].kill();
}
})
複製代碼
在極端狀況下,全部工做進程都中止接受新的鏈接,全出在等待退出的狀態。但在等進程徹底退出才重啓的過程當中,全部新來的請求可能存在沒有工做進程爲新用戶服務的情景,這會丟掉大部分請求
所以可在子進程中監聽uncaughtException,而後發送自殺信號
process.on('uncaughtException', function (err) {
process.send({act: 'suicide'});
worker.close(function () {
process.exit(1);
})
})
複製代碼
node默認提供的機制是採用操做系統的搶佔式策略。
新的策略是輪叫調度。工做方式是由主進程接受鏈接,將其一次分發給工做進程。
在多個進程之間共享數據
實現同步:子進程向第三方進行定時輪訓
主動通知子進程,輪訓。
要建立單機node集羣,因爲有許多細節須要處理,因而引入cluster,解決多核cpu的利用率問題
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
console.log(`主進程 ${process.pid} 正在運行`);
// 衍生工做進程。
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('listening', () => {
console.log('listening')
})
cluster.on('exit', (worker, code, signal) => {
console.log(`工做進程 ${worker.process.pid} 已退出`);
});
} else {
// 工做進程能夠共享任何 TCP 鏈接。
// 在本例子中,共享的是一個 HTTP 服務器。
http.createServer((req, res) => {
res.writeHead(200);
res.end('你好世界\n');
}).listen(8000);
console.log(`工做進程 ${process.pid} 已啓動`);
}
process.on('exit', () => {
console.log('exit')
})
複製代碼
原理:cluster模塊就是child_process和net模塊的組合應用。在fork子進程時,將socket的文件描述符發送給工做進程。經過so_reuseaddr端口重用,從而實現多個子進程共享端口。
項目的組織能力
代碼流程--》stage普通測試環境--》pre-release預發佈環境--》product實際生產環境
node file.js以啓動應用,會站住一個命令行窗口,窗口退出進程也退出
nohup node app.js &
不掛斷進程的方式
bash腳本, 解決進程id不容易查找的問題。重啓,中斷,啓動
動靜分離:
讓node只處理動態請求,將靜態文件引導到專業的靜態文件服務器。用nginx或者專業的cdn來處理
cdn緩存,將文件放在離用戶儘量近的服務器
對靜態請求使用不一樣的域名或者多個域名還能消除掉沒必要要的cookie傳輸和瀏覽器對下載線程數的限制
啓用緩存
提高服務速度,避免沒必要要的計算
多進程架構
讀寫分離
對數據庫進行主從設計,這樣讀取數據操做再也不受到寫入的影響,下降了性能的影響。
寫到磁盤上
數據庫寫入要經歷鎖表,日誌等操做,若是大量訪問會排隊,進而內存泄露。
經過監控異常日誌文件的變更,將新增的異常按異常類型和數量反應出來。
監控訪問日誌,體現業務qps值,pv/uv,預知訪問高峯
在nginx類的反向代理上監控
經過應用自行產生的訪問日誌來監控
檢查操做系統中運行的應用進程數,對於採用多進程架構的web應用,就須要檢查工做進程的數量,若是低於預估值,就應當發出報警
監控磁盤的用量,設置警惕值
健康的內存是有升有降的
cpu分爲內核態,用戶態,iowait等。
用戶態佔用高: 服務器上應用大量cpu開銷
內核態佔用高:服務器花費大量時間進程調度或者系統調用。
描述操做系統當前的繁忙程度
指標太高,在node中可能體如今用子進程模塊反覆啓動新的進程
反應磁盤讀寫狀況
流入流量和流出流量
應用狀態監控
dns監控