Android IM即时通信多进程中间件的传输数据结构设计与实现
theme: cyanosis
这个系列主要解决的是多进程的即时通信,所以我在上次的文章中将长链接部分直接设计成按业务分层的模式了,这样有一个好处就是不管长链接是按照什么渠道实现的,都不影响我这个框架。
接下来最主要的就是要选择长链接中数据的传输格式,对于数据传输在这个框架中只是做一个透传,这得益于我上次设计的长链接分层架构,可以完美的将数据的发起和处理都交给接入SDK的人来管理,而我只负责传输。
以下文章是此系列的前文
Android IM即时通信多进程中间件设计与实现
IM即时通信多进程中间件的设计与实现-剥离长连接,让组件职责更单一
服务端代码 node.js 写的
客户端代码
## 即时通信中常见的传输格式
一切设计、长链接等都是为通信而服务的,所以传输介质的选择是非常重要的,在客户端的开发过程中常见的传输格式有以下几种: 1. JSON(JavaScript Object Notation):JSON是一种轻量级的数据交换格式,易于阅读和编写,并且广泛用于Web应用程序中。在即时通信中,JSON格式通常用于传输聊天消息、好友列表、群组信息等数据。 2. XML(Extensible Markup Language):XML是一种标记语言,可以用于描述复杂的数据结构,与JSON类似,但它更适合用于传输文本数据。在即时通信中,XML格式通常用于传输聊天记录、联系人信息等数据。 4. Protocol Buffers:Protocol Buffers是一种高效的数据序列化格式,可以将结构化数据编码为紧凑且高效的二进制格式,比JSON和XML更小,更快,更简单。在即时通信中,Protocol Buffers格式通常用于传输通讯协议、消息结构等数据。 5. BSON(Binary JSON):BSON是一种二进制表示格式,与JSON类似,但更适合用于处理二进制数据,因为它支持更多的数据类型,如日期、正则表达式、二进制数据等。在即时通信中,BSON格式通常用于传输二进制数据、图片、音频、视频等多媒体数据。
第一种和第二种大家应该是耳熟能详了,毕竟入行以来的所有数据传输都离不开这两个格式,特别是json格式,对于第三种而言,近几年较火,现在用的人也越来越多,最后一个不是很了解,只是知道有这么一个东西。
常见格式的对比
| 格式 | 优点 | 缺点 | 使用场景 | | --- | --- | --- | --- | | JSON | 1. 可读性强,易于调试和开发。2. 支持多种编程语言。3. 适合传输文本数据和结构化数据。4. 可扩展性好,可以添加自定义的数据类型和字段。 | 1. 不支持二进制数据传输,比如图片、音频、视频等多媒体数据。2. 对于大量数据,JSON格式比较冗长,占用网络带宽。 | 适合传输文本数据和结构化数据,如聊天消息、好友列表、群组信息等。 | | Protocol Buffers | 1. 二进制格式,比JSON和XML更小、更快、更简单。2. 支持多种编程语言。3. 适合传输通讯协议、消息结构等数据。 | 1. 不支持自定义数据类型,需要提前定义好消息结构。2. 可读性较差,不易于调试。 | 适合传输通讯协议、消息结构等数据,比如登录认证、消息传输等。 | | XML | 1. 支持自定义数据类型和结构。2. 支持多种编程语言。3. 适合传输文本数据和结构化数据。 | 1. 与JSON和Protocol Buffers相比,XML格式比较冗长,占用网络带宽。2. 可读性较差,不易于调试。 | 适合传输聊天记录、联系人信息等数据。 | | BSON | 1. 支持多种数据类型,如日期、正则表达式、二进制数据等。2. 支持二进制数据传输,比JSON和XML更适合传输多媒体数据。3. 适合传输大量数据。 | 1. 不支持自定义数据类型,需要使用预定义的数据类型。2. 可读性较差,不易于调试。 | 适合传输图片、音频、视频等多媒体数据。 |
即时通信应该选哪个
通过上述对比,在即时通讯方面应该已经很明显了,针对即时通信场景,应该选择 Protocol Buffers 或 BSON 格式。这是因为即时通信需要实时性,传输速度较快,而 Protocol Buffers 和 BSON 格式都是二进制格式,比 JSON 和 XML 更小、更快、更简单,能够更快地传输数据。此外,BSON 格式支持二进制数据传输,适合传输多媒体数据,因此更适合传输图片、音频、视频等多媒体数据。如果需要支持多种编程语言,Protocol Buffers 是更好的选择,因为它支持多种编程语言,包括Java、C++、Python、JavaScript等
在即时通讯的业务中应该没有人传输一张图片或者一个文件的,这太大了,不小心会触发长链接的传输限制,会带来不必要的麻烦。
比如WebSocket。
虽然WebSocket 的数据传输大小并没有一个固定的上限,因为 WebSocket 是基于 TCP 的,而 TCP 协议本身没有数据大小的限制,只受限于网络带宽、网络拥塞等因素。但是,WebSocket 在传输数据时会把数据分片(fragmentation)后发送,每个数据帧(frame)的大小受限于 WebSocket 协议规范和浏览器的实现。
根据 WebSocket 协议规范,WebSocket 数据帧的大小是没有限制的,但浏览器在实现时通常会设置一个默认的大小限制。例如,在 Chrome 浏览器中,单个 WebSocket 数据帧大小的默认上限是 256KB,如果超过这个大小,浏览器会将数据分割成多个数据帧来发送。而在 Firefox 浏览器中,单个 WebSocket 数据帧大小的默认上限为 4MB。
尽管 WebSocket 的数据传输大小没有明确的上限,但是在实际使用中,建议控制单个数据帧的大小,以免占用过多的网络带宽和资源,影响系统性能。通常建议将单个数据帧的大小控制在几百KB以内,根据具体情况来决定。
所以还是选用Protocol Buffers 传输更为实在。
Protocol Buffers
Protocol Buffers(简称 Protobuf)是由 Google 开发的一种轻量级、高效、可扩展的序列化数据交换格式。它可以被用于数据序列化、通信协议设计、配置文件等多个领域。
与其他序列化格式(如 JSON 和 XML)相比,Protocol Buffers 具有更小的数据体积、更快的解析速度和更高的兼容性。它支持多种语言(包括 C++、Java、Python、Go、C# 等)的生成和解析,可以方便地进行跨语言数据交换。
Protocol Buffers 定义数据结构和消息格式时使用的是 .proto 文件,通过编译器生成不同语言的代码。消息格式可以通过添加或删除字段、修改数据类型等方式进行升级,同时保持向前和向后兼容性。这种特性在大规模分布式系统中具有重要意义,因为它可以避免因升级导致的兼容性问题,减少系统维护的难度和风险。
protobuf 在 Android 上的步骤
一般情况下,都是服务端定好pb, 客户端只需要使用就可以了,但是这个系列中,我自己做一个默认实现,所以有了这个模块
配置
- 根项目build.gradle 中添加:
id 'com.google.protobuf' version '0.8.17' apply false
- 在module中添加(主要在主要使用module中添加(编译pb的module)其他module引入pb依赖即可)
``` apply plugin: 'com.google.protobuf'
protobuf{ protoc{ artifact = 'com.google.protobuf:protoc:3.7.0' } plugins { javalite { // The codegen for lite comes as a separate artifact artifact = 'com.google.protobuf:protoc-gen-javalite:3.0.0' } } generateProtoTasks { all().each { task -> task.builtins { // In most cases you don't need the full Java output // if you use the lite output. remove java } task.builtins { java {} } } } }
depends {
implementation 'com.google.protobuf:protobuf-java:3.18.1'
} ```
注意这个位置: :protobuf-java 和 :protobuf-lite 的区别
com.google.protobuf:protobuf-lite 和 com.google.protobuf:protobuf-java 都是 Google Protocol Buffers 的库,不同之处在于它们的功能和大小。
protobuf-java 是完整的 Protocol Buffers 库,支持使用 .proto 文件生成代码和运行时解析和序列化消息。它提供了许多高级特性,例如支持多种语言、自定义选项和扩展等。
protobuf-lite 是一个轻量级库,可以使用 .proto 文件生成代码,但不支持运行时解析和序列化消息。相比于protobuf-java,它更小巧,运行速度更快,适用于移动设备或带宽有限的环境下使用。它不支持一些高级特性,例如扩展、自定义选项、反射等。
如果您需要支持完整的 Protocol Buffers 功能,建议使用 protobuf-java。如果您只需要在移动设备或网络带宽有限的环境下序列化和反序列化简单的消息,可以选择 protobuf-lite。
使用步骤 (这是一个非常简单的步骤)
- 定义消息格式:在.proto 文件中定义消息的结构和字段。
syntax = "proto3"; message Person { string name = 1; int32 age = 2; repeated string hobbies = 3; }
- 编译.proto 文件:使用 protobuf 编译器将 .proto 文件编译成 Java 类。
可以自动编译。
- 使用消息类:使用生成的 Java 类创建消息对象,并设置和获取字段的值。
Person person = Person.newBuilder() .setName("John") .setAge(30) .addHobbies("reading") .addHobbies("swimming") .build();
- 序列化和反序列化:将消息对象序列化成字节数组或将字节数组反序列化成消息对象。
``` // 序列化 byte[] bytes = person.toByteArray();
// 反序列化 Person person2 = Person.parseFrom(bytes); ```
常见的语法
``` syntax = "proto3";
message Person { string name = 1; int32 age = 2; repeated string email = 3; } ```
- syntax = "proto3";:定义了使用的Proto语法版本。
- message:定义了一个消息类型。
- string和int32:定义了两个字段,分别是字符串类型和32位整数类型。
- repeated:定义了一个重复的字段,可以包含多个值。
- name、age和email:定义了三个字段的名称,用于标识数据中的不同部分。
- = 1、= 2和= 3:定义了每个字段的唯一标识符,用于在二进制格式中标识该字段。
当然还有更多的语法,可以去官方
里面有Protocol Buffers的详细介绍、使用指南、语法参考、常见问题解答等内容。
IMClient SDK 种发送消息的结构设计
我的想法是这样的,既然我现在已经完成了长链接的分离,那我直接将发送的消息做贯穿,在即时通信项目中老旧的项目可能用到json格式传输,新型的基本都是pb,二者都可以转换为bytes[] 数组,我在SDK种透传改bytes[],这样就可以做到SDK不侵入业务了,其他人使用SDK时不必考虑传输介质。
默认结构设计与实现
``` syntax = "proto3"; package com.example.mylibrary;
message IMClientParams { // 发送人id string sendId = 1; // 接收人id string chatWithId = 2; // 发送时间 int64 sendTime = 3; // 消息版本 服务端做消息唯一处理 int64 version = 4; // 客户端消息唯一标识 int64 msgId = 6; // 区分消息的所属类型 比如系统消息还是用户消息 int32 type = 7; // 消息类型 根据此类型 int32 cmd = 8; // 包含的消息体 type 消息和cmd 同时作用可过滤唯一消息 bytes body = 8; }
// 文本 message TextMessage { string content = 1; }
// 图片 message ImageMessage { string url = 1; //图片后缀 string prefix = 2; //原图宽 int32 origWight = 3; //原图高 int32 origHeight = 4; //原图大小 int64 origSize = 5; //中等缩略图片url tring midUrl = 6; //模糊图片地址 string blurryImUrl = 7; }
``` 此消息体定义为发送接收一体,使用时可以使用指定type,决定是那种消息,共 - 系统消息 - 用户消息 - 客服消息
接着用cmd 决定消息类型 - 文本 - 图片 - 视频 - 等等
定义全局的枚举,使得前后端统一
``` syntax = "proto3"; package com.example.mylibrary;
enum IMClientCMDEnum { NONE_CMD = 0; //系统消息 SYS_MSG_CMD = 2000; //文本单聊 CHAT_TXT_CMD = 2001; //图片单聊 CHAT_IMG_CMD = 2003; //关注消息 FOLLOW_MSG_CMD = 2004; }
//平台消息类型 enum PlatformMsgType { //用户消息 USER_MSG_TYPE = 0; //系统消息 SYS_MSG_TYPE = 1; //客服消息 CUSTOMER_MSG_TYPE = 2; } ```
使用发送
// 构建文本消息
val textMessage = TextMessage.newBuilder().setContent("我是文本消息").build()
val params = IMClientParams.newBuilder()
//发送的是用户消息
.setType(IMClientEnum.PlatformMsgType.USER_MSG_TYPE_VALUE)
// 发送的是文本消息
.setCmd(IMClientEnum.IMClientCMDEnum.CHAT_TXT_CMD_VALUE)
//发送者ID
.setSendId("1011011")
//接收者
.setChatWithId("102094")
//发送的内容
.setBody(textMessage.toByteString())
//消息唯一ID
.setMsgId(100)
//发送时间
.setSendTime(System.currentTimeMillis())
.build()
IMClient.with().send(params.toByteArray())
此例为构建了一条用户纬度的文本消息
修改接收的代码
interface IMMessageReceiver {
// 传pb
void onMessageReceived(in byte[] receiveMessage);
}
例如,用node.js 发送数据:
``` const WebSocket = require('ws'); const protobuf = require('protobufjs'); const imFile = "../public/IMClientParams.proto";
(async () => { const imContent = await protobuf.load(imFile); const imContentParser = imContent.lookupType("com.example.mylibrary.IMClientParams");
const ws = new WebSocket('ws://localhost:8080');
ws.on('open', () => { console.log('Connected to server'); // 创建一个消息对象 const message = { from: 'Alice', to: 'Bob', content: 'Hello, Bob!' }; // 将消息对象序列化为二进制数据 const buffer = imContentParser.encode(imContentParser.create(message)).finish(); // 发送二进制数据 ws.send(buffer); });
ws.on('message', (message) => { console.log('Received message:', message); }); })();
```