echo icon indicating copy to clipboard operation
echo copied to clipboard

Add SSE function in Context.Response

Open lystxn opened this issue 1 year ago • 11 comments

Add SSE function in Context.Response

New feature discussion

I am building a project that needs to provide an SSE (server-sent event) function to the client. Currently I have to build the struct that meets SSE standard manually. Is there any plan to add SSE function to Context.Response so that we do not need to build the response struct that SSE required manually.

lystxn avatar Dec 03 '23 12:12 lystxn

Do you mean something like these Gin examples are:

  • https://github.com/gin-gonic/examples/blob/master/server-sent-event/main.go
  • https://dev.to/mirzaakhena/server-sent-events-sse-server-implementation-with-go-4ck2
  • https://github.com/lmas/gin-sse/blob/master/sse_handler.go

"SSE server" would probably fit better as separate library.

p.s. to be honest this does not seems much different (conceptually) fro the server you would need when dealing with Websockets. I am saying this because I do not have experience with SSE but I have done application that streams real-time updates for graphs over Websockets.

aldas avatar Dec 03 '23 15:12 aldas

hi @aldas ,

Thank you for your response. You are right, Gin has this function already.

The reason why I choose SSE other than web socket is that I am trying to build a chatgpt-like function, which could send the response back to the frontend word by word as a one-way connection. Both Chatgpt and Llama are using SSE to send the response. And my frontend code is currently working with SSE. So if the backend could work in the same, that would be perfect.

So I manually built the response to meet SSE format requirement.

event: userconnect
data: {"username": "bobby", "time": "02:33:48"}
func buildSeverSentEvent(event, context string) string {
	var result string
	if len(event) != 0 {
		result = result + "event: " + event + "\n"
	}
	if len(context) != 0 {
		result = result + "data: " + context + "\n"
	}
	result = result + "\n"
	return result
}

As LLM is becoming more popular and more and more similar web services will adopt the same pattern to send the response, it would be better to add SSE as a formal function to Echo, which could enlarge Echo's scope and help developers to reduce manual work.

lystxn avatar Dec 05 '23 12:12 lystxn

Is SSE supported now?

zouhuigang avatar Dec 23 '23 02:12 zouhuigang

+1 for SSE support

gedw99 avatar Dec 30 '23 12:12 gedw99

+1 for SSE support

ironytr avatar Jan 11 '24 14:01 ironytr

+1 for SSE support

urashidmalik avatar Mar 18 '24 19:03 urashidmalik

+1 for SSE support

iagapie avatar Mar 19 '24 03:03 iagapie

+1 for SSE support

fikurimax avatar Apr 04 '24 19:04 fikurimax

+1 for SSE support

Flipped199 avatar Apr 07 '24 13:04 Flipped199

Hi,

Could people here specify in which situation you would like to use SSE?

For example if we are talking about broadcasting SSE messages to all connected clients - For that there exists https://github.com/r3labs/sse library

See this example:

main.go

package main

import (
	"errors"
	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
	"github.com/r3labs/sse/v2"
	"log"
	"net/http"
	"time"
)

func main() {
	e := echo.New()

	server := sse.New()             // create SSE broadcaster server
	server.AutoReplay = false       // do not replay messages for each new subscriber that connects
	_ = server.CreateStream("ping") // EventSource in "index.html" connecting to stream named "ping"

	go func(s *sse.Server) {
		ticker := time.NewTicker(1 * time.Second)
		defer ticker.Stop()

		for {
			select {
			case <-ticker.C:
				s.Publish("ping", &sse.Event{
					Data: []byte("ping: " + time.Now().Format(time.RFC3339Nano)),
				})
			}
		}
	}(server)

	e.Use(middleware.Logger())
	e.Use(middleware.Recover())
	e.File("/", "./index.html")

	//e.GET("/sse", echo.WrapHandler(server))

	e.GET("/sse", func(c echo.Context) error { // longer variant 
		log.Printf("The client is connected: %v\n", c.RealIP())
		go func() {
			<-c.Request().Context().Done() // Received Browser Disconnection
			log.Printf("The client is disconnected: %v\n", c.RealIP())
			return
		}()

		server.ServeHTTP(c.Response(), c.Request())
		return nil
	})

	if err := e.Start(":8080"); err != nil && !errors.Is(err, http.ErrServerClosed) {
		log.Fatal(err)
	}
}

index.html (in same folder)

<!DOCTYPE html>
<html>
<body>

<h1>Getting server updates</h1>
<div id="result"></div>

<script>
  // Example taken from: https://www.w3schools.com/html/html5_serversentevents.asp
  if (typeof (EventSource) !== "undefined") {
    const source = new EventSource("/sse?stream=ping");
    source.onmessage = function (event) {
      document.getElementById("result").innerHTML += event.data + "<br>";
    };
  } else {
    document.getElementById("result").innerHTML = "Sorry, your browser does not support server-sent events...";
  }
</script>

</body>
</html>

aldas avatar Apr 07 '24 15:04 aldas

If you do not need broadcasting you can just create Event structure and WriteTo method for it

// Event structure is defined here: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format
type Event struct {
	ID      []byte
	Data    []byte
	Event   []byte
	Retry   []byte
	Comment []byte
}

func (ev *Event) WriteTo(w http.ResponseWriter) error {
	// Marshalling part is taken from: https://github.com/r3labs/sse/blob/c6d5381ee3ca63828b321c16baa008fd6c0b4564/http.go#L16
	if len(ev.Data) == 0 && len(ev.Comment) == 0 {
		return nil
	}

	if len(ev.Data) > 0 {
		if _, err := fmt.Fprintf(w, "id: %s\n", ev.ID); err != nil {
			return err
		}

		sd := bytes.Split(ev.Data, []byte("\n"))
		for i := range sd {
			if _, err := fmt.Fprintf(w, "data: %s\n", sd[i]); err != nil {
				return err
			}
		}

		if len(ev.Event) > 0 {
			if _, err := fmt.Fprintf(w, "event: %s\n", ev.Event); err != nil {
				return err
			}
		}

		if len(ev.Retry) > 0 {
			if _, err := fmt.Fprintf(w, "retry: %s\n", ev.Retry); err != nil {
				return err
			}
		}
	}

	if len(ev.Comment) > 0 {
		if _, err := fmt.Fprintf(w, ": %s\n", ev.Comment); err != nil {
			return err
		}
	}

	if _, err := fmt.Fprint(w, "\n"); err != nil {
		return err
	}

	return nil
}

and this is Echo part for SSE handler

func main() {
	e := echo.New()

	e.Use(middleware.Logger())
	e.Use(middleware.Recover())
	e.File("/", "./index.html")

	e.GET("/sse", func(c echo.Context) error {
		log.Printf("SSE client connected, ip: %v", c.RealIP())

		w := c.Response()
		w.Header().Set("Content-Type", "text/event-stream")
		w.Header().Set("Cache-Control", "no-cache")
		w.Header().Set("Connection", "keep-alive")

		ticker := time.NewTicker(1 * time.Second)
		defer ticker.Stop()
		for {
			select {
			case <-c.Request().Context().Done():
				log.Printf("SSE client disconnected, ip: %v", c.RealIP())
				return nil
			case <-ticker.C:
				event := Event{
					Data: []byte("ping: " + time.Now().Format(time.RFC3339Nano)),
				}
				if err := event.WriteTo(w); err != nil {
					return err
				}
				w.Flush()
			}
		}
	})

	if err := e.Start(":8080"); err != nil && !errors.Is(err, http.ErrServerClosed) {
		log.Fatal(err)
	}
}

and index.html for testing the example:

<!DOCTYPE html>
<html>
<body>

<h1>Getting server updates</h1>
<div id="result"></div>

<script>
  // Example taken from: https://www.w3schools.com/html/html5_serversentevents.asp
  if (typeof (EventSource) !== "undefined") {
    const source = new EventSource("/sse");
    source.onmessage = function (event) {
      document.getElementById("result").innerHTML += event.data + "<br>";
    };
  } else {
    document.getElementById("result").innerHTML = "Sorry, your browser does not support server-sent events...";
  }
</script>

</body>
</html>

aldas avatar Apr 07 '24 16:04 aldas

Thank you so much for your demo code.

lystxn avatar May 08 '24 00:05 lystxn