HTTP面試指南

前言


或許你在面試時遇到過這樣的問題:從輸入URL到瀏覽器顯示頁面發生了什麼?
簡單的回答就是:node

  1. DNS解析
  2. TCP創建鏈接
  3. 發送HTTP請求
  4. 服務器處理請求面試

    • 若是有緩存直接讀緩存
    • 沒有緩存返回響應內容
  5. TCP斷開鏈接
  6. 瀏覽器解析渲染頁面

若是你以爲這樣回答過於簡單,不如來深刻了解一下吧。算法

網絡基礎


在此以前,先了解一下TCP/IP基礎知識。數據庫

TCP/IP參考模型

  • 早期的TCP/IP模型是一個四層結構,從下往上依次是網絡接口層、互聯網層、傳輸層和應用層,後來將網絡接口層劃分爲了物理層和數據鏈路層瀏覽器

    • 應用層(Application)提供網絡與用戶應用軟件之間的接口服務
    • 傳輸層(Transimission)提供創建、維護和取消傳輸鏈接功能,負責可靠地傳輸數據(PC)
      傳輸層有兩個性質不一樣的協議:TCP(傳輸控制協議)和UDP(用戶數據報協議)
    • 網絡層(Network)處理網絡間路由,確保數據及時傳送(路由器)
    • 數據鏈路層(DataLink)負責無錯傳輸數據,確認幀、發錯重傳等(交換機)
    • 物理層(Physics)提供機械、電氣、功能和過程特性(網卡、網線、雙絞線、同軸電纜、中繼器)

各層經常使用協議

這裏能夠看到HTTP協議是構建於TCP之上,屬於應用層協議緩存

具體過程

1. DNS解析

DNS服務是和HTTP協議同樣位於應用層的協議,提供域名到IP地址的解析服務。服務器

獲得IP地址後就能夠創建鏈接了,這裏還有兩個知識須要瞭解:網絡

持久鏈接併發

持久鏈接(也稱爲HTTP keep-alive)的特色是,只要任意一段沒有提出斷開鏈接,就保持TCP鏈接狀態。app

管線化

持久鏈接創建後就可使用管線化發送了,能夠同時併發多個請求,不用等待一個接一個的響應。(在這裏我想到了流的pipe方法。)

2. TCP鏈接與斷開

2.1 TCP報文格式


大體說一下:

  1. 計算機經過端口號識別訪問哪一個服務,好比http;源端口號進行隨機端口,目的端口決定哪一個程序進行接收
  2. 數據序號和確認序號用於保障傳輸數據的完整性和順序
  3. 須要注意的是TCP的鏈接、傳輸和斷開都受六個控制位的指揮(好比三次握手和四次揮手)

    • PSH(push急迫位)緩存區將滿,馬上速度傳輸
    • RST(reset重置位)鏈接斷了從新鏈接
    • URG(urgent緊急位)緊急信號
    • ACK(acknowlegement確認)爲1就表示確認號
    • SYN(synchronous創建聯機)同步序號位 TCP創建鏈接時將這個值設爲1
  4. 用戶數據存儲了應用層生成的HTTP報文

瞭解了這些,那麼開始講重點

2.2 TCP三次握手和四次揮手

三次握手

  1. 客戶端先發送一個帶SYN標誌的數據包給服務器端
  2. 服務器收到後,回傳一個帶有SYN/ACK標誌的數據包表示確認收到
  3. 客戶端再發送一個帶SYN/ACK標誌的數據包,表明握手結束

四次揮手

  1. 客戶端向服務器發出了FIN報文段
  2. 服務器收到後,回覆一個ACK應答
  3. 服務器也向客戶端發送一個FIN報文段,隨後關閉了服務器端的鏈接
  4. 客戶端收到以後,又向服務器回覆一個ACK應答,過了一段計時等待,客戶端也關閉了鏈接(計時等待是爲了確認服務器端已正常關閉)
