[Golang] Try WebSocket
Masui Masanori
Posted on May 2, 2022
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>
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));
}
websocket.type.ts
export type WebsocketMessage = {
messageType: "text"
data: string|Blob|ArrayBuffer,
};
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))
}
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)
}
}
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
"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)
...
}
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.
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)
}
Resources
- RFC6455: The WebSocket Protocol
- Writing WebSocket client applications - Web APIs | MDN
- gorilla / websocket - GitHub
- WebSockets Standard
- Upgrade - HTTP | MDN
- Protocol upgrade mechanism - HTTP | MDN
- ハイパフォーマンス ブラウザネットワーキング(https://www.oreilly.com/library/view/high-performance-browser/9781449344757/)
- Go言語によるWebアプリケーション開発(Go Programming Blueprints)
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
November 14, 2024