![xmpp](https://i-blog.csdnimg.cn/blog_migrate/e81c6fd5f301fcf8c1feece36fcd14c1.png)
xmpp
在本文中,我们将开发用Java编写的XMPP负载测试工具。
目录
1.简介
可扩展消息传递和状态协议( XMPP )是基于XML(可扩展标记语言)的面向消息的中间件的通信协议。 它是由Internet工程任务组(IETF)标准化并由XMPP标准基金会(XSF)支持和扩展的开放协议。 XMPP在开放标准中定义,并使用开放系统的开发和应用方法。 因此,许多服务器,客户端和库的实现都以自由和开源软件的形式分发。 XMPP扩展协议(XEP)中还定义了许多扩展。
IgniteRealtime发行的一种免费的开源发行版提供了以下实现:
Spark是类似于Messenger,What's app,Viber或Google Talk(实际上后者使用XMPP协议)的聊天客户端应用程序。 一个人可以发送聊天消息,文件作为附件等。这些消息被发送到Openfire服务器,然后由它负责将其传递到目的地,该服务器可以是直接与其连接的另一个Spark(或其他)聊天客户端,也可以是另一个Openfire。实例(联盟),直到他们到达最终目的地。
但是,服务器和客户端在负载下的性能如何,即当它们必须处理许多聊天消息或许多文件传输时?
2. XMPP负载测试工具
存在许多解决方案来对XMPP服务器(例如Openfire)进行负载/压力测试(列表并不详尽):
在本文中,我们将使用Smack XMPP库编写Java XMPP负载测试工具。
3.先决条件
您需要在系统上下载并安装Openfire 。 要尝试我们的负载测试工具,使用嵌入式数据库就足够了,即使您需要记住嵌入式数据库(HSQLDB)将在一段时间后填满。 当然,推荐使用真实的RDBMS。
您必须创建许多用户来模拟用户消息交换负载。 在我们的示例中,我们将创建50个用户名,用户名分别为user001
到user050
, user050
都具有相同的密码a
。 如果您不知道该怎么做,请登录管理控制台(例如http:// localhost:9090或https:// 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);
类User
和ChatRoom
的定义如下:
/**
* 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/resource
的JID称为完整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.00001
和sasl.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在场和名册
用户登录后,可以通过创建新的Chat
或MultiUserChat
对象开始与其他用户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(消息,存在和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时,对等网络将失败。 它速度更快,除了上述问题之外,您还无法控制要交换的内容。
- 代理服务器(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,或者甚至可以根据需要使用YourKit或Java Flight Recorder之类的工具进行概要分析。
5.总结
在本教程中,我们学习了如何编写自己的负载测试工具来对XMPP服务器(如Openfire)进行负载/压力测试。 负载/压力工具还可以用于测试XMPP客户端(例如Spark)。 如果您编写了自己的XMPP客户端或服务器,那么它也可以用于测试它们。 该工具使用Smack XMPP库以Java编写。 它可以在两种模式或场景下运行,既可以将消息发送到聊天室,也可以在用户之间发送文件传输。 XMPP服务器需要使用模拟用户和聊天室进行预配置。
您可以根据您的需求进一步扩展源代码,例如用户数量,消息或文件附件的大小,消息之间的延迟,或者发送消息和文件传输的组合,或者测试XMPP的其他方面协议。
6.参考
- Aladev R.(2017a),“ XMPP负载测试–最终指南”。
- Aladev R.(2017b),“ XMPP负载测试–高级方案”。
- Gakwaya D.(2016),“ XMPP的友好介绍”。
- Marx D.(2017),“ Java命令行界面(第1部分):Apache Commons CLI ”,JavaCodeGeeks。
- Saint-Andre P.,Smith K.,Troncon R.(2009年), XMPP:权威指南,O'Reilly。
- Smack API
- 打击文件
- Tsagklis I.(2010a),“ Openfire服务器安装-即时消息基础结构”,JavaCodeGeeks。
- Tsagklis I.(2010b),“ Openfire服务器配置-即时消息基础结构”,JavaCodeGeeks。
- Tsagklis I.(2010c),“带有适用于Java应用程序的Smack的XMPP IM –即时消息基础结构”,JavaCodeGeeks。
7.下载Maven项目
那是一篇有关Java XMPP负载测试工具的文章。
翻译自: https://www.javacodegeeks.com/java-xmpp-load-test-tool.html
xmpp