1. 程式人生 > 實用技巧 >了不起的Nodejs學習筆記(TCP、HTTP、Connect)

了不起的Nodejs學習筆記(TCP、HTTP、Connect)

了不起的Nodejs學習筆記(TCP、HTTP、Connect)

六、TCP

1、TCP特性

  • 面向連線的通訊和保證順序的傳遞
    • TCP協議做基於的IP協議是面向無連線
    • IP是基於資料報的傳輸。這些資料報獨立進行傳輸,送達的順序也是無序。
    • TCP連線內進行資料傳遞時,傳送的IP資料報包含了標識該連線以及資料流順序的資訊
  • 面向位元組
    • TCP對字元以及字元編碼完全無知。
    • 允許ASCII字元 或 Unicode進行傳輸
    • 對訊息格式沒有嚴格約束,具有很好的靈活性
  • 可靠性
    • TCP是基於底層不可靠的服務,因此它必須要基於確認和超時實現一系列的機制來達到可靠性的要求。
    • 這種機制有效解決如網路錯誤或網路阻塞這樣不可測的情況
  • 流控制
    • 舉例:兩臺互相通訊的計算機,一臺速度遠快於另一臺。
    • TCP通過流控制方式確保兩點之間傳輸資料的平衡,避免傳送方壓垮接收方
  • 擁堵控制
    • TCP有一種內建機制能夠控制資料包的延遲率及丟包率不會太高,以此確保服務質量(QoS)
    • TCP通過控制資料包的傳輸速率來避免擁堵的情況

2、Telnet

  • 建立TCP連線

    require('http').createServer(function (req,res) {
        res.writeHead(200,{ 'Content-Type' : 'text/html' });
        res.end('<h1>Hello World</h1>')
    }).listen(3000)
    
  • 建立一個HTTP請求並接收HTTP響應

    # windows操作
    # 需要啟動Telnet客戶端
    # 執行命令
    telnet localhost 3000 # 回車後黑屏 
    # 組合鍵
    CTRL+] # 進入Telnet介面
    # Telnet介面
    回車 # 回車後黑屏
    # 輸入
    GET / HTTP/1.1
    HOST:localhost
    # 兩次回車
    

3、基於TCP的聊天程式

  • 建立一個基本的TCP伺服器

    • 成功連線到伺服器後,伺服器會顯示歡迎資訊,並要求輸入使用者名稱,並顯示當前連線人數
    • 輸入使用者名稱後,按下回車鍵後,就認為成功連線
    • 連線後,可以通過輸入資訊再按回車健,來向其他客戶端進行訊息的收發
  • 為什麼要按回車鍵?

    事實上,Telnet中輸入的任何資訊都會立刻傳送到伺服器。按下回車鍵是為了輸入“\n”字元。

    在Node伺服器端,通過"\n"來判斷訊息是否已完全到達,作為分隔符使用。

    實際上,回車鍵和輸入字元a沒什麼區別

  • 實現

    {
        "name": "tcp-chat",
        "version": "0.0.1",
        "description": "our first TCP server"
    }
    
    // index.js
    // windows系統,程式碼做了下改動
    // 模組依賴
    var net = require('net')
    // 追蹤連線數
    var count = 0,users= {};
    // 建立伺服器
    // createServer指定了一個回撥函式,該函式每次有新的連線建立時都會被執行
    var server = net.createServer(function (conn) {
        conn.write(
            '\r\n > welcome to \033[92mnode-chat\033[39m\r\n'
            + ' > ' + count + ' other people are connected at this time.\r\n'
            + ' > please write your name and press enter: '
        )
        count++;
        // 設定編碼
        conn.setEncoding('utf8');
        var nickname;
        // 廣播訊息
        function broadcast (msg,exceptMyself){
            for (var i in users){
                if(!exceptMyself || i != nickname){
                    users[i].write(msg)
                }
            }
        }
        let words = [];
        conn.on('data',function (data) {
            let str = '';
            if(data == '\r\n'){
                words.forEach(function (v) {
                    str += v;
                })
                words = []
            }else{
                words.push(data);
            }
            // 如果str有值,說明使用者回車操作
            if(str){
                // 尚未註冊使用者,第一份資料是暱稱
                if(!nickname){
                    if(users[str]){
                        conn.write('\033[93m> nickname already in use. try again:\033[39m')
                        return;
                    }else{
                        nickname = str;
                        users[nickname] = conn;
                        broadcast('\033[90m > ' + nickname + ' joined the room\033[39m\r\n')
                    }
                }else{
                    // 否則視為聊天訊息
                    broadcast('\033[90m > ' + nickname + ':\033[39m' + str + '\r\n',true)
                }
            }
        })
        conn.on('close',function () {
            count--;
            // 當有人退出刪除暱稱
            delete users[nickname];
            broadcast('\033[90m > ' + nickname + ' left the room\033[39m\r\n')
        })
    })
    // 監聽
    server.listen(3000,function(){
        console.log('\033[96m  server listening on *:3000\033[39m');
    })
    

七、HTTP

1、HTTP結構

​ HTTP協議構建在請求和響應的概念上,對應的Node.js中就是由http.ServerRequesthttp.ServerResponse這兩個構造器構造出來的物件。

​ 當用戶瀏覽一個網站時,使用者代理(瀏覽器)會建立一個請求,該請求通過TCP傳送給Web伺服器,隨後伺服器會給出響應。

2、頭資訊

HTTP協議,其目的是進行文件交換。它在請求和響應訊息前使用頭資訊(header)來描述不同的訊息內容。

require('http').createServer(function (req,res) {
    // 只有加入Content-Type確定文件型別,瀏覽器才能識別,否則end中的html程式碼會以字串形式展示在網頁
    // 雖然writeHead只指定了一個頭資訊,但是Node還是會把另外兩個頭資訊
    // Transfer-Encoding 和 Connection加進去
    // Transfer-Encoding頭資訊預設值是chunked,主要原因是Node天生的非同步機制,這樣響應就可以逐步產生
    res.writeHead(200, {'Content-Type':'text/html'})
    res.end('Hello <b>World</b>')
}).listen(3000)
require('http').createServer(function (req,res){
    res.writeHead(200);
    res.write('Hello');
    setTimeout(function () {
        res.end('World');
    },500);
}).listen(3000);

// 在呼叫end前,我們可以多次呼叫write方法來發送資料
// 為了儘可能快的響應客戶端,在首次呼叫wirte時,Node就把所有的響應頭資訊及第一塊資料(Hello)傳送出去
require('http').createServer(function (req,res){
    res.writeHead(200, {'Content-Type':'image/png'});
    var stream = require('fs').createReadStream('image.png');
    stream.on('data',function (data){
        res.write(data);
    });
    stream.on('end',function () {
        res.end();
    })
}).listen(3000);
// 以一系列資料塊的形式來將圖片寫入到響應中,有如下好處:
// 1、高效的記憶體分配。
//		要是對每個請求在寫入前都完全把圖片資訊讀取完(通過fs.readFile),在處理大量請求時會消耗大量記憶體
// 2、資料一旦就緒就可以立刻寫入了
// 實際上,以上例子做的就是:
// 把一個流(Stream)(檔案系統)接(peping)到了另一個流上(http.ServerRespnse物件)
// 流是Node.js中非常重要的一種抽象。流的對接是很常見的行為。
// 為此,Node.js提供了一個方法讓上述例子程式碼變得簡潔
require('http').createServer(function (req,res){
    res.writeHead(200, {'Content-Type':'image/png'});
    require('fs').createReadStream('image.png').pipe(res);
}).listen(3000)

