最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會(huì)員登陸 & 注冊(cè)

IM跨平臺(tái)技術(shù)學(xué)習(xí)(四):蘑菇街基于Electron開(kāi)發(fā)IM客戶(hù)端的技術(shù)實(shí)踐

2022-10-13 14:03 作者:nickkckckck  | 我要投稿
本文由蘑菇街前端技術(shù)團(tuán)隊(duì)分享,原題“Electron 從零到一”,有修訂和改動(dòng)。

1、引言

本系列文章的前面幾篇主要是從Electron技術(shù)本身進(jìn)行了討論(包括:第1篇初步了解Electron、第2篇進(jìn)行了快速開(kāi)始和技術(shù)體驗(yàn)、第3篇基于實(shí)際開(kāi)發(fā)考慮的技術(shù)棧選型等),各位讀者也應(yīng)該對(duì)Electron的開(kāi)發(fā)有了較為深入的了解。

本篇將回到IM即時(shí)通訊技術(shù)本身,根據(jù)蘑菇街的實(shí)際技術(shù)實(shí)踐,總結(jié)和分享基于Electron開(kāi)發(fā)跨平臺(tái)IM客戶(hù)端的過(guò)程中,需要考慮的典型技術(shù)問(wèn)題以及我們的解決方案。希望能給你帶來(lái)幫助。

學(xué)習(xí)交流:

- 移動(dòng)端IM開(kāi)發(fā)入門(mén)文章:《新手入門(mén)一篇就夠:從零開(kāi)發(fā)移動(dòng)端IM》

- 開(kāi)源IM框架源碼:https://github.com/JackJiang2011/MobileIMSDK(備用地址點(diǎn)此)

(本文已同步發(fā)布于:http://www.52im.net/thread-4051-1-1.html

2、系列文章

本文是系列文章中的第4篇,本系列總目錄如下:

  • 《IM跨平臺(tái)技術(shù)學(xué)習(xí)(一):快速了解新一代跨平臺(tái)桌面技術(shù)——Electron》

  • 《IM跨平臺(tái)技術(shù)學(xué)習(xí)(二):Electron初體驗(yàn)(快速開(kāi)始、跨進(jìn)程通信、打包、踩坑等)》

  • 《IM跨平臺(tái)技術(shù)學(xué)習(xí)(三):vivo的Electron技術(shù)棧選型、全方位實(shí)踐總結(jié)》

  • 《IM跨平臺(tái)技術(shù)學(xué)習(xí)(四):蘑菇街基于Electron開(kāi)發(fā)IM客戶(hù)端的技術(shù)實(shí)踐》(* 本文)

  • 《IM跨平臺(tái)技術(shù)學(xué)習(xí)(五):融云基于Electron的IM跨平臺(tái)SDK改造實(shí)踐總結(jié)》(稍后發(fā)布.. )

  • 《IM跨平臺(tái)技術(shù)學(xué)習(xí)(六):網(wǎng)易云信基于Electron的IM消息全文檢索技術(shù)實(shí)踐》(稍后發(fā)布.. )

3、IM消息的加密和解密

3.1需求背景

對(duì)IM聊天軟件而言,聊天消息的保密性就比較重要了,誰(shuí)也不希望自己的聊天內(nèi)容泄露甚至暴露在眾人的前面。

所以在收發(fā)IM信息的時(shí)候,我們需要對(duì)信息做一些加密解密操作,保證信息在網(wǎng)絡(luò)中傳輸?shù)臅r(shí)候是加密的狀態(tài)。

3.2簡(jiǎn)單的實(shí)現(xiàn)方法

可能大家會(huì)說(shuō):這還不簡(jiǎn)單?項(xiàng)目里寫(xiě)個(gè)加密解密的方法——收到消息時(shí)候先解密,發(fā)送消息時(shí)候先加密,服務(wù)端收到加密消息直接存儲(chǔ)起來(lái)。

這樣寫(xiě)理論上也沒(méi)有問(wèn)題,不過(guò)客戶(hù)端直接寫(xiě)加解密方法有一些不好的地方。

