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)
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 中与心跳有关的三个选项:
- ZMQ_HEARTBEAT_IVL 选项应设置为指定“套接字”发送 ZMTP 心跳之间的间隔。如果设置此选项且大于 0,则每 ZMQ_HEARTBEAT_IVL 毫秒将发送“PING”ZMTP 命令;
- ZMQ_HEARTBEAT_TIMEOUT 选项应设置在发送“PING”ZMTP 命令且未接收任何流量后连接超时之前等待的时间。该选项仅在 ZMQ_HEARTBEAT_IVL 也被设置且大于 0 时才有效。如果发送“PING”命令后没有收到流量,连接将超时,但收到的流量不必是“PONG” 命令,任何收到的流量都将取消超时;
- 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();
}