Core Java

A Java XMPP Load Test Tool

In this article, we will develop an XMPP Load Test Tool written in Java.

 

1. Introduction

The eXtensible Messaging and Presence Protocol (XMPP) is a communication protocol for message-oriented middleware based on XML (Extensible Markup Language). It is an open protocol standardized by the Internet Engineering Task Force (IETF) and supported and extended by the XMPP Standards Foundation (XSF). XMPP is defined in an open standard and uses an open systems approach of development and application. Hence, many server, client, and library implementations are distributed as free and open-source software. There are also a number of extensions defined in XMPP Extension Protocols (XEPs).

One such free and open-source distribution is the one from IgniteRealtime which provides the following implementations:

XMPP Load Test Tool

Spark is a chat client application similar to Messenger, What’s app, Viber or Google Talk (actually the latter uses the XMPP protocol). One can send chat messages, files as attachments etc. These are sent to the Openfire server which then takes care of delivering them to their destinations, which can be another Spark (or other) chat client that connects directly to it, or to another Openfire instance (federation) until they reach their final destination.

However, how does the server and client perform under load, i.e. when they have to deal with many chat messages or many file transfers?

2. XMPP Load Test tools

A number of solutions exist to load/stress test an XMPP server like Openfire (list is not exhaustive):

  • Apache JMeter with the XMPP Protocol Support plugin (see [1,2])
  • iksemel XMPP C library
  • Tsung, an open-source multi-protocol distributed load testing tool

In this article we will write an XMPP Load Test tool in Java using the Smack XMPP library.

3. Prerequisites

You need to download and install Openfire on your system. To try our load test tool, using an embedded database is sufficient, even though you need to keep in mind that the embedded database (HSQLDB) will fill up after some time. Using a real RDBMS is of course the recommended way.

You must create a number of users that will simulate the user message exchange load. In our example we will create 50 users with usernames user001 to user050 all having the same password, a. If you don’t know how to do that, login to the admin console (e.g. http://localhost:9090, or https://localhost:9091), and click on the Users/Groups tab; there you can click on Create New User to create a user.

Since the creation of a big number of users is rather tedious, there are a couple of plugins that may save you time. Click on the Plugins tab of the Openfire admin console, then on Available Plugins and install the User Creation and/or User Import/Export plugin(s). If you now click back to the Users/Groups tab, you will see that new links have been created; Users Creation (thanks to the User Creation plugin) and Import & Export (due to the User Import/Export plugin). It is left as an exercise to find out how they work.

However, these are not the only changes one needs to do. The security mechanism has been changed in the latest versions of Openfire, so for our program to work, we need to define two properties. Click on the Server tab, Server Manager -> System Properties and on the bottom of the page enter the following property name/value pairs:

sasl.mechs.00001PLAIN
sasl.mechs.00002DIGEST-MD5

4. LoadXmppTest Java program

The tool that we will create is a Java program that uses the smack library. It provides a Command Line Interface (CLI), but you may write a Graphical User Interface (GUI) for it if you find it useful.

$ java -Djava.util.logging.config.file=logging.properties -jar loadxmpptest.jar
Required options: s, d, p, n
usage: java -Djava.util.logging.config.file=logging.properties –jar loadxmpptest.jar
-a,--attachment  Test attachments
-b,--big         Test big attachments or messages
-d,--domain      Domain
-n,--number      Number of users
-o,--observer    Observer
-p,--password    Password
-s,--server      Server
Usage : java -Djava.util.logging.config.file=logging.properties -jar loadxmpptest.jar -s  -d  -p  -n  [-o ] [-a] [-b]
        jabber id : userXXX@
        chatroom  : roomXXX@conference.
        observer  : userXXX@/Spark (just to test)
        10 users per chatroom
        5 chatrooms
Use:
        -a to test small attachments (file transfers) or
        -a -b to test big attachments (file transfers)
or:
        -b to test long messages

Additionally, loadxmpptest.properties allows to further configure the test application:

SHORT_MESSAGES_DELAY_SECONDS = 100
LONG_MESSAGES_DELAY_SECONDS = 60
SMALL_ATTACHMENTS_DELAY_MINUTES = 1
BIG_ATTACHMENTS_DELAY_MINUTES = 5
DELAY_TO_SEND_MESSAGES_MILLISECONDS = 1000
BIG_FILE_NAME_PATH=blob.txt
SMALL_FILE_NAME_PATH=test.txt

Logging is stored in log/loadxmpptest.log and can be configured by editing logging.properties.

Here’s an example of execution where the server is localhost, the domain is localhost (could be something else), 50 users are emulated all with the same password a:

java -Djava.util.logging.config.file=logging.properties -jar loadxmpptest.jar -s localhost -d localhost -p a -n 50

Another example, this time sending big attachments:

java -Djava.util.logging.config.file=logging.properties -jar loadxmpptest.jar -s localhost -d localhost -p a -n 50 -ba

The files to be sent are configured in loadxmpptest.properties as mentioned above.

4.1 Create a new Maven project

Jump to your favourite IDE and create a new Maven project. Add the following dependencies to the pom.xml:

<dependencies>
    <dependency>
        <groupId>org.igniterealtime.smack</groupId>
        <artifactId>smack-core</artifactId>
        <version>4.3.4</version>
    </dependency>
    <dependency>
        <groupId>org.igniterealtime.smack</groupId>
        <artifactId>smack-tcp</artifactId>
        <version>4.3.4</version>
    </dependency>
    <dependency>
        <groupId>org.igniterealtime.smack</groupId>
        <artifactId>smack-im</artifactId>
        <version>4.3.4</version>
    </dependency>    
    <dependency>
        <groupId>org.igniterealtime.smack</groupId>
        <artifactId>smack-extensions</artifactId>
        <version>4.3.4</version>
    </dependency>
    <dependency>
        <groupId>org.igniterealtime.smack</groupId>
        <artifactId>smack-java7</artifactId>
        <version>4.3.4</version>
    </dependency>
    <dependency>
        <groupId>org.igniterealtime.smack</groupId>
        <artifactId>smack-debug</artifactId>
        <version>4.3.4</version>
    </dependency>
    <dependency>
        <groupId>commons-cli</groupId>
        <artifactId>commons-cli</artifactId>
        <version>1.4</version>
    </dependency>
</dependencies>

These are the latest versions at the time of writing article, but you can use the latest versions you may find in Maven Central.

4.2 Create the main class

The program consists of two classes based on [10]. XmppLoadTest contains the main() method and delegates to XmppManager to do the work (contrary to the reality as the norm is that managers delegate instead of actually doing the work :) ).

public static void main(String[] args) throws Exception {
    parseCLIArguments(args);
    final XmppLoadTest loadXmppTest = new XmppLoadTest();
    loadProperties(PROPERTIES_FILE);
    init(loadXmppTest);
    performLoad(loadXmppTest);
}

I ‘ll skip the description of the parseCLIArguments() method. It uses the Apache Commons CLI library to parse the command line arguments (see [4]). You may choose any other CLI library or create a GUI if you like.

Our test simulates 50 users and 5 chat rooms (you can simulate your own scenario to match your needs). These are stored in:

private static final List<User> users = new ArrayList< >(numberOfUsers);
private static final List<ChatRoom> chatRooms = new ArrayList< >(numberOfRooms);

The classes User and ChatRoom are defined like so:

/**
  * User (e.g. {@code user001}). Functionality delegated to @{see
  * XmppManager}.
  */
final class User {
        private final String username;
        private final String password;
        private final String domain;
        private final XmppManager xmppManager; // delegate to it
        private MultiUserChat joinedChatRoom;
        public User(String username, String password, String domain, XmppManager xmppManager) {
            this.username = username;
            this.password = password;
            this.domain = domain;
            this.xmppManager = xmppManager;
        }
        public String getUsername() { return username; }
        public String getPassword() { return password; }
        public String getJabberID() { return username + "@" + domain; }
        public void connect() {
            xmppManager.connect();
        }
        public void disconnect() {
            xmppManager.destroy();
            LOG.info("User " + username + " disconnected.");
        }
        public void login() {
            xmppManager.login(username, password);
        }
        public void setStatus(boolean available, String status) {
            xmppManager.setStatus(available, status);
        }
        public void sendMessage(String toJID, String message) {
            xmppManager.sendMessage(toJID, message);
        }
        public void receiveMessage() {
            xmppManager.receiveMessage();
        }
        public void sendAttachment(String toJID, String path) {
            xmppManager.sendAttachment(toJID, "Smack", path);
        }
        public void receiveAttachment() {
            xmppManager.receiveAttachment(username);
        }
        public void joinChatRoom(String roomName, String nickname) {
            joinedChatRoom = xmppManager.joinChatRoom(roomName, nickname);
        }
        public void leaveChatRoom() {
            try {
                joinedChatRoom.leave();
            } catch (SmackException.NotConnectedException | InterruptedException ex) {
                LOG.severe(ex.getLocalizedMessage());
            }
        }
        public void sendMessageToChatRoom(String message) {
            xmppManager.sendMessageToChatRoom(joinedChatRoom, message);
        }
        public String getJoinedChatRoom() {
            return joinedChatRoom.getRoom().toString();
        }
        public void addRosterListener() {
            xmppManager.rosterChanged();
        }
    }
    /**
     * Chat room, e.g. {@code room001}
     */
    final class ChatRoom {
        private final String name;
        private final String domain;
        public ChatRoom(String name, String domain) {
            this.name = name;
            this.domain = domain;
        }
        public String getName() {
            return name + "@conference." + domain;
        }
    }

