A Quick Guide to writing JRMS applications

1. Introduction

JRMS stands for Java Reliable Multicast Service. It is a Java library for building reliable multicast applications. In this quick guide, we provide various tips to help you write an application using JRMS. We first walk through fragments from a real application to illustrate how to use some of the key objects in the JRMS package. JRMS has many auto-configuration features for ease of use; however, at times, it is necessary or desirable to resort to manual configuration. This is explained in the following section. Finally, we also explain the steps involved to add a new transport protocol into the JRMS framework.

2. Example application

2.1 Overview

This simple example application involves a sender transmitting data to one or more receivers. To help conceptualize, we list the logical steps the sender and receivers perform:

Sender Receiver
(S1) Create a transport profile

(S2) Create a channel and fill out channel information:

  • description
  • transport profile
  • start time
  • etc.
(S3) Advertise the channel

(S4) Create a multicast socket (with transport profile)

(S5) Send data

(S6) Close socket

(R1) Listen for channel advertisements

(R2) For each advertised channel, check if it is of interest

(R3) For the selected channel:

  • extract the transport profile
  • extract the start time
  • etc.
(R4) Create a multicast socket (with transport profile)

(R5) Receive data

(R6) If SessionDoneException detected, close socket

We refer to steps 1-4 as the set-up phase, and step 6 as the tear-down phase.

There are considerable details and variations: for example, what is in the transport profile? Is the data sent as a stream, or in logical blocks? Is data sent continuously (file transfer) or with interleaving gaps (publish and subscribe)? When does the sender stop advertising? We will try to cover some of these below.

2.2 Classes used

Using the JRMS API means importing and instantiating the JRMS classes. In the example application, some of the classes used are:
        import com.sun.multicast.allocation.MulticastAddressManager;

	import com.sun.multicast.reliable.channel.ChannelManagerFinder;
	import com.sun.multicast.reliable.channel.PrimaryChannelManager;
	import com.sun.multicast.reliable.channel.Channel;

	import com.sun.multicast.reliable.transport.TransportProfile;
	import com.sun.multicast.reliable.transport.tram.TRAMTransportProfile;
	import com.sun.multicast.reliable.transport.RMPacketSocket;

	import java.net.InetAddress;
	import java.net.DatagramPacket;
TRAM is used here as an example transport; JRMS supports multiple transports. Documentation on the com.sun.multicast classes can be found from the JRMS javadoc pages.


2.3 JRMS Javadoc Guide

com.sun.multicast.allocation multicast address management
com.sun.multicast.advertising advertising facilities
com.sun.multicast.reliable general JRMS exceptions
com.sun.multicast.reliable.channel channel interfaces, classes, and events
com.sun.multicast.reliable.sample_code documentation and source code for several sample applications
com.sun.multicast.reliable.simple basic objects for simple applications
com.sun.multicast.reliable.transport top-level transport interfaces
com.sun.multicast.reliable.transport.lrmp LRMP transport
com.sun.multicast.reliable.transport.tram TRAM transport
com.sun.multicast.reliable.transport.um Unreliable multicast transport
com.sun.multicast.util JRMS utilities


2.4 Creating a transport profile

The transport profile is a very important channel parameter. The transport profile dictates which transport is used for multicast, and contains various transport parameters, such as multicast address, maximum data rate and if data delivery is ordered. The following code fragment illustrates step (S1):
	private TransportProfile tp;
	private InetAddress addr;
	private int port;
        private Scope scope;
	
	// first, allocate one multicast address


        MulticastAddressManager mam = MulticastAddressManager.getMulticastAddressManager();
        if (scope == null) {
            scope = mam.getScopeList(IPv4AddressType.getAddressType()).findScopeForTTL(ttl);
            if (scope == null)
		throw new IOException("No scope for requested TTL");
        }

        /* get an address with a long duration */
        Lease lease = mam.allocateAddresses(null, scope, (int) ttl,
                    1, startTime, startTime, duration, -1, null);
        InetAddress addr = ((IPv4Address) lease.getAddresses().
                    getFirstAddress()).toInetAddress();

        // create a TRAM transport profile
	// other transports, such as UM, LRMP, can be used instead of TRAM	
	tp = new TRAMTransportProfile(addr,port);

	// set multicast session scope	
	tp.setTTL(ttl);

	// enable ordered delivery
	tp.setOrdered(true);

        // set maximum rate of data transfer
	tp.setMaximumSpeed(speed);

	// and set any other transport-specific parameters
	...

2.5 Creating a channel

The channel is created by first finding a primary channel manager and then using it to create a channel:
	// create a channel
	private PrimaryChannelManager pcm;
	private Channel channel;
	pcm = ChannelManagerFinder.getPrimaryChannelManager(null);
	channel = pcm.createChannel();
	channel.setChannelName(channelName);
	channel.setApplicationName(applicationName);
	channel.setTransportProfile(tp);
	channel.setAbstract(someBlurbAboutThisChannel);
	channel.setAdvertisingRequested(true);

	// plus any other channel parameters
	...

	// once enabled, configuration is complete and advertisement can start
	channel.setEnabled(true);
