Motivation
I’m currently working on a foosball web app that is widely used in breaks where I work. While you track the goals made by each of the team the app running on the app engine calculates a ranking of all the players and awards badges given special scores in a game.
Now I wanted to update all the clients about goals made or if a a foosball table is currently occupied. Since I’m using AngularJS for the client my first cheap approach was to pull the information frequently. But as we all know, pulling doesn’t scale well for the server. In another project I had used the GAE ChannelService before, but there the client technology was GWT. So how would I get this to work for AngularJS? This posts describes the different components necessary and might help you achieving the same.
Technology
The token
@Entity @Cache public class BrowserClient { @Id private Long id; private String token; @Index private Date createdDate; //getter/setter omitted for saving space ... }
The createdDate is used to remove old (and most possibly outdated) client entries from the datastore. Now how the client gets this token? With AngularJS and Restlet an obvious choice is to create a REST endpoint resource:
public class NotificationTokenResource extends ServerResource { @Get(value = "json") public NotificationTokenDto execute() throws UnsupportedEncodingException { ChannelService channelService = ChannelServiceFactory
.getChannelService(); Random random = new Random(); String token = channelService
.createChannel(String.valueOf(random.nextLong())); BrowserClient client = new BrowserClient(); client.setToken(token); ofy().save().entities(client); NotificationTokenDto dto = new NotificationTokenDto(); dto.setToken(token); return dto; } }
So what happens here? First I get an instance of the channel service, then I have it create a token based on a random seed. In case your used is logged in you could also use the userId to create the token. This saves you the trouble to store the token in the datastore, since you always can recreate it on the server side based on the same userId. Then I store the token and fill it into a DTO to be returned as JSON to the client:
{"token":"AHRl[...]ntZ"}
Angular Notification Service
angular.module('gaeChannelService', ['ngResource']) .factory('NotificationToken', ['$resource', function($resource) { return $resource('/rest/notificationToken'); }]) .factory('NotificationService', ['$rootScope', 'NotificationToken', function($rootScope, NotificationToken){ var tokenObject = NotificationToken.get({}, function() { console.log("Token received"); channel = new goog.appengine.Channel(tokenObject.token); socket = channel.open(); socket.onopen = function() { console.log("Channel opened"); }; socket.onmessage = function(message) { console.log("Message received: " + message.data); var messageObject = angular.fromJson(message.data); $rootScope.$emit(messageObject.channel, messageObject); }; }); return { } }]);
First the REST endpoint for the token is defined and later injected into the notification service. In the callback method of the REST call the connection to the channel is opened and more callback methods are defined. The onmessage function will be called, when the server sends a new message. This message will be deserialized using the angular.fromJson method. Since all the messages contain a member called “channel” the message will be sent using the $emit function to all subscribers of that specific channel.
In the controller
So what do we have now? When the page is first loaded, we request a token from the server. The NotificationService uses this token to open a channel and listens for messages. Once a message is received it is relayed on the specified broadcast channel. So now we have to add listeners in a controller:
$rootScope.$on("UpdateOpenGames", function(event, message) { console.log("Received change in open games from server"); $scope.$apply(function() { $scope.games = []; angular.forEach(message.openGames, function(key, value) { $scope.games.push(new OpenGames(key)); }); }); });
This code listens for messages on the channel “UpdateOpenGames” for incoming messages. Once a message has been received the $scope.$apply makes sure, that everybody knows about changes of the current scope.
In the controller
Now what is left is to send a message from the server:
List clients = ofy().load().type(BrowserClient.class).list(); ChannelService channelService = ChannelServiceFactory.getChannelService(); ObjectMapper mapper = new ObjectMapper(); String payload = mapper.writeValueAsString(message); for (BrowserClient client : clients) { ChannelMessage channelMessage = new ChannelMessage(client.getToken(), payload); channelService.sendMessage(channelMessage); }
This code first loads all BrowserClient objects from the datastore. Remember, the BrowserClient class contains the token we need to send a message to a specific channel. Then we serialize the message we want to send into a JSON string, convert it into a ChannelMessage and send it. We doe this for all currently active clients. If by now a client is no longer online we won’t notice here. That’s it, folks!
Remarks
This is a pure broadcast and doesn’t take care much of security. If your application sends secrets over the channel you should be a bit more careful with the token. You have to treat it with care, since it can be used to impersonate your client from a possibly evil browser.
Great Article