12. Advanced Topics

C# Client Compatibility

AMPS clients are available for many languages. Many AMPS customers write clients using a variety of languages, often both Java and C#. While Java and C# are fundamentally different languages, they share enough syntax that it can be straightforward to port code between the two, and especially from Java to C#.

To aid in conversion from Java to C# (and from C# to Java), the C# client has a number of features that make it a little easier to bring code from Java to C#:

  • getXXX()/setXXX() Java-style getters and setters are provided corresponding to properties on the Message class. For example, given a variable message of type Message, the code:

    string userName = message.UserName

    and

    string userName = message.getUserName()

    are equivalent.

  • C# Parameters that take lambda functions also take an interface type. The AMPS Java client defines interfaces such as ClientMessageHandler, which your application implements with a single invoke() method that is called when an event occurs. In C#, the AMPS client uses lambda functions and delegates to provide equivalent functionality. However, the same *Handler interfaces exist in C#, and instead of passing a lambda function, you may also implement these interfaces and pass in derived classes. While doing so would be inconvenient in C#, providing this symmetry allows your Java and C# to be ported interchangeably.

  • Java-style method name conventions are used throughout AMPS. In .NET, method names often begin with a capitalized first letter (e.g. Connect() instead of connect()). However, the C# AMPS client retains the capitalization style of the Java client where possible, making porting straightforward.

Rebuilding the Client

In the rare occasion that you need to make customizations to the AMPS Java Client, the installation packages include the full source code for the AMPS client. Rebuilding the client is straightforward using the provided build.xml, and the Apache ant program. The build.xml file is located in the api/client/java/ directory of the root of your AMPS installation. For example, if AMPS is installed in /opt/AMPS/, then the Java client’s build.xml would be located in the /opt/AMPS/api/client/java/ directory.

Assuming that a JDK version 1.6 or greater is installed and the Apache ant package has been installed, rebuilding the client is as simple as typing:

ant

from the command line in the same directory where the build.xml file is located in the AMPS install. Upon successful completion, the libraries will be located the api/client/java/dist/lib directory.

Transport Filtering

The AMPS Java client offers the ability to filter incoming and outgoing messages in the format they are sent and received on the network. This allows you to inspect or modify outgoing messages before they are sent to the network, and incoming messages as they arrive from the network. To create a transport filter, you implement the interface TransportFilter, construct an instance of the filter class, and install the filter with the setTransportFilter method on the transport.

The AMPS Java client does not validate any changes made by the transport filter. This interface is most useful for application debugging or transport development.

The client includes a sample filter, TransportTraceFilter, that simply writes incoming and outgoing buffers to an OutputStream.

Notice that the transport filter function is called with the verbatim contents of data received from AMPS. This means that, for incoming data, the function may not be called precisely on message boundaries, and that the binary length encoding used by the client and server will be presented to the transport filter.

Advanced Memory Management Techniques

One of the most important features of the Java language and runtime is automatic memory management. One drawback of this approach is that garbage collection can introduce latency and provide an inconsistent response time for the application.

The AMPS Java client is designed to allow you to build an application that requires no memory allocation to process and consume messages during steady-state processing (that is, once the client is connected and messages are flowing to the application).

Use the following techniques to avoid memory allocation in your application:

Publish from a Byte Buffer

The publish() and delta_publish() methods provide overloads that accept a byte array, starting position in the array, and a length rather than a String. These methods directly copy the message bytes provided into the buffer that the transport uses to send the message, without creating an intermediate copy or temporary object.

Avoid Publisher Contention or Use a synchronized Wrapper

The Java ReentrantLock used to protect publishes can result in memory allocation if the lock is contended. To avoid this allocation, either avoid publishing messages using a single instance of the Client from more than one thread at the same time, or wrap calls that publish messages in a method that is marked synchronized to avoid contention in the Client.

Use Asynchronous Message Processing

When a subscription uses asynchronous message processing, the Message that is provided to the MessageHandler is allocated once, per client. The contents of the message are references to an underlying buffer that is reused for each message

Notice that synchronous message processing (the MessageStream interface) allocates a full copy of each message received, and should not be used in environments where minimal memory allocation is a goal.

Use the Raw Versions of Accessors

When retrieving information from an instance of the Message class, use the versions of accessors labeled with Raw. These methods do not allocate memory: instead, they return the Field objects within the Message. These objects, in turn, are references to an underlying buffer. For Message objects provided to a MessageHandler, that underlying buffer is the buffer that the Client uses for reading from the socket.

