This presentation is an HTML5 web site
Press → key to advance.
Available to the public since July 2011.
All major App Engine APIs are supported:
Users visit a URL such as http://cuddle-demo.appspot.com/gtug
This adds them to the "gtug" room.
Users send messages to the room.
All present members receive them.
That's it. (Try it now!)
The path component of the URL (eg, "/gtug") corresponds to a Room record in the datastore.
Each participant in a Room corresponds to a Client record.
When a user loads that URL:
Clients receive messages through the browser channel. When the client-side JavaScript receives a message it draws the message to the UI.
Clients post messages by fetching "/post?room=gtug&=MESSAGE". On receiving that request, the server does this:
cuddle/ - the app root
app.yaml - app engine metadata
cuddle/ - Go code belonging to "package cuddle"
db.go - datastore code
http.go - HTTP code
name.go - naming code
tmpl/
root.html - HTML, CSS, JavaScript (all inline)
application: cuddle-demo version: 1 runtime: go api_version: 2 handlers: - url: /.* script: _go_app
// Serves "Hello, Dave" at http://localhost:8080/Dave
package main
import (
"fmt"
"http"
)
func hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello, ", r.URL.Path[1:])
}
func main() {
http.HandleFunc("/", hello)
http.ListenAndServe("localhost:8080", nil)
}
App Engine apps use the "http" package to serve HTTP, too.
package hello // not package "main"
import (
"fmt"
"http"
)
func hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello, ", r.URL.Path[1:])
}
func init() { // not function "main"
http.HandleFunc("/", hello)
}
Cuddle has two HTTP handlers:
root - join the client to a cuddle and serve the HTML/JS/CSS code.
post - post a message to a cuddle.
(It sends messages to clients via the Channel API.)
http.go:
func init() {
// Register our handlers with the http package.
http.HandleFunc("/", root)
http.HandleFunc("/post", post)
}
The first part of the root handler gets the room name from the request URL and handles the case of a missing or invalid name.
// root is an HTTP handler that joins or creates a Room,
// creates a new Client, and writes the HTML response.
func root(w http.ResponseWriter, r *http.Request) {
// Get the name from the request URL.
name := r.URL.Path[1:]
// If no valid name is provided, show an error.
if !validName.MatchString(name) {
http.Error(w, "Invalid cuddle name", 404)
return
}
// validName matches a valid name string. var validName = regexp.MustCompile(`^[a-zA-Z0-9\-]+$`)
Now that we have the name of the room, call the getRoom function to get or create a Room record in the datastore.
c := appengine.NewContext(r)
// Get or create the Room.
room, err := getRoom(c, name)
if err != nil {
http.Error(w, err.String(), 500)
return
}
// Room represents a chat room.
type Room struct {
Name string
}
// getRoom fetches a Room by name from the datastore,
// creating it if it doesn't exist already.
func getRoom(c appengine.Context, name string) (
*Room, os.Error) {
room = &Room{name}
err := datastore.Get(c, room.Key(), room)
if err == datastore.ErrNoSuchEntity {
_, err = datastore.Put(c, room.Key(), room)
}
return room, err
}
func (r *Room) Key() *datastore.Key {
return datastore.NewKey("Room", r.Name, 0, nil)
}
// getRoom fetches a Room by name from the datastore,
// creating it if it doesn't exist already.
func getRoom(c appengine.Context, name string) (
*Room, os.Error) {
room = &Room{name}
fn := func(c appengine.Context) os.Error {
err := datastore.Get(c, room.Key(), room)
if err == datastore.ErrNoSuchEntity {
_, err = datastore.Put(c, room.Key(), room)
}
return err
}
return room, datastore.RunInTransaction(c, fn)
}
// Create a new Client, getting the channel token.
token, err := room.AddClient(c, randId(clientIdLen))
if err != nil {
http.Error(w, err.String(), 500)
return
}
type Client struct {
ClientID string // the channel Client ID
}
AddClient puts a Client record to the datastore with the Room as its parent, creates a channel, and returns the channel token.
func (r *Room) AddClient(c appengine.Context, id string)
(string, os.Error) {
key := datastore.NewKey("Client", id, 0, r.Key())
client := &Client{ClientID: id}
_, err := datastore.Put(c, key, client)
if err != nil {
return "", err
}
return channel.Create(c, id)
}
Finally, we have a *Room and a channel token. Let's render the HTML template.
The "data" variable is an anonymous struct with the two fields we want to pass to the template, Room and Token.
// Render the HTML template.
data := struct{ Room, Token string }{room.Name, token}
err = rootTmpl.Execute(w, data)
if err != nil {
http.Error(w, err.String(), 500)
}
}
// rootTmpl is the main (and only) HTML template.
var rootTmpl = template.Must(template.ParseFile("tmpl/root.html"))
<div id="heading">{{html .Room}}</div>
<div id="in"><input type="text"></div>
<div id="log"></div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script>
<script src="/_ah/channel/jsapi"></script>
<script>
$(function() {
var room = '{{js .Room}}';
var token = '{{js .Token}}';
var chan = new goog.appengine.Channel(token);
var sock = chan.open();
sock.onmessage = function(msg) {
$("#log").prepend($('<div />').text(msg.data));
}
// post broadcasts a message to a specified Room.
func post(w http.ResponseWriter, r *http.Request) {
c := appengine.NewContext(r)
// Get the room.
room, err := getRoom(c, r.FormValue("room"))
if err != nil {
http.Error(w, err.String(), 500)
return
}
// Send the message to the clients in the room.
err = room.Send(c, r.FormValue("msg"))
if err != nil {
http.Error(w, err.String(), 500)
}
}
// Send sends a message to all Clients in a Room.
func (r *Room) Send(c appengine.Context, message string)
os.Error {
var clients []Client
q := datastore.NewQuery("Client").Ancestor(r.Key())
_, err = q.GetAll(c, &clients)
if err != nil {
return err
}
for _, client := range clients {
err = channel.Send(c, client.ClientID, message)
if err != nil {
c.Errorf("sending %q: %v", message, err)
}
}
return nil
}
func (r *Room) Send(c appengine.Context, message string)
os.Error {
var clients []Client
_, err := memcache.JSON.Get(c, r.Name, &clients)
if err != nil && err != memcache.ErrCacheMiss {
return err
}
if err == memcache.ErrCacheMiss {
// omitted: query datastore as before
memcache.JSON.Set(c, &memcache.Item{
Key: r.Name, Object: clients,
})
}
// omitted: send messages as before
return nil
}
func (r *Room) AddClient(c appengine.Context, id string) (
string, os.Error) {
key := datastore.NewKey("Client", id, 0, r.Key())
client := &Client{id}
_, err := datastore.Put(c, key, client)
if err != nil {
return "", err
}
// Purge the now-invalid cache record (if it exists).
memcache.Delete(c, r.Name)
return channel.Create(c, id)
}
This project: http://code.google.com/p/cuddle/
App Engine for Go: http://code.google.com/appengine/docs/go/
The Go web site: http://golang.org/
The Go blog: http://blog.golang.org/