Websockets sind ansich nur eine kleine Erweiterung zu HTTP: Sie erlauben echte bidirektionale Kommunikation zwischen Webserver und Browser. Go ist mit seiner ausgeprägten Parallelität natürlich prädestiniert, aber der Weg ist etwas steinig.
TIMTOWTDI
Das Gorilla-Package für Websockets wollte bei mir partout nicht funktionieren:
websocket: response does not implement http.Hijacker
An dieser Fehlermeldung bin ich gescheitert. Letztendlich habe ich zu x/net/websocket gewechselt. Dieses Paket behauptet von sich selbst, Gorilla unterlegen zu sein, bietet aber einige nette Features die bei Gorilla fehlen - und es funktionierte am Ende sogar!
Vorbereitung
x/net/websocket wird wie üblich via go get heruntergeladen:
go get golang.org/x/net/websocket
Für die Client-Seite reicht ein kleines JavaScript und die JavaScript-Konsole eines Browsers:
var websocket; var websocket_queue; function ws_connect() { // Connect websocket = new WebSocket("ws://localhost:3456/ws"); websocket.onopen = function() { // Send waiting messages on connect while (websocket_queue.length > 0) { websocket.send(websocket_queue.shift()) } }; websocket.onclose = function() { // Close and ignore errors eval("websocket.close();"); websocket = undefined; }; websocket.onerror = function(e) { // Show error console.log(e); }; websocket.onmessage = function(e) { // Show incoming message console.log(e); try { // Parse message var data = JSON.parse(e.data); if (data["action"] == "ping") { // Reply to ping ws_send({"action": "pong"}); } } catch(err) { console.log('WS Error parsing message: ' + err); } }; } function ws_send(data) { var json_data = JSON.stringify(data); if (!websocket && websocket.readyState == 1) { // Connect if currently not connected ws_connect(); // Push to queue of delayed items websocket_queue.push(json_data); return; } websocket.send(json_data); }
Testen lässt sich das Script relativ einfach in dem in der JavaScript-Konsole des Browsers ein ws_send(...) aufgerufen wird:
ws_send({"action":"ping"});
Am Ende muss der Server dann mit "pong" antworten.
Es handelt sich hierbei nicht um das ping/pong nach Websocket - Standard selbst, sondern um simple JSON-Nachrichten.
Implementierung
In meinem speziellen Anwendungsfall sollen nur authorisierte Benutzer die Websocket - Verbindung aufbauen dürfen. Dies übernimmt ein vorgeschalteter Handler, der mich aber gleichzeitig davon abhält, die im Package vorgesehene Funktion websocket.Handler zu benutzen:
http.Handle("/echo", websocket.Handler(EchoServer))
Der Workaround ist allerdings auch nicht weiter schwierig. Mein ohnehin schon bestehender Handler wird einfach um zwei Zeilen ergänzt:
// Websocket-Handler vorbereiten
// wsStart ist die aufzurufende func nach Etablierung des Websocket
var wsHandler = websocket.Handler(wsStart)
func ws(w http.ResponseWriter, r *http.Request) {
[...] // Authorisierung des Users
wsHandler.ServeHTTP(w, r)
}
Dabei ist unbedingt zu beachten, dass es sich bei dem http.ResponseWriter um das Original aus net/http handelt und nicht um einen Ersatz z.B. für das Logging von HTTP-Anfragen! In Verbindung mit httpReloaded müsste der Websocket Aufruf wie folgt aussehen:
wsHandler.ServeHTTP(httpReloaded.OrigWriter(r), r)
Damit die der komplizierte Teil schon erledigt: Die Websocket-Verbindung steht und kann benutzt werden.
func wsStart(ws *websocket.Conn) {
defer ws.Close() // Force close on func exit
// Create sender channel
outCh := make(chan map[string]interface{}, 10)
defer close(outCh)
go func() {
for content := range outCh {
if err := websocket.JSON.Send(ws, content); err != nil {
log.Println(err)
}
}
}()
// Decode and handle incoming messages
for {
var raw interface{}
if err := websocket.JSON.Receive(ws, &raw); err != nil {
if err == io.EOF {
break
}
log.Printf("Error receiving and decoding JSON: %v", err)
} else {
log.Printf("In: %v", raw)
data := raw.(map[string]interface{})
if data["action"] == nil {
log.Printf("No function called in incoming message: %v", data)
} else {
fn, found := actionMap[data["action"].(string)]
if !found {
log.Printf("Action %s not defined for message: %v", data["action"], data)
} else {
go fn(data, outCh)
}
}
}
}
}
Wird jetzt eine ping JSON Nachricht von Browser geschickt, empfängt websocket.JSON.Receive diese und konvertiert sie gleich von JSON in eine Go-Map. Nach ein paar Fehlerprüfungen wird die gewünschte Aktion in einer actionMap nachgeschlagen und als Goroutine aufgerufen. Damit kann gleich die nächste Nachricht verarbeitet werden, während die erste noch ausgeführt wird.
Für den einfachen ping/pong Fall ist die actionMap überschaubar und könnte auch durch einen if oder switch ersetzt werden, aber ich erwarte bei diesem Anwendungsfall viele verschiedene Actions und habe dafür die Map gewählt:
var actionMap = map[string]func(data map[string]interface{}, outCh chan map[string]interface{}){
"ping": func(_ map[string]interface{}, outCh chan map[string]interface{}) {
outCh <- map[string]interface{}{"action": "pong"}
},
}
An Stelle einer anonymen func könnte auch einfach der Name der entsprechenden Funktion angegeben werden.
Die ping Action ist sehr einfach: Sie bekommt die Daten übergeben und ignoriert diese (zu sehen an dem "_" an Stelle des ersten Parameternamens). Als zweiten Parameter bekommt sie den Versand - Channel und schickt diesem ein pong.
Die Nutzung eines Channel als Zwischenspeicher für verschickte Nachrichten hat zwei Vorteile:
- Die ausführende Goroutine wartet nicht auf den tatsächlichen Versand (zumindest solange der Stau kleiner als 10 Nachrichten lang ist).
- Es ist sichergestellt, dass immer nur eine Nachricht gleichzeitig auf die Reise geht.
Ob der letzte Punkt bei x/net/websocket kritisch bzw. notwendig ist, erwähnt die Dokumentation nicht. Die Gorilla-Websocket-Dokumentation stellt dieses aber als zentrale Anforderung heraus.
Noch keine Kommentare. Schreib was dazu