├── .travis.yml ├── docker-compose.yml ├── .gitignore ├── doc.go ├── options.go ├── examples ├── static │ └── index.html ├── simple.go └── complex.go ├── client.go ├── message_test.go ├── options_test.go ├── message.go ├── LICENSE ├── channel.go ├── README.md └── sse.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.6 4 | - tip 5 | install: go get -t -v 6 | script: go test -v 7 | notifications: 8 | email: false 9 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | sse: 2 | image: golang:1.6 3 | working_dir: /go/src/github.com/alexandrevicenzi/go-sse 4 | command: bash -c "cd examples && go run simple.go" 5 | ports: 6 | - "3000:3000" 7 | volumes: 8 | - .:/go/src/github.com/alexandrevicenzi/go-sse 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package sse implements Server-Sent Events that supports multiple channels. 2 | // 3 | // Server-sent events is a method of continuously sending data from a server to the browser, rather than repeatedly requesting it. 4 | // 5 | // Examples 6 | // 7 | // Basic usage of sse package. 8 | // 9 | // s := sse.NewServer(nil) 10 | // defer s.Shutdown() 11 | // 12 | // http.Handle("/events/", s) 13 | // 14 | package sse 15 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package sse 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | type Options struct { 8 | // RetryInterval change EventSource default retry interval (milliseconds). 9 | RetryInterval int 10 | // Headers allow to set custom headers (useful for CORS support). 11 | Headers map[string]string 12 | // ChannelNameFunc allow to create custom channel names. 13 | // Default channel name is the request path. 14 | ChannelNameFunc func (*http.Request) string 15 | } 16 | 17 | func (opt *Options) HasHeaders() bool { 18 | return opt.Headers != nil && len(opt.Headers) > 0 19 | } 20 | -------------------------------------------------------------------------------- /examples/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SSE Examples 5 | 6 | 7 | Messages 8 |
9 |
10 | 11 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package sse 2 | 3 | type Client struct { 4 | lastEventId, 5 | channel string 6 | send chan *Message 7 | } 8 | 9 | func NewClient(lastEventId, channel string) *Client { 10 | return &Client{ 11 | lastEventId, 12 | channel, 13 | make(chan *Message), 14 | } 15 | } 16 | 17 | // SendMessage sends a message to client. 18 | func (c *Client) SendMessage(message *Message) { 19 | c.lastEventId = message.id 20 | c.send <- message 21 | } 22 | 23 | // Channel returns the channel where this client is subscribe to. 24 | func (c *Client) Channel() string { 25 | return c.channel 26 | } 27 | 28 | func (c *Client) LastEventId() string { 29 | return c.lastEventId 30 | } 31 | -------------------------------------------------------------------------------- /message_test.go: -------------------------------------------------------------------------------- 1 | package sse 2 | 3 | import "testing" 4 | 5 | func TestEmptyMessage(t *testing.T) { 6 | msg := Message{} 7 | 8 | if msg.String() != "\n" { 9 | t.Fatal("Messagem not empty.") 10 | } 11 | } 12 | 13 | func TestDataMessage(t *testing.T) { 14 | msg := Message{data:"test"} 15 | 16 | if msg.String() != "data: test\n\n" { 17 | t.Fatal("Messagem not empty.") 18 | } 19 | } 20 | 21 | func TestMessage(t *testing.T) { 22 | msg := Message{ 23 | "123", 24 | "test", 25 | "myevent", 26 | 10 * 1000, 27 | } 28 | 29 | if msg.String() != "id: 123\nretry: 10000\nevent: myevent\ndata: test\n\n" { 30 | t.Fatal("Messagem not empty.") 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /options_test.go: -------------------------------------------------------------------------------- 1 | package sse 2 | 3 | import "testing" 4 | 5 | func TestHasHeadersEmpty(t *testing.T) { 6 | opt := Options{} 7 | 8 | if opt.HasHeaders() == true { 9 | t.Fatal("There's headers.") 10 | } 11 | } 12 | 13 | func TestHasHeadersNotEmpty(t *testing.T) { 14 | opt := Options{} 15 | 16 | opt = Options{ 17 | Headers: map[string]string { 18 | "Access-Control-Allow-Origin": "*", 19 | "Access-Control-Allow-Methods": "GET, OPTIONS", 20 | "Access-Control-Allow-Headers": "Keep-Alive,X-Requested-With,Cache-Control,Content-Type,Last-Event-ID", 21 | }, 22 | } 23 | 24 | if opt.HasHeaders() == false { 25 | t.Fatal("There's no headers.") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/simple.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/alexandrevicenzi/go-sse" 10 | ) 11 | 12 | func main() { 13 | s := sse.NewServer(nil) 14 | defer s.Shutdown() 15 | 16 | http.Handle("/", http.FileServer(http.Dir("./static"))) 17 | http.Handle("/events/", s) 18 | 19 | go func () { 20 | for { 21 | s.SendMessage("/events/channel-1", sse.SimpleMessage(time.Now().String())) 22 | time.Sleep(5 * time.Second) 23 | } 24 | }() 25 | 26 | go func () { 27 | i := 0 28 | for { 29 | i++ 30 | s.SendMessage("/events/channel-2", sse.SimpleMessage(strconv.Itoa(i))) 31 | time.Sleep(5 * time.Second) 32 | } 33 | }() 34 | 35 | log.Println("Listening at :3000") 36 | http.ListenAndServe(":3000", nil) 37 | } 38 | -------------------------------------------------------------------------------- /message.go: -------------------------------------------------------------------------------- 1 | package sse 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | ) 7 | 8 | type Message struct { 9 | id, 10 | data, 11 | event string 12 | retry int 13 | } 14 | 15 | func SimpleMessage(data string) *Message { 16 | return NewMessage("", data, "") 17 | } 18 | 19 | func NewMessage(id, data, event string) *Message { 20 | return &Message{ 21 | id, 22 | data, 23 | event, 24 | 0, 25 | } 26 | } 27 | 28 | func (m *Message) String() string { 29 | var buffer bytes.Buffer 30 | 31 | if len(m.id) > 0 { 32 | buffer.WriteString(fmt.Sprintf("id: %s\n", m.id)) 33 | } 34 | 35 | if m.retry > 0 { 36 | buffer.WriteString(fmt.Sprintf("retry: %d\n", m.retry)) 37 | } 38 | 39 | if len(m.event) > 0 { 40 | buffer.WriteString(fmt.Sprintf("event: %s\n", m.event)) 41 | } 42 | 43 | if len(m.data) > 0 { 44 | buffer.WriteString(fmt.Sprintf("data: %s\n", m.data)) 45 | } 46 | 47 | buffer.WriteString("\n") 48 | 49 | return buffer.String() 50 | } 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Alexandre Vicenzi 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 | -------------------------------------------------------------------------------- /channel.go: -------------------------------------------------------------------------------- 1 | package sse 2 | 3 | type Channel struct { 4 | lastEventId, 5 | name string 6 | clients map[*Client]bool 7 | } 8 | 9 | func NewChannel(name string) *Channel { 10 | return &Channel{ 11 | "", 12 | name, 13 | make(map[*Client]bool), 14 | } 15 | } 16 | 17 | // SendMessage broadcast a message to all clients in a channel. 18 | func (c *Channel) SendMessage(message *Message) { 19 | c.lastEventId = message.id 20 | 21 | for c, open := range c.clients { 22 | if open { 23 | c.send <- message 24 | } 25 | } 26 | } 27 | 28 | func (c *Channel) Close() { 29 | // Kick all clients of this channel. 30 | for client, _ := range c.clients { 31 | c.removeClient(client) 32 | } 33 | } 34 | 35 | func (c *Channel) ClientCount() int { 36 | return len(c.clients) 37 | } 38 | 39 | func (c *Channel) LastEventId() string { 40 | return c.lastEventId 41 | } 42 | 43 | func (c *Channel) addClient(client *Client) { 44 | c.clients[client] = true 45 | } 46 | 47 | func (c *Channel) removeClient(client *Client) { 48 | c.clients[client] = false 49 | close(client.send) 50 | delete(c.clients, client) 51 | } 52 | -------------------------------------------------------------------------------- /examples/complex.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/alexandrevicenzi/go-sse" 10 | ) 11 | 12 | func main() { 13 | s := sse.NewServer(&sse.Options{ 14 | // Increase default retry interval to 10s. 15 | RetryInterval: 10 * 1000, 16 | // CORS headers 17 | Headers: map[string]string { 18 | "Access-Control-Allow-Origin": "*", 19 | "Access-Control-Allow-Methods": "GET, OPTIONS", 20 | "Access-Control-Allow-Headers": "Keep-Alive,X-Requested-With,Cache-Control,Content-Type,Last-Event-ID", 21 | }, 22 | // Custom channel name generator 23 | ChannelNameFunc: func (request *http.Request) string { 24 | return request.URL.Path 25 | }, 26 | }) 27 | 28 | s.Debug = true 29 | 30 | defer s.Shutdown() 31 | 32 | http.Handle("/", http.FileServer(http.Dir("./static"))) 33 | http.Handle("/events/", s) 34 | 35 | go func () { 36 | for { 37 | s.SendMessage("/events/channel-1", sse.SimpleMessage(time.Now().String())) 38 | time.Sleep(5 * time.Second) 39 | } 40 | }() 41 | 42 | go func () { 43 | i := 0 44 | for { 45 | i++ 46 | s.SendMessage("/events/channel-2", sse.SimpleMessage(strconv.Itoa(i))) 47 | time.Sleep(5 * time.Second) 48 | } 49 | }() 50 | 51 | log.Println("Listening at :3000") 52 | http.ListenAndServe(":3000", nil) 53 | } 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-sse [![Build Status](https://travis-ci.org/alexandrevicenzi/go-sse.svg?branch=master)](https://travis-ci.org/alexandrevicenzi/go-sse) [![GoDoc](https://godoc.org/github.com/alexandrevicenzi/go-sse?status.svg)](http://godoc.org/github.com/alexandrevicenzi/go-sse) 2 | 3 | Server-Sent Events for Go 4 | 5 | ## About 6 | 7 | [Server-sent events](http://www.html5rocks.com/en/tutorials/eventsource/basics/) is a method of continuously sending data from a server to the browser, rather than repeatedly requesting it, replacing the "long polling way". 8 | 9 | It's [supported](http://caniuse.com/#feat=eventsource) by all major browsers and for IE/Edge you can use a [polyfill](https://github.com/Yaffle/EventSource). 10 | 11 | `go-sse` is a small library to create a Server-Sent Events server in Go. 12 | 13 | ## Features 14 | 15 | - Multiple channels (isolated) 16 | - Broadcast message to all channels 17 | - Custom headers (useful for CORS) 18 | - `Last-Event-ID` support (resend lost messages) 19 | - [Follow SSE specification](https://html.spec.whatwg.org/multipage/comms.html#server-sent-events) 20 | 21 | ## Getting Started 22 | 23 | Simple Go example that handle 2 channels and send messages to all clients connected in each channel. 24 | 25 | ```go 26 | package main 27 | 28 | import ( 29 | "log" 30 | "net/http" 31 | "strconv" 32 | "time" 33 | 34 | "github.com/alexandrevicenzi/go-sse" 35 | ) 36 | 37 | func main() { 38 | // Create the server. 39 | s := sse.NewServer(nil) 40 | defer s.Shutdown() 41 | 42 | // Register with /events endpoint. 43 | http.Handle("/events/", s) 44 | 45 | // Dispatch messages to channel-1. 46 | go func () { 47 | for { 48 | s.SendMessage("/events/channel-1", sse.SimpleMessage(time.Now().String())) 49 | time.Sleep(5 * time.Second) 50 | } 51 | }() 52 | 53 | // Dispatch messages to channel-2 54 | go func () { 55 | i := 0 56 | for { 57 | i++ 58 | s.SendMessage("/events/channel-2", sse.SimpleMessage(strconv.Itoa(i))) 59 | time.Sleep(5 * time.Second) 60 | } 61 | }() 62 | 63 | http.ListenAndServe(":3000", nil) 64 | } 65 | ``` 66 | 67 | Connecting to our server from JavaScript: 68 | 69 | ```js 70 | e1 = new EventSource('/events/channel-1'); 71 | e1.onmessage = function(event) { 72 | // do something... 73 | }; 74 | 75 | e2 = new EventSource('/events/channel-2'); 76 | e2.onmessage = function(event) { 77 | // do something... 78 | }; 79 | ``` 80 | -------------------------------------------------------------------------------- /sse.go: -------------------------------------------------------------------------------- 1 | package sse 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | ) 8 | 9 | type Server struct { 10 | options *Options 11 | channels map[string]*Channel 12 | addClient chan *Client 13 | removeClient chan *Client 14 | shutdown chan bool 15 | closeChannel chan string 16 | Debug bool 17 | } 18 | 19 | // NewServer creates a new SSE server. 20 | func NewServer(options *Options) *Server { 21 | if options == nil { 22 | options = &Options{} 23 | } 24 | 25 | s := &Server{ 26 | options, 27 | make(map[string]*Channel), 28 | make(chan *Client), 29 | make(chan *Client), 30 | make(chan bool), 31 | make(chan string), 32 | false, 33 | } 34 | 35 | go s.dispatch() 36 | 37 | return s 38 | } 39 | 40 | func (s *Server) ServeHTTP(response http.ResponseWriter, request *http.Request) { 41 | flusher, ok := response.(http.Flusher) 42 | 43 | if !ok { 44 | http.Error(response, "Streaming unsupported.", http.StatusInternalServerError) 45 | return 46 | } 47 | 48 | h := response.Header() 49 | 50 | if s.options.HasHeaders() { 51 | for k, v := range s.options.Headers { 52 | h.Set(k, v) 53 | } 54 | } 55 | 56 | if request.Method == "GET" { 57 | h.Set("Content-Type", "text/event-stream") 58 | h.Set("Cache-Control", "no-cache") 59 | h.Set("Connection", "keep-alive") 60 | 61 | var channelName string 62 | 63 | if s.options.ChannelNameFunc == nil { 64 | channelName = request.URL.Path 65 | } else { 66 | channelName = s.options.ChannelNameFunc(request) 67 | } 68 | 69 | lastEventId := request.Header.Get("Last-Event-ID") 70 | c := NewClient(lastEventId, channelName) 71 | s.addClient <- c 72 | closeNotify := response.(http.CloseNotifier).CloseNotify() 73 | 74 | go func() { 75 | <-closeNotify 76 | s.removeClient <- c 77 | }() 78 | 79 | response.WriteHeader(http.StatusOK) 80 | flusher.Flush() 81 | 82 | for msg := range c.send { 83 | msg.retry = s.options.RetryInterval 84 | fmt.Fprintf(response, msg.String()) 85 | flusher.Flush() 86 | } 87 | } else if request.Method != "OPTIONS" { 88 | response.WriteHeader(http.StatusMethodNotAllowed) 89 | } 90 | } 91 | 92 | // SendMessage broadcast a message to all clients in a channel. 93 | // If channel is an empty string, it will broadcast the message to all channels. 94 | func (s *Server) SendMessage(channel string, message *Message) { 95 | if len(channel) == 0 { 96 | if s.Debug { 97 | log.Printf("go-sse: broadcasting message to all channels.") 98 | } 99 | 100 | for _, ch := range s.channels { 101 | ch.SendMessage(message) 102 | } 103 | } else if ch, ok := s.channels[channel]; ok { 104 | if s.Debug { 105 | log.Printf("go-sse: message sent to channel '%s'.", ch.name) 106 | } 107 | ch.SendMessage(message) 108 | } else if s.Debug { 109 | log.Printf("go-sse: message not sent because channel '%s' has no clients.", channel) 110 | } 111 | } 112 | 113 | // Restart closes all channels and clients and allow new connections. 114 | func (s *Server) Restart() { 115 | if s.Debug { 116 | log.Printf("go-sse: restarting server.") 117 | } 118 | 119 | s.close() 120 | } 121 | 122 | // Shutdown performs a graceful server shutdown. 123 | func (s *Server) Shutdown() { 124 | s.shutdown <- true 125 | } 126 | 127 | func (s *Server) ClientCount() int { 128 | i := 0 129 | 130 | for _, channel := range s.channels { 131 | i += channel.ClientCount() 132 | } 133 | 134 | return i 135 | } 136 | 137 | func (s *Server) HasChannel(name string) bool { 138 | _, ok := s.channels[name] 139 | return ok 140 | } 141 | 142 | func (s *Server) GetChannel(name string) (*Channel, bool) { 143 | ch, ok := s.channels[name] 144 | return ch, ok 145 | } 146 | 147 | func (s *Server) Channels() []string { 148 | channels := []string{} 149 | 150 | for name, _ := range s.channels { 151 | channels = append(channels, name) 152 | } 153 | 154 | return channels 155 | } 156 | 157 | func (s *Server) CloseChannel(name string) { 158 | s.closeChannel <- name 159 | } 160 | 161 | func (s *Server) close() { 162 | for name, _ := range s.channels { 163 | s.closeChannel <- name 164 | } 165 | } 166 | 167 | func (s *Server) dispatch() { 168 | if s.Debug { 169 | log.Printf("go-sse: server started.") 170 | } 171 | 172 | for { 173 | select { 174 | 175 | // New client connected. 176 | case c := <- s.addClient: 177 | ch, exists := s.channels[c.channel] 178 | 179 | if !exists { 180 | ch = NewChannel(c.channel) 181 | s.channels[ch.name] = ch 182 | 183 | if s.Debug { 184 | log.Printf("go-sse: channel '%s' created.", ch.name) 185 | } 186 | } 187 | 188 | ch.addClient(c) 189 | if s.Debug { 190 | log.Printf("go-sse: new client connected to channel '%s'.", ch.name) 191 | } 192 | 193 | // Client disconnected. 194 | case c := <- s.removeClient: 195 | if ch, exists := s.channels[c.channel]; exists { 196 | ch.removeClient(c) 197 | if s.Debug { 198 | log.Printf("go-sse: client disconnected from channel '%s'.", ch.name) 199 | } 200 | 201 | if s.Debug { 202 | log.Printf("go-sse: checking if channel '%s' has clients.", ch.name) 203 | } 204 | 205 | if ch.ClientCount() == 0 { 206 | delete(s.channels, ch.name) 207 | ch.Close() 208 | 209 | if s.Debug { 210 | log.Printf("go-sse: channel '%s' has no clients.", ch.name) 211 | } 212 | } 213 | } 214 | 215 | // Close channel and all clients in it. 216 | case channel := <- s.closeChannel: 217 | if ch, exists := s.channels[channel]; exists { 218 | delete(s.channels, channel) 219 | ch.Close() 220 | 221 | if s.Debug { 222 | log.Printf("go-sse: channel '%s' closed.", ch.name) 223 | } 224 | } else if s.Debug { 225 | log.Printf("go-sse: requested to close channel '%s', but it doesn't exists.", channel) 226 | } 227 | 228 | // Event Source shutdown. 229 | case <- s.shutdown: 230 | s.close() 231 | close(s.addClient) 232 | close(s.removeClient) 233 | close(s.closeChannel) 234 | close(s.shutdown) 235 | 236 | if s.Debug { 237 | log.Printf("go-sse: server stopped.") 238 | } 239 | return 240 | } 241 | } 242 | } 243 | --------------------------------------------------------------------------------