[Golang] Try WebSocket
Tags: go, typescript
Published: 2022-05-02 15:30:08

## Intro
For understanding [Pion examples](https://github.com/pion/example-webrtc-applications/tree/master/sfu-ws), I try WebSocket in Golang first.
This time, I will use [gorilla / websocket](https://github.com/gorilla/websocket) on the server-side.

* [[Windows] Try Golang 1](https://dev.to/masanori_msl/windows-try-golang-1-138b)

### Environments

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

## Base projects

#### index.html

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

#### main.page.ts
```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));
}
```

#### websocket.type.ts
```ts
export type WebsocketMessage = {
    messageType: "text"
    data: string|Blob|ArrayBuffer,
};
```

#### main.go
```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))
}
```

## Receiving and sending my own messages through WebSocket
#### room.go
```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)
	}
}
```

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

* [Upgrade - HTTP | MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Upgrade)
* [Protocol upgrade mechanism - HTTP | MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Protocol_upgrade_mechanism)

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

"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
```go
...
    var upgrader = websocket.Upgrader{
	ReadBufferSize:  1024,
	WriteBufferSize: 1024,
}
...
func websocketHandler(w http.ResponseWriter, r *http.Request) {
	conn, err := upgrader.Upgrade(w, r, nil)
...
}
```

* [server.go - gorilla / websocket - GitHub](https://github.com/gorilla/websocket/blob/master/server.go)

After connecting, I can get Response headers like below.

#### Response Headers
```
Connection: Upgrade
Sec-WebSocket-Accept: /CQqmm5VOeDDGblpvMXlK56DWFs=
Upgrade: websocket
```

## Manage connections
To conversate with other users, I have to hold the connections like [this](https://dev.to/masanori_msl/aspnet-coretry-websocket-4fhj).

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

For example, [sfu-ws of Pion example](https://github.com/pion/example-webrtc-applications/tree/master/sfu-ws) holds the connections with PeerConnection.
When I access them, I lock them first.

On the other hand, [Chat Example of gorilla/websocket](https://github.com/gorilla/websocket/tree/master/examples/chat) creates one "Hub" instance and put the connections into it.

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

#### room.go
```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)
}
```

## Resources
* [RFC6455: The WebSocket Protocol](https://www.rfc-editor.org/rfc/rfc6455.html)
* [Writing WebSocket client applications - Web APIs | MDN](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications)
* [gorilla / websocket - GitHub](https://github.com/gorilla/websocket)
* [WebSockets Standard](https://websockets.spec.whatwg.org/)
* [Upgrade - HTTP | MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Upgrade)
* [Protocol upgrade mechanism - HTTP | MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Protocol_upgrade_mechanism)
* [ハイパフォーマンス ブラウザネットワーキング(https://www.oreilly.com/library/view/high-performance-browser/9781449344757/)](https://www.oreilly.co.jp/books/9784873116761/)
* [Go言語によるWebアプリケーション開発(Go Programming Blueprints)](https://www.oreilly.co.jp/books/9784873117522/?msclkid=04ddcc18c2ac11ecb7675fec5a3cb48d)