Google App Engine (Java) ChannelService with AngularJS

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

Before we get into the detail a few words to the technology stack. On the server (GAE) side I’m using Java, for persistency I use Objectify since it comes with many nice features that go beyond JPA and JDO. Restlet together with Jackson create the REST endpoints for AngularJS. On the client side Twitter Bootstrap helps me to have a nice looking UI with little effort.

The token

The GAE channel service doesn’t allow you to broadcast a message easily to all clients. You will have to implement this by yourself by sending an update message to all currently connected clients. Client connections are represented by a secret token that is first created on the server side. Once the client knows this token both parties can send messages through the channel identified by the token. For this reason I created a model to store 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

On the client side this token has to be requested, ideally only once per session. So I created an angular service for it:
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.

Posted by Daniel Eichhorn

Daniel Eichhorn is a software engineer and an enthusiastic maker. He loves working on projects related to the Internet of Things, electronics, and embedded software. He owns two 3D printers: a Creality Ender 3 V2 and an Elegoo Mars 3. In 2018, he co-founded ThingPulse along with Marcel Stör. Together, they develop IoT hardware and distribute it to various locations around the world.

One comment

Leave a Reply