3、連線

  • 對比TCP伺服器與HTTP伺服器的實現
    • 相似處
      • 都呼叫了createServer方法
      • 當客戶端連入時,都會執行一個回撥函式
    • 本質區別
      • 回撥函式中的物件型別不同
        • net伺服器(TCP)是一個connection物件
        • HTTP伺服器,是請求和響應物件
      • 原因
        • HTTP伺服器是更高層的API,提供控制和HTTP協議相關的一些功能
        • (重要原因)瀏覽器在訪問站點時不會只用一個連線。很多瀏覽器為了更快地載入網站內容,會開啟很多連線傳送請求。

預設情況下,Node會告訴瀏覽器始終保持連線,通過它傳送更多的請求。通過Connection頭資訊中的keep-alive值通知瀏覽器。為了提供效能(因為瀏覽器不想浪費時間去重新建立和關閉TCP連線),通常都是對的。不過,我們也可以呼叫writeHead方法,傳遞一個不同的值,如close,來將其重寫掉。

4、一個簡單的Web伺服器

{
    "name": "http-form",
    "version": "0.0.1",
    "description": "An HTTP server that processes forms"
}
// 例如:http://myhost.com/url?this+is+a+long+url
// req.url = url?this+is+a+long+url
// req.method:請求的方法
// Web協議HTTP/1.1,為請求定義了以下不同的方法
// GET(預設) POST PUT DELETE PATCH(最新)
require('http').createServer(function (req,res){
    if('/' == req.url){
        res.writeHead(200, {'Content-Type':'text/html'});
        res.end([
            '<form method="POST" action="/url">',
            '<h1>My form</h1>',
            '<fieldset>',
            '<label>Personal information</label>',
            '<p>What is your name?</p>',
            '<input type="text" name="name">',
            '<p><button>Submit</button></p>',
            '</form>'
        ].join(''));
    }else{
        res.writeHead(200, {'Content-Type':'text/html'});
        res.end('You sent a <em>' + req.method + '</em> request')
    }  
}).listen(3000,function () {
    console.log('服務啟動')
})

5、資料

當傳送HTML時,需要隨著響應體定義Content-Type頭資訊。

和響應訊息一樣,請求訊息也可以包含Content-Type頭資訊。

為了更有效地處理表單,這兩部分資訊都是不可或缺的。

require('http').createServer(function (req,res){
    if('/' == req.url){
        res.writeHead(200, {'Content-Type':'text/html'});
        res.end([
            '<form method="POST" action="/url">',
            '<h1>My form</h1>',
            '<fieldset>',
            '<label>Personal information</label>',
            '<p>What is your name?</p>',
            '<input type="text" name="name">',
            '<p><button>Submit</button></p>',
            '</form>'
        ].join(''));
    }else if('/url' == req.url && 'POST' == req.method){
        var body = '';
        req.on('data',function (chunk) {
            body += chunk;
        })
        req.on('end',function () {
            res.writeHead(200, {'Content-Type':'text/html'});
            res.end(
           // Node在拿到瀏覽器傳送的資料後,對其進行分析,然後構建一個JavaScript物件方便在指令碼使用
           // 它將所有頭的資訊都變成小寫:req.headers['content-type']
                '<p>Content-Type:' + req.headers['content-type'] + '</p>'
                + '<p>Data:</p><pre>' + body + '</pre>'
            )
        })
    }  
}).listen(3000,function () {
    console.log('服務啟動')
})
// 在此例中,監聽了data和end時間,建立了一個body字串用來接收資料塊
// 僅當end事件出發時,我們就知道資料接收完全了
// 之所以可以逐塊接收資料,是因為Node.js允許在資料到達伺服器時就可以對其進行處理
// 因為資料是以不同TCP包到達伺服器的,這和現實情況也完全匹配。
// 我們就先獲取一部分資料,再在某個時刻獲取其餘資料
// querystring的模組
// querystring模組將一個字串解析成一個物件
console.log(require('querystring').parse('name=Nickname'));
console.log(require('querystring').parse('q=Query+Nickname'));

// [Object: null prototype] { name: 'Nickname' }
// [Object: null prototype] { q: 'Query Nickname' }

6、整合

const qs = require('querystring');
require('http').createServer(function (req,res){
    if('/' == req.url){
        res.writeHead(200, {'Content-Type':'text/html'});
        res.end([
            '<form method="POST" action="/url">',
            '<h1>My form</h1>',
            '<fieldset>',
            '<label>Personal information</label>',
            '<p>What is your name?</p>',
            '<input type="text" name="name">',
            '<p><button>Submit</button></p>',
            '</form>'
        ].join(''));
    }else if('/url' == req.url && 'POST' == req.method){
        var body = '';
        req.on('data',function (chunk) {
            body += chunk;
        })
        req.on('end',function () {
            res.writeHead(200, {'Content-Type':'text/html'});
            // 使用querystring parse 對請求內容進行解析
            // 然後,從解析生成的物件中獲取name值,並展示給使用者
            // 這裡的name是指<input>標籤中的name值
            res.end(
                '<p>Your name is <b>' + qs.parse(body).name + '</b></p>'
            )
        })
    }  
}).listen(3000,function () {
    console.log('服務啟動')
})

7、讓程式更健壯

const qs = require('querystring');
require('http').createServer(function (req,res){
    if('/' == req.url){
        res.writeHead(200, {'Content-Type':'text/html'});
        res.end([
            '<form method="POST" action="/url">',
            '<h1>My form</h1>',
            '<fieldset>',
            '<label>Personal information</label>',
            '<p>What is your name?</p>',
            '<input type="text" name="name">',
            '<p><button>Submit</button></p>',
            '</form>'
        ].join(''));
    }else if('/url' == req.url && 'POST' == req.method){
        var body = '';
        req.on('data',function (chunk) {
            body += chunk;
        })
        req.on('end',function () {
            res.writeHead(200, {'Content-Type':'text/html'});
            res.end(
                '<p>Your name is <b>' + qs.parse(body).name + '</b></p>'
            )
        })
    }else{
        // URL沒有匹配到任何判斷條件,伺服器端一直都沒響應,瀏覽器一直處於掛起狀態。
        // 當伺服器不知道如何處理該請求時,傳送404狀態碼給客戶端
        res.wirteHead(404);
        res.end('Not Found');
    }  
}).listen(3000,function () {
    console.log('服務啟動')
})

8、Twitter Web客戶端

學習如何使用Node.js向其他Web伺服器傳送請求是十分重要的。

HTTP已經演變成並非僅用於交換最終渲染、展示給使用者的標記語言(如HTML)。

它還是伺服器在不同網路環境傳遞資料到一種方式。

JSON因其語法衍生自JavaScript線性物件,也快速成為了HTTP預設的標準資料格式。

// 服務端
require('http').createServer(function (req,res){
    res.writeHead(200);
    res.end('Hello World')
}).listen(3000);
// 客戶端
require('http').request({
    host:'127.0.0.1',
    port:3000,
    url:'/',
    method:'GET'
},function (res) {
    var body = '';
    res.setEncoding('utf8');
    res.on('data',function (chunk) {
        body += chunk;
    });
    res.on('end',function (chunk) {
        console.log('\n We got: \033[96m' + body + '\033[39m\n')
    });
}).end();

9、傳送資料

// 服務端
const qs = require('querystring');
require('http').createServer(function (req,res){
    var body = '';
    req.on('data',function (chunk) {
        body += chunk;
    })
    req.on('end',function () {
        res.writeHead(200);
        res.end('Done')
        console.log('\n got name \033[90m' + qs.parse(body).name + '\033[39m\n')
    })
}).listen(3000,function () {
    console.log('服務啟動')
})
// 客戶端
var http = require('http'),qs = require('querystring')
function send (theName) {
    http.request({
        host:'127.0.0.1',
        port:3000,
        url:'/',
        method:'GET'
    },function (res) {
        res.setEncoding('utf8');
        res.on('end',function () {
            console.log('\n \033[90m request complete!\033[39m');
            process.stdout.write('\n your name:')
        })
    }).end(qs.stringify({name:theName}))
}
process.stdout.write('\n your name:')
process.stdin.resume();
process.stdin.setEncoding('utf-8');
process.stdin.on('data',function (name) {
    send(name.replace('\n',''))
})

10、獲取推文

// 客戶端
var http = require('http'),qs = require('querystring')
// process.argv.slice(2).獲取Node程式執行時真正的引數值
var search = process.argv.slice(2).join(' ').trim()
if(!search.length) {
    return console.log('\n Usage: node tweets <search term>\n');
}
console.log('\n search for: \033[96m' + search + '\033[39m\n')
http.request({
    host:'search.twitter.com',
    path:'/search.json?' + qs.stringify({q:search})
},function (res) {
    var body = '';
    res.setEncoding('utf8');
    res.on('data',function(chunk) {
        body += chunk;
    })
    res.on('end',function () {
        var obj = JSON.parse(body);
        obj.results.forEach(function (tweet) {
            console.log(' \033[90m' + tweet.text + '\033[39m')
            console.log(' \033[94m' + tweet.from_user + '\033[39m')
            console.log('--')
        })
    })
}).end();
// 執行 node res.js
// Usage: node tweets <search term>
// 執行 node .\res.js Justin Bieber
// 此例會返回錯誤提示:
// The Twitter REST API v1 is no longer active. Please migrate to API v1.1
// 證明方法可用,但是Twitter已經不支援這個API了
// 改寫上例子
var http = require('http'),qs = require('querystring')
var search = process.argv.slice(2).join(' ').trim()
if(!search.length) {
    return console.log('\n Usage: node tweets <search term>\n');
}
console.log('\n search for: \033[96m' + search + '\033[39m\n')
http.get({
    host:'search.twitter.com',
    path:'/search.json?' + qs.stringify({q:search})
},function (res) {
    var body = '';
    res.setEncoding('utf8');
    res.on('data',function(chunk) {
        body += chunk;
    })
    res.on('end',function () {
        var obj = JSON.parse(body);
        obj.results.forEach(function (tweet) {
            console.log(' \033[90m' + tweet.text + '\033[39m')
            console.log(' \033[94m' + tweet.from_user + '\033[39m')
            console.log('--')
        })
    })
})
// 本質不同就是無須呼叫end方法了,而且從語義上更顯然能夠看出是要獲取資料

11、superagent

npm install superagent

// superagent改寫上例
var request = require('superagent')
require.get('https://twitter.com/search.json')
// send和set方法可以多次呼叫,並且均為漸進式API,可以鏈式呼叫,並通過end方法結束
	   .send({q:'justin bieber'})
		// 設定請求頭
	   .set('Date',new Date)
	   .end(function (res) {console.log(res.body)})

八、Connect

​ Connect是一個基於HTTP伺服器的工具集。

​ 它提供了一種新的組織程式碼的方式來與請求、響應物件進行互動,稱為中介軟體

1、通過Connect實現一個簡單的網站

  • 託管靜態檔案
  • 處理錯誤以及損壞或者不存在的URL
  • 處理不同型別的請求
{
    "name":"my-website",
    "version":"0.0.1",
    "dependencies":{
        "connect":"1.8.7"
    }
}
npm install
// 模組依賴
var connect = require('connect');
// 建立伺服器
var server = connect.createServer();
// 處理靜態檔案
server.use(connect.static(__dirname + '/website'))
// 監聽
server.listen(3000);

1、中介軟體

中介軟體由函式組成,它除了處理req和res物件之外,還接收一個next函式來做流控制。

