ページへ戻る
印刷
Google App Engine for Python その2
をテンプレートにして作成 ::
NJF Wiki
xpwiki
:Google App Engine for Python その2 をテンプレートにして作成
開始行:
前の記事はこちら。[[Google App Engine for Python]]
*ブラウザ上のカジュアルゲームデータの保存の概要
Flashで動作するゲームのユーザーデータは以前はSharedObject...
そこで、サーバー上で保存するわけですが、そのために必要な...
-ユーザーIDとパスワード
-ゲームデータ
ユーザーIDとパスワードはユーザーを区別するために必要です...
ここでは最も簡単な、サーバー側でIDとパスワードを発行して...
ゲームデータはjson形式で送ることにします。FlashだとAMFと...
GAEのデータ設計において注意しないといけないのは、1レコー...
すると必要なテーブルは次の二つです。
-ユーザーID
-ユーザーデータ
ユーザーIDテーブルの方では通番のユーザーIDを作成するため...
ユーザーデータの方はユーザーIDとパスワードとゲームのデー...
*処理の流れ
全体の処理の流れは以下の通りです。
**登録処理
+ユーザーが新規登録を選んだ場合、サーバーで登録処理を行う
+サーバーから戻されたIDとパスワードをクライアントはShared...
**ログイン処理
+ShareObjectでローカルにIDとパスワードが保存されている場...
+サーバーから戻されたゲームデータでゲームを開始する
+ユーザーがIDとパスワードを入力していた場合は、それをShar...
**データの保存
+IDとパスワードとゲームデータをサーバーへ送信する
+サーバーではデータの整合性をチェックし、保存して結果を戻す
よって、ゲーム側には新規登録画面、ユーザーIDとパスワード...
また、サーバー側には登録処理、ログイン処理、セーブ処理を...
*URL構成
以上のことを踏まえて、URLの構成は以下のようになります。
/crossdomain.xml
/api/entry
/api/save
/api/load
crossdomain.xmlはflashを別サーバーにWebで公開するときに必...
上記のものは最低限必要な構成で、実際にはルート以下にfavic...
URLと実ファイルの対応は別途定義できるので、実際にはapi以...
*静的ファイルの取り扱い
crossdomain.xmlのような静的ファイルはapp.yamlの中でURLと...
具体的には、/crossdomain.xmlにアクセスがあった場合、stati...
- url: /crossdomain.xml
mime_type: text/xml
static_files: static/crossdomain.xml
upload: static/crossdomain.xml
他にもいろいろな割り振り方がありますが、ゲーム保存用のAPI...
*フレームワークの導入
HTMLの入出力部分などを自分で一から作るのは面倒なので、GAE...
これを使って前回の記事で作ったHello Worldを作り直すと以下...
まず、app.yamlです。
application: hello-world-app
version: 1
runtime: python
api_version: 1
handlers:
- url: /.*
script: helloworld.application
ここで大きく変わったのはscriptのところがファイル名では無...
import webapp2
class HelloWorld(webapp2.RequestHandler):
def get(self):
self.response.headers['Content-Type'] = "text/pl...
self.response.write("Hello, world!")
application = webapp2.WSGIApplication(
[('/', HelloWorld),
],
debug=True)
となります。app.yamlでhelloworld.applicationと指定したせ...
HelloWorldクラスにはgetメソッドが定義してあり、GETリクエ...
getメソッドの中は単にヘッダと本文をクライアント向けに書き...
元のHello Worldのサンプルに比べて複雑になったように見えま...
*GAEのデータ保存
GAEではデータはBigtableというものに保存されます。通常のリ...
カジュアルゲームのデータ保存の場合、集計や表結合は普通無...
また、GAEはデータの排他処理があまり得意ではありません。同...
一方、リレーショナルデータベースと違って、後からテーブル...
では、テーブル定義にあたるモデルの定義を見てみましょう。...
from google.appengine.ext import ndb
class UserData(ndb.Model):
userId = ndb.IntegerProperty()
password = ndb.IntegerProperty()
data = ndb.JsonProperty()
これでユーザーIDとパスワードが整数、dataがJSONであるよう...
どんな型が使えるかはGAEのリファレンスで調べられます。
(
https://cloud.google.com/appengine/docs/python/ndb/proper...
)
よく使うのは上記の整数とJson型、あとは以下の日付とString...
ndb.DateTimeProperty()
ndb.StringProperty()
使い方は、保存はインスタンス化して値を設定してputするだけ...
userData = UserData() #インスタンス作成
userData.userId = 1
userData.password = 12345
userData.data = jsonData
userData.put() #保存
読み出しはいろいろな方法がありますが、IDが1のものを1つだ...
userDatas = UserData.query(UserData.userId==1).fetch(1)
とすると長さ最大1のUserData型の配列として取り出せます。
集計や結合などをしないなら、ほぼ通常のリレーショナルデー...
以前にも書きましたが、GAEでは1レコード取り出す毎に課金さ...
*データモデル
では、ゲームデータの保存に必要なモデルを考えます。
[[ブラウザ上のカジュアルゲームデータの保存の概要>#u36cbcb...
で解説したように、必要なモデルは次の二つです。
-ユーザーID
-ユーザーデータ
ユーザーIDはIDをカウントしていくために必要で、ユーザーデ...
class UserIdCounter(ndb.Model):
userId = ndb.IntegerProperty()
class UserData(ndb.Model):
userId = ndb.IntegerProperty()
password = ndb.IntegerProperty()
data = ndb.JsonProperty()
実際には登録日や更新日時、ゲームのバージョンなど、管理用...
*IDの加算と排他処理
ユーザーIDを取得して加算する処理は以下のようになります。G...
@ndb.transactional
def getUserId():
userIdIndex = "userIdIndex"
lastId = UserIdCounter.get_by_id(userIdIndex)
if lastId is None:
lastId = UserIdCounter(id=userIdIndex)
lastId.userId = 0
nowId = lastId.userId
lastId.userId = lastId.userId + 1
lastId.put()
return nowId
@ndb.transactionalを指定すると以下に続く処理が排他的に行...
+UserIdCounterの取得
+UserIdCounterの加算
+UserIdCounterの取得
+UserIdCounterの加算
となればもちろん問題ないのですが、
+UserIdCounterの取得
+UserIdCounterの取得
+UserIdCounterの加算
+UserIdCounterの加算
となると同じ値をとって最後に2加算することになり、同じ値...
データモデルは初期化の時にidというパラメータを指定するこ...
*ユーザ-登録処理
上記ユーザーIDの生成処理を使うと、ユーザー登録処理は以...
class NjfEntry(webapp2.RequestHandler):
def post(self):
self.response.headers['Content-Type'] = "text/pl...
userId = getUserId()
password = random.randint(1000,1000000)
userData = UserData(id = str(userId) + "_" + str...
userData.userId = userId
userData.password = password
userData.data = {}
userData.put()
returnData = {}
returnData["userId"] = userId
returnData["password"] = password
self.response.write(json.dumps(returnData))
パスワードは乱数にしています。桁数があまり小さいと偶然正...
ここでuserDataにユーザーIDとパスワードを連結した物をI...
戻り値はユーザーIDとパスワードをjsonで送っています。
*ログイン処理
ログイン処理ではユーザーIDとパスワードをチェックして正...
class NjfLogin(webapp2.RequestHandler):
def post(self):
self.response.headers['Content-Type'] = "text/pl...
userId = self.request.get('userId')
password = self.request.get('password')
userData = getUserData(userId, password)
if userData:
self.response.write(json.dumps(userData.data))
else:
self.response.write("-1")
ここでgetUserDataは以下のような、ユーザーIDとパスワード...
def getUserData(userId,password):
ud = UserData.get_by_id(id=userId+"_"+password)
return ud
*セーブ処理
最後に保存処理です。ユーザーIDとパスワードをチェックし...
class NjfSave(webapp2.RequestHandler):
def post(self):
self.response.headers['Content-Type'] = "text/pl...
userId = self.request.get('userId')
password = self.request.get('password')
data = self.request.get('data')
userData = getUserData(userId, password)
if userData:
jsonData = json.loads(data)
deepJsonCopy(jsonData, userData.data)
userData.put()
self.response.write("{}")
else:
self.response.write("-1")
ここでdeepJsonCopy関数は以下のようにjsonデータを再帰的に...
def deepJsonCopy(fromData,toData):
for key, value in fromData.iteritems():
if isinstance(value,dict):
if not toData.has_key(key):
toData[key] = {}
deepJsonCopy(fromData[key],toData[key])
else:
toData[key] = fromData[key]
pythonのjsonライブラリにはディープコピー関数があるのです...
この関数を使うことによって、必要なデータだけをサーバーに...
GAEでは転送量も課金対象になります。また、そうで無くてもや...
しかし、差分だけを送信するとクライアント側とサーバー側で...
毎回全てのデータを上書きするのならその心配はありません。...
以上でサーバーサイドは終わりです。次にクライアントサイド...
*クライアントサイド
Flash側では単にJSONデータをパースしたりダンプするだけであ...
送信データをダンプするには
var data:Object = new Object();
data["key"] = "value";
JSON.stringify(data);
受信データをパースするには
var jsonData:Object;
try
{
jsonData = JSON.parse(data);
}catch (e:Error) {
trace(e,data);
}
です。
オブジェクトが入れ子になっていても大丈夫です。
注意する必要があるのは、日本語を送るとGAE側でエラーになる...
*さらに改良するには
これで最低限のデータの保存と呼び出しが出来るようになりま...
**値のチェック
IDが数値かどうかやパスワードが数値かどうかなどはここでは...
**チート対策
もしランキングなどを実装するならチート対策が必須となりま...
クライアント側での対策は「[[ActionScriptでチート検出]]」...
これに加えてデータ送信時にも署名を行うなどしてデータの改...
たとえば、jsonデータを送信するときに、そのmd5ハッシュを同...
**2重登録チェック
前述のように、データを差分で送るとブラウザを2つあげてそ...
サーバー側でそれぞれの値の整合性チェックを行うと他の不具...
すると2つのクライアントから別々に保存しようとしても、後...
**パスワード暗号化
ここではパスワードを平文で保存していますが、個人情報など...
*費用
費用についてはゲームによってセーブの回数や転送量が違うの...
それ以上の場合で課金されるのは、ほぼ「Datastore Write Ope...
これは名前の通りデータを保存した回数で、レコード毎に課金...
次に課金されやすい項目は「Datastore Read Operations」です...
カジュアルゲームのデータ保存程度ではこれ以外ではほぼ課金...
よって、一日1万PV以上が一年以上続くような場合では月々定額...
*サンプルコード
ここで解説したサンプルコードはこちらです。クライアント側...
&ref(gaeWikiSample.zip);
終了行:
前の記事はこちら。[[Google App Engine for Python]]
*ブラウザ上のカジュアルゲームデータの保存の概要
Flashで動作するゲームのユーザーデータは以前はSharedObject...
そこで、サーバー上で保存するわけですが、そのために必要な...
-ユーザーIDとパスワード
-ゲームデータ
ユーザーIDとパスワードはユーザーを区別するために必要です...
ここでは最も簡単な、サーバー側でIDとパスワードを発行して...
ゲームデータはjson形式で送ることにします。FlashだとAMFと...
GAEのデータ設計において注意しないといけないのは、1レコー...
すると必要なテーブルは次の二つです。
-ユーザーID
-ユーザーデータ
ユーザーIDテーブルの方では通番のユーザーIDを作成するため...
ユーザーデータの方はユーザーIDとパスワードとゲームのデー...
*処理の流れ
全体の処理の流れは以下の通りです。
**登録処理
+ユーザーが新規登録を選んだ場合、サーバーで登録処理を行う
+サーバーから戻されたIDとパスワードをクライアントはShared...
**ログイン処理
+ShareObjectでローカルにIDとパスワードが保存されている場...
+サーバーから戻されたゲームデータでゲームを開始する
+ユーザーがIDとパスワードを入力していた場合は、それをShar...
**データの保存
+IDとパスワードとゲームデータをサーバーへ送信する
+サーバーではデータの整合性をチェックし、保存して結果を戻す
よって、ゲーム側には新規登録画面、ユーザーIDとパスワード...
また、サーバー側には登録処理、ログイン処理、セーブ処理を...
*URL構成
以上のことを踏まえて、URLの構成は以下のようになります。
/crossdomain.xml
/api/entry
/api/save
/api/load
crossdomain.xmlはflashを別サーバーにWebで公開するときに必...
上記のものは最低限必要な構成で、実際にはルート以下にfavic...
URLと実ファイルの対応は別途定義できるので、実際にはapi以...
*静的ファイルの取り扱い
crossdomain.xmlのような静的ファイルはapp.yamlの中でURLと...
具体的には、/crossdomain.xmlにアクセスがあった場合、stati...
- url: /crossdomain.xml
mime_type: text/xml
static_files: static/crossdomain.xml
upload: static/crossdomain.xml
他にもいろいろな割り振り方がありますが、ゲーム保存用のAPI...
*フレームワークの導入
HTMLの入出力部分などを自分で一から作るのは面倒なので、GAE...
これを使って前回の記事で作ったHello Worldを作り直すと以下...
まず、app.yamlです。
application: hello-world-app
version: 1
runtime: python
api_version: 1
handlers:
- url: /.*
script: helloworld.application
ここで大きく変わったのはscriptのところがファイル名では無...
import webapp2
class HelloWorld(webapp2.RequestHandler):
def get(self):
self.response.headers['Content-Type'] = "text/pl...
self.response.write("Hello, world!")
application = webapp2.WSGIApplication(
[('/', HelloWorld),
],
debug=True)
となります。app.yamlでhelloworld.applicationと指定したせ...
HelloWorldクラスにはgetメソッドが定義してあり、GETリクエ...
getメソッドの中は単にヘッダと本文をクライアント向けに書き...
元のHello Worldのサンプルに比べて複雑になったように見えま...
*GAEのデータ保存
GAEではデータはBigtableというものに保存されます。通常のリ...
カジュアルゲームのデータ保存の場合、集計や表結合は普通無...
また、GAEはデータの排他処理があまり得意ではありません。同...
一方、リレーショナルデータベースと違って、後からテーブル...
では、テーブル定義にあたるモデルの定義を見てみましょう。...
from google.appengine.ext import ndb
class UserData(ndb.Model):
userId = ndb.IntegerProperty()
password = ndb.IntegerProperty()
data = ndb.JsonProperty()
これでユーザーIDとパスワードが整数、dataがJSONであるよう...
どんな型が使えるかはGAEのリファレンスで調べられます。
(
https://cloud.google.com/appengine/docs/python/ndb/proper...
)
よく使うのは上記の整数とJson型、あとは以下の日付とString...
ndb.DateTimeProperty()
ndb.StringProperty()
使い方は、保存はインスタンス化して値を設定してputするだけ...
userData = UserData() #インスタンス作成
userData.userId = 1
userData.password = 12345
userData.data = jsonData
userData.put() #保存
読み出しはいろいろな方法がありますが、IDが1のものを1つだ...
userDatas = UserData.query(UserData.userId==1).fetch(1)
とすると長さ最大1のUserData型の配列として取り出せます。
集計や結合などをしないなら、ほぼ通常のリレーショナルデー...
以前にも書きましたが、GAEでは1レコード取り出す毎に課金さ...
*データモデル
では、ゲームデータの保存に必要なモデルを考えます。
[[ブラウザ上のカジュアルゲームデータの保存の概要>#u36cbcb...
で解説したように、必要なモデルは次の二つです。
-ユーザーID
-ユーザーデータ
ユーザーIDはIDをカウントしていくために必要で、ユーザーデ...
class UserIdCounter(ndb.Model):
userId = ndb.IntegerProperty()
class UserData(ndb.Model):
userId = ndb.IntegerProperty()
password = ndb.IntegerProperty()
data = ndb.JsonProperty()
実際には登録日や更新日時、ゲームのバージョンなど、管理用...
*IDの加算と排他処理
ユーザーIDを取得して加算する処理は以下のようになります。G...
@ndb.transactional
def getUserId():
userIdIndex = "userIdIndex"
lastId = UserIdCounter.get_by_id(userIdIndex)
if lastId is None:
lastId = UserIdCounter(id=userIdIndex)
lastId.userId = 0
nowId = lastId.userId
lastId.userId = lastId.userId + 1
lastId.put()
return nowId
@ndb.transactionalを指定すると以下に続く処理が排他的に行...
+UserIdCounterの取得
+UserIdCounterの加算
+UserIdCounterの取得
+UserIdCounterの加算
となればもちろん問題ないのですが、
+UserIdCounterの取得
+UserIdCounterの取得
+UserIdCounterの加算
+UserIdCounterの加算
となると同じ値をとって最後に2加算することになり、同じ値...
データモデルは初期化の時にidというパラメータを指定するこ...
*ユーザ-登録処理
上記ユーザーIDの生成処理を使うと、ユーザー登録処理は以...
class NjfEntry(webapp2.RequestHandler):
def post(self):
self.response.headers['Content-Type'] = "text/pl...
userId = getUserId()
password = random.randint(1000,1000000)
userData = UserData(id = str(userId) + "_" + str...
userData.userId = userId
userData.password = password
userData.data = {}
userData.put()
returnData = {}
returnData["userId"] = userId
returnData["password"] = password
self.response.write(json.dumps(returnData))
パスワードは乱数にしています。桁数があまり小さいと偶然正...
ここでuserDataにユーザーIDとパスワードを連結した物をI...
戻り値はユーザーIDとパスワードをjsonで送っています。
*ログイン処理
ログイン処理ではユーザーIDとパスワードをチェックして正...
class NjfLogin(webapp2.RequestHandler):
def post(self):
self.response.headers['Content-Type'] = "text/pl...
userId = self.request.get('userId')
password = self.request.get('password')
userData = getUserData(userId, password)
if userData:
self.response.write(json.dumps(userData.data))
else:
self.response.write("-1")
ここでgetUserDataは以下のような、ユーザーIDとパスワード...
def getUserData(userId,password):
ud = UserData.get_by_id(id=userId+"_"+password)
return ud
*セーブ処理
最後に保存処理です。ユーザーIDとパスワードをチェックし...
class NjfSave(webapp2.RequestHandler):
def post(self):
self.response.headers['Content-Type'] = "text/pl...
userId = self.request.get('userId')
password = self.request.get('password')
data = self.request.get('data')
userData = getUserData(userId, password)
if userData:
jsonData = json.loads(data)
deepJsonCopy(jsonData, userData.data)
userData.put()
self.response.write("{}")
else:
self.response.write("-1")
ここでdeepJsonCopy関数は以下のようにjsonデータを再帰的に...
def deepJsonCopy(fromData,toData):
for key, value in fromData.iteritems():
if isinstance(value,dict):
if not toData.has_key(key):
toData[key] = {}
deepJsonCopy(fromData[key],toData[key])
else:
toData[key] = fromData[key]
pythonのjsonライブラリにはディープコピー関数があるのです...
この関数を使うことによって、必要なデータだけをサーバーに...
GAEでは転送量も課金対象になります。また、そうで無くてもや...
しかし、差分だけを送信するとクライアント側とサーバー側で...
毎回全てのデータを上書きするのならその心配はありません。...
以上でサーバーサイドは終わりです。次にクライアントサイド...
*クライアントサイド
Flash側では単にJSONデータをパースしたりダンプするだけであ...
送信データをダンプするには
var data:Object = new Object();
data["key"] = "value";
JSON.stringify(data);
受信データをパースするには
var jsonData:Object;
try
{
jsonData = JSON.parse(data);
}catch (e:Error) {
trace(e,data);
}
です。
オブジェクトが入れ子になっていても大丈夫です。
注意する必要があるのは、日本語を送るとGAE側でエラーになる...
*さらに改良するには
これで最低限のデータの保存と呼び出しが出来るようになりま...
**値のチェック
IDが数値かどうかやパスワードが数値かどうかなどはここでは...
**チート対策
もしランキングなどを実装するならチート対策が必須となりま...
クライアント側での対策は「[[ActionScriptでチート検出]]」...
これに加えてデータ送信時にも署名を行うなどしてデータの改...
たとえば、jsonデータを送信するときに、そのmd5ハッシュを同...
**2重登録チェック
前述のように、データを差分で送るとブラウザを2つあげてそ...
サーバー側でそれぞれの値の整合性チェックを行うと他の不具...
すると2つのクライアントから別々に保存しようとしても、後...
**パスワード暗号化
ここではパスワードを平文で保存していますが、個人情報など...
*費用
費用についてはゲームによってセーブの回数や転送量が違うの...
それ以上の場合で課金されるのは、ほぼ「Datastore Write Ope...
これは名前の通りデータを保存した回数で、レコード毎に課金...
次に課金されやすい項目は「Datastore Read Operations」です...
カジュアルゲームのデータ保存程度ではこれ以外ではほぼ課金...
よって、一日1万PV以上が一年以上続くような場合では月々定額...
*サンプルコード
ここで解説したサンプルコードはこちらです。クライアント側...
&ref(gaeWikiSample.zip);
ページ名: