├── .github └── workflows │ └── go.yml ├── .gitignore ├── LICENSE ├── README.md ├── broker.go ├── client_connection.go ├── errors.go ├── event.go ├── example ├── broadcast_message.go ├── broker_connect_custom_heartbeat_interval.go ├── connect_with_sse_feed.go ├── send_to_client_using_reference.go ├── send_to_client_with_multiple_sessions.go └── web_sse_example.html ├── go.mod ├── go.sum └── sse_feed.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: [push] 3 | jobs: 4 | 5 | build: 6 | name: Build 7 | runs-on: ubuntu-latest 8 | steps: 9 | 10 | - name: Set up Go 1.13 11 | uses: actions/setup-go@v1 12 | with: 13 | go-version: 1.13 14 | id: go 15 | 16 | - name: Check out code into the Go module directory 17 | uses: actions/checkout@v2 18 | 19 | - name: Get dependencies 20 | run: | 21 | go get -v -t -d ./... 22 | if [ -f Gopkg.toml ]; then 23 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 24 | dep ensure 25 | fi 26 | 27 | - name: Build 28 | run: go build -v . 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | cmake-build-*/ 2 | *.iws 3 | out/ 4 | .idea_modules/ 5 | atlassian-ide-plugin.xml 6 | com_crashlytics_export_strings.xml 7 | crashlytics.properties 8 | crashlytics-build.properties 9 | fabric.properties 10 | .idea 11 | *.exe 12 | *.exe~ 13 | *.dll 14 | *.so 15 | *.dylib 16 | *.test 17 | *.out -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Thijs van der Burgt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |  2 | 3 | # go-sse 4 | Basic implementation of SSE in golang. 5 | This repository includes a plug and play server-side imlementation and a client-side implementation. 6 | The server-side implementation has been battle-tested while the client-side is usable but in ongoing development. 7 | 8 | Code examples can be found in the `example` folder. 9 | # Install 10 | ```bash 11 | go get github.com/subchord/go-sse@master 12 | ``` 13 | # Server side SSE 14 | 1. Create a new broker and pass `optional` headers that should be sent to the client. 15 | ```Go 16 | sseClientBroker := net.NewBroker(map[string]string{ 17 | "Access-Control-Allow-Origin": "*", 18 | }) 19 | ``` 20 | 2. Set the disconnect callback function if you want to be updated when a client disconnects. 21 | ```Go 22 | sseClientBroker.SetDisconnectCallback(func(clientId string, sessionId string) { 23 | log.Printf("session %v of client %v was disconnected.", sessionId, clientId) 24 | }) 25 | ``` 26 | 3. Return an http event stream in an http.Handler. And keep the request open 27 | ```Go 28 | func (api *API) sseHandler(writer http.ResponseWriter, request *http.Request) { 29 | clientConn, err := api.broker.Connect("unique_client_reference", writer, request) 30 | if err != nil { 31 | log.Println(err) 32 | return 33 | } 34 | <- clientConn.Done() 35 | } 36 | ``` 37 | 4. After a connection is established you can broadcast events or send client specific events either through the clientConnection instance or through the broker. 38 | ```Go 39 | evt := net.StringEvent{ 40 | Id: "self-defined-event-id", 41 | Event: "type of the event eg. foo_update, bar_delete, ..." 42 | Data: "data of the event in string format. eg. plain text, json string, ..." 43 | } 44 | api.broker.Broadcast(evt) // all active clients receive this event 45 | api.broker.Send("unique_client_reference", evt) // only the specified client receives this event 46 | &ClientConnection{}.Send(evt) // Send evt through ClientConnection instance. This instance should always be received by the broker.Connect(...) call. 47 | ``` 48 | 49 | # Client side SSE 50 | The SSE client makes extensive use of go channels. Once a connection with an SSE feed is established you can subscribe to multiple types of events and process them by looping over the subscription's feed (channel). 51 | 52 | 1. Connect with SSE feed. And pass `optional` headers. 53 | ```Go 54 | headers := make(map[string][]string) 55 | feed, err := net.ConnectWithSSEFeed("http://localhost:8080/sse", headers) 56 | if err != nil { 57 | log.Fatal(err) 58 | return 59 | } 60 | ``` 61 | 2. Subscribe to a specific type of event. 62 | ```Go 63 | sub, err := feed.Subscribe("message") // or leave empty to receive all event types 64 | if err != nil { 65 | return 66 | } 67 | ``` 68 | 3. Process the events 69 | ```Go 70 | for { 71 | select { 72 | case evt := <-sub.Feed(): 73 | log.Print(evt) 74 | case err := <-sub.ErrFeed(): 75 | log.Fatal(err) 76 | return 77 | } 78 | } 79 | ``` 80 | 4. When you are done with all subscriptions and the SSE feed. Don't forget to close the subscriptions and the feed in order to prevent unnecessary network traffic and memory leaks. 81 | ```Go 82 | sub.Close() 83 | feed.Close() 84 | ``` 85 | -------------------------------------------------------------------------------- /broker.go: -------------------------------------------------------------------------------- 1 | package net 2 | 3 | import ( 4 | "net/http" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | type Broker struct { 10 | mtx sync.Mutex 11 | 12 | clientSessions map[string]map[string]*ClientConnection 13 | clientMetadata map[string]ClientMetadata 14 | customHeaders map[string]string 15 | 16 | disconnectCallback func(clientId string, sessionId string) 17 | } 18 | 19 | func NewBroker(customHeaders map[string]string) *Broker { 20 | return &Broker{ 21 | clientSessions: make(map[string]map[string]*ClientConnection), 22 | clientMetadata: map[string]ClientMetadata{}, 23 | customHeaders: customHeaders, 24 | } 25 | } 26 | 27 | func (b *Broker) Connect(clientId string, w http.ResponseWriter, r *http.Request) (*ClientConnection, error) { 28 | return b.ConnectWithHeartBeatInterval(clientId, w, r, 15*time.Second) 29 | } 30 | 31 | func (b *Broker) ConnectWithHeartBeatInterval(clientId string, w http.ResponseWriter, r *http.Request, interval time.Duration) (*ClientConnection, error) { 32 | client, err := newClientConnection(clientId, w, r) 33 | if err != nil { 34 | http.Error(w, "Streaming unsupported!", http.StatusInternalServerError) 35 | return nil, NewStreamingUnsupportedError(err.Error()) 36 | } 37 | 38 | b.setHeaders(w) 39 | 40 | b.addClient(clientId, client) 41 | 42 | go client.serve( 43 | interval, 44 | func() { 45 | b.removeClient(clientId, client.sessionId) //onClose callback 46 | }, 47 | ) 48 | 49 | return client, nil 50 | } 51 | 52 | func (b *Broker) setHeaders(w http.ResponseWriter) { 53 | w.Header().Set("Content-Type", "text/event-stream") 54 | w.Header().Set("Cache-Control", "no-cache") 55 | w.Header().Set("Connection", "keep-alive") 56 | w.Header().Set("Transfer-Encoding", "chunked") 57 | 58 | for k, v := range b.customHeaders { 59 | w.Header().Set(k, v) 60 | } 61 | } 62 | 63 | func (b *Broker) IsClientPresent(clientId string) bool { 64 | b.mtx.Lock() 65 | defer b.mtx.Unlock() 66 | _, ok := b.clientSessions[clientId] 67 | return ok 68 | } 69 | 70 | func (b *Broker) SetClientMetadata(clientId string, metadata map[string]interface{}) error { 71 | b.mtx.Lock() 72 | defer b.mtx.Unlock() 73 | 74 | _, ok := b.clientSessions[clientId] 75 | if !ok { 76 | return NewUnknownClientError(clientId) 77 | } 78 | 79 | b.clientMetadata[clientId] = metadata 80 | 81 | return nil 82 | } 83 | 84 | func (b *Broker) GetClientMetadata(clientId string) (map[string]interface{}, error) { 85 | b.mtx.Lock() 86 | defer b.mtx.Unlock() 87 | 88 | _, ok := b.clientSessions[clientId] 89 | md, ok2 := b.clientMetadata[clientId] 90 | if !ok || !ok2 { 91 | return nil, NewUnknownClientError(clientId) 92 | } 93 | 94 | return md, nil 95 | } 96 | 97 | func (b *Broker) addClient(clientId string, connection *ClientConnection) { 98 | b.mtx.Lock() 99 | defer b.mtx.Unlock() 100 | 101 | _, ok := b.clientSessions[clientId] 102 | if !ok { 103 | b.clientSessions[clientId] = make(map[string]*ClientConnection) 104 | } 105 | 106 | b.clientSessions[clientId][connection.sessionId] = connection 107 | } 108 | 109 | func (b *Broker) removeClient(clientId string, sessionId string) { 110 | b.mtx.Lock() 111 | defer b.mtx.Unlock() 112 | 113 | sessions, ok := b.clientSessions[clientId] 114 | if !ok { 115 | return 116 | } 117 | 118 | delete(sessions, sessionId) 119 | 120 | if len(b.clientSessions[clientId]) == 0 { 121 | delete(b.clientSessions, clientId) 122 | delete(b.clientMetadata, clientId) 123 | } 124 | 125 | if b.disconnectCallback != nil { 126 | go b.disconnectCallback(clientId, sessionId) 127 | } 128 | } 129 | 130 | func (b *Broker) Broadcast(event Event) { 131 | b.mtx.Lock() 132 | defer b.mtx.Unlock() 133 | for _, sessions := range b.clientSessions { 134 | for _, c := range sessions { 135 | c.Send(event) 136 | } 137 | } 138 | } 139 | 140 | func (b *Broker) Send(clientId string, event Event) error { 141 | b.mtx.Lock() 142 | defer b.mtx.Unlock() 143 | sessions, ok := b.clientSessions[clientId] 144 | if !ok { 145 | return NewUnknownClientError(clientId) 146 | } 147 | for _, c := range sessions { 148 | c.Send(event) 149 | } 150 | return nil 151 | } 152 | 153 | func (b *Broker) SetDisconnectCallback(cb func(clientId string, sessionId string)) { 154 | b.disconnectCallback = cb 155 | } 156 | 157 | func (b *Broker) Close() error { 158 | b.mtx.Lock() 159 | defer b.mtx.Unlock() 160 | 161 | for _, v := range b.clientSessions { 162 | // Mark all client sessions as done 163 | for _, session := range v { 164 | session.doneChan <- true 165 | } 166 | } 167 | 168 | // Clear client sessions 169 | b.clientSessions = map[string]map[string]*ClientConnection{} 170 | 171 | return nil 172 | } 173 | -------------------------------------------------------------------------------- /client_connection.go: -------------------------------------------------------------------------------- 1 | package net 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "github.com/sirupsen/logrus" 6 | "net/http" 7 | "time" 8 | ) 9 | 10 | type ClientMetadata map[string]interface{} 11 | 12 | type ClientConnection struct { 13 | id string 14 | sessionId string 15 | 16 | responseWriter http.ResponseWriter 17 | request *http.Request 18 | flusher http.Flusher 19 | 20 | msg chan []byte 21 | doneChan chan interface{} 22 | } 23 | 24 | // Users should not create instances of client. This should be handled by the SSE broker. 25 | func newClientConnection(id string, w http.ResponseWriter, r *http.Request) (*ClientConnection, error) { 26 | flusher, ok := w.(http.Flusher) 27 | if !ok { 28 | http.Error(w, "Streaming unsupported!", http.StatusInternalServerError) 29 | return nil, NewStreamingUnsupportedError("ResponseWriter(wrapper) does not support http.Flusher") 30 | } 31 | 32 | return &ClientConnection{ 33 | id: id, 34 | sessionId: uuid.New().String(), 35 | responseWriter: w, 36 | request: r, 37 | flusher: flusher, 38 | msg: make(chan []byte), 39 | doneChan: make(chan interface{}, 1), 40 | }, nil 41 | } 42 | 43 | func (c *ClientConnection) Id() string { 44 | return c.id 45 | } 46 | 47 | func (c *ClientConnection) SessionId() string { 48 | return c.sessionId 49 | } 50 | 51 | func (c *ClientConnection) Send(event Event) { 52 | bytes := event.Prepare() 53 | c.msg <- bytes 54 | } 55 | 56 | func (c *ClientConnection) serve(interval time.Duration, onClose func()) { 57 | heartBeat := time.NewTicker(interval) 58 | 59 | writeLoop: 60 | for { 61 | select { 62 | case <-c.request.Context().Done(): 63 | break writeLoop 64 | case <-heartBeat.C: 65 | go c.Send(HeartbeatEvent{}) 66 | case msg, open := <-c.msg: 67 | if !open { 68 | break writeLoop 69 | } 70 | _, err := c.responseWriter.Write(msg) 71 | if err != nil { 72 | logrus.Errorf("unable to write to client %v: %v", c.id, err.Error()) 73 | break writeLoop 74 | } 75 | c.flusher.Flush() 76 | } 77 | } 78 | 79 | heartBeat.Stop() 80 | c.doneChan <- true 81 | onClose() 82 | } 83 | 84 | func (c *ClientConnection) Done() <-chan interface{} { 85 | return c.doneChan 86 | } 87 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package net 2 | 3 | import "fmt" 4 | 5 | type SSEError struct { 6 | Message string 7 | } 8 | 9 | type StreamingUnsupportedError struct { 10 | SSEError 11 | } 12 | 13 | func NewStreamingUnsupportedError(msg string) *StreamingUnsupportedError { 14 | return &StreamingUnsupportedError{SSEError: SSEError{Message: msg}} 15 | } 16 | 17 | func (s StreamingUnsupportedError) Error() string { 18 | return s.Message 19 | } 20 | 21 | type UnknownClientError struct { 22 | SSEError 23 | } 24 | 25 | func NewUnknownClientError(clientId string) *UnknownClientError { 26 | return &UnknownClientError{SSEError{Message: fmt.Sprintf("clientId: %v", clientId)}} 27 | } 28 | 29 | func (u UnknownClientError) Error() string { 30 | return u.Message 31 | } 32 | -------------------------------------------------------------------------------- /event.go: -------------------------------------------------------------------------------- 1 | package net 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/sirupsen/logrus" 8 | "strings" 9 | ) 10 | 11 | type Event interface { 12 | Prepare() []byte 13 | GetId() string 14 | GetEvent() string 15 | GetData() string 16 | } 17 | 18 | type StringEvent struct { 19 | Id string 20 | Event string 21 | Data string 22 | } 23 | 24 | func (e StringEvent) GetId() string { 25 | return e.Id 26 | } 27 | 28 | func (e StringEvent) GetEvent() string { 29 | return e.Event 30 | } 31 | 32 | func (e StringEvent) GetData() string { 33 | return e.Data 34 | } 35 | 36 | func (e StringEvent) Prepare() []byte { 37 | var data bytes.Buffer 38 | 39 | if len(e.Id) > 0 { 40 | data.WriteString(fmt.Sprintf("id: %s\n", strings.Replace(e.Id, "\n", "", -1))) 41 | } 42 | 43 | data.WriteString(fmt.Sprintf("event: %s\n", strings.Replace(e.Event, "\n", "", -1))) 44 | 45 | // data field should not be empty 46 | lines := strings.Split(e.Data, "\n") 47 | for _, line := range lines { 48 | data.WriteString(fmt.Sprintf("data: %s\n", line)) 49 | } 50 | 51 | data.WriteString("\n") 52 | return data.Bytes() 53 | } 54 | 55 | type HeartbeatEvent struct{} 56 | 57 | func (h HeartbeatEvent) GetId() string { 58 | return "" 59 | } 60 | 61 | func (h HeartbeatEvent) GetEvent() string { 62 | return "" 63 | } 64 | 65 | func (h HeartbeatEvent) GetData() string { 66 | return "" 67 | } 68 | 69 | func (h HeartbeatEvent) Prepare() []byte { 70 | var data bytes.Buffer 71 | data.WriteString(fmt.Sprint(": heartbeat\n")) 72 | data.WriteString("\n") 73 | return data.Bytes() 74 | } 75 | 76 | type JsonEvent struct { 77 | Id string 78 | Event string 79 | Data interface{} 80 | } 81 | 82 | func (j *JsonEvent) GetId() string { 83 | return j.Id 84 | } 85 | 86 | func (j *JsonEvent) GetEvent() string { 87 | return j.Event 88 | } 89 | 90 | func (j *JsonEvent) GetData() string { 91 | marshal, err := json.Marshal(j.Data) 92 | if err != nil { 93 | logrus.Errorf("error marshaling JSONEvent: %v", err) 94 | return "" 95 | } 96 | return string(marshal) 97 | } 98 | 99 | func (j *JsonEvent) Prepare() []byte { 100 | var data bytes.Buffer 101 | 102 | if len(j.Id) > 0 { 103 | data.WriteString(fmt.Sprintf("id: %s\n", strings.Replace(j.Id, "\n", "", -1))) 104 | } 105 | 106 | data.WriteString(fmt.Sprintf("event: %s\n", strings.Replace(j.Event, "\n", "", -1))) 107 | 108 | marshal, err := json.Marshal(j.Data) 109 | if err != nil { 110 | logrus.Errorf("error marshaling JSONEvent: %v", err) 111 | return []byte{} 112 | } 113 | 114 | data.WriteString(fmt.Sprintf("data: %s\n", string(marshal))) 115 | data.WriteString("\n") 116 | 117 | return data.Bytes() 118 | } 119 | -------------------------------------------------------------------------------- /example/broadcast_message.go: -------------------------------------------------------------------------------- 1 | // +build example1 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "github.com/subchord/go-sse" 8 | "log" 9 | "net/http" 10 | "strconv" 11 | "time" 12 | ) 13 | 14 | type API struct { 15 | broker *net.Broker 16 | } 17 | 18 | func main() { 19 | sseClientBroker := net.NewBroker(map[string]string{ 20 | "Access-Control-Allow-Origin": "*", 21 | }) 22 | 23 | api := &API{broker: sseClientBroker} 24 | 25 | http.HandleFunc("/sse", api.sseHandler) 26 | 27 | // Broadcast message to all clients every 5 seconds 28 | go func() { 29 | count := 0 30 | tick := time.Tick(5 * time.Second) 31 | for { 32 | select { 33 | case <-tick: 34 | count++ 35 | api.broker.Broadcast(net.StringEvent{ 36 | Id: fmt.Sprintf("event-id-%v", count), 37 | Event: "message", 38 | Data: strconv.Itoa(count), 39 | }) 40 | } 41 | } 42 | }() 43 | 44 | log.Fatal(http.ListenAndServe(":8080", http.DefaultServeMux)) 45 | } 46 | 47 | func (api *API) sseHandler(writer http.ResponseWriter, request *http.Request) { 48 | client, err := api.broker.Connect(fmt.Sprintf("%v", time.Now().Unix()), writer, request) 49 | if err != nil { 50 | log.Println(err) 51 | return 52 | } 53 | <-client.Done() 54 | log.Printf("connection with client %v closed", client.Id()) 55 | } 56 | -------------------------------------------------------------------------------- /example/broker_connect_custom_heartbeat_interval.go: -------------------------------------------------------------------------------- 1 | // +build connect_custom_interval 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "github.com/subchord/go-sse" 8 | "log" 9 | "net/http" 10 | "strconv" 11 | "time" 12 | ) 13 | 14 | type API struct { 15 | broker *net.Broker 16 | } 17 | 18 | func main() { 19 | sseClientBroker := net.NewBroker(map[string]string{ 20 | "Access-Control-Allow-Origin": "*", 21 | }) 22 | 23 | api := &API{broker: sseClientBroker} 24 | 25 | http.HandleFunc("/sse", api.sseHandler) 26 | 27 | // Broadcast message to all clients every 5 seconds 28 | go func() { 29 | count := 0 30 | tick := time.Tick(5 * time.Second) 31 | for { 32 | select { 33 | case <-tick: 34 | count++ 35 | api.broker.Broadcast(net.StringEvent{ 36 | Id: fmt.Sprintf("event-id-%v", count), 37 | Event: "message", 38 | Data: strconv.Itoa(count), 39 | }) 40 | } 41 | } 42 | }() 43 | 44 | log.Fatal(http.ListenAndServe(":8080", http.DefaultServeMux)) 45 | } 46 | 47 | func (api *API) sseHandler(writer http.ResponseWriter, request *http.Request) { 48 | // set the heartbeat interval to 1 minute 49 | client, err := api.broker.ConnectWithHeartBeatInterval(fmt.Sprintf("%v", time.Now().Unix()), writer, request, 1*time.Minute) 50 | if err != nil { 51 | log.Println(err) 52 | return 53 | } 54 | <-client.Done() 55 | log.Printf("connection with client %v closed", client.Id()) 56 | } 57 | -------------------------------------------------------------------------------- /example/connect_with_sse_feed.go: -------------------------------------------------------------------------------- 1 | // +build connect_with_sse 2 | 3 | package main 4 | 5 | import ( 6 | net "github.com/subchord/go-sse" 7 | "log" 8 | ) 9 | 10 | func main() { 11 | 12 | feed, err := net.ConnectWithSSEFeed("http://localhost:8080/sse", nil) 13 | if err != nil { 14 | log.Fatal(err) 15 | return 16 | } 17 | 18 | sub, err := feed.Subscribe("message") 19 | if err != nil { 20 | return 21 | } 22 | 23 | for { 24 | select { 25 | case evt := <-sub.Feed(): 26 | log.Print(evt) 27 | case err := <-sub.ErrFeed(): 28 | log.Fatal(err) 29 | return 30 | } 31 | } 32 | 33 | sub.Close() 34 | feed.Close() 35 | } 36 | -------------------------------------------------------------------------------- /example/send_to_client_using_reference.go: -------------------------------------------------------------------------------- 1 | // +build example2 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | net "github.com/subchord/go-sse" 8 | "log" 9 | "math/rand" 10 | "net/http" 11 | "time" 12 | ) 13 | 14 | type API struct { 15 | broker *net.Broker 16 | } 17 | 18 | func main() { 19 | rand.Seed(time.Now().Unix()) 20 | 21 | sseClientBroker := net.NewBroker(map[string]string{ 22 | "Access-Control-Allow-Origin": "*", 23 | }) 24 | 25 | sseClientBroker.SetDisconnectCallback(func(clientId string, sessionId string) { 26 | log.Printf("session %v of client %v was disconnected.", sessionId, clientId) 27 | }) 28 | 29 | api := &API{broker: sseClientBroker} 30 | 31 | http.HandleFunc("/sse", api.sseHandler) 32 | 33 | log.Fatal(http.ListenAndServe(":8080", http.DefaultServeMux)) 34 | } 35 | 36 | func (api *API) sseHandler(writer http.ResponseWriter, request *http.Request) { 37 | client, err := api.broker.Connect(fmt.Sprintf("%v", rand.Int63()), writer, request) 38 | if err != nil { 39 | log.Println(err) 40 | return 41 | } 42 | 43 | stop := make(chan interface{}, 1) 44 | 45 | go func() { 46 | ticker := time.Tick(1 * time.Second) 47 | count := 0 48 | for { 49 | select { 50 | case <-stop: 51 | return 52 | case <-ticker: 53 | client.Send(net.StringEvent{ 54 | Id: fmt.Sprintf("%v", count), 55 | Event: "message", 56 | Data: fmt.Sprintf("%v", count), 57 | }) 58 | count++ 59 | } 60 | } 61 | }() 62 | 63 | <-client.Done() 64 | stop <- true 65 | } 66 | -------------------------------------------------------------------------------- /example/send_to_client_with_multiple_sessions.go: -------------------------------------------------------------------------------- 1 | // +build example3 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | net "github.com/subchord/go-sse" 8 | "log" 9 | "math/rand" 10 | "net/http" 11 | "time" 12 | ) 13 | 14 | type API struct { 15 | broker *net.Broker 16 | } 17 | 18 | func main() { 19 | rand.Seed(time.Now().Unix()) 20 | 21 | sseClientBroker := net.NewBroker(map[string]string{ 22 | "Access-Control-Allow-Origin": "*", 23 | }) 24 | 25 | sseClientBroker.SetDisconnectCallback(func(clientId string, sessionId string) { 26 | log.Printf("session %v of client %v was disconnected.", sessionId, clientId) 27 | }) 28 | 29 | api := &API{broker: sseClientBroker} 30 | 31 | http.HandleFunc("/sse", api.sseHandler) 32 | 33 | go func() { 34 | ticker := time.Tick(1 * time.Second) 35 | count := 0 36 | for { 37 | select { 38 | case <-ticker: 39 | if err := api.broker.Send("always same client", net.StringEvent{ 40 | Id: fmt.Sprintf("%v", count), 41 | Event: "message", 42 | Data: fmt.Sprintf("%v", count), 43 | }); err != nil { 44 | log.Print(err) 45 | } 46 | count++ 47 | } 48 | } 49 | }() 50 | 51 | log.Fatal(http.ListenAndServe(":8080", http.DefaultServeMux)) 52 | } 53 | 54 | func (api *API) sseHandler(writer http.ResponseWriter, request *http.Request) { 55 | c, err := api.broker.Connect("always same client", writer, request) 56 | if err != nil { 57 | log.Println(err) 58 | return 59 | } 60 | 61 | <-c.Done() 62 | } 63 | -------------------------------------------------------------------------------- /example/web_sse_example.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |