Web前端開發工程師必須瞭解的HTTP知識

weixin_33686714發表於2018-10-11

前言

官方解釋,HTTP是一個應用層協議,由請求和響應構成,是一個標準的客戶端伺服器模型。HTTP是一個無狀態的協議。通俗來說,就是規定服務端和客戶端通訊的一種規則。更多的是基於瀏覽器環境下使用,那麼從你瀏覽器輸入地址開始到最終頁面的呈現,到底經過了哪些過程呢?廢話不多說,先貼一張圖,如下: Web前端開發工程師必須瞭解的HTTP知識

如上圖,就是http請求發起到返回的完證過程,本以為自己對http的瞭解還算可以,但是乍一看圖還是很蒙的,比如說如果讓你優化http過程,你該從何下手呢?我想大部分還是比較關心requestresponse,以及資料返回之後的DOMContentLoadload,至於http中的一些配置並不是很清楚,其實通過優化配置,同樣能夠加速網頁的開啟速度,因此,我大致總結一些關於http中會經常使用的配置項,以及這些使用一般會在哪些場景中使用到。

注意:上圖中的domContentLoadedEventEnd代表DOMContentLoaded事件完成的時間節點,也就是jQuery中的domready時間。load代表的是onload事件觸發和結束的時間節點。

HTTP之跨域相關內容

基本內容

這裡主要介紹JSONPCORS跨域,現實場景中,以上兩種使用居多,所以其他跨域方案不做詳細介紹。造成跨域的主要原因主要是瀏覽器本身的同源策略引起的。

JSONP能夠實現跨域主要是因為瀏覽器上允許標籤上通過src/href載入外鏈路徑,但是JSONP只支援GET請求,同時因為瀏覽器中url長度的限制,因此JSONP能傳輸的資料大小也有一定的限制。

CORS跨域能夠支援的所有ajax的方法,當然,目前是支援ie9+,低版本暫時不支援,隨時網際網路的發展,相信低版本的瀏覽器會逐漸被淘汰。在只用CORS只需要服務端能夠開啟允許跨域的頭設定即可,也就是Access-Control-Allow-Origin

跨域大致的流程圖如下:

Web前端開發工程師必須瞭解的HTTP知識

注意:JSONP中的資料限制並不是GET請求本身的限制,而是瀏覽器中url本身有長度限制,GET方法是沒有任何長度限制的;不管是JSONP還是CORS跨域,其實伺服器都可以接收來自客戶端的資料請求,並且也都成功返回了,只是瀏覽器本身有同源策略的限制,才會進一步判斷返回的資料是否符合瀏覽器的限制。

這裡有個題外話,Access-Control-Allow-Origin這個配置項預設支援配置單個域名或者*,為了安全起見,不建議配置*,那麼如何配置才能支援多個域名跨域呢?有一個簡易的方法可以解決,主要思路是通過服務端定義可支援的跨域域名集合,通過迴圈判斷當前請求是否支援即可,片段程式碼如下:

// 服務端程式碼,以下是node服務做測試
const http = require('http')
const allowDomains = [
    'http://www.a.com',
    'http://www.b.com',
    'http://www.c.com'
]
const server = http.createServer((req, res) => {
    let acao = ''
    for(let i = 0, l = allowDomains.length; i < l; i++) {
        if(allowDomains[i].indexOf(req.headers.host) > -1) {
            acao = allowDomains[i]
            break
        }
    }
    res.writeHead(200, 
        {
            'Access-Control-Allow-Origin': acao
        }
    )
    res.end('Hello World\n')
}).listen(3001)
console.log('server listen 3001')
複製程式碼

CORS跨域限制

  • 預設允許的方法有GETHEADPOST
  • 預設允許的Content-Type有text/plainmultipart/form-dataapplication/x-www-form-urlencoded
  • HTTP的頭資訊包含AcceptAccept-LanguageContent-LanguageLast-Event-ID

在請求包含以上內容的時候,其實就是簡單請求,在跨域的情況下,瀏覽器預設是直接通過的,其餘剩下的稱之為複雜請求,瀏覽器會預設傳送一次預請求作為驗證,如果驗證通過則代表請求成功。

因此需要對上圖增加限制的修改,最終如下:

Web前端開發工程師必須瞭解的HTTP知識

其實就是對於複雜請求做了一次校驗,大致可以這樣解釋,如果在傳送請求時,例如額外帶了headers的配置項,如果需要驗證通過就必須在服務端也要配置允許該headers的返回,這樣預請求的驗證才會通過。也可以通過程式碼做一下驗證,基本如下:

