本文由蘑菇街前端技术团队分享,原题“Electron 从零到一”,有修订和改动。

1、引言

本系列文章的前面几篇主要是从Electron技术本身进行了讨论(包括:第1篇初步了解Electron、第2篇进行了快速开始和技术体验、第3篇基于实际开发考虑的技术栈选型等),各位读者也应该对Electron的开发有了较为深入的了解。

本篇将回到IM即时通讯技术本身,根据蘑菇街的实际技术实践,总结和分享基于Electron开发跨平台IM客户端的过程中,需要考虑的典型技术问题以及我们的解决方案。希望能给你带来帮助。

学习交流:

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

2、系列文章

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

3、IM消息的加密和解密

3.1需求背景

对IM聊天软件而言,聊天消息的保密性就比较重要了,谁也不希望自己的聊天内容泄露甚至暴露在众人的前面。

所以在收发IM信息的时候,我们需要对信息做一些加密解密操作,保证信息在网络中传输的时候是加密的状态。

3.2简单的实现方法

可能大家会说:这还不简单?项目里写个加密解密的方法——收到消息时候先解密,发送消息时候先加密,服务端收到加密消息直接存储起来。

这样写理论上也没有问题,不过客户端直接写加解密方法有一些不好的地方。

比如:

  • 1)容易逆向:前端代码比较容易被逆向;
  • 2)性能较差:用户可能加了很多群组,各群组中都会收到很多消息,前端处理起来比较慢;
  • 3)多端实现:如果都在客户端实现加解密算法,那么 ios, android 等不同客户端,因为使用的开发语言不同,都要分别实现相同的算法,增加维护成本。

3.3我们的方案

我们使用 C++ Addons 提供的能力,在 c++ sdk 中实现加解密算法,让 js 可以像调用 Node 模块一样去调用 c++ sdk 模块。这样就一次性解决了上面提到的所有问题。

技术原理如下图:

开发完 addon,使用 node-gyp 来构建 C++ Addons。node-gyp 会根据 binding.gyp 配置文件调用各平台上的编译工具集来进行编译。

如果要实现跨平台,需要按不同平台编译 nodejs addon,在 binding.gyp 中按平台配置加解密的静态链接库。

就像下面这样:

{

"targets": [{

"conditions": [

["OS=='mac'", {

"libraries": [

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

]

}],

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

}],

...

]

...

}]

当然也可以根据需要添加更多平台的支持,如 linux、unix。

对 c++ 代码进程封装 addon 的时候,可以使用 node-addon-api

node-addon-api 包对 N-API 做了封装,并抹平了 nodejs 版本间的兼容问题。封装大大降低了非职业 c++ 开发编写 node addon 的成本(关于 node-addon-api、N-API、NAN 等概念可以参考死月同学的文章《从暴力到 NAN 再到 NAPI——Node.js 原生模块开发方式变迁》)。

打包出 .node 文件后,可以在 electron 应用运行时,调用 process.platform 判断运行的平台,分别加载对应平台的 addon。

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

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

} else{

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

}

3.4进一步学习

限于篇幅,本篇里没办法对IM的安全进行更深入的总结和分享,感兴趣的读者可以详读:《IM聊天系统安全手段之通信连接层加密技术》、《IM聊天系统安全手段之传输内容端到端加密技术》。

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

4.1需求背景

IM聊天消息直接通过 JSON 编解码和传输效率是比较低的,我们可以使用高效的消息序列化与反序列化方案。

4.2我们的方案

这里我们引入谷歌的 Protocol Buffer 提升效率。

PS:关于 Protocol Buffer 更多的介绍,可以查看《Protobuf通信协议详解:代码演示、详细原理介绍等》。

node 环境中使用 Protocol Buffer 可以用 protobufjs 包。

npm i protobuff -S

然后通过 pbjs 命令将 proto 文件转换成 pbJson.js

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

要在 js 中支持后端 int64 格式数据,需要使用 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();

};

后面就是消息的压缩转换了,将 js 字符串转成 pb 格式。

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

// 封装数据

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

// 解封数据

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

5、网络传输协议的选择

开发IM时可供选择的网络传输层协议有 UDPTCP 等。UDP 实时性好,但是可靠性不好。这里我们选用 的是 TCP 协议。

 

PS:关于TCP和UDP的区别,以及该如何选择,可以详细阅读这几篇:

快速理解TCP和UDP的差异

一泡尿的时间,快速搞懂TCP和UDP的区别

简述传输层协议TCP和UDP的区别

为什么QQ用的是UDP协议而不是TCP协议?

移动端即时通讯协议选择:UDP还是TCP?

应用层分别使用 WebSocket 协议保持长连接保证实时传输消息,HTTPS 协议传输消息外的其他状态数据。

这里给个例子实现一个简单的 WebSocket 管理类:

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);

}

...

}

如果你对WebSocket协议还不了解,可以从这两篇入门文章入手学习:《新手快速入门:WebSocket简明教程》、《WebSocket从入门到精通,半小时就够!

对于HTTPS 协议的话就不多介绍了,大家天天用。如果你还不是太了解,可以读读这两篇:《如果这样来理解HTTPS原理,一篇就够了》、《一分钟理解 HTTPS 到底解决了什么问题》。

6、IM的私有数据通信协议

上几节我们实现了把IM聊天消息序列化和反序列化,也实现了通过 WebSocket 发送和接收消息,但还不能直接这样发送聊天消息。

因为我们还需要一个数据通信协议(什么是数据通信协议?可以读读这篇《理论联系实际:一套典型的IM通信协议设计详解》)。也就是给通信层的原始“消息“增加一些属性,比如:id 用来关联收发的消息、type 标记消息类型、version 标记、接口的版本,api 标记调用的接口等。

然后据此定义一个编码格式,用 ArrayBuffer 将消息包装起来,放到 WebSocket 中发送,以二进制流的方式传输。

协议设计需要保证足够的扩展性,不然修改的时候需要同时修改前后端,比较麻烦。

下面是个简化的例子:

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 () {}

}

关于IM私有数据通信协议/格式的设计,可以参考《一套海量在线用户的移动端IM架构设计实践分享(含详细图文)》一文中的“3、协议设计”这一节。

另外,如果你自认为对于IM的理论知识很匮乏或不成体系,可以从《新手入门一篇就够:从零开发移动端IM》入手,系统地进行学习。

7、IM模块多进程优化

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

另外:消息通信逻辑不应该和界面逻辑放一个进程里,避免界面卡顿时候影响消息的收发。

这里有个简单的实现方法,把不同的模块放到 electorn 不同的窗口中,因为不同的窗口由不同的进程管理,我们就不需要自己管理进程了。

下面实现一个窗口管理类:

import { EventEmitter } from 'events';

class BaseWindow extends EventEmitter {

open () {}

close () {}

isExist () {}

destroy() {}

createWindow() {

this.win = newBrowserWindow({

...this.browserConfig,

});

}

...

}

其中 browserConfig 可以在子类中设置,不同窗口可以继承这个基类设置自己窗口属性。

通信模块用作后台收发数据,不需要显示窗口,可以设置窗口 width = 0,height = 0 :

class ImWindow extends BaseWindow {

browserConfig = {

width: 0,

height: 0,

show: false,

}

...

}

8、IM数据的本地存储

8.1背景

IM 软件中可能会有几千个联系人信息,无数的聊天记录。如果每次都通过网络请求访问,比较浪费带宽,影响性能。

那么是否有什么优化手段呢?

8.2讨论

在Electorn 中可以使用 localstorage, 但是 localstorage 有大小限制,实际大多只能存 5M 信息,超过存入大小会报错。

有些同学可能还会想到 websql, 但这个技术标准已经被废弃了。

浏览器内置的 indexedDB 也是一个可选项。

不过这个也有限制,也没有 sqlite 一样丰富的生态工具可以用。

8.3方案

这里我们选用 sqlite,在 node 中使用 sqlite 可以直接用 sqlite3 包。

可以先写个 DAO 类:

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 });

}

});

});

}

...

}

再写个 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 比如消息、联系人等 Model 可以直接继承这个类,复用 delete/getById/getAll 之类的通用方法。

如果不喜欢手动编写 SQLite 语句,可以引入 knex 语法封装器。

当然也可以直接时髦点用上 orm ,比如 typeorm 什么的。

使用如下:

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

const messageModel = newMessageModel(dao);

9、IM新消息托盘图标闪烁

在Electron 中没有提供专用的 tray 闪烁的接口,我们可以简单的使用切换 tray 图标来实现这个功能。

import { Tray, nativeImage } from 'electron';

class TrayManager {

...

setState() {

// 设置默认状态

}

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客户端版本更新

一般有几种不同的更新策略,可以一种或几种结合使用,提升体验。

第一种:是整个软件更新。这种方式比较暴力,体验不好,打开应用检查到版本变更,直接重新下载整个应用替换老版本。改一行代码,让用户冲下百来兆的文件。

第二种:是检测文件变更,下载替换老文件进行升级。

第三种:是直接将 view 层文件放在线上,electron 壳加载线上页面访问。有变更发布线上页面就可以。

关于版本更新,在本系列的上篇《vivo的Electron技术栈选型、全方位实践总结》也有提及,可以回顾一下。

11、进程间通信

上一篇文章中,有同学问怎么处理进程间通信。

electron 进程间通信主要用到 ipcMain 和 ipcRenderer

 

可以先写个发消息的方法:

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;

这样不管在主进程还是渲染进程,直接调用这个方法就可以发消息。

对于某些特定功能的消息,还可以做一些封装,比如所有推送消息可以封装一个方法,通过方法中的参数判断具体推送的消息类型。main 进程中根据消息类型,处理相关逻辑,或者对消息进行转发。

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、其他杂项

还有同学提到日志处理功能。

这个和 Electron 关系不大,是 node 项目通用的功能。

可以选用 winston 之类第三方包。

本地日志的话注意一下存储的路径,定期清理等功能点,远程日志提交到接口就可以了。

获取路径可以写些通用的方法,如:

import electron from 'electron';

functiongetUserDataPath() {

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

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

}

returnelectron.app.getPath('userData');

}

export defaultgetUserDataPath;

13、参考资料

[1] Protobuf通信协议详解:代码演示、详细原理介绍等

[2] IM聊天系统安全手段之通信连接层加密技术

[3] IM聊天系统安全手段之传输内容端到端加密技术

[4] TCP/IP详解 - 第11章·UDP:用户数据报协议

[5] TCP/IP详解 - 第17章·TCP:传输控制协议

[6] 移动端即时通讯协议选择:UDP还是TCP?

[7] WebSocket从入门到精通,半小时就够!

[8] 如果这样来理解HTTPS原理,一篇就够了

[9] 一套海量在线用户的移动端IM架构设计实践分享(含详细图文)

[10] 理论联系实际:一套典型的IM通信协议设计详解

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

IM跨平台技术学习(四):蘑菇街基于Electron开发IM客户端的技术实践的更多相关文章

  1. 使用Squirrel创建基于Electron开发的Windows 应用安装包

    我们把自己开发的Electron应用发布之前,需要把app打包成简单的安装包,这样app更容易被获取,以此来发布我们的应用.我们可以参考Wix或其他的安装程序,但是对于Electron应用更好的打包程 ...

  2. 使用Electron开发PC客户端

    最近公司要求开发一个PC客户端,要求不能使用.NET开发(为了不让用户安装.net framework),所以就选择了Electron(随口听别人说了一句,之前从来没有接触过).目前项目要完毕了,所以 ...

  3. (转)全文检索技术学习(二)——配置Lucene的开发环境

    http://blog.csdn.net/yerenyuan_pku/article/details/72589380 Lucene下载 Lucene是开发全文检索功能的工具包,可从官方网站http: ...

  4. 构建基于Electron开发的软件遇到的问题

    构建pdman时,报了好些错. 主要还是网络问题和版本不一致导致的. 前提 npm设置淘宝源,自行搜索. 版本 上面是官方要求的node环境. 需要首先安装nvm, brew install nvm ...

  5. Java开发学习(十二)----基于注解开发依赖注入

    Spring为了使用注解简化开发,并没有提供构造函数注入.setter注入对应的注解,只提供了自动装配的注解实现. 1.环境准备 首先准备环境: 创建一个Maven项目 pom.xml添加Spring ...

  6. EMV技术学习和研究(转)

    刚开始学习EMV&PBOC,磕磕碰碰,感谢xuture的<EMV技术学习和研究>给了很大帮助,让我少走了很多弯路,也感谢广俊.surge.艾零.小SO.Spinach.龙行天下的帮 ...

  7. Singer 学习四 可视化数据操作工具

    knots 是一款基于electron 开发的可视化UI界面,我们可以此工具进行方便的数据处理,注意工具使用了 docker 运行,需要安装docker 下载地址   https://github.c ...

  8. 基于 Electron 实现 uTools 的超级面板

    前言 为了进一步提高开发工作效率,最近我们基于 electron 开发了一款媲美 uTools 的开源工具箱 rubick.该工具箱不仅仅开源,最重要的是可以使用 uTools 生态内所有开源插件!这 ...

  9. 【Electron】Electron开发入门

    Electron简介: Electron提供了丰富的本地(操作系统)的API,使你能够使用纯JavaScript来创建桌面应用程序,并且跨平台(win,mac,linux等各种PC端平台).与其它各种 ...

  10. 桌面应用之electron开发与转换

    桌面应用之electron开发与转换 一,介绍与需求 1.1,介绍 1. Electron简介 Electron是用HTML,CSS和JavaScript来构建跨平台桌面应用程序的一个开源库. Ele ...

随机推荐

  1. SQLSEVER 实现货币数字转中文汉字

    SQLSEVER 实现数字转换成中文(货币) -- ============================================= -- Author: LearnerPing -- Cr ...

  2. 一些新奇的玩意【php篇--持续更新】

    人不进步就等于退步! 接触越多的人以及事就能学到更多的东西. 以下仅为本人记录的一些新奇的东西,不喜勿喷! 1.??运算符号,在新的项目中突然发现很多红线报错,还以为是错误!看了下,是??运算的问题, ...

  3. 欢迎来到IoT解忧杂货铺

    这是一间特殊的杂货铺 门面不大,却包罗万物 如果你也遇到一些烦恼 欢迎来到,IoT解忧杂货铺 解忧秘方·工业 厂里的几十台设备真让人头疼 协议种类太多太复杂 设备没法全联网 产线故障了也不知道 自己出 ...

  4. ToDesk云电脑手机也能玩?安卓系统已上线!

    近日,ToDesk云电脑首次发布移动端!这次云电脑将安卓和iOS双系统都安排上了,基本照顾到所有移动端的用户.并且性能也保持了电脑版的高配置,价格还是那么优惠. 我用了一次都有点不想退出了,用来打游戏 ...

  5. ToDesk云电脑实测!轻松应对游戏电竞、AIGC创作、设计建模等场景

    万物智联时代,现代社会对数字计算的需求呈指数级增长.当算力成为推动技术创新和应用发展的重要引擎,云电脑产业正在悄然占据国内算力应用的市场,成为新时代的数字经济发展方向.1 云电脑,顾名思义,是一台随时 ...

  6. Solon 之 STOMP

    一.STOMP 简介 如果直接使用 WebSocket 会非常累,就像用 Socket 编写 Web 应用.没有高层级的交互协议,就需要我们定义应用间所发消息的语义,还需要确保连接的两端都能遵循这些语 ...

  7. h5+ 检测 APP 是否开启应用通知权限

    h5+ 检测 APP 是否开启应用通知权限 原文可查看此处,搜索 h5+ 检测 APP 是否开启应用通知权限 https://mp.weixin.qq.com/mp/profile_ext?actio ...

  8. 哪些网站可以申请免费的纯IP地址https证书

    申请免费纯IP地址HTTPS证书,您可以按照以下步骤进行: 一.选择证书颁发机构(CA) 目前,虽然一些大型云服务提供商(如阿里云.华为云.腾讯云等)已经取消了免费一年期SSL证书的供应,但仍有一些C ...

  9. php xattr操作文件扩展属性再续

    今天偶然发现自己电脑还有一个隐藏硬盘,500G的我平时没挂载,就没用到,然后这次就给它挂载了,然后发现读取文件,操作xattr都很慢,比之前速度慢10倍左右,猜测可能是固态硬盘和机械硬盘的差别关系.看 ...

  10. MySQL-8.3.0 innovation 创新版本YUM安装配置

    MySQL-8.3.0 innovation版本已发布了,想抢先体验一下最新的功能,可以用以下的方式快速在虚拟机上安装一下哈 服务器环境:[root@node213 ~]# cat /etc/redh ...