GAE Channel API Basics

#GAE の channel API を使って chat を作ってみる。

ソースは
https://github.com/Satachito/GAEChannel

## v0.1 まずはチャネルの生成と使用をおこなう最小のプログラムを考える。

```ruby:main.go
package main

import (
	"net/http"
	"html/template"

	"appengine"
	"appengine/channel"
)

func
init() {
    http.HandleFunc( "/", handle )
}

func
Execute( tag string, w http.ResponseWriter, p interface{} ) {
	if err := template.Must( template.ParseFiles( tag + ".html" ) ).Execute(
		w,
		p,
	); err != nil { panic( err ) }
}

func
handle( w http.ResponseWriter, r *http.Request ) {

	switch r.Method {
	case "GET":
		Execute( "get", w, nil )
	case "POST":
		wID := r.FormValue( "ID" )
		wToken, err := channel.Create( appengine.NewContext( r ), wID )
		if err != nil { 
			Execute( "get", w, map[ string ]string { "Error": err.Error() } )
		} else {
			Execute(
				"post",
				w,
				map[ string ]string {
					"ID"		: wID,
					"Token"		: wToken,
				},
			)
		}
	case "PUT":
		if err := channel.SendJSON(
			appengine.NewContext( r ),
			r.FormValue( "ID" ),
			map[ string ]interface{} { "message": r.FormValue( "Message" ) },
		); err != nil { panic( err ) }
	}
}
```
```ruby:get.html
<!doctype html>
<html>
<head></head>
<body>
	<p>{{.Error}}</p>
	<form method='POST'>
		<input type='text' name='ID' value='TheID' />
		<input type='submit' value='CONNECT' />
	</form>
</body>
</html>
```
```ruby:post.html
<!doctype html>
<html>
<head></head>
<body>
	<script type='text/javascript' src='/_ah/channel/jsapi'></script>

	<script>
		new goog.appengine.Channel( {{.Token}} ).open(
			{	onopen		: function() {
					document.getElementById( 'Content' ).innerHTML += '{{.Token}}<br />';　
				}
			,	onmessage	: function( p ) {
					document.getElementById( 'Content' ).innerHTML += p.data + '<br />';　
				}
			,	onerror		: function( p ) {
					alert( 'ERROR:' + p.description );
				}
			,	onclose		: function() {
					//	UC
				}
			}
		);
		function
		Send() {
			var wHR = new XMLHttpRequest();
			wHR.onload = function( e ) {
				if ( wHR.readyState === 4 ) {
					if ( wHR.status === 200 ) {
					} else {
						alert( wHR.status );
					}
				}
			}
			wHR.open(
				'PUT'
			,	'/?Message=' + document.getElementById( 'Message' ).value + '&ID=' + {{.ID}}
			,	true
			);
			wHR.send();
		}
	</script>

	<input type='text' id='Message' />
	<input type='button' value='OK' onclick='Send()' />
	<div id='Content'></div>
</body>
</html>
```
```ruby:app.yaml
application: 
version: 1
runtime: go
api_version: go1

handlers:
- url: /.*
  script: _go_app
```

以上の４ファイルを１つのディレクトリに入れて、そのディレクトリの中から

> goapp serve

として、ブラウザから localhost:8080 にアクセス。CONNECTボタンを押す。



## v0.2 Datastore を使ってマルチユーザーにする。

変更するファイル
- main.go
- app.yaml

```ruby:main.go
package main

import (
	"net/http"
	"html/template"

	"appengine"
	"appengine/channel"
	"appengine/datastore"
)

func
init() {
    http.HandleFunc( "/", handle )
	http.HandleFunc( "/_ah/channel/connected/", connected )
	http.HandleFunc( "/_ah/channel/disconnected/", disconnected )
}

func
ChannelKey( ctx appengine.Context, id string ) *datastore.Key {
	return datastore.NewKey( ctx, "Channels", id, 0, nil )
}

type
NULL struct {
}

func
connected( w http.ResponseWriter, r *http.Request ) {
	ctx := appengine.NewContext( r )

	if _, err := datastore.Put(
		ctx,
		ChannelKey( ctx, r.FormValue( "from" ) ),
		&NULL{},
	); err != nil { panic( err ) }
}

func
disconnected( w http.ResponseWriter, r *http.Request ) {
	ctx := appengine.NewContext( r )

	if err := datastore.Delete(
		ctx,
		ChannelKey( ctx, r.FormValue( "from" ) ),
	); err != nil { panic( err ) }
}

func
Execute( tag string, w http.ResponseWriter, p interface{} ) {
	if err := template.Must( template.ParseFiles( tag + ".html" ) ).Execute(
		w,
		p,
	); err != nil { panic( err ) }
}

func
handle( w http.ResponseWriter, r *http.Request ) {
	switch r.Method {
	case "GET":
		Execute( "get", w, nil )
	case "POST":
		wID := r.FormValue( "ID" )
		wToken, err := channel.Create( appengine.NewContext( r ), wID )
		if err != nil { 
			Execute( "get", w, map[ string ]string { "Error": err.Error() } )
		} else {
			Execute(
				"post",
				w,
				map[ string ]string {
					"ID"		: wID,
					"Token"		: wToken,
				},
			)
		}
	case "PUT":
		ctx := appengine.NewContext( r )

		keys, err := datastore.NewQuery( "Channels" ).KeysOnly().GetAll( ctx, nil )
		if err != nil { panic( err ) }

		for _, key := range keys {
			if err := channel.SendJSON(
				ctx,
				key.StringID(),
				map[ string ]interface{} {
					"message"	: r.FormValue( "Message" ),
					"ID"		: r.FormValue( "ID" ),
				},
			); err != nil { panic( err ) }
		}
	}
}
```
```ruby:app.yaml
application: 
version: 1
runtime: go
api_version: go1

inbound_services:
- channel_presence

handlers:
- url: /_ah/channel/connected/
  script: _go_app

- url: /_ah/channel/disconnected/
  script: _go_app

- url: /.*
  script: _go_app
```

ブラウザを２画面開いて localhost:8080 にアクセスし、ID を別のものにする。

<img width="705" alt="SS.png" src="https://qiita-image-store.s3.amazonaws.com/0/73493/ce054141-3129-73e1-5399-f4273fcacd8c.png">

## 制約など

- 同一 ID のチャネルは生成できる（エラーにならない）が使用できない。（仕様）
例えばブラウザ２画面で同一 ID で生成すると一見うまく行くが、どちらか片方にしか表示されない。


- 無料枠では 100 チャンネルまで使用可能。それ以上はチャネル数に応じた従量課金になる。


- post.html の中の onmessage で body タグに HTML を追加しようとしたら、ローカルでは動くが、デプロイすると動かない、という不思議な現象が発生した。