目前大部分App後端還沒有統一的網關。其實不止是後端,移動端也是需要網關的。移動網關幫助我們解決穩定性、業務分級隔離、大促容量評估、異構系統支持等問題。移動網關本質是是,以可管控的方式暴露到外網去,這裏的關鍵是如何管控和暴露。從通訊協議上講移動網關是對外接收開放的通信協議,HTTP、gRPC等,一般還有協議轉換講HTTP轉換成內部的RPC協議。本文筆者將談談得物需要什麼樣的移動網關。
一、電商對網絡的要求
1.1 速度 快快快
對於電商平台來説,網絡速度不僅僅是用户體驗的問題,他直接關係到收入,在亞馬遜公開的數據中可以查到:
- 頁面加載超過3秒,57%的用户會離開;
- Amazon頁面加載延長1秒,一年就會減少16億美金營收;
1.2 應對複雜的環境
對於移動端來説資源(電量、內存、CPU)永遠都是不夠用的,最重要的是移動端機型多差異大,而且隨身攜帶,用户可以在任意場景(電梯、高鐵、地下車庫等),在碎片化時間裏使用App;
二、網關的能力
2.1 複用長鏈接
電商的業務場景,如直播、即時日誌回撈、即時消息推送都需要用到長鏈接,但是目前直播和IM分別使用兩套,資源上太浪費,而且在大部分時間,這兩個長鏈接是相對空閒的,如果能利用這個長鏈接收發請求,將會對用户體驗有較大的提升。
把長鏈接統一收到網關層,全業務層複用,業務不用去關心,請求發送的方式和格式。而客户端統一由APP內置網絡服務器來管理所有請求、回調和調度。
在業務層會有“請求(client)--->響應(server)”和“推送(server)--->接收(client)”兩種通訊模式。在此基礎上,客户端不僅可以利用長鏈接發送請求,還可以將IM系統的同步機制拓展到其他模塊,從而讓客户的數據達到增量更新的目的;舉個例子,比如用户有很多訂單記錄,傳統的上客户端會發送http請求給服務端拉取用户的所有訂單記錄,這樣很浪費流量,速度也慢。使用同步機制的話,只需要同步差量數據。這樣數據量小,速度也快同時成功率也高。而且同步機制在用户不在線的情況下會把差量數據保存下來,等用户再次連接時同步數據,這個邏輯和聊天系統非常類似。核心就是用消息同步的邏輯來替換全量拉取的邏輯。
2.2 服務端連接池
HTTP1.0 在默認情況下,client和server每次進行通信時,都需要建立一次連接,傳輸完成後中斷連接。從1.1起默認使用長連接。在長連接中HTTP協議在響應的頭部增加 Connection:keep-alive;雖然是長連接,但是每條連接在同一時間只能處理一個請求/響應,這意味着如果同時收到2兩個請求就需要建立2個TCP連接,TCP建立連接的成本相對來講是很大的。所以在HTTP2.0中引入了Stream/Frame的概念,支持分幀多路複用的能力,在邏輯上區分請求stream和響應stream,即賦予單條連接併發處理多個請求和響應的能力,解決HTTP1.0連接數量和併發量成正比的問題。
http2在協議上實現了stream多路複用,避免了像HTTP1需要排隊的方式進行request 等待response,在未拿到response報文之前,該tcp連接不能被其他協程複用。HTTP2雖然解決了應用層的隊頭阻塞,但是tcp傳輸層也是存在隊頭阻塞的。 比如,client根據內核上的擁塞窗口狀態,可以併發的發送10個tcp包,每個包最大不能超過mss。但因為各種網絡鏈路原因,服務端可能先收到後面的數據包,那麼該數據只能放在內核協議棧上,不能放在socket buf上。這個情況就是tcp的隊頭阻塞。
解決的方案就是增加連接池。
2.3 統一的加密方式
很多App只有傳輸層https加密,如果https做雙向認證那麼也很容易被中間人攻擊,很多金融類App都做了應用層加密來防止報文信息泄漏。而應用層的加密最好是放在網關層。
2.4 DNS解析
DNS劫持是移動網絡常常遇到的問題,常規操作是採用http協議訪問自己的DNS服務器,獲取IP映射,在訪問域名時替換成IP訪問。但是在https請求中這種方式無法使用,這個時候網關就可以發揮作用了,不實用系統的域名解析,而是自己實現域名解析方案。從而獲取正確的域名解析,當然我們還可以提前解析域名,提高連接速度。
2.5 降級策略
前文提到移動網絡是非常複雜的,各種弱網環境都會經常遇到,但是目前我們的App中還沒有對應的降級策略,在網關中我們可以對長鏈接加上降級策略,在長時間無法重連的情況下,嘗試切換協議發送UDP報文。短鏈接也可以嘗試用QUIC收發。
2.6 統一攔截處理邏輯
對於請求中的error code,進行統一攔截,toast彈窗提示,異常處理,監控。有了攔截邏輯我們還可以對請求,進行緩存,短時間內相同的請求可以被合併。減少網絡請求。
2.7 兼容異構的後端服務
目前後端有php、go和java開發的後端服務,請求有rpc/pb和http/json。使用統一網關封裝rpc和http請求,讓業務層從底層協議和數據格式中解脱出來不必關心請求細節專注業務。也可以為以後客户端直接rpc調用打下基礎。
三、App側網關的設計
3.1 協議
Cronet
協議層使用cronet網絡庫
https://chromium.googlesource.com/chromium/src/+/master/components/cronet/
cronet請求對象生命週期
1、協議支持
Cronet 本身支持 HTTP 協議、HTTP/2 協議和 QUIC 協議。
2、請求優先級
該庫支持您為請求設置優先級標籤。服務器可以使用優先級標籤來確定處理請求的順序。
3、資源緩存
Cronet 可以使用內存緩存或磁盤緩存來存儲在網絡請求中檢索到的資源。後續請求會自動通過緩存提供。
4、異步請求
默認情況下,使用 Cronet 庫發出的網絡請求是異步的。您的工作器線程在等待請求返回時不會遭到屏蔽。
5、數據壓縮
Cronet 支持使用 Brotli 壓縮數據格式來壓縮數據。
6、gRPC
在 gRPC 裏客户端應用可以像調用本地對象一樣直接調用另一台不同的機器上服務端應用的方法,使得您能夠更容易地創建分佈式應用和服務。與許多 RPC 系統類似,gRPC 也是基於以下理念:定義一個服務,指定其能夠被遠程調用的方法(包含參數和返回類型)。在服務端實現這個接口,並運行一個 gRPC 服務器來處理客户端調用。在客户端擁有一個存根能夠像服務端一樣的方法。
一旦定義好服務,我們可以使用 protocol buffer 編譯器 protoc 來生成創建應用所需的特定客户端和服務端的代碼 。
有了 gRPC, 我們可以一次性的在一個 .proto 文件中定義服務並使用任何支持它的語言去實現客户端和服務器,反過來,它們可以在各種環境中,從Google的服務器到你自己的平板電腦—— gRPC 幫你解決了不同語言及環境間通信的複雜性。使用 protocol buffers 還能獲得其他好處,包括高效的序列化,簡單的 IDL 以及容易進行接口更新。 gRPC 和 proto3 特別適合移動客户端:gRPC 基於 HTTP/2 實現,相比 HTTP/1.1 更加節省網絡帶寬。序列化和解析 proto 的二進制格式效率高於 JSON,節省了 CPU 和 電池消耗。proto3 使用的運行時在 Google 以及被優化了多年,代碼量極小。
- 一個 應答流式 RPC , 客户端發送請求到服務器,拿到返回的應答消息流。通過在 響應 類型前插入
stream關鍵字,可以指定一個服務器端的流方法。
// Obtains the Features available within the given Rectangle. Results are
// streamed rather than returned at once (e.g. in a response message with a
// repeated field), as the rectangle may cover a large area and contain a
// huge number of features.
rpc ListFeatures(Rectangle) returns (stream Feature) {}
- 一個 請求流式 RPC , 客户端發送一個消息序列到服務器。一旦客户端完成寫入消息,它等待服務器完成讀取返回它的響應。通過在 請求 類型前指定
stream關鍵字來指定一個客户端的流方法。
// Accepts a stream of Points on a route being traversed, returning a
// RouteSummary when traversal is completed.
rpc RecordRoute(stream Point) returns (RouteSummary) {}
- 一個 雙向流式 RPC 是雙方使用讀寫流去發送一個消息序列。兩個流獨立操作,因此客户端和服務器可以以任意喜歡的順序讀寫:比如, 服務器可以在寫入響應前等待接收所有的客户端消息,或者可以交替的讀取和寫入消息,或者其他讀寫的組合。 每個流中的消息順序被預留。你可以通過在請求和響應前加
stream關鍵字去制定方法的類型。
// Accepts a stream of RouteNotes sent while a route is being traversed,
// while receiving other RouteNotes (e.g. from other users).
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
現在讓我們看看服務端的情況——流式RPC。 ListFeatures 是一個服務器端的流式 RPC,因此我們需要給客户端返回多個 Feature。
Status ListFeatures(ServerContext* context, const Rectangle* rectangle,
ServerWriter<Feature>* writer) override {
auto lo = rectangle->lo();
auto hi = rectangle->hi();
long left = std::min(lo.longitude(), hi.longitude());
long right = std::max(lo.longitude(), hi.longitude());
long top = std::max(lo.latitude(), hi.latitude());
long bottom = std::min(lo.latitude(), hi.latitude());
for (const Feature& f : feature_list_) {
if (f.location().longitude() >= left &&
f.location().longitude() <= right &&
f.location().latitude() >= bottom &&
f.location().latitude() <= top) {
writer->Write(f);
}
}
return Status::OK;
}
如你所見,這次我們拿到了一個請求對象(客户端期望在 Rectangle 中找到的 Feature)以及一個特殊的 ServerWriter 對象,而不是在我們的方法參數中獲取簡單的請求和響應對象。在方法中,根據返回的需要填充足夠多的 Feature 對象,用 ServerWriter 的 Write() 方法寫入。最後,和我們簡單的 RPC 例子相同,我們返回Status::OK去告知gRPC我們已經完成了響應的寫入。
如果你看過客户端流方法RecordRoute,你會發現它很類似,除了這次我們拿到的是一個ServerReader而不是請求對象和單一的響應。我們使用 ServerReader 的 Read() 方法去重複的往請求對象(在這個場景下是一個 Point)讀取客户端的請求直到沒有更多的消息:在每次調用後,服務器需要檢查 Read() 的返回值。如果返回值為 true,流仍然存在,它就可以繼續讀取;如果返回值為 false,則表明消息流已經停止。
while (stream->Read(&point)) {
...//process client input
}
最後,讓我們看看雙向流RPCRouteChat()。
Status RouteChat(ServerContext* context,
ServerReaderWriter<RouteNote, RouteNote>* stream) override {
std::vector<RouteNote> received_notes;
RouteNote note;
while (stream->Read(¬e)) {
for (const RouteNote& n : received_notes) {
if (n.location().latitude() == note.location().latitude() &&
n.location().longitude() == note.location().longitude()) {
stream->Write(n);
}
}
received_notes.push_back(note);
}
return Status::OK;
}
這次我們得到的 ServerReaderWriter 對象可以用來讀 和 寫消息。這裏讀寫的語法和我們客户端流以及服務器流方法是一樣的。雖然每一端獲取對方信息的順序和寫入的順序一致,客户端和服務器都可以以任意順序讀寫——流的操作是完全獨立的。
3.2 網關層
整體框架
網關層封裝網關服務,gRPC、代理服務、加密、長鏈接等等
App網絡服務層概要設計
網關管理器統一管理,http/json請求,socket/PB長鏈接,和RPC請求,同時封裝數據轉換邏輯,可以切換json和bp數據格式。達到利用長鏈接發送http請求的需求。數據處理模塊不僅負責數據轉換,還是負責整請求的生命週期。
文/Matrix