[原始碼]OKHttp及Http協議筆記
阿新 • • 發佈:2019-01-08
目的有二:學習一下著名開源專案的架構;大致瞭解一下http
硬知識
- Cache-Control:RFC
- 可以放在Cache-Control(http1.1以後)下,也可以放到Pragma(相容http1.0)下
- 可能會有多個
- max-age:>0則使用快取,否則請求服務端確認是否使用快取
- max-stale:請求時,能容許的超過freshness的最長時長,也就是接受服務端認為超時的快取的時間
- s-maxage:用於共享快取(CDN),語義同max-age
- min-fresh:請求時,僅接受min-fresh時間後還未過期的資料
- no-cache:請求時,服務端評估快取結果;返回時,服務端返回的結果必須經過服務端評估才能再次使用,返回的結果可以針對性的指定header的某些field不可重用
- no-store:不快取
- no-transform:中間人不能修改資料
- only-if-cached:請求只接受快取的結果
- must-revalidate:快取超時後,一定要去服務端校驗一次
- private:僅返回用,私有快取,僅存在對應使用者名稱下的cache,不能存在share cache
- public:使用share cache儲存,細節比較多,還是看rfc吧
OkHttp功能
也可以說是Http1.1+WebDAV支援的功能(請求的路徑):
- Address:
- 選擇使用的協議——Protocol
- 連線配置——ConnectionSpec(看起來是配置tls的)
- DNS——Dns介面,可以替換
- SSL——SSLSocketFactory和HostnameVerifier(Java rt中)、Certificate Pinning
- 請求校驗——Authenticator,對於407的返回碼,為請求新增Authorization Header
- Proxy和選擇器——Proxy、ProxySelector(java.net)
- Request:RFC
- Methods:
- GET:不能有request body
- HEAD:不能有request body,服務端只返回header資料
- POST:必須有request body,除非有明確的cache資訊,否則不cache
- PUT:必須有request body,替換服務端對應的資料
- DELETE:可以有request body,刪除服務端對應的資料
- CONNECT:不能有request body,開啟一個到對應埠的資料通道
- OPTIONS:可以有request body,獲得服務端支援的請求引數
- TRACE:不能有request body,就是trace route的http版
- Header:因為型別很多且可擴充套件,所以OkHttp直接暴露了kv介面,沒有setter/builder
- 控制類:
- Cache-Control,見上
- Expect:僅支援100-continue,如果服務端不能滿足該行為,直接返回417
- Host:目標地址
- Max-Forwards:針對TRACE和OPTIONS方法,限制傳播數量
- Pragma:相容1.0的Cache-Control
- Range:指定希望獲得的資料塊位置,在多執行緒下載、斷點續傳時用
- TE:除了chuncked之外額外支援的編碼
- 判斷類:
- If-Match:資料是否對應,必須使用強校驗,內容是服務端和客戶端的私有協議。主要用在改服務端資料
- If-None-Match:資料是否不存在,必須使用弱校驗。主要用來更新cache
- If-Modified-Since:更新cache或減少Proxy轉發次數
- If-Unmodified-Since:更新服務端,同If-Match
- 內容型別類,都可以用 q=小數 來說明對於指定型別的偏好程度(這個q還是很有借鑑意義的):
- Accept:接受的媒體型別
- Accept-Charset
- Accept-Encoding
- Accept-Language
- 內容類:
- From:來自的人
- Referer:來自的URI
- User-Agent:裝置資訊
- 控制類:
- Methods:
- Response:
- Code(終於知道在哪找了):
- 1XX:資訊類
- 100 Continue:繼續傳送下一步請求
- 101 Switching Protocols
- 2XX:成功類
- 200 OK
- 201 Created:滿足客戶端要求的資源已經被建立了
- 202 Accepted
- 203 Non-Authoritative Information:Proxy已經把原服務端的資料修改了
- 204 No Content
- 205 Reset Content:重置客戶端的輸入內容
- 3XX:重定向
- 300 Multiple Choices
- 301 Moved Permanently
- 302 Found:臨時修改位置
- 303 See Other:指定到另一個資源,可以換Method
- 307 Temporary Redirect:跟302差不多,但是不能換Method
- 4XX:客戶端錯誤
- 400 Bad Request
- 402 Payment Required
- 403 Forbidden
- 404 Not Found
- 405 Method Not Allowed
- 406 Not Acceptable:user-agent跟資源八字不合
- 408 Request Timeout
- 409 Conflict:資源狀態不相符
- 410 Gone:永久性的404
- 411 Length Required
- 413 Payload Too Large
- 414 URI Too Long
- 415 Unsupported Media Type
- 417 Expectation Failed
- 426 Upgrade Required:必須升級協議
- 5XX:服務端錯誤
- 500 Internal Server Error
- 501 Not Implemented
- 502 Bad Gateway:代理伺服器返回,表明被代理伺服器壞掉了
- 503 Service Unavailable:臨時性的501
- 504 Gateway Timeout
- 505 HTTP Version Not Supported
- 1XX:資訊類
- Header
- 控制類
- Age:中間人返回的結果,代表結果的年齡
- Cache-Control
- Expires:結果的過期日期
- Warning:返回碼不能表徵的細節訊息,也是3位錯誤碼
- 控制類
- Code(終於知道在哪找了):
OkHttp請求流程(僅以RealCall.execute為例,應該是覆蓋了全部http流程)
- getResponseWithInterceptorChain():使用Interceptor修改Request。與Http相關的是Gzip壓縮request body
- getResponse():填入內容型別類請求頭。因為可能有gzip,所以這一步要在修改response的最後一步
- getResponse():初始化HttpEngine,其中,會根據request,生成一個地址物件,並使用連線池初始化一個StreamAllocation(細節見後)
- getResponse():while迴圈,開始處理包含的子請求(返回碼)
- HttpEngine.networkRequest():填入連線相關的資料,不可cookie(可以用java的CookieHandler)
- 檢視Cache的response是否可用(使用Request和Response裡的判斷類、控制類和Cache-Control資訊)
- 開始連線
- 向流中寫入Request Header[和body]
- 讀取資料
- 看是否有Connection欄位是否為close,關閉連線
- 特殊處理204/205,返回有資料就報錯
- 更新cookie
- 合併或者更新cache
- 檢查有沒有後續請求(401/407/408/3XX)
- 檢查重定向次數是否過多
- [重新開始新子請求]或[返回response]
StreamAllocation
- 區分Http2和1.1的標誌是是否支援長連線
合理設計
- 使用Builder把成員變數的setter從複雜的邏輯物件裡剝離出來,讓結構清晰一些,也做到了物件的immutable——OkHttpClient.Builder
但是,可能有個FieldWrapper更加方便:Buildee中需要Builder配置的所有Field都放到FieldWrapper中。Builder在建構函式中new一個FieldWrapper,在build時,把FieldWrapper直接作為成員變數賦值給Buildee,這樣可以做到單點修改 - 使用Interceptor和InterceptorChain方式,層層過濾、修改請求和結果,基本做到了開閉。但是cache等功能其實也可以使用Interceptor來做,現在直接用的直接呼叫程式碼
- 用了很多ActiveObject模式,其實大部分操作都是靠request、RealCall這些物件直接execute實現的
- 極完整的隔離了資料鏈路層,HttpStream
- 分層,對每層的介面都有針對性的mock和測試用例
- 對外提供唯一功能介面物件,該物件以context形式對內提供所有相關功能
- 以功能分類,有一種把函式封裝成物件的既視感,其實就是把一個引數、邏輯極為複雜的函式封裝成了一個物件,引數都靠成員變數,流程控制也更加靈活。保證了相關邏輯完全在同一個類中管控:
- Dispatcher:所有跟任務排程和記錄相關
- OkHttpClient:對外介面
- RealCall:請求所有邏輯
- HttpEngine:一次請求的所有邏輯
- 為用途不可預測的物件加一個Object的tag,方便擴充套件——Request、Android中的View
- StreamAllocation作為一箇中臺,協調Connection、Stream和Call
技巧
- 用空interface來進行型別區分——Collections.unmodifiableList——RandomAccess介面
- 統一對外介面(OkHttpClient)不是單例,保證了呼叫方可以方便的擴展出多個不同的請求方法,包括分級qos等
- HttpEngine每次子請求發起前會檢視是否cancel。實現多次不可中斷操作構成的大操作的中斷功能,大概都是這個思路
可能的問題
- static的field比較多,而且都是構造出來的物件,會不會在cinit的時候有效能問題
- Interceptor.Chain的預設實現,會對每個Interceptor new出一個新的Chain出來,沒有get到好處。似乎是為了保證請求間的資料(Interceptor的成員變數)不會相互影響。但是,方法的區域性變數也可以做到啊。。。
- 對於HttpEngine,一個engine僅對應著一次request,每redirect一次都會new出來一個。似乎開發者很喜歡這個用法
- 看起來okHttp對記憶體應該是各種不在乎的
- strategy似乎用的不太完美,對於webSocket各種if做相容
- 對於enqueue的AsyncCall,沒有找到呼叫Dispatcher.finished的地方。。。略尷尬。。。這些邏輯暴露出來也不太好
- OkHttpClient不是單例,但是每個client都會覆蓋Internal的單例,可能有坑啊
- 貌似http2使用了一個單獨的執行緒搞的資料傳輸,而請求只是在等待,沒看太懂