labstack / echo

High performance, minimalist Go web framework
https://echo.labstack.com
MIT License
29.62k stars 2.22k forks source link

Add SSE function in Context.Response #2553

Closed lystxn closed 5 months ago

lystxn commented 10 months ago

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 commented 10 months ago

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 commented 10 months ago

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 commented 9 months ago

Is SSE supported now?

gedw99 commented 9 months ago

+1 for SSE support

ironytr commented 8 months ago

+1 for SSE support

urashidmalik commented 6 months ago

+1 for SSE support

iagapie commented 6 months ago

+1 for SSE support

fikurimax commented 6 months ago

+1 for SSE support

Flipped199 commented 6 months ago

+1 for SSE support

aldas commented 6 months ago

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 commented 6 months ago

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 commented 5 months ago

Thank you so much for your demo code.