エイバースの中の人

アプリとWEBサービスを作っているABARSのBLOGです。

AppEngine

GoogleCloudStorageの公開アクセス設定

GoogleCloudStorageにアップロードしたファイルに誰でもURLでアクセスできるようにするためには、GoogleCloudStorageのBucketを公開アクセスに設定する必要があります。

公開アクセスの設定をするには、Bucketの権限設定でallUsersに読み取り権限を設定します。

公開アクセス


公開アクセスの設定を有効にすると、以下のようなURLでBucket内のファイルにアクセスすることができます。

https://storage.googleapis.com/[Bucket]/[File]

AppEngineでは32MB以上のファイルをアップロードできない

AppEngineではhttpリクエストのgetとpostが32MBまでに制限されます。32MBよりも大きなファイルをアップロードする場合は、Blobstoreを使用する必要があります。

Blobstore Python API Overview

DataStore版のBlobstoreは提供が終了しましたが、GoogleCloudStorage版のBlobstore APIはdeprecatedになっておらず、使用可能です。

Feature Deprecations

AppEngineで32MB以上のファイルをアップロードする方法がBlobstore以外存在しないため、今後も使えるのではないかと考えています。

GoogleAppEngine/GoでCloudStorageのファイルを配信する方法とハマりどころ

Blobstore APIを使用するとCloudStorageのバケットが自動で作成されるため、特にCloudStorageを意識せずに使用可能です。

AppEngineのdevserverでSearchIndexを保持する

AppEngineのdevserverでは、サーバを再起動するとFullTextSearchApiのSearchIndexが初期化されます。これを初期化されないようにするには、devserverのExtraFlagsにsearch_indexes_pathを与えます。

--search_indexes_path=/Users/abars/gae_datastore_tmp/tdnet.search


また、データストアの保存先フォルダを変えるのも、同様に、--datastore_pathを使用することもできます。

--datastore_path=/Users/abars/gae_datastore_tmp/tdnet.datastore

BigQueryのStreamingAPIで日付分割する

AppEngineからBigQueryにinsertする際、templateSuffixに日付を指定すると、TABLE_IDのSchemaを使用して、自動的にテーブルを日付分割することができます。

    SUFFIX_ID = "_"+datetime.datetime.now().strftime("%Y%m%d")
    body = { 'rows':[{'json':data_hash}] ,'ignoreUnknownValues':True ,'templateSuffix':SUFFIX_ID}
...
    response = bigquery.tabledata().insertAll(projectId=PROJECT_ID,
                                              datasetId=DATASET_ID,
                                              tableId=TABLE_ID,
                                              body=body).execute()

AppEngineのデータストアの自動バックアップ

AppEngineのデータストアのバックアップをcronで自動化する方法が公開されていました。 Scheduled Backupsでは、データストアをCloudStorageにバックアップします。

まず、バックアップ先のCloudStorageのバケットを作成します。

cloud_storage

次に、データストアの管理を有効にします。

datastore

AppEngineプロジェクトのcron.yamlにバックアップジョブを設定します。kindにはバックアップ対象のエンティティ名を、gs_bucket_nameに先ほど作成したバケットを登録します。

- description: backup
  url: /_ah/datastore_admin/backup.create?name=BackupToCloud&kind=Bookmark&filesystem=gs&gs_bucket_name=backup-tdnet-search
  schedule: every 12 hours
  target: ah-builtin-python-bundle

cronを実行すると、データストアの管理画面に表示されます。反映までに少し時間がかかります。

datastore_admin

Cloud Storage側にも反映されます。

bucket

今まで、データストアのバックアップは手動でぽちぽちするしかなかったのですが、自動化できるようになって便利です。

少人数でソーシャルゲームを作るツールチェイン

フェイスブックが買収したWhatsAppが社員数50人で4億5000万人に対してサービスを提供できたように、開発環境の進化とクラウド化によって、少人数でも大規模なアプリケーションが開発できるようになってきました。

今回は、エンジニア視点で、少人数で大規模なソーシャルゲームの開発を行うために最適なツールチェインを考えてみます。

Unity5


昔はゲームを作るときはゲームエンジンから作っていたものですが、Unityの登場によって、ゲームのコアだけを記述すればよくなりました。iOSとAndroidのマルチプラットフォーム化が必須な今、開発環境としてのWindowsとMacの対応も考えると、Unityを使わないという選択肢はない気がします。nGUIとIAPの対応で、外部プラグイン不要で完結するようになってきましたし、どうしても不足する機能は自分でプラグインを書けば良いという安心感もあります。また、最新のMAYAのfbxへの対応など、何もしなくてもゲームエンジンがメンテナンスされていくというのは、自社エンジンにはない魅力です。

Maya LT でローポリキャラクタモデリングに挑戦して Unity で動かしてみた

Unity Cloud Build


Unity Cloud Buildを使うと、リポジトリにpushするだけで、自動的にiOSとAndroidのアプリをビルドすることができます。これにより、物理的に離れた場所にいたとしても、チームメンバー全員がいつでも最新版で動作確認することができます。開発はPCだけで行えばよく、実機を有線で接続する必要もないので、単純な開発効率も上がります。iOSアプリをWindowsだけで開発できるというのは革命的です。

Unity Cloud Buildの使い方

Bitbucket


GitのPrivateリポジトリを無料で作ることができます。Unity Cloud Buildとの連携を考えると、リポジトリはクラウドに持った方が便利です。

Unity向け .gitignoreの設定について

Slack


メールだと定型句が必要ですが、チャットだと重要な点だけを書けるのでコミュニケーションの効率が上がります。コミュニケーションの手段としてのメールは今後、衰退していく気がします。ChatWork、HipChatとも比較しましたが、Slackが一番、アプリの出来がよくできていました。

Google Docs


仕様書やドキュメントなどは、複数人同時に編集できるGoogle Docsで管理すると便利です。日付付きのWordファイルやExcelファイルをメールで送る必要はありません。ゲームのリソースもGoogle Driveでやり取りするとスムーズです。

Google AppEngine


少人数で運営することを考えると、サーバ運営をしているリソースはありません。Google AppEngineはDataStoreなど、プログラム側にかなり強い制約がかかりますが、その制約によって、原理的にAppEngineで動けば必ずスケールすることが保証されます。また、マネージドサービスなため、脆弱性の発覚による依存ライブラリのバージョンアップなども不要です。すなわち、サーバの保守が不要になります。

唯一、国内での実績が乏しいのが採用のネックだったのですが、メルカリ アッテがAppEngineを採用したことで、その障壁もなくなりました。さらに、2016年9月には待望の東京リュージョンが開設され、遅延が減ります。証明書なしでSSLが使えるのも魅力です。ゲームサーバなら独自ドメインがいらないので手軽です。

ゲームサーバへのサーバレスアーキテクチャの適用は、今後のトレンドになるのではないでしょうか。

SSL の設定
サーバーレスアーキテクチャという技術分野についての簡単な調査

AppEngineでPDFのバイナリからテキストを抽出する

Google AppEngineでPDFからテキストを抽出するには、pdfminerを使うと便利です。pdfminerは、ピュアPythonで書かれた、PDF解析ライブラリです。

ライブラリのインポートは、GitHubのpdfminerのリポジトリからpdfminerフォルダをAppEngineのプロジェクトフォルダにコピーするだけです。以降、以下のようなコードでテキスト抽出を行うことができます。get_pdf_textの引数のcontentはPDFのBlobです。

from pdfminer.converter import TextConverter
from pdfminer.layout import LAParams
from pdfminer.pdfinterp import PDFResourceManager
from pdfminer.pdfinterp import PDFPageInterpreter
from pdfminer.pdfpage import PDFPage

from io import BytesIO
from StringIO import StringIO

