Pages

Wednesday, June 26, 2013

Google AppEngine Channel API

I am not a big fan of page refresh for getting new data from server. AJAX has been the go to technology for these kind of requirements. However its unidirectional. For two way communication where server push is required , we have seen many technologies like comet etc. Websocket and WebRTC has been really cool technologies which helps sending data from server and client real time. These needs special server code for handling such requests. I have already worked on JSR 356 for websocket and glassfish reference implementation (tyrus) earlier. However in Google Appengine provides channel APIs for bidirectional communication. While client to server communication is still over HTTP GET or POST, sever creates a specific "channel" and enables itself to push data any time to specific clients. Under the hood , its actually the client which keeps polling with GET requests for new data to be sent by the server. In any case this API can be use fill in real time game servers.

The server side code

I added the following code to the example given in the previous post to use channel APIs

[code language="java"]
private void boradCastNotes(Request request,Note note) {
ServletContext context = hsr.getSession().getServletContext();
HashMap<String,ChannelPresence> liveUsers = (HashMap<String,ChannelPresence>)context.getAttribute("liveUsers");
if(liveUsers != null){
ChannelService channelService = ChannelServiceFactory.getChannelService();
ObjectMapper mapper = new ObjectMapper();
System.out.println("List of connected client ... ");
String noteStr = null;
try {
noteStr = mapper.writeValueAsString(note);
} catch (IOException e) {
e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates.
}
for(ChannelPresence cp : liveUsers.values()){
System.out.println(cp.clientId());
System.out.print(" Sending message to client --> " +noteStr);
String channelMessageStr="{"command":"note","data":"+noteStr+"}";
System.out.print(" Sending message to client after wrapping --> " +channelMessageStr);
channelService.sendMessage(new ChannelMessage(cp.clientId(),channelMessageStr));

}
}
}
[/code]

Client Code

In HTML I added an extra button which will send the user entered data using an AJAX request and some JavaScript code to create the channel using the token issues by the server earlier.

[code language="JavaScript"]
<script language="JavaScript">

//Function called when update button us pressed
//This will maken an AJAX POST request
//To the webservice created using sitebricks
postnote = function(){
var noteObj = new Object();
noteObj.text=document.forms[0]['note.text'].value;
sendMessage('/notes','POST',JSON.stringify(noteObj));
};
//Generic method for sending any Ajax request
sendMessage = function(path, method,param) {
var xhr = new XMLHttpRequest();
xhr.open(method, path, true);
//Callback when response is received from the server
xhr.onload = function () {
console.log(this.responseText);
document.forms[0]['note.text'].value="";
};
xhr.send(param);
};

onOpened = function() {
sendMessage('/notes','GET','command=open');
};

onMessage = function(message){
//When message is recived from the server
//Message.data contains the actual string send by the
//Java code
//Now convert the JSON string to a Javascript Object
var data = eval("(" + message.data + ")");
console.log("Received data from Server "+data)
//Adds a new note row in the tables
if(data.command=="note"){
insertRow(data.data)
}
}

insertRow = function(data){
var table=document.getElementById("noteTable");
var row=table.insertRow(1);
var cell1=row.insertCell(0);
var cell2=row.insertCell(1);
cell1.innerHTML=new Date(data.date);
cell2.innerHTML=data.text;
}

//Opens a channel with server with the given token (provided by the server)
//Internally it keeps polling the server for new messages
channel = new goog.appengine.Channel('${token}');
socket = channel.open();
//Define all the listeners
socket.onopen = onOpened;
socket.onmessage = onMessage;
socket.onerror = onError;
socket.onclose = onClose;
</script>
[/code]

Other features

To track the clients which gets connected to the server , listeners can be added to a specific urls.I track the clients to broadcast the messages to all the connected clients.To enable tracking following needs to be added to appengine-web.xml

[code language="XML"]
<inbound-services>
<service>channel_presence</service>
</inbound-services>
[/code]

Then write POST endpoint handlers

[code language="Java"]
public class TrackerServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
ChannelService channelService = ChannelServiceFactory.getChannelService();
ChannelPresence presence = channelService.parsePresence(req);
System.out.print("Client trying to connect with ID " + presence.clientId());
//Save the new client in servlet context
ServletContext context = getServletContext();
//Object liveUsers = context.getAttribute("liveUsers");
HashMap<String, ChannelPresence> liveUsers = (HashMap<String, ChannelPresence>) context.getAttribute("liveUsers");
if (null == liveUsers) {
System.out.println("Initialising client list");
liveUsers = new HashMap<String, ChannelPresence>();
context.setAttribute("liveUsers", liveUsers);
}
if(liveUsers.containsKey(presence.clientId())) {
System.out.println("Err.... this guy was already connected ! ");
} else {
liveUsers.put(presence.clientId(), presence);
System.out.println(" New client connected with ID " + presence.clientId());
}
}
[/code]


Similarly remove the client from servlet context when client gets disconnected.

[code language="Java"]
public class TrackerServlet1 extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
ChannelService channelService = ChannelServiceFactory.getChannelService();
ChannelPresence presence = channelService.parsePresence(req);
System.out.print("Client disconnected with ID " + presence.clientId());

ServletContext context = getServletContext();
HashMap<String, ChannelPresence> liveUsers = (HashMap<String, ChannelPresence>) context.getAttribute("liveUsers");
if (null != liveUsers) {
if (liveUsers.containsKey(presence.clientId())) {
liveUsers.remove(presence.clientId());
System.out.println("Client was disconnected");
} else {
System.out.println("Client was not connected");
}
} else {
System.out.println("No client was ever connected");
}
}
}
[/code]


Note: