项目概述
项目地址:https://github.com/sirius2alpha/scoreboard
使用Redis在服务器上对用户的点击数排序,并返回点击次数排行榜。
技术栈
整体设计
- 用户界面 排行榜展示区: 显示当前排行榜的状态。 点击按钮: 用户点击来增加他们的计分。 昵称输入和提交: 允许新用户输入昵称并参与排行榜。 实时更新监听: 不需要用户交互,自动更新排行榜。
- WebSocket客户端逻辑 建立连接: 当用户访问网站时,建立WebSocket连接。 发送点击事件: 当用户点击按钮时,发送消息到服务器。 接收排行榜更新: 监听来自服务器的排行榜更新,并更新界面。 用户注册: 发送新用户的昵称到服务器。 处理断开连接: 如果用户20秒未操作,发送断开消息到服务器。 后端设计(Gin + Redis)
- WebSocket服务器 处理WebSocket连接: 接受和管理WebSocket连接。 接收消息: 解析从客户端接收到的消息(点击事件,新用户注册)。 Redis交互: 更新用户的分数并重新排序排行榜。 广播排行榜更新: 将更新后的排行榜发送给所有连接的客户端。 处理断开: 移除30秒未操作的用户。
- Redis逻辑 用户分数管理: 存储和更新用户分数。 排行榜排序: 实时更新排行榜。 数据持久化: 保证数据在服务重启后仍然可用。
API设计
本项目API设计采用的是websocket实现。
由于考虑到用户在点击比较频繁,如果采用HTTP会造成头部开销较大,而websocket的头部开销会相对小一些。
消息类型
UserClick: { type: “UserClick”, nickname: “用户昵称” }
NewUser: { type: “NewUser”, nickname: “用户昵称” }
UserInactive: { type: “UserInactive”, nickname: “用户昵称” }
RankUpdate: { type: “RankUpdate”, ranks: [{nickname: “用户昵称”, score: 分数,ClickTime: 上次点击时间, ClickInterval: 上次点击间隔时间}, …] }
API流程
用户点击: 前端发送UserClick消息到服务器。 新用户加入: 前端发送NewUser消息到服务器。 服务器处理: 接收消息,更新Redis数据,并重新排序排行榜。 排行榜更新: 服务器广播RankUpdate消息到所有客户端。 前端更新界面: 客户端接收RankUpdate消息,更新排行榜显示。
前端设计
前端采用vue框架编写完成,UI组件采用elementplus
后端设计
项目后端使用 github.com/gorilla/websocket
和 github.com/gin-gonic/gin
实现一个基于 WebSocket 的实时通信服务。通过 WebSocket,服务能够实时接收和处理客户端发送的各种类型的消息,并根据消息类型执行相应的逻辑。此外,通过后台定时任务,服务还能够定期更新和广播用户的排名信息。这种实现方式对于需要实时通信和快速响应的应用场景非常合适。
backend
├── controllers
│ └── websocket.go
├── go.mod
├── go.sum
├── main.go
├── routers
│ └── router.go
└── services
└── redis-server.go
后端采用Gin框架完成,大致流程:
- 在main.go中启动路由,并且启动端口监听
- 在routers/router.go中定义/ws路由,用于接收websocket的连接
- 对于ws的处理,函数定于在controllers/websocket.go中,包括针对不同任务类型使用redis数据库的函数调用
- 在services/redis-server.go中,对各个任务如何具体操作redis进行定义
使用到的redis数据结构
a. Sorted Set
- 用途: 存储用户的点击次数,用于排名。
- 操作:
ZAdd
: 添加新用户或初始用户,并设置点击次数。ZIncrBy
: 增加用户的点击次数。ZRemRangeByRank
: 清空sorted set。ZRevRangeWithScores
: 获取点击次数最多的前10个用户。ZRem
: 删除不活跃的用户。
b. Hash
- 用途: 存储用户的最后点击时间和点击间隔。
- 操作:
HSet
: 初始化或更新用户的点击时间和点击间隔。HGet
: 获取特定用户的点击时间和点击间隔。Del
: 清空点击时间和间隔的记录。
用户行为处理逻辑
a. 用户添加与更新
- 新用户处理: 当有新用户加入时,通过
AddNewUser
函数将用户添加到sorted set中,并初始化点击次数为0。 - 用户点击处理: 在
HandleUserClick
函数中,每当用户点击,使用ZIncrBy
来增加其在sorted set中的得分(即点击次数),并记录点击时间。
b. 点击间隔更新
- 定时更新:
UpdateClickInterval
函数定期更新每个用户自上次点击以来的时间间隔。 - 时区处理: 代码中特别考虑了时区问题,将时间转换为中国标准时间(Asia/Shanghai)。
c. 用户排名获取
- 排名展示:
GetRanking
函数用于获取并返回用户的点击排名,包括用户ID、点击次数、上次点击时间和点击间隔。
d. 活跃状态检查
- 用户活跃度监测:
CheckAllUsers
和HandleUserInactive
函数用于检查所有用户的活跃状态。若用户的点击间隔超过20秒,则视为不活跃并从sorted set中移除。
WebSocket 通信与处理
a. WebSocket 升级器
- 配置: 设置了读写缓冲区大小为 1024 字节,并允许所有跨域请求。
- 功能: 用于将 HTTP GET 请求升级为 WebSocket 连接。
b. WebSocket 连接管理
- 连接记录: 通过全局变量
connections
记录所有打开的 WebSocket 连接。 - 消息处理: 在无限循环中监听并读取来自 WebSocket 连接的消息。
c. 消息解析与路由
- JSON 检查: 使用
isJSON
函数检查接收到的消息是否为 JSON 格式。 - 类型判断: 根据消息中的 “type” 字段,决定执行对应的处理逻辑。
消息处理逻辑
- 新用户处理: 当收到类型为 “NewUser” 的消息时,调用
services.AddNewUser
添加新用户。 - 用户点击处理: 收到 “UserClick” 类型的消息时,调用
services.HandleUserClick
处理用户点击事件。 - 用户不活跃处理: 对于 “UserInactive” 类型的消息,执行
services.HandleUserInactive
以处理不活跃用户。
定时任务处理
- 后台定时任务: 使用 goroutine 定期执行用户点击间隔的更新、用户活跃状态检查和用户排名获取。
- 间隔设置: 目前设置为每 200 毫秒执行一次循环中的任务。
用户排名的 WebSocket 广播
- 排名信息广播: 定期将用户排名信息通过所有打开的 WebSocket 连接广播给客户端。
需要注意的改进点
- 错误处理: 在一些关键操作后,需要更全面地处理可能的错误返回值。
- 性能优化: 随着 WebSocket 连接数的增加,消息广播可能成为性能瓶颈。
可以修改的一些bug
1、用户在登录的时候遇到相同用户名,会把他直接刷新
2、手机端自适应功能差,体验不好
- 手机在点击按钮的时候。会触发双击浏览器双击放大的功能,影响体验
- 手机端的网页有时候滑动不了
- 有时候手机端最上面的两个按钮会被浏览器的头部遮挡,但是又滑动不上去
3、对于只登录而没有点击的用户,排行榜中会保留下来,但不会清理
上一次的点击间隔和上次点击时间都不会刷新,后端是根据间隔时间清理用户,虽然可以保留,但是一直保存着也不是办法,可以设置一个单独的时长进行清理。
部署到服务器上
现在云服务商的域名管理处新建了一个子域名,然后在服务器上使用了nginx给子域名提供对应的服务。
服务器上保持资源最简单就行,不用把源码都放到服务器上,对于前端的vue框架这边,只用把npm build
生成的/dist目录上传就行;对与gin来说,也只需要把go build main.go
生成的main可执行文件上传到服务器就行,这样也更加安全。
遇到的一些问题:
打开网站显示500,先去检查nginx的日志,然后发现我把/dashboard整个文件夹放在了/root/下面,导致nginx没有权限进行访问,然后就把它转移到了/var/www/下去了。
然后就是需要注意api的地址要写对,比如这个项目中是/ws,需要在/etc/nginx/sites-avilable/scoreboard
中写正确。