白季飞龙的个人主页

XMPP大杂烩

XMPP是什么

XMPP是基于XML的即时通讯协议。对即时通讯场景进行了高度抽象,比如用订阅对方的上下线状态表示好友。提供了文本通讯、用户上下线通知、联系人管理、群组聊天等功能,还可以安装插件或自行拓展。

服务端的安装

服务端一般用OpenFire。

以macOS为例,官网直接下载安装包,安装完成后,在系统菜单中打开OpenFire控制台,进入OpenFire的Web后台,走完配置向导。数据库可以用OpenFire自带的嵌入式数据库,也可以配置为MySQL。如果是局域网测试的话,主机名最好设置成内网IP。最后一步需要设置管理员密码,管理员的账号是admin。配置完成后,可以登录OpenFire的管理后台。

OpenFire服务器默认开放用户注册、开放建群,所以可以不管OpenFire后台,直接拿来使用。

用户注册

大多XMPP客户端不提供用户注册功能,所以最好在OpenFire后台直接添加用户。试了多个客户端,只有Psi可以成功注册用户,而且登录之后找不到退出的地方,也找不到创建用户的地方。

客户端的选择

XMPP的图形客户端特别多,但是一个比一个难用。在macOS上勉强可以使用的主要有Spark、Adium、Jitsi和Thunderbird。Spark是OpenFire官方提供的客户端,在使用时要注意禁用安全选项,如果主机不受信任的话。

命令行下的客户端,可以使用Profanity

用户登录

登录XMPP账户,需要服务器、用户名、密码三个字段。如果客户端没有提供单独的服务器输入框的话,用户名改用用户名@服务器

Profanity的使用

Profanity不提供用户注册功能,需要先在后台添加用户,或者用其他客户端注册。

Smack的使用

Smack是OpenFire提供的支持XMPP协议的Java接口

引入Smack依赖

Smack示例代码

package bj;

import io.vavr.control.Try;
import lombok.extern.slf4j.Slf4j;
import org.jivesoftware.smack.AbstractXMPPConnection;
import org.jivesoftware.smack.ConnectionConfiguration;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.chat2.ChatManager;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.roster.Roster;
import org.jivesoftware.smack.roster.RosterEntry;
import org.jivesoftware.smack.tcp.XMPPTCPConnection;
import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration;
import org.jivesoftware.smackx.iqregister.AccountManager;
import org.junit.Test;
import org.jxmpp.jid.impl.JidCreate;
import org.jxmpp.jid.parts.Localpart;

import java.io.IOException;
import java.net.InetAddress;
import java.util.Set;

/**
 * Created by BaiJiFeiLong@gmail.com at 2018/11/22 下午4:33
 */
@Slf4j
public class BazTest {

    @Test
    public void testAlpha() throws IOException, InterruptedException, XMPPException, SmackException {
        // 创建连接,并连接到服务器
        AbstractXMPPConnection connection = new XMPPTCPConnection(XMPPTCPConnectionConfiguration.builder()
                .setXmppDomain("172.16.5.254")
                .setHostAddress(InetAddress.getByName("172.16.5.254")) // 服务器地址直接用IP的话,不能用setHost()
                .setSecurityMode(ConnectionConfiguration.SecurityMode.disabled) // 禁用安全模式,如果服务器不受信任
                .build()).connect();

        // 注册用户
        AccountManager accountManager = AccountManager.getInstance(connection);
        accountManager.sensitiveOperationOverInsecureConnection(true);
        accountManager.createAccount(Localpart.from("gamma"), "gamma");

        // 登录
        connection.login("gamma", "gamma");

        // 监听XMPP包(包括PING、在线状态、聊天消息等)
        connection.addSyncStanzaListener(packet -> log.info("[SyncStanzaListener] Packet received {}", packet), stanza -> true);

        // 只监听聊天消息
        ChatManager.getInstanceFor(connection).addIncomingListener((entityBareJid, message, chat) -> {
            System.out.println(String.format("[IncomingListener] Message received %s : %s", entityBareJid, message.getBody()));
            // Echo此消息
            Try.run(() -> chat.send(message.getBody()));
            System.out.println(Roster.getInstanceFor(connection).getEntries());
        });

        // 发送消息
        connection.sendStanza(new Message("theta@172.16.5.254", "MESSAGE_BY_CONNECTION"));
        // 发送消息 方式二
        ChatManager.getInstanceFor(connection).chatWith(JidCreate.entityBareFrom("theta@172.16.5.254")).send("MESSAGE_BY_CHAT_MANAGER");

        // 获取联系人集合
        Thread.sleep(1000); // 等待联系人更新
        Set<RosterEntry> entries = Roster.getInstanceFor(connection).getEntries();
        log.info("Contacts: {} persons", entries.size());
        entries.forEach(System.out::println);

        // 保持运行
        Thread.currentThread().join();
    }
}