To Process on Another Thread, Use an Object Pool and Fixed-Size Buffer

Many applications designed with low-latency in mind can perform the processing that they need within the MessageHandler. In many other cases, though, it is necessary for one or more worker threads to process requests.

If your application needs to follow this pattern, three best practices apply:

  1. Create an object that holds the exact data necessary, without requiring allocation.
  2. Use an object pool to manage instances of that object and reuse those instances as necessary.
  3. Use a fixed-size data structure (such as a ring buffer) to pass references to those objects between the receive thread and the processing thread.

Consider Message Type and Parser Implementations

When designing a system that will minimize allocation, it is also important to consider the message type and parser that the system will use. Some popular parsers are not designed with minimal allocation in mind, and those parsers are not a good match for systems that need to control allocation.

The 60East BFlat parser implementation is an example of a parser that is designed to allow an application to consume messages with minimal memory allocation. The parser can operate on a buffer, and the consumer can optionally reuse a single BFlatValue object that is owned by the parser, no matter how many fields are produced or how many messages are parsed.

Working with Messages & Byte Buffers

The AMPS Java client allows you to publish messages that contain data from byte buffers. When working with byte buffers in AMPS, it’s best to follow these simple conventions.

AMPS provides overloaded publish() methods that allow you to publish messages from various formats. In this case, to publish a message using byte buffers, the message data must be provided as a byte[]. Along with the message data, the message topic, to which the message will be published to, must also be provided as a byte[].

The example below shows how to serialize an object into a byte buffer, then publish the message to AMPS using the publish() method.

...

// create the topic string
String topic = "messages";

// create the object data
Employee emp = new Employee();

// create the field for the payload of the message
Field data = new Field();

try {
    // create streams used to serialize the object and
    ByteArrayOutputStream bStream = new ByteArrayOutputStream();
    ObjectOutputStream oStream = new ObjectOutputStream(bStream);

    // serialize the object to the ObjectOutputStream
    oStream.writeObject(emp);

    // set the byte of the message to a field
    data.set(bStream.toByteArray());
}
catch (Exception e) {
    e.printStackTrace();
}

//publish to the "messages" topic using byte buffers
client.publish(topic.getBytes(), 0, topic.getBytes().length, data.buffer, 0, data.buffer.length);

...

In addition to publishing messages, AMPS allows you to access the raw bytes in the data part of a message. The method getDataRaw() returns a Field that is composed of a byte buffer, position of the data in the buffer, and the length of the data. This data can then be deserialized and converted to an object for further use.

The example below shows how to access the raw bytes of a message, and then shows how to deserialize the bytes of that message to a Java object.

   ...

   // access the message raw data
   Field data = message.getDataRaw();

   // construct a ByteArrayInputStream object,
   // an ObjectInputStream object and a Field
   // to deserialize the data
   ByteArrayInputStream bais;
   ObjectInputStream ois;
   Field data;

   // construct the payload object
   Employee emp;

   ByteArrayInputStream
   try {
       // deserialize the data using ObjectInputStream to an object
       bais = new ByteArrayInputStream(data.buffer, data.position, data.buffer.length);
       ois = new ObjectInputStream(bais);
       emp = (Employee) ois.readObject();
   }
   catch(Exception e) {

   ...

}

Providing SSL Certificates to the AMPS Java Client

The AMPS Java client includes support for Secure Sockets Layer (SSL) connections to AMPS. The client automatically attempts to make an SSL connection when the transport in the connection string is set to tcps, as described in Chapter 3.

There is no other change required to application code to use an SSL connection. However, to successfully make a SSL connection to AMPS, the Java runtime requires:

  • A key to provide for the client connection, as stored in the keystore.

  • A trust store to use to validate the server certificate used for the connection.

    Most often, this means that the server certificate for the connection must be signed by a trusted certificate, and that trusted certificate must be in either the default trust store, or a trust store provided to the JVM at runtime. This can also be accomplished by importing the server certificate into the default trust store or a trust store set by the application at run time.

The parameters that you need to provide depend on how you have configured your certificates. If the server certificate is in the default trust store, you only need to provide a key for the client connection. Otherwise, you need to provide both a client key and a trust store that can validate the server certificate.

