在上一篇文章,我們講到了長(zhǎng)連接常見(jiàn)的實(shí)現(xiàn)方案,相信大家對(duì)長(zhǎng)連接已經(jīng)有一定的了解了,這篇文章我們會(huì)講 FeatureProbe 的長(zhǎng)連接實(shí)現(xiàn)方案。
一、為什么FeatureProbe需要長(zhǎng)連接
Feature Toggle 在部分場(chǎng)景下,客戶(hù)端對(duì)實(shí)時(shí)性有較高的要求,如緊急情況,希望配置立刻下發(fā)生效。有的 Feature 在 Web 端加載或 App 啟動(dòng)的時(shí)候就要讀取到開(kāi)關(guān)的值,雖然緩存能解決一部分問(wèn)題,但是最快拿到最新的值,會(huì)更符合用戶(hù)的預(yù)期。我們?cè)谏掀刑岬竭^(guò),長(zhǎng)連接可以解決數(shù)據(jù)推送和請(qǐng)求優(yōu)化這兩個(gè)場(chǎng)景。
1、可選協(xié)議
- SSE :Server Send Event 能滿(mǎn)足服務(wù)端向客戶(hù)端發(fā)送數(shù)據(jù)的需求,協(xié)議簡(jiǎn)單,但因?yàn)椴皇请p工的數(shù)據(jù)通路后期無(wú)法實(shí)現(xiàn) HTTP 的請(qǐng)求優(yōu)化。
- TCP :目前最主流的長(zhǎng)連接協(xié)議,配合 TLS 1.3 可以做到很好的使用效果。
- QUIC :本身握手和 TLS1.3 融合,還支持連接恢復(fù),多Stream避免包頭阻塞問(wèn)題,有很多優(yōu)勢(shì),因?yàn)榛赨DP,可能會(huì)有部分特殊網(wǎng)絡(luò)環(huán)境被禁止。
- UDP :需要自己實(shí)現(xiàn)丟包重傳,部分網(wǎng)絡(luò)環(huán)境有可能被限制。
- WebSocket :對(duì)瀏覽器友好,小程序唯一支持的雙向收發(fā) (全雙工) 協(xié)議,很難做連接優(yōu)化。
2、設(shè)計(jì)目標(biāo)
- 盡可能支持更多的端,小程序,移動(dòng)端,多種語(yǔ)言服務(wù)端;
- 盡量降低 SDK 的實(shí)現(xiàn)復(fù)雜度,方便后期社區(qū)貢獻(xiàn);
- 盡可能使開(kāi)關(guān)快速生效;
- 盡可能低的數(shù)據(jù)傳輸量。
二、FeatureProbe長(zhǎng)連接方案
1、協(xié)議選擇 WebSocket
小程序是我們一期要優(yōu)先支持的平臺(tái),所以所有不支持小程序的協(xié)議都不在一期的考慮范圍內(nèi)。
- 優(yōu)點(diǎn):是可以支持小程序和瀏覽器環(huán)境,小程序是我們優(yōu)先要支持的部分,在國(guó)內(nèi)的重要性非常高,很多創(chuàng)業(yè)團(tuán)隊(duì)甚至只開(kāi)發(fā)小程序的 APP 版本。
- 缺點(diǎn):是連接建立的優(yōu)化很難進(jìn)行.在國(guó)內(nèi)網(wǎng)絡(luò)環(huán)境整體較好的情況下,大部分的請(qǐng)求還是在較快的響應(yīng)范圍之內(nèi).我們可以在后面二期的時(shí)候再針對(duì)其他端做多協(xié)議切換。
我們?cè)? Websocket 的基礎(chǔ)上進(jìn)一步選擇了 Socektio 這個(gè)網(wǎng)絡(luò)庫(kù):
優(yōu)點(diǎn):是在 WebSocket 的基礎(chǔ)上提供了斷開(kāi)重連,發(fā)送緩沖,消息確認(rèn),廣播,整體的編解碼邏輯簡(jiǎn)單,提供了長(zhǎng)輪詢(xún)(long polling)的回退方案,在不支持 WebSocket 的設(shè)備上也能兼容。
缺點(diǎn):客戶(hù)端有限,老項(xiàng)目已經(jīng)比較成熟,目前已經(jīng)不太活躍。
2、服務(wù)端推送
FeatureProbe Server 發(fā)現(xiàn)開(kāi)關(guān)更新后,發(fā)送事件給關(guān)心這個(gè)開(kāi)關(guān)的連接,對(duì)端的 SDK 收到事件,觸發(fā)一次開(kāi)關(guān)拉取。這里面能做的優(yōu)化是直接下發(fā)開(kāi)關(guān)的值,因?yàn)?Server SDK 和 Client SDK 的處理邏輯不同,我們放到下個(gè)迭代優(yōu)化。
如何發(fā)現(xiàn)變化:開(kāi)關(guān)的規(guī)則是存儲(chǔ)在 FeatureProbe API 服務(wù)中的,目前 FeatureProbe Server 通過(guò)接口周期性訪問(wèn)得到,直觀的想法是輪詢(xún)時(shí),去 diff 開(kāi)關(guān)的值,就可以發(fā)現(xiàn)變化,但是效率比較低。因?yàn)?SDK 是針對(duì)項(xiàng)目環(huán)境下所有的開(kāi)關(guān)進(jìn)行獲取,這里環(huán)境的 SDK KEY 拉取整體的開(kāi)關(guān)規(guī)則時(shí),添加一個(gè) version 就可以判斷兩次之間是否一致。
如何表示 SDK 對(duì)某個(gè)開(kāi)關(guān)感興趣: 目前 SDK 向 Featureprobe Server 獲取開(kāi)關(guān),是以 SDK_KEY 為粒度的。在 SocketIO 連接建立后,SDK 會(huì)向 Server 注冊(cè) SDK_KEY, Server 就可以把這個(gè)連接存儲(chǔ)在相同 SDK KEY 的列表中,等有開(kāi)關(guān)發(fā)生變化,Server 知道開(kāi)關(guān)是發(fā)生在哪個(gè) SDK_KEY 中,把 對(duì)應(yīng) SDK_KEY 列表中所有的連接都發(fā)送一個(gè)更新事件,就完成了變更的通知。實(shí)際實(shí)現(xiàn)利用了SocketIO 提供了 Room 的概念,僅需把連接和 SDK KEY做一下關(guān)聯(lián),變更時(shí)直接對(duì) SDK_KEY 發(fā)送事件就可以了。
代碼示意:
import { createServer } from "http";
import { Server } from "socket.io";
const httpServer = createServer();
const io = new Server(httpServer);
io.on("register", (sdk_key, socket) => {
socket.join(sdk_key);
});
httpServer.listen(3000);
// notify clients
io.to(SDK_KEY).emit("update");
3、客戶(hù)端接收
FeatureProbe SDK 目前是 pull 模式和服務(wù)端通信,即啟動(dòng)后通過(guò)輪詢(xún)來(lái)周期性獲取全量開(kāi)關(guān)的數(shù)據(jù)。在 SocketIO 的幫助下,添加 push 的模式很簡(jiǎn)單。在原有基礎(chǔ)上初始化 SocketIO 的客戶(hù)端,連接建立后把 SDK KEY 發(fā)送給 Server, 然后監(jiān)聽(tīng)一個(gè) Server 下發(fā)的 update 事件,收到事件就立刻觸發(fā)一次開(kāi)關(guān)全量的拉取。斷開(kāi)重連,心跳,回調(diào)等都交給 SocketIO 來(lái)做。這里有個(gè)優(yōu)化是下發(fā)的數(shù)據(jù)可以是開(kāi)關(guān)變更的數(shù)據(jù),而不僅僅是變更事件,這個(gè)也是我們后續(xù)準(zhǔn)備做的工作。
三、最終實(shí)現(xiàn)
FeatureProbe Server 是 Rust 語(yǔ)言實(shí)現(xiàn)的,考慮到后續(xù)的性能和擴(kuò)展性等原因,我們不想再引入一個(gè) nodejs 的模塊專(zhuān)門(mén)做長(zhǎng)連接的管理,所以我用 Rust 實(shí)現(xiàn)了 SocketIO 的服務(wù)端 socketio-rs(實(shí)現(xiàn)的rust方案已經(jīng)開(kāi)源到GitHub,點(diǎn)擊socketio-rs可訪問(wèn)),實(shí)際的 FeatureProbe 客戶(hù)端業(yè)務(wù)代碼和服務(wù)端業(yè)務(wù)代碼都相對(duì)比較簡(jiǎn)潔。
目前FeatureProbe 使用 Apache 2.0 License 協(xié)議已經(jīng)完全開(kāi)源。你可以從 GitHub 或 Gitee 上搜索FeatureProbe獲取到所有源代碼。