uoz 作業日記

様々な作業の記録を共有するブログです。

google app engine Channel APIを使って、今そのページを見てる人が何人いるか分かるアプリを作る

gaeのchannel apiの練習に、ソーシャルブラウザブラウザを作ってみる

http://www.synaesthesia.jp/googleAppEngine/channelAPI.php
http://libro.tuyano.com/index3?id=921002


基本は、iframeをつかったブラウザ内ブラウザ
ただし、このアプリを同時に見ている人数などが分かる機能をchanel apiを使って実装
チャット機能もつけよう


eclipsegoogle app engineのSKDとgoogleプラグインを入れて、新規webアプリケーションプロジェクトを作る

iframeを使ったブラウザ内ブラウザ部分をつくる


index.html
iframe要素と、そこに表示するページのURLをいれるURL欄、入力されたURLのページを表示する表示ボタンの3つをつくる。

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
  <head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
    <title>ブラウザ内ブラウザ</title>
  </head>

  <body>

    <h1>ブラウザ内ブラウザ</h1>

    <input type="text" placeholder="URL" id="urlInput" ></input>
     <input type="button" id="goToURLButton" value="go"></input>

     <br />

     <iframe id="webFrame" src="" height=480 width=600>
     この部分は iframe 対応のブラウザで見てください。
     </iframe>



     <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
     <script src="script/index.js"></script>
  </body>
</html>


index.js
ボタンが押されたときにちゃんとページを表示するようにする。
jqueryを使う。

$(document).ready(function(){
     $("#goToURLButton").click(function(){

          $("#webFrame").attr("src",$("#urlInput").val());
     });
});


これは動いた

サーバ側のChannelServiceのところを実装していく

次に、

VisitPageServlet.java
そのページに訪れたときサーブレット

http://libro.tuyano.com/index3?id=921002&page=2
ここを参考に

public void doGet(HttpServletRequest req, HttpServletResponse resp)
               throws IOException {



          req.setCharacterEncoding("utf-8");
          resp.setContentType("text/plain;charset=utf-8");

          ChannelService channelService = ChannelServiceFactory.getChannelService();
          String channelKey =  req.getParameter("channelKey");
          String token = channelService.createChannel(channelKey);
          resp.getWriter().println(token);


     }

     public void doPost(HttpServletRequest req, HttpServletResponse resp){
          ChannelService channelService = ChannelServiceFactory.getChannelService();
          String channelKey = req.getParameter("channelKey");

          String requestURI = req.getParameter("url");

          if(urls.containsKey(requestURI)){
               urls.put(requestURI, urls.get(requestURI) + 1);
          }else{
               urls.put(requestURI, new Long(1));
          }

          channelService.sendMessage(new ChannelMessage(
               channelKey,
               urls.get(requestURI).toString() + " user(s) visit"
          ));

     }

javascriptも変更

var constants = {
          key:"test",
          token:"",
          path:"visit"
};

var socket = null;

$(document).ready(function(){

     //初期化
     init();

     //goボタンが押されたら
     $("#goToURLButton").click(function(){
          var targetUrl = $("#urlInput").val();
          $("#webFrame").attr("src", targetUrl);

          var xhr = new XMLHttpRequest();

          var url = targetUrl;
          xhr.open('POST',constants.path + '?channelKey=' +constants.key   + '&url=' + url, true);
          xhr.send();


     });
});



//初期化
function init(){
     //iframeにwebページを表示
     $("#webFrame").attr("src",$("#urlInput").val());

     //キーを送ってトークンを受け取る
     var xhr = new XMLHttpRequest();
     xhr.open('GET', constants.path + '?channelKey=' + constants.key , true);
     xhr.onreadystatechange = function(){
       if (xhr.readyState == 4 && xhr.status == 200){
         var token = xhr.responseText;
            constants.token = token;
            openChannel();
       }
     };

     xhr.send();
}

//ソケットを開く
function openChannel(){
     var channel = new goog.appengine.Channel( constants.token);
     socket = channel.open();

     //ソケット関連コールバック設定

     //ソケットを開いたら
     socket.onopen = function(){

     }

     //メッセージを受け取ったら
     socket.onmessage = function(resp){
          $("#result").append("<div>" + resp.data + "</div>");
     }


}

htmlには、以下を追加

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

ここまでやって気づいたが、ブラウザを複数開いて同じページにアクセスしたら、サーブレットは違うインスタンスになってるらしい。
これでは、あるページの訪問数をカウントできない。

とりあえず、あるページへのアクセス人数を数えるために、datastoreにデータを入れることにする。

Data storeはリレーショナルデータベースじゃないらしいけど..

参考
http://appengine.keicode.com/gae/datastore.php
http://techbooster.jpn.org/andriod/9667/
http://xawa99.blogspot.jp/2012/08/GAE-DataStore.html
http://www.intepro.co.jp/gaej/datastore/creatinggettinganddeletingdata.html
http://learntogoogleit.com/post/65411773811/counting-entities-in-the-app-engine-datastore

こっちは低レベルAPI
http://tech.topgate.co.jp/gijutsu-shiryou/datastore-lowlevelapi#TOC--6


いろいろ方法があるようだが、JDOを使おう
JDOのリファレンスと、低レベルAPIのリファレンスが混ざってて、非常に調べにくい。



最終的な形


VisitPageServlet

@SuppressWarnings("serial")
public class VisitPageServlet extends HttpServlet {

	private HashMap<String, Long> urls = new HashMap<String, Long>();

	public void doGet(HttpServletRequest req, HttpServletResponse resp)
			throws IOException {


		//チャンネル作る
		ChannelService channelService = ChannelServiceFactory.getChannelService();
		String channelKey =  req.getParameter("channelKey");
		String token = channelService.createChannel(channelKey);

		//ビジターを作る
		//Key key = KeyFactory.createKey(Visitor.class.getSimpleName(),"");
		Visit visit = new  Visit();
		//visitor.setKey(key);
		PersistenceManager pm = PMF.get().getPersistenceManager();

		try {
			// オブジェクトの格納
			pm.makePersistent(visit);
		} finally {
			pm.close();
		}

		//キーの取得
		Key key  = visit.getKey();
		String visitKey = Long.valueOf(key.getId()).toString();

		//返す値を作る
		HashMap<String, String> array = new HashMap<String,String>();
		array.put("token", token);
		array.put("visitKey", visitKey);
		String text = JSON.encode(array);

		req.setCharacterEncoding("utf-8");
		resp.setContentType("text/plain;charset=utf-8");
		resp.getWriter().println(text);


	}


	public void doPost(HttpServletRequest req, HttpServletResponse resp){
		ChannelService channelService = ChannelServiceFactory.getChannelService();
		String channelKey = req.getParameter("channelKey");

		String requestURI = req.getParameter("url");
		String action = req.getParameter("action");
		String visitKey = req.getParameter("visitKey");
		//Key key = KeyFactory.createKey(Visit.class.getSimpleName(), visitKey);

		PersistenceManager pm = PMF.get().getPersistenceManager();
		//DBからビジターを出す
		Visit visit = (Visit) pm.getObjectById(Visit.class,Long.valueOf(visitKey));

		if (action.equals("visit")){
			visit.setUrl(requestURI);

			//ビジターを数える
			javax.jdo.Query query = pm.newQuery(Visit.class);
			query.setFilter("url == '"+requestURI  + "'");
			query.setResult("count(this)");
			Long countResult = (Long) query.execute();

			//返す値を作る
			HashMap<String, String> array = new HashMap<String,String>();
			array.put("message", countResult.toString() + " user(s) visit" + requestURI);
			array.put("url", requestURI);
			String message = JSON.encode(array);

			channelService.sendMessage(new ChannelMessage(
					channelKey,
					message
				));
		}
		if ( action.equals("close")) {




			//ビジターを消す
			pm.deletePersistent(visit);

			//ビジターを数える
			javax.jdo.Query query = pm.newQuery(Visit.class);
			query.setFilter("url == '"+requestURI  + "'");
			query.setResult("count(this)");
			Long countResult = (Long) query.execute();


			//返す値を作る
			HashMap<String, String> array = new HashMap<String,String>();
			array.put("message", "a user leave from " + requestURI + " " + countResult + " remain(s).");
			array.put("url", requestURI);
			array.put("action", action);
			String message = JSON.encode(array);

			channelService.sendMessage(new ChannelMessage(
					channelKey,
					message
				));


		}



	}
}


