2010/12/19 22時頃追記
『通信を手動で終了した場合に、謎のiframeが残ってしまい再接続ができない』
というGAE側の?バグが確認されました。
このページで公開されているソースは、このバグ対策用コードも含みます。
詳細はこのページをご覧になってください。
2010-12-19 - 風柳メモ「Google App Engine/PythonのChannel APIを使ってみた」
Google App Engine Channel APIのクライアント側のサンプルコードです。
サーバ側のコードはこのページに全部書いてあります。
『Channel APIを使うプログラムのサーバ側のコード』
上記のURLのソースコードで作ったChannel APIアプリケーションはこちらです。
ソースにコメントをたくさん書いておいたので、JavaScriptについてあまり詳しくなくても読めるはず?です。
『実際に動作しているChannel API アプリケーションページ』
元のページはこちらです。
『Google App Engine Channel APIの使い方』
サーバとクライアントのサンプルコードはそのままデプロイすればGoogle App Engine上で動きます。
もし分からないことがあったら、@ts_3156に何でもリプライして聞いてください。
<html> <head> <meta http-equiv="content-type" content="text/html; charset=utf-8"> <!-- headタグ内にこの行を入れる必要があるそうです --> <!-- 他のスクリプトは全てbodyタグ内に入れる必要があるそうです --> <script src='/_ah/channel/jsapi'></script> <title>Channel API テスト</title> </head> <body onload="initialize()"> <script type="text/javascript"> // --token--、--userId--の部分は、サーバがこのhtmlを出力する段階で置換されます var token = "--token--"; var userId = "--userId--"; var socket = ""; var isInitialized = false; var isConnected = false; var gettingToken = false; // 標準出力用の変数 var texts = new Array(10); for(var i = 0; i < texts.length; i++) texts[i] = ""; // 標準出力にtextを追加する function addText(text){ blankIndex = -1; for(var i = 0; i < texts.length; i++){ if(texts[i] == ""){ blankIndex = i; break; } } if(blankIndex == -1){ for(var i = 1; i < texts.length; i++) texts[i - 1] = texts[i]; texts[texts.length - 1] = ""; } for(var i = 0; i < texts.length; i++){ if(texts[i] == ""){ texts[i] = text; break; } } updateUI(); } //標準出力にtextを出力する function printText(text){ document.getElementById("stdOut").innerHTML = texts.join("\n"); } // 初期化です。最初にこの関数を実行する必要があります function initialize(){ if(isInitialized == true){ //alert("既に初期化済みです"); return; } if(isConnected == true){ //alert("既に接続済みです"); return; } channel = new goog.appengine.Channel(token); socket = channel.open(); socket.onopen = onOpened; socket.onmessage = onMessage; socket.onerror = onError; socket.onclose = onClose; isInitialized = true; addText("初期化が完了しました。serverからの応答待ちです"); } // ソケットを閉じた後に別のソケットを開く関数 function reOpen(){ if(isConnected == true){ //alert("既に接続済みです"); return; } if(isInitialized == true){ //alert("既に初期化済みです"); return; } if(gettingToken == true){ //alert("Token再取得を要求中です"); return; } getNextToken(); } // 一度閉じたソケットを再度開く時に呼ぶ function getNextToken(){ gettingToken = true; var http = new XMLHttpRequest(); http.open('POST', '/channel_api_test?getToken=true', true); //Tokenの受信時に呼ばれる関数 http.onreadystatechange = function(){ //4で受信完了 if (http.readyState == 4){ //Token再取得時のコールバック関数 comeToken(http); } } http.send(); addText("Token再取得要求を送信しました"); } // Tokenが送られてきた時に呼ばれる function comeToken(http){ var json = eval("(" + http.responseText + ")"); token = "" + json.token; userId = "" + json.userId; addText("serverからトークンを受け取りました"); gettingToken = false; initialize(); } // ソケットを閉じるときに呼ぶ関数 // 正常に閉じられてもサーバからの応答はなし // この関数を呼んだ時点で接続は終了したとみなす function closeSocket(){ if(socket == "" || isInitialized == false || isConnected == false){ addText("まだ接続されていません"); return; } socket.close(); socket = ""; isInitialized = false; isConnected = false; token = "--token--"; userId = "--token--"; bagu_taisaku(); addText("接続を終了しました"); } // [2010/12/19]通信を切断した場合に謎のiframeが残ってしまうバグ対策 // 詳細はこちら↓ // http://d.hatena.ne.jp/furyu-tei/20101219 function bagu_taisaku(){ var iframes = document.getElementsByTagName('iframe'), wcs_iframes = []; for (var ci = 0, len = iframes.length; ci < len; ci++) { var iframe = iframes[ci]; if (iframe.id == 'wcs-iframe' || iframe.name == 'wcs-iframe') wcs_iframes[wcs_iframes.length] = iframe; } for (var ci = 0,len = wcs_iframes.length; ci < len; ci++) { wcs_iframes[ci].parentNode.removeChild(wcs_iframes[ci]); } } // serverにmessageを送るときに呼ばれる関数 function sendMessage(path, message) { if(isInitialized == false){ reOpen(); } if(isInitialized == false){ addText("初期化が終わっていないのでデータを送信できません。数秒後に再度送信してください"); return; } if(isConnected == false){ addText("接続が完了していないのでデータを送信できません。数秒後に再度送信してください"); return; } if(token == ("--" + "token--") || userId == ("--" + "userId--")){ addText("tokenまたはuserIdが不正です"); return; } path += '?message=' + message + '&userId=' + userId + '&time=' + new Date().getTime(); var http = new XMLHttpRequest(); http.open('POST', path, true); http.send(); addText(message + "を送信しました"); } // serverからもらったデータを元にUIを更新する関数 function updateUI(){ printText(); } // onOpened、onMessage、onError、onCloseはcall back関数です。serverから呼ばれます // socketがmessageを受け取る準備ができたときに呼ばれる関数 function onOpened(){ addText("接続が開始されました。データを送信できます"); isConnected = true; } // socketがmessageを受け取ったときに呼ばれる関数 function onMessage(message){ var json = eval("(" + message.data + ")"); var receiveTime = (new Date().getTime() - 0) - (json.time - 0); var receiveTimeStr = receiveTime / 1000; addText("serverから「" + json.text + ", 受信: " + receiveTime + "ミリ秒」を受け取りました"); } // socketでエラーが起きたときに呼ばれる関数 // tokenは2時間で期限切れになります。期限切れになったtokenで接続しようとするとこの関数が呼ばれます // その時は、再度新しいtokenを取得して接続します function onError(error){ addText("serverでエラーが起きました。description=" + error.description + "。code=" + error.code); } // socketが閉じられたときに呼ばれる関数 // クライアントから閉じた時は呼ばれない // サーバから閉じられた時だけ呼ばれる function onClose(){ addText("接続が正常に終了しました"); isConnected = false; isInitialized = false; } </script> <div style="font-size: 12px;"> Channel APIを使ってserverにデータを送信します。<br> 接続の初期化は自動で行います。<br> serverからのデータは、call back関数を通して渡されます。<br> クライアント側でポーリングをする必要はありません。<br> ※時間がおかしいのは仕様です。 </div> <div> <textarea name="stdOut" id="stdOut" cols=80 rows=10 readonly> 標準出力です </textarea> </div> <input id="message" type="text" name="message" value=""> <input type="button" value="データを送信" onClick="sendMessage('/channel_api_test', document.getElementById('message').value)"> <input type="button" value="接続を終了" onClick="closeSocket()"> </body> </html>