根據每個請求的不同情況處理以下幾種不同的任務:

  • 記錄請求處理時間
  • 託管靜態檔案
  • 處理授權
// 利用中介軟體模式滿足傷處要求的應用
server.use(function (req,res,next) {
    // 記錄日誌
    console.error(' %s %s ',req.method,req.url);
    next();
})
server.use(function (req,res,next) {
    if('GET' == req.method && '/image' == req.url.substr(0,7)){
        // 託管圖片
    }else{
        // 交給其他中介軟體處理
        next()
    }
})
server.use(function (req,res,next) {
    if('GET' == req.method && '/' == req.url){
        // 響應index檔案
    }else{
        // 交給其他中介軟體處理
        next()
    }
})
server.use(function (req,res,next) {
    // 最後一箇中間件,到了這裡返回404
    res.writeHead(404);
    res.end('Not Found')
})

2、書寫可重用的中介軟體

// request-time.js
// 頁面向資料庫發起一系列請求,記錄響應時間大於100ms的請求記錄
/**
*  請求時間中介軟體
* 選項:
* 	- 'time'('Number'):超時闕值(預設100)
*/
// 暴露一個函式,此函式本身又返回一個函式
module.exports = function (opts) {
    // 預設超時闕值100
    var time = opts.time || 100;
    // 返回一箇中間件函式
    return function (req,res,next){
        // 建立一個計時器,並在指定時間內觸發
        var timer = setTimeout(function () {
            console.log(
            	'\033[90m%s %s\033[39m \033[91mis taking too long!\033[39m',
                req.method,
                req.url
            )
        },time)
        // 當響應結束時,清除計時器
        var end = res.end;	// 首先保持對原函式的引用
        // 重寫函式,再恢復原始函式,並呼叫它,最後清除計時器
        res.end = function (chunk,encoding){
            res.end = end;	// 恢復原始函式
            res.end(chunk,encoding); // 呼叫
            clearTimeout(timer); // 清除計時器
        }
        next()
    }
}
// 建立一個Connect應用,並建立兩條路由
// 第一條路由很快得到響應,第二條路由1秒後得到響應

// 模組依賴
var connect = require('connect'),time = require('./request-time');
// 建立伺服器
var server = connect.createServer();
// 記錄請求情況
server.use(connect.logger('dev'))
// 實現中介軟體
server.use(time({time:500}));
// 實現快速響應
server.use(function (req,res,next){
    if('/a' == req.url){
        res.writeHead(200);
        res.end('Fast!')
    }else{
        next();
    }
})
// 實現模擬的慢速響應
server.use(function (req,res,next){
    if('/b' == req.url){
        setTimeout(function () {
            res.writeHead(200);
            res.end('Slow!')
        },1000)
    }else{
        next();
    }
})
// 伺服器監聽埠
server.listen(3000);

3、static中介軟體

3.1、掛載

Connect允許中介軟體掛載到URL上。比如,static允許將任意一個URL匹配到檔案系統中任意目錄。

舉例來說,假設要讓/my-images這個URL和名為/images的目錄對應,可以如下設定

server.use('/my-images',connect.static('/path/to/images'))

3.2、maxAge

static中介軟體接收一個名為maxAge的選項,這個選項代表一個資源在客戶端快取的時間。

對一些不經常改動的資源來說,瀏覽器無須每次都去請求它

// 比如:一個Web應用常見的實踐方式就是將所有的客戶端JavaScript檔案都合併一個檔案中。
// 在其檔名中加上修訂號。設定maxAge選項,讓其永遠快取
server.use('js',connect.static('/path/to/bundles',{maxAge:1000000000000000000}))

3.3、hidden

如果該選項為true,Connect就會託管那些檔名以點(.)開始的在UNIX檔案系統中被認為是隱藏的檔案

server.use(connect.static('/path/to/resources',{hidden:true}))

3.4、query中介軟體

