好消息:IM1.0.0版本已经上线啦,支持特性

  • 私聊发送文本/文件
  • 已发送/已送达/已读回执
  • 支持使用ldap登录
  • 支持接入外部的登录认证系统
  • 提供客户端jar包,方便客户端开发

github链接: https://github.com/yuanrw/IM

本篇将带大家从零开始搭建一个轻量级的IM服务端,IM的整体设计思路和架构在我的上篇博客中已经讲过了,没看过的同学请点击从零开始开发IM(即时通讯)服务端

这篇将给大家带来更多的细节实现。我将从三个方面来阐述如何构建一个完整可靠的IM系统。

  1. 可靠性
  2. 安全性
  3. 存储设计

可靠性

什么是可靠性?对于一个IM系统来说,可靠的定义至少是不丢消息消息不重复不乱序,满足这三点,才能说有一个好的聊天体验。

不丢消息

我们先从不丢消息开始讲起。

首先复习一下上一篇设计的服务端架构

我们先从一个简单例子开始思考:当Alice给Bob发送一条消息时,可能要经过这样一条链路:

  1. client-->connecter
  2. connector-->transfer
  3. transfer-->connector
  4. connector-->client

在这整个链路中的每个环节都有可能出问题,虽然tcp协议是可靠的,但是它只能保证链路层的可靠,无法保证应用层的可靠。

例如在第一步中,connector收到了从client发出的消息,但是转发给transfer失败,那么这条消息Bob就无法收到,而Alice也不会意识到消息发送失败了。

如果Bob状态是离线,那么消息链路就是:

  1. client-->connector
  2. connector-->transfer
  3. transfer-->mq

如果在第三步中,transfer收到了来自connector的消息,但是离线消息入库失败,

那么这个消息也是传递失败了。

为了保证应用层的可靠,我们必须要有一个ack机制,使发送方能够确认对方收到了这条消息。

具体的实现,我们模仿tcp协议做一个应用层的ack机制。

tcp的报文是以字节(byte)为单位的,而我们以message单位。



发送方每次发送一个消息,就要等待对方的ack回应,在ack确认消息中应该带有收到的id以便发送方识别。

其次,发送方需要维护一个等待ack的队列。 每次发送一个消息之后,就将消息和一个计时器入队。

另外存在一个线程一直轮询队列,如果有超时未收到ack的,就取出消息重发。

超时未收到ack的消息有两种处理方式:

  1. 和tcp一样不断发送直到收到ack为止。
  2. 设定一个最大重试次数,超过这个次数还没收到ack,就使用失败机制处理,节约资源。例如如果是connector长时间未收到client的ack,那么可以主动断开和客户端的连接,剩下未发送的消息就作为离线消息入库,客户端断连后尝试重连服务器即可。

不重复、不乱序

有的时候因为网络原因可能导致ack收到较慢,发送方就会重复发送,那么接收方必须有一个去重机制。

去重的方式是给每个消息增加一个唯一id。这个唯一id并不一定是全局的,只需要在一个会话中唯一即可。例如某两个人的会话,或者某一个群。如果网络断连了,重新连接后,就是新的会话了,id会重新从0开始。

接收方需要在当前会话中维护收到的最后一个消息的id,叫做lastId

每次收到一个新消息, 就将id与lastId作比较看是否连续,如果不连续,就放入一个暂存队列 queue中稍后处理。

例如:

  • 当前会话的lastId=1,接着服务器收到了消息msg(id=2),可以判断收到的消息是连续的,就处理消息,将lastId修改为2。

  • 但是如果服务器收到消息msg(id=3),就说明消息乱序到达了,那么就将这个消息入队,等待lastId变为2后,(即服务器收到消息msg(id=2)并处理完了),再取出这个消息处理。

因此,判断消息是否重复只需要判断msgId>lastId && !queue.contains(msgId)即可。如果收到重复的消息,可以判断是ack未送达,就再发送一次ack。

接收方收到消息后完整的处理流程如下:

伪代码如下:

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_id1user_id2是互为好友的用户id,为了避免重复,存储时按照user_id1<user_id2的顺序存,并且加上联合索引。
  • encrypt_key是随机生成的密钥。当客户端登录时,就会从数据库中获取该用户的所有的relation,存在内存中,以便后续加密解密。
  • 当客户端给某个好友发送消息时,取出内存中该关系的密钥,加密后发送。同样,当收到一条消息时,取出相应的密钥解密。

