1. 程式人生 > >[原始碼]OKHttp及Http協議筆記

[原始碼]OKHttp及Http協議筆記

目的有二:學習一下著名開源專案的架構;大致瞭解一下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:裝置資訊
  • 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
    • Header
      • 控制類
        • Age:中間人返回的結果,代表結果的年齡
        • Cache-Control
        • Expires:結果的過期日期
        • Warning:返回碼不能表徵的細節訊息,也是3位錯誤碼

OkHttp請求流程(僅以RealCall.execute為例,應該是覆蓋了全部http流程)

  1. getResponseWithInterceptorChain():使用Interceptor修改Request。與Http相關的是Gzip壓縮request body
  2. getResponse():填入內容型別類請求頭。因為可能有gzip,所以這一步要在修改response的最後一步
  3. getResponse():初始化HttpEngine,其中,會根據request,生成一個地址物件,並使用連線池初始化一個StreamAllocation(細節見後)
  4. getResponse():while迴圈,開始處理包含的子請求(返回碼)
    1. HttpEngine.networkRequest():填入連線相關的資料,不可cookie(可以用java的CookieHandler)
    2. 檢視Cache的response是否可用(使用Request和Response裡的判斷類、控制類和Cache-Control資訊)
    3. 開始連線
    4. 向流中寫入Request Header[和body]
    5. 讀取資料
    6. 看是否有Connection欄位是否為close,關閉連線
    7. 特殊處理204/205,返回有資料就報錯
    8. 更新cookie
    9. 合併或者更新cache
  5. 檢查有沒有後續請求(401/407/408/3XX)
  6. 檢查重定向次數是否過多
  7. [重新開始新子請求]或[返回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使用了一個單獨的執行緒搞的資料傳輸,而請求只是在等待,沒看太懂