基於 websocket 的多端橋接平臺
1. 要除錯什麼
我們主要要知道除錯什麼,最終回去到什麼樣子的結果:
除錯介面,傳入介面地址,即可獲取對應的結果;並且可以同時除錯多個裝置;
除錯jsapi,輸入對應的方法,則即可在新聞客戶端中展示出效果。
在除錯介面方面,其實我們有一種方法可以方便地進行除錯,但有兩個限制條件:Android系統和測試版的客戶端,這樣通過 Chrome瀏覽器進行橋接。但這種方式,在 iOS 系統和正式版的客戶端中,就失效了。
2. websocket 的特性
WebSocket 協議的最大特點就是,伺服器可以主動向客戶端推送資訊,客戶端也可以主動向伺服器傳送資訊,是真正的雙向平等對話,屬於伺服器推送技術的一種。
其他特點包括:
建立在 TCP 協議之上,伺服器端的實現比較容易。
與 HTTP 協議有著良好的相容性。預設埠也是 80 和 443,並且握手階段採用 HTTP 協議,因此握手時不容易遮蔽,能通過各種 HTTP 代理伺服器。
資料格式比較輕量,效能開銷小,通訊高效。
可以傳送文字,也可以傳送二進位制資料。
沒有同源限制,客戶端可以與任意伺服器通訊。
協議識別符號是 ws(如果加密,則為 wss),伺服器網址就是 URL。
3. 建立 socket 連線
為了滿足我們在第 1 部分設定的除錯目標,我們這裡要實現的功能有:
PC 端相當於房主,建立房間後,其他裝置可以進入到該房間,一個裝置只能進入到一個房間中;
客戶端有斷線重連的機制,當客戶端斷開連線後,可以嘗試重連;
服務端維護一個心跳檢測的機制,當有新裝置進入或者之前的裝置退出時,要及時地更新當前房間中的裝置列表;
3.1 如何建立房間
在瀏覽器上輸入房間的標識,若瀏覽器與服務端成功建立起 websocket 連線後,則在瀏覽器端建立對應的二維碼。用微信/手 Q 或者其他掃描二維碼的裝置進行掃描,即可通過提前設定的 scheme 協議,跳轉到新聞客戶端裡對應的除錯頁面。
若客戶端裡也與服務端成功建立 websocket 連線後,則相當於進入房間成功,PC 端會出現一個對應的圖示。
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");
});
3.2 客戶端的斷線重現機制
在移動端中的頁面有個特點,當螢幕黑屏後,或者因為其他的原因,客戶端會自動斷開 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);
});
});
3.3 心跳檢測
就像我們在 QQ 群裡聊天一樣,哪個人線上要一目瞭然,若有人進入到聊天群,或者有人退出了,都要通知房主,並及時地更新群列表。
心跳檢測主要有 2 種方式:客戶端發起的心跳檢測和服務端維護的心跳檢測。我們稍微講解下這兩種:
客戶端發起的心跳:每隔一段固定的時間,向伺服器端傳送一個 ping 資料,如果在正常的情況下,伺服器會返回一個 pong 給客戶端,如果客戶端通過 onmessage 事件能監聽到的話,說明請求正常。
服務端維護的心跳:每隔一段時間,檢測所有連線的狀態,若狀態為斷開時,則將其從列表中剔除。
我在這裡使用的是服務端維護的心跳檢測,當房間裡的裝置數量發生變化時,則服務端向客戶端推送最新的裝置列表:
// 持續監測客戶端的連線狀態
// 若已斷開連線,則將客戶端清除
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);
4. 進行介面的除錯
我們在第 3 節已經成功把 PC 端和新聞客戶端連線起來了,那麼怎麼進行雙端資料的通訊?
4.1 介面的除錯
我們在這裡要傳入 3 個欄位:
serverId: 即房間號,服務端要將資訊廣播給所有帶有 serverId 的成員;
type: 型別,這條指令是要做什麼的;
msg: 傳入的引數;
在介面除錯的過程中,則傳入的引數是:
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
}
});
這樣就能在前端展示出結果了,而且是真實的資料請求。
4.2 歷史記錄的儲存
歷史記錄這塊,我們周邊的同學在試用的過程中,還是非常迫切需要的需求。要不然每次要測試之前的介面地址時,都需要重新輸入或者貼上,非常不方便。
我們把使用者請求的 URL、返回的結果、cookie、裝置資訊等比較完整的資訊儲存到 boss 中,而本地只儲存歷史的 URL,當用戶需要再次測試之前的介面時,點選一下即可。若需要檢視之前除錯的介面,可以去鷹眼上進行檢視。
本地採用的是localStorage的方式進行儲存。還有更重要的是,我們也使用mobx的響應式工具,能夠在使用者完成這次請求後,馬上在側邊的歷史記錄裡看到結果。
5. 新聞客戶端內jsapi 的除錯
除了可以除錯介面外,還可以進行一些新聞客戶端內的 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 });
});
});
};
http://www.xihuanfan.com 手機遊戲下載
6. 總結
到這裡,我的“基於 websocket 的多端橋接平臺”基本上已經構建完畢了。不過還是有 2 個問題要簡要的說明下。
6.1 為什麼要手動輸入 serverId
最開始想著使用者建立房間時,由系統隨機產生一個 uuid,但後來想,如果使用者重新整理頁面了,這個 uuid 就會發生變化,導致無法連線到之前的 uuid,所以這裡就換成了手動輸入。
6.2 如何保證一個客戶端的 socket 請求都進入到同一個程序中
當我們後臺採用多個程序時,若使用者的請求我們不做干預,會造成請求的隨機訪問,產生 400 的請求,畢竟最開始連線在 A 程序中,現在發起的請求到 B 程序中,B 程序不知道怎麼處理了。
這裡有多種方式可以進行處理:
方法 | 介紹 | 優點 | 缺點 |
---|---|---|---|
一致性 hash 演算法 | 所有的主機和連線都分配到 0 ~ 2^32-1 的虛擬圓中 | 1. 適用在大規模的應用; 2. 某個主機或者程序掛掉後,影響小 |
實現比較複雜 |
nginx 分配 | 自帶的 ip_hash 可實現負載均衡; 同一 ip 會被分配給固定的後端伺服器 |
配置方便 | 可能會集中到某個程序中 |
我這裡的平臺是內部的除錯平臺,使用者量不大,殺雞焉用牛刀,而且我們只有一臺機器,因此我們考慮的是同一個 IP 進入到同一個程序中。這裡我借用裡 nginx 中的 ip_hash 思想:當請求來到主程序後,我這裡對 IP 進行加權計算後,然後按照程序的個數進行取模。
顯然這種方式也有可能存在一個程序中 socket 連線過多的問題,不過在使用者量不多的時候完全可以接受(針對這個問題我也考慮了別的方法,例如瀑布流的方式,每次給子程序分配連線的時候,都首先獲取到連線數最少的那個程序,然後連線分配給這個程序,不過還要維護一個表,每次都要計算)。
6.4 多程序之間的通訊
同一個房間裡,當 PC 端的 socket 連線和多個移動端的連線不在同一個程序中時,就會存在跨程序的問題。一個極端的例子,每個 socket 連線都在不同的程序中,那麼就要考慮如何通知其他的程序,需要給客戶端傳送請求了。
比較簡單的方式利用我們的機制,每個 PC 端的使用者就是房主,可以建立一個房間,移動裝置就是房間中的成員,每個房間都是獨立的,互不干擾。這樣我們把房間裡所有的 socket 連線,通過房間的標識,都放到同一個程序中,這樣就沒有跨程序的問題了。但這種方式存在的一個問題是:一個房間裡的連線過多時,都需要這同一個程序來承擔,而別的程序卻閒著的。
還有可以使用 redis:利用 redis 的釋出/訂閱者模式,將當前程序中的房間標識和資訊廣播到其他的程序中,其他程序中有相同房間標識的 socket 連線,進行相應的操作。