[Go] Try Server-Sent Events
Tags: go
Published: 2022-05-09 17:50:50

## Intro
I've tried WebSockets before, but some networks are blocking the WebSocket protocol, so I need another way instead.
According to the SignalR documents, it use Server-Sent Events(SSE), Ajax Long Polling, etc.

* [Introduction to SignalR - Microsoft Docs](https://docs.microsoft.com/en-us/aspnet/signalr/overview/getting-started/introduction-to-signalr?msclkid=b2d03b78cd4911ec9d1d230205641a6c)
* [Cutting Edge - Long Polling and SignalR - Microsoft Docs](https://docs.microsoft.com/en-us/archive/msdn-magazine/2012/april/cutting-edge-long-polling-and-signalr?msclkid=b2cf4d3fcd4911ec95f1b41bec1d57ae)

This time, I will try SSE.

I will keep using the project.

* [[Golang] Try WebSocket](https://dev.to/masanori_msl/golang-try-websocket-1ghm)
* [[Golang] Try handling web requests](https://dev.to/masanori_msl/golang-try-handling-web-requests-1pij)

## Using SSE with multiple clients
Most features of SSE are defined on the client side.

According to [the PHP sample](https://github.com/mdn/dom-examples/tree/master/server-sent-events), the most important thing to use SSE on the server side is set "Content-Type: text/event-stream" to the response header.

After setting the response header, the client keeps connecting until closing the connection.

Because SSE only can push data from the server side, so I send data from the client side by POST requests.

To broadcast data to multiple clients, I wrote the program this time with reference to the sample of gorilla / websocket.

* [websocket/examples/chat - gorilla/websocket - GitHub](https://github.com/gorilla/websocket/tree/master/examples/chat)

## Sample project
#### sse.html
```html
<!DOCTYPE html>
<html>
    <head>
        <title>Server-Sent Events Sample</title>
        <meta charset="utf-8">
        <link href="css/site.css" rel="stylesheet" />
    </head>
    <body>
        <input type="text" id="user_name" placeholder="User Name">
        <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/sse.page.js"></script>
    </body>
</html>
```

#### sse.page.ts
```ts
let es: EventSource;
let userName = ""
export function connect(): void {
    const userNameInput = document.getElementById("user_name") as HTMLInputElement;
    userName = userNameInput.value;
    const receivedArea = document.getElementById("received_text_area") as HTMLElement;
    es = new EventSource(`http://localhost:8080/sse?user=${userName}`);
    es.onopen = (ev) => {
        console.log(ev);
    };
    es.onmessage = ev => {
        const newText = document.createElement("div")
        newText.textContent = ev.data;
        receivedArea.appendChild(newText);
    };
    es.onerror = ev => {
        console.error(ev);        
    };
}
export function send() {
    const messageInput = document.getElementById("input_message") as HTMLTextAreaElement;
    fetch("http://localhost:8080/sse/message", 
        {
            method: "POST",
            body: JSON.stringify({ message: messageInput.value, user: userName }),
        })
        .then(res => res.json())
        .then(json => console.log(json))
        .catch(err => console.error(err));
}
export function close() {    
    es.close();
}
```
#### main.go
```go
...
func main() {
	hub := *newSseHub()
	go hub.run()
...
	http.HandleFunc("/sse/message", func(w http.ResponseWriter, r *http.Request) {
		sendSseMessage(w, r, &hub)
	})
	http.HandleFunc("/sse", func(w http.ResponseWriter, r *http.Request) {
		registerSseClient(w, r, &hub)
	})
	http.Handle("/pages/sse", &templateHandler{filename: "sse.html", serverUrl: "http://localhost:8080/sse"})
...
	log.Fatal(http.ListenAndServe("localhost:8080", nil))
}
```

#### sseHub.go
```go
package main

type SseHub struct {
	// registered clients
	clients    map[*SseClient]bool
	broadcast  chan string
	register   chan *SseClient
	unregister chan *SseClient
}

func newSseHub() *SseHub {
	return &SseHub{
		clients:    make(map[*SseClient]bool),
		broadcast:  make(chan string),
		register:   make(chan *SseClient),
		unregister: make(chan *SseClient),
	}
}
func (h *SseHub) run() {
	for {
		select {
		case client := <-h.register:
			h.clients[client] = true
		case client := <-h.unregister:
			if _, ok := h.clients[client]; ok {
				close(client.messageChan)
				delete(h.clients, client)
			}
		case message := <-h.broadcast:
			for client := range h.clients {
				select {
				case client.messageChan <- message:
				default:
					close(client.messageChan)
					delete(h.clients, client)
				}
			}
		}
	}
}
```

#### sseClient.go
```go
package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
)

type SseClient struct {
	messageChan chan string
	userName    string
}
type SseMessage struct {
	Message string `json:"message"`
	User    string `json:"user"`
}

func registerSseClient(w http.ResponseWriter, r *http.Request, hub *SseHub) {
	userName, err := getParam(r, "user")
	if err != nil {
		log.Println(err.Error())
		fmt.Fprint(w, "The parameters have no names")
		return
	}
	// For pushing data to clients, I call "flusher.Flush()"
	flusher, _ := w.(http.Flusher)

	w.Header().Set("Content-Type", "text/event-stream")
	w.Header().Set("Cache-Control", "no-cache")
	w.Header().Set("Connection", "keep-alive")
	newClient := SseClient{messageChan: make(chan string), userName: userName}
	hub.register <- &newClient

	defer func() {
		hub.unregister <- &newClient
	}()
	for {
		select {
		case message := <-newClient.messageChan:
			// push received messages to clients
			fmt.Fprintf(w, "data: %s", message)
			flusher.Flush()
		case <-r.Context().Done():
			// when "es.close()" is called, this loop operation will be ended.
			return
		}
	}
}
func sendSseMessage(w http.ResponseWriter, r *http.Request, hub *SseHub) {
	returnValue := &sampleResult{}
	w.Header().Set("Content-Type", "application/json")
	body, readBodyError := ioutil.ReadAll(r.Body)
	if readBodyError != nil {
		log.Println(readBodyError.Error())
		returnValue.Succeeded = false
		returnValue.ErrorMessage = "Failed reading values from body"
		failedReadingData, _ := json.Marshal(returnValue)
		w.Write(failedReadingData)
		return
	}
	message := &SseMessage{}
	convertError := json.Unmarshal(body, &message)
	if convertError != nil {
		log.Println(convertError.Error())
		returnValue.Succeeded = false
		returnValue.ErrorMessage = "Failed converting to WebSocketMessage"
		failedConvertingData, _ := json.Marshal(returnValue)
		w.Write(failedConvertingData)
		return
	}
	w.WriteHeader(200)
	hub.broadcast <- message.Message

	returnValue.Message = "OK"
	returnValue.Succeeded = true
	data, _ := json.Marshal(returnValue)
	w.Write(data)
}
```

## Resources
* [9.2 Server-sent events - HTML Standard](https://html.spec.whatwg.org/multipage/server-sent-events.html)
* [Using server-sent events - Web APIs|MDN](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events)
* [Server-sent events - Web APIs|MDN](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events)
* [Writing a Server Sent Events server in Go - thoughtbot](https://thoughtbot.com/blog/writing-a-server-sent-events-server-in-go)
* [ハイパフォーマンス ブラウザネットワーキング](https://www.oreilly.com/library/view/high-performance-browser/9781449344757/)