因收到Google相关通知,网站将会择期关闭。相关通知内容 11 期中实战:动手写一个简易版的IM系统 你好,我是袁武林。 到上一讲为止,IM的相关课程已经进行过半,在前面的课程中,我们讨论的大部分内容都比较偏理论,你理解起来可能会比较抽象。为了让你对前面讲到的知识有更深入的理解,今天我们就来回顾、梳理近期学习的内容,一起尝试搭建一个简单的IM聊天系统。 在开始之前呢,我先来说明一下IM课程的期中、期末实战的课程计划和设计思路。 期中和期末实战是希望你以自己动手实现为主,提供的Demo主要作为参考,在设计层面上,并不能让你直接用于线上使用。 本次期中实战Demo的主要关注点是:消息的存储、未读数的设计,并以“短轮询”的方式来实现消息的实时触达。希望你能从用户的使用场景出发,来理解消息存储设计的思路,以及未读数独立两套存储的必要性。 另外,在期末实战中,我会从“短轮询”方式调整为WebSocket长连接的方式,并且加上ACK机制、应用层心跳等特性。希望你能在两次实战中,通过对比,真正理解长连接方式相比“短轮询”方式的优势,并且通过ACK机制和应用层心跳,真正理解为什么它们能够解决“数据丢失”和“连接可靠性”的问题。 OK,下面我们说回本次实战。 这个聊天系统要求并不复杂,只需要构建简单的Web界面(没有界面通过命令行能实现也行)。我在这里写了一个简易版的Demo,供你参考。 示例代码你可以在GitHub上下载。 需求梳理 这个简易聊天系统的大概要求有以下几点: 支持用户登录; 双方支持简单的文本聊天; 支持消息未读数(包括总未读和会话未读); 支持联系人页和未读数有新消息的自动更新; 支持聊天页有新消息时自动更新。 需求分析 我们先来分析一下整体需求。 首先,要支持用户登录,先要有“用户”。对应的数据底层需要有一个用户表,用户表的设计可以比较简单,能够支持唯一的UID和密码用于登录即可。当然,如果有用户头像信息,聊天时的体验会更好,所以这里我们也加一下。简单的库表设计可以是这样的: CREATE TABLE IM_USER ( uid INT PRIMARY KEY, username VARCHAR(500) NOT NULL, password VARCHAR(500) NOT NULL, email VARCHAR(250) DEFAULT NULL, avatar VARCHAR(500) NOT NULL ); 对应的实体类User字段和库表一致,这里就不罗列了,我们需要设计用户通过邮箱和密码来登录。因为课程主要是涉及IM相关的知识,所以这里对用户信息的维护可以不做要求,启动时内置几个默认用户即可。 有了用户后,接下来就是互动了,这一期我们只需要关注简单的文本聊天即可。在设计中,我们需要对具体的聊天消息进行存储,便于在Web端使用,因此可以简单地按照“02 | 消息收发架构:为你的App,加上实时通信功能”中讲到的消息存储来实现此项功能。 消息的存储大概分为消息内容表、消息索引表、联系人列表,这里我用最基础的字段来给你演示一下。单库单表的设计如下: 消息内容表: CREATE TABLE IM_MSG_CONTENT ( mid INT AUTO_INCREMENT PRIMARY KEY, content VARCHAR(1000) NOT NULL, sender_id INT NOT NULL, recipient_id INT NOT NULL, msg_type INT NOT NULL, create_time TIMESTAMP NOT NUll ); 消息索引表: CREATE TABLE IM_MSG_RELATION ( owner_uid INT NOT NULL, other_uid INT NOT NULL, mid INT NOT NULL, type INT NOT NULL, create_time TIMESTAMP NOT NULL, PRIMARY KEY (`owner_uid`,`mid`) ); CREATE INDEX `idx_owneruid_otheruid_msgid` ON IM_MSG_RELATION(`owner_uid`,`other_uid`,`mid`); 联系人列表: CREATE TABLE IM_MSG_CONTACT ( owner_uid INT NOT NULL, other_uid INT NOT NULL, mid INT NOT NULL, type INT NOT NULL, create_time TIMESTAMP NOT NULL, PRIMARY KEY (`owner_uid`,`other_uid`) ); 消息内容表:由于只是单库单表,消息ID采用自增主键,主要包括消息ID和消息内容。 消息索引表:使用了索引“归属人”和消息ID作为联合主键,可以避免重复写入,并增加了“归属人”和“关联人”及消息ID的索引,用于查询加速。 联系人列表:字段和索引表一致,不同点在于采用“归属人”和“关联人”作为主键,可以避免同一个会话有超过一条的联系人记录。 消息相关实体层类的数据结构和库表的字段基本一致,这里不再列出,需要注意的是:为了演示的简单方便,这里并没有采用分库分表的设计,所以分库的Sharding规则你需要结合用户消息收发和查看的场景,多加考虑一下库表的设计。 OK,有了用户和消息存储功能,现在来看如何支持消息未读数。 在“07 | 分布式锁和原子性:你看到的未读消息提醒是真的吗?”一讲中,我讲到了消息未读数在聊天场景中的重要性,这里我们也把未读数相关的功能加上来。 未读数分为总未读和会话未读,总未读虽然是会话未读之和,但由于使用频率很高,会话很多时候聚合起来性能比较差,所以冗余了总未读来单独存储。比如,你可以采用Redis来进行存储,总未读可以使用简单的K-V(Key-Value)结构存储,会话未读使用Hash结构存储。大概的存储格式如下: 总未读: owneruid_T, 2 会话未读: owneruid_C, otheruid1, 1 owneruid_C, otheruid2, 1 最后,我们一起来看看如何支持消息和未读自动更新。 在“03 | 轮询与长连接:如何解决消息的实时到达问题?”一讲中,我讲到了保证消息实时性的三种常见方式:短轮询、长轮询、长连接。对于消息和未读的自动更新的设计,你可以采用其中任意一种,我实现的简版代码里就是采用的“短轮询”来在联系人页面和聊天页面轮询未读和新消息的。实现上比较简单,Web端核心代码和服务端核心代码如下。 Web端核心代码: newMsgLoop = setInterval(queryNewcomingMsg, 3000); $.get( '/queryMsgSinceMid', { ownerUid: ownerUid, otherUid: otherUid, lastMid: lastMid }, function (msgsJson) { var jsonarray = $.parseJSON(msgsJson); var ul_pane = $('.chat-thread'); var owner_uid_avatar, other_uid_avatar; $.each(jsonarray, function (i, msg) { var relation_type = msg.type; owner_uid_avatar = msg.ownerUidAvatar; other_uid_avatar = msg.otherUidAvatar; var ul_pane = $('.chat-thread'); var li_current = $('
');//创建一个li li_current.text(msg.content); ul_pane.append(li_current); }); }); ); 服务端核心代码: List