A Java XMPP Load Test Tool
In this article, we will develop an XMPP Load Test Tool written in Java.
Table Of Contents
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:
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.00001 | PLAIN |
sasl.mechs.00002 | DIGEST-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
- Aladev R. (2017a), “XMPP Load Testing – The Ultimate Guide“.
- Aladev R. (2017b), “XMPP Load Testing – Advanced Scenarios“.
- Gakwaya D. (2016), “A friendly introduction to XMPP”.
- Marx D. (2017), “Java Command-Line Interfaces (Part 1): Apache Commons CLI”, JavaCodeGeeks.
- Saint-Andre P., Smith K., Troncon R. (2009), XMPP: The Definitive Guide, O’ Reilly.
- Smack API
- Smack Documentation
- Tsagklis I. (2010a), “Openfire server installation – Infrastructure for Instant Messaging“, JavaCodeGeeks.
- Tsagklis I. (2010b), “Openfire server configuration – Infrastructure for Instant Messaging“, JavaCodeGeeks.
- 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.
You can download the full source code here: Java XMPP Load Test Tool