訪問を表すデータのクラス

@PersistenceCapable
public class Visit {

	 @PrimaryKey
     @Persistent( valueStrategy = IdGeneratorStrategy.IDENTITY )
     private Key key;

	 @Persistent
     private String url;


	public Visit() {

    }


	public Key getKey() {
		return key;
	}


	public void setKey(Key key) {
		this.key = key;
	}


	public String getUrl() {
		return url;
	}


	public void setUrl(String url) {
		this.url = url;
	}
}


クライアントサイドのjavascript

var constants = {
		key:"test",
		token:"",
		path:"visit",
		visitKey:""
};

var targetUrl = "";
var socket = null;

$(document).ready(function(){

	//初期化
	init();

	//goボタンが押されたら
	$("#goToURLButton").click(function(){
		targetUrl = $("#urlInput").val();
		$("#webFrame").attr("src", targetUrl);

		var xhr = new XMLHttpRequest();

		var url = targetUrl;
		var path = constants.path + '?channelKey=' +constants.key   + '&url=' + url + '&action=' + "visit" + "&visitKey=" +  constants.visitKey
		xhr.open('POST',path, true);
		xhr.send();


	});

	$("#closeSocketButton").click(function(){
		closeSocket();
	});
});



//初期化
function init(){
	//iframeにwebページを表示
	$("#webFrame").attr("src",$("#urlInput").val());

	//キーを送ってトークンを受け取る
	var xhr = new XMLHttpRequest();
	xhr.open('GET', constants.path + '?channelKey=' + constants.key , true);
	xhr.onreadystatechange = function(){
	  if (xhr.readyState == 4 && xhr.status == 200){
		var obj = JSON.parse(xhr.responseText);
		constants.visitKey = obj.visitKey; //キーを受け取る
		constants.token =  obj.token;;
		openChannel();
	  }
	};

	xhr.send();
}

//ソケットを開く
function openChannel(){
	var channel = new goog.appengine.Channel( constants.token);
	socket = channel.open();

	//ソケット関連コールバック設定

	//ソケットを開いたら
	socket.onopen = function(){

	}

	//ソケットを閉じたら
	socket.onclose = function(){

	}

	//メッセージを受け取ったら
	socket.onmessage = function(resp){
		var obj = JSON.parse(resp.data);
		if (obj.url == targetUrl){ //同じURLを訪問中の人だけ
			$("#result").append("<div>" + obj.message + "</div>");

		}

	}


}

function closeSocket(){
	var xhr = new XMLHttpRequest();

	var url = targetUrl;
	var path = constants.path + '?channelKey=' +constants.key   + '&url=' + url + '&action=' + "close" + "&visitKey=" + constants.visitKey;
	xhr.open('POST',path, true);
	xhr.send();

	//$("#result").append("<div>closed</div>");
	//socket.close();
}

何とか、datastoreに見ているURLを入れて参照できるようになった。
地味に訪問データを消すボタンもつけた。
ホントはこれはブラウザを閉じたときにするべき処理。

今度は、違うページを見ている結果も表示されている。
つまり、チャンネルの表示側を絞ることができない。
チャンネル自体違うのを使えという話か。
キーを変えればいいんだ。

上では、強引にクライアントサイドで同じURLを見てる人の訪問情報以外をはじいてる。
でもこれじゃその分の通信が無駄になる。

これは今後の課題