// 後端服務程式碼
const http = require('http')
const server = http.createServer((req, res) => {
    res.writeHead(200, 
        {
            'Access-Control-Allow-Origin': '*',
            'Access-Control-Allow-Headers': 'aa' // 通過設定了這個,才能使得預請求驗證通過
        }
    )
    res.end('Hello World\n')
}).listen(3001)
console.log('server listen 3001')

// 前端服務程式碼
const http = require('http')
const fs = require('fs')
const server = http.createServer((req, res) => {
const html = fs.readFileSync('index.html', 'utf8')
    res.writeHead(200, 
        {
            'Content-Type': 'text/html' 
        }
    )
    res.end(html)
}).listen(3000)
console.log('server listen 3000')

// index.html主要程式碼如下
fetch('http://localhost:3001', {
    method: 'post',
    headers: {
        aa:'123'
    }
}) 
複製程式碼

以上測試程式碼主要是在發起post請求的時候額外攜帶了一個headers引數,只有在服務端配置了允許該headers傳輸才能使得瀏覽器預請求驗證通過,反之則會失敗。大家可以根據以上測試程式碼在自己的本機測試就能明白了。

注意:通過設定Access-Control-Request-Method可以配置其他的方法,例如PUTDELETE等。

細心的同學可能會發現,根據以上程式碼的確可以通過預請求,但是如果再次重新整理網頁,會發現仍然還會存在預請求,對於第一次預請求已經通過了,為什麼同樣的請求還會再傳送一次呢?其實這裡可以做一個優化,減少預請求的傳送。

通過設定Access-Control-Max-Age來確定預請求的有效時間,只要在有效時間內,就不會再次傳送預請求了。

HTTP之Cache-Control

Cache-Control包含很多特性,其中no-cache這個配置項肯定最熟悉,官方解釋是在釋放快取副本之前,強制快取記憶體將請求提交給原始伺服器進行驗證,其實就是代表沒有快取。但是其實它依然有很多特性,經過資料查詢,大致分為以下幾類,

Web前端開發工程師必須瞭解的HTTP知識

介紹下一般常用的配置引數的文字解釋:

  • public代表http從請求到返回的整個路徑上的都可以被快取,例如客戶端瀏覽器,經過的代理伺服器等等。

  • private指發起的瀏覽器這一端才能進行快取,也就是代理伺服器是不能快取的。

  • no-cache 是否使用快取需要通過伺服器驗證後才能判斷。

  • max-age=<seconds> 最大能快取多少秒,過期之後,請求會再次傳送到服務端,對於返回的資料會再次被快取。

  • s-maxage=<seconds> 會覆蓋max-age或者Expires頭,應用於共享(如:代理伺服器)快取,並且在代理伺服器生效,客戶端不生效。

  • max-stale[=<seconds>] 表明客戶端願意接收一個已經過期的資源。即使max-age已經過期,同樣會使用本地的過期快取。

  • must-revalidate 如果max-age過期,必須通過服務端來驗證返回的資料是否真的過期。

  • proxy-revalidate 主要使用在代理伺服器端,對於過期的資料必須向服務端重新請求一遍。

  • no-store 本地和代理伺服器都不允許存快取。

  • no-transform 不得對資源進行轉換或轉變,主要使用在代理伺服器上。

具體每個配置的官方解釋參考具體說明

資源驗證

  • Last-Modified表明請求的資源上次的修改時間,主要配合If-Modified-Since(客戶端保留的資源上次的修改時間)進行使用,主要是在傳送請求的時候帶上。通過對比上次修改時間以驗證資源是否更新。
  • Etag資源的內容標識。(不唯一,通常為檔案的md5或者一段hash值,只要保證寫入和驗證時的方法一致即可),配合If-Match或者If-None-Match進行使用,對比資源的內容標識來判斷是否使用快取。

其實伺服器可以通過Etag來區分返回哪些資料,具體可以參考下面的示例:

// server.js
const http = require('http')
const fs = require('fs')
const server = http.createServer((req, res) => {
const url = req.url
const html = fs.readFileSync('index.html', 'utf8')
const etag = req.headers['if-none-match']
if(url === '/') {
res.writeHead(200, 
    {
    'Content-Type': 'text/html',
    'Cache-Control': 'max-age=2000, no-cache',
    'Last-Modified': '123',
    'Etag': '444'
    }
)
res.end(html)
}
if(url === '/aa.js') {
if(etag === '444') {
    res.writeHead(304, 
    {
        'Content-Type': 'text/javascript',
        'Cache-Control': 'max-age=2000, no-cache',
        'Last-Modified': '123',
        'Etag': '444'
    }
    )
    res.end('888888')
} else {
    res.writeHead(200, 
    {
        'Content-Type': 'text/javascript',
        'Cache-Control': 'max-age=2000, no-cache',
        'Last-Modified': '123',
        'Etag': '444'
    }
    )
    res.end('1111111111')
}
}
}).listen(3000)
console.log('server listen 3000')
複製程式碼
<!-- html檔案 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<script src="/aa.js"></script>
</body>
</html>
複製程式碼