Note, the parameter passed to getPrimaryChannelManager is a reference to the primary channel manager. When null is specified, the local channel manager is returned. These code fragments illustrate steps (S2) and (S3). Note the lines setTransportProfile and setAdvertisingRequested; the former provides the transport mechanism, and the latter gets the channel advertised so that receivers can find out about it.

2.6 Receiver's set-up phase

At a receiver, the application uses the static ChannelManagerFinder to get a primary channel manager, which is then used to check advertised channels. The application then selects the ones it is interested in:
	// find the channel of interest
	private PrimaryChannelManager pcm;
	private Channel channel;
	pcm = ChannelManagerFinder.getPrimaryChannelManager(null);
	
	// channel lookup is by channelName and applicationName
	// since more than one may match, need to determine the right one
	long channelids[] = pcm.getChannelList(channelName, applicationName);
	
	// loop through the following to find the channel of interest
	channel = pcm.getChannel(channelids[i]);

	// apply application-specific criteria
	checkInterest(channel);
	...
In order to receive data, the application needs the transport profile:
	// extract the transport profile from the advertised channel
	tp = channel.getTransportProfile();
The transport profile that was originally created by the sender has been made part of the channel which through advertisement became known to the local channel manager. This explains how the receiver gets hold of the sender's transport profile. The above code fragments illustrated the receiver's set-up phase, steps (R1)-(R3).

2.7 Socket and data

In order to send and receive data, the sender and receiver both create sockets using the transport profile. This is what the sender does:
	private RMPacketSocket ms;
	private DatagramPacket sendPacket;
	byte[]	senddata = new byte[PACKET_SIZE];
	
	// create socket
	ms = channel.createRMPacketSocket(tp, TransportProfile.SENDER);
	// or directly using tp
	// ms = tp.createRMPacketSocket(TransportProfile.SENDER);

	// data transmission
	// repeat the following according to application needs
	preparePacket(senddata);
	sendPacket = new DatagramPacket(senddata, senddata.length);
	ms.send(sendPacket);
	...

	// teardown phase
	ms.close();

	// stop advertising this channel
	// this can be done earlier, e.g. after data transmission starts
	channel.setAdvertisingRequested(false);
Note there are a couple of ways to create a socket, either using the channel's createRMPacketSocket method, or using the transport profile's createRMPacketSocket method. Usually, the sockets are created using channel methods. Channels provide more services to applications, such as security and filtering mechanisms. A very simple application that does not need channel features can use methods of the transport profile to create sockets. This is what the receiver does:
	private RMPacketSocket ms;
	private DatagramPacket recvPacket;

	// create socket
	ms = channel.createRMPacketSocket(tp, TransportProfile.RECEIVER);

	// data reception
	// loop through the following until end of transmission recognized
	try {
		recvPacket = ms.receive();
		// application-specific processing
		consumePacket(recvPacket);
	} catch ( SessionDoneException e ) {
		// teardown phase
		ms.close();
	} catch ( JRMSException e) {
		System.out.println("...");
	}
Note, the use of exceptions to dispatch different return conditions is a very basic Java feature. For the sake of simplicity, we have not used exceptions in the example code fragments, except in the above case where we have illustrated the use of a couple of them. The JRMS API contains a set of common exceptions supported by its implementations.

2.8 Stream socket interface

It is also possible to use a stream socket interface. In that case, the sending and receiving are in terms of reading and writing to a stream interface. At the sender side:
	// create stream socket
	ms = channel.createRMStreamSocket(tp, TransportProfile.SENDER);
	// or directly
	// ms = tp.createMRStreamSocket(TransportProfile.SENDER);
	OutputStream s = ms.getOutputStream();

	// data transmission
	// use whatever stream write method that's convenient, e.g.
	s.write("hello");
	...
	// send
	s.flush();

	// teardown phase
	ms.close();
	channel.setAdvertisingRequested(false);
Equivalently, at the receiver:
	// create stream socket at receiver
	ms = channel.createRMStreamSocket(tp, TransportProfile.RECEIVER);
	// or directly
	// ms = tp.createMRStreamSocket(TransportProfile.RECEIVER);
	InputStream s = ms.getInputStream();

	// data reception
	// use whatever read method that's convenient
	byte message[] = new byte[SIZE];
	int len = s.read(message);
	...

	// teardown phase
	ms.close();


3. Operational Tips

JRMS provides many auto-configuration features. For example, information about a multicast session (channel) is broadcasted using SAP (Session Announcement Protocol). This means little or no configuration needs to be done to the receivers before the multicast session can begin. When using the TRAM protocol, the receivers automatically organize themselves into a repair tree to provide reliability service.

