消息队列 Pulsar 入门:Node.js + Pulsar 实现简单的聊天室应用

2,096 阅读5分钟

image.png

Pulsar 是云原生时代的分布式消息队列平台,最早由 Yahoo 开发,现由 Apache 基金会维护。

一般来说,在日常开发中,我们可能较少需要直接接触到需要应用消息队列的场景,通常一般的数据库或 Redis 等内存缓存即可满足我们的要求。

但是在开发中,我们却经常使用发布-订阅模式,例如跨语言的 ReactiveX,前端中的 document.addEventListener,Node.js 中的 process.on 等。

消息队列即是在不同的服务,客户端间实现了这一模式,当系统本身不同服务间具有类似生产者和消费者的发布订阅关系时,消息队列即可以提供方便的功能。当然,在系统需要将耗时事件滞后处理时,消息队列也可以通过缓存事件,提供了提升系统的总体吞吐量,提升系统性能。

消息队列的工作模式不难理解,很多地方也有介绍,但怎么具体使用起来的实例还是较少,因此,本文就此用一个简单的例子具体讲解:

部署 Pulsar

和其他文章中一样,为了部署的便捷性和不同系统环境下的一致性,以及最终方便的迁移到云原生平台,我们尽量借助 Docker 实现 Pulsar 的部署。

具体的,在 Docker Hub 上,Pulsar 官方账号 apachepulsar 提供了很多官方镜像,我们通过编写 docker-compose 文件实现它的本地部署。

version: "3.1"
services:
    pulsar:
        # 使用 pulsar 官方镜像
        image: apachepulsar/pulsar:latest
        # 将 pulsar 的数据和配置映射出来
        volumes:
            - ./pulsar/data:/pulsar/data
            - ./pulsar/conf:/pulsar/conf
        # 导出两个外部需要的端口
        ports:
            # 二进制 API 端口
            - 6650:6650
            # HTTP API 端口
            - 8080:8080
        # 以 standalone 模式运行 pulsar
        command: bin/pulsar standalone
        # 限制使用内存,加速启动
        environment:
            PULSAR_MEM: " -Xms512m -Xmx512m -XX:MaxDirectMemorySize=1g"
    pulsar-dashboard:
        image:  apachepulsar/pulsar-dashboard
        depends_on: 
            - pulsar
        environment: 
            # 指定 pulsar HTTP 端口的路径
            #(pulsar 会被 docker-compose 的 DNS 服务解析为 docker 内部的虚拟 IP)
            SERVICE_URL: http://pulsar:8080/
        ports:
            # Web Dashboard 端口导出到 80
            - 80:80

Pulsar Node.js Client 安装

接下来,我们用 Node.js 来与 Pulsar 建立连接,一般来说,SDK 的安装只需要 npm install xxx 即可,可惜 Pulsar 的 Node.js Client 并不是很完善,而且依赖于 C++ Client,故这一步还略为复杂。

image.png

我们可以通过这里提供的方式以及要求安装 apache-pulsar-clientapache-pulsar-client-dev 两个包:

wget https://downloads.apache.org/pulsar/pulsar-2.7.2/DEB/apache-pulsar-client-dev.deb
wget https://downloads.apache.org/pulsar/pulsar-2.7.2/DEB/apache-pulsar-client.deb
apt install ./apache-pulsar-client.deb ./apache-pulsar-client-dev.deb
# 初始化 NPM 项目
npm init

接下来,我们安装 Pulsar Node.js 客户端:

# 安装 Pulsar 客户端
npm install pulsar-client@1.2.0

Windows 下开发

需要注意的是,在 Windows 下因为无法下载到 libpulsar 的二进制包,故我们只能采用在虚拟机中安装的方式,这里,我们利用容器创建一个开发环境:

FROM node:lts
WORKDIR /client
COPY package.json ./
RUN wget https://downloads.apache.org/pulsar/pulsar-2.7.2/DEB/apache-pulsar-client-dev.deb
RUN wget https://downloads.apache.org/pulsar/pulsar-2.7.2/DEB/apache-pulsar-client.deb
RUN ls
RUN apt install ./apache-pulsar-client.deb ./apache-pulsar-client-dev.deb
RUN npm install pulsar-client --save

在 compose 中加入构建并运行 pulsar-client 部分:

version: "3.1"
services:
    pulsar:
        # ...
    pulsar-dashboard:
        # ...
    # `tail -f` 可以防止镜像运行后退出
    pulsar-client:
        build: ./client
        command: tail -f

在构建成功后,我们可以借助 vscode 的 Docker 插件接入容器继续进行开发:

image.png

Node.js 聊天室开发

首先,和数据库类似的,我们需要创建一个 Pulsar 的客户端实例:

// 引入 Client
const Pulsar = require('pulsar-client');

(async () => {
    console.log('[Enter Chatroom]')

    // 创建 Pulsar Client 的实例
    const client = new Pulsar.Client({
        // 因为 pulsar 和 Client 在一个 docker-compose 服务中,用 pulsar 作为主机名即可查到它在服务中的 IP
        serviceUrl: 'pulsar://pulsar:6650',
        log: () => {},
        operationTimeoutSeconds: 30,
    });
})();

而消息队列分为生产者 Producer 和消费者 Consumer 两种角色,在这里,我们发送消息的人对应为生产者,而所有人都可以接受到他人的消息,即为消费者。而生产者和消费者都是对于一个主题 Topic 进行操作,生产者将消息放入一个主题中,而消费者使用其中的消息,不同主题直接不会互相影响。

首先,我们在全局变量中定义固定的主题名,并从环境变量中获取用户名:

const topic = 'persistent://public/default/chat-room';
const username = process.env.USERNAME;

接下来,我们监听用户的输入,并在用户输入一行消息后,将消息发送到消息队列中:

async function createMessageSender(client) {
    // 创建生产者
    const producer = await client.createProducer({
        topic,
        sendTimeoutMs: 1000,
        batchingEnabled: true,
    });


    const rl = readline.createInterface({
        input: process.stdin,
        output: process.stdout
    });

    // 当输入一行时,发送消息
    rl.on('line', async (input) => {
        const data = { username, message: input };
        // 序列化消息并发送
        const msg = JSON.stringify(data);
        await producer.send({
            data: Buffer.from(msg),
        });
        console.log(`[SEND] ${data.message}`);
    });
}

而当消费者收到消息时,我们则将消息打印出来:

async function createMessageReceiver(client) {
    // 当收到消息时,输出消息发送者和内容
    await client.subscribe({
        topic,
        subscription: username,
        subscriptionType: 'Shared',
        ackTimeoutMs: 10000,
        listener: (msg, msgConsumer) => {
            // 反序列化消息并输出
            const data = JSON.parse(msg.getData().toString());
            console.log(`[FROM ${data.username}] ${data.message}`);
            msgConsumer.acknowledge(msg);
        },
    });
}

最后,在创建 Pulsar Client 后调用这两个函数,我们的程序就完成了,完整的程序如下:

// 引入 Client
const Pulsar = require('pulsar-client');
const readline = require('readline');


const topic = 'persistent://public/default/chat-room';
const username = process.env.USERNAME;

async function createMessageSender(client) {
    // 创建生产者
    const producer = await client.createProducer({
        topic,
        sendTimeoutMs: 1000,
        batchingEnabled: true,
    });


    const rl = readline.createInterface({
        input: process.stdin,
        output: process.stdout
    });

    // 当输入一行时,发送消息
    rl.on('line', async (input) => {
        const data = { username, message: input };
        // 序列化消息并发送
        const msg = JSON.stringify(data);
        await producer.send({
            data: Buffer.from(msg),
        });
        console.log(`[SEND] ${data.message}`);
    });
}

async function createMessageReceiver(client) {
    // 当收到消息时,输出消息发送者和内容
    await client.subscribe({
        topic,
        subscription: username,
        subscriptionType: 'Shared',
        ackTimeoutMs: 10000,
        listener: (msg, msgConsumer) => {
            // 反序列化消息并输出
            const data = JSON.parse(msg.getData().toString());
            console.log(`[FROM ${data.username}] ${data.message}`);
            msgConsumer.acknowledge(msg);
        },
    });
}

(async () => {
    console.log('[Enter Chatroom]')

    const client = new Pulsar.Client({
        serviceUrl: 'pulsar://pulsar:6650',
        log: () => {},
        operationTimeoutSeconds: 30,
    });

    createMessageSender(client);
    createMessageReceiver(client);

    process.on('SIGINT', async () => {
        console.log('[Exit Chatroom]')
        await client.close();
        process.exit();
    })
})();

运行并测试

我们可以在三个命令行窗口中依次分别启动三个不同用户名的程序,具体如下:

USERNAME=user1 node index.js
USERNAME=user2 node index.js
USERNAME=user3 node index.js

然后,在三个窗口中输入消息并回车,他们就可以看到相互发送的消息了:

image.png

就此,我们借助消息队列的能力实现了一个简单的聊天室,借助该程序,只要能够连接到 Pulsar 消息队列上,即可和其他客户端交换信息。

总结

借这个简单的例子,我们大概看到了 Pulsar 消息队列的使用方式和工作的效果,相较 Kafka,Pulsar 的部署还是非常方便的。但是,Pulsar 对不同操作系统和语言环境的支持也还不是特别完善。

另外,Pulsar 的强大之处还在于它分布式部署的能力和实现消息异地同步的功能,在这里我们只是使用了基本的功能,还没有对它的这些特性进行实践与应用。