├── .gitignore
├── README.md
├── server.go
└── templates
└── index.html
/.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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Golang HTML5 SSE Example
2 | ========================
3 |
4 | This is an minimalistic example of how to do
5 | [HTML5 Server Sent Events](http://en.wikipedia.org/wiki/Server-sent_events)
6 | with [Go (golang)](http://golang.org/). From the server's perspective,
7 | SSE is nearly identical to long polling. The client makes a GET request
8 | that establishes a TCP connection. The server keeps this connection open
9 | and sends events to the client when they are available. In this example,
10 | the server pushes a new event every five seconds, consisting of a short
11 | message with the current time. Any number of clients can be
12 | connected: they will all receive the same events if they're connected
13 | concurrently. (This is achieved using Go's channels and a fan-out
14 | pattern. In other languages you may need to use some kind of pubsub
15 | messaging, like Redis or Zeromq.)
16 |
17 | The main advantage of HTML5 SSE over long polling is that there is a nice
18 | API for it in modern browsers, so that you need not use iframes and such.
19 | SSE is easier than Websockets in the sense that it communicates exclusively
20 | over HTTP and therefore does not require a separate server. Websockets,
21 | however, supports two-way real-time communication between the client and
22 | the server.
23 |
24 | ## Installing
25 |
26 | Check out the repository from GitHub
27 |
28 | git clone https://github.com/kljensen/golang-html5-sse-example
29 |
30 | ## Running
31 |
32 | To run the server, do
33 |
34 | go run ./server.go
35 |
36 | Then point your web browser to `http://localhost:8000`.
37 | You should see output in the browser like the following:
38 |
39 | Yo friend, here are some fascinating messages about the current time:
40 | Message: 20 - the time is 2013-03-08 21:08:01.260967 -0500 EST
41 | Message: 21 - the time is 2013-03-08 21:08:06.262034 -0500 EST
42 | Message: 22 - the time is 2013-03-08 21:08:11.262608 -0500 EST
43 | Message: 23 - the time is 2013-03-08 21:08:16.263491 -0500 EST
44 | Message: 24 - the time is 2013-03-08 21:08:21.264218 -0500 EST
45 | Message: 25 - the time is 2013-03-08 21:08:26.264433 -0500 EST
46 |
47 | And, you should see output in the terminal like the following
48 |
49 | 2013/03/15 03:55:55 Sent message 0
50 | 2013/03/15 03:55:55 Broadcast message to 0 clients
51 | 2013/03/15 03:55:55 Added new client
52 | 2013/03/15 03:55:55 Added new client
53 | 2013/03/15 03:55:58 Finished HTTP request at /
54 | 2013/03/15 03:55:58 Added new client
55 | 2013/03/15 03:56:00 Sent message 1
56 | 2013/03/15 03:56:00 Broadcast message to 3 clients
57 | 2013/03/15 03:56:05 Sent message 2
58 | 2013/03/15 03:56:05 Broadcast message to 3 clients
59 | 2013/03/15 03:56:10 Sent message 3
60 | 2013/03/15 03:56:10 Broadcast message to 3 clients
61 | 2013/03/15 03:56:15 Sent message 4
62 | 2013/03/15 03:56:15 Broadcast message to 3 clients
63 | 2013/03/15 03:56:20 Sent message 5
64 | 2013/03/15 03:56:20 Broadcast message to 3 clients
65 |
66 | ## Thanks
67 |
68 | This code is based off of a few sources, mostly
69 |
70 | * [Leroy Campbell's SSE example in Go](https://gist.github.com/artisonian/3836281); and,
71 | * the [HTML5Rocks SSE tutorial](http://www.html5rocks.com/en/tutorials/eventsource/basics/).
72 |
73 |
74 | ## License (the Unlicense)
75 |
76 | This is free and unencumbered software released into the public domain.
77 |
78 | Anyone is free to copy, modify, publish, use, compile, sell, or
79 | distribute this software, either in source code form or as a compiled
80 | binary, for any purpose, commercial or non-commercial, and by any
81 | means.
82 |
83 | In jurisdictions that recognize copyright laws, the author or authors
84 | of this software dedicate any and all copyright interest in the
85 | software to the public domain. We make this dedication for the benefit
86 | of the public at large and to the detriment of our heirs and
87 | successors. We intend this dedication to be an overt act of
88 | relinquishment in perpetuity of all present and future rights to this
89 | software under copyright law.
90 |
91 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
92 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
93 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
94 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
95 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
96 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
97 | OTHER DEALINGS IN THE SOFTWARE.
98 |
99 | For more information, please refer to
100 |
101 |
--------------------------------------------------------------------------------
/server.go:
--------------------------------------------------------------------------------
1 | // Golang HTML5 Server Side Events Example
2 | //
3 | // Run this code like:
4 | // > go run server.go
5 | //
6 | // Then open up your browser to http://localhost:8000
7 | // Your browser must support HTML5 SSE, of course.
8 |
9 | package main
10 |
11 | import (
12 | "fmt"
13 | "html/template"
14 | "log"
15 | "net/http"
16 | "time"
17 | )
18 |
19 | // A single Broker will be created in this program. It is responsible
20 | // for keeping a list of which clients (browsers) are currently attached
21 | // and broadcasting events (messages) to those clients.
22 | type Broker struct {
23 |
24 | // Create a map of clients, the keys of the map are the channels
25 | // over which we can push messages to attached clients. (The values
26 | // are just booleans and are meaningless.)
27 | //
28 | clients map[chan string]bool
29 |
30 | // Channel into which new clients can be pushed
31 | //
32 | newClients chan chan string
33 |
34 | // Channel into which disconnected clients should be pushed
35 | //
36 | defunctClients chan chan string
37 |
38 | // Channel into which messages are pushed to be broadcast out
39 | // to attahed clients.
40 | //
41 | messages chan string
42 | }
43 |
44 | // This Broker method starts a new goroutine. It handles
45 | // the addition & removal of clients, as well as the broadcasting
46 | // of messages out to clients that are currently attached.
47 | func (b *Broker) Start() {
48 |
49 | // Start a goroutine
50 | //
51 | go func() {
52 |
53 | // Loop endlessly
54 | //
55 | for {
56 |
57 | // Block until we receive from one of the
58 | // three following channels.
59 | select {
60 |
61 | case s := <-b.newClients:
62 |
63 | // There is a new client attached and we
64 | // want to start sending them messages.
65 | b.clients[s] = true
66 | log.Println("Added new client")
67 |
68 | case s := <-b.defunctClients:
69 |
70 | // A client has dettached and we want to
71 | // stop sending them messages.
72 | delete(b.clients, s)
73 | close(s)
74 |
75 | log.Println("Removed client")
76 |
77 | case msg := <-b.messages:
78 |
79 | // There is a new message to send. For each
80 | // attached client, push the new message
81 | // into the client's message channel.
82 | for s := range b.clients {
83 | s <- msg
84 | }
85 | log.Printf("Broadcast message to %d clients", len(b.clients))
86 | }
87 | }
88 | }()
89 | }
90 |
91 | // This Broker method handles and HTTP request at the "/events/" URL.
92 | func (b *Broker) ServeHTTP(w http.ResponseWriter, r *http.Request) {
93 |
94 | // Make sure that the writer supports flushing.
95 | //
96 | f, ok := w.(http.Flusher)
97 | if !ok {
98 | http.Error(w, "Streaming unsupported!", http.StatusInternalServerError)
99 | return
100 | }
101 |
102 | // Create a new channel, over which the broker can
103 | // send this client messages.
104 | messageChan := make(chan string)
105 |
106 | // Add this client to the map of those that should
107 | // receive updates
108 | b.newClients <- messageChan
109 |
110 | // Listen to the closing of the http connection via the Request.Context
111 | notify := r.Context().Done()
112 | go func() {
113 | <-notify
114 | // Remove this client from the map of attached clients
115 | // when `EventHandler` exits.
116 | b.defunctClients <- messageChan
117 | log.Println("HTTP connection just closed.")
118 | }()
119 |
120 | // Set the headers related to event streaming.
121 | w.Header().Set("Content-Type", "text/event-stream")
122 | w.Header().Set("Cache-Control", "no-cache")
123 | w.Header().Set("Connection", "keep-alive")
124 | w.Header().Set("Transfer-Encoding", "chunked")
125 |
126 | // Don't close the connection, instead loop endlessly.
127 | for {
128 |
129 | // Read from our messageChan.
130 | msg, open := <-messageChan
131 |
132 | if !open {
133 | // If our messageChan was closed, this means that the client has
134 | // disconnected.
135 | break
136 | }
137 |
138 | // Write to the ResponseWriter, `w`.
139 | fmt.Fprintf(w, "data: Message: %s\n\n", msg)
140 |
141 | // Flush the response. This is only possible if
142 | // the repsonse supports streaming.
143 | f.Flush()
144 | }
145 |
146 | // Done.
147 | log.Println("Finished HTTP request at ", r.URL.Path)
148 | }
149 |
150 | // Handler for the main page, which we wire up to the
151 | // route at "/" below in `main`.
152 | func handler(w http.ResponseWriter, r *http.Request) {
153 |
154 | // Did you know Golang's ServeMux matches only the
155 | // prefix of the request URL? It's true. Here we
156 | // insist the path is just "/".
157 | if r.URL.Path != "/" {
158 | w.WriteHeader(http.StatusNotFound)
159 | return
160 | }
161 |
162 | // Read in the template with our SSE JavaScript code.
163 | t, err := template.ParseFiles("templates/index.html")
164 | if err != nil {
165 | log.Fatal("Error parsing your template.")
166 | }
167 |
168 | // Render the template, writing to `w`.
169 | t.Execute(w, "friend")
170 |
171 | // Done.
172 | log.Println("Finished HTTP request at", r.URL.Path)
173 | }
174 |
175 | // Main routine
176 | func main() {
177 |
178 | // Make a new Broker instance
179 | b := &Broker{
180 | make(map[chan string]bool),
181 | make(chan (chan string)),
182 | make(chan (chan string)),
183 | make(chan string),
184 | }
185 |
186 | // Start processing events
187 | b.Start()
188 |
189 | // Make b the HTTP handler for "/events/". It can do
190 | // this because it has a ServeHTTP method. That method
191 | // is called in a separate goroutine for each
192 | // request to "/events/".
193 | http.Handle("/events/", b)
194 |
195 | // Generate a constant stream of events that get pushed
196 | // into the Broker's messages channel and are then broadcast
197 | // out to any clients that are attached.
198 | go func() {
199 | for i := 0; ; i++ {
200 |
201 | // Create a little message to send to clients,
202 | // including the current time.
203 | b.messages <- fmt.Sprintf("%d - the time is %v", i, time.Now())
204 |
205 | // Print a nice log message and sleep for 5s.
206 | log.Printf("Sent message %d ", i)
207 | time.Sleep(5e9)
208 |
209 | }
210 | }()
211 |
212 | // When we get a request at "/", call `handler`
213 | // in a new goroutine.
214 | http.Handle("/", http.HandlerFunc(handler))
215 |
216 | // Start the server and listen forever on port 8000.
217 | http.ListenAndServe(":8000", nil)
218 | }
219 |
--------------------------------------------------------------------------------
/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | HTML5 Server Side Event Example in Go
5 |
6 |
7 |
8 | Yo {{.}}, here are some fascinating messages about the
9 | current time:
10 |
11 |
23 |
24 |
25 |
--------------------------------------------------------------------------------