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