broker的目的
相对于XPub/XSub模式,我们很容易想到Pub/Sub模式,即订阅发布模式。当我们使用ZeroMQ创建一个包含订阅发布模式的系统时,我们通常创建一个消息的发布者,即Publisher,和若干个消息订阅者(Subscriber)。消息的发布者绑定端口,订阅者通过发布者的IP和端口连接发布者,并且注册消息主题(Topic),然后进行接收匹配主题的消息。整体结构如下图:
在订阅发布模式下,订阅者可以动态加入,随时连接消息的发布者,然后接收消息。但是,在这种结构中,如果有新的Publisher加入,那么所有订阅者都需要连接到这个Publisher上。如果系统中有成百上千的订阅者,每一个新的Publisher的加入都会给系统造成很大的操作成本,这显然限制了系统规模。
要解决这个问题,也很简单,就像只有一个发布者情况,所有的订阅者都只与这一个消息发布者交互,不管是Publisher内部发生什么变化,Subscriber都可以动态感知这种变化。所以很容易我们可以想到创建一个中间件来解耦Publishers和Subscribers,所有Subscriber都只与这一个中间件交互,换句话说,这个中间件从很多个Publisher那里接收消息,然后转发给Subscibers。事实上,有了这个中间件,我们可以做很多Pub/Sub模式做不了的事情,比如说对传送过程中的消息进行管理,重构,或者对系统进行负载均衡等等。我们把这个中间件称为Broker,上面说的这种模式,我们称之为XPub/XSub模式。
XPub/XSub
在XPub/XSub模式中,对Publisher来说由Pub/Sub模式中的bind操作变成了connect操作,connect的对象为Broker中的XSub端口。对Subscriber而言,和Publisher的操作一样,只不过connect的是Broker的XPub端口。在Broker中我们绑定XSub和XPub这两个端口。Proxy的作用即为中转消息,在ZMQ的API中提供了zmq.proxy方法来中转消息,其实Proxy就是一个代码块,在这个代码块中可以做任何我们想做的操作。后面会介绍一个简单的例子。从XPub/XSub这个模式中,我们可以发现,不管是Publisher还是Subscriber,它们的加入和离开都可以被系统动态发现。
从上图可以看出,在增加了broker(XSUB_XPUB)之后,Publish和Subscriber都可以动态的加入和离开,并且双方都是透明的,互不影响。这就达到了动态扩展和横向水平扩容的目的。
示例代码
publisher
/**
* @file pub.cpp
* @brief pub demo
* @author shlian
* @version 1.0
* @date 2020-11-06
*/
#include <chrono>
#include <iostream>
#include <string>
#include <gflags/gflags.h>
#include <zmqpp/context.hpp>
#include <zmqpp/context_options.hpp>
#include <zmqpp/loop.hpp>
#include <zmqpp/message.hpp>
#include <zmqpp/socket_types.hpp>
#include <zmqpp/zmqpp.hpp>
#include "../include/common.h"
DEFINE_string(broker_endpoint,"tcp://127.0.0.1:15556","the broker backend endpoint that connected by pub server");
DEFINE_int32(pub_interval,1000,"publish interval,ms");
DEFINE_int32(io_thread_count,1,"the io_thread count of the zeromq context");
DEFINE_string(id,"pub","the server id");
bool pub_message(zmqpp::socket &socket);
int main(int argc, char *argv[])
{
gflags::SetUsageMessage("Usage");
gflags::ParseCommandLineFlags(&argc,&argv,true);
zmqpp::context context;
context.set(zmqpp::context_option::io_threads,FLAGS_io_thread_count);
zmqpp::socket pub_socket(context,zmqpp::socket_type::pub);
pub_socket.connect(FLAGS_broker_endpoint);
zmqpp::loop looper;
looper.add(std::chrono::milliseconds(FLAGS_pub_interval),0,std::bind(pub_message,std::ref(pub_socket)));
looper.start();
return 0;
}
bool pub_message(zmqpp::socket &socket)
{
static unsigned long long index=0;
zmqpp::message msg;
if(index++%2==0)
{
msg.add("even");
}else{
msg.add("odd");
}
msg.add(FLAGS_id+":"+common::format_time());
LOG_INFO(FLAGS_id<<" pub["<<msg.get(0)<<"],["<<msg.get(1)<<"]");
auto res=socket.send(msg);
return res;
}
broker
/**
* @file broker.cpp
* @brief broker demo
* @author shlian
* @version 1.0
* @date 2020-11-06
*/
#include <gflags/gflags.h>
#include <zmqpp/context.hpp>
#include <zmqpp/context_options.hpp>
#include <zmqpp/loop.hpp>
#include <zmqpp/socket_options.hpp>
#include <zmqpp/socket_types.hpp>
#include <zmqpp/zmqpp.hpp>
#include "../include/common.h"
DEFINE_string(front_endpoint,"tcp://*:15555","the endpoint of the broker that connected by sub client");
DEFINE_string(backend_endpoint,"tcp://*:15556","the endpoint of the broker that connected by pub server");
DEFINE_int32(io_thread_count,1,"the io_thread count of the zeromq context");
bool front_proxy(zmqpp::socket &xpub_socket,zmqpp::socket &xsub_scoket);
bool backend_proxy(zmqpp::socket &xsub_scoket,zmqpp::socket &xpub_socket);
int main(int argc, char *argv[])
{
gflags::SetUsageMessage("Usage");
gflags::ParseCommandLineFlags(&argc,&argv,true);
zmqpp::context context;
context.set(zmqpp::context_option::io_threads,FLAGS_io_thread_count);
//bind front endpoint
zmqpp::socket xpub_socket(context,zmqpp::socket_type::xpub);
xpub_socket.set(zmqpp::socket_option::xpub_verbose,1);
xpub_socket.bind(FLAGS_front_endpoint);
//bind backend endpoint
zmqpp::socket xsub_socket(context,zmqpp::socket_type::xsub);
xsub_socket.bind(FLAGS_backend_endpoint);
xsub_socket.send(std::string(1,0x01));
//start event loop
zmqpp::loop looper;
looper.add(xsub_socket,std::bind(backend_proxy,std::ref(xsub_socket),std::ref(xpub_socket)));
looper.add(xpub_socket,std::bind(front_proxy,std::ref(xpub_socket),std::ref(xsub_socket)));
looper.start();
return 0;
}
unsigned long long forward_topic=0;
bool front_proxy(zmqpp::socket &xpub_socket,zmqpp::socket &xsub_socket)
{
zmqpp::message msg;
bool res=xpub_socket.receive(msg);
if(res)
{
++forward_topic;
//std::string topic=msg.get(0);//must manage the topics and process subscribe and unsubscribe topic,because socket receives only topic part and do not known if subscribe or unsubscribe
LOG_INFO("handle subscribe topic:["<<msg.get(0)<<"],parts="<<msg.parts());
std::string topic(1,0x01);
topic.append(msg.get(0));
res=xsub_socket.send(topic);
}
return res;
}
unsigned long long forward_data_msg=0;
bool backend_proxy(zmqpp::socket &xsub_socket,zmqpp::socket &xpub_socket)
{
zmqpp::message msg;
bool res=xsub_socket.receive(msg);
if(res)
{
res=xpub_socket.send(msg);
if((res)&&(forward_data_msg++%100==0))
{
LOG_INFO("forward:"<<forward_data_msg<<" data messages");
}
}
return res;
}
subscriber
/**
* @file sub.cpp
* @brief sub demo
* @author shlian
* @version 1.0
* @date 2020-11-06
*/
#include <gflags/gflags.h>
#include <zmqpp/context.hpp>
#include <zmqpp/context_options.hpp>
#include <zmqpp/socket.hpp>
#include <zmqpp/socket_types.hpp>
#include <zmqpp/zmqpp.hpp>
#include "../include/common.h"
DEFINE_string(broker_endpoint,"tcp://127.0.0.1:15555","the broker endpoint that connected by sub client");
DEFINE_string(topic,"","the subscribing topics");
DEFINE_int32(io_thread_count,1,"the io_thread count of the zeromq context");
bool handle_message(zmqpp::socket &socket);
int main(int argc, char *argv[])
{
gflags::SetUsageMessage("Usage");
gflags::ParseCommandLineFlags(&argc,&argv,true);
zmqpp::context context;
context.set(zmqpp::context_option::io_threads,FLAGS_io_thread_count);
zmqpp::socket sub_socket(context,zmqpp::socket_type::sub);
sub_socket.connect(FLAGS_broker_endpoint);
sub_socket.subscribe(FLAGS_topic);
LOG_INFO("subscribe:"<<FLAGS_topic);
zmqpp::loop looper;
looper.add(sub_socket,std::bind(handle_message,std::ref(sub_socket)));
looper.start();
return 0;
}
bool handle_message(zmqpp::socket &socket)
{
zmqpp::message msg;
auto res=socket.receive(msg);
if(res)
{
LOG_INFO("recv ["<<msg.get(0)<<"]"<<"["<<msg.get(1)<<"]");
}
return res;
}
CMakeLists
cmake_minimum_required( VERSION 3.8 FATAL_ERROR)
project(broker LANGUAGES CXX)
#set dirs
set(PROJECT_ROOT ${CMAKE_CURRENT_LIST_DIR})
message("project dir:${PROJECT_ROOT}")
SET(BIN_DESTINATION ${PROJECT_SOURCE_DIR}/bin)
SET(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${BIN_DESTINATION})
SET(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${BIN_DESTINATION})
SET(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${BIN_DESTINATION})
#include cmake files
include(${PROJECT_ROOT}/../version.cmake)
#set compile flags
#add_definitions(-std=c++11 -g -rdynamic)
set(CMAKE_CXX_FLAGS "-g3 -rdynamic -std=c++11")
set(CMAKE_CXX_FLAGS_DEBUG "-g3 -O0 ")#-fsanitize=address -fno-omit-frame-pointer -fsanitize=leak")
set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG")
#include dirs
include_directories(./ ../include/
)
#link dirs
link_directories(${BIN_DESTINATION})
#execute
SET(SRC_MAIN broker.cpp ../include/common.cpp)
add_executable( ${PROJECT_NAME} ${SRC_MAIN})
set_target_properties(${PROJECT_NAME} PROPERTIES VERSION ${PROJECT_VERSION})
target_link_libraries(${PROJECT_NAME} pthread zmq zmqpp gflags)
add_executable(pub pub.cpp ../include/common.cpp)
set_target_properties(pub PROPERTIES VERSION ${PROJECT_VERSION})
target_link_libraries(pub pthread zmq zmqpp gflags)
add_executable(sub sub.cpp ../include/common.cpp)
set_target_properties(sub PROPERTIES VERSION ${PROJECT_VERSION})
target_link_libraries(sub pthread zmq zmqpp gflags)