Created
August 9, 2015 13:56
-
-
Save rhzs/1d80cfcb8b07033fa485 to your computer and use it in GitHub Desktop.
Go - Demonstrate simple HTML5 Server Side Events Example without Redis / RabbitMQ / anykind
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>HTML5 Server Side Event Example in Go</title> | |
</head> | |
<body> | |
Yey! {{.}}, here are some new messages about the | |
current time:<br> | |
<script type="text/javascript"> | |
// Create a new HTML5 EventSource | |
var source = new EventSource('/events/'); | |
// Create a callback for when a new message is received. | |
source.onmessage = function(e) { | |
// Append the `data` attribute of the message to the DOM. | |
document.body.innerHTML += e.data + '<br>'; | |
}; | |
</script> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Purpose: | |
// Demonstrate simple HTML5 Server Side Events Example without Redis / RabbitMQ / anykind | |
// | |
// Run in terminal: | |
// > go run server.go | |
// | |
// Then open up your browser to http://localhost:8080 | |
// Your browser must support HTML5 SSE or get the latest chrome/firefox (IE is not tested). | |
// index.html must be in the same directory (or you can change it if you want by default it sets on the same directory with server.go file | |
package main | |
import ( | |
"fmt" | |
"html/template" | |
"log" | |
"net/http" | |
"time" | |
) | |
// A single Broker will be created in this program. It is responsible | |
// for keeping a list of which clients (browsers) are currently attached | |
// and broadcasting events (messages) to those clients. | |
type Broker struct { | |
// Create a map of clients, the keys of the map are the channels | |
// over which we can push messages to attached clients. (The values | |
// are just booleans and are meaningless.) | |
clients map[chan string]bool | |
// Channel into which new clients can be pushed | |
newClients chan chan string | |
// Channel into which disconnected clients should be pushed | |
// | |
defunctClients chan chan string | |
// Channel into which messages are pushed to be broadcast out | |
// to attahed clients. | |
// | |
messages chan string | |
} | |
// This Broker method starts a new goroutine. It handles | |
// the addition & removal of clients, as well as the broadcasting | |
// of messages out to clients that are currently attached. | |
// | |
func (b *Broker) Start() { | |
// Start a goroutine | |
go func() { | |
// Loop endlessly | |
for { | |
// Block until we receive from one of the | |
// three following channels. | |
select { | |
case s := <-b.newClients: | |
// There is a new client attached and we | |
// want to start sending them messages. | |
b.clients[s] = true | |
log.Println("Added new client") | |
case s := <-b.defunctClients: | |
// A client has dettached and we want to | |
// stop sending them messages. | |
delete(b.clients, s) | |
log.Println("Removed client") | |
case msg := <-b.messages: | |
// There is a new message to send. For each | |
// attached client, push the new message | |
// into the client's message channel. | |
for s, _ := range b.clients { | |
s <- msg | |
} | |
log.Printf("Broadcast message to %d clients", len(b.clients)) | |
} | |
} | |
}() | |
} | |
// This Broker method handles and HTTP request at the "/events/" URL. | |
func (b *Broker) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |
// Make sure that the writer supports flushing. | |
f, ok := w.(http.Flusher) | |
if !ok { | |
http.Error(w, "Streaming unsupported!", http.StatusInternalServerError) | |
return | |
} | |
// Create a new channel, over which the broker can | |
// send this client messages. | |
messageChan := make(chan string) | |
// Add this client to the map of those that should | |
// receive updates | |
b.newClients <- messageChan | |
// Listen to the closing of the http connection via the CloseNotifier | |
notify := w.(http.CloseNotifier).CloseNotify() | |
go func() { | |
<-notify | |
// Remove this client from the map of attached clients | |
// when `EventHandler` exits. | |
b.defunctClients <- messageChan | |
log.Println("HTTP connection just closed.") | |
}() | |
// Set the headers related to event streaming. | |
w.Header().Set("Content-Type", "text/event-stream") | |
w.Header().Set("Cache-Control", "no-cache") | |
w.Header().Set("Connection", "keep-alive") | |
// Don't close the connection, instead loop 10 times, | |
// sending messages and flushing the response each time | |
// there is a new message to send along. | |
// | |
// NOTE: we could loop endlessly; however, then you | |
// could not easily detect clients that dettach and the | |
// server would continue to send them messages long after | |
// they're gone due to the "keep-alive" header. One of | |
// the nifty aspects of SSE is that clients automatically | |
// reconnect when they lose their connection. | |
// | |
// A better way to do this is to use the CloseNotifier | |
// interface that will appear in future releases of | |
// Go (this is written as of 1.0.3): | |
// https://code.google.com/p/go/source/detail?name=3292433291b2 | |
// | |
for { | |
// Read from our messageChan. | |
msg := <-messageChan | |
// Write to the ResponseWriter, `w`. | |
fmt.Fprintf(w, "data: Message: %s\n\n", msg) | |
// Flush the response. This is only possible if | |
// the repsonse supports streaming. | |
f.Flush() | |
} | |
log.Println("Finished HTTP request at ", r.URL.Path) | |
} | |
// Handler for the main page, which we wire up to the | |
// route at "/" below in `main`. | |
func MainPageHandler(w http.ResponseWriter, r *http.Request) { | |
// Did you know Golang's ServeMux matches only the | |
// prefix of the request URL? It's true. Here we | |
// insist the path is just "/". | |
if r.URL.Path != "/" { | |
w.WriteHeader(http.StatusNotFound) | |
return | |
} | |
// Read in the template with our SSE JavaScript code. | |
t, err := template.ParseFiles("index.html") | |
if err != nil { | |
log.Fatal("WTF dude, error parsing your template.") | |
} | |
// Render the template, writing to `w`. | |
t.Execute(w, "Duder") | |
log.Println("Finished HTTP request at ", r.URL.Path) | |
} | |
// Main routine | |
func main() { | |
// Make a new Broker instance | |
b := &Broker{ | |
make(map[chan string]bool), | |
make(chan (chan string)), | |
make(chan (chan string)), | |
make(chan string), | |
} | |
// Start processing events | |
b.Start() | |
// Make b the HTTP handler for "/events/". It can do | |
// this because it has a ServeHTTP method. That method | |
// is called in a separate goroutine for each | |
// request to "/events/". | |
http.Handle("/events/", b) | |
// Generate a constant stream of events that get pushed | |
// into the Broker's messages channel and are then broadcast | |
// out to any clients that are attached. | |
go func() { | |
for i := 0; ; i++ { | |
// Create a little message to send to clients, | |
// including the current time. | |
b.messages <- fmt.Sprintf("%d - the time is %v", i, time.Now()) | |
// Print a nice log message and sleep for 5s. | |
log.Printf("Sent message %d ", i) | |
time.Sleep(5 * 1e9) | |
} | |
}() | |
// When we get a request at "/", call `MainPageHandler` | |
// in a new goroutine. | |
http.Handle("/", http.HandlerFunc(MainPageHandler)) | |
// Start the server and listen forever on port 8080. | |
http.ListenAndServe(":8080", nil) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment