Zack Scholl

zack.scholl@gmail.com

Websockets

 / #golang #tutorial 

A simple pattern to get started using websockets with Go.

Every website I’ve built recently has made use of websockets. The frontend is always Javascript and the backend is always Go. I’ve now gotten used to a programming pattern that I’ve been implementing over and over for doing websockets. There are a lot of ways to implement websockets in the frontend and backend, but here’s what I like to use.

Frontend code for websockets

The frontend just consists of a few lines of Javascript. No need for externally libraries. Basically these functions are pulled straight from the Web APIs for Websockets from MDN:

 1var socket;
 2const socketMessageListener = (e) => {
 3    console.log(e.data);
 4};
 5const socketOpenListener = (e) => {
 6    console.log('Connected');
 7    socket.send(JSON.stringify({ message: "hello, server" }))
 8};
 9const socketErrorListener = (e) => {
10    console.error(e);
11}
12const socketCloseListener = (e) => {
13    if (socket) {
14        console.log('Disconnected.');
15    }
16    var url = window.origin.replace("http", "ws") + '/ws';
17    socket = new WebSocket(url);
18    socket.onopen = socketOpenListener;
19    socket.onmessage = socketMessageListener;
20    socket.onclose = socketCloseListener;
21    socket.onerror = socketErrorListener;
22};
23window.addEventListener('load', (event) => {
24    socketCloseListener();
25});

The program is started by calling the closing listener. Since websockets naturally uses Keep-Alive this code will automatically re-join broken connections so you don’t have to code that yourself! Modern browser’s are great eh.

Backend code for websockets

There are basically two libraries for doing this in Go. Both of them are pretty much the same - zero dependencies, fast, they bind JSON, and they wrap the http std library.

There’s the old and reliable gorilla/websocket.

 1var wsupgrader = websocket.Upgrader{
 2	ReadBufferSize:  1024,
 3	WriteBufferSize: 1024,
 4	CheckOrigin: func(r *http.Request) bool {
 5		return true
 6	},
 7}
 8
 9func handleWebsocket(w http.ResponseWriter, r *http.Request) (err error) {
10	c, errUpgrade := wsupgrader.Upgrade(w, r, nil)
11	if errUpgrade != nil {
12		return errUpgrade
13	}
14	defer c.Close()
15
16	for {
17		var p interface{}
18		err := c.ReadJSON(&p)
19		if err != nil {
20			log.Debug("read:", err)
21			break
22		}
23		log.Debugf("recv: %v", p)
24		c.WriteJSON(struct{ Message string }{
25			"hello, browser",
26		})
27
28	}
29	return
30}

And there’s the more recent nhooyr/websocket, which I’ve forked into schollz/websocket to remove all the test dependencies:

 1import (
 2	"github.com/schollz/websocket"
 3	"github.com/schollz/websocket/wsjson"
 4)
 5
 6func handleWebsocket(w http.ResponseWriter, r *http.Request) (err error) {
 7	c, err := websocket.Accept(w, r, nil)
 8	if err != nil {
 9		return
10	}
11	defer c.Close(websocket.StatusInternalError, "internal error")
12
13	ctx, cancel := context.WithTimeout(r.Context(), time.Hour*120000)
14	defer cancel()
15
16	for {
17		var v interface{}
18		err = wsjson.Read(ctx, c, &v)
19		if err != nil {
20			break
21		}
22		log.Debugf("received: %v", v)
23		err = wsjson.Write(ctx, c, struct{ Message string }{
24			"hello, browser",
25		})
26		if err != nil {
27			break
28		}
29	}
30	if websocket.CloseStatus(err) == websocket.StatusGoingAway {
31		err = nil
32	}
33	c.Close(websocket.StatusNormalClosure, "")
34	return
35}

The latter example will eventually handle HTTP/2 and it seems the gorilla/websocket never will (unless they start updating it!).

Try it!

There is code for this on my Github.