The ChatRoom class is straightforward. A chat room is identified as e.g. room001@conference.localhost, where conference is the subdomain defined in the Openfire administrator console when you click on Group Chat -> Group Chat Settings and localhost is the domain we passed with the command line argument -d. The String returned by the getName() is the bare JID of the room, as we will see later.

The User class is more complex. It requires a username, a password and a domain and delegates to XmppManager as we will see shortly.

XMPP clients have addresses of the form user@server.com where user is the username and server.com is the domain. A node address in XMPP is called a Jabber ID abbreviated as JID. A JID can also have a resource (user@server.com/resource) which means that the user can connect from multiple devices. A JID of the form user@server.com is called a bare JID, while a JID of the form user@server.com/resource is called a full JID.

A user can connect() to an Openfire server, then login() and then the user can setStatus(), can sendMessage()/receiveMesage(), sendAttachment()/receiveAttachment(), joinChatRoom()/leaveChatRoom() and sendMessageToChatRoom().

The init() method initializes XmppManager() and creates 50 users, each one connects, logins and sets its status to available. If file transfers are to be tested, then each user starts listening to file transfers. Five chat rooms are created, too. Each one of the 50 users is distributed to a chatroom, so that in the end, every chat room contains 10 users.

private static void init(XmppLoadTest loadXmppTest) {
    XmppManager xmppManager = new XmppManager(server, domain, port);
    for (int i = 1; i <= numberOfUsers; i++) {
        User user = loadXmppTest.new User("user" + String.format("%03d", i), password, domain, xmppManager);
        user.connect();
        user.login();
        user.setStatus(true, "Hello from " + user.getUsername());
        users.add(user);
        if (testAttachments || testBigAttachments) {
                user.receiveAttachment();
        }
    }
    for (int i = 0; i < numberOfRooms; i++) {
        chatRooms.add(loadXmppTest.new ChatRoom("room" + String.format("%03d", i + 1), domain));
    }
    if (!testAttachments && !testBigAttachments) {
        // join chatrooms
        for (int i = 1; i <= numberOfUsers; i++) {
            ChatRoom chatRoom = chatRooms.get((i - 1) % numberOfRooms);
            User user = users.get(i - 1);
            user.joinChatRoom(chatRoom.getName(), user.getJabberID());
        }
    }
}

One scenario is for each user to connect to one of the five chat rooms and send messages. A task is created (chatRoomMessageTask) in performLoad() and is executed every every seconds depending on the type of message (long or short) as configured in loadxmpptest.properties.

ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
....
        } else { // send messages to chat rooms
    final Runnable task = () -> {
        while (true) {
            if (Thread.currentThread().isInterrupted()) {
                return;
            }
            loadXmppTest.chatRoomMessageTask();
        }
    };
    int every = testLongMessages ? longMessagesDelayInSeconds : shortMessagesDelayInSeconds;
    scheduler.scheduleWithFixedDelay(task, 0, every, SECONDS); // every x seconds
}

The other scenario is to send attachments to another user instead of sending messages to a chat room:

if (testAttachments || testBigAttachments) {  // send attachments
   String filePath  = testBigAttachments ? bigFileNamePath : smallFileNamePath;
   int delay = testBigAttachments ? bigAttachmentsDelayInMinutes : smallAttachmentsDelayInMinutes;
   final Runnable task = () -> {
       while (true) {
           if (Thread.currentThread().isInterrupted()) {
               return;
           }
           loadXmppTest.fileTransferTask(filePath);
       }
   };
 scheduler.scheduleWithFixedDelay(task, 0, delay, MINUTES); 

You may of course combine the two scenarios but you need to make sure that you don’t overflow the caches of Openfire.

/** Each user sends a message to a chat room. */
private synchronized void chatRoomMessageTask() {
    for (int i = 1; i <= numberOfUsers; i++) {
        String message = testLongMessages ? LONG_MESSAGE : MESSAGE;
        User user = users.get(i - 1);
        try {
            Thread.currentThread().sleep(delayToSendMessagesInMillis); // sleep 1"
            user.sendMessageToChatRoom(message);
            LOG.info(user.getJabberID() + " sent " + (testLongMessages ? "long" : "short") + " message to " + user.getJoinedChatRoom());
        } catch (InterruptedException ie) {
            Thread.currentThread().interrupt(); // reset the flag
        }
    }
}

In the above method, called the first scenario, each user sends a message (short or long) to the chatroom the user has joined to.

In the fileTransferTask(), each user sends an attachment to another user (avoiding to send one to themselves). Please pay attention to the synchronized keyword in this and the previous method to avoid deadlocks in the code.

/**
 * Exchange file attachments between users.
 *
 * @param path path of the file to send
 * @see #transferFile(int, java.lang.String)
 */
private void fileTransferTask(String path) {
    for (int i = 1; i <= numberOfUsers; i++) {
        transferFile(i, path);
    }
}
/**
 * Transfer the file to all other users.
 *
 * @param i i-th user
 * @param path path of the file to be sent
 */
private synchronized void transferFile(int i, String path) {
    int j;
    for (j = 1; j <= numberOfUsers; j++) {
        if (i != j) {
            try {
                int delay = testBigAttachments ? bigAttachmentsDelayInMinutes : smallAttachmentsDelayInMinutes;
               Thread.currentThread().sleep(delay); 
               if (users.get(i - 1).sendAttachment(users.get(j - 1).getJabberID(), path)) {
                 LOG.info("Attachment " + path + " sent from " + users.get(i - 1).getJabberID() + " to " + users.get(j - 1).getJabberID());
               } else {
                 LOG.severe("Attachment " + path + " from " + users.get(i - 1).getJabberID() + " to " + users.get(j - 1).getJabberID() + "  was not sent!");
               }
            } catch (InterruptedException ie) {
                Thread.currentThread().interrupt(); // reset the flag
            }
        }
    }
}

This concludes the description of the XmppLoadTest class.

4.3 XmppManager class

The XmppManager class uses the smack library [6, 7] to communicate with the Openfire server. Smack is a library for communicating with XMPP servers to perform real-time communications, including instant messaging and group chat.

XmppManager is similar to the one in [10], but things have evolved till then and the API has changed. As already mentioned, User delegates to XmppManager.

4.3.1 Connect to Openfire

To connect to an Openfire server you need the name of the server that hosts the Openfire, the domain and the port (which is fixed: 5222). The XMPPTCPConnection class is used to create a connection to an XMPP server. Further connection parameters can be configured by using a XMPPTCPConnectionConfiguration.Builder:

private String resource = "Smack";
...
XMPPTCPConnectionConfiguration.Builder builder =  XMPPTCPConnectionConfiguration.builder();
try {
    builder.setXmppDomain(JidCreate.domainBareFrom(domain))
            .setHost(server)
            .setPort(port)
            .setResource(resource)
            .setSecurityMode(SecurityMode.disabled)
            .setHostnameVerifier((String hostname, SSLSession session) -> true);
} catch (XmppStringprepException ex) {
    LOG.severe(ex.getLocalizedMessage());
}
try {
    builder = TLSUtils.acceptAllCertificates(builder);
} catch (KeyManagementException | NoSuchAlgorithmException ex) {
    LOG.log(Level.SEVERE, null, ex);
}
XMPPTCPConnection.setUseStreamManagementDefault(true);
XMPPTCPConnectionConfiguration config = builder.build();

The resource String is important for file transfers. If you are using smack, it can be either "Smack" or "Resource". If you use another client, e.g. Spark, then you can set it to "Spark“. It allows to identify which resource to send the file to.

//SASLMechanism mechanism = new SASLDigestMD5Mechanism();
SASLMechanism mechanism = new SASLPlainMechanism();
SASLAuthentication.registerSASLMechanism(mechanism);
SASLAuthentication.unBlacklistSASLMechanism("PLAIN");
SASLAuthentication.blacklistSASLMechanism("SCRAM-SHA-1");
SASLAuthentication.unBlacklistSASLMechanism("DIGEST-MD5");        

try {
    builder = TLSUtils.acceptAllCertificates(builder);
} catch (KeyManagementException | NoSuchAlgorithmException ex) {
    LOG.severe(ex.getLocalizedMessage());
}
XMPPTCPConnection.setUseStreamManagementDefault(true);
XMPPTCPConnectionConfiguration config = builder.build();

The TLSUtils.acceptAllCertificates(builder); is important since the security model has changed in the latest versions of Openfire. For that reason, we added the sasl.mechs.00001 & sasl.mechs.00002 in the administration console of Openfire. If he still encounter connection/authentication issues, this link maybe of help.

4.3.2 Login

Once you have configured the connection to Openfire, it is time to connect to it:

private AbstractXMPPConnection connection;
...
connection = new XMPPTCPConnection(config);
connection.setReplyTimeout(1000L);
try {
     connection.connect();
} catch (SmackException | IOException | XMPPException | InterruptedException ex) {
     LOG.severe(ex.getLocalizedMessage());
}

By default Smack will try to reconnect the connection in case it was abruptly disconnected. The reconnection manager will try to immediately reconnect to the server and increase the delay between attempts as successive reconnections keep failing. Once a connection has been created, a user should login with the XMPPConnection.login() method using their credentials:

public void login(String username, String password) {
    if (connection != null && connection.isConnected()) {
        try {
            connection.login(username, password);
        } catch (XMPPException | SmackException | IOException | InterruptedException ex) {
            LOG.severe(ex.getLocalizedMessage());
        }
    }
    LOG.info(username + " authenticated? " + connection.isAuthenticated());
}

4.3.3 Presence and Roster

Once a user has logged in, they can begin chatting with other users by creating new Chat or MultiUserChat objects. The user can also set its status to available:

public void setStatus(boolean available, String status) {
    Presence.Type type = available ? Type.available : Type.unavailable;
    Presence presence = new Presence(type);
    presence.setStatus(status);
    try {
        connection.sendStanza(presence);
    } catch (SmackException.NotConnectedException | InterruptedException ex) {
        LOG.severe(ex.getLocalizedMessage());
    }
}

Each message to the XMPP server from a client is called a packet or a stanza and is sent as XML. A stanza is the smallest piece of XML data a client can send to a server and vice versa in one package. The org.jivesoftware.smack.packet Java package contains classes that encapsulate the three different basic packet types allowed by XMPP (message, presence, and IQ). Each of these stanzas is handled differently by XMPP servers and clients. Stanzas have type attributes and these can be used to further differentiate stanzas [3].

The message stanza is meant to be used to send data between XMPP entities. It is fire and forget i.e. stanzas are not acknowledged by the receiving party. Usually when you send a message stanza from your client and no error of any kind is generated, you can assume that it has been sent successfully. Message stanzas can be of type “chat”, “groupchar”, “error” etc.

The presence stanza advertises the online status (network availability) of other entities. Presence works like subscription in XMPP. When you are interested in the presence of some JID, you subscribe to their presence, i.e. you tell the XMPP server “everytime this JID sends you a presence update, I want to be notified”. Of course, the server asks the JID holder if they accept to disclose their presence information to you. When they accept, the server remembers their decision and updates anyone subscribed to their presence whenever they change their online status. The term presence also means whether a user is online or not.

Finally, the IQ (Info/Query) stanza is used to get some information from the server (e.g. info about the server or its registered clients) or to apply some settings to the server.

In XMPP, the term roster is used to refer to the contact list. A user’s contact list is usually stored on the server. The roster lets you keep track of the availability (presence) of other users. Users can be organized into groups such as “Friends” and “Colleagues”, and then you discover whether each user is online or offline. The Roster class allows you to find all the roster entries, the groups they belong to, and the current presence status of each entry.

Every user in a roster is represented by a RosterEntry, which consists of:

  • An XMPP address (e.g. john@example.com).
  • A name you’ve assigned to the user (e.g. "John").
  • The list of groups in the roster that the entry belongs to. If the roster entry belongs to no groups, it’s called an “unfiled entry”.

Every entry in the roster has presence associated with it. The Roster.getPresence(String user) method will return a Presence object with the user’s presence or null if the user is not online or you are not subscribed to the user’s presence. A user either has a presence of online or offline. When a user is online, their presence may contain extended information such as what they are currently doing, whether they wish to be disturbed, etc.

public Roster createRosterFor(String user, String name) throws Exception {
    LOG.info(String.format("Creating roster for buddy '%1$s' with name %2$s", user, name));
    Roster roster = Roster.getInstanceFor(connection);
    roster.createEntry(JidCreate.bareFrom(user), name, null);
    return roster;
}
public void printRosters() throws Exception {
    Roster roster = Roster.getInstanceFor(connection);
    Collection entries = roster.getEntries();
    for (RosterEntry entry : entries) {
        LOG.info(String.format("Buddy: %s", entry.getName()));
    }
}
public void rosterChanged() {
    Roster roster = Roster.getInstanceFor(connection);
    roster.addRosterListener(new RosterListener() {
        @Override
        public void presenceChanged(Presence presence) {
            LOG.info("Presence changed: " + presence.getFrom() + " " + presence);
            resource = presence.getFrom().getResourceOrEmpty().toString();
        }
        @Override
        public void entriesAdded(Collection clctn) {  }
        @Override
        public void entriesUpdated(Collection clctn) {  }
        @Override
        public void entriesDeleted(Collection clctn) {  }
    });
}

The presence information will likely change often, and it’s also possible for the roster entries to change or be deleted. To listen for changing roster and presence data, a RosterListener is used. To be informed about all changes to the roster, the RosterListener should be registered before logging into the XMPP server. It is important for file transfers to know if the resource of a recipient has changed, as described here.

4.3.4 Chat and MultiChat

You send and receive chat messages with the help of the ChatManager. Although individual messages can be sent and received as packets, it’s generally easier to treat the string of messages as a chat using the org.jivesoftware.smack.chat2.Chat class. A chat creates a new thread of messages between two users. The Chat.send(String) method is a convenience method that creates a Message object, sets the body using the String parameter and then sends the message.

/**
 * Send message to another user.
 *
 * @param buddyJID recipient
 * @param message to send
 */
public void sendMessage(String buddyJID, String message) {
    LOG.info(String.format("Sending message '%1$s' to user %2$s", message, buddyJID));
    try {
        Chat chat = ChatManager.getInstanceFor(connection).chatWith(JidCreate.entityBareFrom(buddyJID));
        chat.send(message);
    } catch (XmppStringprepException | SmackException.NotConnectedException | InterruptedException ex) {
        LOG.severe(ex.getLocalizedMessage());
    }
}
public void receiveMessage() {
    ChatManager.getInstanceFor(connection).addIncomingListener(
            (EntityBareJid from, Message message, Chat chat) -> {
                LOG.info("New message from " + from + ": " + message.getBody());
            });
}

To join a chat room (MultiUserChat) and send messages to it:

public MultiUserChat joinChatRoom(String roomName, String nick) {
    try {
        MultiUserChatManager manager = MultiUserChatManager.getInstanceFor(connection);
        MultiUserChat muc = manager.getMultiUserChat(JidCreate.entityBareFrom(roomName));
        Resourcepart nickname = Resourcepart.from(nick);
        muc.join(nickname);
       LOG.info(muc.getNickname() + "joined chat room " + muc.getRoom());
        return muc;
    } catch (XmppStringprepException | SmackException.NotConnectedException | InterruptedException | SmackException.NoResponseException | XMPPException.XMPPErrorException | MultiUserChatException.NotAMucServiceException ex) {
        LOG.severe(ex.getLocalizedMessage());
    }
    return null;
}

public void sendMessageToChatRoom(MultiUserChat muc, String message) {
    try {
        muc.sendMessage(message);
        LOG.fine("Message '" + message + "' was sent to room '" + muc.getRoom() + "' by '" + muc.getNickname() + "'");
    } catch (InterruptedException | SmackException.NotConnectedException ex) {
        LOG.severe(ex.getLocalizedMessage());
    }
}

You can define a nickname when joining a chat room.

4.3.5 File transfers

To send/receive attachments, it is more complicated (see here):

/**
 * File transfer.
 *
 * @param buddyJID recipient
 * @param res e.g. "Spark-2.8.3", default "Smack" (cannot be empty or null)
 * @param path path of the file attachment to send
 * @return {@code true} if file transfer was successful
 */
public boolean sendAttachment(String buddyJID, String res, String path) {
    LOG.info(String.format("Sending attachment '%1$s' to user %2$s", path, buddyJID));
    FileTransferManager fileTransferManager = FileTransferManager.getInstanceFor(connection);
    FileTransferNegotiator.IBB_ONLY = true;
    OutgoingFileTransfer fileTransfer = null;
    try {
        fileTransfer = fileTransferManager.createOutgoingFileTransfer(JidCreate.entityFullFrom(buddyJID + "/Spark-2.8.3"));
    } catch (XmppStringprepException ex) {
        LOG.log(Level.SEVERE, null, ex);
        return false;
    }
    if (fileTransfer != null) {
        OutgoingFileTransfer.setResponseTimeout(15 * 60 * 1000);
        LOG.info("status is:" + fileTransfer.getStatus());
        File file = Paths.get(path).toFile();
        if (file.exists()) {
            try {
                fileTransfer.sendFile(file, "sending attachment...");
            } catch (SmackException ex) {
                LOG.severe(ex.getLocalizedMessage());
                return false;
            }
            LOG.info("status is:" + fileTransfer.getStatus());
            if (hasError(fileTransfer)) {
                LOG.severe(getErrorMessage(fileTransfer));
                return false;
            } else {
                return monitorFileTransfer(fileTransfer, buddyJID);
            }
         } else                 
            try {
                    throw new FileNotFoundException("File " + path + " not found!");
                } catch (FileNotFoundException ex) {
                    LOG.severe(ex.getLocalizedMessage());
                    return false;
                }
            }
        }
        return true;
}

