DEV Community

loading...

Server Sent Events (SSE) Server implementation with Go

Mirza Akhena
Updated on ・4 min read

What is SSE?

SSE is the way we send messages from server to client.
By default, server cannot send messages to client. Because server is not recognize the client. But client can accessed easily the server because server has public IP.

When to use SSE?

We use SSE whenever we have requirement that need one-way communication only from server to client. If we need two-way communication from server to client, then it's better to use websocket instead.

How this SSE works?

Since server does not recognize the client, the first handshake must started by client. When server gets this handshake request from client, server does not immediately reply. Right before server has to return the response, server will trap this request with loop forever. From here, server can take the advantage to send as many messages to the client as desired. Basically under this request, client waits forever for the server's response but it will never happen.

How to close connection?

The server can decide whether to close the connection to the client by leaving the loop. On the other hand, the client can also close the connection to the server easily by canceling the handshake request that has not been completed (because of loop traps). This will trigger the server to close the connection to the client too.

Why not just using websocket?

We can also use websocket for this purpose but SSE is much simpler. Back to our requirements, if we need two-way communication we would prefer to use websocket.

How to implement SSE with go?

We will use the built in golang http libary. Actually you can also use other libraries such as gin or echo to implement the same thing

func (w http.ResponseWriter, r *http.Request)

We will start by define the header for this SSE support

w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("Access-Control-Allow-Origin", "*")

Instantiate a channel variable called messageChan. A message to client will be delivered through this channel. It will defined somewhere that we can access it. Let say now we only simply deliver a string message.

messageChan = make(chan string)

For the "trap request" part we will use the loop forever. Under this loop. The process will blocked by messageChan. The messageChan channel is keep waiting and listening for the message to be ready. If the message is available, then it will print to the http writer. We also need to flush the message so client can see the message. In case of we receive close connection from client, Context().Done() function will give a "close" signal and we can just exit from the loop (actually we are exiting from the function not just a loop). Here we are maintaining the connection.

flusher := w.(http.Flusher)

for  {
  select {

    case message := <- messageChan:
      fmt.Fprintf(w, "data: %s\n\n", message)
      flusher.Flush()

    case <-r.Context().Done():
      return
  }
}

Right before we are exiting the function, we need close the channel to avoid memory leak. We can do it by define it under defer. This defer statement should be right after we are instantiate the messageChan variable. Don't forget to put it as nil to make sure it can not be used anymore.

defer func() {
  close(messageChan)
  messageChan = nil
}()

How to start sending the message to client?

We can just simply put the message under channel. Make sure the messageChan is instantiate before by call the handshake first, before we can use it.

messageChan <- "Hello"

That's it!

Let see the complete code for this implementation

package main

import (
  "fmt"
  "log"
  "net/http"
)

var messageChan chan string

func handleSSE() http.HandlerFunc {
  return func(w http.ResponseWriter, r *http.Request) {

    log.Printf("Get handshake from client")

    // prepare the header
    w.Header().Set("Content-Type", "text/event-stream")
    w.Header().Set("Cache-Control", "no-cache")
    w.Header().Set("Connection", "keep-alive")
    w.Header().Set("Access-Control-Allow-Origin", "*")

    // instantiate the channel
    messageChan = make(chan string)

    // close the channel after exit the function
    defer func() {
      close(messageChan)
      messageChan = nil
      log.Printf("client connection is closed")
    }()

    // prepare the flusher
    flusher, _ := w.(http.Flusher)

    // trap the request under loop forever
    for {

      select {

      // message will received here and printed
      case message := <-messageChan:
        fmt.Fprintf(w, "%s\n", message)
        flusher.Flush()

      // connection is closed then defer will be executed
      case <-r.Context().Done():
        return

      }
    }

  }
}

func sendMessage(message string) http.HandlerFunc {
  return func(w http.ResponseWriter, r *http.Request) {

    if messageChan != nil {
      log.Printf("print message to client")

      // send the message through the available channel     
      messageChan <- message
    }

  }
}

func main() {

  http.HandleFunc("/handshake", handleSSE())

  http.HandleFunc("/sendmessage", sendMessage("hello client"))

  log.Fatal("HTTP server error: ", http.ListenAndServe("localhost:3000", nil))
}

You can try run the code by open a console to run this code. Lets name it a FIRST console

$ go run main .go

Open two other console as a client. This second console will do the handshake. It will waiting for the message. Let's name it as SECOND console. Watch the log in the FIRST console after you call it.

$ curl http://localhost:3000/handshake

The third console will trigger sending the message to client. Let's name it THIRD console. Watch the SECOND console after you run the THIRD console.

$ curl http://localhost:3000/sendmessage

Close the SECOND console by run ctrl+C then watch the FIRST console.

FIRST console will print all the event

(1a) $ go run main.go 
(2b) Get handshake from client
(3b) print message to client
(4b) client connection is closed

SECOND console act as client. It is print the message from server

(2a) $ curl http://localhost:3000/handshake
(3c) hello client
(4a) ^C
(  ) $ 

THIRD console only for trigger the server to sends the message

(3a) $ curl http://localhost:3000/sendmessage
(  ) $ 

Follow the 1a, 2a, 2b, 3a, 3b, 3c, 4a, 4b for the sequence in this scenario.

You can also use two browser tab as a client for console replacement.

This is very basic code for the SSE implementation. Of course it will only serve one client. For more advance implementation wait for the next post

Discussion (2)

Collapse
sjoerd82 profile image
Sjoerd82

Is that second post still coming? :-)

Collapse
jimmont profile image
Jim Montgomery

What resource usage have you seen with your implementations? I'm curious to understand the cost of running this specific type of service.