Loading...

This presentation is an HTML5 web site

Press key to advance.

Controls
  • and to move around.
  • Ctrl/Command and + or - to zoom.
  • S to view page source.
  • N to toggle speaker notes.
  • 3 to toggle 3D effect.
  • 0 to toggle help.

Cuddle

A Go App Engine Demo
Andrew Gerrand
adg@golang.org
GTUG Sydney
Aug 30, 2011

Go on App Engine

Available to the public since July 2011.

All major App Engine APIs are supported:

  • capability
  • channel
  • datastore
  • mail
  • memcache
  • taskqueue
  • urlfetch
  • user
Cuddle: multi-user anonymous chat

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!)

How it works: joining (1)

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:

  • Look up the Room named "gtug". If it doesn't exist, create it.
  • Generate a random ID for this user/session, and create a Client record as a child of the Room record.
  • Create a browser channel with the Channel API. The channel is identified by the Client ID.
  • Serve the HTML/JS/CSS to the user, including the room name and browser channel token.
  • The client-side JavaScript connects to the channel after the page loads.
How it works: messages (2)

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:

  • Query the datastore for all Clients whose parent is the Room "gtug".
  • Send a message to each of those Clients using the Channel API.
Anatomy of a Go app

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)
app.yaml

application: cuddle-demo
version: 1
runtime: go
api_version: 2

handlers:
- url: /.*
  script: _go_app
Go's http package

// 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)
}
Package http and App Engine

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's HTTP handlers

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 root handler (1)

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\-]+$`)
The root handler (2)

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
}
The getRoom function

// 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)
}
The getRoom function (transactional)

// 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)
}
The root handler (3)

    // 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
}
The AddClient method

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)
}
The root handler (4)

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"))
The root template

<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));
    }
The post handler

// 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)
    }
}
The Send method

// 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
}
Caching: the Send method

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
}
Caching: the AddClient method

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)
}
That's it!

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/