├── .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 [](https://travis-ci.org/alexandrevicenzi/go-sse) [](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 |
--------------------------------------------------------------------------------