咱們如今的業務是基於新聞客戶端實現的,都要通過新聞客戶端的環境,進行先後端數據上的交互。可是咱們在調試過程當中,很是的不方便。javascript
一般使用的工具備:modheader, postman, fiddler 等,但這些工具都會存在的問題:html
針對這些存在的問題和不足,我基於 websocket 雙向通訊的特色,並實現了「多端橋接管理平臺」:經過在 PC 端上的操做,能夠直接在新聞客戶端內直接執行相應的命令,並將結果、cookie、設備信息等一塊兒返回到 PC 端。前端
咱們主要要知道調試什麼,最終回去到什麼樣子的結果:java
在調試接口方面,其實咱們有一種方法能夠方便地進行調試,但有兩個限制條件:Android系統
和測試版的客戶端
,這樣經過 Chrome 瀏覽器進行橋接。但這種方式,在 iOS 系統和正式版的客戶端中,就失效了。nginx
WebSocket 協議的最大特色就是,服務器能夠主動向客戶端推送信息,客戶端也能夠主動向服務器發送信息,是真正的雙向平等對話,屬於服務器推送技術的一種。web
其餘特色包括:redis
爲了知足咱們在第 1 部分設置的調試目標,咱們這裏要實現的功能有:算法
斷線重連
的機制,當客戶端斷開鏈接後,能夠嘗試重連;心跳檢測
的機制,當有新設備進入或者以前的設備退出時,要及時地更新當前房間中的設備列表;在瀏覽器上輸入房間的標識,若瀏覽器與服務端成功創建起 websocket 鏈接後,則在瀏覽器端建立對應的二維碼。用微信/手 Q 或者其餘掃描二維碼的設備進行掃描,便可經過提早設定的 scheme 協議,跳轉到新聞客戶端裏對應的調試頁面。後端
若客戶端裏也與服務端成功創建 websocket 鏈接後,則至關於進入房間成功,PC 端會出現一個對應的圖標。api
ws.open(serverId) .then(() => { // PC 端成功創建鏈接後 setStatus("linked"); // 更新頁面的狀態 // 生成二維碼 qrcode(`/tools/index.html#/newslist?serverId=${serverId}`).then(url => { setCodeUrl(url); }); }) .catch(e => { // 創建鏈接失敗 console.error(e); Modal.error({ title: "當前服務器出現問題啦,正在搶修中" }); setStatus("unlink"); });
在移動端中的頁面有個特色,當屏幕黑屏後,或者由於其餘的緣由,客戶端會自動斷開 socket 鏈接。
爲了方便進行調試,而不是每次在斷開鏈接後,須要手動點擊,或者從新進入頁面。我在這裏實現了一個簡單的斷線重連機制。websocket 鏈接斷開時,會執行onclose
的回調,所以,咱們能夠在 onclose 事件中進行再次重連的機制。
同時,爲了防止無限制的重連嘗試,我在這裏也進行了下限制,最多重連 3 次,3 次後尚未從新鏈接上,則中止鏈接;若重連成功,則將重連次數重置爲 3。
斷開鏈接時:
// 斷開鏈接時 ws.onclose(() => { timer = setTimeout(() => { setStatus("unlink"); setCodeUrl(""); }, 500); reconnectNum--; // 限制重連的次數 if (reconnectNum >= 0) { _open(); // 嘗試從新鏈接 } });
鏈接成功時:
ws.open(serverId).then(() => { // PC 端成功創建鏈接後 +reconnectNum = 3; +timer && clearTimeout(timer); setStatus("linked"); // 更新頁面的狀態 // 生成二維碼 qrcode(`/tools/index.html#/newslist?serverId=${serverId}`).then(url => { setCodeUrl(url); }); });
就像咱們在 QQ 羣裏聊天同樣,哪一個人在線要一目瞭然,如有人進入到聊天羣,或者有人退出了,都要通知房主,並及時地更新羣列表。
心跳檢測主要有 2 種方式:客戶端發起的心跳檢測和服務端維護的心跳檢測。咱們稍微講解下這兩種:
我在這裏使用的是服務端維護的心跳檢測
,當房間裏的設備數量發生變化時,則服務端向客戶端推送最新的設備列表:
// 持續監測客戶端的鏈接狀態 // 若已斷開鏈接,則將客戶端清除 let aliveClients = new Map(); let lastAliveLength = new Map(); setInterval(() => { let clients = {}; wss.clients.forEach(function each(ws) { if (ws.isAlive === false) { return ws.terminate(); } const serverId = ws.serverId; if (clients[serverId]) { clients[serverId].push(ws); } else { clients[serverId] = [ws]; } ws.isAlive = false; ws.ping(() => {}); }); for (let serverId in clients) { aliveClients.set(serverId, clients[serverId]); const length = clients[serverId].length; // 若當前serverId鏈接的設備數量發生變化,則發送消息 if (length !== lastAliveLength.get(serverId)) { // 想當前全部serverId的設備發送消息 sendAll("devices", clients[serverId], serverId); // 存儲上次當前serverId的鏈接數 lastAliveLength.set(serverId, length); } } const size = wss.clients.size; console.log("connection num: ", size, new Date().toTimeString()); }, 2000);
咱們在第 3 節已經成功把 PC 端和新聞客戶端鏈接起來了,那麼怎麼進行雙端數據的通訊?
咱們在這裏要傳入 3 個字段:
在接口調試的過程當中,則傳入的參數是:
const params = { type: "post", // 類型 msg: { // 參數 url: "https://api.prize.qq.com/v1/newsapp/answer/share/oneQ?qID=506336" } };
當客戶端正常完成接口的請求後,則將接口結果、cookie 和設備信息等返回到 PC 端:
// 請求的方法 const post = url => { if (window.TencentNews && window.TencentNews.post) { window.TencentNews.post(url, {}, window[id], { loginType: "qqorweixin" }, {}); } else if (window.TencentNews && window.TencentNews.postData) { window.TencentNews.postData(url, '{"a":"b"}', id, "requestErrorCallback"); } }; // 移動端向服務端發起的數據 ws.send({ type: "postCb", // 執行的結果 msg: { method: "post", result, cookie: document.cookie, appInfo } });
這樣就能在前端展現出結果了,並且是真實的數據請求。
歷史記錄這塊,咱們周邊的同窗在試用的過程當中,仍是很是迫切須要的需求。要否則每次要測試以前的接口地址時,都須要從新輸入或者粘貼,很是不方便。
咱們把用戶請求的 URL、返回的結果、cookie、設備信息等比較完整的信息存儲到 boss 中,而本地只存儲歷史的 URL,當用戶須要再次測試以前的接口時,點擊一下便可。若須要查看以前調試的接口,能夠去鷹眼上進行查看。
本地採用的是localStorage
的方式進行存儲。還有更重要的是,咱們也使用mobx
的響應式工具,可以在用戶完成此次請求後,立刻在側邊的歷史記錄裏看到結果。
除了能夠調試接口外,還能夠進行一些新聞客戶端內的 jsapi 調試。咱們新聞客戶端的 jsapi 有兩種調用的方式:
// 直接調用 window.TencentNews.login("qqorweixin", isLogined => console.log(isLogined)); // invoke方式調用 window.TencentNews.invoke("login", "qqorweixin", isLogined => console.log(isLogined));
這裏我選擇了使用invoke
的方式來調用 jsapi。
PC 端發起 jsapi 的調用:
ws.send({ type: "call", msg: { method: method, params: slice.call(arguments) } });
移動端在收到服務端發過來的請求後,進行 jsapi 的調用,並將執行的結果返回到 PC 端便可:
const handleNewsApi = async (msg: any): Promise<any> => { await tencentReady(); const { method, params } = msg; return new Promise(resolve => { window.TencentNews.invoke(method, ...params, (result: any) => { resolve({ method, result }); }); }); };
到這裏,個人「基於 websocket 的多端橋接平臺」基本上已經構建完畢了。不過仍是有 2 個問題要簡要的說明下。
最開始想着用戶建立房間時,由系統隨機產生一個 uuid,但後來想,若是用戶刷新頁面了,這個 uuid 就會發生變化,致使沒法鏈接到以前的 uuid,因此這裏就換成了手動輸入。
當咱們後臺採用多個進程時,若用戶的請求咱們不作干預,會形成請求的隨機訪問,產生 400 的請求,畢竟最開始鏈接在 A 進程中,如今發起的請求到 B 進程中,B 進程不知道怎麼處理了。
這裏有多種方式能夠進行處理:
方法 | 介紹 | 優勢 | 缺點 |
---|---|---|---|
一致性 hash 算法 | 全部的主機和鏈接都分配到 0 ~ 2^32-1 的虛擬圓中 | 1. 適用在大規模的應用;<br/>2. 某個主機或者進程掛掉後,影響小 | 實現比較複雜 |
nginx 分配 | 自帶的 ip_hash 可實現負載均衡;<br/>同一 ip 會被分配給固定的後端服務器 | 配置方便 | 可能會集中到某個進程中 |
我這裏的平臺是內部的調試平臺,用戶量不大,殺雞焉用牛刀,並且咱們只有一臺機器,所以咱們考慮的是同一個 IP 進入到同一個進程中。這裏我借用裏 nginx 中的 ip_hash 思想:當請求來到主進程後,我這裏對 IP 進行加權計算後,而後按照進程的個數進行取模。
顯然這種方式也有可能存在一個進程中 socket 鏈接過多的問題,不過在用戶量很少的時候徹底能夠接受(針對這個問題我也考慮了別的方法,例如瀑布流的方式,每次給子進程分配鏈接的時候,都首先獲取到鏈接數最少的那個進程,而後鏈接分配給這個進程,不過還要維護一個表,每次都要計算)。
同一個房間裏,當 PC 端的 socket 鏈接和多個移動端的鏈接不在同一個進程中時,就會存在跨進程的問題。一個極端的例子,每一個 socket 鏈接都在不一樣的進程中,那麼就要考慮如何通知其餘的進程,須要給客戶端發送請求了。
比較簡單的方式利用咱們的機制,每一個 PC 端的用戶就是房主,能夠建立一個房間,移動設備就是房間中的成員,每一個房間都是獨立的,互不干擾。這樣咱們把房間裏全部的 socket 鏈接,經過房間的標識,都放到同一個進程中,這樣就沒有跨進程的問題了。但這種方式存在的一個問題是:一個房間裏的鏈接過多時,都須要這同一個進程來承擔,而別的進程卻閒着的。
還有可使用 redis:利用 redis 的發佈/訂閱者模式,將當前進程中的房間標識和信息廣播到其餘的進程中,其餘進程中有相同房間標識的 socket 鏈接,進行相應的操做。
歡迎個人公衆號,多多交流:
原文出處:https://www.cnblogs.com/xumengxuan/p/12582184.html