1. 简介
IM (instant messaging) 即时通讯,之前做过一款即使通讯的项目。今天在这里和大家分享一下即时通讯中比较重要的一个内容 ——数据结构或者说通信协议。
通信协议的选择是采用谷歌的protobuf结构序列化方法。至于这里为什么选择采用protobuf,这里先和大家简单说一下,后面的其他文章再详细的介绍一下protobuf的用法。
-
protobuf的优点:
protobuf优点就是传输效率快(据说在数据量大的时候,传输效率比xml和json快10-20倍),序列化后体积相比Json和XML很小,支持跨平台多语言,消息格式升级和兼容性还不错,序列化反序列化速度很快。 -
protobuf的缺点:
protobuf缺点就是使用不太方便。protobuf的原生格式并不具备像xml或者json格式的可读性和人类可编辑性。因此,再使用起来的时候有一些难度。
综上,protobuf肯定是首选,IM数据通讯效率优先级肯定要大于我们程序员开发中遇到的一些困难。
2. 基础数据结构
syntax = "proto3";
//基础消息结构
message IMRequestInfo
{
RequestType requestType = 1;
int64 requestId = 2;
string fromUserId = 3;
string sign = 4;
int64 timestamp = 5; //单位毫秒
int64 msgServerId = 6; //消息在服务端生成的唯一ID
oneof requestData {
IMLogin login = 11;
IMMsg msg = 12;
IMAck ack = 13;
IMCmd cmd = 14;
}
enum RequestType {
LOGIN = 0; //登录
LOGIN_ACK = 1; //登录确认
HEARTBEAT = 2; //心跳
HEARTBEAT_ACK = 3; //心跳确认
MSG = 4; //数据
MSG_ACK = 5; //数据确认
OFFLINE = 6; //用户下线
CMD = 7; //透传消息
CMD_ACK = 8; //透传消息确认
}
}
/**
聊天消息
*/
message IMMsg {
MsgType msgType = 1;
TargetType targetType = 2;
repeated string targetIds = 3;
oneof msgData {
string txt = 21; //文本消息
IMMsgImg img = 22; //图片消息
IMMsgVideo video = 23; //视频消息
IMMsgAudio audio = 24; //音频消息
IMMsgFile file = 25; //文件
IMMsgLocation location = 26; //地理位置
IMMsgCustom custom = 27; //自定义消息
}
enum MsgType {
TXT = 0;//文本消息
IMG = 1;//图片消息
VIDEO = 2;//视频
AUDIO = 3;//音频
LOCATION = 4;//地理位置
FILE = 5;//文件
CUSTOM = 6;//自定义
}
}
enum TargetType {
USERS = 0; // 给用户发消息
CHATGROUPS = 1; // 给群发消息
CHATROOMS = 2; // 给聊天室发消息
}
message IMLogin
{
string token = 1;
}
message IMAck
{
int64 sourceRequestId = 1; //接收到的请求ID
string tip = 2; //提示信息
string code = 3;//错误码,0000表示成功
}
/**
图片消息
*/
message IMMsgImg
{
string fileName = 1;//图片名称
string url = 2;
int32 width = 3;
int32 height = 4;
}
/**
视频消息
*/
message IMMsgFile
{
string fileName = 1;//文件名称
string url = 2;//地址
int32 fileSize = 3;//文件大小(单位:字节)
}
/**
音频消息
*/
message IMMsgAudio
{
string fileName = 1;//音频文件名称
string url = 2;//音频地址
int32 playLength = 3;//音频播放长度
int32 fileSize = 4;//音频文件大小(单位:字节)
}
/**
视频消息
*/
message IMMsgVideo
{
string fileName = 1;//视频文件名称
string url = 2;//音视频地址
int32 playLength = 3;//视频播放长度
int32 fileSize = 4;//视频文件大小(单位:字节)
}
/**
地理位置消息
*/
message IMMsgLocation
{
string lat = 1;
string lng = 2;
string addr = 3;
}
/**
自定义消息
*/
message IMMsgCustom
{
string event = 1;
string data = 2;
}
/**
透传消息
*/
message IMCmd
{
TargetType targetType = 1;
repeated string targetIds = 2;
string action = 3;
string data = 4;
bool needAck = 5;
int64 expireRemainSec = 6;
}
接下来我们再来分析一下上面的数据结构:
我们首先来分析一下在整个通讯的过程中,我们需要传输的数据,整体可以被分为几个部分。
首选最为重要的就是通讯消息(msg),这是其中最为重要的部分,整个即时通讯的项目可以来说都是围绕着这部分进行设计的。
其次自然而然的就会产生另外一种类型的消息——消息反馈(ack),对于一个客户端来说,发出的消息服务端是否收到是未知的。所以服务端在收到客户端发送的消息之后,就需要发送一个消息回执或者说消息反馈告诉客户端消息收到了,否则客户端在没有收到消息回执的情况下就需要重新发送,以保障消息发送的可靠性。
反过来,服务端往客户端发送消息的时候,也是这个道理,客户端时需要对来自服务端的消息做消息回执。
然后我们在来思考另外一种情况,在服务端和客户端进行通讯的过程中肯定存在一些消息是不需要用户进行处理的,只需要客户端中的预先设置好程序进行处理就好了。既然不需要用户进行参与,就需要区别于普通的用户消息(msg)以便更为简单方便的处理,在这里或者项目中我们简单的称之为透传消息(cmd)。
以上三部分的内容可以说是整个即时通讯的主体。但是在项目中还遇到了一种特殊的情况。在整个即时通讯服务端的功能设计中,服务端需要满足web端,ios端和android端三个部分的内容。服务端中采用的netty框架。在netty的通讯框架中,每一个用户对应着一个channel(通道),在用户连接到服务器之后我们就需要把用户的Id和用户对应的channel绑定起来,以便后续的通讯中使用。所以整个通讯的第一步就是获取用户的Id(登录),这一步一般情况下是通过登录接口来实现的,用户登录之后获取到对应的token,然后再socket通讯的过程中携带此token,服务端就可以明确的知道这个消息是来自哪一个用户了。一个非常简便的方式是把token放到channel的attr属性中。这一步再ios和android都是可以实现的。但是在web端这块,采用的websocket和服务端进行通讯,websocket中不存在attr这种属性。所以,整个服务端为了兼容web端这块,就添加了一种消息格式(login)。
以上就是整个通讯协议种四种消息格式的选择。对应着protobuf文件种的一下内容:
oneof requestData {
IMLogin login = 11;
IMMsg msg = 12;
IMAck ack = 13;
IMCmd cmd = 14;
}
然后我们在来分析基础消息结构中的以下内容:
RequestType requestType = 1;
int64 requestId = 2;
string fromUserId = 3;
string sign = 4;
int64 timestamp = 5; //单位毫秒
int64 msgServerId = 6; //消息在服务端生成的唯一ID
这部分的内容感觉更像是HTTP协议中的header部分。
- RequestType 是请求消息的类型,写在这里是为了更为细致的区分消息的类型。
这里面的LOGIN对应着四种基本消息类型中的IMLogin;所有以ACK作为后缀的请求消息类型对应着IMAck;CMD对应着IMCmd,剩余的消息对应着IMMsg。enum RequestType { LOGIN = 0; //登录 LOGIN_ACK = 1; //登录确认 HEARTBEAT = 2; //心跳 HEARTBEAT_ACK = 3; //心跳确认 MSG = 4; //数据 MSG_ACK = 5; //数据确认 OFFLINE = 6; //用户下线 CMD = 7; //透传消息 CMD_ACK = 8; //透传消息确认 }
- requestId是本次消息请求的Id,作为本次消息的唯一标识。
- fromUserId表示本次消息来自于哪一个用户。
- sign签名认证字段。
- timestamp时间戳。
- msgServerId服务端消息的唯一Id。
下面我们再来仔细地说说每一种的消息类型:
3. IMLogin
message IMLogin{
string token = 1;
}
登录的数据结构是非常的简单的,就只有一个token属性。就是用户登录之后获取的token,用户在每次连接服务端之后,第一步要做的发送一个登录的消息。把连接的通道和具体的用户绑定起来,以便后面服务端区分各个用户对应的各个通道。
4. IMAck
message IMAck
{
int64 sourceRequestId = 1; //接收到的请求ID
string tip = 2; //提示信息
string code = 3;//错误码,0000表示成功
}
其中的tip和code都是非常好理解的,code的值为0000就表示消息已经成功接收到了。
可能有点难以理解的就是sourceRequestId属性了,这个属性是就是基础数据结构中的requestId属性,消息回执通过这个属性来表明是针对哪一个消息的回执。
5. IMCmd
/**
透传消息
*/
message IMCmd
{
TargetType targetType = 1;
repeated string targetIds = 2;
string action = 3;
string data = 4;
bool needAck = 5;
int64 expireRemainSec = 6;
}
TargetType属性是该消息的用户群体,这里主要分为三个对象:用户,群组和聊天室
enum TargetType {
USERS = 0; // 给用户发消息
CHATGROUPS = 1; // 给群发消息
CHATROOMS = 2; // 给聊天室发消息
}
targetIds是用户群体的表示,比如如果TargetType的类型是用户,targetIds就是用户Id。如果TargetType是群组,targetIds就是群组Id。而且当群发消息时,id时按照逗号进行分割。
action和data字段时透传消息中的自定义字段。action时透传消息属于哪一种行为,data是该行为下所需要的数据。
needAck表示该透传消息是否需要进行消息反馈。
expireRemainSec属性表示该透传消息的过期时间。如果用户在下线期间,针对该用户产生一些透传消息。在用户上线之后是需要对该用户发送离线期间产生的透传消息。该字段就标明了透传消息的过期时间,当透传消息的过期时间过了之后,用户再上线时,就不需要发送该消息了。
6. IMMsg
/**
聊天消息
*/
message IMMsg {
MsgType msgType = 1;
TargetType targetType = 2;
repeated string targetIds = 3;
oneof msgData {
string txt = 21; //文本消息
IMMsgImg img = 22; //图片消息
IMMsgVideo video = 23; //视频消息
IMMsgAudio audio = 24; //音频消息
IMMsgFile file = 25; //文件
IMMsgLocation location = 26; //地理位置
IMMsgCustom custom = 27; //自定义消息
}
enum MsgType {
TXT = 0;//文本消息
IMG = 1;//图片消息
VIDEO = 2;//视频
AUDIO = 3;//音频
LOCATION = 4;//地理位置
FILE = 5;//文件
CUSTOM = 6;//自定义
}
}
IMMsg是整个数据结构中最为复杂的部分。我们来慢慢的分析:
- MsgType表示的是聊天消息的类型,再项目中分为了7中聊天消息的类型。基本上满足了我们日常聊天所需要的各种数据类型。
enum MsgType {
TXT = 0;//文本消息
IMG = 1;//图片消息
VIDEO = 2;//视频
AUDIO = 3;//音频
LOCATION = 4;//地理位置
FILE = 5;//文件
CUSTOM = 6;//自定义
}
- TargetType和targetIds两个属性和在透传消息中介绍的一样。TargetType表示的是消息的目标群体, targetIds表示的是目标群体的Id。
- msgData属性对应着不同的消息类型所对用的数据格式。
- txt 即是普通的文本消息,对应的消息类型是TXT
- IMMsgImg表示的是图片消息,对应的消息类型为IMG
消息结构为message IMMsgImg{ string fileName = 1;//图片名称 string url = 2; int32 width = 3; int32 height = 4; }
- IMMsgVideo为视频消息,对应的消息类型为VIDEO
message IMMsgVideo { string fileName = 1;//视频文件名称 string url = 2;//音视频地址 int32 playLength = 3;//视频播放长度 int32 fileSize = 4;//视频文件大小(单位:字节) }
- IMMsgAudio为音频消息,对应的消息类型为AUDIO
message IMMsgAudio { string fileName = 1;//音频文件名称 string url = 2;//音频地址 int32 playLength = 3;//音频播放长度 int32 fileSize = 4;//音频文件大小(单位:字节) }
- IMMsgFile为文件,对应的消息类型为FILE
message IMMsgFile { string fileName = 1;//文件名称 string url = 2;//地址 int32 fileSize = 3;//文件大小(单位:字节) }
- IMMsgLocation为地理位置,对应的消息类型为LOCATION
message IMMsgLocation { string lat = 1; string lng = 2; string addr = 3; }
- IMMsgCustom为自定义消息,对应的消息类型为CUSTOM
自定义消息在这里有必要在说明一下,自定义消息的目的可以说是为了应对一些未知或者说不确定的业务。message IMMsgCustom { string event = 1; string data = 2; }
其中event是自定义消息的事件,目的是为了区分不同的自定义消息。
data表示不同event下的数据结构。
7. 总结
以上便是对整个即时通讯中的数据结构(通信协议)的介绍。这里是项目中第一版中所采用的结构。
优点是简单容易理解,就是对即时通讯过程中所用到的信息结构的简单概括,最后通过提炼出来的。
缺点也是比较明显的,整个的通讯结构中是比较臃肿的,其中包含了不少的枚举类型,这在IM通讯中有点多余的,在传输的过程中也占用资源。其次还有一点消息的请求类型和请求的数据结构的对应有些混乱。比如请求类型(RequestType)中的心跳(HEARTBEAT),用户下线(OFFLINE)这些请求类型,并没有对应的数据结构,而是归纳到了IMMsg结构中。这对前后端人员对接或者刚开始接触项目的人员来说,是存在一定的混淆的可能的。
因此,在后期对数据结构进行了重构。下次在和大家分享。