四次揮手並非必然的,當服務器已經沒有內容發給客戶端了,就直接發送FIN報文段,這樣就變成了三次揮手。

3. HTTP請求/響應

3.1 HTTP報文

HTTP報文大體可分爲報文首部和報文主體兩塊,二者由空行(就至關於用了兩個換行符rnrn)來劃分。報文主體並非必定要有的。

3.1.1 請求報文

經常使用請求行方法:

  • GET 獲取資源
  • POST 向服務器端發送數據,傳輸實體主體
  • PUT 傳輸文件
  • HEAD 獲取報文首部
  • DELETE 刪除文件
  • OPTIONS 詢問支持的方法
  • TRACE 追蹤路徑

3.1.2 響應報文

說到響應報文,就必要談到狀態碼:

  • 2XX 成功

    • 200(OK) 客戶端發過來的數據被正常處理
    • 204(Not Content) 正常響應,沒有實體
    • 206(Partial Content) 範圍請求,返回部分數據,響應報文中由Content-Range指定實體內容
  • 3XX 重定向

    • 301(Moved Permanently) 永久重定向
    • 302(Found) 臨時重定向,規範要求方法名不變,可是都會改變
    • 303(See Other) 和302相似,但必須用GET方法
    • 304(Not Modified) 狀態未改變 配合(If-Match、If-Modified-Since、If-None_Match、If-Range、If-Unmodified-Since) (一般緩存會返回304狀態碼)
  • 4XX 客戶端錯誤

    • 400(Bad Request) 請求報文語法錯誤
    • 401 (unauthorized) 須要認證
    • 403(Forbidden) 服務器拒絕訪問對應的資源
    • 404(Not Found) 服務器上沒法找到資源
  • 5XX 服務器端錯誤

    • 500(Internal Server Error) 服務器故障
    • 503(Service Unavailable) 服務器處於超負載或正在停機維護

3.1.3 首部

通用首部

首部字段名 說明
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 資源的最後修改時間

3.2 實現客戶端訪問服務端

建立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到瀏覽器顯示頁面發生了什麼,不用多說,咱們再來看一下緩存

4. 緩存

4.1 緩存做用

  • 減小了冗餘的數據傳輸,節省了網費。
  • 減小了服務器的負擔, 大大提升了網站的性能
  • 加快了客戶端加載網頁的速度

4.2 緩存分類

強制緩存

強制緩存:說白了就是第一次請求數據時,服務端將數據和緩存規則一併返回,下一次請求時瀏覽器直接根據緩存規則進行判斷,有就直接讀緩存數據庫,不用鏈接服務器;沒有,再去找服務器。

對比緩存
  • 對比緩存,顧名思義,須要進行比較判斷是否可使用緩存。
  • 瀏覽器第一次請求數據時,服務器會將緩存標識與數據一塊兒返回給客戶端,客戶端將兩者備份至緩存數據庫中。
  • 再次請求數據時,客戶端將備份的緩存標識發送給服務器,服務器根據緩存標識進行判斷,判斷成功後,返回304狀態碼,通知* 客戶端比較成功,可使用緩存數據。

4.3 請求流程

第一次請求,此時沒有緩存

第二次請求

從上張圖咱們能夠看到,判斷緩存是否可用,有兩種方式

  • ETag是實體標籤的縮寫,根據實體內容生成的一段hash字符串,能夠標識資源的狀態。當資源發生改變時,ETag也隨之發生變化。ETag是Web服務端產生的,而後發給瀏覽器客戶端。
  • Last-Modified是此資源的最後修改時間,

    • 若是客戶端在請求到的資源中發現實體首部裏有Last-Modified聲明,再次請求就會在頭裏帶上if-Modified-Since字段
    • 服務端收到請求後發現if-Modified-Since字段則與被請求資源的最後修改時間進行對比

說了這麼多,不如直接來實現一下緩存

經過最後修改時間來判斷緩存是否可用

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》。本人水平有限,有不足之處,望你們指出改正。

相關文章
相關標籤/搜索