1. OkHttp源碼解析 (三)——代理和路由
初看OkHttp源碼,由於對Address、Route、Proxy、ProxySelector、RouteSelector等理解不夠,讀源碼非常吃力,看了幾遍依然對於尋找復用連接、創建連接、連接伺服器、連接代理伺服器、創建隧道連接等邏輯似懂非懂,本篇決定梳理一遍相關的概念及基本原理。
● HTTP/1.1(HTTPS)
● HTTP/2
● SPDY
一個http請求的流程(直連):
1、輸入url及參數;
2、如果是url是域名則解析ip地址,可能對應多個ip,如果沒有指定埠,則用默認埠,http請求用80;
3、創建socket,根據ip和埠連接伺服器(socket內部會完成3次TCP握手);
4、socket成功連接後,發送http報文數據。
一個https請求的流程(直連):
1、輸入url及參數;
2、如果是url是域名則解析ip地址,可能對應多個ip,如果沒有指定埠,則用默認埠,https請求用443;
3、創建socket,根據ip和埠連接伺服器(socket內部會完成3次TCP握手);
4、socket成功連接後進行TLS握手,可通過java標准款提供的SSLSocket完成;
5、握手成功後,發送https報文數據。
1、分類
● HTTP代理:普通代理、隧道代理
● SOCKS代理:SOCKS4、SOCKS5
2、HTTP代理分類及說明
普通代理
HTTP/1.1 協議的第一部分。其代理過程為:
● client 請求 proxy
● proxy 解析請求獲取 origin server 地址
● proxy 向 origin server 轉發請求
● proxy 接收 origin server 的響應
● proxy 向 client 轉發響應
其中proxy獲取目的伺服器地址的標准方法是解析 request line 里的 request-URL。因為proxy需要解析報文,因此普通代理無法適用於https,因為報文都是加密的。
隧道代理
通過 Web 代理伺服器用隧道方式傳輸基於 TCP 的協議。
請求包括兩個階段,一是連接(隧道)建立階段,二是數據通信(請求響應)階段,數據通信是基於 TCP packet ,代理伺服器不會對請求及響應的報文作任何的處理,都是原封不動的轉發,因此可以代理 HTTPS請求和響應。
代理過程為:
● client 向 proxy 發送 CONNET 請求(包含了 origin server 的地址)
● proxy 與 origin server 建立 TCP 連接
● proxy 向 client 發送響應
● client 向 proxy 發送請求,proxy 原封不動向 origin server 轉發請求,請求數據不做任何封裝,為原生 TCP packet.
3、SOCKS代理分類及說明
● SOCKS4:只支持TCP協議(即傳輸控制協議)
● SOCKS5: 既支持TCP協議又支持UDP協議(即用戶數據包協議),還支持各種身份驗證機制、伺服器端域名解析等。
SOCK4能做到的SOCKS5都可得到,但反過來卻不行,比如我們常用的聊天工具QQ在使用代理時就要求用SOCKS5代理,因為它需要使用UDP協議來傳輸數據。
有了上面的基礎知識,下面分析結合源碼分析OkHttp路由相關的邏輯。OkHttp用Address來描述與目標伺服器建立連接的配置信息,但請求輸入的可能是域名,一個域名可能對於多個ip,真正建立連接是其中一個ip,另外,如果設置了代理,客戶端是與代理伺服器建立直接連接,而不是目標伺服器,代理又可能是域名,可能對應多個ip。因此,這里用Route來描述最終選擇的路由,即客戶端與哪個ip建立連接,是代理還是直連。下面對比下Address及Route的屬性,及路由選擇器RouteSelector。
描述與目標伺服器建立連接所需要的配置信息,包括目標主機名、埠、dns,SocketFactory,如果是https請求,包括TLS相關的SSLSocketFactory 、HostnameVerifier 、CertificatePinner,代理伺服器信息Proxy 、ProxySelector 。
Route提供了真正連接伺服器所需要的動態信息,明確需要連接的伺服器IP地址及代理伺服器,一個Address可能會有很多個路由Route供選擇(一個DNS對應對個IP)。
Address和Route都是數據對象,沒有提供操作方法,OkHttp另外定義了RouteSelector來完成選擇的路由的操作。
1、讀取代理配置信息:resetNextProxy()
讀取代理配置:
● 如果有指定代理(不讀取系統配置,在OkHttpClient實例中指定),則只用1個該指定代理;
● 如果沒有指定,則讀取系統配置的,可能有多個。
2、獲取需要嘗試的socket地址(目標伺服器或者代理伺服器):resetNextInetSocketAddress()
結合Address的host和代理,解析要嘗試的套接字地址(ip+埠)列表:
● 直連或者SOCK代理, 則用目標伺服器的主機名和埠,如果是HTTP代理,則用代理伺服器的主機名和埠;
● 如果是SOCK代理,根據目標伺服器主機名和埠號創建未解析的套接字地址,列表只有1個地址;
● 如果是直連或HTTP代理,先DNS解析,得到InetAddress列表(沒有埠),再創建InetSocketAddress列表(帶上埠),InetSocketAddress與InetAddress的區別是前者帶埠信息。
3、獲取路由列表:next()
選擇路由的流程解析:
● 遍歷每個代理對象,可能多個,直連的代理對象為Proxy.DIRECT(實際是沒有中間代理的);
● 對每個代理獲取套接字地址列表;
● 遍歷地址列表,創建Route,判斷Route如果在路由黑名單中,則添加到失敗路由列表,不在黑名單中則添加到待返回的Route列表;
● 如果最後待返回的Route列表為空,即可能所有路由都在黑名單中,實在沒有新路由了,則將失敗的路由集合返回;
● 傳入Route列表創建Selection對象,對象比較簡單,就是一個目標路由集合,及讀取方法。
為了避免不必要的嘗試,OkHttp會把連接失敗的路由加入到黑名單中,由RouteDatabase管理,該類比較簡單,就是一個失敗路由集合。
1、創建Address
Address的創建在RetryAndFollowUpInteceptor里,每次請求會聲明一個新的Address及StreamAllocation對象,而StreamAllocation使用Address創建RouteSelector對象,在連接時RouteSelector確定請求的路由。
每個Requst都會構造一個Address對象,構造好了Address對象只是有了與伺服器連接的配置信息,但沒有確定最終伺服器的ip,也沒有確定連接的路由。
2、創建RouteSelector
在StreamAllocation聲明的同時會聲明路由選擇器RouteSelector,為一次請求尋找路由。
3、選擇可用的路由Route
下面在測試過程跟蹤實例對象來理解,分別測試直連和HTTP代理HTTP2請求路由的選擇過程:
● 直連請求流程
● HTTP代理HTTPS流程
請求url: https://www.jianshu.com/p/63ba15d8877a
1、構造address對象
2、讀取代理配置:resetNextProxy
3、解析目標伺服器套接字地址:resetNextInetSocketAddress
4、選擇Route創建RealConnection
5、確定協議
測試方法:
● 在PC端打開Charles,設置埠,如何設置代理,網上有教程,比較簡單;
● 手機打開WIFI,選擇連接的WIFI修改網路,在高級選項中設置中指定了代理伺服器,ip為PC的ip,埠是Charles剛設置的埠;
● OkHttpClient不指定代理,發起請求。
1、構造address對象
2、讀取代理配置:resetNextProxy
3、解析目標伺服器套接字地址:resetNextInetSocketAddress
4、選擇Route創建RealConnection
5、創建隧道
由於是代理https請求,需要用到隧道代理。
從圖可以看出,建立隧道其實是發送CONNECT請求,header包括欄位Proxy-Connection,目標主機名,請求內容類似:
6、確定協議,SSL握手
1、代理可分為HTTP代理和SOCK代理;
2、HTTP代理又分為普通代理和隧道代理;普通代理適合明文傳輸,即http請求;隧道代理僅轉發TCP包,適合加密傳輸,即https/http2;
3、SOCK代理又分為SOCK4和SOCK5,區別是後者支持UDP傳輸,適合代理聊天工具如QQ;
4、沒有設置代理(OkHttpClient沒有指定同時系統也沒有設置),客戶端直接與目標伺服器建立TCP連接;
5、設置了代理,代理http請求時,客戶端與代理伺服器建立TCP連接,如果代理伺服器是域名,則解釋代理伺服器域名,而目標伺服器的域名由代理伺服器解析;
6、設置了代理,代理https/http2請求時,客戶端與代理伺服器建立TCP連接,發送CONNECT請求與代理伺服器建立隧道,並進行SSL握手,代理伺服器不解析數據,僅轉發TCP數據包。
如何正確使用 HTTP proxy
OkHttp3中的代理與路由
HTTP 代理原理及實現(一)
2. FastAPI 源碼閱讀 (一) ASGI應用
本章開啟 FastAPI 的源碼閱讀,FastAPI是當下python web中一顆新星,是一個劃時代的框架。從誕生便是以快速和簡潔為核心理念。
它繼承於 Starlette ,是在其基礎上的完善與擴展。詳細內容可以翻看我之前的源碼閱讀。
openapi() 與 setup() 是在初始化階段,對 OpenAPI 文檔進行初始化的函數。
而 add_api_route() 一直到 trace() ,是關於路由的函數,它們都是直接對 router 的方法傳參引用。所以這些放在解讀 routing.py 時一並進行。
除了 Starlette 原生的參數,大量參數都是和API文檔相關。
而路由從 Starlette 的 Router 換成了新式的 APIRouter
這兩個概念查詢了大量文檔才搞明白。他們主要是關於文檔與反向代理的參數。當使用了Nginx時等反向代理時,從Uvicorn直接訪問,和從Nginx代理訪問,路徑可能出現不一致。比如Nginx中的Fastapi根目錄是 127.0.0.1/api/ ,而Uvicorn角度看是 127.0.0.1:8000/ 。對於API介面來說,其實這個是沒有影響的,因為伺服器會自動幫我們解決這個問題。但對於API文檔來說,就會出現問題。
因為當我們打開/docs時,網頁會尋找 openapi.json 。他的是寫在html內部的,而不是變化的。這會導致什麼問題?
例如當我們從Uvicorn訪問 127.0.0.1:8000/docs 時,他會尋找 /openapi.json 即去訪問 127.0.0.1:8000/openapi.json (了解前端的應該知道)
但是假如我們這時,從Nginx外來訪問文檔,假設我們這樣設置Nginx:
我們需要訪問 127.0.0.1/api/docs ,才能從代理外部訪問。而打開docs時,我們會尋找 openapi.json 。
所以這時,它應該在 127.0.0.1/api/openapi 這個位置存在。
但我們的瀏覽器不知道這些,他會按照 /openapi.json ,會去尋 127.0.0.1/openapi.json 這個位置。所以他不可能找到 openapi.json ,自然會啟動失敗。
這其實是openapi文檔前端的問題。
root_path ,是用來解決這個問題的。既然 /openapi.json 找不到,那我自己改成 /api/openapi.json 不就成了么。
root_path 即是這個 /api ,這個是在定義時手動設置的參數。為了告訴FastAPI,它處在整個主機中的哪個位置。即告知 所在根目錄 。這樣,FastAPI就有了"自知之明",乖乖把這個 前綴 加上。來找到正確的 openapi.json
加上了 root_path , openapi.json 的位置變成了 /api/openapi.json 。當你想重新用Uvicorn提供的地址從代理內訪問時,他會去尋找哪?沒錯 127.0.0.1:8000/api/openapi.json ,但我們從代理內部訪問,並不需要這個 前綴 ,但它還是 「善意」 的幫我們加上了,所以這時候內部的訪問失靈了。
雖然我們不大可能需要從兩個位置訪問這個完全一樣的api文檔。但這點一定要注意。
我在翻官方文檔時,看到他們把 root_path 吹得天花亂墜,甚至棄用了 openapi_prefix 參數。但最後是把我弄得暈頭轉向。
這樣要提到 servers 這個參數,官方首先給了這么段示例,稍作修改。
當我們打開API文檔時
我們可以切換這個 Servers 時,底下測試介面的發送鏈接也會變成相應的。
但是記住,切換這個server,下面的介面不會發送變化,只是發送的host會改變。
這代表,雖然可以通過文檔,測試多個其他主機的介面。但是這些主機和自己之間,需要擁有一致的介面。這種情況通常像在 線上/開發伺服器 或者 伺服器集群 中可能出現。雖然不要求完全一致,但為了這樣做有實際意義,最好大體上是一致的。
但是我們看到,這是在代理外打開的,如果我們想從代理內打開,需要去掉 root_path 。會發生什麼?
我們將 root_path 注釋掉:
如果想解決這個問題,只需要將自身手動加入到 Servers 中。
關於 root_path_in_servers ,當 root_path 和 servers 都存在時, root_path 會自動將自己加入到 servers 中。但如果這個置為False,就不會自動加入。(默認為True)
API文檔實際上以字元串方式,在FastAPI內部拼接的。實際上就是傳統的 模板(Templates) ,這個相信大家都很熟悉了。優點是生成時靈活,但缺點是不容易二次開發。fastapi提供了好幾種文檔插件,也可以自己添加需要的。
這么長一大串,實際上就一句話 self.router.add_api_route() ,其他剩下的那些我暫且省略的,其實基本都是這樣的。就是調用router的一個功能。下面我用省略方式將它們列出。
可以看到有些在這里就做了閉包,實際上除了這里的'add_api_route()'他們最終都是要做閉包的。只是過程放在里router里。它們最終的指向都是 router.add_api_route() ,這是一個添加真正將 endpoint 加入到路由中的方法。
FastAPI 添加路由的方式,在starlette的傳統路由列表方式上做了改進,變成了裝飾器式。
其實就是通過這些方法作為裝飾器,將自身作為 endpoint 傳入生成 route 節點,加入到 routes 中。
FastAPI的入口沒有太大的變化,借用starlette的 await self.middleware_stack(scope, receive, send) 直接進入中間件堆棧。
3. vue項目實現動態路由和動態菜單搭建插件式開發框架免費源碼
以往我們在開發vue項目的時候,總是通過將路徑和路由寫在route/index.js文件中,然後直接進行訪問即可,一般實現許可權匹配都是通過菜單下面的許可權參數和路由守衛進行一個驗證攔截和許可權匹配,然而這樣安全性仍然不足。因為我們在route/index.js中已經寫滿了所有的路由,這樣子不僅造成靜態路由內容過多、修改困難,同時當靜態路由內容過多的時候,我們在路由中的內容就顯得極其復雜。
而後端對前端的控制也顯得較為無力,無法實現嚴格性的控制。
由此我們發現通過動態路由控制是必然的,此時我們只需要通過後端獲取數據菜單和路由信息json,然後動態添加路由並生成菜單,使菜單與動態路由內容進行一個匹配,這樣子我們可以實現由後端控制前端的菜單和路由,我們的項目往往只需要內置幾個組件無需許可權的公共頁面如登陸、注冊、忘記密碼和404錯誤這幾個常用頁面組件。
我們只需要將寫好的組件放置到我們的view視圖下,然後我們通過動態的路由和菜單實現路由添加和菜單進行匹配,我們便可實現對插件進行訪問,我們減少了對route/index.js內容寫入,同時也有利於減少內存的佔用。
我們通過動態路由的形式,我們生成的菜單許可權更加的完善,不僅實現依靠菜單與路由守衛攔截實現鑒權,也可以通過動態路由實現動態載入vue文件,控制更加深度
我們通過動態路由的形式,我們可以將項目分給不同的人進行完成,便於組建一個開發團隊,因為他們所開發的組件,我們只需要在具備基本的javascript庫的情況下。我們直接進行動態路由的一個掛載和菜單生成便可完成項目合作,減少了對route/index.js文件的操作,保證項目的完整性。
最後我發現在非node環境的開發條件下,我們可以實現遠程的vue文件載入,這不僅為我們開發提供了便利,同時也有利於我們及時修改文件,以達到項目的需求,更有利於保障安全,實現伺服器vue文件載入。
Vue:2.6.11。
Vue-route:3.2.0。
主頁
聊天
第一通過後端返回的一個路由json數據,我們通過前端生成符合路由路由靜態內容數組的一個數組,然後再通過addRoute進行一個循環添加,我們以此生成動態路由。在登陸時獲取後端返回的菜單信息,我們進行菜單的一個循環生成,由此我們的一個動態項目就已經完成。
第二怎樣對動態路由和菜單項目進行一個管理。
我們首先可以通過搭建一個組件通過添加路由信息和管理菜單實現二者的動態匹配。我們只需要對路由信息進行一個添加和修改,並和菜單相互間進行匹配,我們便可實現簡單的路由掛載。
組件管理
菜單管理
此時將數據提交的後端由後端進行數據保存,我們此時的組件只需要放在views文件夾下,添加路由進行文件載入,我們便可實現路由管理。
第一登陸頁面配置。
我們需要在靜態文件夾下創建一個menu.json和route.json。兩個json文件模擬伺服器登錄時返回的數據。
我們在登錄頁面模擬獲取數據之後,我們通過菜單的一個方法進行生成菜單,通過路由的方法生成路由數組並進行循環添加,然後執行路由跳轉。
第二配置路由初始化內容。我們將route/index.js的路由信息填為空是非常不理智的,而且會報錯,因為路由初始化在載入前已經完成。有些頁面完全不需要許可權便可訪問,比如登錄、注冊、找回密碼和404錯誤,這種不需要許可權的頁面,我們還是需要將其直接以靜態的形式寫在route/index.js文件中。
Index初始數據
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue . use ( VueRouter )
const routes = [{
path: '/' , //訪問url
name: 'login' , //路由名稱
component : () => import ( '@/unitui/pages/Login.vue' ), //載入模板文件
meta: {
show_site: 0 , //是否全屏顯示
web_title: "登錄" //網站標題
}
},
{
path: '/register' , //訪問url
name: 'register' , //路由名稱
component : () => import ( '@/unitui/pages/Register.vue' ), //載入模板文件
meta: {
show_site: 0 , //是否全屏顯示
web_title: "注冊" //網站標題
}
},
{
path: '/forget' , //訪問url
name: 'forget' , //路由名稱
component : () => import ( '@/unitui/pages/Forget.vue' ), //載入模板文件
meta: {
show_site: 0 , //是否全屏顯示
web_title: "找回密碼" //網站標題
}
},
{
path: '/404' , //訪問url
name: '404' , //路由名稱
component : () => import ( '@/unitui/pages/404.vue' ), //載入模板文件
meta: {
show_site: 0 , //是否全屏顯示
web_title: "404錯誤" //網站標題
}
},
]
const router = new VueRouter ({
routes
})
router . beforeEach (( to , from , next ) => {
document . title = to . meta . web_title
console . log ( to );
next ()
})
export default router
第三,關於防止刷新後丟失的問題。我們需要在app.vue文件中的methods方法中定義一個路由生成方法。
示例:
init_route () { //初始化路由,防止刷新丟失
if ( sessionStorage . getItem ( "route_data" ) != null ) { //只有後端已經返回數據的情況下才允許生成
const route_data = JSON . parse ( sessionStorage . getItem ( "route_data" )); //獲取路由信息
const data = []; //默認路由數組
for ( let index = 0 ; index < route_data . length ; index ++) { //生成路由信息
data [ index ] = {
path: route_data [ index ]. path , //訪問url
name: route_data [ index ]. name , //路由名稱
component : resolve =>
require ([ `@/views/ ${ route_data [ index ]. component } ` ], resolve ), //載入模板文件
meta: {
show_site: route_data [ index ]. meta . show_site , //是否全屏顯示
web_title: route_data [ index ]. meta . web_title //網站標題
}
};
}
for ( let index = 0 ; index < data . length ; index ++) { //循環添加路由
this . $router . addRoute ( data [ index ]);
}
}
}
在mounted中進行方法調用,防止刷新的時路由丟失,導致發生錯誤。該方法內容基本和登陸頁面的菜單出路由初始內容基本相同,但我們唯一差別的是,我們需要判斷登陸所獲取的路由信息是否存在,只有在存在的時候及後端已經返回了路由信息,即證明登錄成功的時候,我們才會動態添加路由。
第一在刷新之後,默認跳轉到path:』*』的一個路由界面中去,此時我們解決方法只需要將path:』*』路由進行一個刪除,將其刪除就變可正常訪問。
第二動態路由跳轉時發生Cannot find mole xxx錯誤。
意思是無法載入我們指定的一個vue文件,這是由於route3.0版本後import方式不支持傳入變數,此時我們只需要將其改為require方式便可。
我們此次動態vue項目開發已經基本完成,我的開發的項目是基於element-ui進行,那麼如果你需要源碼參考。可以私信回復unit便可獲取。