有時要傳送給伺服器的資料會以查詢字串形式,作為URL的一部分。

比如,url /blog-posts?page=5。當在瀏覽器中訪問該URL時,Node會以字串的形式將該URL儲存到req.url變數中。

server.use(function (req) {
    // req.url == '/blog-posts?page=5'
}) 

使用query中介軟體,就能夠通過req.query物件自動獲取這些資料

server.use(connect.query)
server.use(function (req,res) {
    // req.query.page == '5'
})

3.5、logger中介軟體

logger中介軟體時一個Web應用非常有用的診斷工具。

它將傳送進來的請求資訊和傳送出去的響應資訊列印在終端。

它提供了四種日誌格式:

  • default
  • dev
  • short
  • tiny
// 初始化logger中介軟體
server.use(connect.logger('dev'))
// 自定義日誌輸出格式
server.use(connect.logger(':method:remote-addr'));
// 通過動態的req和res來記錄頭資訊
// 以下:記錄響應的content-length和content-type資訊
server.use(
    connect.logger(
        'type is :res[content-type],length is ' 
        + ':res[content-length] and it took :reponse-time ms.'
))

3.6、body parser中介軟體

// 新增後,能夠在req.body中獲取POST資料了
server.use(connect.bodyParser())
server.use(function (req,res) {
    // req.body.myinput
})

3.6.1、處理上傳

bodyParser另一個功能就是使用formidable模組,它可以讓你處理使用者上傳的檔案

// 服務端
var server = connect(
	connect.bodyParser(),
    connect.static('static')
)
function (req,res,next){
    if('POST' == req.method){
        console.log(req.body.file)
    }else{
        next()
    }
}
<!--單檔案-->
<form action="/" method="POST" enctype="multipart/form-data">
    <input type='file'/>
    <button>
        Send File
    </button>
</form>
<!--多檔案-->
<!--req.body.files就包含一個數組,陣列中的元素就是此前單個檔案的物件-->
<form action="/" method="POST" enctype="multipart/form-data">
    <input type='file' name="files[]"/>
    <input type='file' name="files[]"/>
    <button>
        Send File
    </button>
</form>

3.7、cookie

當瀏覽器傳送cookie資料時,會將其寫道Cookie頭資訊中。其資料格式和URL中的查詢字串類似。

如果不想手動解析,也不想使用正則表示式去抽取,就可以使用cookieParser中介軟體

server.use(connect.cookieParser())
// 訪問cookie資料
server.use(function () {
    // req.cookies.secret = "value"
})

3.8、會話(session)

​ 在絕大多數Web應用中,多個請求間共享“使用者會話”的概念非常必要。

​ “登入”一個網站時,多少會使用某種形式的會話系統,它主要通過在瀏覽器中設定cookie來實現,該cookie資訊會在隨後所有的請求頭資訊中被帶回到伺服器。

Connect提供了簡單的實現方式。