比如:

  • 1)容易逆向:前端代碼比較容易被逆向;

  • 2)性能較差:用戶(hù)可能加了很多群組,各群組中都會(huì)收到很多消息,前端處理起來(lái)比較慢;

  • 3)多端實(shí)現(xiàn):如果都在客戶(hù)端實(shí)現(xiàn)加解密算法,那么 ios, android 等不同客戶(hù)端,因?yàn)槭褂玫拈_(kāi)發(fā)語(yǔ)言不同,都要分別實(shí)現(xiàn)相同的算法,增加維護(hù)成本。

3.3我們的方案

我們使用?C++ Addons?提供的能力,在 c++ sdk 中實(shí)現(xiàn)加解密算法,讓 js 可以像調(diào)用 Node 模塊一樣去調(diào)用 c++ sdk 模塊。這樣就一次性解決了上面提到的所有問(wèn)題。

技術(shù)原理如下圖:

開(kāi)發(fā)完 addon,使用?node-gyp?來(lái)構(gòu)建 C++ Addons。node-gyp 會(huì)根據(jù) binding.gyp 配置文件調(diào)用各平臺(tái)上的編譯工具集來(lái)進(jìn)行編譯。

如果要實(shí)現(xiàn)跨平臺(tái),需要按不同平臺(tái)編譯 nodejs addon,在 binding.gyp 中按平臺(tái)配置加解密的靜態(tài)鏈接庫(kù)。

就像下面這樣:

{

????"targets": [{

????????"conditions": [

????????????["OS=='mac'", {

????????????????"libraries": [

????????????????????"<(module_root_dir)/lib/mac/security.a"

????????????????]

????????????}],

????????????["OS=='win'", {??????????????? "libraries": [??????????????????? "<(module_root_dir)/lib/win/security.lib"]

????????????}],

????????????...

????????]

????????...

????}]

當(dāng)然也可以根據(jù)需要添加更多平臺(tái)的支持,如 linux、unix。

對(duì) c++ 代碼進(jìn)程封裝 addon 的時(shí)候,可以使用?node-addon-api。

node-addon-api 包對(duì)?N-API?做了封裝,并抹平了 nodejs 版本間的兼容問(wèn)題。封裝大大降低了非職業(yè) c++ 開(kāi)發(fā)編寫(xiě) node addon 的成本(關(guān)于 node-addon-api、N-API、NAN 等概念可以參考死月同學(xué)的文章《從暴力到 NAN 再到 NAPI——Node.js 原生模塊開(kāi)發(fā)方式變遷》)。

打包出 .node 文件后,可以在 electron 應(yīng)用運(yùn)行時(shí),調(diào)用 process.platform 判斷運(yùn)行的平臺(tái),分別加載對(duì)應(yīng)平臺(tái)的 addon。

if(process.platform === 'win32') {

????????addon = require('../lib/security_win.node');

} else{

????????addon = require('../lib/security_mac.node');

}

3.4進(jìn)一步學(xué)習(xí)

限于篇幅,本篇里沒(méi)辦法對(duì)IM的安全進(jìn)行更深入的總結(jié)和分享,感興趣的讀者可以詳讀:《IM聊天系統(tǒng)安全手段之通信連接層加密技術(shù)》、《IM聊天系統(tǒng)安全手段之傳輸內(nèi)容端到端加密技術(shù)》。

4、IM消息的序列化與反序列化

4.1需求背景

IM聊天消息直接通過(guò) JSON 編解碼和傳輸效率是比較低的,我們可以使用高效的消息序列化與反序列化方案。

4.2我們的方案

這里我們引入谷歌的?Protocol Buffer?提升效率。

PS:關(guān)于 Protocol Buffer 更多的介紹,可以查看《Protobuf通信協(xié)議詳解:代碼演示、詳細(xì)原理介紹等》。

node 環(huán)境中使用 Protocol Buffer 可以用?protobufjs?包。

npm i protobuff -S

然后通過(guò)?pbjs?命令將 proto 文件轉(zhuǎn)換成 pbJson.js

pbjs -t json-module --sparse --force-long -w commonjs -o src/im/data/pbJson.js proto/*.proto

要在 js 中支持后端 int64 格式數(shù)據(jù),需要使用?long?包配置下 protobuf。

var Long = require("long");

$protobuf.util.Long = Long;

$protobuf.configure();

$protobuf.util.LongBits.prototype.toLong = functiontoLong (unsigned) {

????returnnew $protobuf.util.Long(this.lo | 0, this.hi | 0, Boolean(unsigned)).toString();

};

后面就是消息的壓縮轉(zhuǎn)換了,將 js 字符串轉(zhuǎn)成 pb 格式。

import PbJson from './path/to/src/im/data/pbJson.js';

?

// 封裝數(shù)據(jù)

let encodedMsg = PbJson.lookupType('pb-api').ctor.encode(data).finish();

?

// 解封數(shù)據(jù)

let decodedMsg = PbJson.lookupType('pb-api').ctor.decode(buff);

5、網(wǎng)絡(luò)傳輸協(xié)議的選擇

開(kāi)發(fā)IM時(shí)可供選擇的網(wǎng)絡(luò)傳輸層協(xié)議有?UDP、TCP?等。UDP 實(shí)時(shí)性好,但是可靠性不好。這里我們選用 的是 TCP 協(xié)議。

?

PS:關(guān)于TCP和UDP的區(qū)別,以及該如何選擇,可以詳細(xì)閱讀這幾篇:

《快速理解TCP和UDP的差異》

《一泡尿的時(shí)間,快速搞懂TCP和UDP的區(qū)別》

《簡(jiǎn)述傳輸層協(xié)議TCP和UDP的區(qū)別》

《為什么QQ用的是UDP協(xié)議而不是TCP協(xié)議?》

《移動(dòng)端即時(shí)通訊協(xié)議選擇:UDP還是TCP?》

應(yīng)用層分別使用 WebSocket 協(xié)議保持長(zhǎng)連接保證實(shí)時(shí)傳輸消息,HTTPS 協(xié)議傳輸消息外的其他狀態(tài)數(shù)據(jù)。

這里給個(gè)例子實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 WebSocket 管理類(lèi):

import { EventEmitter } from 'events';

const webSocketConfig = 'wss://xxxx';

class SocketServer extends EventEmitter {

????connect () {

????????if(this.socket){

????????????????????????this.removeEvent(this.socket);

????????????????????????this.socket.close();

????????????????}

????????????????this.socket = newWebSocket(webSocketConfig);

????????????????this.bindEvents(this.socket);

????????returnthis;

????}

????close () {}

????async getSocket () {

????}

????bindEvents() {}

????removeEvent() {}

????onMessage (e) {

????????// 消息解包

????????let decodedMSg = 'xxx;

????????this.emit(decodedMSg);

????}

????async send(sendData) {

????????const socket = await this.getSocket()

????????socket.send(sendData);

????}

????...

}

如果你對(duì)WebSocket協(xié)議還不了解,可以從這兩篇入門(mén)文章入手學(xué)習(xí):《新手快速入門(mén):WebSocket簡(jiǎn)明教程》、《WebSocket從入門(mén)到精通,半小時(shí)就夠!》

對(duì)于HTTPS 協(xié)議的話(huà)就不多介紹了,大家天天用。如果你還不是太了解,可以讀讀這兩篇:《如果這樣來(lái)理解HTTPS原理,一篇就夠了》、《一分鐘理解 HTTPS 到底解決了什么問(wèn)題》。

6、IM的私有數(shù)據(jù)通信協(xié)議

上幾節(jié)我們實(shí)現(xiàn)了把IM聊天消息序列化和反序列化,也實(shí)現(xiàn)了通過(guò) WebSocket 發(fā)送和接收消息,但還不能直接這樣發(fā)送聊天消息。

因?yàn)槲覀冞€需要一個(gè)數(shù)據(jù)通信協(xié)議(什么是數(shù)據(jù)通信協(xié)議?可以讀讀這篇《理論聯(lián)系實(shí)際:一套典型的IM通信協(xié)議設(shè)計(jì)詳解》)。也就是給通信層的原始“消息“增加一些屬性,比如:id 用來(lái)關(guān)聯(lián)收發(fā)的消息、type 標(biāo)記消息類(lèi)型、version 標(biāo)記、接口的版本,api 標(biāo)記調(diào)用的接口等。

然后據(jù)此定義一個(gè)編碼格式,用 ArrayBuffer 將消息包裝起來(lái),放到 WebSocket 中發(fā)送,以二進(jìn)制流的方式傳輸。

協(xié)議設(shè)計(jì)需要保證足夠的擴(kuò)展性,不然修改的時(shí)候需要同時(shí)修改前后端,比較麻煩。

下面是個(gè)簡(jiǎn)化的例子:

class PocketManager extends EventEmitter {

????encode (id, type, version, api, payload) {

????????????????let headerBuffer = Buffer.alloc(8);

????????let payloadBuffer = Buffer.alloc(0);

????????let offset = 0;

????????let keyLength = Buffer.from(id).length;

????????headerBuffer.writeUInt16BE(keyLength, offset);

????????offset += 2;

????????headerBuffer.write(id, offset, offset + keyLength, 'utf8');

????????...

????????payloadBuffer = Buffer.from(payload);

????????????????returnBuffer.concat([headerBuffer, payloadBuffer], 8 + payloadBuffer.length);

????}

????decode () {}

}

關(guān)于IM私有數(shù)據(jù)通信協(xié)議/格式的設(shè)計(jì),可以參考《一套海量在線(xiàn)用戶(hù)的移動(dòng)端IM架構(gòu)設(shè)計(jì)實(shí)踐分享(含詳細(xì)圖文)》一文中的“3、協(xié)議設(shè)計(jì)”這一節(jié)。

另外,如果你自認(rèn)為對(duì)于IM的理論知識(shí)很匱乏或不成體系,可以從《新手入門(mén)一篇就夠:從零開(kāi)發(fā)移動(dòng)端IM》入手,系統(tǒng)地進(jìn)行學(xué)習(xí)。

7、IM模塊多進(jìn)程優(yōu)化

IM 界面有很多模塊:聊天模塊,群管理模塊,歷史消息模塊等。

另外:消息通信邏輯不應(yīng)該和界面邏輯放一個(gè)進(jìn)程里,避免界面卡頓時(shí)候影響消息的收發(fā)。

這里有個(gè)簡(jiǎn)單的實(shí)現(xiàn)方法,把不同的模塊放到 electorn 不同的窗口中,因?yàn)椴煌拇翱谟刹煌倪M(jìn)程管理,我們就不需要自己管理進(jìn)程了。

下面實(shí)現(xiàn)一個(gè)窗口管理類(lèi):

import { EventEmitter } from 'events';

class BaseWindow extends EventEmitter {

????open () {}

????close () {}

????isExist () {}

????destroy() {}

????createWindow() {

????????this.win = newBrowserWindow({

????????????????????????...this.browserConfig,

????????????????});

????}

????...

}

其中 browserConfig 可以在子類(lèi)中設(shè)置,不同窗口可以繼承這個(gè)基類(lèi)設(shè)置自己窗口屬性。

通信模塊用作后臺(tái)收發(fā)數(shù)據(jù),不需要顯示窗口,可以設(shè)置窗口 width = 0,height = 0 :

class ImWindow extends BaseWindow {

????browserConfig = {

????????????????width: 0,

????????????????height: 0,

????????????????show: false,

????}

????...

}

8、IM數(shù)據(jù)的本地存儲(chǔ)?

8.1背景

IM 軟件中可能會(huì)有幾千個(gè)聯(lián)系人信息,無(wú)數(shù)的聊天記錄。如果每次都通過(guò)網(wǎng)絡(luò)請(qǐng)求訪問(wèn),比較浪費(fèi)帶寬,影響性能。

那么是否有什么優(yōu)化手段呢?

8.2討論

在Electorn 中可以使用 localstorage, 但是 localstorage 有大小限制,實(shí)際大多只能存 5M 信息,超過(guò)存入大小會(huì)報(bào)錯(cuò)。

有些同學(xué)可能還會(huì)想到?websql, 但這個(gè)技術(shù)標(biāo)準(zhǔn)已經(jīng)被廢棄了。

瀏覽器內(nèi)置的?indexedDB?也是一個(gè)可選項(xiàng)。

不過(guò)這個(gè)也有限制,也沒(méi)有 sqlite 一樣豐富的生態(tài)工具可以用。

8.3方案

這里我們選用 sqlite,在 node 中使用 sqlite 可以直接用?sqlite3?包。

可以先寫(xiě)個(gè) DAO 類(lèi):

import sqlite3 from 'sqlite3';

class DAO {

????constructor(dbFilePath) {

????????this.db = newsqlite3.Database(dbFilePath, (err) => {

????????????//

????????});

????}

????run(sql, params = []) {

????????returnnewPromise((resolve, reject) => {

????????????this.db.run(sql, params, function(err) {

????????????????if(err) {

????????????????????reject(err);

????????????????} else{

????????????????????resolve({ id: this.lastID });

????????????????}

????????????});

????????});

????}

????...

}

再寫(xiě)個(gè) base Model:

class BaseModel {

????constructor(dao, tableName) {

????????this.dao = dao;

????????this.tableName = tableName;

????}

????delete(id) {

????????returnthis.dao.run(`DELETE FROM ${this.tableName} WHERE id = ?`, [id]);

????}

????...

}

其他 Model 比如消息、聯(lián)系人等 Model 可以直接繼承這個(gè)類(lèi),復(fù)用 delete/getById/getAll 之類(lèi)的通用方法。

如果不喜歡手動(dòng)編寫(xiě) SQLite 語(yǔ)句,可以引入?knex?語(yǔ)法封裝器。

當(dāng)然也可以直接時(shí)髦點(diǎn)用上?orm?,比如?typeorm?什么的。

使用如下:

const dao = newAppDAO('path/to/database-file.sqlite3');

const messageModel = newMessageModel(dao);

9、IM新消息托盤(pán)圖標(biāo)閃爍

在Electron 中沒(méi)有提供專(zhuān)用的?tray?閃爍的接口,我們可以簡(jiǎn)單的使用切換 tray 圖標(biāo)來(lái)實(shí)現(xiàn)這個(gè)功能。

import { Tray, nativeImage } from 'electron';

?

class TrayManager {

????...

????setState() {

????????// 設(shè)置默認(rèn)狀態(tài)

????}

????????startBlink(){

????????????????if(!this.tray){

????????????????????????return;

????????????????}

????????????????let emptyImg = nativeImage.createFromPath(path.join(__dirname, './empty.ico'));

????????????????let noticeImg = nativeImage.createFromPath(path.join(__dirname, './newMsg.png'));

????????????????let visible;

????????????????clearInterval(this.trayTimer);

????????????????this.trayTimer = setInterval(()=>{

????????????????????????visible = !visible;

????????????????????????if(visible){

????????????????????????????????this.tray.setImage(noticeImg);

????????????????????????}else{

????????????????????????????????this.tray.setImage(emptyImg);

????????????????????????}

????????????????},500);

????????}

?

????????//停止閃爍

????????stopBlink(){

????????????????clearInterval(this.trayTimer);

????????????????this.setState();

????????}

}

10、IM客戶(hù)端版本更新

一般有幾種不同的更新策略,可以一種或幾種結(jié)合使用,提升體驗(yàn)。

第一種:是整個(gè)軟件更新。這種方式比較暴力,體驗(yàn)不好,打開(kāi)應(yīng)用檢查到版本變更,直接重新下載整個(gè)應(yīng)用替換老版本。改一行代碼,讓用戶(hù)沖下百來(lái)兆的文件。

第二種:是檢測(cè)文件變更,下載替換老文件進(jìn)行升級(jí)。

第三種:是直接將 view 層文件放在線(xiàn)上,electron 殼加載線(xiàn)上頁(yè)面訪問(wèn)。有變更發(fā)布線(xiàn)上頁(yè)面就可以。

關(guān)于版本更新,在本系列的上篇《vivo的Electron技術(shù)棧選型、全方位實(shí)踐總結(jié)》也有提及,可以回顧一下。

11、進(jìn)程間通信

上一篇文章中,有同學(xué)問(wèn)怎么處理進(jìn)程間通信。

electron 進(jìn)程間通信主要用到?ipcMain?和?ipcRenderer。

?

可以先寫(xiě)個(gè)發(fā)消息的方法:

import { remote, ipcRenderer, ipcMain } from 'electron';

?

function sendIPCEvent(event, ...data) {

????if(require('./is-electron-renderer')) {

????????constcurrentWindow = remote.getCurrentWindow();

????????if(currentWindow) {

????????????currentWindow.webContents.send(event, ...data);

????????}

????????ipcRenderer.send(event, ...data);

????????return;

????}

????ipcMain.emit(event, null, ...data);

}

export defaultsendIPCEvent;

這樣不管在主進(jìn)程還是渲染進(jìn)程,直接調(diào)用這個(gè)方法就可以發(fā)消息。

對(duì)于某些特定功能的消息,還可以做一些封裝,比如所有推送消息可以封裝一個(gè)方法,通過(guò)方法中的參數(shù)判斷具體推送的消息類(lèi)型。main 進(jìn)程中根據(jù)消息類(lèi)型,處理相關(guān)邏輯,或者對(duì)消息進(jìn)行轉(zhuǎn)發(fā)。

class ipcMainManager extends EventEmitter {

????constructor() {

????????ipcMain.on('imPush', (name, data) => {

????????????this.emit(name, data);

????????})

????????this.listern();

????}

????listern() {

????????this.on('imPush', (name, data) => {

????????????//

????????});

????}

}

class ipcRendererManager extends EventEmitter {

????push (name, data) {

????????ipcRenderer.send('imPush', name, data);

????}

}

12、其他雜項(xiàng)

還有同學(xué)提到日志處理功能。

這個(gè)和 Electron 關(guān)系不大,是 node 項(xiàng)目通用的功能。

可以選用?winston?之類(lèi)第三方包。

本地日志的話(huà)注意一下存儲(chǔ)的路徑,定期清理等功能點(diǎn),遠(yuǎn)程日志提交到接口就可以了。

獲取路徑可以寫(xiě)些通用的方法,如:

import electron from 'electron';

functiongetUserDataPath() {

????if(require('./is-electron-renderer')) {

????????returnelectron.remote.app.getPath('userData');

????}

????returnelectron.app.getPath('userData');

}

export defaultgetUserDataPath;

13、參考資料

[1]?Protobuf通信協(xié)議詳解:代碼演示、詳細(xì)原理介紹等

[2]?IM聊天系統(tǒng)安全手段之通信連接層加密技術(shù)

[3]?IM聊天系統(tǒng)安全手段之傳輸內(nèi)容端到端加密技術(shù)

[4]?TCP/IP詳解?-?第11章·UDP:用戶(hù)數(shù)據(jù)報(bào)協(xié)議

[5]?TCP/IP詳解?-?第17章·TCP:傳輸控制協(xié)議

[6]?移動(dòng)端即時(shí)通訊協(xié)議選擇:UDP還是TCP?

[7]?WebSocket從入門(mén)到精通,半小時(shí)就夠!

[8]?如果這樣來(lái)理解HTTPS原理,一篇就夠了

[9]?一套海量在線(xiàn)用戶(hù)的移動(dòng)端IM架構(gòu)設(shè)計(jì)實(shí)踐分享(含詳細(xì)圖文)

[10]?理論聯(lián)系實(shí)際:一套典型的IM通信協(xié)議設(shè)計(jì)詳解

(本文已同步發(fā)布于:http://www.52im.net/thread-4051-1-1.html)

IM跨平臺(tái)技術(shù)學(xué)習(xí)(四):蘑菇街基于Electron開(kāi)發(fā)IM客戶(hù)端的技術(shù)實(shí)踐的評(píng)論 (共 條)

分享到微博請(qǐng)遵守國(guó)家法律
平罗县| 万宁市| 河源市| 富蕴县| 泽库县| 华蓥市| 卢龙县| 久治县| 株洲县| 临夏市| 扎囊县| 达日县| 茌平县| 万年县| 奉新县| 桃源县| 阿坝| 奉贤区| 成都市| 大连市| 广宁县| 南宁市| 南汇区| 葵青区| 海原县| 哈密市| 阿拉善盟| 永济市| 永胜县| 石阡县| 师宗县| 札达县| 西乌珠穆沁旗| 高青县| 文水县| 汾西县| 抚顺市| 宁德市| 安丘市| 马关县| 文昌市|