def process_pdf(rsrcmgr, device, fp, pagenos=None, maxpages=0, password='', caching=True, check_extractable=True):
	interpreter = PDFPageInterpreter(rsrcmgr, device)
	for page in PDFPage.get_pages(fp, pagenos, maxpages=maxpages, password=password, caching=caching, check_extractable=check_extractable):
		interpreter.process_page(page)

def get_pdf_text(content):
	rsrcmgr = PDFResourceManager()
	retstr = StringIO()
	codec = 'utf-8'
	laparams = LAParams()
	device = TextConverter(rsrcmgr, retstr, codec=codec)

	input_io = BytesIO(content)
	try:
		process_pdf(rsrcmgr, device, input_io, None , 8)	#max 8 page
	except:
		return ""
	device.close()

	str = retstr.getvalue()
	retstr.close()

	self.text=db.Text(str, encoding="utf-8")

	return str

ただし、認識できなかった文字コードは(cid:xxx)のようなテキストが返ってくるので、見栄えが気になる場合は適当にgrepしてやる必要があります。また、process_pdfに大きなPDFを与えると、AppEngineのスモールインスタンスではメモリが不足するので、ページ数は適当に制約をかける必要があるかと思います。

他、ピュアPythonのPDF解析ライブラリとして、pypdf2もありますが、日本語のPDFがうまく解析できなかったので、pdfminerの方がオススメです。

AppEngineのSearchAPIで検索できない単語がある場合の対策

AppEngineのSearch APIでは、一般的な単語が含まれる場合は適切に検索できません。例えば、「三井倉庫」で検索しようとした場合、「倉庫」を含むだけで検索結果に出現してしまいます。

この問題の対策としては、三井倉庫ではなく、”三井倉庫”で検索する必要があります。しかし、一般のユーザに、意識的に””を入力してもらうのは難しいのが現状です。

そこで、AppEngineのSearch APIに投げる前に、クエリを加工するのがよいと考えています。

		query=query.replace(" "," ")
		query_list=query.split(" ")
		query_actual=""
		for one_query in query_list:
			if(one_query!="OR" and one_query!="AND" and one_query!="NOT"):
				if(not re.match(".*[ :><=\"].*",one_query)):
					one_query="\""+one_query+"\""
			if(query_actual!=""):
				query_actual+=" "
			query_actual+=one_query

		try:
			query=search.Query(
				query_string=query_actual,
				options=options,)
		except:
			return "query error"

上記コードでは、ORやANDやNOT、title:などの制御系クエリでない場合に、自動的に””を付加します。また、全角スペースを半角スペース(OR)に置換します。これで、普通のGoogle検索のように使うことができます。

iOS9とAppEngineでjsの読み込みに30秒かかる

iOS9に上げると、AppEngineのjsのGETリクエストのレイテンシが30秒になってしまいました。

ワークアラウンドとしては、以下のように、jsのドメインを別のドメインにする必要があります。独自ドメインを使用している場合は、規定で割り当てられる、appspot.comを流用できるかと思います。

変更前
script src="/javascript/categoryScript.js">

変更後
script src="yourid.appspot.com/javascript/categoryScript.js">


モバイルSafariの通信を、Safariでプロファイルすると、以下のようにレイテンシが30secになっています。

masonry_latency

同じ通信をAppEngineサイドから。最初のGETリクエストはAppEngineのログには残っておらず、30sec後の最後のリクエストだけが残っています。成功したリクエストはHTTP1.1ですね。

masonry2

最初は、iOS9から対応したHTTP2で、AppEngineとプロトコルの齟齬が発生しているんじゃないかなと推測していたのですが、AppEngineはhttpsでないとHTTP2で通信しないようです。HTTP2 and SPDY Indicatorでも、HTTP1.1となっていたので、HTTP2が問題ではなさそうです。

AppEngineでListPropertyを追加する場合の問題

SDK1.9.17のAppEngineのEntityにListPropertyを追加した場合、既存のEntityのfetchでBadValueErrorが発生します。Loading db.ListProperty() with AppEngine bulkloaderによると、これはAppEngineのバグとして報告されているようですが、まだ修正されていないようです。

