Java 架构师眼中的 HTTP 协议
本文來自作者 張振華?在 GitChat 上分享 「Java 架構師眼中的 HTTP 協議」,「閱讀原文」查看交流實錄。
編輯 | 哈比
HTTP 協議的基本內容
· 什么是 HTTP 協議?
協議,是指計算機通信網絡中兩臺計算機之間進行通信所必須共同遵守有規則的文本格式。
一旦有了協議,就可以使很多公司分工起來,有些公司做 Server 端,如 Tomcat,而有些公司就可以做瀏覽器了。這樣大家只要一套約定,彼此的通訊就會相互兼容。
什么是 HTTP?
HTTP 是基于 TCP/IP 的應用層通信協議,它是客戶端和服務器之間相互通信的標準。它規定了如何在互聯網上請求和傳輸內容。
通過應用層協議,它只是一個規范了主機(客戶端和服務器)如何通信的抽象層,并且它本身依賴于 TCP/IP 來獲取客戶端和服務器之間的請求和響應。
默認的 TCP 端口是 80 端口,當然,使用其他端口也是可以的。然而,HTTPS 使用的端口是 443 端口。
· HTTP 協議的簡單歷史
根據上圖,我們可將 HTTP 協議的發展歷程分為五個階段。
第一階段,1996 年之前。
第一版的 HTTP 文檔是 1991 年提出來的 HTTP/0.9,其主要特點有:
(1)它僅有一個 GET 方法。
(2)沒有 header 數據塊。
(3)必須以 HTML 格式響應。
第二階段,HTTP/1.0 - 1996。
HTML 格式響應,HTTP/1.0 能夠處理其他的響應格式,例如:圖像、視頻文件、純文本或其他任何的內容類型(Content-Type 來區分)。
它增加了更多的方法(即 POST 和 HEAD),請求 / 響應的格式也發生了改變,請求和響應中均加入了 HTTP 頭信息。
響應數據還增加了狀態碼標識,還介紹了字符集的支持、多部分發送、權限、緩存、內容編碼等很多內容。
HTTP/1.0 的主要缺點之一是,你不能在每個連接中發送多個請求。
也就是說,每當客戶端要向服務器端請求東西時,它都會打開一個新的 TCP 連接,并且在這個單獨請求完成后,該連接就會被關閉。
每一次連接里面都包含了著名的三次握手協議。
于是有些 HTTP/1.0 的實現試圖通過引入一個新的頭信息 Connection: keep-alive,來解決這個問題。
第三個階段,HTTP/1.1 - 1999。
HTTP/1.0 發布之后,隨著 HTTP 開始普及之后,它的缺點也開始展現。
時隔三年,HTTP/1.1 便在 1999 年問世,它在之前的基礎上做了很多的改進。主要內容包含:
新增的 HTTP 方法有 PUT、PATCH、HEAD、OPTIONS、DELETE。
主機名標識。在 HTTP/1.0 中,Host 頭信息不是必須項,但 HTTP/1.1 中要求必須要有 Host 頭信息。
持久性連接。正如前面所說,在 HTTP/1.0 中每個連接只有一個請求,且在這個請求完成后該連接就會被關閉,從而會導致嚴重的性能下降及延遲問題。HTTP/1.1 引入了對持久性連接的支持,例如:默認情況下連接不會被關閉,在多個連續的請求下它會保存連接的打開狀態。想要關閉這些連接,需要將 Connection: close 加入到請求的頭信息中。客戶端通常會在最后一次請求中發送這個頭信息用來安全的關閉連接。
管道機制。HTTP/1.1 也引入了對管道機制的支持,客戶端可以向服務器發送多個請求,而無需等待來自同一連接上的服務器響應,并且當收到請求時服務器必須以相同的順序來響應。但你可能會問客戶端是怎么知道第一個響應下載完成和下一個響應內容開始的?要解決這個問題,必須要有 Content-Length 頭信息,客戶端可以用它來確定響應結束,然后開始等待下一個響應。
第四個階段,SPDY - 2009。
Google 走在前面,它開始試驗一種可替換的協議來減少網頁的延遲,使得網頁加載更快、提升 Web 安全性。
2009 年,他們稱這種協議為 SPDY。SPDY 的功能包含多路復用、壓縮、優先級、安全等。
2015 年,谷歌不想存在兩個相互競爭的標準,因此他們決定把它合并到 HTTP 中成為 HTTP/2,同時放棄 SPDY。
第五個階段,HTTP/2 - 2015。
HTTP/2 是專為低延遲傳輸的內容而設計。關鍵特征或與 HTTP / 1.1 舊版本的差異如下。
(1)二進制協議。
HTTP/2 傾向于使用二進制協議來減少 HTTP/1.x 中的延遲。二進制協議更容易解析,而不具有像 HTTP/1.x 中那樣對人的可讀性。
HTTP/2 中的數據塊是幀和流。
HTTP 消息是由一個或多個幀組成的。有一個叫做 HEADERS 的幀存放元數據,真正的數據是放在 DATA 幀中的。
幀類型定義在 the HTTP/2 specs(HTTP/2 規范),如 HEADERS、DATA、RST_STREAM、SETTINGS、PRIORITY 等。
每個 HTTP/2 請求和響應都被賦予一個唯一的流 ID 且放入了幀中。幀就是一塊二進制數據。
一系列幀的集合就稱為流。每個幀都有一個流 id,用于標識它屬于哪一個流,每一個幀都有相同的頭。
同時,除了流標識是唯一的,值得一提的是,客戶端發起的任何請求都使用奇數和服務器的響應是偶數的流 id。
除了 HEADERS 和 DATA, 另外一個值得說一說幀類型是 RST_STREAM,它是一個特殊的幀類型,用于中止流,如客戶端發送這兒幀來告訴服務器我不再需要這個流了。
在 HTTP/1.1 中只有一種方式來實現服務器停止發送響應給客戶端,那就是關閉連接引起延遲增加,因為后續的請求就需要打開一個新的連接。
在 HTTP/2 中,客戶端可以使用 RST_FRAME 來停止接收指定的流而不關閉連接且還可以在此連接中接收其它流。
(2)多路復用。
由于 HTTP/2 現在是一個二進制協議,且是使用幀和流來實現請求和響應。
一旦 TCP 連接打開了,所有的流都通過這一連接來進行異步的發送而不需要打開額外的連接。
反過來,服務器的響應也是異步的方式,如響應是無序的、客戶端使用流 id 來標識屬于流的包。
這就解決了存在于 HTTP/1.x 中 head-of-line 阻塞問題,如客戶端將不必耗時等待請求,而其他請求將被處理。如下圖所示:
(3)HPACK 頭部壓縮。
它是一個單獨的用于明確優化發送 Header RFC 的一部分。
它的本質是,當我們同一個客戶端不斷的訪問服務器時,在 header 中發送很多冗余的數據,有時 cookie 就增大 header,且消耗帶寬和增加了延遲。
為了解決這個問題, HTTP/2 引入了頭部壓縮。與請求和響應不同,header 不是使用 gzip 或 compress 等壓縮格式,它有不同的機制。
它使用了霍夫曼編碼和在客戶端和服務器維護的頭部表來消除重復的 headers(如 User Agent),在后續的請求中就只使用頭部表中引用。
它與 HTTP/1.1 中的一樣,不過增加了偽 header,如 :method、:scheme、:host 和:path。
(4)服務器推送。
在服務器端,Server Push 是 HTTTP/2 的另外一個重要功能。
我們知道,客戶端是通過請求來獲取資源的,它可以通過推送資源給客戶端而不需客戶端主動請求。
例如,瀏覽器載入了一個頁面,瀏覽器解析頁面時發現了需要從服務器端載入的內容,接著它就發送一個請求來獲取這些內容。
Server Push 允許服務器推送數據來減少客戶端請求。
它是如何實現的呢,服務器在一個新的流中發送一個特殊的幀 PUSH_PROMISE,來通知客戶端:“嘿,我要把這個資源發給你 ! 你就不要請求了。”
(5)請求優先級。
客戶端可以在一個打開的流中在流的 HEADERS 幀中放入優先級信息。在任何時間,客戶端都可以發送一個 PRIORITY 的幀來改變流的優先級。
如果沒有優先級信息,服務器就會異步的處理請求,比如無序處理。
如果流被賦予了優先級,它就會基于這個優先級來處理,由服務器決定需要多少資源來處理該請求。
(6)安全。
大家對 HTTP/2 是否強制使用安全連接(通過 TLS)進行了充分的討論。最后的決定是不強制使用。
然而,大多數廠商表示,他們將只支持基于 TLS 的 HTTP/2。所以,盡管 HTTP/2 規范不需要加密,但它已經成為默認的強制執行的。
在這種情況下,基于 TLS 實現的 HTTP/2 需要的 TLS 版本最低要求是 1.2。 因此必須有最低限度的密鑰長度、臨時密鑰等。
通過開發者工具我們看一下 Google 的請求協議。
而我們大多數的網站的協議的版本都是 HTTP 1.1。
HTTP 協議的具體內容
而我們平時老生常談的 HTTP 的協議大都是指的是 HTTP 1.1 協議的內容,接下去我們一起看一下 HTTP 1.1 協議的結構。如下圖所示。
接下來,我將通過四部分大概介紹一下 HTTP 協議的基本內容。
1.URL & URI
schema://host[:port#]/path/.../[;url-params][?query-string][#anchor]
URL(Uniform Resource Locator)主要包括以下幾部分。
scheme:指定低層使用的協議,一般是 HTTP,如果強調安全的話可以是 HTTPS。
host:HTTP 服務器的 IP 地址或者域名。
port:HTTP 服務器的默認端口是 80,這種情況下端口號可以省略。如果使用了別的端口,必須指明。
path:訪問資源的路徑。
url-params:URL 的參數。
query-string:發送給 HTTP 服務器的數據。
anchor:錨。
URI,在 Java 的 Servlet 中指的是 resource path 部分。
2. 請求方法 Method
主要包括以下幾種請求方法。
GET:向指定的資源發出 “顯示” 請求。使用 GET 方法應該只用在讀取數據,而不應當被用于產生 “副作用” 的操作中,例如在 Web Application 中。其中一個原因是 GET 可能會被網絡蜘蛛等隨意訪問。
POST:向指定資源提交數據,請求服務器進行處理(例如提交表單或者上傳文件)。數據被包含在請求本文中。這個請求可能會創建新的資源或修改現有資源,或二者皆有。
PUT:向指定資源位置上傳其最新內容。
DELETE:請求服務器刪除 Request-URI 所標識的資源。
OPTIONS:這個方法可使服務器傳回該資源所支持的所有 HTTP 請求方法。用 “*” 來代替資源名稱,向 Web 服務器發送 OPTIONS 請求,可以測試服務器功能是否正常運作。
HEAD:與 GET 方法一樣,都是向服務器發出指定資源的請求。只不過服務器將不傳回資源的本文部分。它的好處在于,使用這個方法可以在不必傳輸全部內容的情況下,就可以獲取其中 “關于該資源的信息”(元信息或稱元數據)。
TRACE:回顯服務器收到的請求,主要用于測試或診斷。
CONNECT:HTTP/1.1 協議中預留給能夠將連接改為渠道方式的代理服務器。通常用于 SSL 加密服務器的鏈接(經由非加密的 HTTP 代理服務器)。
Method 名稱是區分大小寫的。
當某個請求所針對的資源不支持對應的請求方法的時候,服務器應當返回狀態碼 405(Method Not Allowed),當服務器不認識或者不支持對應的請求方法的時候,應當返回狀態碼 501(Not Implemented)。
3.HTTP 之狀態碼
狀態代碼有三位數字組成,第一個數字定義了響應的類別,共分五種類別:
1xx:指示信息—表示請求已接收,繼續處理。
2xx:成功—表示請求已被成功接收、理解、接受。
3xx:重定向—要完成請求必須進行更進一步的操作。
4xx:客戶端錯誤—請求有語法錯誤或請求無法實現。
5xx:服務器端錯誤—服務器未能實現合法的請求。
常見狀態碼有:
200 OK ? ? ? ? ? ? ? ? ? ? ? ?// 客戶端請求成功 400 Bad Request ? ? ? ? ? ? ? // 客戶端請求有語法錯誤,不能被服務器所理解 401 Unauthorized ? ? ? ? ? ? ?// 請求未經授權,這個狀態代碼必須和 WWW-Authenticate 報頭域一起使用 403 Forbidden ? ? ? ? ? ? ? ? // 服務器收到請求,但是拒絕提供服務 404 Not Found ? ? ? ? ? ? ? ? // 請求資源不存在,eg:輸入了錯誤的 URL 500 Internal Server Error ? ? // 服務器發生不可預期的錯誤 503 Server Unavailable ? ? ? ?// 服務器當前不能處理客戶端的請求,一段時間后可能恢復正常
4. 請求體 & 響應體
請求體 & 響應體,這個沒有特殊規定,需要配合不同的 Content-Type 來使用。
唯一需要注意的是 multipart/form-data、application/x-www-from-urlencoded、raw、binary 的區別。
(1)multipart/form-data
它將表單的數據組織成 Key-Value 形式,用分隔符 boundary(boundary 可任意設置)處理成一條消息。
由于有 boundary 隔離,所以當即上傳文件,又有參數的時候,必須要用這種 ?content-type 類型。如下圖所示:
(2)x-www-form-urlencoded
即 application/x-www-from-urlencoded,將表單內的數據轉換為 Key-Value。這種和 Get 方法把參數放在 URL 后面一樣的想過,這種不能文件上傳。
(3)raw
可以上傳任意格式的 “文本”,可以上傳 Text、JSON、XML、HTML 等。
(4)binary
即 Content-Type:application/octet-stream,只可以上傳二進制數據流,通常用來上傳文件。由于沒有鍵值,所以一次只能上傳一個文件。
(5)Header
HTTP 消息的 Headers 共分為三種,分別是 General Headers、Entity Headers、Request/Response Headers。
General Headers
我把被 Request 和 Response 共享的 Headers 成為 General Headers,具體有:
general-header = Cache-Control| Connection
? ? ? ? ? ? ? | Date
? ? ? ? ? ? ? | Pragma
? ? ? ? ? ? ? | Trailer
? ? ? ? ? ? ? | Transfer-Encoding
? ? ? ? ? ? ? | Upgrade
? ? ? ? ? ? ? | Via
? ? ? ? ? ? ? | Warning
其中,Cache-Control 指定請求和響應遵循的緩存機制;
Connection 允許客戶端和服務器指定與請求 / 響應連接有關的選項;
Date 提供日期和時間標志,說明報文是什么時間創建的;
Pragma 頭域用來包含實現特定的指令,最常用的是 Pragma:no-cache;
Trailer,如果報文采用了分塊傳輸編碼 (chunked transfer encoding) 方式,就可以用這個首部列出位于報文拖掛(trailer)部分的首部集合;
Transfer-Encoding 告知接收端為了保證報文的可靠傳輸,對報文采用了什么編碼方式;
Upgrade 給出了發送端可能想要 “升級” 使用的新版本和協議;
Via 顯示了報文經過的中間節點(代理,網嘎 un)。
Entity Headers
Entity Headers 主要用來描述消息體(message body)的一些元信息,具體有:
entity-header ?= Allow
? ? ? ? ? ? ? | Content-Encoding
? ? ? ? ? ? ? | Content-Language
? ? ? ? ? ? ? | Content-Length
? ? ? ? ? ? ? | Content-Location
? ? ? ? ? ? ? | Content-MD5
? ? ? ? ? ? ? | Content-Range
? ? ? ? ? ? ? | Content-Type
? ? ? ? ? ? ? | Expires
? ? ? ? ? ? ? | Last-Modified
其中,以 Content 為前綴的 Headers 主要描述了消息體的結構、大小、編碼等信息,Expires 描述了 Entity 的過期時間,Last-Modified 描述了消息的最后修改時間。
Request/Response Headers
Request-Line 是 Request 消息體的第一部分,其具體定義如下:
Request-Line = Method SP URI SP HTTP-Version CRLF
Method = "OPTIONS"| "HEAD"
? ? ? | "GET"
? ? ? | "POST"
? ? ? | "PUT"
? ? ? | "DELETE"
? ? ? | "TRACE"
其中 SP 代表字段的分隔符,HTTP-Version 一般就是 “http/1.1”,后面緊接著是一個換行。
在 Request-Line 后面緊跟著的就是 Headers。我們在上面已經介紹了 General Headers 和 Entity Headers,下面便是 Request Headers 的定義。
request-header = Accept
? ? ? ? ? ? ?| Accept-Charset
? ? ? ? ? ? ?| Accept-Encoding
? ? ? ? ? ? ?| Accept-Language
? ? ? ? ? ? ?| Authorization
? ? ? ? ? ? ?| Expect
? ? ? ? ? ? ?| From
? ? ? ? ? ? ?| Host
? ? ? ? ? ? ?| If-Match
? ? ? ? ? ? ?| If-Modified-Since
? ? ? ? ? ? ?| If-None-Match
? ? ? ? ? ? ?| If-Range
? ? ? ? ? ? ?| If-Unmodified-Since
? ? ? ? ? ? ?| Max-Forwards
? ? ? ? ? ? ?| Proxy-Authorization
? ? ? ? ? ? ?| Range
? ? ? ? ? ? ?| Referer
? ? ? ? ? ? ?| TE
? ? ? ? ? ? ?| User-Agent
Request Headers 扮演的角色其實就是一個 Request 消息的調節器。
需要注意的是若一個 Headers 名稱不在上面列表中,則默認當做 Entity Headers 的字段。
前綴為 Accept 的 Headers 定義了客戶端可以接受的媒介類型、語言和字符集等。
From、Host、Referer 和 User-Agent 詳細定義了客戶端如何初始化 Request。
前綴為 If 的 Headers 規定了服務器只能返回符合這些描述的資源,若不符合,則會返回 304 Not Modified。
Request Body,若 Request-Line 中的 Method 為 GET,請求中不包含消息體,若為 POST,則會包含消息體。
一個具體的 Request 消息實例,如下。
GET /articles/http-basics HTTP/1.1 Host: www.articles.com Connection: keep-alive Cache-Control: no-cache Pragma: no-cache Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Response 消息體
Response 消息格式和 Request 類似,也分為三部分,即 Response-Line、Response Headers、Response Body。
Response-Line 具體定義如下:
Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF HTTP-Version 字段值一般為 HTTP/1.1 Status-Code 前面已經討論過了 Reason-Phrase 是對 status code 的具體描述
一個最常見的 Response 響應為:
HTTP/1.1 200 OK
Response Headers 的定義如下。
response-header = Accept-Ranges
? ? ? ? ? ? ? | Age
? ? ? ? ? ? ? | ETag
? ? ? ? ? ? ? | Location
? ? ? ? ? ? ? | Proxy-Authenticate
? ? ? ? ? ? ? | Retry-After
? ? ? ? ? ? ? | Server
? ? ? ? ? ? ? | Vary
? ? ? ? ? ? ? | WWW-Authenticate
其中,Age 表示消息自 server 生成到現在的時長,單位是秒;ETag 是對 Entity 進行 MD5 hash 運算的值,用來檢測更改;Location 是被重定向的 URL;Server 表示服務器標識。
Http 更加詳細的介紹,請參考?http://www.runoob.com/http/http-status-codes.html。
架構師關注 HTTP 協議的重點
HTTP 協議內容其實也挺多的,架構師其實也應該有重點,哪些是我們必須重點關注的,心里要清楚。
· 采用哪個 HTTP 協議版本及其 Java 里面如何配置
1.Tomcat 的原始配置在 server.xml 里面。
<Connector port="8443"
protocol="org.apache.coyote.http11.Http11AprProtocol" ? ? ? ? ? ? ? maxThreads="150" SSLEnabled="true" > ? ? ? ? ? <UpgradeProtocol
className="org.apache.coyote.http2.Http2Protocol" /> ? ? ? ? ? <SSLHostConfig> ? ? ? ? ? ? ? <Certificate certificateKeyFile="conf/localhost-rsa-key.pem" ? ? ? ? ? ? ? ? ? ? ? ? ?certificateFile="conf/localhost-rsa-cert.pem" ? ? ? ? ? ? ? ? ? ? ? ? ?certificateChainFile="conf/localhost-rsa-chain.pem" ? ? ? ? ? ? ? ? ? ? ? ? ?type="RSA" /> ? ? ? ? ? </SSLHostConfig></Connector>
2.Spring boot2.0 項目的配置方法。
只需要在 properties 里面選擇如下配置即可。
server.ssl.enabled=true server.ssl.****// 等等 server.http2.enabled=true server.tomcat.protocol-header-https-value=https
需要注意的是一般 HTTP 2 的使用都要伴隨著證書。詳細的配置直接參考 ServerProperties 類里面的具體描述即可。
· HTTPS 配置注意事項
一般很少針對單個項目去設置的,一般都是通過開源的云如 ali、asw 里面的 SLB 配置 HTTP2 和證書,在網關那層統一做掉。
客戶端一般是各個瀏覽器或者 App 的瀏覽器內核庫來支持的。其實也很少需要開發來關心具體如何按照 HTTP2 來實現一些代碼邏輯。
· 緩存機制 HTTP 緩存
1. 如何緩存
降低網絡上發送 HTTP 請求的次數,這里采用 “過期” 機制。
HTTP 服務器通過兩種實體頭(Entity-Header)來實現 “過期” 機制:Expires 頭和 Cache-Control 頭的 max-age 子項。
Expires/Cache-Control 控制瀏覽器是否直接從瀏覽器緩存取數據還是重新發請求到服務器取數據。
只是 Cache-Control 比 Expires 可以控制的多一些,而且 Cache-Control 會重寫 Expires 的規則。
降低網絡上完整回復 HTTP 請求包的次數,這里采用 “確證” 機制。
HTTP 服務器通過兩種方式實現 “確證” 機制:ETag 以及 Last-Modified。
2. 相關的 Header
主要包括以下幾個。
Cache-Control
常用的值有:
(1)max-age(單位為 s)指定設置緩存最大的有效時間,定義的是時間長短。
當瀏覽器向服務器發送請求后,在 max-age 這段時間里瀏覽器就不會再向服務器發送請求了。
(2)s-maxage(單位為 s)同 max-age,只用于共享緩存(比如 CDN 緩存),也就是說 max-age 用于普通緩存,而 s-maxage 用于代理緩存。
如果存在 s-maxage,則會覆蓋掉 max-age 和 Expires header。
(3)public 指定響應會被緩存,并且在多用戶間共享。如果沒有指定 public 還是 private,則默認為 public。
(4)private 響應只作為私有的緩存,不能在用戶間共享。如果要求 HTTP 認證,響應會自動設置為 private。
(5)no-cache 指定不緩存響應,表明資源不進行緩存,比如,設置了 no-cache 之后并不代表瀏覽器不緩存,而是在緩存前要向服務器確認資源是否被更改。
因此有的時候只設置 no-cache 防止緩存還是不夠保險,還可以加上 private 指令,將過期時間設為過去的時間。
(6)no-store 表示絕對禁止緩存。一看就知道,如果用了這個命令,當然就是不會進行緩存啦!每次請求資源都要從服務器重新獲取。
(7)must-revalidate 指定如果頁面是過期的,則去服務器進行獲取。這個指令并不常用,就不做過多的討論了。
Expires
緩存過期時間,用來指定資源到期的時間,是服務器端的具體時間點。
也就是說,Expires=max-age + 請求時間,需要和 Last-modified 結合使用。
但在上面我們提到過 cache-control 的優先級更高。
Expires 是 Web 服務器響應消息頭字段,在響應 HTTP 請求時告訴瀏覽器在過期時間前瀏覽器可以直接從瀏覽器緩存取數據,而無需再次請求。
Last-modified
服務器端文件的最后修改時間,需要和 cache-control 共同使用,是檢查服務器端資源是否更新的一種方式。
當瀏覽器再次進行請求時,會向服務器傳送 If-Modified-Since 報頭,詢問 Last-Modified 時間點之后資源是否被修改過。
如果沒有修改,則返回碼為 304,使用緩存;
如果修改過,則再次去服務器請求資源,返回碼和首次請求相同為 200,資源為服務器最新資源。
Etag
根據實體內容生成一段 hash 字符串,標識資源的狀態,由服務端產生。瀏覽器會將這串字符串傳回服務器,驗證資源是否已經修改。
為什么要使用 Etag 呢 ?Etag 主要為了解決 Last-Modified 無法解決的一些問題。
一些文件也許會周期性的更改,但是它的內容并不改變(僅僅改變的修改時間),這個時候我們并不希望客戶端認為這個文件被修改了,而重新 Get。
某些文件修改非常頻繁,比如在秒以下的時間內進行修改(比方說 1s 內修改了 N 次),If-Modified-Since 能檢查到的粒度是 s 級的,這種修改無法判斷(或者說 UNIX 記錄 MTIME 只能精確到秒)。
某些服務器不能精確的得到文件的最后修改時間。
緩存過程如下圖所示:
· Session 與 Cookie 必知必會
很好的解決了 HTTP 通訊中狀態問題,但其本身也存在一些問題,比如:
客戶端存儲,可能會被修改或刪除。
發送請求時,Cookie 會被一起發送到服務器,當 Cookie 數據量較大時也會帶來額外的請求數據量。
客戶端對 Cookie 數量及大小有一定的限制,Session 解決了 Cookie 的一些缺點。Session 同樣是為了記錄用戶狀態,對于每個用戶來說都會有相應的一個狀態值保存在服務器中,而只在客戶端記錄一個 sessionID 用于區分是哪個用戶的 Session。
與 Cookie 相比,Session 有一定的優勢,如:
Session 值存儲在服務器,相對來說更安全。
客戶端發送給服務器的只有一個 sessionID,數據量更小。Session 同樣需要在客戶端存儲一個 sessionID。可以這個值存儲在 Cookie,每次發送請求時通過 Cookie 請求頭將其發送到服務器;也可以不使用 Cookie,而將 sessionID 作為一個額外的請求參數,通過 URL 或請求體發送到服務器。
基于 Cookie 實現 Session 的實現原理如下圖:
由上可見,基于 Cookie 實現 Session 時,其本質上還是在客戶端保存一個 Cookie 值。
這個值就是 sessionID,sessionID 的名稱也可按需要設置,為保存安全,其值也可能會在服務器端做加密處理。
服務器在收到 sessionID 后,就可以對其解密及查找對應的用戶信息等。
· 協議格式如何統一(見文章后面內容)
Spring 對 HTTP 協議的支持
我為什么想提一下這個呢,我看到太多的開發者遇到 HTTP 協議都喜歡自定義變量,自定義類,其實完全沒有必要。且看下面的分析。
· Spring MVC Web
在 spring-web**.jar 里面我們可以找到如下幾個類:
需要我們重點關注的有 HttpStatus、MediaType 等。
Spring web bind 中對應的注解,如下圖所示:
上面是一些主要的(需要注意的是 @RequestParam、@PostMapping、@**Mapping 與我們上面的 HttpMethod 相對應。
而里面還有 Headers、Consumes 和 produces 等參數來確定一些 Mapping 的條件),下面的可以根據需要查看源碼里面有哪些注解。
· Spring Data Rest
Spring Data Rest 是基于 Spring Data repositories,分析實體之間的關系。為我們生成 Hypermedia API(HATEOAS) 風格的 Http Restful API 接口。
Spring Data REST 通過構建在 Spring Data repositories 之上,自動將其導出為 REST 資源的 api,減少了大量重復代碼和無聊的樣板代碼。
它利用超媒體來允許客戶端查找存儲庫暴露的功能,并將這些資源自動集成到相關的超媒體功能中。
Spring Data REST 本身就是一個 Spring MVC 應用程序,它的設計方式應該是盡可能少的集成到現有的 Spring MVC 應用程序中。
現有的(或將來的)服務層可以與 Spring Data REST 一起運行,只有較小的考慮。
· Spring RestTemplate 的實際使用
Spring RestTemplate 是 Spring 提供的用于訪問 Rest 服務的客戶端,RestTemplate 提供了多種便捷訪問遠程 HTTP 服務的方法,能夠大大提高客戶端的編寫效率。
簡單例子,如下。
RestTemplate restTemplate = new RestTemplate();
String fooResourceUrl = "http://localhost:8080/spring-rest/foos";
ResponseEntity<String> response =
restTemplate.getForEntity(fooResourceUrl + "/1", String.class);
assertThat(response.getStatusCode(), equalTo(HttpStatus.OK));
更詳盡例子,如下。
/** * 發送一個 get 請求,并接受封裝成 map */ @Test public void restTemplateMap() {RestTemplate restTemplate = new RestTemplate();Map map=restTemplate.getForObject("https://api.weixin.qq.com/cgi-bin/getcallbackip?access_token=ACCESS_TOKEN",Map.class);System.out.println(map.get("errmsg")); }/** * 發送一個 get 請求,并接受封裝成 string */ @Test public void restTemplateString() {RestTemplate restTemplate = new RestTemplate();String str=restTemplate.getForObject("https://api.weixin.qq.com/cgi-bin/getcallbackip?access_token=ACCESS_TOKEN",String.class);System.out.println(str); } /** * 添加消息頭 */ @Test public void httpHeaders() {final String uri = "https://api.weixin.qq.com/cgi-bin/getcallbackip?access_token=ACCESS_TOKEN";RestTemplate restTemplate = new RestTemplate();HttpHeaders headers = new HttpHeaders();headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));HttpEntity<String> entity = new HttpEntity<String>("parameters", headers);ResponseEntity<String> result = restTemplate.exchange(uri, HttpMethod.GET, entity, String.class);System.out.println(result); }
我們在實際工作中會覆蓋默認的 RestTemplate。
/*** 替代默認的 SimpleClientHttpRequestFactory* 設置超時時間重試次數* 還可以設置一些攔截器以便監控** @return*/ @Bean public RestTemplate restTemplate() {// 生成一個設置了連接超時時間、請求超時時間、異常重試次數 3 次 ? ?RequestConfig config = RequestConfig.custom().setConnectionRequestTimeout(10000).setConnectTimeout(10000).setSocketTimeout(30000).build(); // 實際工作中,這個地方還會加上 filter 來抓取每次 restTemplate 的日志信息。 ? ?HttpClientBuilder builder = HttpClientBuilder.create().setDefaultRequestConfig(config).setRetryHandler(new DefaultHttpRequestRetryHandler(3, false)); ? ?HttpClient httpClient = builder.build(); ? ?ClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient); ? ?RestTemplate restTemplate = new RestTemplate(requestFactory); ? ?return restTemplate; }
使用的地方會變成如下即可。
@Autowired public RestTemplate restTemplate;
· Spring Data Jpa
隨著使用 Spring Data Jpa 的人越來越多,它里面也對 Spring Web 做了很好的支持。
我們可以重點看一下 Pageable 和 Page,以及 PageImpl 和 PageRequest 對分頁和排序做了很好的封裝,及其返回的 JSON 格式也做了很好的約定。
· Spring Cloud 中的知識點
Spring Cloud 的微服務管理都是基于 HTTP 協議的 Rest 風格的 API 來管理的,所以我們詳細了解 HTTP 協議還是非常有必要的。
JSON API
我們都知道了約定的好處。如果你和你的團隊曾經爭論過使用什么方式構建合理 JSON 響應格式, 那么 JSON API 就是你的 anti-bikeshedding 武器。
通過遵循共同的約定,可以提高開發效率,利用更普遍的工具,可以是你更加專注于開發重點:你的程序。
點擊?http://jsonapi.org 訪問 JSON API 官方。
(1)JSON API 介紹
JSON API 是數據交互規范,用以定義客戶端如何獲取與修改資源,以及服務器如何響應對應請求。
JSON API 設計用來最小化請求的數量,以及客戶端與服務器間傳輸的數據量。在高效實現的同時,無需犧牲可讀性、靈活性和可發現性。
JSON API 需要使用 JSON API 媒體類型(application/vnd.api+json)進行數據交互。
JSON API 服務器支持通過 GET 方法獲取資源。而且必須獨立實現 HTTP POST,PUT 和 DELETE 方法的請求響應,以支持資源的創建、更新和刪除。
JSON API 服務器也可以選擇性支持 HTTP PATCH 方法 [RFC5789] 和 JSON Patch 格式 [RFC6902],進行資源修改。
JSON Patch 支持是可行的,因為理論上來說,JSON API 通過單一 JSON 文檔,反映域下的所有資源,并將 JSON 文檔作為資源操作介質。
在文檔頂層,依據資源類型分組。每個資源都通過文檔下的唯一路徑辨識。
(2)規則約定
文檔中的關鍵字 MUST、MUST NOT、REQUIRED、SHALL、SHALL NOT、SHOULD、 SHOULD NOT、RECOMMENDED、MAY 和 OPTIONAL。依據 RFC 2119 [RFC2119] 規范解釋。
(3)內容約定
客戶端職責,即客戶端必須在包含 Content-Type: application/vnd.api+json 頭并且不包含媒體類型參數的請求文檔中發送所有 JSON API 數據。
在 Accept 頭中包含 JSON API 媒體類型并且不包含媒體類型參數的客戶端必須在 Accept 頭中指定媒體類型至少一次。
客戶端必須忽略任何從響應文檔的 Content-Type 頭中獲取的 application/vnd.api+json 媒體類型參數。
服務器職責,即服務器必須在包含 Content-Type: application/vnd.api+json 頭并且不包含媒體類型參數的請求文檔中發送所有 JSON API 數據。
如果接收到一個用任何媒體類型參數指定 Content-Type: application/vnd.api+json 頭的請求,服務器必須返回一個 415 Unsupported Media Type 狀態碼響應。
如果接收到一個在 Accept 頭中包含任何 JSON API 媒體類型并且所有實體都以媒體類型參數更改的請求,服務器必須返回一個 406 Not Acceptable 狀態碼響應。
(4)文檔結構
我們將描述 JSON API 文檔結構,通過媒體類型?application/vnd.api+json 標示。
JSON API 文檔使用 JavaScript 對象(JSON)[RFC4627] 定義。
盡管同種媒體類型用以請求和響應文檔,但某些特性只適用于其中一種。差異在下面呈現。
除非另有說明,根據本規范定義的對象都不應該包含任何其他鍵。
客戶端和服務器實現必須忽略本規范未指定的鍵。
(5)Top Level
JSON 對象必須位于每個 JSON API 文檔的根級。這個對象定義文檔的 “top level”。
文檔必須包含以下至少一種 top-level 鍵。
data: 文檔的”primary data”。
errors: 錯誤對象列表。
meta: 包含非標準元信息的元對象。
data 鍵和 errors 鍵不能再一個文檔中同時存在。
文檔可能包含以下任何 top-level 鍵。
jsonapi: 描述服務器實現的對象。
links: 與 primary data 相關的鏈接對象。
include: 與 primary data 或其他資源相關的資源對象(included resources)列。
如果文檔不包含 top-level data 鍵,included 鍵也不應該出現。
文檔的 top-level 鏈接對象可能包含以下鍵。
self: 生成當前響應文檔的鏈接。
related: 當 primary data 代表資源關系時,表示相關資源鏈接。
Primary data 的分頁鏈接。
文檔中的 “primary data” 代表一個請求所要求的資源或資源集合。
Primary data 必須是以下列舉的一種。
如果請求要求單一資源,應該是一個單一資源對象,或一個單一資源標識符,或 null。
如果請求要求資源集合,應該是一個資源對象列表,或一個空列表 ([])。
例如,以下 primary data 表示一個單一資源對象。
{"data": {"type": "articles", ? ?"id": "1", ? ?"attributes": { ? ? ?// ... this article's attributes ? ?}, ? ?"relationships": { ? ? ?// ... this article's relationships ? ?}} }
以下 primary data 表示一個指向同樣資源的單一資源標識符。
{ ? ?"data": { ? ? ?"type": "articles", ? ? ?"id": "1" ? ?} }
即使只包含一個元素或為空,資源的一個邏輯集合也必須表示為一個列表。
資源對象,即 JSON API 文檔中的 “Resuorce objects”,代表資源。
一個資源對象必須至少包含以下 top-level 鍵。
id
`type’
例外,當資源對象來自客戶端并且代表一個將要在服務器創建的新資源時,id 鍵不是必須的。
此外,資源對象可能包含以下 top-level 鍵。
‘attribute’: 屬性對象代表資源的某些數據。
relationshiops: 關聯對象描述該資源與其他 JSON API 資源之間的關系。
links: 鏈接資源包含與資源相關的鏈接。
meta: 元數據資源包含與資源對象相關的非標準元信息,這些信息不能被作為屬性或關聯對象表示。
一篇文獻(即一個 “文獻” 類型的資源)在文檔中這樣表示:
{ ?"type": "articles","id": "1","attributes": {"title": "Rails is Omakase"},"relationships": {"author": {"links": {"self": "/articles/1/relationships/author","related": "/articles/1/author" ? ? ?}, ? ? ?"data": { "type": "people", "id": "9" }}} }
標識符,即每個資源對象包含一個 id 鍵和一個 type 鍵。id 鍵和 typ e 鍵的值必須是字符串類型。
對于每一個既定 API,每個資源對象的 type 和 id 對必須定義一個單獨且唯一的資源(由一個或多個但行為表現為一個服務器的服務器控制的 URI 集合構成一個 API)。
type 鍵用于描述共享相同屬性和關聯的資源對象。type 鍵的值必須與遵循鍵名稱相同的約束條件。
字段,即資源對象的屬性和關聯被統稱為 “fields”。
一個資源對象的所有字段必須與 type 和 id 在同一命名空間中。即一個資源不能擁有名字相同的屬性與關聯,也不能擁有被命名為 type 或 id 的屬性和關聯。
屬性,即 attribute,鍵的值必須是一個對象(一個 “attributes object”)。屬性對象的鍵(“attributes”)代表與資源對象中定義的與其有關的信息。
屬性可以包含任何合法 JSON 值。JSON 對象和列表涉及的復雜數據結構可以作為屬性的值。
但是一個組成或被包含于屬性中的對象不能包含 relationships 或 links 鍵,因為這些鍵為此規范未來的用途所預留。
雖然一些 has-one 關系的外鍵(例如 author_id)被在內部與其他將要在資源對象中表達的信息一起儲存,但是這些鍵不能作為屬性出現。
關聯,即 relationships,鍵的值必須是一個對象(“relationships object”)。
關聯對象(“relationships”)的鍵表示在資源對象中定義的與其相關的其他資源對象。
關聯可以是單對象關聯或多對象關聯。
一個 “relationship object” 必須包含以下至少一種鍵:
links: 一個鏈接對象至少包含以下一種鍵:
self: 指向關聯本身的鏈接(“relationship link”)。此鏈接允許客戶端直接修改關聯。例如,通過一個 articale 的關聯 URL 移除一個 author 將會解除一個人與 article 的關系,而不需要刪除這個 people 資源本身。獲取成功后,這個鏈接將返回一個相關資源之間的連接,將其作為 primary data(見獲取關聯)。
related: 相關資源鏈接。
data: 資源連接。
meta: 包含關于此關聯的非標準元信息的元對象。
更多介紹見 官方文檔。
· Yahoo elide 對 JSON API 的支持
點擊 http://elide.io/pages/guide/01-start.html,訪問 Yahoo elide 官方網站。
1. elide 介紹
elide 通過 Spring Data Jpa 的 Entity,加上自定義的 @Include(rootLevel = true) 注解,來完成 JSON API 標準的輸出。
使用方法如下圖所示:
效果如下:
生產環境中 HTTP 協議有哪些架構
· RestTemplate 的重試和監控
代碼如下。
@SpringBootApplication public class DubbingApiApplication {/** ? * 使用全局的 RestTemplate ? * 設置了連接超時時間、請求超時時間、異常重試次數 3 次 ? * 并且會記錄所有請求的詳細日志 ? * ? * @return ? */ ? @Bean ? public RestTemplate restTemplate() { ? ? ?RequestConfig config = RequestConfig.custom().setConnectionRequestTimeout(10000).setConnectTimeout(10000).setSocketTimeout(30000).build(); ? ? ?HttpClentBuilder builder = HttpClientBuilder.create().setDefaultRequestConfig(config).setRetryHandler(new DefaultHttpRequestRetryHandler(3, false)); ? ? ?HttpClient httpClient = builder.build(); ? ? ?ClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient); ? ? ?RestTemplate restTemplate = new RestTemplate(requestFactory); ? ? ?restTemplate.setInterceptors(Collections.singletonList(new LoggingRestTemplate())); ? ? ?restTemplate.setRequestFactory(new HttpComponentsAsyncClientHttpRequestFactory()); ? ? ?return restTemplate;}public static void main(String[] args) { ? ? ?SpringApplication.run(DubbingApiApplication.class, args);} }
· 請求格式和返回結果的格式約定。
(1)實現 ResponseBodyAdvice 對 controller 返回的 Result 進行統一的包裝,如下代碼。
public class ElideResponseBodyAdvice implements ResponseBodyAdvice{
? ? @Autowired
? ? private ElideProperties elideProperties;
? ? /**
? ? ?* 配置注解可以跳過去,類上,方法上都行
? ? ?*
? ? ?* @param returnType
? ? ?* @param converterType
? ? ?* @return
? ? ?*/
? ? ?@Override
? ? ?public boolean supports(MethodParameter returnType, Class converterType) {
? ? ? ? ?if (converterType != null && converterType.isAssignableFrom(StringHttpMessageConverter.class)) {
? ? ? ? ? ? ?return false;
? ? ? ? ?}
? ? ? ? ?ElideSkippable elideSkippable = returnType.getMethodAnnotation(ElideSkippable.class);
? ? ? ? ?if (elideSkippable == null) {
? ? ? ? ? ? ?elideSkippable = returnType.getDeclaringClass().getAnnotation(ElideSkippable.class);
? ? ? ? ?}
? ? ? ? ?return !(elideSkippable != null && elideSkippable.value());
? ? ?}
? ? ?@Override
? ? ?public Object beforeBodyWrite(
? ? ? Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
? ? ? ? ?HttpServletRequest httpServletRequest = ((
? ? ? ? ?ServletServerHttpRequest)
? ? ? ? ?request).getServletRequest();
? ? ? ? ?//only process elide persistable
? ? ? ? ?return
Result
ElideWrapHandler.process(httpServletRequest, body);
? ? ?}
}
(2)異常統一格式的處理,返回固定的 error 級別的格式結果,如下代碼。
/*** 框架級別的通用異常處理*/ @ControllerAdvice @Slf4j @Order(4) public class ExceptionAdvice {@ExceptionHandler({BindException.class}) ? @ResponseBody ? public JsonApiErrorDocument exception(BindException e, HttpServletResponse response) { ? ? ?log.warn(e.getMessage(), e); ? ? ?ErrorResponse errors = new ErrorResponse(Constant.ERROR_VALIDATION, response.getStatus(), e.getMessage()); ? ? ?errors.setMeta(e.getAllErrors()); ? ? ?return new JsonApiErrorDocument(errors);} }
· 對緩存 Etag 的支持。
針對 HTTP 里 Etag 的支持,也需要我們框架層面去支持 Etag 緩存,這里就不給大家貼代碼了,大家可以思考一下。
微服務中 HTTP 與 RPC 的權衡
· HTTP 與 RPC 比較
實際工作中建議兩者都用,API 對外,Ali Dubbo 的 RPC 對內部使用,這樣兩個的優點都能使用到。
總結:面試中起到的關鍵作用是什么?
如果面試中問到你這個問題,主要的考驗的點有:
思路是否清晰。
實戰解決那些問題如緩存可能會讓你說的非常細。
知識是否全面,及其是否針對一個問題了解的足夠多。
基本上面的東西如果你都能提到,面試官的印象基本上是非常好的,加分會加很多。
近期熱文
《這么糟糕的代碼,真的是我以前寫的嗎?》
《一條挨踢老狗的 2017 年終總結》
《OpenVPN 的穿墻遠程連接旅程》
《前端跨域問題各種解決方案》
《程序員跳槽時,如何高效地準備面試?》
「閱讀原文」看交流實錄,你想知道的都在這里
總結
以上是生活随笔為你收集整理的Java 架构师眼中的 HTTP 协议的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 中国第一个口罩
- 下一篇: 我爷爷都看的懂的《栈和队列》,学不会来打