3. Your First AMPS Program

In this chapter, we will learn more about the structure and features of the AMPS C/C++ library, and build our first C/C++ program using AMPS.

Connecting to AMPS

Let’s begin by writing a simple program that connects to an AMPS server and sends a single message to a topic:

#include <ampsplusplus.hpp>
#include <iostream>

int main(void)
{
    const char* uri = "tcp://127.0.0.1:9007/amps/json";

    // Construct a client with the name "examplePublisher".

    AMPS::Client ampsClient("examplePublisher");

    try
    {
        // connect to the server and log on
        ampsClient.connect(uri);
        ampsClient.logon();

        // publish a JSON message
        ampsClient.publish("messages",
                           R"({ "message" : "Hello, World!" ,)"
                           R"(client" : 1 })");

    }
    catch (const AMPS::AMPSException&amp; e)
    {
        std::cerr << e.what() << std::endl;
        exit(1);
    }
    return 0;
}

Example 3.1: Connecting to AMPS

In the preceding 3.1, we show the entire program; but future examples will isolate one or more specific portions of the code. The next section describes how to build and run the application and explains the code in further detail.

Build and run

To build the program that you’ve created:

  • Create a new .cpp file and use your c compiler to build it, making sure the amps-c++-client/include directory is in your compiler’s include path
  • Link to the libamps.a or amps.lib static libraries.
  • Additionally, link to any operating system libraries required by AMPS; a full list may be found by examining the Makefile and project files in the samples directory.

If the message is published successfully, there is no output to the console. We will demonstrate how to create a subscriber to receive messages in Subscribing.

Examining the code:

Let us revisit the code we listed earlier:

/* These are the include files required for an AMPS C++ Client. The
 * first is <ampsplusplus.hpp>. This header includes everything needed to
 * compile C++ programs for AMPS. The next include is the Standard C++ Library
 * <iostream>, necessary due to use of std::cerr and std::endl
 */
#include <ampsplusplus.hpp>
#include <iostream>

int main()
{
    /* The URI to use to connect to AMPS. The URI consists of the transport,
     * the address, and the protocol to use for the AMPS connection. In this case,
     * the transport is tcp, the address is 127.0.0.1:9007, and the protocol is
     * amps. In this case, AMPS is configured to allow any message type on that
     * transport, so we specify json in the URI to let AMPS know which message
     * type this connection will use. Even though a transport that uses the
     * amps protocol can accept multiple message types, each connection must specify
     * the exact message type that connection will use. Check with the person who
     * manages the AMPS instance to get the connection string to use for your programs.
     */
    const char* uri = "tcp://127.0.0.1:9007/amps/json";


    /* This is where we first interact with AMPS by instantiating an AMPS::Client
     * object. Client is the class used to connect to and interact with an AMPS
     * server. We pass the string "exampleClient" as the clientName. This name
     * will be used to uniquely identify this client to the server. Errors relating
     * to this connection will be logged with reference to this name, and AMPS uses
     * this name to help detect duplicate messages. AMPS enforces uniqueness for
     * client names when a transaction log is configured, and it is good practice
     * to always use unique client names.
     */
    AMPS::Client ampsClient("exampleClient");

    /* Here we open a try block. AMPS C++ classes throw exceptions to indicate
     * errors. For the remainder of our interactions with AMPS, if an
     * error occurs, the exception thrown by AMPS will be caught and handled
     * in the exception handler below.
     */
    try
    {
        /* At this point, we establish a valid AMPS network connection and
         * can begin to use it to publish and subscribe to messages. In this
         * example, we use the URI specified earlier in the file. If any errors
         * occur while attempting to connect to AMPS, the connect() method will
         * throw an exception.
         */
        ampsClient.connect(uri);

        /* The AMPS logon() command connects to AMPS and creates a named
         * connection. If we had provided logon credentials in the URI,
         * the command would pass those credentials to AMPS. Without credentials, the
         * client logs on to AMPS anonymously. AMPS versions 5.0 and later
         * require a logon() command in the default configuration.
         */
        ampsClient.logon();

        /* publish a JSON message
         * Here, a single message is published to AMPS on the messages topic,
         * containing the data Hello world. This data is placed into an XML
         * message and sent to the server. Upon successful completion of this
         * function, the AMPS client has sent the message to the server, and
         * subscribers to the messages topic will receive this Hello world message.
         */
        ampsClient.publish("messages",
                           R"({ "message" : "Hello, World!" ,)"
                           R"( "client" : 1 })");
    }
    /* Error handling begins with the catch block. All exceptions thrown by
     * AMPS derive from AMPSException classes. More specific exceptions may
     * be caught to handle certain conditions, but catching
     * AMPSException&amp;</code> allows us to handle all AMPS errors in one
     * place. In this example, we print out the error to the console and exit
     * the program.
     */
    catch (const AMPS::AMPSException&amp; e)
    {
        std::cerr << e.what() << std::endl;
        exit(1);
    }

    /* At this point we return from main() and our ampsClient object falls
     * out of scope.  When this happens AMPS automatically disconnects from the
     * server and frees all of the client resources associated with the
     * connection.  In the AMPS C++ client, objects are reference-counted,
     * meaning that you can safely copy a client, for example, and destroy copies
     * of client without worrying about premature closure of the server connection
     * or memory leaks.
     */
    return 0;
}