この問題は、default_property=[]にしても解決しませんし、required=Falseに設定することもできません。この場合、問題を解決するために、プロパティを削除するか、既存のEntityを全て更新しなければなりません。

StringListPropertyも同様です。修正されるまで、ListPropertyは追加しないほうがよさそうです。

AppEngineでREMOTE_HOSTを取得する

Google App EngineではREMOTE_HOSTが取得できません。そのため、ユーザのIPアドレスは分かりますが、ホスト名は分かりません。また、Socket APIのgethostbyipは実装されていません。

そこで、別のサーバで取得したREMOTE_HOSTをJavaScript経由で受け渡すことを考えます。REMOTE_HOSTを取得するサーバは、App Engineとは別のドメインになるため、クロスドメインポリシーを気にする必要があります。

クロスドメインを超える簡単な方法は、JSONPを使用することです。JSONPは、サーバが返り値を含むJavaScriptを返すことで、ドメインを超えます。

サーバ側のスクリプトは以下のようになります。

<?php
header('content-type: application/json; charset=utf-8');
$hostname = gethostbyaddr($_SERVER['REMOTE_ADDR']);
print $_GET['callback'] . '("' . $hostname . '");';
?>

呼び出し側のスクリプトは以下のようになります。

<script type="application/javascript">
function get_host(host_name){
    alert(host_name); // alerts the host name
}
</script>
<script type="application/javascript" src="http://hoge/remote_host.php?callback=get_host"></script>

AppEngineからGoogle Analyticsにアクセスする

Core Reporting API


Google AnalyticsにはCore Reportion APIがあり、API経由でアクセス情報を取得することができます。これを使って、自分のサイトのアクセスランキングなどを作って、リアルタイムにサイトに表示することを考えます。


Api Clientによるアクセス


Core Reporting APIは、Googleの提供するapi clientを使用してアクセスすることができます。api clientは、oauth2による認証が必要です。実装例が、Google APIs Client Library for Python : Using Google App Engineに記載されているので、参考にします。

読んで頂ければ、用途によっていろいろな認証が定義されていることが分かります。最初に解説されているOAuth2DecoratorFromClientSecretsは、アクセスしているユーザで認証するため、今回のようにアクセスランキングを作るような用途には向きません。やりたいことは、server-to-serverのアクセスであり、AppAssertionCredentialsになります。

AppAssertionCredentialsは、Googleのサービスアカウントでログインする形になります。Analyticsのアカウント設定で、このサービスアカウントに表示権限を与えれば、目的は達成できます。

しかし、AppAssertionCredentialsは、開発用サーバでは動きません。開発用サーバでも動くようにするには、SignedJwtAssertionCredentialsを使う必要があります。


サービスアカウントの作成とAnalyticsへの追加


SignedJwtAssertionCredentialsは、Google Api Consoleで作成したサービスアカウントでAnalyticsにアクセスする形になります。認証は秘密鍵を使用します。

まず、Google Api ConsoleのAPIと認証のタブで、サービス アカウントを作成します。サービスアカウントを作成すると、秘密鍵を.p12形式でダウンロードすることができます。


service


AppEngineで使えるようにするために、.p12形式のファイルを、pem形式に変換します。

openssl pkcs12 -passin pass:notasecret -in privatekey.p12 -nocerts -passout pass:notasecret -out key.pem
openssl pkcs8 -nocrypt -in key.pem -passin pass:notasecret -topk8 -out privatekey.pem
rm key.pem


次に、Analyticsの設定で、サービスアカウントのメールアドレス(hoge@developer.gserviceaccount.com)に表示権限を与えます。


analytics


Pycryptoのインストール


SignedJwtAssertionCredentialsを使うにはpycryptoが必要ですので、app.yamlに定義します。

libraries:
- name: pycrypto
version: latest


本番環境はこれでOKですが、開発サーバは自前でpycryptoをinstallする必要があります。

sudo port install py27-crypto


コード例


SignedJwtAssertionCredentialsが使えるようになりました。

import httplib2

from google.appengine.api import memcache

from apiclient.errors import HttpError
from apiclient import discovery
from apiclient.discovery import build

from oauth2client.client import AccessTokenRefreshError
from oauth2client import appengine
from oauth2client import client
from oauth2client.client import SignedJwtAssertionCredentials

class AnalyticsGet():
def get(self,bbs_name):
KEY = "privatekey.pem"
SCOPES = [
'https://www.googleapis.com/auth/analytics',
'https://www.googleapis.com/auth/analytics.edit',
'https://www.googleapis.com/auth/analytics.manage.users',
'https://www.googleapis.com/auth/analytics.readonly',
]
SERVICE_ACCOUNT = "hoge@developer.gserviceaccount.com"
key = open(KEY).read()
credentials = SignedJwtAssertionCredentials(SERVICE_ACCOUNT,
key,
scope=SCOPES)
http = httplib2.Http()
httplib2.debuglevel = True
http = credentials.authorize(http)
service = build('analytics', 'v3', http=httplib2.Http(memcache))


後は、Hello Analyticsのサンプルプログラムの、get_first_profile_id以降を呼ぶような感じで、Analyticsのデータを取得することができます。

API制約

Google Analytics APIは、1日あたり、Analyticsのアカウント単位で50000アクセス、Profile IDにつき、10000アクセスまでに制約されます。Quota limit for google analytics APIによると、アカウント単位の制約は問い合わせで増加させることができるようですが、Profile IDの制約を増加させることはできないので、注意が必要です。ページランキングなどの場合は、動的に生成せず、一定間隔でcronなどでランキングを作成、表示するなどして、APIのアクセス数を減らす必要があります。

App Engineの検索APIで日付によるスコアリングを行う

Google App Engineの検索APIでは、SortExpressionを定義することができます。一番簡単な方法が、日付によるソートです。以下のコードで、新しい順に検索結果を取得することができます。

search.SortExpression(expression='date' direction=search.SortExpression.DESCENDING, default_value=0)


しかし、これだけだと、日付だけでソートされてしまうため、あまりユーザビリティが高くありません。ユーザからの評価の高いものや、ブックマーク数が多いものなどで重み付けをしたくなります。

expressionには、式が記述できるため、以下のようにすることで、任意のスコアで検索結果をソートすることができます。

search.SortExpression(expression='bookmark_cnt*5+like_cnt' ,
direction=search.SortExpression.DESCENDING, default_value=0)


しかし、古い投稿ほどブックマーク数は多くなりがちなので、まだ不十分です。新しい投稿ほど、先に表示しやすくしたくなります。しかし、dateプロパティはそのままだと式に使用できません。

そこで、日付をNumberFieldに変換して保存しておきます。

search.NumberField(name='sec', value=_get_sec(thread.create_date))


その上で、現在の日付で重み付けを行います。

now_sec=_get_sec(datetime.datetime.now())
reduct='(1+('+str(now_sec)+'-sec)/(3600*24*30))'; #一ヶ月で半分のスコアにする
search.SortExpression(expression='(bookmark_cnt*5+like_cnt)/'+reduct,
direction=search.SortExpression.DESCENDING, default_value=0)


これで、それなりにそれっぽい検索結果になります。

尚、デフォルトだと、検索条件を満たす1000件の範囲でのみソートが行われます。これだと、データ数が多くなると正常にソートできないため、SortOptionsのlimitプロパティを大きく設定することをオススメします。limit値は、最大で10000まで設定することができます。

Google App Engineが2014年4月1日から約30%の値下げ

