java xmpp_Java XMPP负载测试工具

java xmpp

在本文中,我们将开发用Java编写的XMPP负载测试工具。

1.简介

可扩展消息传递和状态协议XMPP是基于XML(可扩展标记语言)的面向消息的中间件的通信协议。 它是由Internet工程任务组 (IETF) 标准化并由XMPP标准基金会 (XSF)支持和扩展的开放协议。 XMPP是在开放标准中定义的,并使用开放系统的开发和应用方法。 因此,许多服务器,客户端和库的实现都以自由和开源软件的形式分发。 XMPP扩展协议 (XEP)中还定义了许多扩展。

IgniteRealtime发行的一种免费的开源发行提供了以下实现:

XMPP负载测试工具

Spark是类似于Messenger,What's app,Viber或Google Talk(实际上后者使用XMPP协议)的聊天客户端应用程序。 一个人可以发送聊天消息,文件作为附件等。这些消息被发送到Openfire服务器,然后由其负责将它们传递到目的地,该服务器可以是直接与其连接的另一个Spark(或其他)聊天客户端,也可以是另一个Openfire。实例(联盟),直到他们到达最终目的地。

但是,服务器和客户端在负载下的性能如何,即当它们必须处理许多聊天消息或许多文件传输时?

2. XMPP负载测试工具

存在许多解决方案来对XMPP服务器(例如Openfire)进行负载/压力测试(列表并不详尽):

  • 带有XMPP协议支持插件的Apache JMeter(请参阅[1,2])
  • iksemel XMPP C库
  • Tsung ,一种开源的多协议分布式负载测试工具

在本文中,我们将使用Smack XMPP库编写Java XMPP负载测试工具。

3.先决条件

您需要在系统上下载并安装Openfire 。 要尝试我们的负载测试工具,使用嵌入式数据库就足够了,即使您需要记住嵌入式数据库(HSQLDB)将在一段时间后填满。 当然,推荐使用真实的RDBMS。

您必须创建许多用户,这些用户将模拟用户消息交换负载。 在我们的示例中,我们将创建50个用户名,用户名分别为user001user050并具有相同的密码a 。 如果您不知道该怎么做,请登录管理控制台(例如http:// localhost:9090https:// localhost:9091 ),然后单击“ 用户/组”选项卡;然后单击“确定”。 在那里,您可以单击创建新用户来创建用户。

由于创建大量用户非常繁琐,因此有几个插件可以节省您的时间。 单击Openfire管理控制台的“ 插件”标签,然后单击“ 可用插件”并安装“ 用户创建”和/或“ 用户导入/导出”插件。 如果现在单击返回到“ 用户/组”选项卡,您将看到已创建新链接; 用户创建 (由于用户创建插件)和导入和导出 (由于用户导入/导出插件)。 剩下的练习是找出它们如何工作。

但是,这些并不是唯一需要做的更改。 在最新版本的Openfire中,安全机制已更改,因此要使我们的程序正常运行,我们需要定义两个属性。 单击服务器选项卡, 服务器管理器->系统属性,然后在页面底部输入以下属性名称/值对:

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

4. LoadXmppTest Java程序

我们将创建的工具是一个使用smack库的Java程序。 它提供了命令行界面(CLI),但是如果发现有用,则可以为其编写图形用户界面(GUI)。

$ 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

此外, loadxmpptest.properties允许进一步配置测试应用程序:

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

日志存储在log/loadxmpptest.log ,可以通过编辑logging.properties进行配置。

这是一个执行示例,其中服务器为localhost ,域为localhost (可以是其他名称),使用相同的密码a模拟了50个用户:

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

另一个例子,这次发送大型附件:

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

如上所述,要在loadxmpptest.properties中配置要发送的文件。

4.1创建一个新的Maven项目

跳到您最喜欢的IDE并创建一个新的Maven项目。 将以下依赖项添加到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>

这些是撰写本文时的最新版本,但是您可以使用在Maven Central中可能找到的最新版本。

4.2创建主类

该程序由基于[10]的两个类组成。 XmppLoadTest包含main()方法,并委托XmppManager来完成工作(与现实相反,因为规范是经理委托而不是实际进行工作:))。

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

我将跳过parseCLIArguments()方法的描述。 它使用Apache Commons CLI库来解析命令行参数(请参见[4])。 您可以根据需要选择其他任何CLI库或创建GUI。

我们的测试模拟了50个用户和5个聊天室(您可以模拟自己的方案来满足您的需求)。 这些存储在:

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

UserChatRoom的定义如下:

/**
  * 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;
        }
    }

ChatRoom类很简单。 聊天室被标识为例如room001@conference.localhost ,其中conference是您在单击Group Chat- > Group Chat Settings时在Openfire管理员控制台中定义的子域,而localhost是我们通过命令行参数-d传递的域。 getName()返回的String是房间的裸JID ,我们将在后面看到。

User类更为复杂。 它需要一个username ,一个password和一个domain并委托给XmppManager ,我们将很快看到。

XMPP客户端的地址格式为user@server.com ,其中user用户名server.com 。 XMPP中的节点地址称为Jabber ID,缩写为JID 。 JID也可以具有资源user@server.com/resource ),这意味着用户可以从多个设备连接。 格式为user@server.com JID称为裸JID ,而格式为user@server.com/resourceJID称为完整JID

用户可以setStatus() connect()到Openfire服务器,然后再login() ,然后用户可以setStatus()sendMessage()/receiveMesage(), sendAttachment()/receiveAttachment(), joinChatRoom()/leaveChatRoom()sendMessageToChatRoom()

init()方法初始化XmppManager()并创建50个用户,每个用户连接,登录并将其状态设置为available 。 如果要测试文件传输,则每个用户都开始收听文件传输。 也创建了五个聊天室。 50个用户中的每个用户都分配到一个聊天室,因此最后,每个聊天室都包含10个用户。

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());
        }
    }
}

一种方案是让每个user连接到五个聊天室之一并发送消息。 任务被创建( chatRoomMessageTask )在performLoad()和每执行every取决于消息的类型秒( )作为配置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
}

另一种情况是将附件发送给另一个用户,而不是将消息发送到聊天室:

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);

当然,您可以将两种情况结合起来,但是您需要确保不会溢出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
        }
    }
}

在上述方法(称为第一种情况)中,每个用户向该用户加入的聊天室发送一条消息(短消息或长消息)。

fileTransferTask() ,每个用户将附件发送给另一个用户(避免将附件发送给自己)。 请注意此方法和先前方法中的synchronized关键字,以避免代码中出现死锁。

/**
 * 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
            }
        }
    }
}

这样就完成了XmppLoadTest类的描述。

4.3 XmppManager类

XmppManager类使用smack库[6,7]与Openfire服务器进行通信。 Smack是用于与XMPP服务器通信以执行实时通信(包括即时消息传递和群聊)的库。

XmppManager与[10]中的类似,但是直到那时事情已经发生了发展,API也发生了变化。 如前所述, User委托给XmppManager

4.3.1连接到Openfire

要连接到Openfire服务器,您需要托管Openfire 的服务器名称端口 (已固定: 5222 )。 XMPPTCPConnection类用于创建与XMPP服务器的连接。 可以使用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();

resource String对于文件传输很重要。 如果您使用的是smack,则可以是"Smack""Resource" 。 如果使用其他客户端,例如Spark,则可以将其设置为"Spark ”。它可以确定将文件发送到的资源。

//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();

TLSUtils.acceptAllCertificates(builder); 这一点很重要,因为最新版本的Openfire中的安全模型已更改。 因此,我们在Openfire的管理控制台中添加了sasl.mechs.00001sasl.mechs.00002 。 如果他仍然遇到连接/身份验证问题,则此链接可能有帮助。

4.3.2登录

配置与Openfire的连接后,就可以连接到它了:

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

默认情况下,如果突然断开连接,Smack将尝试重新连接。 重新连接管理器将尝试立即重新连接到服务器,并增加尝试之间的延迟,因为连续的重新连接始终会失败。 创建连接后,用户应使用其凭据使用XMPPConnection.login()方法登录:

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在场和名册

用户登录后,可以通过创建新的ChatMultiUserChat对象开始与其他用户Chat 。 用户还可以将其状态设置为可用

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());
    }
}

从客户端到XMPP服务器的每条消息称为数据包节,并以XML的形式发送。 节是客户端可以在一个程序包中发送给服务器的最小XML数据段,反之亦然。 org.jivesoftware.smack.packet Java软件包包含一些类,这些类封装了XMPP允许的三种不同的基本数据包类型( message状态IQ )。 这些节中的每个节由XMPP服务器和客户端以不同的方式处理。 节具有类型属性 ,这些可用于进一步区分节[3]。

消息 旨在用于在XMPP实体之间发送数据。 实在是忘了,也就是说,接收方不承认节。 通常,当您从客户端发送消息节并且未生成任何类型的错误时,可以假定消息已成功发送。 消息节的类型可以为“聊天”,“ groupchar”,“错误”等。

状态节会通告其他实体的在线状态(网络可用性)。 在线状态类似于XMPP中的订阅。 当您对某些JID的存在感兴趣时,您就订阅它们的存在,即,您告诉XMPP服务器“每次该JID向您发送状态更新时,我都希望收到通知”。 当然,服务器会询问JID持有者是否接受向您透露其在线信息。 当他们接受后,服务器会记住他们的决定,并在更改在线状态时更新订阅该状态的任何人。 术语“ 存在”还表示用户是否在线。

最后, IQ (信息/查询)节用于从服务器获取一些信息(例如,有关服务器或其注册客户端的信息)或将某些设置应用于服务器。

在XMPP中,术语名册用于指联系人列表。 用户的联系人列表通常存储在服务器上。 该名册使您可以跟踪其他用户的可用性(状态)。 可以将用户分为“朋友”和“同事”之类的组,然后您会发现每个用户是联机还是脱机。 Roster类允许您查找所有名单条目,它们所属的组以及每个条目的当前状态。

名册中的每个用户都由RosterEntry表示,该成员包括:

  • XMPP地址(例如john@example.com )。
  • 您分配给用户的名称(例如"John" )。
  • 条目所属的名册中的组的列表。 如果名册条目不属于任何组,则称为“未归档条目”。

在名单中的每个条目都有一个与之关联的存在Roster.getPresence(String user)方法将返回一个具有用户状态的Presence对象;如果用户不在线或您尚未订阅该用户的状态,则返回null 。 用户要么在线要么离线 。 当用户在线时,他们的存在可能包含扩展信息,例如他们当前正在做什么,是否希望受到打扰等。

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) {  }
    });
}

状态信息可能会经常更改,并且名册条目也可能会更改或删除。 要侦听更改的花名册和状态数据,请使用RosterListener 。 为了通知有关名册的所有更改,应登录XMPP服务器之前注册RosterListener 。 文件传输知道,如果收件人的资源发生了变化,所描述的是很重要的位置

4.3.4聊天和多聊

您可以在ChatManager的帮助下发送和接收聊天消息。 尽管可以将单个消息作为数据包发送和接收,但是使用org.jivesoftware.smack.chat2.Chat类将消息字符串视为聊天通常会更容易。 聊天会在两个用户之间创建新的消息线程。 Chat.send(String)方法是一种便捷方法,它创建一个Message对象,使用String参数设置正文,然后发送消息。

/**
 * 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());
            });
}

要加入聊天室( MultiUserChat )并向其中发送消息:

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());
    }
}

您可以在加入聊天室时定义昵称。

4.3.5文件传输

要发送/接收附件,它比较复杂(请参见此处 ):

/**
 * 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());
            }
        }
    });
}

Openfire中定义了3种类型的文件传输

  • 带内FileTransferNegotiator.IBB_ONLY ),其中消息被分解为多个块并作为编码消息发送。 它速度较慢,但​​始终有效。 此外,由于交换的消息存储在Openfire数据库中,因此备份起来更容易。
  • 当两个用户都在同一网络上时, 对等 (p2p)效果很好,但是当一个用户在防火墙后面或使用NAT时, 对等 (p2p)失败。 它速度更快,除了上述问题之外,您还无法控制要交换的内容。
  • 代理服务器 (SOCKS5,请参阅XEP-0096或更新的XEP-0234 )使用文件传输代理,但需要打开端口7777。它比p2p慢,但比带内快。

在我们的测试工具中,正在使用带内文件传输。

一旦成功发送文件,就需要监视其状态( monitorFileTransfer() )。 可能存在网络错误,或者收件人可能只是拒绝文件传输。 实际上,其他用户可以选择接受,拒绝或忽略文件传输请求。

发送附件是OutgoingFileTransfer ,而接收是IncomingFileTransfer 。 这是通过将侦听器添加到FileTransferManager来实现的。 如前所述,接收者需要在发送者发送文件之前开始侦听。 此外,在我们的负载测试中,正在发送和接收相同的文件。 为了避免覆盖同一文件,源文件以不同的名称存储,在文件名中添加"_"和收件人的名称。 当然,这些文件名在负载测试工具运行时会一次又一次地写入。

建立

为了能够执行负载测试工具,您需要创建一个可执行文件XmppLoadTest-1.0.jar 。 一种方法是将以下内容添加到pom.xml中:

<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>

并且还需要将依赖项包括到classpath中 。 或者,您可以使用依赖项插件来创建一个单个jar ,该jar可以创建此处所述的所有内容。

<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>

您也可以使用maven命令代替执行它:

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

4.5负载测试

一旦执行了负载测试工具,您将看到许多发送到Openfire服务器的消息。 根据您选择的方案(群聊或文件传输),如果您使用第50个用户(例如Spark)之类的聊天客户端进行连接并加入聊天室,您将看到他们被重复发送的相同消息所填充其他49个模拟用户。

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
...

user050情况下运行该工具时,您不会在Spark或以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]

一旦运行了加载/压力工具,就可以搜索XMPP服务器或客户端的内存泄漏或CPU使用率过高。 您可以使用VisualVM之类的工具来监视内存和CPU,或者甚至可以根据需要使用YourKitJava Flight Recorder之类的工具进行概要分析。

5.总结

在本教程中,我们学习了如何编写自己的负载测试工具来对XMPP服务器(如Openfire)进行负载/压力测试。 负载/压力工具还可以用于测试XMPP客户端(例如Spark)。 如果您编写了自己的XMPP客户端或服务器,那么它也可以用于测试它们。 该工具使用Smack XMPP库以Java编写。 它可以在两种模式或场景下运行,既可以将消息发送到聊天室,也可以在用户之间发送文件传输。 XMPP服务器需要使用模拟用户和聊天室进行预配置。

您可以根据需要进行自定义,进一步扩展源代码,例如用户数量,消息或文件附件的大小,消息之间的延迟,发送消息和文件传输的组合或测试XMPP的其他方面协议。

6.参考

  1. Aladev R.(2017a),“ XMPP负载测试–最终指南 ”。
  2. Aladev R.(2017b),“ XMPP负载测试–高级方案 ”。
  3. Gakwaya D.(2016),“ XMPP的友好介绍 ”。
  4. Marx D.(2017),“ Java命令行界面(第1部分):Apache Commons CLI ”,JavaCodeGeeks。
  5. Saint-Andre P.,Smith K.,Troncon R.(2009年), XMPP:权威指南 ,O'Reilly。
  6. Smack API
  7. 打击文件
  8. Tsagklis I.(2010a),“ Openfire服务器安装-即时消息基础结构 ”,JavaCodeGeeks。
  9. Tsagklis I.(2010b),“ Openfire服务器配置-即时消息基础结构 ”,JavaCodeGeeks。
  10. Tsagklis I.(2010c),“ 带有适用于Java应用程序的Smack的XMPP IM –即时消息基础结构 ”,JavaCodeGeeks。

7.下载Maven项目

那是一篇有关Java XMPP负载测试工具的文章。

下载
您可以在此处下载完整的源代码: Java XMPP负载测试工具

翻译自: https://www.javacodegeeks.com/java-xmpp-load-test-tool.html

java xmpp

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值