141 lines
5.2 KiB
Markdown
141 lines
5.2 KiB
Markdown
+++
|
||
date = '2024-11-24T15:23:50+01:00'
|
||
draft = true
|
||
title = 'Classroom Buzzer App'
|
||
tags = ["education","golang","websockets","concurrency"]
|
||
+++
|
||
|
||
## I started working on a new project today.
|
||
|
||
The client is an educational services provider and wants me to develop a tool to facilitate in-person group activities. This tool should:
|
||
|
||
- provide a platform which students can log into from a mobile device, in a frictionless process that takes seconds
|
||
- allow a teacher to assign groups and pairs in
|
||
- allow the teacher to dynamically reassign groups without repeating combinations
|
||
- implement a simple score-keeping functionality
|
||
- be able to run a 'buzzer' game
|
||
- have a clean, appealing, and user-friendly UI
|
||
|
||
A quick read of this brief should make it clear that what is required here is a server capable of handling multiple, live, two-way connections. The server needs to be able to update the clients whenever the teacher wants to shuffle the groups, and the buzzer game requires that when a student 'buzzes', state is propagated via the server to all other clients. The solution to this problem is websockets, and a server capable of handling concurrency.
|
||
|
||
Now I'm relatively new to both of these topics, but my project [GoPaper](https://projects.ajstepien.xyz/andrzej/gopaper) made use of concurrent goroutines to implement a wallpaper daemon, so I'll be building off that knowledge and developing the server side of this project in Go.
|
||
|
||
## WebSockets in Go
|
||
|
||
It is possible to build a websocket framework entirely from scratch in Go, but seeing as I don't hate myself *quite that much*, I'll be using the framework [Gorilla](https://github.com/gorilla/websocket).
|
||
|
||
## Getting started: running concurrent websocket connections
|
||
|
||
```go
|
||
package main
|
||
|
||
import (
|
||
"log"
|
||
"net/http"
|
||
"sync"
|
||
|
||
"github.com/gorilla/websocket"
|
||
)
|
||
|
||
type webSocketHandler struct {
|
||
upgrader websocket.Upgrader
|
||
}
|
||
|
||
// GLOBALS
|
||
var connections int = 0
|
||
var wg sync.WaitGroup
|
||
|
||
func (wsh webSocketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||
//upgrade http connection to websocket
|
||
connection, err := wsh.upgrader.Upgrade(w, r, nil)
|
||
if err != nil {
|
||
log.Printf("Error while upgrading connection to websocket: %s", err)
|
||
return
|
||
}
|
||
connections++
|
||
log.Printf("New websocket connection. There are now %v\n", connections)
|
||
wg.Add(1)
|
||
go func() {
|
||
for {
|
||
//actually handle the websocket connection here
|
||
}
|
||
}()
|
||
}
|
||
|
||
func main() {
|
||
webSocketHandler := webSocketHandler{
|
||
upgrader: websocket.Upgrader{},
|
||
}
|
||
var port string = "8080"
|
||
http.Handle("/", webSocketHandler)
|
||
wg.Add(1)
|
||
log.Printf("listening on port %s...\n", port)
|
||
log.Fatal(http.ListenAndServe("localhost:8080", nil))
|
||
wg.Wait()
|
||
}
|
||
```
|
||
|
||
In this quick-and-dirty bit of code, we create a websocket handler that upgrades an http connection to a websocket connection, and then launches a goroutine. The goroutine doesn't do anything yet, but running ´websocat´ from multiple terminals shows us that we are now capable of handling multiple concurrent websocket connections 👍.
|
||
|
||
```bash
|
||
websocat ws://localhost:8080/
|
||
```
|
||
|
||
```
|
||
New websocket connection. There are now 1.
|
||
```
|
||
|
||
```
|
||
New websocket connection. There are now 2.
|
||
```
|
||
|
||
```
|
||
New websocket connection. There are now 3.
|
||
```
|
||
|
||
We know the connections have been made as either websocat or our Go app would error out otherwise. We should certainly do something to handle closed connections, as this code is a memory leak waiting to happen as things stand, but as a proof of concept, it works.
|
||
|
||
Now, time to actually do something with these connections! Let's add some logic to that goroutine.
|
||
|
||
```go
|
||
func (wsh webSocketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||
//upgrade http connection to websocket
|
||
connection, err := wsh.upgrader.Upgrade(w, r, nil)
|
||
if err != nil {
|
||
log.Printf("Error while upgrading connection to websocket: %s", err)
|
||
return
|
||
}
|
||
connections++
|
||
log.Printf("New websocket connection. There are now %v\n", connections)
|
||
wg.Add(1)
|
||
go func() {
|
||
defer connection.Close()
|
||
for {
|
||
msgType, message, err := connection.ReadMessage()
|
||
if err != nil {
|
||
log.Printf("Error trying to read message from client: %s", err)
|
||
return
|
||
}
|
||
if msgType == websocket.BinaryMessage {
|
||
err = connection.WriteMessage(websocket.TextMessage, []byte("this server does not support binary messages"))
|
||
if err != nil {
|
||
log.Printf("Error trying to send message to client: %s", err)
|
||
}
|
||
return
|
||
}
|
||
log.Printf("received message from client: %s", message)
|
||
err = connection.WriteMessage(websocket.TextMessage, []byte("Message received!"))
|
||
}
|
||
}()
|
||
}
|
||
```
|
||
|
||
### Here's what we just added:
|
||
|
||
- The `defer` keyword closes the connection if/when the goroutine returns. In its current state, that would only happen in the case of an error.
|
||
- `connection.ReadMessage()` blocks until a message is received from the client
|
||
- If the client tries to send binary data, we bounce an error message back to the client, log an error on the server and return from the goroutine.
|
||
- If the message is text, we log it on the server and send a thank-you message back to the client. Testing this again with multiple instances of `websocat` shows that everything is working as it should. We have established two-way channels of communication between a server and multiple clients! 🥳
|
||
|
||
In my next post, I'll begin implementing the business logic.
|