Google App Engineの料金が2014年4月1日から値下げされることが発表されました。(新しい料金体系

App Engine pricing is drastically simplified. We've lowered pricing for instance-hours by 37.5%, dedicated memcache by 50% and Datastore writes by 33%. In addition, many services, including SNI SSL and PageSpeed are now offered to all applications at no extra cost.

従来の料金と比較してみました。

新料金旧料金値下げ幅
Instances $0.05 / instance / hour$0.08/Hour37.5%
Cloud Datastore
(NoSQL Database)
$0.06 / 100k read or write ops
Small operations free*
$0.18 / GB / month
write $0.90/Million Ops
read $0.60/Million Ops
small $0.10/Million Ops
storage $0.006/GByte-day
write 33.3%
read 0%
small 100%
storage 0%
Outgoing Network Traffic$0.12 / GB $0.12/GByte0%

ほとんどのアプリでは、インスタンスとデータストアが支配的なので、30%程度の値下げが期待できるかと思います。とても嬉しいです。また、small datastore operationがfreeになったので、Keys Onlyなオペレーションが捗ります。

気になるのはBackendsのFree Quotaの記載が無くなっていること。最近は、Modulesを推奨していることもあって、Backendsは非推奨の流れかもしれません。

AppEngineで画像を縮小すると右端に灰色の縦線が出る問題の回避法

AppEngineの開発環境では問題ないのですが、本番環境で画像を縮小した場合に、縮小率によっては、画像の右端と下端に灰色の縦線が出ることがあります。

問題の出るコードは以下です。

img = images.Image(image)
img.resize(width=w,height=h)
img.execute_transforms()

推測するに、バイリニアで縮小をかける際に、+1で画面外の画素を参照してしまっており、画面外の画素が黒となり、白との合成で灰色の線が出るのだと思います。

この問題を回避するには、+1のサイズの画像に縮小した後、cropで右端と下端を使用しない形に切り取ります。これによって、縦線が出ても、それを除去することができます。

img = images.Image(image)
margin=1
img.resize(width=w+margin,height=h+margin)
img.crop(0.0,0.0,1.0*w/(w+margin),1.0*h/(h+margin))
img.execute_transforms()

AppEngineでアルファ付きPNGをJPEG変換した場合に背景が黒くなる問題の対処法

AppEngineでサムネイルを作る場合に、元データにアルファが含まれている画像をJPEG変換した場合、背景を黒と仮定して合成されるため、真っ黒な画像が生成されます。

具体的に、次のようなコードの場合、背景は黒く塗りつぶされてしまいます。

jpeg=img.execute_transforms(output_encoding=images.JPEG)


背景を白と仮定して合成するには、次のように、compositeメソッドでJPEG変換を行います。compositeメソッドでは、第四引数に、背景色を指定することができます。

jpeg=images.composite([(img, 0, 0, 1.0, images.TOP_LEFT)], img.width, img.height, 0xffffffff, images.JPEG, 90)

AppEngineでLAN内の端末からデバッグする

AppEngineのdevserverではhttp://localhost:8084/にアクセスすることでデバッグをすることができます。iPad対応のサイトを作る場合は、実機でもテストできると便利です。何となく、http://マシンのIP:8084/で繋がりそうですが、実際は繋がりません。

LAN内の端末からアクセスできるようにする場合、GoogleAppEngineLauncherのApplicationSettingsのLaunchSettingsのExtraFlagsに、--host=0.0.0.0を追加する必要があります。

host

この設定をしておくと、http://マシンのIP:8084/、でアクセスできるようになります。

AppEngineのChannelAPIを使用して非同期通信を行う

サーバからクライアントにメッセージをPUSHしたい場合、一番簡単な実装はポーリングになります。クライアントが、1秒に1回など、サーバに更新が無いかを問い合わせることで、擬似的にPUSHを行うのです。

しかし、ポーリングで実装した場合、ラグを少なくするために、問い合わせの間隔を短くすると、クライアントとサーバの両方の負荷が大きくなります。そのため、ポーリング間隔は、ある程度大きく取る必要があり、ラグが大きくなってしまいます。

そこで、サーバとのセッションを維持し続けておくロングポーリングや、HTML5のWebSocketなどを使って、このラグを小さくすることが行われています。これらの非同期通信を、簡単に使えるようにライブラリ化したのが、GoogleAppEngineのChannelAPIです。

ChannelAPIは、サーバサイドのPythonのライブラリと、クライアントサイドのJavaScriptのライブラリがあり、サーバからクライアントへのPUSH通信が行えます。開発サーバはポーリングで実装されており、実サーバではロングポーリングもしくはWebSocketで実装されています。

ChannelAPIのサーバサイドは、何と2つのメソッドしかありません。まず、任意のユニークな64文字以下の文字列(クライアントID)から、create_channelでJavaScriptのAPIで使用するトークンを作ります。


from google.appengine.api import channel

client_id=str(user.user_id()) + "_"+ str(server_time)
token = channel.create_channel(client_id)


サーバからデータをPUSHする際の宛先にはクライアントIDを使用するので、作成したクライアントIDをDataStoreなどに保存しておきます。


room.channel_client_list.append(client_id)
room.channel_client_list_for_reconnect.append(client_id)


2つのStringListに格納しているのは、ConnectionとDisconnectionの管理のためで、後述します。

メッセージをクライアントにPUSHするには、channel.send_messageを使用します。引数は、クライアントIDとメッセージです。例外は起きません。データが確実に送信できたかも保証されません。


for client in room.channel_client_list:
 channel.send_message( client , message )


JavaScript側では、次のようにしてライブラリを読込みます。


<script src="/_ah/channel/jsapi"></script>


クライアントでは、サーバのcreate_channelで作成したトークンを引数に、Channelオブジェクトを作成します。Channelオブジェクトのopenにサーバからのメッセージを受け取るコールバック関数を指定します。


var channel = new goog.appengine.Channel( token );
var socket = channel.open({
onopen : function(){
 alert("サーバとのコネクションを確立しました。");
}
, onmessage : function(message) {
 alert("サーバから"+message.data+"を受信しました。");
}
, onerror : function(error) {
 alert("サーバとの接続でエラーが発生しました。リロードして下さい。"+error.description);
}
, onclose : function(){
 alert("サーバとの接続がクローズされました。リロードして下さい。");
}
});


クライアントからサーバにデータを送る方法は無いので、普通にHTTPでポストします。

サーバでは、新規クライアントからの接続と、切断で、POSTイベントが来ます。


application = webapp.WSGIApplication(
('/_ah/channel/connected/',ChatConnected),
('/_ah/channel/disconnected/',ChatDisconnected),


イベントのfrom引数にトークンに対応したクライアントIDが届きます。


class ChatConnected(webapp.RequestHandler):
 def post(self):
  client_id = self.request.get('from')

class ChatDisconnected(webapp.RequestHandler):
 def post(self):
  client_id = self.request.get('from')


チャットなどでは、現在ログインしているクライアントIDのリストを適当なStringListPropertyのchannel_client_listなどで管理しておき、/_ah/channel/disconnected/でchannel_client_listからクライアントIDを除外します。

ここで注意しないといけないのは、PCでは接続->切断まで一回しかイベントが来ませんが、iOSなどでは接続->切断->接続->切断などのように、同じクライアントIDから、定期的に再接続が来たりします。具体的に、MobileSafariでホームボタンを押すと切断、ブラウザを再び起動すると接続が来ますし、何もしてなくても一定時間で再接続が来たります。

そのため、チャットなどでは、現在ログインしているクライアントIDを管理するchannel_client_listの他に、一度でも接続しているクライアントIDのリストchannel_client_list_for_reconnectなども準備しておき、/_ah/channel/connected/が呼ばれた場合に、channel_client_list_for_reconnectに含まれる場合は、channel_client_listに再び追加する必要があります。


class ChatConnected(webapp.RequestHandler):
 def post(self):
  client_id = self.request.get('from')

  #再接続先のチャットルームを探す
  query=ChatRoom.all()
  query.filter("channel_client_list_for_reconnect =",client_id)
  room_list=query.fetch(offset=0,limit=100)

  #再接続
  for room in room_list:
   if(not(client_id in room.channel_client_list)):
    Chat.add_user(room.key(),client_id)


また、Disconnectが呼ばれないこともあるので、2時間経過したユーザは取り除くとかしておくとよいです。2時間というのは、ChannelAPIのトークンの有効期間です。また、切断された場合にJavaScript側にイベントが来ないこともあるので、POSTする際に、クライアントIDも送って、サーバのchannel_client_listに含まれていなかった場合はエラーを返すなどもした方がよいです。

ということで、ChannelAPIを使用して、HTML5で動くお絵かきチャットを作りました。iOSやAndroidでもChannelAPIが動くことを確認できました。



サーバ側のコードは以下に置いておきました。

Chat.py
ChatConnected.py
ChatDisconnected.py
ChatRoom.py

ChannelAPIは手軽に非同期通信できて便利です。リアルタイム系のゲームもGAEでいけそうですね。

参考文献:[GAE] [Python] Google App EngineのChannel APIを試してみた

AppEngineでIndexed=Falseを活用して課金額を下げる

AppEngineでdb.Modelを定義した場合に、Propertyには自動で昇降順のIndexが付きますが、これはQuotaのDatastore Writeに影響します。具体的に、オブジェクトをputした際、オブジェクトが書き込まれると同時に、Propertyに対応したIndexも更新され、そのIndexの更新に課金されます。このIndexは、DataStoreのfilterで使用されますが、検索に使用しないPropertyにもIndexが自動的に作成され、課金されています。

そこで、queryのfilterに使用しない項目については、次のようにpropertyにindexed=Falseを付けると、Indexが作成されなくなり、オブジェクトのput時のDatastoreWriteを抑制でき、課金額が下がります。(参考:PropartyにIndexを付けないことは課金チューニングになるが、そもそも課金処理おかしくないか?(おかしくなかった)


class Analyze(db.Model):
bbs_key =db.ReferenceProperty()
ip = db.StringProperty(indexed=False)
adr = db.StringListProperty(indexed=False)


ただし、indexed=Trueに再度変更した場合でも、インデックスの追加は次回のputからになるので、慎重にindexed=Falseを設定する必要があります。

AppEngineのPython2.7でPILを使う(MountainLion)

AppEngineで画像を扱うにはPIL(PythonInstallLibrary)を使用します。しかし、PILはデフォルトでインストールされていないので、自前でインストールする必要があります。

まず、MacPortをインストールします。次に、Xcodeをインストールして、Preference->DownloadsからCommandLineToolsをインストールします。

コマンドラインから次のコマンドを打って、PILをインストールします。

sudo port install PIL


この状態では、AppleのPython2.7(/usr/bin/python)と、MacPortのPython2.7(/opt/local/bin/python)が存在しています。AppEngineのデフォルトではAppleのPython2.7(/usr/bin/python)を使用するようになっているので、AppEngineLauncherのPreferenceのPythonPathを、/opt/local/bin/python2.7に変更し、MacPortのPILを含んだPython2.7を使用するようにします。

path


これで、PILが使えるようになります。
Search
Profile

abars

アプリとWEBサービスを開発しています。最近はUnityとGAE/pyが主戦場。

ブラウザ向けMMOのメトセライズデストラクタ、イラストSNSのイラストブック、東証の適時開示情報を検索できるTDnetSearchを開発しています。

かつてエンターブレインのTECH Win誌でATULADOを連載しました。

サイト:ABARS
Twitter:abars
Github:abars

Twitter
TopHatenar
HotEntry
Counter

アクセス解析付きカウンター。あなたのBLOGにもどうですか?登録はこちらから。

TOP/ BLOG/ LECTURE/ ONLINE/ RUINA/ ADDON/ THREAD/ METHUSELAYZE/ IPHONE/ MET_IPHONE/ ENGLISH/ RANKING