導語
2021騰訊遊戲年度釋出會線上上舉行。今年,釋出會以“超級數字場景”戰略理念為核心,傳遞對遊戲認知、產業邊界的建設性思考,並通過60餘款遊戲產品與內容集中釋出,展現騰訊遊戲為玩家帶來的豐富體驗與多元價值。
本次釋出會再次選擇了雲開發 CloudBase 作為技術選型之一,以極低的成本實現了實時彈幕系統,並保障穩定執行,為遊戲愛好者帶來了優質互動體驗。下文將重點介紹專案組使用雲開發實現彈幕功能的全過程。
“各部門注意,前方高能!”
一、業務背景
2021騰訊遊戲年度釋出會開發了專屬小程式,包含直播、抽獎、觀看回放等功能,其中所有的彈幕功能均基於雲開發的實時資料推送實現。
在進行彈幕功能的技術選型前,開發同學梳理了業務場景:
- 彈幕實時互動
- 允許少量的彈幕丟失
- 僅釋出會直播當晚使用
- 敏感資訊/關鍵字過濾
在綜合考慮成本、穩定性、與小程式適配性等多個方面後,專案最終選擇了雲開發的實時資料推送功能,早在去年的釋出會裡,專案組就使用了雲開發的實時資料推送來實現直播節目單進度提醒等功能,在此基礎上,把彈幕也統一搬上雲開發。
二、技術實踐
開發思路
一開始想直接把全部使用者的彈幕集合直接監聽,但官方限制單次監聽資料不能大於5000條,且監聽資料條越多初始化效能越差,超出上限會拋錯並停止監聽。最後設計為:使用者彈幕插入集合a,監聽資料集合b,使用雲函式的定時器定期合併彈幕,並更新到對應的正在監聽的資料記錄上(如圖)。
這樣保證了使用者監聽的資料記錄為恆定數量,這裡採用10條記錄(迴圈陣列)彙總彈幕資料,每秒更新當前時間戳的所有彈幕到 index = timestamp%10 的資料記錄上,同時彈幕重新整理頻率固定為1s,減輕前端由於資料頻繁改動而不斷 callback/ 渲染的效能消耗。
程式碼演示
使用者傳送彈幕部分程式碼:
exports.main = async (event, context) => {
// ...省略部分鑑權/黑名單/校驗內容安全邏輯
let time = Math.ceil(new Date().getTime() / 1000);
// 插入彈幕
let res = await db.collection('danmu_all').add({
data: {
openid,
content,
time,
},
});
return {err: 0, msg: 'ok'};
};
彈幕合併處理:
exports.main = async (event, context) => {
// ....省略一部分非關鍵程式碼
// 只取其中100條彈幕,可動態調整
let time = Math.ceil(new Date().getTime() / 1000) - 1;
const result = await db
.collection('danmu_all')
.where({time}).limit(100).get();
let msg = [];
for (let i of result.data) {
msg.push({
openid: i.openid,
content: i.content,
});
}
// 更新迴圈陣列的對應位置
db
.collection('watch_collection')
.where({index: time % 10})
.update({
data: {msg,time},
});
return msg;
}
前端處理訊息通知,注意不要重複 watch。其中如果開啟了雲開發的匿名登入,那 H5 端的頁面同樣可以使用同步彈幕功能:
this.watcher = db.collection('watch_collection').watch({
onChange: function(snapshot) {
for (let i of snapshot.docChanges) {
// 忽略非更新的資訊
if (!i.doc || i.queueType !== 'update') {
continue;
}
switch (i.doc.type) {
// ...省略其他型別的訊息處理
case 'danmu':
// 彈幕渲染
livePage.showServerBarrage(i.doc.msg);
break;
}
}
},
});
至此,整個彈幕的核心功能已經完全實現。
二次優化
跑了一段時間後發現偶現丟棄幾秒內的彈幕,後面檢視執行日誌,發現即使配置定時器為每秒執行一次,實際生產中也不是嚴格每秒執行一次,有時候會跳過1-3秒去執行,這裡另外使用了 redis 去標記當前處理的進度,即使有跳過的秒數,也能往前回溯未處理的時間進行補錄。其中雲函式使用 redis 的教程可以檢視官方雲函式使用 redis 教程。
使用者傳送彈幕部分程式碼新增標記程式碼:
exports.main = async (event, context) => {
// ...省略部分鑑權跟校驗內容安全程式碼
// ...省略插入程式碼
// 標記合併任務
await redisClient.zadd('danmu_task', time, time+'')
};
彈幕合併處理,注意:要 redis5.0 以上的才支援 zpopmin 命令,如需購買,需要選對版本。
exports.main = async (event, context) => {
//當前秒
let time = Math.ceil(new Date().getTime() / 1000) - 1;
while (true) {
// 彈出最小的任務
let minTask = await redisClient.zpopmin('danmu_task');
// 當前無任務
if (minTask.length <= 0) {
return;
}
// 當前秒的任務,往回塞,並結束
if (parseInt(minTask[0]) > time) {
redisClient.zadd('danmu_task', minTask[1], minTask[0]);
return;
}
// 執行合併任務
await danmuMerge(time);
}
};
安全邏輯上也做了一定的策略,如本地先渲染髮送的彈幕,客戶端收到彈幕推送時,判斷 openid 為自己時候不渲染,這樣即使使用者的彈幕被過濾掉也能在本地展現,保留一定的使用者體驗。
另外,單個雲函式的例項上限是1000,如果確定當晚流量比較大,可以考慮用多個雲函式分攤流量。
管理後臺的實現
同時,利用 watch 功能可以做到管理後臺同步實時重新整理客戶端的彈幕,達到管理的目的,同一份程式碼前端和管理端都能複用:
節選部分管理後臺程式碼:
methods: {
stop() {
this.watcher.close();
},
},
beforeDestroy() {
this.watcher.close();
},
mounted() {
this.app = this.$store.state.app;
this.db = this.app.database();
let that = this;
this.watcher = this.db.collection('danmu_merge').watch({
onChange(snapshot) {
for (let d of snapshot.docChanges) {
for (let v of d.msg) {
that.danmu.unshift(v);
}
}
if (that.danmu.length > 500) {
that.danmu = that.danmu.slice(0, 499);
}
},
});
集合的讀許可權設定在實時資料推送裡同樣生效,如果許可權是設定為僅可讀使用者自己的資料,則監聽的時候無法監聽到非使用者自己建立的數。
Tips
當時沒注意到 watch 對資料庫許可權限制的問題,資料庫許可權預設為僅建立者可讀寫,迴圈陣列第一次初始化是開發過程中在客戶端建立,預設新增了當前使用者的openid,導致其他使用者無法讀取到 merge 的資料,解決方法:刪除 openid 欄位或設定許可權為全部人可讀。
集合的讀許可權設定在實時資料推送裡同樣生效,如果許可權是設定為僅可讀使用者自己的資料,則監聽的時候無法監聽到非使用者自己建立的數。
三、專案成果與價值
基於雲開發的雲函式、實時資料推送、雲資料庫等能力,專案全程平穩執行,即便在釋出會當晚流量峰值的時候,彈幕的寫入執行穩定。在監聽方面(讀),watch 的效能能夠穩定支援百萬級同時線上。
最終,2名研發僅用2天就完成了彈幕系統的開發和除錯。而在費用方面,支撐整個專案彈幕系統執行的總費用僅為100元左右,主要集中在資料庫讀寫和雲函式呼叫(目前監聽資料庫實時資料功能處於免費階段,不會計算到資料庫讀取費用上),拋去其他模組的費用,實際彈幕模組可能僅消耗了小几十塊錢,費用大大低於預期,相對比傳統即時通訊等方案節省超過數十倍。
總體上,專案採用雲開發,具備以下優勢:
- 自帶彈性擴縮容,可以抗住瞬時高併發流量,保障直播順利進行;
- 費用便宜,只收取雲函式呼叫和資料庫讀寫費用,實時資料推送免費使用,非常適合專案;
- 安全穩定,專案的訪問都基於雲開發自帶的微信私有鏈路實現,保證安全性;
- 自由度高,能夠契合其他開發框架和服務。