diff --git a/content/posts/buzzer-game.md b/content/posts/buzzer-game.md new file mode 100644 index 0000000..08392ca --- /dev/null +++ b/content/posts/buzzer-game.md @@ -0,0 +1,140 @@ ++++ +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. diff --git a/public/categories/index.html b/public/categories/index.html index 25c0f27..e0639f4 100644 --- a/public/categories/index.html +++ b/public/categories/index.html @@ -1,12 +1,12 @@ -
+