控制台日志

10:55:24.924 [Smack Cached Executor] INFO bj.BazTest - [SyncStanzaListener] Packet received IQ Stanza (query jabber:iq:roster) [to=gamma@172.16.5.254/612m1g1ciz,id=gvsBl-5,type=result,]
10:55:24.933 [Smack Cached Executor] INFO bj.BazTest - [SyncStanzaListener] Packet received Presence Stanza [to=gamma@172.16.5.254/612m1g1ciz,from=gamma@172.16.5.254/1gwvh0rdvv,id=L85kQ-6,type=available,]
10:55:24.933 [Smack Cached Executor] INFO bj.BazTest - [SyncStanzaListener] Packet received Presence Stanza [to=gamma@172.16.5.254/612m1g1ciz,from=gamma@172.16.5.254/1pj92olp11,id=RamcN-7,type=available,]
10:55:24.933 [Smack Cached Executor] INFO bj.BazTest - [SyncStanzaListener] Packet received Presence Stanza [to=gamma@172.16.5.254/612m1g1ciz,from=gamma@172.16.5.254/8aadbwm30s,id=xkaYN-7,type=available,]
10:55:24.934 [Smack Cached Executor] INFO bj.BazTest - [SyncStanzaListener] Packet received Presence Stanza [to=gamma@172.16.5.254/612m1g1ciz,from=gamma@172.16.5.254/profanity,id=prof_presence_595,type=available,]
10:55:24.937 [Smack Cached Executor] INFO bj.BazTest - [SyncStanzaListener] Packet received Presence Stanza [to=gamma@172.16.5.254/612m1g1ciz,from=gamma@172.16.5.254/9trigmn7f2,id=SJ41s-7,type=available,]
10:55:24.937 [Smack Cached Executor] INFO bj.BazTest - [SyncStanzaListener] Packet received Presence Stanza [to=gamma@172.16.5.254/612m1g1ciz,from=gamma@172.16.5.254/42awvmq6te,id=iMJUx-6,type=available,]
10:55:24.937 [Smack Cached Executor] INFO bj.BazTest - [SyncStanzaListener] Packet received Presence Stanza [to=gamma@172.16.5.254/612m1g1ciz,from=gamma@172.16.5.254/92ijqtzgxi,id=9gG6o-7,type=available,]
10:55:24.937 [Smack Cached Executor] INFO bj.BazTest - [SyncStanzaListener] Packet received Presence Stanza [to=gamma@172.16.5.254/612m1g1ciz,from=gamma@172.16.5.254/494vwn454w,id=byYRm-7,type=available,]
10:55:24.938 [Smack Cached Executor] INFO bj.BazTest - [SyncStanzaListener] Packet received Presence Stanza [to=gamma@172.16.5.254/612m1g1ciz,from=gamma@172.16.5.254/4wurqkoj8y,id=3BnGN-7,type=available,]
10:55:24.939 [Smack Cached Executor] INFO bj.BazTest - [SyncStanzaListener] Packet received Presence Stanza [to=gamma@172.16.5.254/612m1g1ciz,from=gamma@172.16.5.254/9vx8ve5vdm,id=YSZxk-6,type=available,]
10:55:25.939 [main] INFO bj.BazTest - Contacts: 3 persons
: beta@172.16.5.254
: alpha@172.16.5.254
: theta@172.16.5.254
10:55:26.876 [Smack Cached Executor] INFO bj.BazTest - [SyncStanzaListener] Packet received Message Stanza [to=gamma@172.16.5.254/612m1g1ciz,from=theta@172.16.5.254/profanity,id=prof_msg_670,type=chat,]
[IncomingListener] Message received theta@172.16.5.254 : hello
[: beta@172.16.5.254, : alpha@172.16.5.254, : theta@172.16.5.254]
10:55:28.839 [Smack Cached Executor] INFO bj.BazTest - [SyncStanzaListener] Packet received Message Stanza [to=gamma@172.16.5.254/612m1g1ciz,from=theta@172.16.5.254/profanity,id=prof_msg_671,type=chat,]
[IncomingListener] Message received theta@172.16.5.254 : world
[: beta@172.16.5.254, : alpha@172.16.5.254, : theta@172.16.5.254]

