1. HTTP協議
1.1 HTTP請求
HTTP請求由請求頭、請求體兩部分組成,請求頭又分為請求行(request line)和普通的請求頭組成。通過瀏覽器的開發者工具,我們能查看請求和響應的詳情。 下面是一個HTTP請求發送的完整內容。
POST https://track.abc.com/v4/track HTTP/1.1
Host: track.abc.com
Connection: keep-alive
Content-Length: 2048
Pragma: no-cache
Cache-Control: no-cache
sec-ch-ua: "Google Chrome";v="87", " Not;A Brand";v="99", "Chromium";v="87"
Accept: application/json, text/javascript, */*;
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36
Content-Type: application/x-www-form-urlencoded;charset=UTF-8
Origin: https://class.abc.com
Sec-Fetch-Site: same-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://class.abc.com/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: HJ_UID=1fafb8b2-2a34-9cbe-e6ba-b8b4aabab4a1; _SREF_45=d=2020&t=1609310840140
按照上面的理論,我們可以將這一個完整的請求拆分為3部分,請求行、請求頭、請求體。
1. 請求行
POST https://track.abc.com/v4/track HTTP/1.1
POST
用于指定請求的方法,此外還可以有OPTOINS GET HEAD PUT DELETE TRACE CONNECT
等,更多詳細解釋可以參見RFC 2616。后面跟的https://track.abc.com/v4/track
是我們要訪問的資源URI。最后的HTTP/1.1
指定了HTTP協議的版本,HTTP/1.1
是目最常見的版本。
2. 請求頭
Host: track.abc.com
Connection: keep-alive
Content-Length: 2048
Pragma: no-cache
Cache-Control: no-cache
Accept: application/json, text/javascript, */*;
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36
Content-Type: application/x-www-form-urlencoded;charset=UTF-8
Referer: https://class.abc.com/
Accept-Encoding: gzip, deflate, br
Cookie: HJ_UID=1fafb8b2-2a34-9cbe-e6ba-b8b4aabab4a1; _SREF_45=
Header | 說明 |
---|---|
Host | 指定要訪問的域名。請求行的域名會在客戶端訪問時轉換為具體的IP。 Nginx是通過請求頭的Host 來將請求轉發到不同的域名配置的。 |
Connection | 用于指定連接保持的策略,這里的keep-alive 是期望服務器保持連接,在后續的請求中直接復用當前連接。 |
Pragma | 設置代理服務器(varnish)是否允許緩存,設置為no-cache 時代理即使發現有緩存也會回源上層服務器。 |
Cache-Control | 類是Pragma,不同的是這個請求體是HTTP 1.0時代的規范,請這個頭同時支持做為響應頭,但Pragma不能。 |
Accept | 指定支持的MIME-TYPE |
Content-Type | 指定請求內容類型以及編碼 |
Referer | 上一頁地址 |
Accept-Encoding | 支持的壓縮方式 |
Cookie | 符合當前請求的Cookie值 |
HTTP請求是無狀態的,Java服務端的Session都是通過Cookie保存會話標識來實現的。 Chrome新版本加了Cookie跨域的邏輯,SameSite設置會影響Cookie上報,對應邏輯查看SameSite對應的筆記。
3. 請求體
d=2020&t=1609310840140
請求體的格式可以通過Content-Type
指定,日常我們常用的有兩種格式:
- Form表單提交,上面我們給定的就是Form表單提交的數據格式,通過
&
符合切割字段,通過=
連接字段名和字段值。 - JSON請求體,一般我們在SpringBoot后端通過@ResponseBody接收
1.2 HTTP響應
類似于HTTP請求,HTTP響應同樣由響應頭、響應內容兩部分組成。 響應頭有分為兩類: 狀態行、 響應頭。 下面是一個HTTP響應的完整內容:
HTTP/1.1 200 OK
Date: Wed, 30 Dec 2020 06:47:20 GMT
Content-Length: 0
Connection: keep-alive
Server: nginx/1.14.0
Access-Control-Allow-Origin: https://class.abc.com
Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE
Access-Control-Max-Age: 86400
Access-Control-Allow-Headers: x-requested-with,Authorization,Cookie
Access-Control-Allow-Credentials: true
Set-Cookie: HJ_SSID_45=hsrein-fe2d-455c-a765-ce1b76647d4c; Domain=.abc.com; Expires=Wed, 30-Dec-2020 07:17:20 GMT; Path=/
Set-Cookie: _SREF_45=""; Domain=.abc.com; Expires=Thu, 01-Jul-2021 06:47:20 GMT; Path=/
Set-Cookie: HJ_CSST_45=0; Domain=.abc.com; Expires=Wed, 30-Dec-2020 07:17:20 GMT; Path=/
X-Via: 1.1 PS-000-01AdS239:3 (Cdn Cache Server V2.0)
X-Ws-Request-Id: 5fec2278_PS-000-01yOO242_18720-46076{"hj_vt": 1
}
1. 狀態行
HTTP/1.1 200 OK
狀態行有3部分組成,HTTP/1.1
標識HTTP協議的版本號,200
是我們的HTTP響應的狀態碼,OK
是HTTP響應的描述。
目前的狀態碼分為5類:
狀態碼 | 描述 |
---|---|
1xx | 請求已經接收,后臺內部處理中 |
2xx | 請求成功 |
3xx | 重定向,需要客戶端(瀏覽器)發起后續操作 |
4xx | 客戶端錯誤 |
5xx | 服務端錯誤 |
2. 響應頭
Date: Wed, 30 Dec 2020 06:47:20 GMT
Connection: keep-alive
Server: nginx/1.14.0
Access-Control-Allow-Origin: https://class.aaa.com
Set-Cookie: HJ_SSID_45=hsrein-fe2d-455c-a765-ce1b76647d4c; Domain=.bbb.com; Expires=Wed, 30-Dec-2020 07:17:20 GMT; Path=/
X-Via: 1.1 PS-000-01AdS239:3 (Cdn Cache Server V2.0)
X-Ws-Request-Id: 5fec2278_PS-000-01yOO242_18720-46076
Header | 說明 |
---|---|
Date | 響應內容生成的時間 |
Connection | 連接復用策略 |
Access-Control-Allow-Origin | 允許跨域,https://class.aaa.com 頁面發起到當前接口跨域請求 |
Set-Cookie | 向客戶端寫入Cookie |
2. 領域對象設計
設計良好的系統有清晰劃分和邊界,層層遞進,領域對象設計很考驗架構師的全局觀。隨意的繼承、組合,很快就會變的不可維護,導致項目失敗。
所謂的架構就是定義劃分和邊界,讓系統的增長不受限于當初的定義的能力,具體的技術只是幫助實現這個定義的手段。
2.1 HttpMessage
HTTP請求和HTTP響應都繼承了HttpMessage
,HttpMessage
提供HTTP頭各種操作(讀取、寫入、遍歷等)。
以下的代碼是對HttpMessage
的基本操作:
HttpMessage ht = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
ht.addHeader("Set-Cookie", "c1=a; path=/; domain=localhost");
ht.addHeader("Set-Cookie", "c2=b; path=\"/\", c3=c; domain=\"localhost\"");
Header h1 = ht.getFirstHeader("Set-Cookie");
System.out.println(h1);
Header h2 = ht.getLastHeader("Set-Cookie");
System.out.println(h2);
Header[] hs = ht.getHeaders("Set-Cookie");
System.out.println(hs.length);
遍歷所有HTTP頭:
HeaderIterator it = ht.headerIterator("Set-Cookie");
while (it.hasNext()) {System.out.println(it.next());
}
通過BasicHeaderElementIterator
解析HTTP頭:
HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
response.addHeader("Set-Cookie", "c1=a; path=/; domain=localhost");
response.addHeader("Set-Cookie", "c2=b; path=\"/\", c3=c; domain=\"localhost\"");HeaderElementIterator it = new BasicHeaderElementIterator(response.headerIterator("Set-Cookie"));while (it.hasNext()) {HeaderElement elem = it.nextElement();System.out.println(elem.getName() + " = " + elem.getValue());NameValuePair[] params = elem.getParameters();for (int i = 0; i < params.length; i++) {System.out.println(" " + params[i]);}
}
2.2 HTTP請求
HttpCore使用HttpRequest
表示HTTP請求,我們可以通過下面這段代碼創建一個最簡單的HTTP請求:
HttpRequest request = new BasicHttpRequest("GET", "/index.html", HttpVersion.HTTP_1_1);
System.out.println(request.getRequestLine().getMethod()); // 輸出 GET
System.out.println(request.getRequestLine().getUri()); // 輸出 /index.html
System.out.println(request.getProtocolVersion()); // 輸出 HTTP/1.1
System.out.println(request.getRequestLine().toString()); // 輸出 GET /index.html HTTP/1.1
1. HttpRequest
HttpCore提供了大量實現類,下面的圖片是httpcore:4.4.13
版本下的類繼承結構
HttpMessage
提供了HTTP頭和HTTP協議版本號相關的操作。HttpRequest
在HttpMessage
的基礎上額外提供了請求行。AbstractExecutionAwareRequest
實現HttpMessage
、HttpRequest
,額外實現HttpExecutionAware
查看是否取消、接收Cancellable對象取消請求。HttpUriRequest
繼承自HttpRequest
,額外提供了獲取HTTP方法、URI,運行取消執行(abort方法),以及查詢是否已經取消(isAborted方法)HttpEntityEnclosingRequest
繼承自HttpRequest
,額外提供攜帶請求體的能力(setEntity(HttpEntity entity)
)BasicHttpRequest
提供了HttpRequest
最基本的實現,對于只需要請求行、HTTP頭的請求,可以直接使用。
2. HttpGet、HttpPost、HttpPut、HttpDelete等
HttpRequest
的4個主要實現類中AbstractExecutionAwareRequest
、HttpUriRequest
共同做為HttpRequestBase
的基類,提供不包含請求體的HTTP請求實現,我們常用的有HttpGet
、HttpDelete
、HttpOptions
、HttpHead
、HttpTrace
。
HttpRequestBase
組合HttpEntityEnclosingRequest
提供實現類HttpEntityEnclosingRequestBase
,它是所有包含請求體的HTTP請求實現,包括HttpPut
、HttpPost
、HttpDelete
、HttpPatch
。
2.3 HTTP響應
HttpCore使用HttpResponse
表示HTTP響應,同樣繼承自HttpMessage
,額外提供了狀態行、響應體(HttpEntity)。通過下面的代碼可以創建一個最簡單的HTTP響應:
HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
System.out.println(response.getProtocolVersion()); // 輸出 HTTP/1.1
System.out.println(response.getStatusLine().getStatusCode()); // 輸出 200
System.out.println(response.getStatusLine().getReasonPhrase()); // 輸出 OK
System.out.println(response.getStatusLine().toString()); // 輸出 HTTP/1.1 200 OK
1. HttpResponse
HttpResponse
提供了獲取狀態行、狀態碼、狀態描述,以及響應內容的方法。
2. BasicHttpResponse
HttpResponse
只提供了兩個實現類,常用的BasicHttpResponse
封裝普通HTTP響應,HttpResponseProxy
供代理服務器使用。
2.4 HttpEntity
HttpCore抽象了HttpEntity表示請求體/響應體,回想一下,前面我們學習的HttpRequest
有部分是實現了HttpEntityEnclosingRequest
的可以攜帶HttpEntity向服務器提交數據。 HttpResponse
都包含一個setEntity
和getEntity
方法,當然RFC文檔定義,部分響應如302跳轉不應該包含HttpEntity
。
HttpCore官方將HttpEntity分為3類:
類型 | 說明 |
---|---|
streamed | 請求體內容來自于InputStream或者程序生成,因為流無法重復讀取,導致HttpEntity內容只能被消費一次 |
self-contained | 請求體內容存儲在內存中,可以反復讀取 |
wrapping | 裝飾器模式,請求體內容來自其他HttpEntity,額外包裝處理后對外提供 |
1. HttpEntity定義
HttpEntity的核心作用就是表示請求體,請求體會被用在輸入和輸出,基本上這也就確定了HttpEntity的接口定義。
- 我們要從請求體讀取數據,于是定義了
InputStream HttpEntity#getContent()
- 我們要將請求體發送到服務端(寫到輸出流),于是定義了
void HttpEntity#writeTo(OutputStream)
- 服務端需要知道我們發送的是圖片還是文本,于是定義了
Header HttpEntity#getContentType()
- 服務端需要知道我們發送的文本用什么編碼,于是定義了
Header HttpEntity#getContentEncoding()
創建HttpEntity要提供ContentType對象,用于定于HttpEntity
包含的內容及編碼,后面的HTTP協議攔截器會協助我們處理HTTP頭和HttpEntity
的關系。
- 發送
HttpEntity
的時候HTTP協議攔截器會自動從HttpEntity
讀取ContentType,并在HttpRequest
下添加HTTP頭Content-Type
。 - 接收
HttpResponse
初始化HttpEntity
時,通過HTTP協議攔截器自動從Content-Type
頭讀取并設置HttpEntity
的ContentType
。
HttpEntity
有大量的實現類,我們來看一個最簡單的HttpEntity
初始化:
StringEntity myEntity = new StringEntity("important message", Consts.UTF_8); // 默認ContentType是text/plainSystem.out.println(myEntity.getContentType()); // 輸出 Content-Type: text/plain; charset=UTF-8
System.out.println(myEntity.getContentLength()); // 將字符串轉為字節數組后的長度
System.out.println(EntityUtils.toString(myEntity)); // 構造函數傳入的字符串
System.out.println(EntityUtils.toByteArray(myEntity).length); // 將字符串轉為字節數組后的長度
HttpEntity
有4組實現類:
-
RequestEntityProy
,用于實現代理服務器 -
StreamingHttpEntity
,提供Body對象,將Body.writeTo方法寫OutputStream,適用于生成InputStream開銷大的場景 -
HttpEntityWrapper
,裝飾器模式,主要用于實現壓縮、解壓
-
AbstractHttpEntity
,最實用的實現,它是我們常用的StringEntity、InputStreamEntity、BasicHttpEntity、FileEntity、ByteArrayEntity等的父類
1. BasicHttpEntity
BasicHttpEntity
提供無參構造函數,默認表示空的HttpEntity。 通過BasicHttpEntity#setContent
傳入InputStream,BaiscHttpEntity#setContentLength
設置HttpEntity長度,能構造出有實際能容的HttpEntity
。
我們看一個簡單的示例:
BasicHttpEntity e = new BasicHttpEntity();
e.setContent(new ByteArrayInputStream("helloworld".getBytes()));
e.setContentLength(-1);
2. ByteArrayEntity
ByteArrayEntity
屬于self-contained
的HttpEntity
,只需要提供byte數組即可構建。
我們看一個簡單的示例:
ByteArrayEntity myEntity = new ByteArrayEntity(new byte[] {1,2,3}, ContentType.APPLICATION_OCTET_STREAM);
3. StringEntity
StringEntity
也是self-contained
的HttpEntity
, 有3種構造方式:
HttpEntity myEntity1 = new StringEntity(sb.toString()); // 默認MIME-TYPE為text/plain,默認編碼 ISO-8859-1
HttpEntity myEntity2 = new StringEntity(sb.toString(), Consts.UTF_8); // 默認MIME-TYPE為text/plain,自己指定編碼
HttpEntity myEntity3 = new StringEntity(sb.toString(), ContentType.create("text/plain", Consts.UTF_8)); // 自己字段MIME-TYPE和編碼
4. InputStreamEntity
通過InputStream
構建,允許傳入要讀取的字節數(-1表示不限)。比較使用的場景是前端提交一個文件后我們拿到一個輸入流,通過這個流我們再上傳到文件到分布式文件系統。
我們看一個簡單的示例:
InputStream instream = getSomeInputStream();
InputStreamEntity myEntity = new InputStreamEntity(instream, 16);
5. FileEntity
通過提供一個File
對象構建,是self-contained
類型的HttpEntity
。
我們看一個簡單的示例:
HttpEntity entity = new FileEntity(staticFile,ContentType.create("application/java-archive"));
6. BufferedHttpEntity
BufferedHttpEntity
繼承自HttpEntityWrapper
,使用裝飾器模式,將其他類型的HttpEntity
轉為self-contained
類型,內部實現是將其他類型的HttpEntity
內容讀取并緩存在內存中。
我們看一個簡單的示例:
myNonRepeatableEntity.setContent(someInputStream);
BufferedHttpEntity myBufferedEntity = new BufferedHttpEntity(myNonRepeatableEntity);
2. EntityUtils
通過InputStream HttpEntity#getContent()
太過底層,使用麻煩。 HttpCore提供了EntityUtils
幫助我們消費HttpEntity
。 EntityUtils
主要提供3類方法:
consume
、consumerQuietly
用于關閉HttpEntity
的輸入流,以便是否資源,在我們不需要HttpEntity
的內容的時候使用。toByteArray
將HttpEntity
內容轉換為字節數字,適用于傳輸非文本內容的場景,比如圖片。toString
系列,用于將HttpEntity
的內容轉換為字符串,可以使用HttpEntity
字段的編碼信息,或者自己指定編碼
2.5 HTTP協議處理器
通常復雜而多變的邏輯都會采用責任鏈模式或者攔截器模式分而治之,HttpCore中的HTTP協議就是通過攔截器鏈完成的。
HttpCore中將攔截器劃分為兩類:
HttpRequestInterceptor
,請求攔截器HttpResponseInterceptor
,響應攔截器
HttpCore定義了大量的攔截器來出來HTTP協議的細節,下面的表格我們列出一些常見的攔截器:
攔截器 | 說明 |
---|---|
RequestContent | 計算請求體長度,添加Content-Length、Transfer-Content頭,添加HTTP版本號 |
RequestConnControl | 負責在請求中添加Connection頭 |
RequestDate | 負責在請求添加Date頭 |
RequestExpectContinue | 負責添加請求的Expect頭 |
RequestTargetHost | 負責添加請求的Host頭 |
RequestUserAgent | 負責添加請求的User-Agent頭 |
ResponseContent | 計算響應體長度,添加Content-Length、Transfer-Content頭,添加HTTP版本號 |
ResponseConnControl | 負責在響應中添加Connection頭 |
ResponseDate | 負責在響應添加Date頭 |
ResponseServer | 負責添加響應的Server頭 |
HTTP攔截器需要配合HttpProcessorBudiler
使用,這個Builder會創建一個ImmutableHttpProcessor
,ImmutableHttpProcessor
會在process
方法內循環調用其他HttpProcessor
。
HttpProcessor httpproc = HttpProcessorBuilder.create().add(new RequestContent()).add(new RequestTargetHost()).add(new RequestConnControl()).add(new RequestUserAgent("MyAgent-HTTP/1.1")).add(new RequestExpectContinue(true)).build();HttpCoreContext context = HttpCoreContext.create();
context.setTargetHost(HttpHost.create("www.a.com"));HttpRequest request = new BasicHttpRequest("GET", "/index.html");
request.addHeader("Host","www.a.com");httpproc.process(request, context);HeaderIterator it = request.headerIterator();
while (it.hasNext()) {Header h = it.nextHeader();System.out.println(h.getName() + ":" + h.getValue());
}System.out.println(request);
服務端處理邏輯
HttpResponse = <...>
httpproc.process(response, context);
2.6 HttpCoreContext
HTTP請求本身是無狀態的,很多場景下我們希望保留狀態,比如Java服務端會話需要的JSESSIONID。 HttpCoreContext
的目的就是為了解決這個問題。
下面是一個最簡單的使用示例:
HttpProcessor httpproc = HttpProcessorBuilder.create().add(new HttpRequestInterceptor() {public void process(HttpRequest request,HttpContext context) throws HttpException, IOException {String id = (String) context.getAttribute("session-id");if (id != null) {request.addHeader("Session-ID", id);}}}).build();HttpCoreContext context = HttpCoreContext.create();
HttpRequest request = new BasicHttpRequest("GET", "/");
httpproc.process(request, context);
3. 總結
4. 參考資料
- HTTP協議文檔 RFC2616 https://tools.ietf.org/html/rfc2616
- HttpComponents文檔 https://hc.apache.org/httpcomponents-core-ga/tutorial/html/fundamentals.html