项目概述

项目地址:https://github.com/sirius2alpha/scoreboard

使用Redis在服务器上对用户的点击数排序,并返回点击次数排行榜。 首页

排行榜

技术栈

scoreboard技术栈

整体设计

  1. 用户界面 排行榜展示区: 显示当前排行榜的状态。 点击按钮: 用户点击来增加他们的计分。 昵称输入和提交: 允许新用户输入昵称并参与排行榜。 实时更新监听: 不需要用户交互,自动更新排行榜。
  2. WebSocket客户端逻辑 建立连接: 当用户访问网站时,建立WebSocket连接。 发送点击事件: 当用户点击按钮时,发送消息到服务器。 接收排行榜更新: 监听来自服务器的排行榜更新,并更新界面。 用户注册: 发送新用户的昵称到服务器。 处理断开连接: 如果用户20秒未操作,发送断开消息到服务器。 后端设计(Gin + Redis)
  3. WebSocket服务器 处理WebSocket连接: 接受和管理WebSocket连接。 接收消息: 解析从客户端接收到的消息(点击事件,新用户注册)。 Redis交互: 更新用户的分数并重新排序排行榜。 广播排行榜更新: 将更新后的排行榜发送给所有连接的客户端。 处理断开: 移除30秒未操作的用户。
  4. 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/websocketgithub.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. 活跃状态检查

  • 用户活跃度监测: CheckAllUsersHandleUserInactive函数用于检查所有用户的活跃状态。若用户的点击间隔超过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中写正确。