客户端完整登录流程如下:

  1. client调用rest接口登录。
  2. client调用rest接口获取该用户所有relation
  3. client向connector发送greet消息,通知上线。
  4. connector拉取离线消息推送给client。
  5. connector更新用户session。

那为什么connector要先推送离线消息再更新session呢?我们思考一下如果顺序倒过来会发生什么:

  1. 用户Alice登录服务器
  2. connector更新session
  3. 推送离线消息
  4. 此时Bob发送了一条消息给Alice

如果离线消息还在推送的过程中,Bob发送了新消息给Alice,服务器获取到Alice的session,就会立刻推送。这时新消息就有可能夹在一堆离线消息当中推过去了,那这时,Alice收到的消息就乱序了。

而我们必须保证离线消息的顺序在新消息之前。

那么如果先推送离线消息,之后才更新session。在离线消息推送的过程中,Alice的状态就是“未上线”,这时Bob新发送的消息只会入库im_offlineim_offline表中的数据被读完之后才会“上线”开始接受新消息。这也就避免了乱序。

存储设计

存储离线消息

当用户不在线时,离线消息必然要存储在服务端,等待用户上线再推送。理解了上一个小节后,离线消息的存储就非常容易了。增加一张离线消息表im_offline,表结构如下:

CREATE TABLE `im_offline` (
`id` int(11) COMMENT '主键',
`msg_id` bigint(20) COMMENT '消息id',
`msg_type` int(2) COMMENT '消息类型',
`content` varbinary(5000) COMMENT '消息内容',
`to_user_id` varchar(100) COMMENT '收件人id',
`has_read` tinyint(1) COMMENT '是否阅读',
`gmt_create` timestamp COMMENT '创建时间',
PRIMARY KEY (`id`)
);

msg_type用于区分消息类型(chat,ack),content加密后的消息内容以byte数组的形式存储。

用户上线时按照条件to_user_id=用户id拉取记录即可。

防止离线消息重复推送

我们思考一下多端登录的情况,Alice有两台设备同时登陆,在这种并发的情况下,我们就需要某种机制来保证离线消息只被读取一次。

这里利用CAS机制来实现:

  1. 首先取出所有has_read=false的字段。
  2. 检查每条消息的has_read值是否为false,如果是,则改为true。这是原子操作。
update im_offline set has_read = true where id = ${msg_id} and has_read = false
  1. 修改成功则推送,失败则不推送。

相信到这里,同学们已经可以自己动手搭建一个完整可用的IM服务端了。更多问题欢迎评论区留言~~

IM1.0.0版本已上线,github链接:

https://github.com/yuanrw/IM

觉得对你有帮助请点个star吧~!

从零开始开发IM(即时通讯)服务端(二)的更多相关文章

  1. 使用tomcat方式实现websocket即时通讯服务端讲解

    使用tomcat方式实现websocket即时通讯服务端讲解 第一种方案:使用Tomcat的方式实现 tomcat版本要求:tomcat7.0+.需要支持Javaee7 导入javeee-api的ja ...

  2. [iOS]从零开始开发一个即时通讯APP

    前言 这是我的毕业设计.刚开始确定这个课题的时候是因为以前有稍微研究过一些XMPP协议,在这个基础上做起来应该不难.然后开始选技术的时候还有半年,我想为什么不从更底层做起呢!那就不用XMPP,当时接触 ...

  3. Golang 在电商即时通讯服务建设中的实践

    马蜂窝技术原创文章,更多干货请搜索公众号:mfwtech ​即时通讯(IM)功能对于电商平台来说非常重要,特别是旅游电商. 从商品复杂性来看,一个旅游商品可能会包括用户在未来一段时间的衣.食.住.行等 ...

  4. .net平台 基于 XMPP协议的即时消息服务端简单实现

    .net平台 基于 XMPP协议的即时消息服务端简单实现 昨天抽空学习了一下XMPP,在网上找了好久,中文的资料太少了所以做这个简单的例子,今天才完成.公司也正在准备开发基于XMPP协议的即时通讯工具 ...

  5. 使用GSoap开发WebService客户端与服务端

    Gsoap 编译工具提供了一个SOAP/XML 关于C/C++ 语言的实现, 从而让C/C++语言开发web服务或客户端程序的工作变得轻松了很多. 用gsoap开发web service的大致思路 我 ...

  6. C#开发BIMFACE系列4 服务端API之源上传文件

    在注册成为BIMFACE的应用开发者后,要能在浏览器里浏览你的模型或者获取你模型内的BIM数据, 首先需要把你的模型文件上传到BIMFACE.根据不同场景,BIMFACE提供了丰富的文件相关的接口. ...

  7. C#开发BIMFACE系列3 服务端API之获取应用访问凭证AccessToken

    系列目录     [已更新最新开发文章,点击查看详细] BIMFACE 平台为开发者提供了大量的服务器端 API 与 JavaScript API,用于二次开发 BIM 的相关应用. BIMFACE ...

  8. C#开发BIMFACE系列15 服务端API之获取模型的View token

    系列目录     [已更新最新开发文章,点击查看详细] 在<C#开发BIMFACE系列3 服务端API之获取应用访问凭证AccessToken>中详细介绍了应用程序访问API的令牌凭证.我 ...

  9. C#开发BIMFACE系列40 服务端API之模型集成

    BIMFACE二次开发系列目录     [已更新最新开发文章,点击查看详细] 随着建筑信息化模型技术的发展,越来越多的人选择在云端浏览建筑模型.现阶段的云端模型浏览大多是基于文件级别,一次只可以浏览一 ...

  10. C#开发BIMFACE系列41 服务端API之模型对比

    BIMFACE二次开发系列目录     [已更新最新开发文章,点击查看详细] 在建筑施工图审查系统中,设计单位提交设计完成的模型/图纸,审查专家审查模型/图纸.审查过程中如果发现不符合规范的地方,则流 ...

随机推荐

  1. 教老婆学Linux运维(一)初识Linux

    零.前言 之一 为什么写这个系列?为什么是Linux? 老婆自从怀孕以后,辞职在家待了好几年了,现在时常感觉与社会脱节.所以想找个工作. 做了多年程序员,有点人脉也都基本是在IT圈子里,只能帮忙找找I ...

  2. Unity工程无代码化

     目的 Unity默认是将代码放入工程,这样容易带来一些问题.1. 代码和资源混合,职能之间容易互相误改.2. 当代码量膨胀到一定程度后,代码的编译时间长到无法忍受.新版的unity支持通过asmde ...

  3. 【Kubernetes 系列二】从虚拟机讲到 Kubernetes 架构

    目录 什么是虚拟机? 什么是容器? Docker Kubernetes 架构 Kubernetes 对象 基础设施抽象 在认识 Kubernetes 之前,我们需了解下容器,在了解容器之前,我们得先知 ...

  4. Python 命令行之旅 —— 初探 argparse

    『讲解开源项目系列』启动--让对开源项目感兴趣的人不再畏惧.让开源项目的发起者不再孤单.跟着我们的文章,你会发现编程的乐趣.使用和发现参与开源项目如此简单.欢迎联系我们给我们投稿,让更多人爱上开源.贡 ...

  5. DT-06 For MQTT

    感谢关注深圳四博智联科技有限公司产品!我公司提供完整的WiFi信号强度采集方案,包括WiFi信号采集.设备远程管理平台.智能终端应用等. Doit_MQTT透传固件基于乐鑫ESP_IOT_SDK使用C ...

  6. 7.19 包 logging模块 hashlib模块 openpyxl模块 深浅拷贝

    包 包是什么 他是一系列文件的结合体,表现形式就是文件夹 包的本质还是模块 他通常会有__init__.py文件 我们首先回顾一下模块导入的过程 import module首次导入模块(.py文件) ...

  7. 十分钟入门流处理框架Flink --实时报表场景的应用

    随着业务的发展,数据量剧增,我们一些简单报表大盘类的任务,就不能简单的依赖于RDBMS了,而是依赖于数仓之类的大数据平台. 数仓有着巨量数据的存储能力,但是一般都存在一定数据延迟,所以要想完全依赖数数 ...

  8. ThreadLocalSingleton.h——base

    #ifndef MUDUO_BASE_THREADLOCALSINGLETON_H #define MUDUO_BASE_THREADLOCALSINGLETON_H #include <boo ...

  9. redis最基础的入门教程

      Redis最基础入门教程 简介 Redis 简介 Redis 优势 Redis与其他key-value存储有什么不同? 字符串(Strings) 哈希(Hash) 列表(List) 集合(Sets ...

  10. Nginx总结(一)Linux下如何安装Nginx

    以前写过一些Nginx的文章,但都是用到什么说什么,没有一个完整系统的总结.趁最近有时间,打算将Nginx相关的内容重新整理一下.nginx系列文章地址如下:https://www.cnblogs.c ...