一屏長文,更深刻的瞭解HTTP協議。對於入門前端不久的同窗來講,可能學習前端,就從HTML,CSS,JS學起,而後再入手一個框架,但對於http的理解可能還僅在知道一些面試中關於http的考題或比較少在代碼層面去真正理解一些理論的知識,看完本篇但願你能對http有一個較爲深刻的理解,而且能在開發中對你有所幫助javascript
瀏覽器輸入URL後HTTP請求返回的完整過程
經典五層模型
html
後續小節咱們會涉及到的知識點就是應用層和傳輸層。前端
它旨在向用戶提供可靠的端到端的服務,數據傳輸過程可能涉及到分片分包等,以及傳輸過去如何組裝等,這個無需讓開發者來作,所以傳輸層向高層屏蔽了下層數據通訊的細節。正由於如此,理解傳輸層的細節可以讓咱們實現一個性能更高HTTP實現方式java
它幫咱們實現了http協議,爲應用層提供了不少服務,而且構建與TCP協議之上,屏蔽網絡傳輸相關細節node
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狀態碼和狀態碼含義等數據庫
用來定義對於資源的操做json
2XX 成功
3XX 重定向
4XX 客戶端錯誤
5XX 服務器錯誤
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'
瀏覽器就是最多見的客戶端,瀏覽器爲了保證數據傳輸的安全性,具備同源策略,所謂同源是指:域名、協議、端口相同
同源策略又能夠分爲如下兩種:
瞭解了瀏覽器同源策略的做用,若是不一樣源發出請求,就會產生跨域。可是在實際開發中,咱們不少時候須要突破這樣的限制,方法有如下幾種(後面會有方法實踐):
跨域知識詳細可參考前端跨域整理
經過代碼來看下具體是怎麼樣的
建立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的終端)依然能收到請求,只是返回的內容在瀏覽器端沒有接收到,所以跨域並非發不出請求,只是返回的內容被瀏覽器攔截了而已
修改hello.html,js改成
fetch('http://192.168.0.106:8887/', { method: 'POST', headers: { 'Test-Cors': '123' } })
瀏覽器訪問localhost:8888,出現
緣由是什麼呢,且聽我慢慢道來
瀏覽器的請求在跨域的時候默認容許的方法爲
GET、HEAD、POST,其餘方法不容許,須要有預檢請求
其餘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'
去掉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
public、private、no-cache、no-store
指的緩存時間,最經常使用的就是max-age,單位是秒,指的就是緩存的有效期是多長時間
s-max-age這個是代理服務器的緩存時間,只在代理服務器生效
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
若是沒有設置緩存,每次請求都會從服務器獲取。須要驗證能夠自行測試下
緩存命中能夠查看這張圖
如今咱們並非真正須要驗證資源,而是爲了驗證瀏覽器是否會把驗證頭帶過來,所以咱們能夠隨便設個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
再發送請求,能夠看到在Request Headers中出現,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
http是不保存狀態的協議,所以咱們須要一個身份能來證實訪問服務器的是誰,這裏咱們用到的就是cookie和session
接下來經過代碼看下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自行驗證下
長鏈接指的是在一次請求完成後,是否要關閉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的使用目前還沒大面積
當咱們經過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服務,掌握它固然也不可或缺