Example 3.2: Connecting to AMPS

Using the C client

The AMPS C/C++ client is built in two layers: the C layer that exposes lower-level primitives for sending and receiving messages to AMPS, and the C++ layer providing a set of abstractions over the C layer that makes it easier to work with AMPS and create robust applications. The C++ layer is recommended for many applications, since it offers a good balance of performance, control, and ease of use. If you are integrating AMPS into existing C code, or need fine-grained control over how your application interacts with AMPS, then you may choose to use the C layer directly.

The C Client offers low-level functionality for working with AMPS. With the C client, your application is responsible for correctly assembling the parameters to each command to AMPS and interpreting the response from AMPS. The C client does not provide higher-level abstractions such as publish stores, automatic failover and reconnection, sequence number management for published messages, and so on. Instead, you build the capabilities that your application needs over the low level primitives.

As an example, Example 3.3 shows the previous sample rewritten to use the C layer directly:

#include <amps.h>

int main()
{
    /* At this point in the program, the necessary objects are declared
     * in order to permit interaction with AMPS. When AMPS errors occur,
     * their text is available through the amps_client_get_error() function,
     * so it is here that we will create a small char array to hold the errors.
     */
    char errorBuffer[256];

    /* Here an amps_handle is created for each object and message objects
     * that are constructed later. An amps_handle is an opaque handle to
     * an object constructed by AMPS, which cannot be dereferenced or used
     * by means other than AMPS functions. amps_handle is the size of a pointer and may be
     * passed by value wherever needed.
     */
    amps_handle message;
    amps_handle logon_command;
    amps_handle client;

    /* Next we declare an amps_result object, which is used to store the
     * return value from functions that may fail, such as during connection or
     * interaction with an AMPS server. Many AMPS functions return an
     * amps_results.
     */
    amps_result result;

    /* Here we construct our AMPS client with a unique name. This
     * function allocates resources that must be freed, and can only be
     * freed by a corresponding call to amps_client_destroy
     */
    client = amps_client_create("cClient");

    /* This is how a connection is established; control continues to
     * where the AMPS message is allocated.
     */
    result = amps_client_connect(client, "tcp://localhost:9007/amps/json");

    if (result != AMPS_E_OK) {
        amps_client_get_error(client, errorBuffer, sizeof(errorBuffer));
        printf("error %s\n", errorBuffer);
        amps_client_destroy(client);
        return 1;
    }

    /* AMPS applications communicate with the AMPS server by sending and
     * receiving messages. A logon command, like any other command, is
     * simply a message to AMPS. Instead of calling a function that assembles
     * and sends the logon command, we construct the command ourselves. When a
     * message is constructed, the AMPS C client allocates resources that must
     * be freed by a corresponding amps_message_destroy function.
     */
    logon_command = amps_message_create(client);

    /* When a message is created in the AMPS C client, the message contains no
     * information at all. To make the message a logon command, we need to set
     * the command type to "logon".
     *
     * The C client provides a number of functions to assist in interacting
     * with the data and fields of a message. In this example _nts functions
     * are used, which allow for quick population of messages fields and data
     * with C-style null-terminated strings.
     *
     * The next few lines add a minimal set of fields for the logon command.
     * See the AMPS Command Reference for the full set of header fields
     * supported.  For simplicity in this basic example, we set the smallest
     * number of fields possible. For example, this sample does not provide a
     * user name or password on the command, nor does the command request an
     * acknowledgment message. In this case, the application relies on the
     * fact that AMPS will disconnect the client if the logon command fails.
     * Production applications should register a message handler, request
     * acknowledgments for each command, and take appropriate actions if the
     * command fails.
     */
    amps_message_set_field_value_nts(m, AMPS_Command, "logon");
    amps_message_set_field_value_nts(m, AMPS_ClientName, "cClient");
    amps_message_set_field_value_nts(m, AMPS_MessageType, "json");

    /* Once the command is constructed, we send the message. The return
     * value does not indicate the result of the command sent to AMPS. Instead, the
     * return value indicates whether the AMPS client was able to send the
     * command to AMPS.
     */
    result = amps_client_send(client, logon_command);

    if (result != AMPS_E_OK)
    {
        amps_client_get_error(client, errorBuffer, sizeof(errorBuffer));
        printf("error %s\n", errorBuffer);
        amps_message_destroy(logon_command);
        amps_client_destroy(client);
        return 1;
    }

    /* Free the resources associated with the logon_command message by
     * calling amps_message_destroy with the message.
     */
    amps_message_destroy(logon_command);

    /* As with the <code>logon</code> command, we must construct a message
     * that contains a <code>publish</code> message. While the C++ client
     * constructs and sends the message for us, with the C client we construct
     * them ourselves. Note that this line also allocates resources that must
     * be freed by a corresponding amps_message_destroy function.
     */
    message = amps_message_create(client);

    /* These next few lines are responsible for setting the necessary fields
     * and data to construct a valid publish message for AMPS.
     */
    amps_message_set_field_value_nts(message, AMPS_CommandId, "12345");
    amps_message_set_field_value_nts(message, AMPS_Command, "publish");
    amps_message_set_field_value_nts(message, AMPS_Topic, "messages");
    amps_message_set_data_nts(message, "{\"message\":\"HelloWorld\"}");

    /* Once the message is constructed to our satisfaction, it is sent. As
     * with the logon command, AMPS processes the publish command asynchronously.
     * If an acknowledgment is requested, AMPS returns the acknowledgment
     * message in response to the publish command. Your application must
     * process that acknowledgment asynchronously.
     */
    result = amps_client_send(client, message);

    if (result != AMPS_E_OK) {
        /* Any errors from the operation are detected and examined here */
        amps_client_get_error(client, errorBuffer, sizeof(errorBuffer));
        printf("error sending: %s\n", errorBuffer);
    }

    /* This where we free any message that was allocated and then destroy
     * the client, freeing up the remaining AMPS resources.
     */
    amps_message_destroy(message);
}
amps_client_destroy(client);
return 0;

Example 3.3: Connecting in C

Structurally, the example in C and in C++ are similar. In the C program more details are needed to form your program, and the messages that are sent need to be constructed directly, instead of having portions of the message already created.

With the C client, your application is responsible for forming commands to AMPS, receiving the responses, and interpreting the results. As mentioned above, the AMPS Command Reference contains detailed information on the headers that need to be set for specific commands. The Command Cookbook in 10. AMPS Programming: Working with Commands contains information on how to set headers for commonly used AMPS commands.

Client Names

AMPS uses the name of the client as a session identifier and as part of the identifier for a message. For this reason, when a transaction log is enabled in the AMPS instance (that is, when the instance is recording a sequence of publishes and attempting to eliminate duplicate publishes), an AMPS instance will only allow one application with a given client name to connect to the instance.

When a transaction log is present, AMPS requires the Client Name for a publisher to be:

  • Unique within a set of replicated AMPS instances
  • Consistent from invocation to invocation if the publisher will be publishing tthe same logical stream of messages

60East recommends always using consistent, unique client names. For example, the client name could be formed by combining the application name, an identifier for the host system, and the id of the user running the application. A strategy like this provides a name that will be different for different users or on different systems, but consistent for instances of the application that should be treated as equivalent to the AMPS system.

Connection Strings

The AMPS clients use connection strings to determine the server, port, transport, and protocol to use to connect to AMPS. When the connection point in AMPS accepts multiple message types, the connection string also specifies the precise message type to use for this connection. Connection strings have a number of elements.

elements of a connection string

Figure 3.1: Elements of a Connection String

