ZeroMQ proxy 使用心跳检测 server 是否可用

ZeroMQ proxy 使用心跳检测 server 是否可用

1 zmq_proxy

int zmq_proxy (const void *frontend, const void *backend, const void *capture);

zmq_proxy()函数在当前应用程序线程中启动内置的0MQ 代理。

代理将前端套接字连接到后端套接字。从概念上讲,数据从前端流向后端。根据套接字类型,回复可能会以相反的方向流动。该方向只是概念性的;代理是完全对称的,前端和后端之间没有技术差异。

在调用 zmq_proxy() 之前,需要设置必要的套接字选项,并连接或绑定前端和后端套接字。

zmq_proxy() 在当前线程中运行,并且仅在当前上下文关闭时返回

如果捕获套接字不为 NULL,则代理应将前端和后端接收到的所有消息发送到捕获套接字。捕获套接字应该是“ZMQ_PUB”、“ZMQ_DEALER”、“ZMQ_PUSH”或“ZMQ_PAIR”套接字。

使用 proxy 实现异步客户端/服务器模式:

3. Advanced Request-Reply Patterns | ØMQ - The Guide (zeromq.org)

image.png

1.1 proxy

#include <czmq.h>

#include <atomic>
#include <iostream>
#include <thread>
#include <zmq.hpp>

class Proxy
{
public:
    Proxy()
        : _ctx(1),
          _frontend(_ctx, ZMQ_ROUTER),
          _backend(_ctx, ZMQ_DEALER),
          _capture(_ctx, ZMQ_DEALER)
    {
    }
    void run()
    {
        try
        {
            _frontend.bind("tcp://*:5570");
            _backend.bind("tcp://*:5571");
            _capture.bind("inproc://capture");

            /* 捕获消息的线程 */
            zmq::socket_t sck(_ctx, ZMQ_DEALER);
            std::thread th([&]() {
                sck.connect("inproc://capture");
                int cnt = 0;
                while (true)
                {
                    zmq::message_t identity;
                    zmq::message_t msg;
                    sck.recv(&identity);  // 身份帧
                    sck.recv(&msg);  // 消息主体
                    std::cout << "count:" << ++cnt << std::endl;
                }
            });
            th.detach();

            zmq::proxy(static_cast<void *>(_frontend), static_cast<void *>(_backend),
                       static_cast<void *>(_capture));

        } catch (std::exception &e)
        {
            std::cerr << e.what();
        }
    }
    
private:
    zmq::context_t _ctx;
    zmq::socket_t _frontend;
    zmq::socket_t _backend;
    zmq::socket_t _capture;
};

int main()
{
    Proxy proxy;
    proxy.run();
}

