端到端的实现
我们大多数人都喜欢知识竞赛,对吧? 有许多应用程序可以通过让我们回答来自不同职业的问题来解渴。
在这篇文章中,我将解释我是如何使用 Golang 实现一个实时竞赛应用程序的。
申请流程
需要遵循一些业务规则。
- 当连接用户达到两个时,比赛将在 3 秒内自动开始。
- 我们的比赛共有三个州。这些是 NOT_STARTED、STARTED、FINISHED。
- 问题有四个选项,用户必须在 10 秒内回答一个问题。
- 比赛结束后,向用户展示排行榜以查看比赛结果。
我的架构决策
在本节中,我将尝试解释为什么我做出了一些决定,并在开始之前尝试对我们的项目提出一些观点。这部分就像电影剧透一样。
- Websocket 是实现实时应用程序的基本协议。它提供客户端和服务器之间的双向通信。有很多技术文章介绍了它的概念,我就不赘述了。我用它来发送问题并获得连接用户的答案。
- 我为每个连接的用户使用了一个唯一的 id(如 Session-id)。这样做,我可以轻松区分用户。在我们的例子中,我们用他们的会话 ID 存储我们的用户,我们的服务器使用它们来管理读写操作。
- 为了支持并发读写操作,我使用了 sync.Map 。我使用 sessionID 作为键,使用 Client 结构作为如下所示的值。客户端结构由两个字段组成,一个用于写入和读取的客户端 WebSocket 连接以及用于计算排行榜的 totalScore。
- 广播是一个特殊术语,表示同时将消息传输给所有接收者的方法。 不幸的是,gorilla/websocket 中没有广播方法; 因此,我们将使用我们自定义的广播方法向所有用户发送消息。
- 要将问题发送给我们的用户,我们需要定义一个模型。 在我们的应用程序中,我不想使用 Question 结构,因为它具有 correct_answer 字段。 我想对连接的用户隐藏这些信息,所以我创建了另一个名为 QuestionDTO 的模型,如下所示。
比赛流程
我曾经使用 RunCompetition() 方法管理比赛流程,如下所示。
我通过创建一个 goroutine 在应用程序的主要流程中调用了这个函数。
func RunCompetition() {
CompetitionState = CompetitionNotStartedState
for {
if CompetitionState == CompetitionNotStartedState {
time.Sleep(CompetitionStateDuration)
numberOfClients := CountClient()
msg := DetermineCompetitionState(numberOfClients)
BroadcastMessage([]byte(msg))
if numberOfClients == 2 {
time.Sleep(CompetitionStartDuration)
CompetitionState = CompetitionStartedState
}
} else if CompetitionState == CompetitionStartedState {
PrepaRequestions()
StartSendingQuestions()
CompetitionState = CompetitionFinish
} else if CompetitionState == CompetitionFinish {
leaderBoard := CreateLeaderBoard()
jsonBytes, _ := json.Marshal(leaderBoard)
BroadcastMessage(jsonBytes)
BroadcastMessage([]byte(CompetitionFinishedStateMessage))
break
}
}
}
RunCompetition() 方法中有三个 CompetitionState。
- CompetitionNotStartedState:要开始比赛,必须有两个用户。 当 numberOfClients 等于 2 时,状态变为 CompetitionStartedState。
- CompetitionStartedState:在这种状态下,我们每 10 秒向所有连接的用户发送问题。
func StartSendingQuestions() {
for i := range Questions {
Questions[i].IsTimeout = false
questionDTO := Questions[i].ToDTO()
questionDTOBytes, _ := json.Marshal(questionDTO)
BroadcastMessage(questionDTOBytes)
time.Sleep(QuestionResponseIntervalDuration)
Questions[i].IsTimeout = true
}
}
发送问题时,我们将 Question 结构体转换为 questionDTO 以向连接的用户隐藏正确答案以防止作弊。
10 秒后,我们将 IsTimeout 更改为 true,因为给问题的时间到了。
- CompetitionFinish:发送完所有问题后, CompetitionState 变为 CompetitionFinish。 比赛结束后,我们必须创建排行榜并将其发送给所有连接的用户。
处理客户端应答流
我们需要从用户那里得到答案来检查和计算他们的分数。
func HandleClientAnswer(sessionID string, message []byte) {
var ClientMsg ClientMessage
json.Unmarshal(message, &ClientMsg)
for _, question := range Questions {
if question.ID == ClientMsg.QuestionId {
if question.IsTimeout == true {
fmt.Println("Response Time is out")
} else {
load, _ := Clients.Load(sessionID)
client := load.(Client)
if ClientMsg.Answer == question.CorrectAnswer {
client.totalScore = ScoreForCorrectAnswer
Clients.Store(sessionID, client)
fmt.Printf("Right Answer!!! SessionId: %s, TotalScore: %d\n", sessionID, client.totalScore)
} else {
fmt.Printf("Wrong Answer!! Your Answer is : %s, Right Answer is : %s, SessionId: %s, TotalScore: %d\n", ClientMsg.Answer, question.CorrectAnswer, sessionID, client.totalScore)
}
}
}
}
}
用户在指定的时间间隔内正确回答问题可以获得分数( 10)。
这里有一些重要的点:
- IsTimeout:指定时间间隔
- 比较 question.ID 和 ClientMsg.QuestionId :判断是否被问到问题
- 比较 ClientMsg.Answer 和 question.CorrectAnswer :判断用户是否给出了正确答案。
满足这些条件后,用户的分数通过 sessionID 存储在地图上。 我们在 ws() 方法中调用这个函数。
func ws(c echo.Context) error {
numberOfClients := CountClient()
if numberOfClients >= 2 {
return c.string(http.StatusBadRequest, "")
}
wsConn, err := Upgrader.Upgrade(c.Response(), c.Request(), nil)
if err != nil {
return err
}
defer wsConn.Close()
sessionID := IDGenerator()
Clients.Store(sessionID, Client{
wsConn: wsConn,
totalScore: 0,
})
for {
_, message, err := wsConn.ReadMessage()
if err != nil {
Clients.Delete(sessionID)
c.Logger().Errorf("Client disconnect msg=%s err=%s", string(message), err.Error())
return nil
}
HandleClientAnswer(sessionID, message)
}
}
为了让我们的应用程序简单,比赛将从两个用户开始。 如果用户的数量大于 2,我们的应用会发送 http.StatusBadRequest 给用户。
关注七爪网,获取更多APP/小程序/网站源码资源!
,