As shown in the figure above, connection strings have the following elements:

  • Transport defines the network used to send and receive messages from AMPS. In this case, the transport is tcp. For connections to transports that use the Secure Sockets Layer (SSL), use tcps. For connection to AMPS over a Unix domain socket, use unix.

  • Host address defines the destination on the network where the AMPS instance receives messages. The format of the address is dependent on the transport. For tcp and tcps, the address consists of a host name and port number. In this case, the host address is localhost:9007. For unix domain sockets, a value for hostname and port must be provided to form a valid URI, but the content of the hostname and port are ignored, and the file name provided in the path parameter is used instead (by convention, many connection strings use localhost:0 to indicate that this is a local connection that does not use TCP/IP).

  • Protocol sets the format in which AMPS receives commands from the client. Most code uses the default amps protocol, which sends header information in JSON format. AMPS supports the ability to develop custom protocols as extension modules, and AMPS also supports legacy protocols for backward compatibility.

  • MessageType specifies the message type that this connection uses. This component of the connection string is required if the protocol accepts multiple message types and the transport is configured to accept multiple message types. If the protocol does not accept multiple message types, this component of the connection string is optional, and defaults to the message type specified in the transport.

    Legacy protocols such as fix, nvfix and xml only accept a single message type, and therefore do not require or accept a message type in the connection string.

As an example, a connection string such as

tcp://localhost:9007/amps/json

would work for programs connecting from the local host to a Transport configured as follows:

<AMPSConfig>
    ...

    <!-- This transport accepts any known message type for the instance: the
         client must specify the message type. -->
    <Transport>
        <Name>any-tcp</Name>
        <Type>tcp</Type>
        <InetAddr>9007</InetAddr>
        <Protocol>amps</Protocol>
    </Transport>

    ...
</AMPSConfig>

See the AMPS Configuration Guide for more information on configuring transports.

Providing Credentials in a Connection String

When using the default authenticator, the AMPS clients support the standard format for including a username and password in a URI, as shown below:

tcp://user:password@host:port/protocol/message_type

When provided in this form, the default authenticator provides the username and password specified in the URI. If you have implemented another authenticator, that authenticator controls how passwords are provided to the AMPS server.

Connection Parameters

When specifying a URI for connection to an AMPS server, you may specify a number of transport-specific options in the parameters section of the URIconnection parameters. Here is an example:

tcp://localhost:9007/amps/json?tcp_nodelay=true&tcp_sndbuf=100000

In this example, we have specified the AMPS instance on localhost, port 9007, connecting to a transport that uses the amps protocol and sending JSON messages. We have also set two parameters, tcp_nodelay, a Boolean (true/false) parameter, and tcp_sndbuf, an integer parameter. Multiple parameters may be combined to finely tune settings available on the transport. Normally, you’ll want to stick with the defaults on your platform, but there may be some cases where experimentation and fine-tuning will yield higher or more efficient performance.

The AMPS client supports the value of tcp in the scheme component connection string for TCP/IP connections, and the value of tcps as the scheme for SSL encrypted connections.

For connections that use Unix domain sockets, the client supports the value of unix in the scheme, and requires the additional option described below.

TCP and SSL transport options

The following transport options are available for TCP connections:

bind (ip address) Sets the interface to bind the outgoing socket to.
tcp_rcvbuf (integer) Sets the socket receive buffer size. This defaults to the system default size. (On Linux, you can find the system default size in /proc/sys/net/core/rmem_default.)
tcp_sndbuf (integer) Sets the socket send buffer size. This defaults to the system default size. (On Linux, you can find the system default size in /proc/sys/net/core/wmem_default.)
tcp_nodelay (boolean) Enables or disables the TCP_NODELAY setting on the socket. By default TCP_NODELAY is disabled.
tcp_linger (integer) Enables and sets the SO_LINGER value for the socket By default, SO_LINGER is enabled with a value of 10, which specifies that the socket will linger for 10 seconds.
tcp_keepalive (boolean) Enables or disables the SO_KEEPALIVE value for the socket. The default value for this option is true.

Unix transport parameters

The unix transport type communicates over unix domain sockets. This transport requires the following additional option:

path The path to the unix domain socket to connect to.

Unix domain sockets always connect to the local system. When the scheme specified is unix, the host address is ignored in the connection string. For example, the connection string:

unix://localhost:0/amps/json?path=/sockets/the-amps-socket

and the connection string:

unix://unix:unix/amps/json?path=/sockets/the-amps-socket

are equivalent.

The other components of the connection string, including the protocol, message type, user name, and authentication token are processed just as they would be for TCP/IP sockets.

AMPS additional logon options

The connection string can also be used to pass logon parameters to AMPS. AMPS supports the following additional logon option:

pretty Provide formatted representations of binary messages rather than the original message contents.

Next steps

You are now able to develop and build an application in C or C++ that publishes messages to AMPS. In the following chapters, you will learn how to subscribe to messages, use content filters, work with SOW caches, and fine-tune messages that you send.