文:徐江威
功能设计
即时消息的两个基本功能就是发送消息和接收消息。我们定义如下通信指令来实现这两个功能:
- Push 推送消息
- Pull 拉取消息
- Notify 消息通知
Push 推送消息指令将客户端消息发给指定的对端,也就是说服务器需要在收到客户端 Push 指令时将消息转发给目标客户端。
Pull 拉取消息指令用于客户端在必要时(例如,移动平台的 App 从后台回到前台时)从服务器端获取未被即时推送的消息。
Notify 消息通知是这三个指令里唯一个服务器发给客户端的指令,用来通知客户端“你有新消息送达”。客户端在收到该指令后即可将消息进行存储和呈现。
当然对于即时消息来说除了收发消息,还需要实现召回消息、转发消息、撤回消息等功能,我们将在后面的章节再介绍这些功能的实现。
数据包结构
我们定义一个 Packet 结构来封装我们的数据包,Packet 包含三个主要属性:序列号、名称、负载。
- 序列号 - 标识每个 Packet 的序号,该序列号唯一标识了一个 Packet 。
- 名称 - 标记该 Packet 的名称,我们使用名称来表示不同业务操作。
- 负载 - Packet 携带的数据,这里我们约定负载遵循 JSON 格式。
字段 | 类型 | 是否必填 | 描述 |
---|---|---|---|
sn | long | Y | 序列号 |
name | string | Y | 名称 |
data | JSON | Y | 负载数据 |
数据实体设计
我们定义消息实体对象具有以下字段:
字段 | 类型 | 是否必填 | 描述 |
---|---|---|---|
id | long | Y | 消息的 ID |
from | long | Y | 消息的发送人 ID |
to | long | Y | 消息的接收人 ID |
source | long | Y | 消息的广播源 ID,一般在群组消息里标记为群组 ID |
owner | long | Y | 消息的副本持有人 ID,该字段客户端程序无需识别和使用 |
lts | long | Y | Local Timestamp 消息本地时间戳 |
rts | long | Y | Remote Timestamp 消息服务器时间戳 |
state | int | Y | 消息状态 |
payload | JSON | N | 消息的负载数据 |
attachment | JSON | N | 消息的附件 |
对于消息实体我们约定每条消息对于每个客户端账户使用 ID 字段来唯一标识,这是因为我们这里将采用副本方式来管理消息实体,即每个客户端账户都拥有该 ID 消息的副本。这样做的好处是我们在生成副本后,各个客户端账户各自维护自己的副本及副本状态,有利于消息按照时序进行管理,提高服务器处理消息的读写速度,当然缺点也比较明显,就是不能用一个 ID 来全局标识一条消息,在进行存储管理时需要配合其他字段来进行操作。
编写服务器程序
我们的服务器将使用 Java 作为主要的编码语言。
准备工作
在开始实现服务器程序前,我们需要从魔方服务器依赖库下载我们需要的依赖库。
git clone https://gitee.com/shixinhulian/cube-server-dependencies.git
你需要在你服务器工程中指定依赖 cube-server-dependencies
目录下的 cell-2.3.jar
的 JAR 包。这个包封装了我们需要的即时通信的网络实现,该依赖包的文档可以从魔方手册下载:
git clone https://gitee.com/shixinhulian/cube-manual.git
服务器代码
如前所述,服务器需要接收来自客户端的 Push 和 Pull 指令,我们需要在 Cellet 里实现对这两个数据包的接收处理:
public class MessagingCellet extends Cellet {
private ExecutorService executor;
public MessagingCellet() {
super("Messaging");
}
@Override
public boolean install() {
...
}
@Override
public void uninstall() {
...
}
@Override
public void onListened(TalkContext talkCont