从零开始开发IM(即时通讯)服务端(二)
好消息:IM1.0.0版本已经上线啦,支持特性:
- 私聊发送文本/文件
- 已发送/已送达/已读回执
- 支持使用ldap登录
- 支持接入外部的登录认证系统
- 提供客户端jar包,方便客户端开发
github链接: https://github.com/yuanrw/IM
本篇将带大家从零开始搭建一个轻量级的IM服务端,IM的整体设计思路和架构在我的上篇博客中已经讲过了,没看过的同学请点击从零开始开发IM(即时通讯)服务端 。
这篇将给大家带来更多的细节实现。我将从三个方面来阐述如何构建一个完整可靠的IM系统。
- 可靠性
- 安全性
- 存储设计
可靠性
什么是可靠性?对于一个IM系统来说,可靠的定义至少是不丢消息、消息不重复、不乱序,满足这三点,才能说有一个好的聊天体验。
不丢消息
我们先从不丢消息开始讲起。
首先复习一下上一篇设计的服务端架构:
我们先从一个简单例子开始思考:当Alice给Bob发送一条消息时,可能要经过这样一条链路:
伪代码如下:
class ProcessMsgNode{ /** * 接收到的消息 */ private Message message; /** * 处理消息的方法 */ private Consumer<Message> consumer; } public CompletableFuture<Void> offer(Long id,Message message,Consumer<Message> consumer) { if (isRepeat(id)) { //消息重复 sendAck(id); return null; } if (!isConsist(id)) { //消息不连续 notConsistMsgMap.put(id, new ProcessMsgNode(message, consumer)); return null; } //处理消息 return process(id, message, consumer); } private CompletableFuture<Void> process(Long id, Message message, Consumer<Message> consumer) { return CompletableFuture .runAsync(() -> consumer.accept(message)) .thenAccept(v -> sendAck(id)) .thenAccept(v -> lastId.set(id)) .thenComposeAsync(v -> { Long nextId = nextId(id); if (notConsistMsgMap.containsKey(nextId)) { //队列中有下个消息 ProcessMsgNode node = notConsistMsgMap.get(nextId); return process(nextId, node.getMessage(), consumer); } else { //队列中没有下个消息 CompletableFuture<Void> future = new CompletableFuture<>(); future.complete(null); return future; } }) .exceptionally(e -> { logger.error("[process received msg] has error", e); return null; }); }
安全性
无论是聊天记录还是离线消息,肯定都会在服务端存储备份,那么消息的安全性,保护客户的隐私也至关重要。
因此所有的消息都必须要加密处理。
在存储模块里,维护用户信息和关系链有两张基础表,分别是im_user
用户表和im_relation
关系链表。
im_user
表用于存放用户常规信息,例如用户名密码等,结构比较简单。im_relation
表用于记录好友关系,结构如下:
CREATE TABLE `im_relation` ( `id` bigint(20) COMMENT '关系id', `user_id1` varchar(100) COMMENT '用户1id', `user_id2` varchar(100) COMMENT '用户2id', `encrypt_key` char(33) COMMENT 'aes密钥', `gmt_create` timestamp DEFAULT CURRENT_TIMESTAMP, `gmt_update` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `USERID1_USERID2` (`user_id1`,`user_id2`) );
user_id1
和user_id2
是互为好友的用户id,为了避免重复,存储时按照