第一次重新整理和第二次重新整理截圖如下: Web前端開發工程師必須瞭解的HTTP知識 Web前端開發工程師必須瞭解的HTTP知識 根據測試程式碼能得出,在服務端通過etag === '444'做了判斷,但是最終返回的依然是1111111111,這是由於服務端選取了第一次的快取資料作為返回。雖然發生了一次請求,但請求內容長度減少了,節省了頻寬。

總結

其實以上所有的概念基本可以用一個圖來展示,大致分為2個主要部分:

1、本地快取,其實也就是本地資源的快取。 2、伺服器快取,包含驗證快取和非驗證快取。 Web前端開發工程師必須瞭解的HTTP知識

HTTP之cookie

  • 通過Set-Cookie來設定cookie,下次請求的時候,會自動帶上之前設定的cookie(同域情況下),取值型別是String/Array。
  • 通過max-ageExpires設定過期時間。
  • 通過Secure來設定只能在https的時候傳送。
  • 通過HttpOnly來設定無法通過document.cookie訪問。
  • 通過domain=來設定該cookie是否在同域下共享。

以上內容基本能彙總成一張圖解釋,如下: Web前端開發工程師必須瞭解的HTTP知識

HTTP之keep-alive

目前開發web網頁,所有請求預設都是Connection: keep-alive,除非服務端手動去關閉配置(Connection: close),使用keep-alive可以複用之前的請求的通道,這樣減少tcp三次握手的時間(同域情況下才能生效)。

HTTP之資料協商

web服務請求會攜帶以下資訊內容:

  • Accept 想要的資料型別
  • Accept-Encoding 限制服務端資料的壓縮方式(gzip、deflate、br)
  • Accept-Language 服務端返回資料的語言型別
  • User-Agent瀏覽器頭資訊內容

服務端返回資料會攜帶以下資訊內容:

  • Content-type 返回的資料型別
  • Content-Encoding 對應的是 Accept-Encoding 代表資料壓縮型別
  • Content-Language 對應的是 Accept-Language 代表資料語言型別

當然,web請求也可以自定義Content-type來傳輸資料,一般在form表單中比較常用,例如上傳檔案,會指定Content-type:multipart/form-data,這樣服務端就能接收上傳的檔案資訊內容。

HTTP之重定向

瀏覽器能識別的重定向code碼有兩種,分別是301302,兩者在使用上會有一定的區別,大致如下:

  • 301重定向是永久重定向,使用者在訪問資源的時候,瀏覽器預設是從快取中獲取之前指定的跳轉資訊;
  • 302重定向可以隨時取消,也就是使用者在訪問資源的時候,每次都會經過服務端並且在服務端通過跳轉邏輯進行跳轉;

可以使用一段程式碼來描述以上的不同之處,基本程式碼如下:

// 302跳轉測試程式碼
const http = require('http')
const fs = require('fs')
const server = http.createServer((req, res) => {
const url = req.url
const html = fs.readFileSync('index.html', 'utf8')
console.log(url)
if(url === '/') {
res.writeHead(302, 
    {
        Location: '/wq'
    }
)
res.end('')
}
if(url === '/wq') {
res.writeHead(200, 
    {
        'Content-Type': 'text/html',
    }
)
res.end(html)
}
    
}).listen(3000)
console.log('server listen 3000')
複製程式碼

301測試程式碼跟上面基本一樣,只要將302改成301即可,根據以上程式碼測試,你會發現,每次在重新整理頁面的時候,如果是302跳轉,那麼console.log(url)每次都會列印/ 和 /wq,如果是301的話,只會列印/wq,這就說明,302的跳轉是從服務端指定跳轉的,而301的跳轉則是永久性的,除非清楚本地瀏覽器的快取,要麼無法改變。

HTTP之內容安全策略

配置內容安全策略涉及到新增Content-Security-Policy,HTTP頭部到一個頁面,並配置相應的值,以控制使用者代理(瀏覽器等)可以為該頁面獲取哪些資源。更多詳細參考具體說明

常用示例如下:

// 一個網站管理者想要所有內容均來自站點的同一個源 (不包括其子域名)
'Content-Security-Policy': default-src 'self'

// 一個網站管理者允許內容來自信任的域名及其子域名 (域名不必須與CSP設定所在的域名相同)
'Content-Security-Policy': default-src 'self' *.trusted.com

