Многие из нас не прочь посостязаться в эрудиции. Утолить свой соревновательный азарт можно с помощью специальных приложений, которые предлагают ответить на вопросы из разных профессиональных областей знаний.
В статье мы рассмотрим способ реализации приложения для соревнований на эрудицию, написанное на 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
.
Исходный код можно найти здесь.
Читайте также:
- Привет, Go!
- Как построить масштабируемый API на Go с помощью Gin
- Топ-10 самых распространенных ошибок в проектах Go. Часть 2
Читайте нас в Telegram, VK и Дзен
Перевод статьи Dilara Görüm: Build a Basic Real-Time Competition App With Go