Источник: Nuances of Programming
Многие из нас не прочь посостязаться в эрудиции. Утолить свой соревновательный азарт можно с помощью специальных приложений, которые предлагают ответить на вопросы из разных профессиональных областей знаний.
В статье мы рассмотрим способ реализации приложения для соревнований на эрудицию, написанное на Golang и работающее в режиме реального времени.
Архитектура приложения
Ниже — требования, предъявляемые к бизнес-логике.
- Когда количество подключенных пользователей достигает 2-х человек, в течение 3 секунд автоматически начинается соревнование.
- Соревнование включает 3 состояния: NOT_STARTED (не начато), STARTED (начато) и FINISHED (закончено).
- Вопросы сопровождаются 4 вариантами ответов, и пользователь должен ответить на вопрос в течение 10 секунд.
- По окончании соревнования выводится рейтинговая таблица, отражающая результаты пользователей.
Архитектурные решения
В данном разделе я аргументирую выбранные решения и представляю видение проекта. Эта часть статьи — как спойлер к фильму.
- Websocket — важный протокол для реализации приложений в реальном времени. Он обеспечивает двустороннее взаимодействие между клиентом и сервером. Здесь мы не будем подробно разбирать его концепции, поскольку по ним представлено немало практических материалов. Websocket осуществляет отправку вопросов и получение ответов от подключенных пользователей.
- Для каждого пользователя используется уникальный id (по аналогии с id сессии), благодаря которому их легко различать. Мы сохраняем пользователей с id сессии, и с их помощью сервер управляет операциями чтения и записи.
- sync.Map поддерживает одновременные операции чтения и записи. sessionID применяется в качестве ключа, а структура Client — в качестве значения, как показано ниже. Client состоит из двух полей: клиентского соединения WebSocket для записи и чтения и totalScore для расчета рейтинговой таблицы.
- Широковещание (англ. broadcast) — это специальный термин для обозначения метода одновременной передачи сообщения всем получателям. К сожалению, такого метода нет в gorilla/websocket, поэтому воспользуемся пользовательским методом широковещания для отправки сообщений пользователям.
- Для отправки вопросов пользователям необходимо определить модель. Я отказалась от применения в приложении структуры Question из-за наличия у нее поля correct_answer. Мне хотелось скрыть информацию от подключенных пользователей, поэтому я создала модель QuestionDTO, как показано ниже:
Управление потоком соревнования
Для управления потоком соревнования используется метод RunCompetition().
Создаем горутину и вызываем эту функцию в основном потоке приложения:
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() содержит 3 состояния 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, чтобы скрыть правильный ответ correct_answer от пользователей и предотвратить обманные действия.
По истечении 10 секунд IsTimeout устанавливается в значение true, поскольку истекает время, выделенное на ответ.
- CompetitionFinish. После отправки всех вопросов состояние CompetitionState меняется на CompetitionFinish. В момент окончания соревнования мы должны создать и отправить LeaderBoard.
Обработка ответов пользователей
Нам нужно получить ответы от пользователей, чтобы проверить и подсчитать результаты:
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)
}
}
Чтобы приложение оставалось простым, соревнование начинается при наличии двух пользователей. Если же их больше двух, то приложение выдает ошибку http.StatusBadRequest.
Исходный код можно найти здесь.
Читайте также:
Перевод статьи Dilara Görüm: Build a Basic Real-Time Competition App With Go