// 該伺服器僅允許通過HTTPS方式並僅從onlinebanking.jumbobank.com域名來訪問文件
'Content-Security-Policy': default-src https://onlinebanking.jumbobank.com

// 限制向百度請求
'Content-Security-Policy': connect-src http://baidu.com

// 通過設定report-uri來指定上報伺服器地址
'Content-Security-Policy': default-src 'self'; report-uri http://reportcollector.example.com/collector.cgi
複製程式碼

HTTP之HTTP2

HTTP/2是HTTP協議自1999年HTTP1.1釋出後的首個更新,主要基於SPDY協議。它由網際網路工程任務組(IETF)的Hypertext Transfer Protocol Bis(httpbis)工作小組進行開發。該組織於2014年12月將HTTP/2標準提議遞交至IESG進行討論,於2015年2月17日被批准。HTTP/2標準於2015年5月以RFC 7540正式發表。

大致總結有以下特性:

  • 二進位制分幀
  • 多路複用:同域名下所有通訊都在單個連線上完成;單個連線可以承載任意數量的雙向資料流;資料流以訊息的形式傳送,而訊息又由一個或多個幀組成,多個幀之間可以亂序傳送,因為根據幀首部的流標識可以重新組裝;
  • 伺服器推送:服務端可以在傳送頁面HTML時主動推送其它資源,而不用等到瀏覽器解析到相應位置,發起請求再響應。
  • 頭部壓縮:HTTP/2對訊息頭採用HPACK(專為http2頭部設計的壓縮格式)進行壓縮傳輸,能夠節省訊息頭佔用的網路的流量。而HTTP/1.x每次請求,都會攜帶大量冗餘頭資訊,浪費了很多頻寬資源。

貼一張簡圖,可以更好的體現,更多詳情可以參考Web前端開發工程師必須瞭解的HTTP知識

為了能更好的體驗實際場景中的效果,做了簡單的測試,使用express配合node開啟http2來測試具體效果,在這之前,需要生成一個SSL,生成程式碼如下:

// 直接在cmd中執行
openssl req -x509 -nodes -newkey rsa:2048 -keyout example.com.key -out example.com.crt
複製程式碼

編寫基礎的server.js程式碼,基本如下:

const port = 3000
const spdy = require('spdy')
const express = require('express')
const path = require('path')
const fs = require('fs')
const resolve = file => path.resolve(__dirname, file)
const app = express()

const serve = (path, cache) => express.static(resolve(path), {
  maxAge: cache ? 1000 * 60 * 60 * 24 * 30 : 0
})
app.use('/html', serve('./html', true))

app.get('/', (req, res) => {
    res.status(200)
    res.send('hello world')
})

app.get('/timg.jpeg', (req, res) => {
    const img = fs.readFileSync('/html/timg.jpeg')
    res.writeHead(200,
        {"Content-Type": 'image/jpeg'
    });
    res.send(img)
})

const options = {
    key: fs.readFileSync(__dirname + '/example.com.key'),
    cert:  fs.readFileSync(__dirname + '/example.com.crt')
}
spdy
    .createServer(options, app)
    .listen(port, (error) => {
    if (error) {
        console.error(error)
        return process.exit(1)
    } else {
        console.log('Listening on port: ' + port + '.')
    }
    })
複製程式碼

根目錄的靜態檔案定義fetch方法來請求100張圖片,這樣來測試請求的通道以及載入時長,基本程式碼如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>http2測試</title>
</head>
<body>
    <script>
        for(let i = 0; i < 100; i++) {
            fetch('//localhost:3000/html/timg.jpeg')
        }
    </script>
</body>
</html>
複製程式碼

按照以上程式碼執行,並且對比HTTP/1.1的效果如下(圖1是HTTP/1.1,圖2是HTTP2): Web前端開發工程師必須瞭解的HTTP知識 Web前端開發工程師必須瞭解的HTTP知識

從圖1和圖2就能看出,圖2中Connection ID只有一個,而圖1中Connection ID會隨著請求的增加而增加,每增加一個通道,就會建立一次TCP連結,也就是需要經過三次握手,並且還受限瀏覽器本身的併發數(其中谷歌瀏覽器的最大併發數是6個),所以才會出現等待的情況;從Size那一列能看出,HTTP2中的請求資料也會小(因為本身資料就小,所以不明顯),這樣能夠減少頻寬;從最終的Finsh時間能看出,同樣是100張圖片的請求,HTTP2耗時更少。

總結

以上內容主要是和HTTP相關的內容,由於比較簡單,就不提供測試程式碼壓縮包,基本從示例中直接複製貼上就能本地測試執行,如果有什麼不正確的地方,歡迎提Issues

相關文章