server 和 client 代码详见 [[#4 完整代码]]。

运行结果:

捕获套接字共捕获到 30 条消息(5 条请求,25 条回复)。

在这里插入图片描述

2 设置心跳

由于 zmq_proxy() 在管道中存在积压的的未消费的消息时,会导致高额的 CPU 开销,为了解决这一问题,需要在没有消费者(server)时,及时关闭 proxy,可能导致消息积压的原因有:网络中断,server 进程退出等。

TCP 的连接信息是由内核维护的,所以当服务端的进程正常退出或崩溃时,内核需要回收该进程的所有 TCP 连接资源,于是内核会发送第一次挥手 FIN 报文,后续的挥手过程也都是在内核完成,并不需要进程的参与,所以即使服务端的进程退出了,还是能与客户端完成 TCP 四次挥手的过程,这个时候只需要设置 seocket_monitor 监听 ZMQ_EVENT_DISCONNECTED 事件,关闭代理线程即可。

但是当网络中断时,tcp 连接仍然存在,不会触发 ZMQ_EVENT_DISCONNECTED。这时候就需要向 server 发送心跳包,检测 server 是否存活。

zmq_setsockopt 中与心跳有关的三个选项:

  1. ZMQ_HEARTBEAT_IVL 选项应设置为指定“套接字”发送 ZMTP 心跳之间的间隔。如果设置此选项且大于 0,则每 ZMQ_HEARTBEAT_IVL 毫秒将发送“PING”ZMTP 命令;
  2. ZMQ_HEARTBEAT_TIMEOUT 选项应设置在发送“PING”ZMTP 命令且未接收任何流量后连接超时之前等待的时间。该选项仅在 ZMQ_HEARTBEAT_IVL 也被设置且大于 0 时才有效。如果发送“PING”命令后没有收到流量,连接将超时,但收到的流量不必是“PONG” 命令,任何收到的流量都将取消超时;
  3. ZMQ_HEARTBEAT_TTL 选项应设置远端对等点上 ZMTP 心跳的超时。如果该选项大于0,则如果远端在 TTL 时间内没有收到更多流量,则连接超时。如果 ZMQ_HEARTBEAT_IVL 未设置或为 0,此选项不会产生任何影响。在内部,此值会向下舍入到最接近的十分之一,任何小于 100 的值都不会产生任何影响。

在代理的后端套接字添加心跳(需要在 bind 之前设置):

//...
_backend.setsockopt(ZMQ_HEARTBEAT_IVL, 1000);
_backend.setsockopt(ZMQ_HEARTBEAT_TIMEOUT, 5000);

_frontend.bind("tcp://*:5570");
_backend.bind("tcp://*:5571");
_capture.bind("inproc://capture");
// ...

这样,proxy 会对连接到 _backend 的每一个 tcp 连接发送心跳。设置了 seocket_monitor 监听后,连接超时时,会触发 ZMQ_EVENT_DISCONNECTED 事件(在局域网内两台机器上分别启动 proxy 与 server,一段时间后断开网络连接,达到超时时间后触发 ZMQ_EVENT_DISCONNECTED 事件)。

使用 tcpdump -i lo -vnnX port 5571 抓包可以看到 proxy 与 server 之间的一秒一次的 ping pong 心跳。

3 监听 ZMQ_EVENT_DISCONNECTED 事件

int zmq_socket_monitor (void *socket, char *endpoint, int events);

zmq_socket_monitor() 方法允许应用程序线程跟踪 ZeroMQ 套接字上的套接字事件(例如连接)。每次调用此方法都会创建一个“ZMQ_PAIR”套接字并将其绑定到指定的 inproc://prot。要收集套接字事件,需要手动创建的“ZMQ_PAIR”套接字,并将其连接到端点。

“events”参数是监视的套接字事件的位掩码,可监听的事件详见 :zmq_socket_monitor(3) (libzmq.readthedocs.io)。要监视所有事件,请使用事件值 ZMQ_EVENT_ALL。注意:随着新事件的添加,catch-all 值将开始返回它们。依赖于严格且固定的事件序列的应用程序不得使用 ZMQ_EVENT_ALL 以保证与未来版本的兼容性。

每个事件都作为两个帧发送。第一帧包含事件编号(16位)和根据事件编号提供附加数据的事件值(32位)。第二帧包含一个指定受影响端点的字符串。

当一个 server 主动断开或者心跳超时后,会触发 ZMQ_EVENT_DISCONNECTED 事件。

3.1 获取事件类型

官方例子:

static int get_monitor_event(void *monitor, int *value, char **address)
{
    // 第一帧包含事件编号(16位)和根据事件编号提供附加数据的事件值(32位)
    zmq_msg_t msg;
    zmq_msg_init(&msg);
    if (zmq_msg_recv(&msg, monitor, 0) == -1) return -1;  //  Interrupted, presumably
    assert(zmq_msg_more(&msg));

    uint8_t *data = (uint8_t *)zmq_msg_data(&msg);  // 指向“msg”引用的消息对象的消息内容的指针
    uint16_t event = *(uint16_t *)(data);         // 事件编号
    if (value) *value = *(uint32_t *)(data + 2);  // 事件值

    // 第二帧包含一个指定受影响端点的字符串。
    zmq_msg_init(&msg);
    if (zmq_msg_recv(&msg, monitor, 0) == -1) return -1;  //  Interrupted, presumably
    assert(!zmq_msg_more(&msg));

    if (address)  // 指向字符串指针的指针
    {
        uint8_t *data = (uint8_t *)zmq_msg_data(&msg);
        size_t size = zmq_msg_size(&msg);
        // 分配 size + 1 的空间给字符串,返回其地址,赋值给 address 指向的空间
        *address = (char *)malloc(size + 1);
        memcpy(*address, data, size);
        (*address)[size] = 0;
    }
    return event;
}

3.2 处理事件

// 套接字监控仅适用于 inproc://
int rc = zmq_socket_monitor(_backend, "inproc://monitor", ZMQ_EVENT_ALL);
assert(rc == 0);

/* 收集监听事件 */
zmq::socket_t server_mon(_ctx, ZMQ_PAIR);
std::thread th1([&]() {
    server_mon.connect("inproc://monitor");
    int event = 0;
    while (event != ZMQ_EVENT_MONITOR_STOPPED)
    {
        event = get_monitor_event(server_mon, NULL, NULL);
        switch (event)
        {
            case ZMQ_EVENT_LISTENING:
                std::cout << "ZMQ_EVENT_LISTENING" << std::endl;
                break;
            case ZMQ_EVENT_ACCEPTED:
                std::cout << "ZMQ_EVENT_ACCEPTED" << std::endl;
                break;
            case ZMQ_EVENT_HANDSHAKE_SUCCEEDED:
                std::cout << "ZMQ_EVENT_HANDSHAKE_SUCCEEDED" << std::endl;
                ++_serverCnt;
                break;
            case ZMQ_EVENT_DISCONNECTED:
                std::cout << "ZMQ_EVENT_DISCONNECTED" << std::endl;
                --_serverCnt;
                if (_serverCnt == 0)
                {
                    // 关闭proxy,注意资源回收
                }
                break;
            case ZMQ_EVENT_CLOSED:
                std::cout << "ZMQ_EVENT_DISCONNECTED" << std::endl;
                break;
            case ZMQ_EVENT_MONITOR_STOPPED:
                std::cout << "ZMQ_EVENT_MONITOR_STOPPED" << std::endl;
                break;
            default:
                break;
        }
    }
});

4 完整代码

以下代码实现了多对多的异步客户端/服务器模式,proxy 在 server 数量为零时,会关闭代理。

运行结果:

在这里插入图片描述

4.1 proxy

#include <czmq.h>

#include <atomic>
#include <condition_variable>
#include <iostream>
#include <thread>
#include <zmq.hpp>

class Proxy
{
public:
    Proxy()
        : _ctx(1),
          _frontend(_ctx, ZMQ_ROUTER),
          _backend(_ctx, ZMQ_DEALER),
          _capture(_ctx, ZMQ_DEALER),
          _eventCollect(_ctx, ZMQ_PAIR),
          _msgCollect(_ctx, ZMQ_DEALER)
    {
    }

    void init()
    {
        try
        {
            // 异步处理关闭 proxy
            std::thread t([&]() {
                while (true)
                {
                    std::unique_lock<std::mutex> lck(_mtx);
                    _cond.wait(lck);
                    // 等待服务重启
                    std::this_thread::sleep_for(std::chrono::seconds(3));
                    if (_serverCnt == 0)
                    {
                        //  直接使用 pthread_cancel 可能会导致zmq上下文崩溃,建议使用 proxy_steerable() 代替 proxy(), 向 control 发送 TERMINATE 指令
                        pthread_cancel(_proxyThread.native_handle());
                        break;
                    }
                }
            });

            t.detach();

            // 套接字监控仅适用于 inproc://
            int rc = zmq_socket_monitor(_backend, "inproc://monitor", ZMQ_EVENT_ALL);
            assert(rc == 0);

            /* 收集监听事件 */
            std::thread th1([&]() {
                _eventCollect.connect("inproc://monitor");
                int event = 0;
                while (event != ZMQ_EVENT_MONITOR_STOPPED)
                {
                    event = get_monitor_event(_eventCollect, NULL, NULL);
                    switch (event)
                    {
                        case ZMQ_EVENT_LISTENING:
                            std::cout << "ZMQ_EVENT_LISTENING" << std::endl;
                            break;
                        case ZMQ_EVENT_ACCEPTED:
                            std::cout << "ZMQ_EVENT_ACCEPTED" << std::endl;
                            break;
                        case ZMQ_EVENT_HANDSHAKE_SUCCEEDED:
                            std::cout << "ZMQ_EVENT_HANDSHAKE_SUCCEEDED" << std::endl;
                            ++_serverCnt;
                            break;
                        case ZMQ_EVENT_DISCONNECTED:
                            std::cout << "ZMQ_EVENT_DISCONNECTED" << std::endl;
                            --_serverCnt;
                            if (_serverCnt == 0)
                            {
                                // 关闭proxy
                                _cond.notify_one();
                            }
                            break;
                        case ZMQ_EVENT_CLOSED:
                            std::cout << "ZMQ_EVENT_CLOSED" << std::endl;
                            break;
                        case ZMQ_EVENT_MONITOR_STOPPED:
                            std::cout << "ZMQ_EVENT_MONITOR_STOPPED" << std::endl;
                            break;
                        default:
                            break;
                    }
                }
            });
            _monitorThread.swap(th1);

            _frontend.setsockopt(ZMQ_SNDHWM, 10000);
            _backend.setsockopt(ZMQ_HEARTBEAT_IVL, 1000);
            _backend.setsockopt(ZMQ_HEARTBEAT_TIMEOUT, 5000);

            _frontend.bind("tcp://*:5570");
            _backend.bind("tcp://*:5571");
            _capture.bind("inproc://capture");

            /* 捕获消息的线程 */
            std::thread th2([&]() {
                _msgCollect.connect("inproc://capture");
                int cnt = 0;
                while (true)
                {
                    zmq::message_t identity;
                    zmq::message_t msg;
                    //
                    _msgCollect.recv(&identity);
                    _msgCollect.recv(&msg);

                    std::cout << "count:" << ++cnt << std::endl;
                }
            });
            _captureThread.swap(th2);

        } catch (std::exception &e)
        {
            std::cerr << e.what();
        }
    }

    void join()
    {
        _proxyThread.join();
        _frontend.close();
        _backend.close();
        _capture.close();
        std::cout << "proxy exit." << std::endl;

        pthread_cancel(_captureThread.native_handle());
        _captureThread.join();
        _msgCollect.close();
        _monitorThread.join();
        _eventCollect.close();
        _ctx.close();
    }

    void run()
    {
        try
        {
            std::thread th([&]() {
                zmq::proxy(static_cast<void *>(_frontend), static_cast<void *>(_backend),
                           static_cast<void *>(_capture));
            });
            _proxyThread.swap(th);
            std::cout << "proxy running..." << std::endl;

        } catch (std::exception &e)
        {
            std::cerr << e.what();
        }
    }

private:
    zmq::context_t _ctx;
    zmq::socket_t _frontend;
    zmq::socket_t _backend;
    zmq::socket_t _capture;
    zmq::socket_t _eventCollect;
    zmq::socket_t _msgCollect;
    std::atomic<int> _serverCnt{0};
    std::thread _proxyThread;
    std::thread _captureThread;
    std::thread _monitorThread;
    mutable std::mutex _mtx;
    std::condition_variable _cond;

private:
    int get_monitor_event(void *monitor, int *value, char **address)
    {
        // 第一帧包含事件编号(16位)和根据事件编号提供附加数据的事件值(32位)
        zmq_msg_t msg;
        zmq_msg_init(&msg);
        if (zmq_msg_recv(&msg, monitor, 0) == -1) return -1;  //  Interrupted, presumably
        // assert(zmq_msg_more(&msg));

        uint8_t *data = (uint8_t *)zmq_msg_data(&msg);  // 指向“msg”引用的消息对象的消息内容的指针
        uint16_t event = *(uint16_t *)(data);         // 事件编号
        if (value) *value = *(uint32_t *)(data + 2);  // 事件值

        // 第二帧包含一个指定受影响端点的字符串。
        zmq_msg_init(&msg);
        if (zmq_msg_recv(&msg, monitor, 0) == -1) return -1;  //  Interrupted, presumably
        // assert(!zmq_msg_more(&msg));

        if (address)  // 指向字符串指针的指针
        {
            uint8_t *data = (uint8_t *)zmq_msg_data(&msg);
            size_t size = zmq_msg_size(&msg);
            // 分配 size + 1 的空间给字符串,返回其地址,赋值给 address 指向的空间
            *address = (char *)malloc(size + 1);
            memcpy(*address, data, size);
            (*address)[size] = 0;
        }
        return event;
    }
};

int main()
{
    Proxy proxy;
    proxy.init();
    proxy.run();
    proxy.join();
}

4.2 server

#include <czmq.h>

#include <thread>
#include <zmq.hpp>

class Server
{
public:
    Server() : _ctx(1), _serverSocket(_ctx, ZMQ_DEALER) {}

    void start()
    {
        _serverSocket.connect("tcp://localhost:5571");

        try
        {
            while (true)
            {
                zmq::message_t identity;
                zmq::message_t msg;
                zmq::message_t copied_id;

                _serverSocket.recv(&identity);
                _serverSocket.recv(&msg);

                // int replies = within(3000);
                // int replies = within(10);
                for (int reply = 0; reply < 5; ++reply)
                {
                    // s_sleep(within(1000) + 1);
                    copied_id.copy(&identity);
                    _serverSocket.send(copied_id, ZMQ_SNDMORE);
                    char response_string[50] = {};
                    sprintf(response_string, "%s, response #%d", msg.to_string().c_str(), reply);
                    _serverSocket.send(response_string, strlen(response_string), ZMQ_DONTWAIT);
                    std::this_thread::sleep_for(std::chrono::milliseconds(1));
                }
            }
        } catch (std::exception &e)
        {
        }
    }

private:
    zmq::context_t _ctx;
    zmq::socket_t _serverSocket;
};

int main()
{
    Server ser;
    ser.start();
}

4.3 client

#include <czmq.h>

#include <iostream>
#include <thread>
#include <zmq.hpp>

#define within(num) (int)((float)((num)*random()) / (RAND_MAX + 1.0))

class Client
{
public:
    Client() : _ctx(1), _clientSocket(_ctx, ZMQ_DEALER) {}
    void start()
    {
        char identity[10] = {};
        sprintf(identity, "%04X-%04X", within(0x10000), within(0x10000));
        // printf("%s\n", identity);
        _clientSocket.setsockopt(ZMQ_IDENTITY, identity, strlen(identity));
        // _clientSocket.setsockopt(ZMQ_SNDHWM, 100);
        _clientSocket.setsockopt(ZMQ_RCVHWM, 5000);
        _clientSocket.connect("tcp://localhost:5570");
        // _clientSocket.connect("tcp://10.113.110.107:5570");
        zmq::pollitem_t items[] = {{_clientSocket, 0, ZMQ_POLLIN, 0}};
        int request_nbr = 0;
        try
        {
            std::thread th([&]() {
                while (true)
                {
                    zmq::poll(items, 1, 1);
                    if (items[0].revents & ZMQ_POLLIN)
                    {
                        // printf("\n%s ", identity);
                        s_dump(_clientSocket);
                    }
                }
            });
            int cnt = 100;
            do
            {
                char request_string[16] = {};
                sprintf(request_string, "request #%d", ++request_nbr);
                std::cout << "send " << request_string << std::endl;
                _clientSocket.send(request_string, strlen(request_string), ZMQ_DONTWAIT);
                std::this_thread::sleep_for(std::chrono::seconds(1));
                
            } while (--cnt);

            th.join();
            std::this_thread::sleep_for(std::chrono::seconds(60));

        } catch (std::exception &e)
        {
        }
    }

private:
    zmq::context_t _ctx;
    zmq::socket_t _clientSocket;

private:
    void s_dump(zmq::socket_t &socket)
    {
        // std::cout << "----------------------------------------" << std::endl;

        while (1)
        {
            //  Process all parts of the message
            zmq::message_t message;
            socket.recv(&message);

            //  Dump the message as text or binary
            size_t size = message.size();
            std::string data(static_cast<char *>(message.data()), size);

            bool is_text = true;

            size_t char_nbr;
            unsigned char byte;
            for (char_nbr = 0; char_nbr < size; char_nbr++)
            {
                byte = data[char_nbr];
                if (byte < 32 || byte > 127) is_text = false;
            }
            std::cout << "[" << std::setfill('0') << std::setw(3) << size << "]";
            for (char_nbr = 0; char_nbr < size; char_nbr++)
            {
                if (is_text)
                    std::cout << (char)data[char_nbr];
                else
                    std::cout << std::setfill('0') << std::setw(2) << std::hex
                              << (unsigned int)data[char_nbr];
            }
            std::cout << std::endl;

            int more = 0;  //  Multipart detection
            size_t more_size = sizeof(more);
            socket.getsockopt(ZMQ_RCVMORE, &more, &more_size);
            if (!more) break;  //  Last message part
        }
    }
};

int main()
{
    Client cli;
    cli.start();
}

5 参考资料

  • 20
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AWei_i_i

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值