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

SSE example #338

Merged
merged 1 commit into from
Apr 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions cookbook/sse/broadcast/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<body>

<h1>Getting server-sent 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=time");
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>
55 changes: 55 additions & 0 deletions cookbook/sse/broadcast/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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("time") // EventSource in "index.html" connecting to stream named "time"

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

for {
select {
case <-ticker.C:
s.Publish("time", &sse.Event{
Data: []byte("time: " + 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 with disconnect logic
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)
}
}
21 changes: 21 additions & 0 deletions cookbook/sse/simple/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<body>

<h1>Getting server-sent 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>
49 changes: 49 additions & 0 deletions cookbook/sse/simple/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package main

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

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("time: " + time.Now().Format(time.RFC3339Nano)),
}
if err := event.MarshalTo(w); err != nil {
return err
}
w.Flush()
}
}
})

if err := e.Start(":8080"); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}
75 changes: 75 additions & 0 deletions cookbook/sse/simple/serversentevent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package main

import (
"bytes"
"fmt"
"io"
)

// Event represents Server-Sent Event.
// SSE explanation: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format
type Event struct {
// ID is used to set the EventSource object's last event ID value.
ID []byte
// Data field is for the message. When the EventSource receives multiple consecutive lines
// that begin with data:, it concatenates them, inserting a newline character between each one.
// Trailing newlines are removed.
Data []byte
// Event is a string identifying the type of event described. If this is specified, an event
// will be dispatched on the browser to the listener for the specified event name; the website
// source code should use addEventListener() to listen for named events. The onmessage handler
// is called if no event name is specified for a message.
Event []byte
// Retry is the reconnection time. If the connection to the server is lost, the browser will
// wait for the specified time before attempting to reconnect. This must be an integer, specifying
// the reconnection time in milliseconds. If a non-integer value is specified, the field is ignored.
Retry []byte
// Comment line can be used to prevent connections from timing out; a server can send a comment
// periodically to keep the connection alive.
Comment []byte
}

// MarshalTo marshals Event to given Writer
func (ev *Event) MarshalTo(w io.Writer) 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
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
github.com/labstack/echo/v4 v4.12.0
github.com/labstack/gommon v0.4.2
github.com/lestrrat-go/jwx v1.2.29
github.com/r3labs/sse/v2 v2.10.0
golang.org/x/crypto v0.22.0
golang.org/x/net v0.24.0
google.golang.org/appengine v1.6.8
Expand Down Expand Up @@ -39,6 +40,7 @@ require (
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/r3labs/sse/v2 v2.10.0 h1:hFEkLLFY4LDifoHdiCN/LlGBAdVJYsANaLqNYa1l/v0=
github.com/r3labs/sse/v2 v2.10.0/go.mod h1:Igau6Whc+F17QUgML1fYe1VPZzTV6EMCnYktEmkNJ7I=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
Expand All @@ -73,6 +75,7 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
Expand All @@ -93,6 +96,7 @@ golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
Expand Down Expand Up @@ -144,6 +148,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y=
gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
Expand Down
44 changes: 44 additions & 0 deletions website/docs/cookbook/sse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
description: SSE recipe
---

# Server-Sent-Events (SSE)

[Server-sent events](
https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format) can be
used in different ways. This example here is per connection - per handler SSE. If your requirements need more complex
broadcasting logic see https://github.com/r3labs/sse library.

## Using SSE

### Server

```go reference
https: //github.com/labstack/echox/blob/master/cookbook/sse/simple/server.go
```

### Event structure and Marshal method

```html reference
https://github.com/labstack/echox/blob/master/cookbook/sse/simple/serversideevent.go
```

### HTML serving SSE

```html reference
https://github.com/labstack/echox/blob/master/cookbook/sse/simple/index.html
```

## Using 3rd party library [r3labs/sse](https://github.com/r3labs/sse) to broadcast events

### Server

```go reference
https: //github.com/labstack/echox/blob/master/cookbook/sse/broadcast/server.go
```

### HTML serving SSE

```html reference
https://github.com/labstack/echox/blob/master/cookbook/sse/broadcast/index.html
```
Loading