HTTP最通俗的理解,不用擔憂記不住了

導語

一屏長文,更深刻的瞭解HTTP協議。對於入門前端不久的同窗來講,可能學習前端,就從HTML,CSS,JS學起,而後再入手一個框架,但對於http的理解可能還僅在知道一些面試中關於http的考題或比較少在代碼層面去真正理解一些理論的知識,看完本篇但願你能對http有一個較爲深刻的理解,而且能在開發中對你有所幫助javascript

進入HTTP

http經典圖

瀏覽器輸入URL後HTTP請求返回的完整過程

網絡協議分層

經典五層模型
模型圖html

後續小節咱們會涉及到的知識點就是應用層和傳輸層。前端

  • 物理層:主要做用就是定義物理設備如何傳輸數據
  • 數據鏈路層:在通訊的實體間創建數據鏈路鏈接
  • 網絡層:在節點之間傳輸建立邏輯鏈路

傳輸層

它旨在向用戶提供可靠的端到端的服務,數據傳輸過程可能涉及到分片分包等,以及傳輸過去如何組裝等,這個無需讓開發者來作,所以傳輸層向高層屏蔽了下層數據通訊的細節。正由於如此,理解傳輸層的細節可以讓咱們實現一個性能更高HTTP實現方式java

應用層

它幫咱們實現了http協議,爲應用層提供了不少服務,而且構建與TCP協議之上,屏蔽網絡傳輸相關細節node

http的三次握手

http只有請求和響應的概念,建立鏈接是屬於TCP的操做,而鏈接的請求和響應是在tcp鏈接之上的。這是新手很容搞混的一點。在http1.1中鏈接能夠保持,這樣的好處是由於http三次握手是有開銷的。http2.0中請求能夠在同一個tcp鏈接中併發,也是大大節省了創建鏈接的開銷。具體後續將詳講,如今說回http三次握手,以下圖
nginx

首先客戶端發送一個要建立鏈接的數據包請求到服務端,包含一個標誌位SYN=1和seq=Y。
而後服務端會開啓一個TCP的socket端口,返回一個標誌位SYN=1,確認位ACK=x+1和seq=y的數據包
最後客戶端再發送一個ACK=Y+1,Seq=Z的數據包到服務端 web

這就是HTTP的三次握手全過程,三次握手的緣由是防止服務端開啓一些無用鏈接,由於網絡鏈接是有延遲的,若是沒有第三次鏈接,因爲網絡延遲,客戶端關閉了鏈接,而服務端一直在等待客戶端請求發送過來,這就形成了資源浪費,有了三次握手,就能確認請求發送和響應請求沒有問題。面試

HTTP報文

HTTP報文格式圖

請求報文中首行包括一些請求方法 請求資源地址和http協議版本。
響應報文中首行包括協議版本、http狀態碼和狀態碼含義等數據庫

HTTP方法

用來定義對於資源的操做json

  • HTTP方法:GET, POST,HEAD,OPTIONS,PUT,DELETE,TRACE和CONNECT
  • GET: 一般用於請求服務器發送某些資源
  • HEAD: 請求資源的頭部信息, 而且這些頭部與 HTTP GET 方法請求時返回的一致. 該請求方法的一個使用場景是在下載一個大文件前先獲取其大小再決定是否要下載, 以此能夠節約帶寬資源
  • OPTIONS: 用於獲取目的資源所支持的通訊選項
  • POST: 發送數據給服務器
  • PUT: 用於新增資源或者使用請求中的有效負載替換目標資源的表現形式
  • DELETE: 用於刪除指定的資源
  • PATCH: 用於對資源進行部分修改
  • CONNECT: HTTP/1.1協議中預留給可以將鏈接改成管道方式的代理服務器
  • TRACE: 回顯服務器收到的請求,主要用於測試或診斷

參考:面試官(9):多是全網最全的http面試答案

Http Code碼

2XX 成功

  • 200 OK,表示從客戶端發來的請求在服務器端被正確處理
  • 201 Created 請求已經被實現,並且有一個新的資源已經依據請求的須要而創建
  • 202 Accepted 請求已接受,可是還沒執行,不保證完成請求
  • 204 No content,表示請求成功,但響應報文不含實體的主體部分
  • 206 Partial Content,進行範圍請求

