或許你在面試時遇到過這樣的問題:從輸入URL到瀏覽器顯示頁面發生了什麼?
簡單的回答就是:node
服務器處理請求面試
若是你以爲這樣回答過於簡單,不如來深刻了解一下吧。算法
在此以前,先了解一下TCP/IP基礎知識。數據庫
早期的TCP/IP模型是一個四層結構,從下往上依次是網絡接口層、互聯網層、傳輸層和應用層,後來將網絡接口層劃分爲了物理層和數據鏈路層瀏覽器
這裏能夠看到HTTP協議是構建於TCP之上,屬於應用層協議。緩存
DNS服務是和HTTP協議同樣位於應用層的協議,提供域名到IP地址的解析服務。服務器
獲得IP地址後就能夠創建鏈接了,這裏還有兩個知識須要瞭解:網絡
持久鏈接併發
持久鏈接(也稱爲HTTP keep-alive)的特色是,只要任意一段沒有提出斷開鏈接,就保持TCP鏈接狀態。app
管線化
持久鏈接創建後就可使用管線化發送了,能夠同時併發多個請求,不用等待一個接一個的響應。(在這裏我想到了流的pipe方法。)
大體說一下:
須要注意的是TCP的鏈接、傳輸和斷開都受六個控制位的指揮(好比三次握手和四次揮手)
瞭解了這些,那麼開始講重點
三次握手
四次揮手
四次揮手並非必然的,當服務器已經沒有內容發給客戶端了,就直接發送FIN報文段,這樣就變成了三次揮手。
HTTP報文大體可分爲報文首部和報文主體兩塊,二者由空行(就至關於用了兩個換行符rnrn)來劃分。報文主體並非必定要有的。
經常使用請求行方法:
說到響應報文,就必要談到狀態碼:
2XX 成功
3XX 重定向
4XX 客戶端錯誤
5XX 服務器端錯誤
通用首部
首部字段名 | 說明 |
---|---|
Cache-Control | 控制緩存行爲 |
Connection | 鏈接的管理 |
Date | 報文日期 |
Pragma | 報文指令 |
Trailer | 報文尾部的首部 |
Trasfer-Encoding | 指定報文主體的傳輸編碼方式 |
Upgrade | 升級爲其餘協議 |
Via | 代理服務器信息 |
Warning | 錯誤通知 |
請求首部
首部字段名 | 說明 |
---|---|
Accept | 用戶代理可處理的媒體類型 |
Accept-Charset | 優先的字符集 |
Accept-Encoding | 優先的編碼 |
Accept-Langulage | 優先的語言 |
Authorization | Web認證信息 |
Expect | 期待服務器的特定行爲 |
From | 用戶的電子郵箱地址 |
Host | 請求資源所在的服務器 |
If-Match | 比較實體標記 |
If-Modified-Since | 比較資源的更新時間 |
If-None-Match | 比較實體標記 |
If-Range | 資源未更新時發送實體Byte的範圍請求 |
If-Unmodified-Since | 比較資源的更新時間(和If-Modified-Since相反) |
Max-Forwards | 最大傳輸條數 |
Proxy-Authorization | 代理服務器須要客戶端認證 |
Range | 實體字節範圍請求 |
Referer | 請求中的URI的原始獲取方 |
TE | 傳輸編碼的優先級 |
User-Agent | HTTP客戶端程序的信息 |
響應首部
首部字段名 | 說明 |
---|---|
Accept-Ranges | 是否接受字節範圍 |
Age | 資源的建立時間 |
ETag | 資源的匹配信息 |
Location | 客戶端重定向至指定的URI |
Proxy-Authenticate | 代理服務器對客戶端的認證信息 |
Retry-After | 再次發送請求的時機 |
Server | 服務器的信息 |
Vary | 代理服務器緩存的管理信息 |
www-Authenticate | 服務器對客戶端的認證 |
實體首部
首部字段名 | 說明 |
---|---|
Allow | 資源可支持的HTTP方法 |
Content-Encoding | 實體的編碼方式 |
Content-Language | 實體的天然語言 |
Content-Length | 實體的內容大小(字節爲單位) |
Content-Location | 替代對應資源的URI |
Content-MD5 | 實體的報文摘要 |
Content-Range | 實體的位置範圍 |
Content-Type | 實體主體的媒體類型 |
Expires | 實體過時時間 |
Last-Modified | 資源的最後修改時間 |
建立HTTP服務端
let http = require('http'); let app = http.createServer((req, res) => {// req是可讀流/res是可寫流 // 獲取請求報文信息 let method = req.method;// 方法 let httpVersion = req.httpVersion;// HTTP版本 let url = req.url; let headers = req.headers; console.log(method, httpVersion, url, headers); // 獲取請求體(若是請求體的數據大於64k,data事件會被觸發屢次) let buffers = []; req.on('data', data => { buffers.push(data); }) req.on('end', () => { console.log(Buffer.concat(buffers).toString()); res.write('hello'); res.end('world'); }) }) // 監聽服務器事件 app.on('connection', socket => { console.log('創建鏈接'); }); app.on('close', () => { console.log('服務器關閉') }); app.on('error', err => { console.log(err); }); app.listen(3000, () => { console.log('server is starting on port 3000'); });
建立客戶端
let http = require('http'); let options = { hostname: 'localhost', port: 3000, path: '/', method: 'GET', // 設置實體首部 告訴服務端我當前要給你發什麼樣的數據 headers: { 'content-Type': 'application/x-www-form-urlencoded', 'Content-Length': 15 } } let req = http.request(options); req.on('response', res => { res.on('data', chunk => { console.log(chunk.toString()); }); }); req.end('name=js&&age=22')
而後使用node運行咱們的客戶端
說了這麼多,你可能已經大體瞭解了
從輸入URL到瀏覽器顯示頁面發生了什麼,不用多說,咱們再來看一下緩存。
強制緩存:說白了就是第一次請求數據時,服務端將數據和緩存規則一併返回,下一次請求時瀏覽器直接根據緩存規則進行判斷,有就直接讀緩存數據庫,不用鏈接服務器;沒有,再去找服務器。
從上張圖咱們能夠看到,判斷緩存是否可用,有兩種方式
Last-Modified是此資源的最後修改時間,
說了這麼多,不如直接來實現一下緩存
經過最後修改時間來判斷緩存是否可用
let http = require('http'); let url = require('url'); let path = require('path'); let fs = require('fs'); let mime = require('mime'); let app = http.createServer((req, res) => { // 根據url獲取客戶端要請求的文件路徑 let { parsename } = url.parse(req.url); let p = path.join(__dirname, 'public', '.' + pathname); // fs.stat()用來讀取文件信息,文件最後修改時間就是stat.ctime fs.stat(p, (err, stat) => { if (!err) { let since = req.headers['if-modified-since'];//客戶端發來的文件最後修改時間 if (since) { if (since === stat.ctime.toUTCString()) {//最後修改時間相等,讀緩存 res.statusCode = 304; res.end(); } else { sendFile(req, res, p, stat);//最後修改時間不相等,返回新內容 } } else { sendError(res); } } }) }) function sendError(res) { res.statusCode = 404; res.end(); } function sendFile(req, res, p, stat) { res.setHeader('Cache-Control', 'no-cache');// 設置通用首部字段 控制緩存行爲 res.setHeader('Last-Modified', stat.ctime.toUTCString());// 實體首部字段 資源最後修改時間 res.setHeader('Content-Type', mime.getType(p) + ';charset=utf8') fs.createReadStream(p).pipe(res); } app.listen(3000, () => { console.log('server is starting on port 3000'); });
最後修改時間存在問題:
1. 某些服務器不能精確獲得文件的最後修改時間, 這樣就沒法經過最後修改時間來判斷文件是否更新了。
2. 某些文件的修改很是頻繁,在秒如下的時間內進行修改. Last-Modified只能精確到秒。
3. 一些文件的最後修改時間改變了,可是內容並未改變。 咱們不但願客戶端認爲這個文件修改了。
4. 若是一樣的一個文件位於多個CDN服務器上的時候內容雖然同樣,修改時間不同。
經過ETag來判斷緩存是否可用
ETag就是根據文件內容來判斷,說白了就是採用MD5(md5並不叫加密算法,它不可逆,應該叫摘要算法)產生信息摘要,用摘要來進行比對。
let http = require('http'); let url = require('url'); let path = require('path'); let fs = require('fs'); let mime = require('mime'); // crypto是node.js中實現加密和解密的模塊 具體詳解請自行了解 let crypto = require('crypto'); let app = http.createServer((req, res) => { // 根據url獲取客戶端要請求的文件路徑 let { parsename } = url.parse(req.url); let p = path.join(__dirname, 'public', '.' + pathname); // fs.stat()用來讀取文件信息,文件最後修改時間就是stat.ctime fs.stat(p, (err, stat) => { let md5 = crypto.createHash('md5');//建立md5對象 let rs = fs.createReadStream(p); rs.on('data', function (data) { md5.update(data); }); rs.on('end', () => { let r = md5.digest('hex'); // 對文件進行md5加密 // 下次就拿最新文件的加密值 和客戶端請求來比較 let ifNoneMatch = req.headers['if-none-match']; if (ifNoneMatch) { if (ifNoneMatch === r) { res.statusCode = 304; res.end(); } else { sendFile(req, res, p, r); } } else { sendFile(req, res, p, r); } }); }) }); function sendError(res) { res.statusCode = 404; res.end(); } function sendFile(req, res, p, stat) { res.setHeader('Cache-Control', 'no-cache');// 設置通用首部字段 控制緩存行爲 res.setHeader('Etag', r);// 響應首部字段 資源的匹配信息 res.setHeader('Content-Type', mime.getType(p) + ';charset=utf8') fs.createReadStream(p).pipe(res); } app.listen(3000, () => { console.log('server is starting on port 3000'); });
想深刻學習http的同窗,我推薦一本書《圖解HTTP》。本人水平有限,有不足之處,望你們指出改正。