簡單的登入系統

  • user.json

    {
        "tobi":{
            "pwd":"123",
            "name":"tobi"
        }
    }
    
  • client.js

    // 這裡直接require了JSON檔案。
    // 當只要對外暴露資料時,就不需要加module.exports,直接把資料暴露出來就好
    var connect = require('connect'),users = require('./users')
    // logger、bodyParser和session中介軟體
    // 由於session中介軟體要操作cookie,所以在之前要引入cookieParser中介軟體
    var server = connect(
    	connect.logger('dev'),
        connect.bodyParser(),
        connect.cookieParser(),
        // 出於安全考慮,在初始化session中介軟體的時候需要提供secret選項
        connect.session({secret:'my app secret'}),
        function (req,res,next){
            // 驗證使用者是否登入
            if('/' == req.url && req.session.logged_in){
                res.writeHead(200,{'Content-Type':'text/html'});
                res.end(
                    'Welcome back, <b>' + req.session.name + '</b>'
                    + '<a href="/logout">Logout</a>'
                )
            }else{
                // 沒有登入交給其他中介軟體處理
                next();
            }
        },
        // 登入頁
        function (req,res,next){
            // 展示登入表單
            if('/' == req.url && 'GET' == req.method) {
                res.writeHead(200,{'Content-Type':'text/html'})
                res.end([
                    '<form action="/login" method="POST">',
                    '<fieldset>',
                    '<legend>Please log in</legend>',
                    '<p>User: <input type="text" name="user"></p>',
                    '<p>Password:<input type="password" name="pwd"></p>',
                    '<button>Submit</button>',
                    '</fieldset>',
                    '</form>'
                ].join(''))
            }else{
                next()
            }
        },
        function (req,res,next){
            // 登入操作
            if('/login' == req.url && 'POST' == req.method) {
                res.writeHead(200);
                // 校驗登入憑證
                if(!users[req.body.user] || req.body.pwd != users[req.body.user].pwd){
                    res.end('Bad username/password')
                }else{
                    // 修改req.session物件
                    // 儲存 logged_in name
                    // req.session物件在響應傳送出去時就會自動儲存,無須手動處理
                    req.session.logged_in = true;
                    req.session.name = users[req.body.user].name;
                    res.end('Authenticated!')
                }
            }else{
                next();
            }
        },
        // 退出操作
        function (req,res,next){
            if('/logout' == req.url){
                // 修改req.session物件
                req.session.logged_in = false;
                res.writeHead(200);
                res.end('Logged out');
            }else{
                next();
            }
        },
        // 以上操作都不攔截,那就提示‘Not Found’
        function (req,res){
            res.writeHead(404);
            res.end('Not Found')
        }
    );
    server.listen(3000)
    

3.9、Redis session

登陸後,重啟node伺服器,重新整理瀏覽器,發現session丟失

原因就在於session預設的儲存方式是在記憶體。這就意味著session資料都是存在在記憶體中,當程序退出後,session資料自然也就丟失

在生產環境中,需要一種當應用重啟,還能夠將session資訊持久化儲存下來的機制,如Redis

var connect = require('connect'),RedisStore = require('connect-redis')(connect)
// 將其作為store選項的值傳遞給session中介軟體
server.use(connect.session({store:new RedisStore,secret:'my secret'}))

3.10、methodOverride中介軟體

一些早期瀏覽器不支援建立如PUT、DELETE、PATCH這樣的請求。

解決方案是在GET或者POST請求中加上一個_method變數來模擬上述請求。

// POST url?_method=PUT HTTP/1.1
// 中介軟體是序列執行的,所以務必確保把它放在其他處理請求中介軟體之前
server.use(connect.methodOverride())

3.11、basicAuth中介軟體

有些專案需要對客戶端進行基本的身份驗證,為此,Connect提供了一個名為basicAuth的中介軟體。

// 建立一個簡單的身份驗證系統,並且通過命令列對使用者進行驗證操作


var connect = require('connect')
var server = connect.createServer();
// 等待輸入
process.stdin.resume();
process.stdin.setEncoding('ascii');
// 新增basicAuth中介軟體
connect.basicAuth(function (user,pass,fn) {
    process.stdout.write(
        'Allow user \033[96m' + user + '\033[39m' +
        'with pass \033[90m' + pass + '\033[39m ? [y/n]: '
    );
    // 只需要對每個請求獲取一次資料
    process.stdin.once('data',function (data) {
        if(data[0] == 'y'){
            // 通過驗證,傳遞null作為第一個引數
            // 以及user物件用來生成req.remoteUser物件(供後續中介軟體使用)
            fn(null,{username:user});
        }else{
            // 未過驗證,傳遞Error物件作為第一個引數
            fn(new Error('Unauthorized'))
        }
    })
}),
function (req,res){
    res.writeHead(200);
    res.end('Welcome to the protected area,' + req.remoteUser.username);
}
server.listen(3000)