3XX 重定向

  • 301 moved permanently,永久性重定向,表示資源已被分配了新的 URL
  • 302 found,臨時性重定向,表示資源臨時被分配了新的 URL
  • 303 see other,表示資源存在着另外一個 URL,應使用 GET 方法丁香獲取資源
  • 304 not modified,表示服務器容許訪問資源,但因發生請求未知足條件的狀況
  • 307 temporary redirect,臨時重定向,和302含義相同

4XX 客戶端錯誤

  • 400 bad request,請求報文存在語法錯誤
  • 401 unauthorized,表示發送的請求須要有經過 HTTP 認證的認證信息
  • 403 forbidden,表示對請求資源的訪問被服務器拒絕
  • 404 not found,表示在服務器上沒有找到請求的資源
  • 408 Request timeout, 客戶端請求超時
  • 409 Confict, 請求的資源可能引發衝突

5XX 服務器錯誤

  • 500 internal sever error,表示服務器端在執行請求時發生了錯誤
  • 501 Not Implemented 請求超出服務器能力範圍,例如服務器不支持當前請求所須要的某個功能,或者請求是服務器不支持的某個方法
  • 503 service unavailable,代表服務器暫時處於超負載或正在停機維護,沒法處理請求
  • 505 http version not supported 服務器不支持,或者拒絕支持在請求中使用的 HTTP 版本

經過node建立一個簡單的node服務

server.js

const http = require('http')

http.createServer(function(request, response) {
    console.log('request come',request.url)

    response.end('hello world')
}).listen(8888)

console.log('server.listening on 8888')

終端進入到server.js文件下,執行node server.js 瀏覽器輸入localhost:8888,便可看見'hello world'

HTTP特性總覽

瀏覽器就是最多見的客戶端,瀏覽器爲了保證數據傳輸的安全性,具備同源策略,所謂同源是指:域名、協議、端口相同

同源策略又能夠分爲如下兩種:

  • DOM同源策略:禁止對不一樣源頁面DOM進行操做。這裏主要場景就是iframe跨域的狀況,不一樣域名的iframe是限制互相訪問的
  • XMLHttpRequest同源策略: 靜止使用XHR對象向不一樣源的服務器發起HTTP請求

瞭解了瀏覽器同源策略的做用,若是不一樣源發出請求,就會產生跨域。可是在實際開發中,咱們不少時候須要突破這樣的限制,方法有如下幾種(後面會有方法實踐):

  • JSONP: 利用script的src標籤不受同源限制,動態建立script標籤
  • CORS: 服務端設置access-allow-origin
  • 經過window.name跨域
  • 經過document.domain
  • 經過Html5的postMessage

跨域知識詳細可參考前端跨域整理
經過代碼來看下具體是怎麼樣的

cors跨域

建立server.js

const http = require('http')
const fs = require('fs')

http.createServer(function (request, response) {
    console.log('request come', request.url)

    const html = fs.readFileSync('test.html','utf8')
    response.writeHead(200, {
        'Content-Type': 'text/html'
    })
    response.end(html)

}).listen(8888)

server.js同目錄下建立hello.html,js代碼以下(地址換成本身電腦ip地址)

var xhr = new XMLHttpRequest()

xhr.open('GET','http://0.0.0.0:8887')

xhr.send()

同目錄下建立server2.js

const http = require('http')

http.createServer(function (request, response) {
    console.log('request come',request.url)
    response.end('hello world')
}).listen(8887)

console.log('server listening on 8887')

分別啓動server.js和server2.js,並在瀏覽器輸入localhost:8888

跨域

解決方案:在server2.js中加入

response.writeHead(200, {
    'Access-Control-Allow-Origin': '*'
})

跨域請求成功
<font color='red'> 注意 </font>:當咱們沒有加跨域請求頭的時候,能夠發現服務端(也就是運行server2.js的終端)依然能收到請求,只是返回的內容在瀏覽器端沒有接收到,所以跨域並非發不出請求,只是返回的內容被瀏覽器攔截了而已

CORS跨域限制以及預檢請求驗證

修改hello.html,js改成

fetch('http://192.168.0.106:8887/', {
    method: 'POST',
    headers: {
        'Test-Cors': '123'
    }
})

瀏覽器訪問localhost:8888,出現

請求頭不容許

緣由是什麼呢,且聽我慢慢道來
瀏覽器的請求在跨域的時候默認容許的方法爲
GET、HEAD、POST,其餘方法不容許,須要有預檢請求

  • 容許的Content-Type爲
  • text/plain
  • multipart/form-data
  • application/x-www-form-urlencoded