OpenFireAPI的使用

OpenFire通过插件对外提供REST格式的API,用于管理OpenFire服务

插件名: REST API

插件安装: 进入OpenFire控制台,进入插件标签页,选择REST API插件并安装。因为有墙下载不动的话,可以去官网下载,在控制台上传插件。

插件安装完成后,需要在控制台找到REST API的设置页,启用REST API,并指定认证方式(用户名+密码 或 令牌)

通过官方JavaAPI调用

OpenFire提供了JavaAPI,需要添加依赖项:

<dependency>
    <groupId>org.igniterealtime</groupId>
    <artifactId>rest-api-client</artifactId>
    <version>1.1.4</version>
</dependency>

<dependency>
    <groupId>org.glassfish.jersey.inject</groupId>
    <artifactId>jersey-hk2</artifactId>
</dependency>

用法:

@Test
public void testBeta() {
    AuthenticationToken authenticationToken = new AuthenticationToken("vg85cTxbTYuXiOMH");
    RestApiClient restApiClient = new RestApiClient("http://localhost", 9090, authenticationToken);
    UserEntities users = restApiClient.getUsers();
    users.getUsers().forEach($ -> System.out.println($.getUsername()));
}

直接通过HTTP调用

OpenFire的REST-API主要提供了用户管理、群组管理、聊天室管理、系统属性管理、广播消息、踢用户下线等功能,示例调用如下:

### 查询用户列表

GET http://localhost:9090/plugins/restapi/v1/users
Authorization: vg85cTxbTYuXiOMH
Accept: application/json

### 查询alpha用户的通讯录

GET http://localhost:9090/plugins/restapi/v1/users/alpha/roster
Authorization: vg85cTxbTYuXiOMH
Accept: application/json

### 创建用户

POST http://localhost:9090/plugins/restapi/v1/users
Content-Type: application/json
Authorization: vg85cTxbTYuXiOMH
Accept: application/json

{
  "username": "one",
  "password": "one"
}

### 查询聊天室列表

GET http://localhost:9090/plugins/restapi/v1/chatrooms
# Authorization: Basic YWRtaW46c29uZ2ppYW53ZWkxOTkz
# Authorization: Basic YWRtaW46MTIzNDU=
Authorization: vg85cTxbTYuXiOMH
Accept: application/json

### 查询系统属性列表

GET http://localhost:9090/plugins/restapi/v1/system/properties
Authorization: vg85cTxbTYuXiOMH
Accept: application/json

### 查询当前会话列表(当前登录的用户)

GET http://localhost:9090/plugins/restapi/v1/sessions
Authorization: vg85cTxbTYuXiOMH
Accept: application/json

### 广播消息给所有用户

POST http://localhost:9090/plugins/restapi/v1/messages/users
Content-Type: application/json
Authorization: vg85cTxbTYuXiOMH
Accept: application/json

