+++ 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.