[Golang] Try WebSocket

masanori_msl

Masui Masanori

Posted on May 2, 2022

[Golang] Try WebSocket

Intro

For understanding Pion examples, I try WebSocket in Golang first.
This time, I will use gorilla / websocket on the server-side.

Environments

  • Go ver.1.18.1 windows/amd64
  • Node.js ver.18.0.0
  • TypeScript ver.4.6.3

Base projects

index.html

<!DOCTYPE html>
<html>
    <head>
        <title>Go Sample</title>
        <meta charset="utf-8">
        <link href="css/site.css" rel="stylesheet" />
    </head>
    <body>
        <div id="sample_message"></div>
        <textarea id="input_message"></textarea>
        <button onclick="Page.connect('{{.}}')">Connect</button>
        <button onclick="Page.send()">Send</button>
        <button onclick="Page.close()">Close</button>
        <div id="received_text_area"></div>
        <script src="js/main.page.js"></script>
    </body>
</html>
Enter fullscreen mode Exit fullscreen mode

main.page.ts

import { WebsocketMessage } from "./websocket.type";

let ws: WebSocket|null = null;
export function connect(url: string): void {
    ws = new WebSocket(url);
    ws.onopen = () => sendMessage({
        messageType: "text",
        data: "connected",
    });
    ws.onmessage = data => {
        const message = <WebsocketMessage>JSON.parse(data.data);
        switch(message.messageType) {
            case "text":
                if(typeof message.data === "string") {
                    addReceivedMessage(message.data);
                } else {
                    console.error(message.data);                  
                }
                break;
            default:
                console.log(data);
                break;
        }
    };
}
export function send() {
    const messageArea = document.getElementById("input_message") as HTMLTextAreaElement;
    sendMessage({
        messageType: "text",
        data: messageArea.value,
    });
}
export function close() {
    if(ws == null) {
        return;
    }
    ws.close();
    ws = null;
}
function addReceivedMessage(message: string) {
    const receivedMessageArea = document.getElementById("received_text_area") as HTMLElement;
    const child = document.createElement("div");
    child.textContent = message;
    receivedMessageArea.appendChild(child);
}
function sendMessage(message: WebsocketMessage) {
    if (ws == null) {
        return;
    }
    ws.send(JSON.stringify(message));
}
Enter fullscreen mode Exit fullscreen mode

websocket.type.ts

export type WebsocketMessage = {
    messageType: "text"
    data: string|Blob|ArrayBuffer,
};
Enter fullscreen mode Exit fullscreen mode

main.go

package main

import (
    "html/template"
    "log"
    "net/http"
    "path/filepath"
    "sync"
)

type templateHandler struct {
    once     sync.Once
    filename string
    templ    *template.Template
}

func (t *templateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    t.once.Do(func() {
        t.templ = template.Must(template.ParseFiles(filepath.Join("templates", t.filename)))
    })
    t.templ.Execute(w, "Hello world")
}

func main() {
    http.Handle("/css/", http.FileServer(http.Dir("templates")))
    http.Handle("/js/", http.FileServer(http.Dir("templates")))
    http.HandleFunc("/websocket", websocketHandler)
    http.Handle("/", &templateHandler{filename: "index.html"})
    log.Fatal(http.ListenAndServe("localhost:8080", nil))
}
Enter fullscreen mode Exit fullscreen mode

Receiving and sending my own messages through WebSocket

room.go

package main

import (
    "encoding/json"
    "log"
    "net/http"
    "strconv"

    "github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{
    ReadBufferSize:  1024,
    WriteBufferSize: 1024,
}

type websocketMessage struct {
    MessageType string `json:"messageType"`
    Data        string `json:"data"`
}

func websocketHandler(w http.ResponseWriter, r *http.Request) {
    conn, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Println(err)
        return
    }
    // Close the connection when the for-loop operation is finished.
    defer conn.Close()

    message := &websocketMessage{}
    for {
        // the first message is "connected"
        messageType, raw, err := conn.ReadMessage()
        if err != nil {
            log.Println(err)
            return
        } else if err := json.Unmarshal(raw, &message); err != nil {
            log.Println(err)
            return
        }
        conn.WriteJSON(message)
    }
}
Enter fullscreen mode Exit fullscreen mode

Upgrade

To establishing WebSocket connection, I have to change the protocols of established connection from HTTP/1.1 to WebSocket.

WebSocket API of client-side adds WebSocket-specific headers.

Request Headers (Google Chrome)

Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9,ja-JP;q=0.8,ja;q=0.7,zh-CN;q=0.6,zh;q=0.5
Cache-Control: no-cache
Connection: Upgrade
Host: localhost:8080
Origin: http://localhost:8080
Pragma: no-cache
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Sec-WebSocket-Key: loDo1/V2L+3izvedjmEt9A==
Sec-WebSocket-Version: 13
Upgrade: websocket
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36
Enter fullscreen mode Exit fullscreen mode

"websocket.Upgrader.Upgrade" inspects the request headers, creates a new connection(net.Conn), and makes it be able to take over the connection.

room.go

...
    var upgrader = websocket.Upgrader{
    ReadBufferSize:  1024,
    WriteBufferSize: 1024,
}
...
func websocketHandler(w http.ResponseWriter, r *http.Request) {
    conn, err := upgrader.Upgrade(w, r, nil)
...
}
Enter fullscreen mode Exit fullscreen mode

After connecting, I can get Response headers like below.

Response Headers

Connection: Upgrade
Sec-WebSocket-Accept: /CQqmm5VOeDDGblpvMXlK56DWFs=
Upgrade: websocket
Enter fullscreen mode Exit fullscreen mode

Manage connections

To conversate with other users, I have to hold the connections like this.

Because there are no common specifications to hold the connections, every samples implements it in different way.

For example, sfu-ws of Pion example holds the connections with PeerConnection.
When I access them, I lock them first.

On the other hand, Chat Example of gorilla/websocket creates one "Hub" instance and put the connections into it.

In this time, I hold the connections like the Pion example.

room.go

package main

import (
    "encoding/json"
    "log"
    "net/http"
    "sync"

    "github.com/gorilla/websocket"
)

var (
    upgrader = websocket.Upgrader{
        ReadBufferSize:  1024,
        WriteBufferSize: 1024,
    }
    listLock    sync.RWMutex
    connections []connectionState
)

type websocketMessage struct {
    MessageType string `json:"messageType"`
    Data        string `json:"data"`
}
type connectionState struct {
    websocket *threadSafeWriter
}
type threadSafeWriter struct {
    *websocket.Conn
    sync.Mutex
}

func websocketHandler(w http.ResponseWriter, r *http.Request) {
    unsafeConn, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Println(err)
        return
    }
    conn := &threadSafeWriter{unsafeConn, sync.Mutex{}}
    // Close the connection when the for-loop operation is finished.
    defer conn.Close()
    listLock.Lock()
    connections = append(connections, connectionState{websocket: conn})
    listLock.Unlock()

    message := &websocketMessage{}
    for {
        _, raw, err := conn.ReadMessage()
        if err != nil {
            log.Println(err)
            return
        } else if err := json.Unmarshal(raw, &message); err != nil {
            log.Println(err)
            return
        }
        for _, c := range connections {
            c.websocket.WriteJSON(message)
        }
    }
}
func (t *threadSafeWriter) WriteJSON(v interface{}) error {
    t.Lock()
    defer t.Unlock()

    return t.Conn.WriteJSON(v)
}
Enter fullscreen mode Exit fullscreen mode

Resources

💖 💪 🙅 🚩
masanori_msl
Masui Masanori

Posted on May 2, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related