其餘Type也須要預檢請求
其餘限制包括header 詳見[默認容許header](),XMLHttpRequestUpload對象均沒有註冊任何事件監聽器以及請求中沒有使用ReadableStream對象。後兩個實際接觸很少,能夠不深究
說回預檢請求,先看下圖

注意:新版chorme瀏覽器改了,在network裏面看不到了,換個瀏覽器

若是咱們須要這個請求頭,在server2.js中的response.writeHead裏面添加
'Access-Control-Allow-Headers': 'X-Test-Cors'
同理,若是須要添加容許的方法,能夠添加
'Access-Control-Allow-Headers': 'Delete,PUT'
若是咱們但願在某一段時間內發送的跨域請求再也不發送預檢請求,能夠在response.writeHead中設置
'Access-Control-Max-Age': '100'

JSONP跨域

去掉server.js中的請求頭,並修改hello.html中js爲
<script src="http://192.168.0.107:8887/"></script>
這就是一個簡單的jsonp跨域,具體的能夠參考上面的跨域文章

瀏覽器的緩存

爲了減小請求,加快頁面訪問速度。開發者能夠根據須要對資源進行緩存。分爲強緩存和協商緩存,經過http首部字段進行設置

強緩存

Expires是一個絕對時間,即服務器時間。瀏覽器檢查當前時間,若是還沒到失效時間就直接使用緩存
可是該方法存在一個問題:服務器時間與客戶端時間可能不一致。所以該字段已經不多使用

cache-control中的max-age保存一個相對時間。例如Cache-Control: max-age = 484200,表示瀏覽器收到文件,緩存在484200S內均有效。若是同時存在cache-control和Expires,瀏覽器老是優先使用cache-control

協商緩存

last-modified是第一次請求資源時,服務器返回的字段,表示最後一段更新的時間。下一次瀏覽器
請求資源時就發送if-modified-since字段。服務器用本地last-modified時間與if-modified-since
時間比較,若是不一致則認爲緩存已過時並返回新資源給瀏覽器;若是時間一致則發送304狀態碼,讓瀏覽器
繼續使用緩存

Etag 資源的實體標識(哈希字符串),當資源內容更新時,Etag會改變。服務器會判斷Etag是否發送變化
若是變化則返回新資源,不然返回304

接下來咱們詳細看下Cache-Control

  1. 可緩存性

public、private、no-cache、no-store

  • public指的是http返回的內容所通過的任何路徑(包括代理服務器和客戶端瀏覽器)當中均可以被緩存
  • private指的是隻有發起請求的瀏覽器才能夠緩存
  • no-cache能夠在本地緩存,能夠在代理服務器緩存,可是這個緩存要服務器驗證纔可使用
  • no-store 完全得禁用緩衝,本地和代理服務器都不緩衝,每次都從服務器獲取
  1. 到期

指的緩存時間,最經常使用的就是max-age,單位是秒,指的就是緩存的有效期是多長時間
s-max-age這個是代理服務器的緩存時間,只在代理服務器生效

  1. 從新驗證

must-revalidate若是設置的緩存已通過期了,必須去原服務端請求,而後從新驗證數據是否已通過期
proxy-revalidate應用於代理服務器緩存
理論說完了,接下來咱們經過實戰看看
修改test.html,js部分修改成
<script src="./script.js"></script>

修改server.js

const http = require('http')
const fs = require('fs')

http.createServer(function (request, response) {
    console.log('request come',request.url)
    if (request.url == '/') {
        const html = fs.readFileSync('test.html', 'utf8')
        response.writeHead(200, {
            'Content-Type': 'text/html'
        })
        response.end(html)
    }

    if (request.url == '/script.js') {
            response.writeHead(200, {
                'Content-Type': 'text/javascript',
                'Cache-Control':'max-age=2020',
               // 'Last-Modified': '2020',
                //'Etag': '20200217'
            })
        response.end('console.log("script loaded")')
    }
    
}).listen(8888)

console.log('server start on the 8888')

打開開發者工具,咱們能夠看到scripts第一次加載以後,再請求就會從緩存中獲取,看下圖黃色圈中部分,注意須要把紅色勾選去掉
緩存加載圖

再看下響應的header

響應header圖

