====== 即时消息系统 ====== ===== 系统结构 ===== {{:技术:pasted:20210728-062015.png}} 客户端:x,A,B,C,D共5个客户端用户 服务端 -所有模块与服务抽象为server -所有用户在线状态抽象存储在高可用cache里 -所有数据信息,例如群成员、群离线消息抽象存储在db里 典型群消息投递流程,如图步骤1-4所述: 步骤1:群消息发送者x向server发出群消息 步骤2:server去db中查询群中有多少用户(x,A,B,C,D) 步骤3:server去cache中查询这些用户的在线状态 步骤4:对于群中在线的用户A与B,群消息server进行实时推送 步骤5:对于群中离线的用户C与D,群消息server进行离线存储 典型的群离线消息拉取流程,如图步骤1-3所述: 步骤1:离线消息拉取者C向server拉取群离线消息 步骤2:server从db中拉取离线消息并返回群用户C 步骤3:server从db中删除群用户C的群离线消息 ===== 技术难点及解决方案 ===== ====群消息方案==== 1-群消息表 gid,sender_id,time,msgid,msg **2-群成员表 group_user uid,gid,last_ack_msg_id存ack最后的一个msgid** 3-server发送实时消息: 保存群消息在数据表中, 在处理每个消息的keepalive ,发送ack 时,同时推送群消息,得到ack 后,更新 offline_msgs 也可以推送群消息id,访客再拉,但是这个就很麻烦。 4-拉取离线消息 步骤1:访客先拉取所有的离线消息msg_id 步骤2:访客再根据msg_id拉取msg_detail 步骤3:应用层ack 后,更新离线msg_id, 5- 消除“消息风暴扩散系数”的存在,减少ACK: 假设1个群有500个用户,“每条”群消息都会变为500个应用层ACK,将对服务器造成巨大的冲击,有没有办法减少ACK请求量呢? 批量ACK的方式又有两种: (1)每收到N条群消息ACK一次,这样请求量就降低为原来的1/N了 (2)每隔时间间隔T进行一次群消息ACK,也能达到类似的效果 复合打包在ack 报文中 6- 群离线消息过多:拉取过慢 - 分页拉取(按需拉取),取最后10条,然后上拉接着取更多 ====消息可靠传输方案==== === 方案 应用层确认=== im的报文分为三种请求报文(Request R),应答报文(acknowledge,后简称为A)通知报文(notify,后简称为N) 典型的报文发送 A发送给服务器,服务器发送给B,B 发送确认,服务器确认收到ACK,服务器通知A 已经收到回包 {{:技术:pasted:20210728-094030.png}} {{:技术:pasted:20210728-094147.png}} 一个应用层即时通讯消息的可靠投递,共涉及6个报文,这就是im系统中消息投递的最核心技术。 === 丢消息解决方案 -超时重传 === client-A需要在本地维护一个等待ack队列,并配合timer超时机制,来记录哪些消息没有收到ack:N,以定时重发。 一旦收到了ack:N,说明client-B收到了“你好”消息,对应的消息将从“等待ack队列”中移除。 client-B 根据接收到的消息id , 来进行去重,或者进行拉取前面的消息,并且进行排序。 === 文件传输 === 断点续传,支持断点续传协议;文件起始位置;文件接收位置,MD5校验 === 语音视频 === 数据采集频率和数据播放频率的一致,还有数据队列处理。 ==== 数据一致性方案 单点序列化==== 【利用单点序列化,可以保证多机相同时序】 数据为了保证高可用,需要做到进行数据冗余,同一份数据存储在多个地方,怎么保证这些数据的修改消息是一致的呢?利用的就是“单点序列化”: (1)先在一台机器上序列化操作 (2)再将操作序列分发到所有的机器,以保证多机的操作序列是一致的,最终数据是一致的 ==== 消息一致性方案 ==== ===自增ID === 发送方ID , 服务器方ID , 全局id 集群 群消息id “全局序号生成器”作为“时序基准”,可以解决每条消息没有标准“生产日期”的问题,按着实现方式可以分为两类,一是支持单调自增序号的生成,如Redis的原子自增命令incr、MySQL的自增ID,二是分布式时间相关的ID生成,如snowflake算法、时间相关的分布式序号生成服务等。 时间+机器+序号 snowflake是Twitter开源的分布式ID生成算法,结果是一个long型的ID 64位。其核心思想是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是0。 ===消息整流 === 按序号排序 ==== 省流量 ==== (1)先拉取100个好友的时间戳 (2)客户端将100个好友的时间戳与本地时间戳对比,找出差异,假设有10个好友的信息发生了变化,时间戳改变了 (3)拉取有变化的10个好友的信息 ==== 状态同步 ==== (1)好友状态,如果对实时性要求较高,可以采用推送的方式同步;如果实时性要求不高,可以采用轮询拉取的方式同步 (2)群友的状态,由于消息风暴扩散系数过大,可以采用按需拉取,延时拉取的方式同步 (3)系统消息/开屏广告等对实时性要求不高的业务,可以采用拉取的方式获取消息 (4)“消息风暴扩散系数”是指一个消息发出时,变成N个消息的扩散系数,这个系数与业务及数据相关,一定程度上它的大小决定了技术采用推送还是拉取 ==== WEBIM 方案 ==== == http长轮询 == {{:技术:pasted:20210728-101114.png}} 这个http消息连接对于webserver的请求压力是90秒1次,能够大大节省了web服务器资源。 当数据更新频率不确定时长轮训机制能够很明显地减少请求数。但是,在数据更新比较频繁的场景下,长轮训方式的优势就没那么明显了。 在Web开发中使用得最为普遍的长轮训实现方案为Comet(Comet (web技术)) == HTTP Streaming == HTTP Streaming则试图改变这种方式,其实现机制为:客户端发送获取数据更新请求到服务端时,服务端将保持该请求的响应数据流一直打开,只要有数据更新就实时地发送给客户端。 问题: (1)HTTP Streaming的实现机制违背了HTTP协议本身的语义,使得客户端与服务端不再是“请求-响应”的交互方式,而是直接在二者建立起了一个单向的“通信管道”。 (2)在HTTP Streaming模式下,服务端只要得到数据更新就发送给客户端,那么就需要客户端与服务端协商如何区分每一个更新数据包的开始和结尾,否则就可能出现解析数据错误的情况。 (3)另外,处于客户端与服务端的网络中介(如:代理)可能会缓存响应数据流,这可能会导致客户端无法真正获取到服务端的更新数据,这实际上与HTTP Streaming的本意是相违背的。 == websocket == websocket 基于tcp 协议 , WebSocket协议比较轻量,WebSocket是一个全新的应用层协议,专门用于Web应用中需要实现动态刷新的场景。 一个数据包就是一条完整的消息;而WebSocket客户端与服务端通信的最小单位是帧(frame),由1个或多个帧组成一条完整的消息(message)。即:发送端将消息切割成多个帧,并发送给服务端;服务端接收消息帧,并将关联的帧重新组装成完整的消息。 WebSocket是双向通信模式,客户端与服务器之间只有在握手阶段是使用HTTP协议的“请求-响应”模式交互,而一旦连接建立之后的通信则使用双向模式交互,不论是客户端还是服务端都可以随时将数据发送给对方;而HTTP协议则至始至终都采用“请求-响应”模式进行通信。 == 全双工实现UDP: == _beginthread(RecvMain, 1024 * 1024, (void*)sock); while (true) { sendto(sock, s, strlen(s), 0, (sockaddr*)&sa, sizeof(sa)); == 全双工实现TCP == char s[256] = { 0 }; while (true) { SOCKET socka = accept(sock, NULL, NULL); //第二三个参数是连接者的ip和端口等信息,是返回类型的值,不需要可以置null _beginthread(recvProc, 0, (void*)socka); //void*指向任何类型的指针 _beginthread(sendProc, 0, (void*)socka); //void*指向任何类型的指针 }