/**
 * Monitor file transfer.
 *
 * @param fileTransfer
 * @param buddyJID
 * @return {@code false} if file transfer failed.
 */
private boolean monitorFileTransfer(FileTransfer fileTransfer, String buddyJID) {
    while (!fileTransfer.isDone()) {
            if (isRejected(fileTransfer) || isCancelled(fileTransfer)
                    || negotiationFailed(fileTransfer) || hasError(fileTransfer)) {
                LOG.severe("Could not send/receive the file to/from " + buddyJID + "." + fileTransfer.getError());
                LOG.severe(getErrorMessage(fileTransfer));
                return false;
            } else if (inProgress(fileTransfer)) {
                LOG.info("File transfer status: " + fileTransfer.getStatus() + ", progress: " + fileTransfer.getProgress());
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException ex) {
                LOG.severe(ex.getLocalizedMessage());
            }
        }
        if (isComplete(fileTransfer)) {
            LOG.info(fileTransfer.getFileName() + " has been successfully transferred.");
            LOG.info("The file transfer is " + (fileTransfer.isDone() ? "done." : "not done."));
            return true;
        }
        return true;
}

public void receiveAttachment(String username) {
    final FileTransferManager manager = FileTransferManager.getInstanceFor(connection);
    manager.addFileTransferListener((FileTransferRequest request) -> {
        // Check to see if the request should be accepted
        if (request.getFileName() != null) {
            StringBuilder sb = new StringBuilder(BUFFER_SIZE);
            try {
                // Accept it
                IncomingFileTransfer transfer = request.accept();
                String filename = transfer.getFileName() + "_" + username;
                transfer.receiveFile(new File(filename));
                while (!transfer.isDone()) {
                    try {
                        Thread.sleep(1000);
                        LOG.info("STATUS: " + transfer.getStatus()
                                + " SIZE: " + sb.toString().length()
                                + " Stream ID : " + transfer.getStreamID());
                    } catch (Exception e) {
                        LOG.severe(e.getMessage());
                    }
                    if (transfer.getStatus().equals(FileTransfer.Status.error)) {
                        LOG.severe(transfer.getStatus().name());
                    }
                    if (transfer.getException() != null) {
                        LOG.severe(transfer.getException().getLocalizedMessage());
                    }
                }
                LOG.info("File received " + request.getFileName());
            } catch (SmackException | IOException ex) {
                LOG.severe(ex.getLocalizedMessage());
            }
        } else {
            try {
                // Reject it
                request.reject();
                LOG.warning("File rejected " + request.getFileName());
            } catch (SmackException.NotConnectedException | InterruptedException ex) {
                LOG.severe(ex.getLocalizedMessage());
            }
        }
    });
}

There are 3 types of file transfers defined in Openfire:

  • in-band (FileTransferNegotiator.IBB_ONLY) where the message is broken down into chunks and sent as encoded messages. It is slower but works always. Additionally, it is easier to backup the messages exchanged as they are stored in the Openfire database.
  • peer-to-peer (p2p) works great when both users are on the same network but fail when one user is behind a firewall or using NAT. It is faster and apart from the aforementioned problem you don’t have control of what is being exchanged.
  • proxy server (SOCKS5 see XEP-0096 or the newer XEP-0234) uses a File Transfer Proxy but it requires opening port 7777. It is slower than p2p but faster than in-band.

In our test tool, in-band file transfer is being used.

Once you manage to send the file, you need to monitor its status (monitorFileTransfer()). There can be a network error or the recipient might simply refuse the file transfer. Actually, the other user has the option of accepting, rejecting, or ignoring the file transfer request.

While the sending of an attachment is an OutgoingFileTransfer, the reception is an IncomingFileTransfer. This is achieved by adding a listener to FileTransferManager. As already mentioned, the recipient needs to start listening before the sender sends the file. Additionally, in our load test the same file is being sent and received. In order to avoid overwriting the same file, the source file is stored under different name, adding "_" and the name of the recipient to the file name. Of course, these file names are written again and again while the load test tool is running.

4.4. Build

In order to be able to execute the load test tool, you need to create an executable XmppLoadTest-1.0.jar. One way is to add to your pom.xml the following:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-jar-plugin</artifactId>
            <version>3.2.0</version>
            <configuration>
                <archive>
                    <manifest>
                        <addClasspath>true</addClasspath>
                        <mainClass>test.xmpp.xmpploadtest.XmppLoadTest</mainClass>
                    </manifest>
                </archive>
            </configuration>
        </plugin>
    </plugins>
</build>

and you also need to include the dependencies to the classpath. Alternatively, you can use the dependency plugin to create a single jar that creates everything as described here.

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-assembly-plugin</artifactId>
    <version>3.2.0</version>
    <configuration>
        <archive>
            <manifest>
                <mainClass>test.xmpp.xmpploadtest.XmppLoadTest</mainClass>
            </manifest>
        </archive>
        <descriptorRefs>
            <descriptorRef>jar-with-dependencies</descriptorRef>
        </descriptorRefs>
    </configuration>
    <executions>
        <execution>
            <id>make-assembly</id> <!-- this is used for inheritance merges -->
            <phase>package</phase> <!-- bind to the packaging phase -->
            <goals>
                <goal>single</goal>
            </goals>
        </execution>
    </executions>
</plugin>

You may also use the maven command instead to execute it:

mvn exec:java -Dexec.mainClass=test.xmpp.xmpploadtest.XmppLoadTest "-Dexec.args=-s localhost -d localhost -p a -n 50"

4.5 Load test

Once you execute the load test tool, you will see a number of messages sent to the Openfire server. Depending on the scenario you chose (group chat or file transfers), if you connect as e.g. the 50th user using a chat client like Spark, and join the chat rooms, you will see them filled up by the same message sent again and again by the other 49 simulated users.

Apr 25, 2020 11:55:16 PM test.xmpp.xmpploadtest.XmppManager connect
INFO: Initializing connection to server localhost port 5222
Apr 25, 2020 11:55:18 PM test.xmpp.xmpploadtest.XmppManager connect
INFO: Connected: true
Apr 25, 2020 11:55:18 PM test.xmpp.xmpploadtest.XmppManager login
INFO: user001 authenticated? True
...
Apr 25, 2020 11:55:21 PM test.xmpp.xmpploadtest.XmppManager joinChatRoom
INFO: user001@localhost joined chat room room001@conference.localhost
Apr 25, 2020 11:55:21 PM test.xmpp.xmpploadtest.XmppManager joinChatRoom
INFO: user002@localhost joined chat room room002@conference.localhost
...
Apr 25, 2020 11:55:24 PM test.xmpp.xmpploadtest.XmppLoadTest chatRoomMessageTask
INFO: user001@localhost sent short message to room001@conference.localhost
Apr 25, 2020 11:55:25 PM test.xmpp.xmpploadtest.XmppLoadTest chatRoomMessageTask
INFO: user002@localhost sent short message to room002@conference.localhost
...

When you run the tool in the 2nd scenario, you don’t see any attachments in Spark or the chat client you connect to as user050.

INFO: Sending attachment 'test.txt' to user user003@localhost [Sun May 10 17:55:15 CEST 2020]
INFO: status is:Initial [Sun May 10 17:55:15 CEST 2020]
INFO: status is:Initial [Sun May 10 17:55:15 CEST 2020]
INFO: STATUS: Complete SIZE: 0 Stream ID : jsi_2604404248040129956 [Sun May 10 17:55:15 CEST 2020]
INFO: File received test.txt [Sun May 10 17:55:15 CEST 2020]
INFO: STATUS: In Progress SIZE: 0 Stream ID : jsi_4005559316676416776 [Sun May 10 17:55:16 CEST 2020]
WARNING: Closing input stream [Sun May 10 17:55:16 CEST 2020]
INFO: STATUS: In Progress SIZE: 0 Stream ID : jsi_6098909703710301467 [Sun May 10 17:55:16 CEST 2020]
INFO: STATUS: In Progress SIZE: 0 Stream ID : jsi_2348439600749627884 [Sun May 10 17:55:16 CEST 2020]
INFO: STATUS: In Progress SIZE: 0 Stream ID : jsi_8708250841661514027 [Sun May 10 17:55:16 CEST 2020]
INFO: STATUS: In Progress SIZE: 0 Stream ID : jsi_2119745768373873364 [Sun May 10 17:55:16 CEST 2020]
INFO: STATUS: In Progress SIZE: 0 Stream ID : jsi_6583436044582265363 [Sun May 10 17:55:16 CEST 2020]
INFO: STATUS: In Progress SIZE: 0 Stream ID : jsi_3738252107587424431 [Sun May 10 17:55:16 CEST 2020]
INFO: STATUS: In Progress SIZE: 0 Stream ID : jsi_4941117510857455094 [Sun May 10 17:55:16 CEST 2020]
INFO: test.txt has been successfully transferred. [Sun May 10 17:55:16 CEST 2020]
INFO: The file transfer is done. [Sun May 10 17:55:16 CEST 2020]

Once you have the load/stress tool running, you can search for memory leaks or high CPU usage of the XMPP server or client. You can monitor the memory and the CPU by using tools like VisualVM or you can even profile, if you need to, with tools like YourKit or Java Flight Recorder.

5. Summary

In this tutorial we learned how to write our own load test tool to load/stress test an XMPP server like Openfire. The load/stress tool can also be used to test XMPP clients like Spark. If you have written your own XMPP client or server then it could be used to test them, too. The tool is written in Java using the Smack XMPP library. It can run in two modes or scenarios, either to send messages to chat rooms, or send file transfers between users. The XMPP server needs to be preconfigured with the simulated users and the chat rooms.

You can further extend the source code to your needs customizing e.g. the number of users, the size of the messages or of the file attachments, the delays between the messages, or send a combination of messages and file transfers or test other aspects of the XMPP protocol.

6. References

  1. Aladev R. (2017a), “XMPP Load Testing – The Ultimate Guide“.
  2. Aladev R. (2017b), “XMPP Load Testing – Advanced Scenarios“.
  3. Gakwaya D. (2016), “A friendly introduction to XMPP”.
  4. Marx D. (2017), “Java Command-Line Interfaces (Part 1): Apache Commons CLI”, JavaCodeGeeks.
  5. Saint-Andre P., Smith K., Troncon R. (2009), XMPP: The Definitive Guide, O’ Reilly.
  6. Smack API
  7. Smack Documentation
  8. Tsagklis I. (2010a), “Openfire server installation – Infrastructure for Instant Messaging“, JavaCodeGeeks.
  9. Tsagklis I. (2010b), “Openfire server configuration – Infrastructure for Instant Messaging“, JavaCodeGeeks.
  10. Tsagklis I. (2010c), “XMPP IM with Smack for Java applications – Infrastructure for Instant Messaging“, JavaCodeGeeks.

7. Download the Maven project

That was an article about the Java XMPP Load Test Tool.

Download
You can download the full source code here: Java XMPP Load Test Tool

Ioannis Kostaras

Software architect awarded the 2012 Duke's Choice Community Choice Award and co-organizing the hottest Java conference on earth, JCrete.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button