Custom Bridges Between RESTful Web Services and DDS

By Timothy Simpson, Object Computing Principal Software Engineer

October 2022


In a previous article, we explored the idea of mapping simple HTTP requests onto analogous operations for DDS instances in order to facilitate communication between RESTful web services and DDS systems. In this article, we will consider node-opendds, the Node.js binding for OpenDDS, as one potential technology for building such bridges between the web and DDS.

Scope

We will reuse the multiplayer game example from the previous article, but our focus for this article will primarily be on the Server application which will act as a bridge between REST and DDS. In doing so, we will showcase the flexibility of Node.js working alongside OpenDDS. In addition to discussing the Server application, we will also briefly describe a C++ OpenDDS Control application for coordinating information between multiple Servers, as well as mocking interactions from an Admin process. And lastly, we will briefly describe a lightweight HTML/Javascript Client web page for interacting with the Server.

Data Modeling

The multiplayer game example in question focuses largely on managing player connections between multiple Servers. For our player connection topics, we will reuse the IDL from the previous article.

PlayerConnection

@topic
struct PlayerConnection {
  @key string guid;
  string player_id;
  DDS:Time_t connected_since;
  string server_id;
};

A PlayerConnection object holds information about new or returning players attempting to connect to the game. When new players use their browsers to PUT a new playerConnection resource, the Server application will then write out PlayerConnection object on a Player Connections DDS topic. The Control application subscribes to this topic and checks the incoming updates against its internal state.

AugmentedPlayerConnection

@topic
struct AugmentedPlayerConnection {
  @key string guid;
  string player_id;
  DDS:Time_t connected_since;
  string server_id;
  boolean force_disconnect;
  float average_connection;
};

Assuming no issues are detected, the Control application then repeats the player's information within an AugmentedPlayerConnection object on a separate topic,Augmented Player Connections, which all Servers will monitor. Once a Server application receives a new (or returning) player's information back in an AugmentedPlayerConnection update, it is free to treat the player’s connection as fully established, potentially allowing access to other parts of the game state. The connection remains fully established until the player disconnects, the Server shuts down, or the player is forced to disconnect (e.g. by the Admin application).

Control (C++ OpenDDS)

The Control application, for the sake of this article, operates purely within the DDS environment. It serves as the primary point of coordination for game state, including active player connections. To do this, it subscribes to PlayerConnection updates on the Player Connections topic and then records those player connections in a single data store so that incoming updates may be compared against what’s already been received. If a player connection is attempted using an already-active GUID, the update is ignored as a duplicate. Otherwise, the player connection state is copied into a AugmentedPlayerConnection object and then shared on the Augmented Player Connections topic in order for all interested parties (all Servers) to be made aware of the updated list of player connections.

Client (HTML / Javascript)

The Client application, for the sake of this article, operates purely within the realm of the web and web services. It is a browser-based HTML and Javascript application. It uses HTTP requests in order to interact with resources made available by a Server application. In order to join the game, it sends an HTTP PUT request to /playerConnections/<player_name>/<player_id> (or a DELETE to leave) and it then monitors /augmentedPlayerConnection in order to determine which player connections are currently active in the game. The Client also displays the currently active player connections so players may know who else is connected. For the sake of convenience, the Server application serves up the Client application page (index.html) when requested by a browser.

Server (Node.js)

In the context of these two applications which live in different communication ecosystems, the Server application serves as a custom bridge using Node.js, facilitating communication between the RESTful web and DDS. In order to do this, it makes use of Express, a minimalist web framework for Node.js, and node-opendds, the Node.js binding for OpenDDS.

GameControlClient

Since this is a custom bridge between REST and DDS based on the example of a multi-player game, it makes sense to create a domain-specific wrapper for the Server application’s interactions with node-opendds. We’ll call this custom wrapper the GameControlClient, and we can examine the functionality it provides by considering the following methods it provides.

initializeDds

This method initializes OpenDDS via the node-opendds package and loads the domain-specific Game IDL TypeSupport library.

  this.factory = opendds.initialize.apply(null, argsArray);
  this.library = opendds.load(path.join(process.env.DEMO_ROOT, 'lib', 'Game_Idl'));

It then creates a DDS DomainParticipant, the PlayerConnection DDS Topic, and a DDS Publisher and DataWriter for that Topic.

  this.participant = this.factory.create_participant(CONTROL_DOMAIN_ID);
 
  var qos = {};
  this.playerConnectionWriter =
    this.participant.create_datawriter(
      'Player Connections',
      'Game::PlayerConnection',
      qos
    );

Minimal signal handling is set up to ensure a clean shutdown when the process is killed.

finalizeDds

This method is called on shutdown, for example when the Node.js process is killed via a signal (e.g. CTRL-C from the terminal). It deletes the DDS entities which were created by initializeDds and shuts down OpenDDS via the node-opendds package.

  if (this.factory) {
    if (this.participant) {
      this.factory.delete_participant(this.participant);
      delete this.participant
    }
    opendds.finalize(this.factory);
    delete this.factory;
  }
create_player_connection / remove_player_connection

These methods use the PlayerConnection DataWriter, created by initializeDds, to write or dispose a PlayerConnection sample on the Player Connections topic, respectively.

      this.playerConnectionWriter.write(player_connection_request); // or dispose()
subscribe_augmented_player_connections

This method creates, if it hasn’t already been created, an AugmentedPlayerConnection DataReader and sets up a DDS DataReaderListener to monitor updates. Two callbacks are provided to this method, one for normal updates and one for unregister or dispose messages.

GameControlClient.prototype.subscribe_augmented_player_connections = function(apc_received, apc_removed) {
  var qos = { durability: 'TRANSIENT_LOCAL_DURABILITY_QOS' };
  this.augmentedPlayerConnectionReader =
    this.participant.subscribe(
      'Augmented Player Connections',
      'Game::AugmentedPlayerConnection',
      qos,
      function(dr, sInfo, sample) {
        if (sInfo.valid_data) {
          apc_received(sample);
        } else if (sInfo.instance_state == 2 || sInfo.instance_state == 4) {
          apc_removed(sample.guid);
        }
      }
    );
};

Mapping HTTP to DDS

With our custom domain-specific DDS wrapper in place, we can now concentrate on the final mapping between HTTP requests and DDS operations. First, we set up express:

var app = require('express')();
var http = require('http').Server(app);
http.listen(PORT);

Next we set up our GameControlClient:

var gc_client = new GameControlClient();
var ddsArgs = process.argv.slice(process.argv.indexOf(__filename) + 1);
gc_client.initializeDds(ddsArgs);

Then we set up endpoints using express for both creating and removing Player Connections.

app.put('/playerConnection/:player_name/:player_id', function(req, res) {
  const {player_name, player_id} = req.params;
  var pc = {
    guid: player_id,
    player_id: player_name,
    connected_since: get_time(),
    server_id : server_id
  };
  let success = gc_client.create_player_connection(pc);
  res.status(success ? 200 : 404).end();
});
 
app.delete('/playerConnection/:player_name/:player_id', function(req, res) {
  const {player_name, player_id} = req.params;
  var pc = {
    guid: player_id,
    player_id: player_name,
    connected_since: get_time(),
    server_id : server_id
  };
  let success = gc_client.remove_player_connection(pc);
  res.status(success ? 200 : 404).end();
});

When we subscribe to AugmentedPlayerConnection updates, we pass in two functions which will be used to manage our local cache of Augmented Player Connections (i.e. apc_cache).

function add_apc(sample) {
  console.log('Adding APC:' + JSON.stringify(sample));
  apc_cache.set(sample.guid, sample);
}
 
function remove_apc(guid) {
  console.log('Removing APC for guid:' + JSON.stringify(guid));
  apc_cache.delete(guid);
}
 
gc_client.subscribe_augmented_player_connections(add_apc, remove_apc);

Finally, we may set up an endpoint for Clients to request the full contents of the local cache of Augmented Player Connections.

app.get('/augmentedPlayerConnection/', function(req, res){
  res.status(200).send(JSON.stringify(apc_cache.size ? Object.fromEntries(apc_cache) : {}));
});

Demo Repository

The current demo code is available at https://github.com/oci-labs/node-opendds-rest-demo. It requires OpenDDS version 3.22 or later, and also requires node-opendds version 0.2.1 or later. Instructions for building and running the demo can be found in the repository’s README file.

Potential Improvements

For the sake of this article, this demonstration of node-opendds capabilities is necessarily brief and focuses most of its attention on the mapping between HTTP requests and DDS operations. For real-world games, additional features and improvements would obviously be required (not least of which would be an actual game).

For example, any real online game would likely desire to make use of industry standard security practices to safeguard player information and accounts. We have explored how this is possible in a different article, and these ideas could potentially be applied to this node-opendds demo.

Additionally, we may wish to expand the types of communication supported between Clients. Player chat, for example, may benefit from use of different web technologies such as websockets for the ability to receive updates from the Server without needing to poll various exposed endpoints.

And lastly, as more types of game state are added to the Control application, different DDS QoS policies may be of benefit in order to best model the needs of the distributed system.

Conclusion

This demo shows that node-opendds is a powerful tool for creating custom bridges between RESTful web services and DDS applications. The specifics of each mapping can be tailored to the needs of the individual system using the wealth of packages available within the Node.js ecosystem. For more information on node-opendds, check out the repository on GitHub. For more information on OpenDDS, visit opendds.org.

Software Engineering Tech Trends (SETT) is a regular publication featuring emerging trends in software engineering.


secret