Android IM即時通信多進程中間件的傳輸數據結構設計與實現

語言: CN / TW / HK

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。

使用步驟 (這是一個非常簡單的步驟)

  1. 定義消息格式:在.proto 文件中定義消息的結構和字段。 syntax = "proto3"; message Person { string name = 1; int32 age = 2; repeated string hobbies = 3; }
  2. 編譯.proto 文件:使用 protobuf 編譯器將 .proto 文件編譯成 Java 類。

可以自動編譯。

  1. 使用消息類:使用生成的 Java 類創建消息對象,並設置和獲取字段的值。 Person person = Person.newBuilder() .setName("John") .setAge(30) .addHobbies("reading") .addHobbies("swimming") .build();
  2. 序列化和反序列化:將消息對象序列化成字節數組或將字節數組反序列化成消息對象。

``` // 序列化 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); }); })();

```