# apps **Repository Path**: edwardaaron/apps ## Basic Information - **Project Name**: apps - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2024-08-10 - **Last Updated**: 2025-01-18 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README #### 简介 ##### 主要功能 - 单聊、群聊 - 在线发送消息 - 离线发送消息 - 查看历史记录 ##### 组成模块 该系统主要由如下几个模块组成 - 网关模块:使用springBoot+nacos-discover+nacos-config+springCloudGateway实现,具体功能包路由转发和jwt认证 - 用户服务模块:使用springBoot+springMVC+nacos-discover+nacos-config+redis+mysql+es实现,使用swagger2作为文档工具,主要功能包括 - 用户管理 - 选择消息服务器 - 搜索模块:使用springBoot+springMVC+nacos-discover+nacos-config+es实现,主要功能包括 - 搜索用户 - 搜索消息 - 所有聊天群 - id生成模块:使用springBoot+nacos-discover+nacos-config+mysql实现,使用swagger2作为文档工具,能够快速的获取id,id是单调、递增、非连续且两个id间隔不固定,中间缓存是自己写的,后续可以使用caffine 代替(目前感觉不错,临时不用),主要包括 - 获取单个id - 批量获取id - 获取某个范围内的id - 消息服务器模块:使用springBoot+nacos-discover+nacos-config +es+rocketmq+redis实现,该模块集群部署,该模块主要包许下功能 - 接收消息、存储消息 - 将消息转发到客户端 - 将消息转发到其他消息服务器节点 - 消息的类型目前包括 - 握手消息 - 添加联系人消息 - 普通消息,这种消息即用户在客户端可以看见,可以发送的消息, 按照消息的来源可以分为 - 系统消息 - 用户消息 按照接收人的个数,可以分为 - 个人对个人消息 - 群消息 这种消息含有一个明确的消息体,消息体可以包括如下几种类型 - 文本消息 - 图片消息 - 声音消息(未实现) - 视频消息(未实现) - 通用模块:该模块被其他模块引用,不独立部署。该提供了一下通用的类、工具、通过springboot 自动部署功能,添加了一些bean,这些bean并不是通用的,会依据不同条件创建或不创建。 - 监控模块(还没有开发) ##### 技术概要 ###### 所使用的技术 - springboot、swagger2、log4j2、jackson、des、msgpack、netty、rocketmq、es、javaSwing、mysql、redis、nacos、springCloudGateway、jwt等,具体如下 - 使用springBoot作为基础架构,使用log4j2作为日志架构 - 客户端和通用服务端(包括用户管理服务、搜索服务、id服务等)通过发送的http请求进行通讯。请求使用jackson+msgpack进行解码和编码 - 消息服务器使用netty发送和接受消息,客户端和消息服务器之间的息消息使用des进行加密,具体参考 **消息加密** ##### 附加内容 ###### 消息存储 - 考虑到数据量,消息服务器只存储未读消息 todo 后续可能存储,比如存储30天 - 已读消息存储在客户端,因此如果客户端重新安装(在其他目录中),会丢失数据。 ###### 消息加密 - 使用des对称加密 - 加密内容为消息体 - 客户端c1 ,使用密钥cs1对消息体进行加密,然后发送 - 服务端n,接收到消息m使用cs1解密出m2,使用ns1(整个系统只有一个,目前不支持更改)对m2加密,加密后保存,使用cs2对m2加密,然后发送给c2 - c2接收到消息后,使用cs2解密 ###### 关于消息集群服务器 - 客户端和消息服务器node-i之间的链接为长链接conn,node-i会将用户id和conn关联,保存本地缓存中,同时也同步到redis中(表现形式未用户id->node-i表示)。 - node-i 不能把消息转发到客户端的时候,需要将消息交给其他node-i处理,通过rocketmq可以将消息转发到其他node-i上,这里需要处理如下几个问题 - 其他node-i怎样接收到正确的消息,即不能从mq中接收到发送给其他node-i的消息,解决方法为每个node-i分配一个队列,可以使用ip进行区分,也可以java 启动参数进行区分,在本模块使用ip+java配置参数结合的方式,即优先使用java参数,其次使用ip - node-i怎样把消息发送给正确的队列上。首先可以考虑将所有 **用户id->node-i**的对应映射关系存储起来,通过该映射关系就可以确定要将消息发送给正确的队列。但是如果用户过多则会出现问题,假定在线用户为1亿(想象有😄),使用Long存储id,经过计算则需要700m来存储,会有如下缺点 - 占用过多内存 - 服务器启动的时候会特别长(可以通过懒加载来解决) - 用户上下线时候,需要在所有节点之间同步数据,占用带宽 **本项目采用如下策略** - 将**用户id->node-i**的对应映射关系存储到redis中 - node-i 存储部分映射,存储数量为**用户id数量/node-i的数量**,使用caffine作为缓存技术存储 - 为每个node-i和redis建立一个心跳机制,本项目是用过设置一个变量time-node-i的过期时间来实现。 - 每个node-i为 key **time-node-i** 设置一个监听器,删除本地无效的映射,同时删除redis中的无效映射 ###### 关于http请求 - 客户端到服务端使用 https(还未实现,目前是http) 发送请求,服务端内部使用http发送请求 - 客户端到服务端使用jwt实现认证 ###### 接收和发送消息 使用netty作为发送和接收消息的底层构建,客户端和消息服务器有且只有一个链接,建立链接后,会生成一个channelPipeLine实例:     在客户端该实例会保存到spring容器中,其他发送和接收消息的service会使用该实例     在服务端该实例会保存到内存缓存中(caffine),该缓存有特定的淘汰机制 ###### p2p消息流程 假定用户A向用户B发送一条消息,其流程如下 - 1- 用户A在客户端 ca 发送一条消息,在本地存储该消息,该消息的状态为 *发送中*,如果发送失败,则将消息的状态改为 *发送失败* - 2- netty服务器集群中的一台机器 n1 接收到该消息 - 3- n1 检查是否可以将消息发送到B, - 如果可以发送, - 3.1-存储该消息(存储方式多样,建议存储在es上,实现也是存储在es上,该存储过程一定保证成功),该消息的状态为 *未接收* - 3.2- 向A发送一条响应消息,A接收到后,会更新消息的状态未 *发送成功* - 3.3-将消息发送用户B,如果发送失败(例如链接断开、用户下线),则将消息发送到mq上的delay_q(所有的netty都监听该队列)上,netty 服务器会每隔一段时间,从该队列获取一批消息(mq控制时间间隔),尝试发送到Bi(B1,B2 ...)上 - 如果消息不能发送,查找可以发送消息的netty 服务器 ni, - 4-1 如果ni存在,则将消息发送到mq上的m_q_i队列上(ni 监听该队列),ni接收到该消息后按照3.1的方式处理该消息 - 4-2 如果ni不存在,说明B在线,则将消息发送到delay_q上 - 4 - cb(用户B所在的客户端),接收消息后,转换、存储、展示 消息,向n2发送一条响应消息,表示该消息已接收、或已读 - 5 -n2接收到消息后,处理方式同步骤3,注意和3.1有所不同的是, - 消息的状态为*已接收* 或 *已读* - 如果消息的状态为已接受,则需要更新用户的未读消息数量 - 6- ca 接收到响应消息后,更新消息的状态为 *已接收* 或 *已读* ###### p2p消息流程-已接受未读 此流程和**p2p消息流程** 类似,但在此基础上会有额外的处理流程 - 1 用户已读消息后(即切换到该消息对应的聊天窗口),发送一条请求消息,表示该消息已经读取 - 2 n2 接收到该消息后,处理方式和**p2p消息流程** 步骤3处理基本一样,但是有一些区别, - 该消息不会存储在es上,而是从es查找a发送的消息,将该消息的状态改为 *已读* - 跟新用户未读消息的数量 - 3 ca接收到该,更新消息的状态未 *已读*,同时向n1 发送一条响应消息 ###### 添加好友流程 该流程和**p2p消息流程**类似,但也有些许不同,在技术上体现为使用不同processer,区别具体如下:假如用户A要将用户B添加到通讯录 - 消息的具体类型不同,该消息为:请求将用户B添加到通讯录 - cb 处理消息的方式不同:返回的响应表示 同意请求或拒绝请求 - netty server处理响应不同,根据响应的具体内容会做出如下不同操作 - 更新es上的消息的状态(*未读*)为 *同意* 或 *拒绝* - 如果响应消息表示为同意, - 向用户系统发送http请求,将A和B添加为好友 - 将http请求的结果,发送给A - 如果响应消息表示为拒绝,则将响应消息直接发送给A - ca接收到响应消息后,查看结果,如果是同意,则在本地将B添加到通讯录中 ###### 关于消息服务处理消息  参考 系统架构图.jpg ###### 关于客户端和服务器数据同步     当客户端在其他设备登录的时候,需要数据同步     这里的数据包括3类: - 用户自身属性数据,包括用户的名称、性别、头像等 - 联系人、聊天室、收藏夹(没有实现)等非自身属性数据 - 未读消息     不包括已读消息(系统消息、其他用户发送过来的消息),已读消息室通过手动刷新来实现        同步的方法 - 客户端在登录的时候进行同步,系统会为每个用户维护一个版本号v,当用户登录的时候会比较本地的版本号,和服务端的版本号,如果不同,则更新数据 - 同步未读消息需要特殊的方法 1. 客户端定时(10秒)扫描一次,发现是否有新的未读消息,如果有则同步到服务器 2. 客户端在登录的时候,会比较本地版本号v1(和v相互独立)和服务器版本号,如果不同,则从服务器上获取最新的内容 ###### 关于客户端和消息服务端之间的健康链接 待补充 ###### 关于客户界面     客户端界面模仿微信客户端界面。 ###### 关于客户端自动重连     当客户端检测到和消息服务器的链接断开时候,会使用原有的地址和端口链接,失败三次后,获取新的消息服务器的地址和端口,再链接一次,失败后再使用该地址链接三次,然后再获取新的地址,依次循环,知道链接成功。 待补充 #### 具体功能介绍 ##### 网关 ##### 用户管理 ##### 搜索 ##### id服务 ##### 消息服务器 ##### 客户端     目前只有pc客户端 ###### 本地保存数据 ###### 数据同步