Coding with Andrzej

Classroom Buzzer App

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:

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

Getting started: running concurrent websocket connections

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

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.

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:

In my next post, I’ll begin implementing the business logic.

Tags: