Sunday, May 5, 2013

Google App Engine channel API in Java (push)

Google App Engine uses long polling as a push technology.

It uses two HTTP connections to the server. When the client has data to send to the server, it initiates an HTTP connection and posts the data to the server. The client also maintains a long-lived HTTP connection to the server that the server uses to return data back to the client. We refer to the first type of connection as the send channel and the second type of connection as the receive channel. Together these two unidirectional channels provide a bidirectional channel of communication between the browser and the server.

In this example we will use GAE push technology support (it however doesn't support full duplex communication, client send HTTP POST request on the server). It is kind of slow, but I think that it is sufficient for non-aggressive client state refresh.

I created some example on GAE, here is youtube video. In this example you can see update of four separate clients. Each client paints canvas with different color.

 

Also you can try it online here: http://codingwithpassion.appspot.com/dots
Delete cookies when you want to restart application.


There are several moving parts you need to orchestrate when developing push capable application. Client side should listen for changes and to update view. Second responsibility for client is off course to send messages to server when client state changes. It should look something like this:
//some demolib for this example
var demolib = {};  
demolib.sendMessage = function(path, opt_params) {      
    if (opt_params) {
      path += opt_params;
    }
    var xhr = new XMLHttpRequest();
    console.log("Posting: " + path)
    xhr.open('POST', path, true);
    xhr.send();
};    
demolib.writeCircle = function(xPos, yPos, color) {
    var canvas = document.getElementById('simpleCanvas');
    var context = canvas.getContext('2d');
    var radius = 10;          
    context.beginPath();
    context.arc(xPos-radius, yPos-radius, radius, 0, 2 * Math.PI, false);
    context.fillStyle = color;
    context.fill();
    context.lineWidth = 5;
    context.strokeStyle = '#003300';
    context.stroke();
};
demolib.onOpened = function() {
 demolib.sendMessage('/opened');
};
demolib.onMessage = function(m) {
 var newState = JSON.parse(m.data);
 if (newState.color != '${color}') {
  demolib.writeCircle(newState.x, newState.y, newState.color); 
 }     
};
demolib.openChannel = function() {
    var token = "${token}";
    var channel = new goog.appengine.Channel(token);
    var handler = {
      'onopen': demolib.onOpened,
      'onmessage': demolib.onMessage,
      'onerror': function() {},
      'onclose': function() {}
    };
    var socket = channel.open(handler);
    'onopen' = demolib.onOpened;
    socket.onmessage = demolib.onMessage;
};
demolib.init = function() {
    demolib.openChannel();
    var canvas = document.getElementById('simpleCanvas');     
    canvas.onclick = function(e) {      
        var centerX = e.pageX - canvas.offsetLeft;      
   var centerY = e.pageY - canvas.offsetTop;
   var token = "${token}";
   var color = "${color}";
   console.log(centerX + ' ' + centerY);
    
   demolib.writeCircle(centerX, centerY, color);
   demolib.sendMessage('/play', '?x='+centerX+'&y='+centerY+'&color='+color);
    };
    demolib.onMessage();
}();    
Then on server side, you need to create channel for each client. In this example we are using user session to manage clients. Only new clients from same IP are allowed (to differentiate between clients and to make this example simple as it can be).
public void doGet(HttpServletRequest req, HttpServletResponse resp)
 throws IOException {   
 try {
  HttpSession session = req.getSession();
  Integer colorIndex = (Integer)session.getAttribute(DotsServlet.COLOR_INDEX_ATTRIBUTE);
  colorIndex = colorIndex != null ? colorIndex + 1 : 0;
  if (colorIndex > Color.size() - 1) {
   noMoreClients(resp, Color.size());    
  } else {   
   session.setAttribute(DotsServlet.COLOR_INDEX_ATTRIBUTE, colorIndex);   
   Color color = Color.getColorByIndex(colorIndex);    
   String ipAddress = Game.getClientIpAddr(req);
   ChannelService channelService = ChannelServiceFactory.getChannelService();   
   String token = channelService.createChannel(color + ipAddress);
   req.setAttribute("token", token);
   req.setAttribute("color", color.toString());
   req.getRequestDispatcher("/jsp/htmlsocket/dots.jsp").forward(req, resp);
  }
 } catch (ServletException exc) {   
  exc.printStackTrace();
 }
}
After each message from client, you need to update each client. We are updating each client for current IP. Something like this:
for (int i = 0; i <= colorIndex; i++) {  
 String user = Color.getColorByIndex(i).toString();
 ChannelService channelService = ChannelServiceFactory.getChannelService();
 String ipAddress = Game.getClientIpAddr(request);
 String channelKey = user + ipAddress;
 channelService.sendMessage(new ChannelMessage(channelKey, getMessageString(x, y, color)));
}
That's basically it. You can also update each client when new client is connected using socket.onopen.

6 comments:

  1. Who ever has posted this, Have you tried watching video yourselves?

    ReplyDelete
  2. Hi, can you please leave the code in a repository? Thanks.

    ReplyDelete
  3. can u suplly the code(server + client)?

    ReplyDelete
  4. I don't know if I have it. Isn't this all you actually need?
    You can also look at google official docs:
    https://developers.google.com/appengine/docs/java/channel/

    ReplyDelete
  5. Hi, may I ask some question?
    1. What *.js should be included for the client?
    2. What are "Game", "ChannelService" in the server-side?
    3. Can I only depend on Google Application Engine? or Google Compute Engine also required (and must pay for this)?

    ReplyDelete