This section explains various manual configuration tips when it is desirable/necessary to override autoconfiguration.

3.1 Storing channel parameters in a file

The SAP message size has some limits, when exceeded they would not traverse beyond the local network. Sometimes, network managers do not want to allow SAP in their networks. In such situations, the channel can be created and stored in a file. Note, this is much easier than storing the channel parameters one by one into a file. The receiver application fetches the file somehow (e.g. via the web, or file sharing) and restores the channel object. The methods to file and read a channel are implemented as part of the LocalPCM object (which implements the PrimaryChannelManager interface). The created channel must be cast into a LocalChannel. LocalChannel is a serializable implementation of Channel (some strictly local parameters of LocalChannel are declared transient to make it serializable). The following code fragment illustrate how to store a channel in a file:
	// Get a LocalPCM
	LocalPCM m = 
          (LocalPCM) ChannelManagerFinder.getPrimaryChannelManager(null);
          
        // Create a serializable channel (LocalChannel) and fill in the
        // parameters, including the transport profile
	LocalChannel c = (LocalChannel) m.createChannel();
	c.setChannelName(channelName);
	c.setApplicationName(applicationName); 
	InetAddress mcastAddress = InetAddress.getByName(address);
	tp = new TRAMTransportProfile(mcastAddress, port);
	tp.setMaxDataRate(speed);
	tp.setTTL(ttl);
	tp.setOrdered(true);    
	c.setTransportProfile(tp);
	c.setEnabled(true);
	
	// Use the LocalPCM fileChannel method to store the channel in
	// a file.  Here, the ChannelName is used as the file name.
	m.fileChannel(c, channelName);
And this is how you read it back:
	LocalPCM m =
	  (LocalPCM) ChannelManagerFinder.getChannelManager(null);
	LocalChannel c = (LocalChannel) cm.readChannel(channelFileName);
        TransportProfile tp = c.getTransportProfile();
For many situations, it is convenient to create a channel once and use it again and again. For example, your application distributes daily news continuously. The sender and receivers can be restarted at different times and it is not convenient to use SAP. Then a channel can be created and posted on a web page (or a well-known directory if file sharing is available). Both the sender and receivers read the channel from the same place. The side-effect, in this case, is that the different sessions will have the same session Id.

In the simple package, there is a SimpleChannel program that lets you create a simple channel with a few of the essential parameters that you can enter on the command line.

3.2 Static repair tree configuration

There is some limited capability for configurating the repair tree. If you follow the steps below, you can control which repair head a particular receiver uses.
  1. When you prepare the transport profile, add the following step:
      tp.setTreeFormationPreference(TRAMTRansportProfile.TREE_FORM_HAMTHA_STATIC_R) 
    This tells all the receivers that the session is using HAMTHA method for for tree formation, and reading a static file is allowed. (HAMTHA stands for "Head Advertisement before data starts and Member Triggered Head Advertisement after data started", which is the default method).

  2. For each receiver that needs to have a specific repair head, create a file, named jrmstree.cfg (in the application's directory), that contains the following line:
      [head's ip address] [ttl from head]   
    The ip address is in the a.b.c.d form, and the ttl is the number of hops (in terms of multicast) the receiver is away from the head (e.g. equal to 1 if both hosts are on the same LAN). If you allow a number of possible repair heads, you can include a number of lines in the file.

  3. For each receiver that must serve as a head, create a file, named jrmstree.cfg (in the application's directory), that contains the following line:
      [local host's ip address] 0  
    If this receiver must have a specific parent, then it must contain an additional line as described above.
This is often found useful when setting up a long-term service, or during testing.

4. Importing new transport protocols into JRMS

The following describes the procedure to add a new transport protocol into the JRMS environment.
  1. Select a package name for the transport protocol to be added. For example:
           com.mycompany.newprotocolName  
  2. Implement a transport profile class for the new transport. Note that the transport profile class implements the interface specified in TransportProfile.java.
  3. Depending on the protocol, implement any one or both of the following interfaces.
  4. Implement an RMStatistics interface (optional).
  5. Compile the package. Applications will have to import the new transport package to use it.

Transport Profile Variable Names
Here is a suggestion about variable names in the transport profile class. This class defines a set of parameters used by your transport, for example:
	Transport name and version
	Multicast Address
	Multicast Port
	Ordered Delivery Available
	MultiSender Support Available
	AuthenticationSecurity Specifications
	Maximum Data Rate
While the names of these parameters may be long externally, it is helpful to use shortened variable names internally. The reason is that these names get carried in the serialized form of the transport profile sent in channel advertisements. Using shorter variable names helps avoid exceeding the packet size limit imposed by the advertising protocol. The following is an example definition:
	private int mr = DEFAULT_MAX_RATE;
	public int getMaximumDataRate() { return mr; }
	public void setMaximumDataRate(int rate) { mr = rate; }