深入淺出Nodejs
模塊機(jī)制
Commonjs規(guī)范
-
node的模塊實(shí)現(xiàn)
步驟:
路徑分析
文件定位
-
編譯執(zhí)行
核心模塊在node進(jìn)程啟動(dòng)時(shí),會(huì)直接加載進(jìn)內(nèi)存中,所以文件定位和編譯執(zhí)行兩步可以省略,加載速度快
文件模塊則是運(yùn)行時(shí)動(dòng)態(tài)加載,需要經(jīng)過上面3個(gè)步驟
-
優(yōu)先從緩存加載
會(huì)對(duì)引入過的模塊進(jìn)行緩存
-
路徑分析和文件定位
核心模塊/ 以./ 等開頭的相對(duì)路徑文件/以/開頭的絕對(duì)路徑
模塊的路徑轉(zhuǎn)為真實(shí)路徑,作為索引
module.paths,依次向上查找node_modules目錄
-
模塊編譯
// 針對(duì)js模塊進(jìn)行一層包裝 (function(exports, require, module, __filename, __dirname) { var math = require('math') exports.area = function(radius) { return radius } })
exports 和 module.exports的區(qū)別
exports是module.exports的引用
exports === module.exports
exports返回的是模塊函數(shù),使用點(diǎn)運(yùn)算符
module.exports返回的是模塊對(duì)象本身,返回的是一個(gè)類
exports.age = 18 moudle.exports = {name: 18} require('.a.js') // 引入的是 module.exports
-
-
npm包規(guī)范
-
包結(jié)構(gòu)
Package.json / bin / lib / doc / test
常用功能
-
局域npm
-
AMD / CMD
是解決common js 在客戶端中異步加載的場(chǎng)景
-
amd 依賴前置,聲明時(shí)指定依賴
definde(['dep1, dep2'], function(dep1, dep2) { })
-
cmd, 依賴前置,支持動(dòng)態(tài)引入
define(function(require, exports, module){ ... require('./xx.js') })
-
-
umd
兼容多種模塊規(guī)范
;(function(root, factory){ if(typeof exports === 'object' && typeof module === 'object') module.exports = factory(require("vue")); else if(typeof define === 'function' && define.amd) //// AMD環(huán)境 CMD環(huán)境 define("components", ["vue"], factory); else if(typeof exports === 'object') //// 定義為 通Node模塊 exports["components"] = factory(require("vue")); else // 將模塊的執(zhí)行結(jié) 在window 量中 在 器中this window對(duì)象 root["components"] = factory(root["Vue"]); })(window, function(){...})
異步I/O
-
單線程
無法利用多核cpu的優(yōu)勢(shì)和阻塞帶來的延遲
-
多進(jìn)程
進(jìn)程創(chuàng)建和上下文切換的開銷,狀態(tài)同步和鎖的問題
node的方案:
? 單線程 + 非阻塞異步 I/O
? child_process / cluster模塊提供多進(jìn)程,可以利用多核cpu
非阻塞I/O,為了獲取i/o響應(yīng)的數(shù)據(jù),需要重復(fù)調(diào)用i/o是否完成 — 輪詢
-
read
重復(fù)檢查i/o狀態(tài)是否完成數(shù)據(jù)的讀取
-
select
在read基礎(chǔ)上的改進(jìn)
通過對(duì)文件描述符上的事件狀態(tài)來判斷
-
poll
效率最高的i/o事件通知機(jī)制
事件訂閱,事件通知,執(zhí)行回調(diào)的方式
理想的非阻塞異步I/O
事件循環(huán) event loop
觀察者
請(qǐng)求對(duì)象
node event loop
process.nextTick 會(huì)優(yōu)于其他microtask執(zhí)行
-
timer
執(zhí)行定時(shí)器
i/o
Idle /prepare
-
poll
回到timer階段,執(zhí)行到時(shí)的回調(diào)
-
執(zhí)行poll隊(duì)列中的事件
poll 中沒有定時(shí)器的情況下
- poll隊(duì)列不為空, 便利回調(diào)隊(duì)列并同步執(zhí)行
- poll隊(duì)列為空,
- 有setImmediate需要執(zhí)行, 進(jìn)入check階段執(zhí)行setImmediate
- 沒有setImmediate,會(huì)等待回調(diào)被加入到隊(duì)列中并立即執(zhí)行
fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout') }, 0) setImmediate(() => { console.log('setImmediate') }) }) // 執(zhí)行順序 setImmediate -》 timeout // readFile的回調(diào)在poll階段執(zhí)行 // 發(fā)現(xiàn)setImmediate,去執(zhí)行 setImmediate // 再到timer階段執(zhí)行回調(diào)
check
Close
異步編程
函數(shù)式編程
高階函數(shù)
-
偏函數(shù),將多參數(shù)的函數(shù),返回一個(gè)預(yù)設(shè)部分參數(shù)的函數(shù)。可接受剩下參賽的函數(shù)
柯里化,是偏函數(shù)的一種,將多參數(shù)的函數(shù),變?yōu)榻邮軉我粎?shù)的函數(shù)
function curry(fn) { var content = this var args = [...arguments].slice(1) return function(){ var finalArgs = args.concat([...arguments]) fn.apply(content, finalArgs) } }
多線程:缺點(diǎn),上下文切換開銷,死鎖,同步問題
-
node的優(yōu)勢(shì)
基于事件驅(qū)動(dòng)的非阻塞I/O模型
借用v8高性能引擎
-
異步編程難點(diǎn)
1.**異常處理 **
try/catch/final不適用異步編程錯(cuò)誤捕獲
-
第一參數(shù)異常
node在處理異常形成的約定,回調(diào)函數(shù)的一個(gè)參數(shù)為err
fs.readFile('dd.js', function(err, data) { // err就是捕獲的異常信息 })
2.函數(shù)嵌套過深
3.阻塞代碼
? 需要阻塞代碼,不能達(dá)到真正的線程沉睡,cpu資源會(huì)一直為其服務(wù)
4.多線程編程
? 發(fā)揮多核cpu的優(yōu)勢(shì),借助web worker模式,node提出child_process,cluster 模塊,用于多線程編程
5.異步轉(zhuǎn)同步
-
異步編程的解決方案
-
事件發(fā)布/訂閱模式
- node中event模塊就是發(fā)布/訂閱模式的實(shí)現(xiàn)案例
- 解耦業(yè)務(wù)邏輯,添加偵聽器,當(dāng)emit事件時(shí),添加在這個(gè)事件的偵聽器進(jìn)行變化
- 事件發(fā)布/訂閱模式的特點(diǎn)為執(zhí)行流程需要被預(yù)先設(shè)定
-
promise/deferred模式
-
執(zhí)行流程不需要被預(yù)先設(shè)定
$.get('/api').then(res=>{...})
-
promise/deferred模式發(fā)布在commonjs規(guī)范中,已經(jīng)抽象出promise/A,promise/B等模型規(guī)范
Promise/A規(guī)范
定義里3中狀態(tài)(pendding, resolve,rejected)
promise對(duì)象具備then方法即可
狀態(tài)不可逆
- Promise 用于外部,通過then方法收集事件,
- Deferred 延遲對(duì)象,用于內(nèi)部,維護(hù)狀態(tài)修改,觸發(fā)相應(yīng)狀態(tài)事件
then鏈?zhǔn)秸{(diào)用
-
-
-
流程控制庫
中間件middleware
尾觸發(fā)與next
-
異步并發(fā)控制
避免高并發(fā)
- 通過隊(duì)列來控制并發(fā)量,設(shè)定閥值,超出部分放入隊(duì)列中,等調(diào)用結(jié)束后,從隊(duì)列中取出并執(zhí)行
- 超時(shí)控制,設(shè)置時(shí)間閥值
內(nèi)存控制
V8的垃圾回收機(jī)制和內(nèi)存限制
-
v8的內(nèi)存限制
64位約1.4GB,32位約0.7GB,導(dǎo)致node無法直接操作大內(nèi)存對(duì)象
-
v8的對(duì)象分配
process.memoryUsage() / 查看v8堆內(nèi)存信息
調(diào)整內(nèi)存限制的大小 --max-old-space-size --max-new-space-size=1024
-
v8垃圾回收機(jī)制
-
v8主要的垃圾回收算法
分代式垃圾回收機(jī)制
按對(duì)象的存活時(shí)間分為老生代和新生代
-
Scavenge算法(新生代)
新生代中的對(duì)象主要通過scavenge算法進(jìn)行垃圾回收
采用了cheney算法,一種采用復(fù)制的方式實(shí)現(xiàn)的垃圾回收算法,將內(nèi)存一分位二,from和to空間,把from空間中存活的對(duì)象復(fù)制到to空間,from中非存活的對(duì)象被回收,讓后將to空間中的對(duì)象復(fù)制到from空間,to空間清空
缺點(diǎn)是,將內(nèi)存一分為二,犧牲了空間換時(shí)間,所以非常適合新生代中
回收過程中,新生代的對(duì)象會(huì)晉升為老生代中(滿足已經(jīng)被scavenge過等等條件)
-
標(biāo)記清除 && 標(biāo)記整理(老生代)
遍歷對(duì)象,標(biāo)記活著的對(duì)象,最后清除未被標(biāo)記的對(duì)象
垃圾回收后,有可能造成內(nèi)存空間不連續(xù)的狀態(tài)(空間碎片),Mark-Compact(標(biāo)記整理)來解決
Mark-Compact:回收過程中,將活著的對(duì)象向一端移動(dòng),形成連續(xù)新存儲(chǔ)
-
incremental marking(增量標(biāo)記)
垃圾回收會(huì)造成js運(yùn)行停頓
增量標(biāo)記將垃圾回收拆分為許多小的‘步進(jìn)’,每完成一段,將執(zhí)行棧交換回js運(yùn)行應(yīng)用,然后再執(zhí)行垃圾回收。。。,直到垃圾回收完成
-
-
查看垃圾回收日志
node --trace_gc xxxxxxx
高效實(shí)用內(nèi)存
-
作用域(scope)
全局作用域,函數(shù)作用域,with作用域
作用域鏈查找,一直向上查找,直到全局作用域
全局作用域直到進(jìn)程退出才能釋放變量
-
閉包(closure)
實(shí)現(xiàn)外部作用域可以訪問內(nèi)部作用域中變量的函數(shù)或方法叫做閉包
作用域一直在內(nèi)存中占用,不會(huì)釋放
buffer對(duì)象不經(jīng)過v8的內(nèi)存分配機(jī)制,不會(huì)有堆內(nèi)存的大小限制,利用堆外內(nèi)存可以突破內(nèi)存限制的問題
內(nèi)存泄漏
-
緩存
緩存的對(duì)象會(huì)長(zhǎng)駐內(nèi)存中,由于js對(duì)象沒有過期策略,會(huì)導(dǎo)致緩存長(zhǎng)期存在
解決方法:限度緩存對(duì)象的大小,加上完善的過期策略(設(shè)置過期時(shí)間或設(shè)定對(duì)象大小的閥值)
模塊也有緩存機(jī)制
node的解決方法:
- 將緩存轉(zhuǎn)移到外部,減少常駐內(nèi)存的對(duì)象的數(shù)量,讓垃圾回收更高效
- 進(jìn)程之間可以共享緩存
- redis
-
隊(duì)列消費(fèi)不及時(shí)
消費(fèi)速度跟不上生成速度,造成隊(duì)列的累積
- 監(jiān)控隊(duì)列的長(zhǎng)度,一旦堆積,觸發(fā)報(bào)警系統(tǒng)
- 任意異步調(diào)用都包含超時(shí)機(jī)制
-
作用域未釋放
閉包。。。全局變量
內(nèi)存泄漏排查
- node-heapdump插件
- node-memwatch插件
大內(nèi)存應(yīng)用
不可避免還是會(huì)操作大文件的場(chǎng)景,使用stream模塊,繼承EventEmitter,具備事件功能
由于v8的內(nèi)存限制,使用fs.createReadStream/createWriteStream 替代fs.readFile/writeFile
不考慮字符串的情況下,使用buffer, buffer不受v8堆內(nèi)存的限制
Buffer
- 類似于數(shù)組的二進(jìn)制數(shù)據(jù),他的元素為16進(jìn)制的兩位數(shù),即0到255的數(shù)值
內(nèi)存分配
buffer對(duì)象不占用v8的堆內(nèi)存中,便于處理大量的字節(jié)數(shù)據(jù),在c++層申請(qǐng)內(nèi)存,在js中分配內(nèi)存的策略
slab分配機(jī)制
buffer的轉(zhuǎn)換
- 字符串轉(zhuǎn)buffer __new Buffer(str, [encoding])
- buffer轉(zhuǎn)字符串 —— buf.toString([encoding], [start], [end])
- 判讀是否支持的編碼類型 —— Buffer.isEncoding(encoding)
buffer的拼接
setEncoding(encoding)
buffer與性能
通過預(yù)先將靜態(tài)內(nèi)容轉(zhuǎn)為buffer對(duì)象,可以有效減少cpu的重復(fù)使用,節(jié)省服務(wù)器資源
網(wǎng)絡(luò)編程
node提供了net,dgram,http,https模塊用于搭建服務(wù)器
-
tcp
傳輸層控制協(xié)議
面向連接的協(xié)議,需要3次握手建立連接, 在創(chuàng)建回話的過程中,服務(wù)端和客戶端分別提供一個(gè)套接字,這兩個(gè)套接字共同形成一個(gè)連接
開啟keepalive,一個(gè)tcp會(huì)話可以用于多次請(qǐng)求和響應(yīng)
var net = require('net') var server = net.createServer(function(socket) { socket.on('data', function(data) { socket.write('hello word') }) socket.on('end', function(){ console.log('connect abort') }) socket.write('sdjfsdjfs') }) server.listen(8124, function(){ console.log(serve bound) })
// 客戶端 var net = require('net') var client = net.connect({port: 8124}, function() { console.log('client connected') client.write('word\r\n') }) client.on('data', function(data){ console.log(data) }) client.on('end', function() { console.log('client disconnected') })
-
服務(wù)器事件
- listen事件
- connection事件
- close事件
- error事件
-
連接事件
服務(wù)器可以同時(shí)與多個(gè)客戶端保持連接,對(duì)每個(gè)連接而言是典型的可寫可讀stream對(duì)象,stream對(duì)象用于服務(wù)器端和客戶端之間的通信
-
data
當(dāng)一端調(diào)用了write()發(fā)送數(shù)據(jù)時(shí),另一端會(huì)觸發(fā)data事件
end
connect 用于客戶端連接
drain 當(dāng)一端調(diào)用write()發(fā)送數(shù)據(jù)時(shí),當(dāng)前這端會(huì)觸發(fā)drain事件
error 異常事件
close 當(dāng)套接字完全關(guān)閉時(shí),會(huì)觸發(fā)
timeout 當(dāng)一定時(shí)間后連接不再活躍時(shí),該事件將會(huì)被觸發(fā),通知用戶當(dāng)前該連接已經(jīng)被閑置
tcp 中的Nagle算法
針對(duì)小數(shù)據(jù)包,采用延遲,合并數(shù)據(jù)包,達(dá)到一定數(shù)量或時(shí)間后發(fā)出,以此優(yōu)化網(wǎng)絡(luò)
tcp默認(rèn)開啟nagle算法,可以調(diào)用socket.setNoDelay(true)去掉
-
-
-
UDP
用戶數(shù)據(jù)包協(xié)議
不是面向連接的,無需連接
一對(duì)多,多對(duì)多
安全性,可靠性低
適用于現(xiàn)在的直播,視頻
// 創(chuàng)建udp套接字 var dgram = require('dgram') var server = dgram.createSocket('upd4') 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)
// 客戶端 var dgram = require('dgram') var message = Buffer.from('深入淺出node.js') var client = dgram.createSocket('upd4') client.send(message, 0, message.length, 41234, 'localhost', function(err, bytes){ client.close() })
send方法將信息發(fā)送到服務(wù)端
socket.send(buf, offset, length, port, address, [callback])
- 套接字事件
- message
- listening
- close
- error
- 套接字事件
-
Http
應(yīng)用層協(xié)議,基于tcp協(xié)議
基于請(qǐng)求響應(yīng)式,一問一答實(shí)現(xiàn)服務(wù)
http服務(wù)端的事件
- connection事件 tcp連接后觸發(fā)
- request事件
- close事件
- checkContinue事件
- connect事件 客戶端發(fā)起請(qǐng)求時(shí)觸發(fā)
- upgrade事件 客戶端升級(jí)連接的協(xié)議時(shí),服務(wù)端接受到時(shí)觸發(fā)
- clientError事件 連接的客戶端觸發(fā)error事件,服務(wù)端接受到錯(cuò)誤時(shí)觸發(fā)
http客戶端
? http.request(options, connect) 構(gòu)造http客戶端
var options = { hostname: '127.0.0.1', port: 1234, path: '/', method: 'GET', headers: {}, // auth basic認(rèn)證, 這個(gè)值將被計(jì)算成請(qǐng)求頭中的authorization部分 } var req = http.request(options, function(res) { res.setEncoding('utf-8') res.on('data', function(chunk) { console.log(chunk) }) }) req.end()
http代理
new http.Agent({ maxSockets: 10, //當(dāng)前連接池中使用的連接數(shù) requests: 5 // 處于等待狀態(tài)的請(qǐng)求數(shù) })
http客戶端事件
- response
- socket 服務(wù)端響應(yīng)了200狀態(tài)碼,客戶端將會(huì)觸發(fā)該事件
- upgrade
- continue 服務(wù)端響應(yīng)100 continue,客戶端將觸發(fā)該事件
構(gòu)建websocket服務(wù)
- 客戶端和服務(wù)端只要建立一個(gè)tcp連接
- server push,雙向通信
- 更輕量的協(xié)議頭,減少數(shù)據(jù)傳送量
// websocket在客戶端中應(yīng)用 var socket = new WebSocket('ws://127.0.0.1:1200/updates') socket.onopen = function() { } socket.onmessage = function(event){ }
websocket的握手
- Sec-WebSocket-Key 用于安全校驗(yàn), 值為base64編碼的字符串
Comet 長(zhǎng)輪詢
網(wǎng)絡(luò)服務(wù)與安全
- SSL(secure sockets layer)安全套接層,一種安全協(xié)議,在傳輸層對(duì)網(wǎng)絡(luò)連接加密
- 最初ssl應(yīng)用在web上,后期標(biāo)準(zhǔn)化,稱為TLS(transport layer security)安全傳輸層協(xié)議
node提供的3個(gè)模塊
-
crypto 加密解密
SHA1, MD5
-
tls
提供了與net模塊類似的功能,區(qū)別在與它建立在TLS/SSL加密的TCP連接上
TLS/SSL
是一個(gè)公鑰/私鑰的結(jié)構(gòu),它是一個(gè)非對(duì)稱的結(jié)構(gòu)
每個(gè)客戶端和服務(wù)端都有自己的公私鑰,公鑰用來加密,私鑰用來解密
建立安全傳輸之前,客戶端和服務(wù)器端之間需要互換公鑰,客戶端發(fā)送數(shù)據(jù)需要服務(wù)端的公鑰加密,服務(wù)端收到后用自己的私鑰解密,反之亦然
node在底層采用的是openssl實(shí)現(xiàn)TLS/SSL的,為此生成公鑰私鑰可以通過openssl完成
// 生成服務(wù)器端私鑰 > openssl genrsa -out server.key 1024 // 生成客戶端私鑰 > openssl genrsa -out client.key 1024 //生成公鑰 > openssl rsa -in server.key -pubout -out server.pem > openssl rsa -in client.key -pubout -out client.pem
中間人攻擊
在客戶端和服務(wù)器端在交換公鑰的過程中,有可能受到中間人攻擊,中間人對(duì)客戶端扮演服務(wù)端的角色,對(duì)服務(wù)端扮演客戶端角色,分別獲取相應(yīng)的公鑰,存在安全威脅
解決方法:
? 需要對(duì)公鑰進(jìn)行認(rèn)證,確認(rèn)得到的公鑰出自目標(biāo)服務(wù)器
? 數(shù)字證書,其中包含了服務(wù)器的名稱和主機(jī)名,服務(wù)器公鑰,簽發(fā)機(jī)構(gòu)的信息和簽名
? CA(數(shù)字證書認(rèn)證中心),作用為站點(diǎn)頒發(fā)證書,且這個(gè)證書中具有ca通過自己公鑰和私鑰實(shí)現(xiàn)的簽名,服務(wù)器需要通過自己的私鑰生成CSR(證書簽名請(qǐng)求文件)
? 中小企業(yè)多半采用自簽名證書,就是自己扮演CA機(jī)構(gòu)
// 扮演ca機(jī)構(gòu) 生成私鑰,生成csr文件,通過私鑰自簽名生成證書的過程 > openssl genrsa -out ca.key 1024 > openssl req -new -key ca.key -out ca.csr > openssl x509 -req -in ca.csr -signkey ca.key -out ca.crt
服務(wù)器申請(qǐng)簽名證書之前創(chuàng)建自己的csr文件
> openssl req -new -key server.key -out server.csr
申請(qǐng)簽名,需要ca的證書和私鑰參與,最后頒發(fā)一個(gè)帶有ca簽名的證書
> openssl x509 -req -CA ca.crt -CAkey ca.key -CAcreateserial -in server.csr -out server.crt
客戶端發(fā)送請(qǐng)求前去獲取服務(wù)器的證書,并通過ca的證書驗(yàn)證真?zhèn)?/p>
-
Https
https就是工作在TLS/SSL上的Http
需要私鑰和簽名證書
// 創(chuàng)建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 word\n') }).listen(8000)
// 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) })
構(gòu)建web應(yīng)用
-
請(qǐng)求方法
req.method
-
路徑解析
req.url
var pathname = url.parse(req.url).pathname
-
查詢字符串
var url = require('url') var querystring = require('querystring') var query = querystring.parse(url.parse(req.url).query) // 更簡(jiǎn)潔實(shí)現(xiàn) // var query = url.parse(req.url, true).query // {foo: 'bar', baz: 'val'} // 如果鍵值出現(xiàn)多次,會(huì)解析為數(shù)組 // foo=bar&foo=baz // {foo: ['bar', 'baz']}
-
Cookie
http是無狀態(tài)協(xié)議,cookie用來保存狀態(tài)
服務(wù)器生成cookie,發(fā)送給瀏覽器,瀏覽器保存本地,每次發(fā)送請(qǐng)求,攜帶cookie
// 生成cookie res.setHeader('Set-Cookie': '') Set-Cookie: name=value; Path=/;Expires=Sun,23-Apr-23 09:01:35 GMT; Domain=.domain.com; // 設(shè)置HttpOnly, 防止cookie篡改 // 設(shè)置Secure 為true,只能在https中有效
Path: cookie影響到的路徑
-
expires 和Max-age 設(shè)置過期時(shí)間,如果沒設(shè)置,默認(rèn)cookie只存活在會(huì)話器,關(guān)閉瀏覽器,cookie丟失
expires UTC時(shí)間格式, 當(dāng)服務(wù)器時(shí)間和瀏覽器時(shí)間不一致時(shí),會(huì)有偏差
max-age:多久過期
Cookie 性能影響
一旦cookie過多,造成請(qǐng)求頭部體積過大,造成待寬的浪費(fèi)
設(shè)置domain,限定使用的域
為靜態(tài)文件使用不同的域名,不需要發(fā)送cookie,缺點(diǎn)是,多個(gè)域名就多一次dns查詢,好在dns可以緩存
前端可以通過document.cookie修改cookie
目前的應(yīng)用場(chǎng)景:廣告和在線統(tǒng)計(jì)領(lǐng)域最為依賴cookie
-
Session
解決cookie的缺點(diǎn):1請(qǐng)求攜帶cookie,造成請(qǐng)求頭過大,2.前后端都可以篡改cookie, 有安全隱患
session保留在服務(wù)器端,客戶端無法修改
如何將每個(gè)客戶和服務(wù)器中的數(shù)據(jù)一一對(duì)應(yīng)起來
-
基于cookie來實(shí)現(xiàn)用戶和數(shù)據(jù)的映射
服務(wù)器生成session,將sessionId放在cookie中,發(fā)送客戶端
session是有有效期設(shè)置的一般20分鐘
客戶端每次發(fā)送請(qǐng)求攜帶sessionId,通過cookie發(fā)送
服務(wù)端接收后校驗(yàn),如果過期,則重新生成
-
通過查詢字符串來實(shí)現(xiàn)瀏覽器和服務(wù)器數(shù)據(jù)的對(duì)應(yīng)
檢查請(qǐng)求的查詢字符串,如果沒有值,會(huì)先生成新的帶值的url
然后跳轉(zhuǎn),讓客戶端重新發(fā)起請(qǐng)求
session與內(nèi)存
? 統(tǒng)一集中存儲(chǔ)在redis中
session與安全
? 將口令進(jìn)行簽名
-
xss
跨站腳本攻擊
反射型 誘導(dǎo)用戶點(diǎn)擊惡意鏈接,注入惡意腳本
存儲(chǔ)型
在頁面中注入惡意腳本代碼,發(fā)送給服務(wù)器,其他用戶訪問時(shí),接受到惡意腳本,產(chǎn)生安全隱患,比如在評(píng)論區(qū)注入腳本
解決方法:
-
開啟瀏覽器csp(內(nèi)容安全策略),本質(zhì)是建立白名單,規(guī)定瀏覽器只能執(zhí)行特定來源的代碼
通過Content-Security-Policyhttp來開啟
cookie設(shè)置httpOnly
輸入輸出字符進(jìn)行轉(zhuǎn)譯過濾
-
-
-
緩存
get請(qǐng)求緩存
檢查本地文件是否有緩存
強(qiáng)緩存
-
expires
utc格式時(shí)間字符串
缺陷:瀏覽器時(shí)間和服務(wù)器時(shí)間可能不一致
-
Cache-Control
public/private/no-cache/no-store/max-age
檢查服務(wù)端文件是否有緩存
協(xié)商緩存 發(fā)送請(qǐng)求,服務(wù)器檢查請(qǐng)求頭部是否有緩存標(biāo)識(shí),命中返回304
-
Last-modified / if-modified-since
時(shí)間搓改動(dòng)但內(nèi)容未必修改
時(shí)間搓只能到秒,更新頻繁,無法生效
-
Etag / If-None-Match
唯一標(biāo)識(shí)符
var getHash = function(str) { var shasum = crypto.createHash('sha1') return shasum.update(str).digest('base64') } // 服務(wù)端 var handle = function(req, res) { fs.readFile(filename, function(err, file) { var hash = getHash(file) var noneMatch = req.headers['if-none-match'] if(hash == noneMatch) { res.writeHead(304, 'not Modified') res.end() }else{ res.setHeader('Etag', hash) res.writeHead(200, 'OK') res.end(file) } }) }
正常鏈接進(jìn)入: 匹配強(qiáng)緩存,再匹配協(xié)商緩存
頁面刷新:跳過強(qiáng)緩存,匹配協(xié)商緩存
強(qiáng)制刷新:跳過緩存策略
勾選disable cache 或請(qǐng)求頭中設(shè)置no-cache,跳過緩存策略
清除緩存
Url
url中跟隨版本號(hào)
url中跟隨hash ,hash發(fā)生變化,發(fā)送新的請(qǐng)求,推薦
http://url.com/?hash=adjfisdfsd
-
-
Basic認(rèn)證
當(dāng)客戶端和服務(wù)端進(jìn)行請(qǐng)求時(shí),允許通過用戶名和密碼實(shí)現(xiàn)的一種身份認(rèn)證方式
檢查請(qǐng)求頭中的Authorization字段,該字段由認(rèn)證方式和加密值構(gòu)成
Authorization: Basic dXNlcjpwYXNz // dXNlcjpwYXNz 是由用戶名和密碼結(jié)合并base64編碼的值
如果首次訪問網(wǎng)頁,請(qǐng)求頭中沒有攜帶認(rèn)證內(nèi)容,那么瀏覽器會(huì)響應(yīng)401未授權(quán)的狀態(tài)碼
缺點(diǎn):雖然經(jīng)過base64加密傳送,但是安全系數(shù)低
優(yōu)點(diǎn):兼容性好,幾乎所有的瀏覽器都支持
-
數(shù)據(jù)上傳
contetn-length代表報(bào)文的長(zhǎng)度
-
表單數(shù)據(jù)
Content-Type: application/x-www.form-urlencoded
-
附件上傳
content-Type: multipart/form-data; boundary=AaBo3x
boundary=AaBo3x指定每部分的分界符,AaBo3x是隨機(jī)生成的一段字符串
<form action='/upload' enctype='multipart/form-data'></form>
-
數(shù)據(jù)上傳與安全
-
內(nèi)存限制
避免大體積數(shù)據(jù)上傳,內(nèi)存被占光
限制上傳內(nèi)容的大小,超過限制,停止接受
通過流式解析,將數(shù)據(jù)流導(dǎo)向磁盤中,node中只保留文件路徑信息
-
csrf
跨站請(qǐng)求偽造,利用已經(jīng)登錄的用戶信息,發(fā)送惡意的請(qǐng)求,獲取用戶信息的一種攻擊方式
用戶驗(yàn)證
服務(wù)器refrere check
token驗(yàn)證
-
-
路由解析
-
文件路徑型
- 靜態(tài)文件
- 動(dòng)態(tài)文件
-
MVC 將業(yè)務(wù)邏輯按職責(zé)分離
- 控制器(Controller),行為的集合
- 模型(Model),數(shù)據(jù)相關(guān)的操作和封裝
- 視圖(View),視圖的渲染
路由解析-》對(duì)應(yīng)的控制器中的行為-》調(diào)用相關(guān)的模型,進(jìn)行數(shù)據(jù)操作-》視圖更新
RESTful
-
-
中間件
var middleware = function(req, res, next) { ... next() }
-
異常處理
由于異步方法中的異常不能直接捕獲,需要通過next(err) 向外傳出
-
中間件與性能
編寫高效的中間件
-
合理使用路由
配置路徑,app.use('/public', staticFile)
頁面渲染
-
內(nèi)容響應(yīng)
響應(yīng)報(bào)頭中的Content-*字段
Content-Encoding: gzip
Content-length: 22217
Content-Type: text/javascript;charset=utf-8
客戶端接收這個(gè)報(bào)文后,通過gzip來解碼報(bào)文體的內(nèi)容,用長(zhǎng)度校驗(yàn)內(nèi)容是否正確,讓后以字符集utf-8解碼后內(nèi)容插入到文檔中
MIME
-
附件下載
客戶端不用打開它,之需彈出下載,
Content-Disposition: inline// 代表內(nèi)容只需要即時(shí)查看; attachment//代碼數(shù)據(jù)可以存為可下載的附件
res.sendfile = function(filePath) { fs.stat(filepath, function(err, stat) { var stream = fs.createReadStream(filepath) res.setHeader('Content-Type', mime.lookup(filepath)) res.setHeader('Content-Length', stat.size) //設(shè)置長(zhǎng)度 // 設(shè)置為附件 res.setHeader('Content-Disposition', 'attachment; filename="'+path.basename(filepath) + "'") res.writeHead(200) stream.pipe(res) }) }
響應(yīng)json
-
響應(yīng)跳轉(zhuǎn)
res.redirect = function(url) { res.setHeader('Location', url) res.writeHead(302) res.end('Redirect to' + url) }
-
視圖渲染
將數(shù)據(jù)和模版文件結(jié)合,通過模版引擎渲染成最終的html頁面
早期的模版語言 jsp, asp, php
破局者: Mustache, 以{{ }}為標(biāo)志的一套模版語言
//簡(jiǎn)易模版函數(shù),主要是正則匹配
var render = function(str, data) {
var tpl = str.replace(/<%=(.*)&>/g, function(str, code) {
return " '+ obj." + code + "+ '"
})
tpl = "var tpl = '" + tpl + "'\nreturn tpl;"
// Function中tpl為模版, obj為參數(shù)
var complied = new Function('obj', tpl)
return complied(data)
}
? 1.with的應(yīng)用
var complie = function(str, data) {
var tpl = str.replace(/<%(.*)%>/g, function(all, code) {
return "'+" + code + "+ '"
})
tpl = "tpl = '" + tpl + "'"
tpl = 'var tpl = "";\nwith(obj) {' + tpl + '}\nreturn top;'
return new Function('obj', tpl)
}
? new Function ([arg1[, arg2 [,...argN]]], fnBody)
? 2.模版安全
? 轉(zhuǎn)譯函數(shù)
var escape = function(html) {
return String(html).replace(/&(?!\w+;)/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"').replace(/'/g, ''')
}
? 3.模版邏輯
? 4. 集成文件系統(tǒng), 引入緩存,避免多次重復(fù)編譯
? 5. 子模版 include
-
Bigpipe
翻譯為風(fēng)笛, 是用于調(diào)用限流的
用于將頁面分割為多個(gè)部分,先向用戶輸出沒有數(shù)據(jù)的布局,將每個(gè)部分逐步輸出到前端,再最終渲染填充框架,完成整個(gè)頁面渲染
- 頁面布局框架(無數(shù)據(jù))
- 后端持續(xù)性的數(shù)據(jù)輸出
- 前端渲染
玩轉(zhuǎn)進(jìn)程
? 進(jìn)程: cpu資源分配的最小單位 (工廠)
? 線程: cpu調(diào)度的最小單位 (工人)
nodejs : v8引擎,單線程
單線程的缺點(diǎn):不能發(fā)揮多核cpu的優(yōu)勢(shì),拋出的異常未被捕獲處理,會(huì)造成進(jìn)程退出,健壯性和穩(wěn)定性低
優(yōu)點(diǎn): 沒有多線程上下文切換的問題,提高cpu的使用率,沒有鎖,線程同步問題
服務(wù)模型的變遷
石器時(shí)代: 同步,阻塞,已淘汰
-
青銅時(shí)代: 復(fù)制進(jìn)程
通過進(jìn)程的復(fù)制同時(shí)服務(wù)更多的請(qǐng)求和用戶,每個(gè)連接需要一個(gè)進(jìn)程服務(wù)
-
白銀時(shí)代: 多線程
線程之間共享數(shù)據(jù), 建立線程池,減少創(chuàng)建和銷毀線程的開銷
多線程上下文切換問題,大并發(fā)量時(shí),會(huì)暴露一些問題
-
黃金時(shí)代: 單線程+事件驅(qū)動(dòng)
node/nginx
解決高并發(fā)問題
單線程避免了不必要的內(nèi)存開銷和上下文切換開銷
-
多進(jìn)程架構(gòu)
解決單線程對(duì)多核cpu使用不足的問題,每個(gè)進(jìn)程利用一個(gè)cpu
node中提供了child_process /curster模塊
主從模式(Master-Worker模式):主進(jìn)程和工作進(jìn)程, 典型的分布式架構(gòu)中用于處理并行業(yè)務(wù)的模式,主進(jìn)程不負(fù)責(zé)具體的業(yè)務(wù)處理,復(fù)制調(diào)度和管理工作進(jìn)程, 工程進(jìn)程負(fù)責(zé)具體的業(yè)務(wù)處理
-
創(chuàng)建子進(jìn)程
Child_process spawn()/exec()/fork()/execFile()
-
進(jìn)程間的通信
主線程和工作線程之間通過
onmessage()
postMessage()
子進(jìn)程對(duì)象則通過send() 和message()事件
-
IPC通道
進(jìn)程間通信(ipc),讓不同的進(jìn)程之間通信
node中實(shí)現(xiàn)ipc的事pipe技術(shù)
-
句柄傳遞
多個(gè)進(jìn)程監(jiān)聽同一個(gè)端口,會(huì)報(bào)端口被占用的錯(cuò)誤,
現(xiàn)在采用的是主進(jìn)程監(jiān)聽主端口(如80),主進(jìn)程對(duì)外接收所有的網(wǎng)絡(luò)請(qǐng)求,再將這些請(qǐng)求分別代理到不同端口的進(jìn)程上
通過代理,解決端口不能重復(fù)被監(jiān)聽,可以適當(dāng)?shù)呢?fù)載均衡
child.send(message, [sendHandle]) //第二個(gè)可選參數(shù)就是句柄
句柄是一種可以用來標(biāo)識(shí)資源的引用,內(nèi)部包含了指向?qū)ο蟮奈募枋龇?/p>
可以去掉代理這種方案,使主進(jìn)程接收到socket請(qǐng)求后,將這個(gè)socket直接發(fā)送給工作進(jìn)程
var child = require('child_process').fork('child.js') var server = require('net').createServer() server.on('connection', function(socket) { socket.end('handle by parent\n') }) server.listent(1337, function(){ child.send('server', server) //將server傳遞給子進(jìn)程 }) // 子進(jìn)程 process.on('message', function(m, server) { if(m === 'server') { server.on('connection', function(socket) { socket.end('handle by children \n') }) } })
- 句柄的發(fā)送與還原
- 端口共同監(jiān)聽
-
-
集群穩(wěn)定之路
-
進(jìn)程事件
send() /message()
error
-
exit 子進(jìn)程退出時(shí)觸發(fā),正常退出,第一個(gè)參數(shù)為退出碼,被kill()殺死,會(huì)得到第二個(gè)參數(shù),代表殺死進(jìn)程的信號(hào)
process.exit(1)
process.kill(process.pid, 'SIGTERM')
close 子進(jìn)程的標(biāo)準(zhǔn)輸入輸出流中止時(shí)觸發(fā)
-
自動(dòng)重啟
// master.js var fork = require('child_process').fork var cpus = require('os').cpus var server = require('net').createServer() server.listen(1337) var workers = {} var createWorker = function() { // 限量重啟 if(xxx){ // 發(fā)送giveup事件,不再重啟 process.emit('giveup') return } var worker = fork(__dirname + '/worker.js') // 接收到自殺信號(hào)后,啟動(dòng)新的進(jìn)程, 保持總是有新的工作進(jìn)程存在,可以處理請(qǐng)求 worker.on('message', function(message) { if(message.act === 'suicide') { createWorker() } }) // 退出時(shí)重新啟動(dòng)新的進(jìn)程 worker.on('exit', function() { delete workers[worker.pid] createWorker() }) // 句柄轉(zhuǎn)發(fā) worker.send('server', server) worker[worker.pid] = worker } for(var i=0;i<cpus.length; i++) { createWorker() } // 進(jìn)程自己退出時(shí), 讓所有工作進(jìn)程退出 process.on('exit', function() { for(var pid in workers) { workers[pid].kill() } })
// worker.js var http = require('http') var server = http.createServer(function(req, res) { res.writeHead(200, {'Content-Type': 'text/plain'}) res.end('handled by child') }) var worker process.on('message', function(m, tcp) { if(m === 'server') { worker = tcp worker.on('connection', function(socket) { server.emit('connection', socket) }) } }) //報(bào)錯(cuò)處理 process.on('uncaughtException', function(err) { // 記錄日志 logger.error(err) process.send({act: 'suicide'}) //向主進(jìn)程發(fā)送‘自殺’信號(hào) // 停止接收新的連接 worker.close(function() { process.exit(1) // 退出進(jìn)程 }) // 5秒后退出進(jìn)程 setTimeout(function() { process.exit(1) }, 5000) })
-
負(fù)載均衡
多進(jìn)程之間監(jiān)聽相同的端口,使用戶請(qǐng)求能夠分散到多個(gè)進(jìn)程上進(jìn)行處理,保證多個(gè)進(jìn)程處理的工作量公平的策略就叫負(fù)載均衡
將cpu資源都調(diào)用起來
node默認(rèn)提供的機(jī)制是采用操作系統(tǒng)的搶占式策略,就是閑置的進(jìn)程對(duì)請(qǐng)求進(jìn)行搶奪,誰搶到誰服務(wù),它的繁忙由cpu和i/o構(gòu)成,影響搶占的是cpu的繁忙度,有可能存在cpu空閑,但是i/o忙的情況,這樣去搶占服務(wù),會(huì)形成負(fù)載不均衡
node v0.11提供了新的策略, Round-Robin(輪叫調(diào)度),由主進(jìn)程接收請(qǐng)求服務(wù),依次發(fā)給工作進(jìn)程
//啟用round-robin
cluster.schedulingPolicy = cluster.SCHED_RR
// 不啟用
cluster.schedulingPolicy = cluster.SCHED_NONE
狀態(tài)共享
第三方數(shù)據(jù)存儲(chǔ)(數(shù)據(jù)庫, 磁盤, redis等)
主動(dòng)通知, 進(jìn)程通知
cluster模塊
? 創(chuàng)建單機(jī)node集群
//cluster.js
var cluster = require('cluster')
cluster.setupMaster({
exec: 'worker.js'
})
var cpus = require('os').cpus()
for(var i = 0; i< cpus.length; i++) {
cluster.fork()
}
執(zhí)行node cluster.js, 和上面用child_process創(chuàng)建子進(jìn)程集群效果相同
Cluster.setupMaster() /cluster.fork()創(chuàng)建子進(jìn)程
cluster原理
- cluster模塊是child_process 和net模塊的組合應(yīng)用
- 內(nèi)部隱式創(chuàng)建tcp服務(wù)
cluster 事件
- fork 復(fù)制一個(gè)工作進(jìn)程后觸發(fā)該事件
- online
- listening
- disconnect 主進(jìn)程和工作進(jìn)程之間ipc通道斷開后會(huì)觸發(fā)
- exit 進(jìn)程退出
- setup cluster.setupMaster()執(zhí)行后觸發(fā)
測(cè)試
-
單元測(cè)試
代碼規(guī)范:
- 單一職責(zé)
- 接口抽象
- 層次分離
單元測(cè)試介紹
-
斷言
assert模塊
Should.js 斷言庫
var assert = require('assert') assert.equal(Math.max(1, 100), 100) // 如果不滿足期望,則會(huì)拋出AssertionError異常,整個(gè)程序會(huì)停止執(zhí)行
ok() 判斷結(jié)果是否為真
equal 是否相等
notEqual() 是否不相等
deepEqual() 是否深度相等
strictEqual() 是否嚴(yán)格相等
throws() 判斷代碼塊是否拋出異常
doesNotThrow() 判斷代碼塊是否沒有拋出異常
ifError() 判斷實(shí)際值是否為一個(gè)假值(null, undefined, 0, '', false)
-
測(cè)試框架, 用來管理測(cè)試用咧和生成測(cè)試報(bào)告
mocha
測(cè)試風(fēng)格
tdd(測(cè)試驅(qū)動(dòng)開發(fā))
-
bdd(行為驅(qū)動(dòng)開發(fā))
describe("#indexOf()", function() { it('should return -1 when not present', function() { [1,2,3].indexOf(4).should.equal(-1); }) it('shoudl return index when present', function(){ [1,2,3].indexOf(1).should.equal(0); [1,2,3].indexOf(2).should.equal(1); [1,2,3].indexOf(3).should.equal(2); }) })
異步測(cè)試
產(chǎn)品化
項(xiàng)目工程化
項(xiàng)目的組織能力
目錄結(jié)構(gòu)
-
構(gòu)建工具
合并,壓縮文件,打包應(yīng)用,編譯模塊等
grunt,webpack
-
編碼規(guī)范
文檔式約定——靠自覺
代碼提交時(shí)強(qiáng)制檢查——考工具
jsLint/EsLint
-
代碼審查
代碼托管平臺(tái)gitlab/github
git拉取分支,完成編程,合并到主支
-
部署流程
開發(fā)-》審查-》合并-》部署
-
性能
-
動(dòng)靜分離
node處理靜態(tài)文件的能力不算突出,將圖片,腳本樣式等靜態(tài)文件托管到nginx或cdn靜態(tài)服務(wù)器上,node只處理動(dòng)態(tài)請(qǐng)求即可
單獨(dú)部署到靜態(tài)服務(wù)器的好
-
啟用緩存
避免重復(fù)的請(qǐng)求和計(jì)算
redis
-
多進(jìn)程架構(gòu)
利用多核cpu的優(yōu)勢(shì),保障服務(wù)更健壯,持續(xù)化的服務(wù)
cluster模塊,child_process
社區(qū)中提供了pm,forever,pm2模塊
-
讀寫分離
針對(duì)數(shù)據(jù)庫,讀取速度遠(yuǎn)遠(yuǎn)高于寫入速度,寫入的時(shí)候會(huì)鎖表,會(huì)影響讀取速度
將數(shù)據(jù)庫讀寫分離,主從設(shè)計(jì)
-
日志
建立健全的排查和跟蹤機(jī)制
還原問題現(xiàn)場(chǎng),定位問題
分割日志
-
監(jiān)控報(bào)警
日志監(jiān)控
響應(yīng)時(shí)間
進(jìn)程監(jiān)控
磁盤監(jiān)控
內(nèi)存監(jiān)控
cpu占用監(jiān)控
網(wǎng)絡(luò)流量監(jiān)控
應(yīng)用狀態(tài)監(jiān)控
dns監(jiān)控
-
報(bào)警系統(tǒng)
郵件報(bào)警/短信或電話報(bào)警
穩(wěn)定性
-
多機(jī)器
更多的硬件資源
-
多機(jī)房
解決地理位置帶來的網(wǎng)絡(luò)延遲問題,在容災(zāi)方面,機(jī)房之間互為備份
容災(zāi)備份至少4臺(tái)服務(wù)器來構(gòu)建穩(wěn)定的服務(wù)集群
-
-