Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SSE function in Context.Response #2553

Closed
lystxn opened this issue Dec 3, 2023 · 12 comments
Closed

Add SSE function in Context.Response #2553

lystxn opened this issue Dec 3, 2023 · 12 comments

Comments

@lystxn
Copy link

lystxn commented Dec 3, 2023

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.

@aldas
Copy link
Contributor

aldas commented Dec 3, 2023

Do you mean something like these Gin examples are:

"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.

@lystxn
Copy link
Author

lystxn commented Dec 5, 2023

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.

@zouhuigang
Copy link

Is SSE supported now?

@gedw99
Copy link

gedw99 commented Dec 30, 2023

+1 for SSE support

5 similar comments
@ironytr
Copy link

ironytr commented Jan 11, 2024

+1 for SSE support

@urashidmalik
Copy link

+1 for SSE support

@iagapie
Copy link

iagapie commented Mar 19, 2024

+1 for SSE support

@fikurimax
Copy link

+1 for SSE support

@Flipped199
Copy link

+1 for SSE support

@aldas
Copy link
Contributor

aldas commented Apr 7, 2024

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
Copy link
Contributor

aldas commented Apr 7, 2024

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>

@lystxn
Copy link
Author

lystxn commented May 8, 2024

Thank you so much for your demo code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

9 participants