├── .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 | --------------------------------------------------------------------------------