{
  "body": "广播"
}

### 查询安全日志

GET http://localhost:9090/plugins/restapi/v1/logs/security?limit=3
Authorization: vg85cTxbTYuXiOMH
Accept: application/json

OpenFire保存用户聊天记录

OpenFire默认不保存用户聊天记录。需要Monitoring Service监控插件提供支持。

插件安装完成后,默认不归档用户聊天记录。打开OpenFire控制台, 进入服务器 => 档案文件 => 存档设置菜单,勾选Archive one-to-one chatsArchive group chats后,可启用用户聊天记录归档。

聊天记录归档存放在数据表ofMessageArchive中,示例归档记录如下:

<table> <tr><th>messageID</th><th>conversationID</th><th>fromJID</th><th>fromJIDResource</th><th>toJID</th><th>toJIDResource</th><th>sentDate</th><th>body</th></tr> <tr><td>1</td><td>1</td><td>gamma@172.16.5.254</td><td>profanity</td><td>alpha@172.16.5.254</td><td>NULL</td><td>1543326953867</td><td>hello</td></tr> <tr><td>2</td><td>1</td><td>gamma@172.16.5.254</td><td>profanity</td><td>alpha@172.16.5.254</td><td>NULL</td><td>1543326954898</td><td>world</td></tr> <tr><td>3</td><td>1</td><td>alpha@172.16.5.254</td><td>jitsi-357v9t2</td><td>gamma@172.16.5.254</td><td>profanity</td><td>1543327016162</td><td>what</td></tr> <tr><td>4</td><td>1</td><td>alpha@172.16.5.254</td><td>jitsi-357v9t2</td><td>gamma@172.16.5.254</td><td>profanity</td><td>1543327017366</td><td>who</td></tr> <tr><td>5</td><td>1</td><td>gamma@172.16.5.254</td><td>profanity</td><td>alpha@172.16.5.254</td><td>jitsi-357v9t2</td><td>1543327056415</td><td>i know you</td></tr> <tr><td>6</td><td>1</td><td>gamma@172.16.5.254</td><td>profanity</td><td>alpha@172.16.5.254</td><td>jitsi-357v9t2</td><td>1543327063030</td><td>you know me ?</td></tr></table>

OpenFire通过组件实现机器人

OpenFire可以通过Smake实现机器人,但是这种实现一次只能实现一个机器人。组件可以注册到特定子域名,对所有的消息进行拦截处理,可以一次实现无数个机器人。

组件密码可以在OpenFire的后台设置。

示例代码:

@Test
public void testGamma() throws ComponentException, InterruptedException {
    ExternalComponentManager componentManager = new ExternalComponentManager("172.16.5.254");
    componentManager.setSecretKey("MyBot", "mypwd");
    componentManager.setMultipleAllowed("MyBot", true);
    componentManager.addComponent("MyBot", new MyRobot());
    Thread.currentThread().join();
}

static class MyRobot extends AbstractComponent {

    @Override
    public String getDescription() {
        return "This is my bot";
    }

    @Override
    public String getName() {
        return "MyWonderfulRobot";
    }

    @Override
    protected void handleMessage(org.xmpp.packet.Message message) {
        System.out.println("==================");
        send(new org.xmpp.packet.Message() {{
            this.setFrom(message.getTo());
            this.setTo(message.getFrom());
            this.setType(message.getType());
            this.setBody(message.getBody());
        }});
        System.out.println(message);
    }
}

永和可以通过JID(<any>@mybot.172.16.5.254)与机器人通信。

注意:

OpenFire不会自动清理挂掉的机器人,将multipleAllowed设为true后,可以在一个子域名上注册多个机器人,每个机器人对用户消息进行负载均衡。当其中一台机器人挂掉后,用户消息就会按挂掉机器人的比例失去响应。此时只能重新启动OpenFire服务器。

文章首发: https://baijifeilong.github.io/2018/11/22/xmpp


漫漫路,莫论逍遥;潜心修,只为悟道