深入淺出Nodejs之筆記

深入淺出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è)步驟

      1. 優(yōu)先從緩存加載

        會(huì)對(duì)引入過的模塊進(jìn)行緩存

      2. 路徑分析和文件定位

        核心模塊/ 以./ 等開頭的相對(duì)路徑文件/以/開頭的絕對(duì)路徑

        模塊的路徑轉(zhuǎn)為真實(shí)路徑,作為索引

        module.paths,依次向上查找node_modules目錄

      3. 模塊編譯

        // 針對(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

    1. 回到timer階段,執(zhí)行到時(shí)的回調(diào)

    2. 執(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, '&amp;')
                        .replace(/</g, '&lt;')
                        .replace(/>/g, '&gt;')
                        .replace(/"/g, '&quot;').replace(/'/g, '&#039')
}

? 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ù)集群

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,310評(píng)論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,951評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,566評(píng)論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,055評(píng)論 1 322
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,303評(píng)論 0 288
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,799評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,683評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,899評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評(píng)論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,135評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評(píng)論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,528評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,844評(píng)論 2 372

推薦閱讀更多精彩內(nèi)容