若是沒有設置緩存,每次請求都會從服務器獲取。須要驗證能夠自行測試下

緩存命中能夠查看這張圖

緩存命中圖

協商緩存驗證頭(Last-Modified,Etag)

如今咱們並非真正須要驗證資源,而是爲了驗證瀏覽器是否會把驗證頭帶過來,所以咱們能夠隨便設個Last-Modified和Etag,在server.js中修改response.writeHead

response.writeHead(200, {
    'Content-Type': 'text/javascript',
    'Cache-Control':'max-age=2020, no-cache',
    'Last-Modified': '2020',
    'Etag': '20200217'
})

啓動服務,下圖是第一次請求,能夠看到響應頭裏面有Last-Modify和Etag

第一次請求帶有Last-modify和Etag

再發送請求,能夠看到在Request Headers中出現,if-Modified-since和if-None-Match

第二次請求帶有if-Modified-since和if-None-Match

到這裏尚未結束,當咱們驗證緩存完,若是尚未過時,咱們但願直接拿緩存,可是咱們再看下咱們的response

由圖發現response中仍是有資源返回,而且code碼是200,這是爲啥呢,緣由很簡單,咱們在服務端尚未對if-Modified-since和if-None-Match進行處理,咱們把server.jshttp.createServer修改成

http.createServer(function (request, response) {
    console.log('request come',request.url)
    if (request.url == '/') {
        const html = fs.readFileSync('test.html', 'utf8')
        response.writeHead(200, {
            'Content-Type': 'text/html'
        })
        response.end(html)
    }

    if (request.url == '/script.js') {
        const etag = request.headers['if-none-match']
        if (etag === '20200217') {
            response.writeHead(304, {
                'Content-Type': 'text/javascript',
                'Cache-Control': 'max-age=2020,no-cache',
                'Last-Modified': '2020',
                'Etag': '20200217'
            })
            response.end('')
        } else {
            response.writeHead(200, {
                'Content-Type': 'text/javascript',
                'Cache-Control': 'max-age=2020,no-cache',
                'Last-Modified': '2020',
                'Etag': '20200217'
            })
            response.end('console.log("script loaded twice")')
        }
    }
    
}).listen(8888)

無論是否須要傳資源,咱們都要在最後response.end,否則本次請求一直沒有結束。修改完以後,咱們能夠看到請求code碼變成了304,時間縮短了,可是在response中仍是有資源,這又是什麼狀況,這時候咱們確實成功驗證了緩存,並拿取的是緩存資源,在瀏覽器的response中,瀏覽器會自動把拿到的緩存資源顯示出來,並無在服務器獲取。若是須要驗證,能夠自行在第一個response.end中添加其餘內容,再看瀏覽器接口的response

剛纔讓瀏覽器去作協商緩存,是由於咱們設置了no-cahce,咱們把no-cache刪除,瀏覽器應該是直接拿緩存(由於咱們設置的max-age=2020),驗證以前,咱們得在剛纔打開的頁面去清楚瀏覽器的緩存,而後刪除代碼中的no-cache,重複刷新,均可以看到script.js 是from mermory cache。no-store也可再自行驗證下

最後再提一下關於last-modify和Etag,last-modify咱們能夠在把數據庫取出的時候,拿取一個時間,最爲數據的update time.Etag的話,數據取出的時候作個數據簽名,存入Etag

cookie和session

http是不保存狀態的協議,所以咱們須要一個身份能來證實訪問服務器的是誰,這裏咱們用到的就是cookie和session

  • cookie的特性:經過Set-Cookie設置、下次請求會自動帶上、鍵值對形式,能夠設置多個
  • cookie的屬性:max-age和expires設置過時時間、HttpOnly沒法經過document.cookie訪問

接下來經過代碼看下cookie,在server.js中修改response.writeHead

{
    'Content-Type': 'text/html',
    'Set-Cookie': ['id=123;max-age=2','time=2020']
}

啓動服務後,能夠在application中的cookie看到兩個cookie或者network中的接口中。id=123這個cookie設置了過時時間,過一下子再刷新能夠看到id=123這個cookie消失了

前面說過,cookie跨域不共享,可是若是我想一級域名下的二級域名共享cookie,這時候我能夠經過設置document.domain來實現,具體以下

{
    'Content-Type': 'text/html',
    'Set-Cookie': ['id=123;max-age=2','time=2020;domain=test.com']
}

修改後,可添加host自行驗證下

HTTP長鏈接

長鏈接指的是在一次請求完成後,是否要關閉TCP鏈接。若是TCP鏈接一直開着,會有必定資源消耗,可是若是還有請求,就能夠繼續在本次TCP鏈接上發送,這樣能夠不用再三次握手,節省了時間。實際狀況中,網站併發量比較大,所以是保持長鏈接的,而且長鏈接是能夠設置超時時間的,若是在這個時間裏都沒有發送請求了,那麼鏈接就會關閉

接下來咱們能夠分析下實際場景,以百度首頁爲例,打開開發者面板,而後network中,右擊name屬性,勾選Connection ID
咱們看到大部分鏈接都有複用,在http1.1中,一個域名下最大TCP鏈接數爲6個(Chorme),所以剛開始的時候會一下建立6個鏈接,後面的請求會複用這些鏈接。

經過代碼來驗證下這部份內容,首先建立一個test.html

<body>
    <img src="/test1.jpg" alt="">
    <img src="/test2.jpg" alt="">
    <img src="/test3.jpg" alt="">
    <img src="/test4.jpg" alt="">
    <img src="/test5.jpg" alt="">
    <img src="/test6.jpg" alt="">
    <img src="/test7.jpg" alt="">
    <img src="/test8.jpg" alt="">
</body>

新建server.js

const http = require('http')
const fs = require('fs')

http.createServer(function (request, response) {
    console.log('request come',request.url)
    const html = fs.readFileSync('test.html', 'utf8')
    const img = fs.readFileSync('timg.jpg')
    if (request.url === '/') {
        response.writeHead(200, {
            'Content-Type': 'text/html',
            // 'Connection': 'close'
        })
        response.end(html)
    } else {
        response.writeHead(200, {
            'Content-Type': 'image/jpg',
            // 'Connection': 'close'
        })
        response.end(img)
    }
    
}).listen(8888)

console.log('server start on the 8888')

啓動服務
加載時序圖

能夠看下Waterfall,網絡請求分時過程。若是須要關閉長鏈接,Connection的值能夠寫爲close
這裏再簡單提下http2.0如今使用信道複用技術,只須要建立一個TCP鏈接,全部同域下請求均可以併發。若是要使用http2.0,須要保證請求時https協議,而且後端須要作較大的改變,所以如今http2.0的使用目前還沒大面積

Redirect

當咱們經過url去訪問一個資源的時候,該資源已經再也不url指定的位置了,服務器應通知客戶端該資源如今所處的位置,瀏覽器再去請求該資源。
經過代碼來看下,新建一個server.js

const http = require('http')
const fs = require('fs')

http.createServer(function (request, response) {
    console.log('request comme', request.url)
    if (request.url === '/') {
        response.writeHead(302, {
            'Location': '/new'
        })
        response.end()
    }
    if (request.url === '/new') {
        response.writeHead(200, {
            'Content-Type': 'text/html'
        })
        response.end('<div>hello world</div>')
    }
}).listen(8888)

console.log('server listening on 8888')

此處測試是在同域的狀況下,因此只寫了一個路由,若是不相同,則把真正的地址替換/new.啓動服務,輸入localhost:8888以後,會直接跳轉到資源真正的位置,而且在network中也可查看發現,除了圖標,有兩個請求。

代碼中咱們寫的code碼是302,若是咱們改爲200,就會發現沒有辦法重定向。302是臨時重定向,301是永久重定向,前面咱們已經說過。若是咱們把上面的302code碼改爲301,咱們會在終端中發現,除了第一次,無論咱們後面再輸入localhost:8888多少次,終端打印請求都只有重定向後的請求,只是由於瀏覽器記住了原地址被永久重定向了,因此,不會向原路徑發起請求。在實際開發中,應當謹慎使用永久重定向,由於一旦永久重定向了,會在瀏覽器儘量長的時間保留定向後的資源路徑而不會請求原路徑

結束語

本次分享目的是經過代碼來把原來咱們知道的一些知識點能夠再深刻一些,梳理好Http知識的前因後果。但願能對一些小夥伴有所幫助,若是你們喜歡個人行文風格的話,我接下來將帶入web 服務器Nginx的一些實戰,在實際開發中咱們會用nginx作代理和一些cache,所以做爲一個http服務,掌握它固然也不可或缺

相關文章
相關標籤/搜索