There are three common methods of providing these certificates:

  • Setting global parameters on the command line
  • Setting global system properties
  • Creating an SSL context object for a specific connection

These options are discussed in more detail below. In general, the first two methods are easiest for simple testing, and allow you to easily set the same options for all connections from the application. The third method is the most flexible, and lets an application use different options for different connections.

Java Virtual Machine implementations may differ somewhat in the implementation details for javax.net.ssl. For full details on providing certificates to the Java runtime, see the documentation for your JVM implementation. For example, documentation on the Oracle JVM implementation of javax.net is available at https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/JSSERefGuide.html Java Secure Socket Extension (JSSE) Reference Guide, and this section focuses on the Oracle JVM.

Setting Global Keystore and Truststore on the Command Line

For example, if you have a key installed into a keystore file and the server certificate is present in the default truststore or signed by a certificate in the default truststore, you might provide the client key to an application using the following command-line flags:

-Djavax.net.ssl.keyStore=<path to keystore file> \
-Djavax.net.ssl.keyStorePassword=<password>

If the server certificate is in a different truststore, you might provide the following set of flags to specify both the keystore and the truststore to use:

-Djavax.net.ssl.trustStore=<path to truststore> \
-Djavax.net.ssl.trustStorePassword=<truststore password> \
-Djavax.net.ssl.keyStore=<path to keystore file> \
-Djavax.net.ssl.keyStorePassword=<keystore password>

When you provide passwords on the command line, it is possible for other users on the system to see the password. This method is most often used during development and debugging.

Setting Global Keystore and Truststore Within the Application

Within an application, you can use the System.setProperty method to set flags before creating a Client or HAClient. For example, if you have a key installed into a keystore file and the server certificate is present in the default truststore, you might set these properties when your application starts.

System.setProperty("javax.net.ssl.keyStore", "path to keystore file");
System.setProperty("javax.net.ssl.keyStorePassword", "keystore password");

This is equivalent to the first set of command line arguments above. As with the command line arguments above, you can use this method to set the truststore arguments as necessary.

System.setProperty("javax.net.ssl.keyStore", "path to keystore file");
System.setProperty("javax.net.ssl.keyStorePassword", "keystore password");
System.setProperty("javax.net.ssl.trustStore", "path to truststore file");
System.setProperty("javax.net.ssl.trustStorePassword", "truststore password");

Creating an SSL Context

Setting options on the command line or with the System.setProperty method affects every connection made from the application. You can also create your own SSLContext for the application to use for the tcps transport. This method provides the most flexibility, and allows you to use different information for different connections.

The snippet below has the same end result as the first command line above for the AMPS connection (that is, it sets a keystore and uses the default trust store). However, because this SSLContext will only be used by the AMPS client, you can use a keystore and certificates that are only used for AMPS connections, rather than for the application as a whole. This may be required if the application needs to use SSL to connect to a different external system, and the keys or trust store used for AMPS are different than the keys or trust store for the other system.

// Snippet showing how to provide a keystore to the
// TCPSTransport for use of the AMPS Java client.

// Create a keystore. Assume that the keystore
// is in JKS format.

KeyStore ks = KeyStore.getInstance("JKS");

String password = ... ; // Obtain password for keystore

FileInputStream fis = null;
try {
    fis = new java.io.FileInputStream(<path to keystore file>);
    ks.load(fis, password.toCharArray());
}
finally {
   if (fis !=null ) fis.close();
}

// Get the key manager factory, using the default
// algorithm.
KeyManagerFactory kmf =
    KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());

// Initialize the factory with the keystore.
kmf.init(ks, password.toCharArray());

// Get the SSL context
SSLContext context = SSLContext.getInstance("TLS");

// Use the key manager just constructed, with defaults
// for the trust manager and randomness source.

context.init(kmf.getKeyManagers(), null, null);

// Set the SSLContext for the TCPS transport
// to the context just set up with the keystore.
TCPSTransport.setDefaultSSLContext(context);

Troubleshooting SSL Connectivity to AMPS

To troubleshoot SSL connectivity, run the application with the following flag:

-Djavax.net.debug=all

With this flag set, the javax.net library will produce detailed information about the connection, including extensive information designed to help identify and resolve certificate problems. In particular, this flag will produce information on the certificates that the application is using, whether the application can successfully load those certificates, and whether the application can validate the certificate provided by the AMPS server.