PUB/SUB/XPUB/XSUB Publish-Subscribe¶
This chapter's contract-owning documents — the PUB, SUB, XPUB, and XSUB specs own the contract. This chapter shows that contract through language examples.
1. Overview¶
The Publish-Subscribe pattern distributes messages based on topics. zlink provides two levels: basic PUB/SUB and advanced XPUB/XSUB.
| Socket | Role | Characteristics |
|---|---|---|
| PUB | Publisher | Broadcasts to all subscribers. Cannot receive. |
| SUB | Subscriber | Topic prefix match filtering. Cannot send. |
| XPUB | Advanced Publisher | PUB + can receive subscription events |
| XSUB | Advanced Subscriber | Receives all messages without local filtering |
Valid socket combinations: - PUB → SUB, PUB → XSUB - XPUB → SUB, XPUB → XSUB
SUB vs XSUB — Key Difference¶
Both SUB and XSUB send subscription info to the upstream PUB via
zlink_set_subscription(). The public API usage is identical.
The difference is whether the local filter engine is on or off.
| SUB (filtered) | XSUB (unfiltered) | |
|---|---|---|
| With subscriptions | Receives only matching messages | Receives all messages (no filter check) |
| No subscriptions | Receives nothing | Receives all messages |
"" empty subscription |
Receives all (matches every topic) | Already receives all without subscribing |
| Use case | Normal subscriber | Proxy/relay (pass-through) |
Common confusion: "If I subscribe SUB with
"", isn't it the same as XSUB?" → Both receive all messages in practice, but SUB still evaluates its local byte-prefix filter on every message while XSUB never applies one. Also, SUB with no subscriptions receives nothing, while XSUB with no subscriptions still receives everything.
Why XSUB/XPUB in the proxy pattern:
%%{init: {'flowchart': {'nodeSpacing': 32, 'rankSpacing': 40, 'padding': 8, 'wrappingWidth': 180}, 'themeVariables': {'fontSize': '18px'}}}%%
flowchart LR
PUB -- data --> XSUB
XSUB == proxy ==> XPUB
XPUB -- data --> SUB
SUB -. subscribe .-> XPUB
XPUB -. propagate .-> XSUB
- XSUB passes all messages from PUB without applying its own filter.
- XPUB exposes SUB subscription events via
zlink_xpub_recv(), allowing the proxy to inject subscription management logic (filtering, logging, authorization, etc.). - Plain SUB/PUB cannot build this relay structure.
%%{init: {'flowchart': {'nodeSpacing': 32, 'rankSpacing': 40, 'padding': 8, 'wrappingWidth': 180}, 'themeVariables': {'fontSize': '18px'}}}%%
flowchart LR
PUB --> SUB1["SUB 1 (weather)"]
PUB --> SUB2["SUB 2 (sports)"]
Part I: PUB/SUB¶
2. PUB/SUB Basic Usage¶
Publisher (PUB)¶
zlink_publish() submits the complete payload-part array as one message record
from a PUB or XPUB socket.
void *pub = zlink_socket(ctx, ZLINK_SOCKET_PUB);
zlink_bind(pub, "tcp://*:5556");
/* Publish message -- dropped for a subscriber whose queue is at HWM */
zlink_msg_t part;
zlink_msg_init_size(&part, 14);
memcpy(zlink_msg_data(&part), "weather: sunny", 14);
zlink_submit_result_t rc = zlink_publish(
pub, "weather", &part, 1, ZLINK_SEND_FLAGS_NONE);
Subscriber (SUB)¶
zlink_subscribe() receives a topic and its complete payload record in one
call. There is no callback surface; the intended pattern is to observe ZLINK_POLLIN
from a poller and pull with this function.
void *sub = zlink_socket(ctx, ZLINK_SOCKET_SUB);
zlink_connect(sub, "tcp://127.0.0.1:5556");
/* Subscribe to topic -- set after connect */
zlink_set_subscription(sub, "weather");
char topic[256];
size_t topic_len = 0;
zlink_msg_t parts[8];
size_t part_count = 0;
zlink_recv_result_t rc = zlink_subscribe(
sub, NULL, topic, sizeof(topic), &topic_len,
parts, 8, &part_count, ZLINK_RECV_FLAGS_NONE);
if (rc == ZLINK_RECV_OK) {
printf("Topic: %.*s, Data: %.*s\n",
(int)topic_len, topic,
(int)zlink_msg_size(&parts[0]), (char *)zlink_msg_data(&parts[0]));
zlink_multipart_close(parts, part_count);
}
/* other rc values: ZLINK_RECV_NO_DATA (EAGAIN), TERMINATED, BUFFER_TOO_SMALL */
Reference:
core/tests/integration/test_pubsub.cpp-- empty subscription ("") → receives all messages
Sending and Receiving Summary¶
| Socket | Direction | Send/Receive API | Notes |
|---|---|---|---|
| PUB | Send only | zlink_publish() |
Cannot receive |
| SUB | Receive only | zlink_subscribe() |
Topic + payload array returned separately |
| XPUB | Bidirectional | zlink_publish() / zlink_xpub_recv() |
Receives subscription events |
| XSUB | Receive only | zlink_subscribe() |
No local filter; receives all |
Note:
zlink_send()/zlink_recv()returnZLINK_SUBMIT_NOT_SUPPORTED/ZLINK_RECV_NOT_SUPPORTEDon all 4 PUB/SUB sockets. Usezlink_publish()for publishing andzlink_subscribe()for receiving.PUB / XPUB default:
ZLINK_PUB_OPT_NODROPdefaults to0. When the HWM is reached, the message for that subscriber is silently dropped andzlink_publish()reports success. Callers that need backpressure instead of dropping must setZLINK_PUB_OPT_NODROPto1explicitly.When PUB's send queue is full (HWM), the default (
ZLINK_PUB_OPT_NODROP=0) silently drops the message for that subscriber. For details, see Performance Guide.
3. Topic Filtering¶
Topic filtering in SUB sockets uses prefix matching.
| Subscription Topic | Received Message | Match |
|---|---|---|
"weather" |
"weather: sunny" |
O |
"weather" |
"weathering storm" |
O |
"weather" |
"sports: baseball" |
X |
"" (empty string) |
All messages | O |
Multiple Topic Subscriptions¶
/* Subscribe to multiple topics */
zlink_set_subscription(sub, "weather");
zlink_set_subscription(sub, "sports");
/* Unsubscribe */
zlink_unset_subscription(sub, "sports");
Empty Subscription (All Messages)¶
Reference:
core/tests/integration/test_pubsub.cpp--zlink_set_subscription(subscriber, "")
4. Message Format¶
zlink_publish() takes a topic and a payload-part array as separate
parameters:
ZLINK_EXPORT zlink_submit_result_t zlink_publish (void *subject_,
const char *topic_id_,
zlink_msg_t *parts_,
size_t part_count_,
zlink_send_flags_t flags_);
For a multipart publish, place every part in array order and call
zlink_publish() once. Core atomically submits the complete array as one
publish record.
/* Publish: topic = "sensor:cpu", payload = 2 frames */
zlink_msg_t parts[2];
zlink_msg_init_size(&parts[0], 4);
memcpy(zlink_msg_data(&parts[0]), "host", 4);
zlink_msg_init_size(&parts[1], 2);
memcpy(zlink_msg_data(&parts[1]), "73", 2);
zlink_submit_result_t rc = zlink_publish(
pub, "sensor:cpu", parts, 2, ZLINK_SEND_FLAGS_NONE);
/* One zlink_subscribe() call returns the complete payload array:
topic = "sensor:cpu"
parts[0] = "host"
parts[1] = "73"
part_count = 2 */
The topic is sent on the wire as the first frame. zlink_subscribe()
separates the topic from the payload array on the receive side. Callers never
need to assemble topic frames manually.
Note: Passing
NULLastopic_id_(zlink_publish(pub, NULL, &part, ...)) activates a compatibility path where the first message frame carries the topic according to the wire-prefix rule. This is not recommended. Always pass thetopic_id_parameter explicitly.
5. PUB/SUB Socket Options¶
SUB-Specific Functions¶
| Function | Description |
|---|---|
zlink_set_subscription() |
Add topic subscription (prefix match) |
zlink_unset_subscription() |
Remove topic subscription |
Common Options¶
| Option | Type | Default | Description |
|---|---|---|---|
ZLINK_OPT_SNDHWM |
uint64_t bytes |
automatic (fanout floor by default) | Default for PUB-family sockets. Recomputed within the same role budget as connections grow; 0 is unlimited |
ZLINK_OPT_RCVHWM |
uint64_t bytes |
automatic (recv_ingress floor by default) | Default for SUB-family sockets. Recomputed within the same role budget as connections grow; 0 is unlimited |
ZLINK_OPT_LINGER |
int | -1 | Wait time on close (ms) |
6. PUB/SUB Usage Patterns¶
Pattern 1: Basic PUB/SUB¶
/* PUB */
void *pub = zlink_socket(ctx, ZLINK_SOCKET_PUB);
zlink_bind(pub, "tcp://*:5556");
/* SUB -- receive all messages */
void *sub = zlink_socket(ctx, ZLINK_SOCKET_SUB);
zlink_connect(sub, "tcp://127.0.0.1:5556");
zlink_set_subscription(sub, "");
msleep(100); /* time for subscription to reach PUB */
zlink_msg_t msg;
zlink_msg_init_size(&msg, 4);
memcpy(zlink_msg_data(&msg), "test", 4);
zlink_publish(pub, NULL, &msg, 1, ZLINK_SEND_FLAGS_NONE);
/* zlink_subscribe() on sub returns "test" once the record arrives */
Reference:
core/tests/integration/test_pubsub.cpp--test_tcp()
Pattern 2: Multiple SUBs¶
Multiple SUBs connect to a single PUB. Each SUB receives only its own topics.
void *pub = zlink_socket(ctx, ZLINK_SOCKET_PUB);
zlink_bind(pub, "tcp://*:5556");
void *sub_weather = zlink_socket(ctx, ZLINK_SOCKET_SUB);
zlink_connect(sub_weather, "tcp://127.0.0.1:5556");
zlink_set_subscription(sub_weather, "weather");
void *sub_sports = zlink_socket(ctx, ZLINK_SOCKET_SUB);
zlink_connect(sub_sports, "tcp://127.0.0.1:5556");
zlink_set_subscription(sub_sports, "sports");
/* Only sub_weather receives weather, only sub_sports receives sports */
Pattern 3: Multiple PUBs → SUB¶
A SUB can connect to multiple PUBs. It receives messages from all PUBs via fair-queue.
void *sub = zlink_socket(ctx, ZLINK_SOCKET_SUB);
zlink_set_subscription(sub, "");
zlink_connect(sub, "tcp://pub1:5556");
zlink_connect(sub, "tcp://pub2:5557");
7. PUB/SUB Caveats¶
Slow Subscriber (HWM Handling)¶
By default PUB/XPUB run in lossy mode — ZLINK_PUB_OPT_NODROP
defaults to 0. When a slow subscriber's send queue reaches the HWM, the
message for that subscriber is silently dropped (no error returned) and
zlink_publish() reports success. Delivery to the other subscribers is
unaffected.
/* Default — the slow subscriber's copy is dropped on HWM, publish succeeds */
struct quote_tick tick = {.price_micros = 91450000000LL, .volume = 1420};
zlink_msg_t quote;
zlink_msg_init_size("e, sizeof(tick));
memcpy(zlink_msg_data("e), &tick, sizeof(tick));
zlink_publish(pub, "quotes.KRW-BTC", "e, 1, ZLINK_SEND_FLAGS_DONTWAIT);
/* Raise the HWM to absorb bursts and reduce loss */
uint64_t hwm_bytes = 64 * 1024 * 1024; /* HWM is bytes */
zlink_set_option(pub, ZLINK_OPT_SNDHWM, &hwm_bytes, sizeof(hwm_bytes));
NODROP Mode — Backpressure Instead of Drop¶
Setting ZLINK_PUB_OPT_NODROP to 1 makes zlink_publish() return
ZLINK_SUBMIT_BACKPRESSURED on HWM instead of dropping, so the caller can
react.
/* Enable NODROP mode (backpressure on HWM) */
int nodrop = 1;
zlink_set_pub_option(pub, ZLINK_PUB_OPT_NODROP, &nodrop, sizeof(nodrop));
struct quote_tick tick = {.price_micros = 91450000000LL, .volume = 1420};
zlink_msg_t quote;
zlink_msg_init_size("e, sizeof(tick));
memcpy(zlink_msg_data("e), &tick, sizeof(tick));
zlink_submit_result_t rc = zlink_publish(
pub, "quotes.KRW-BTC", "e, 1, ZLINK_SEND_FLAGS_DONTWAIT);
if (rc == ZLINK_SUBMIT_BACKPRESSURED) {
/* HWM reached — the call already consumed quote; resubmit a retained
retained copy of the complete record as a new array. */
}
This mode couples the publisher to its slowest subscriber: one full pipe stops delivery to every subscriber on the socket. Reliable delivery that must not depend on subscriber speed belongs on a request-reply socket, not on PUB/SUB.
| Mode | Behavior on HWM | When to Use |
|---|---|---|
Default (NODROP=0, lossy) |
Silent drop — no error, message lost | Ordinary fanout (observation, notification, sensor, tick) |
NODROP=1 |
Returns ZLINK_SUBMIT_BACKPRESSURED — caller controls |
Loss is unacceptable and coupling to the slowest subscriber is acceptable |
ZLINK_PUB_OPT_NODROPapplies to both PUB and XPUB sockets (PUB is implemented on top of XPUB).
Late Joiner (Messages Lost Before Subscription)¶
Messages published before the subscription message from SUB reaches PUB are lost.
/* Time needed for subscription to propagate to PUB */
zlink_connect(sub, "tcp://127.0.0.1:5556");
zlink_set_subscription(sub, "topic");
msleep(100); /* wait for subscription propagation */
/* Only messages published after this point can be received */
Direction Constraints¶
PUB/SUB each have their own dedicated API:
/* PUB: send via zlink_publish(). Cannot receive */
zlink_msg_t parts[4];
for (size_t i = 0; i < 4; ++i) {
zlink_msg_init_size(&parts[i], 5);
memcpy(zlink_msg_data(&parts[i]), "sunny", 5);
}
zlink_publish(pub, "weather", &parts[0], 1, ZLINK_SEND_FLAGS_NONE); /* OK */
/* Using zlink_send() on PUB → returns ZLINK_SUBMIT_NOT_SUPPORTED */
zlink_send(pub, &parts[1], 1, ZLINK_SEND_FLAGS_NONE, NULL, NULL);
/* SUB: receive via zlink_subscribe(). Cannot send/publish */
zlink_publish(sub, "weather", &parts[2], 1, ZLINK_SEND_FLAGS_NONE); /* ZLINK_SUBMIT_NOT_SUPPORTED */
zlink_send(sub, &parts[3], 1, ZLINK_SEND_FLAGS_NONE, NULL, NULL); /* ZLINK_SUBMIT_NOT_SUPPORTED */
Part II: XPUB/XSUB¶
8. XPUB/XSUB Overview¶
XPUB/XSUB are advanced publish-subscribe sockets that allow applications to handle subscription frames directly. They are used for building proxies/brokers, subscription monitoring, and Last-Value Caching.
SUB vs XSUB — Key Difference¶
| SUB | XSUB | |
|---|---|---|
| Topic registration | zlink_set_subscription() |
zlink_set_subscription() (same) |
| Message receive | zlink_subscribe() — filtered |
zlink_subscribe() — no filter, receives all |
| Local filter | On — drops non-matching | Off — passes all messages |
| No subscriptions | Receives nothing | Receives all messages |
XSUB is needed in proxies because it passes all messages through
without applying its own filter. Topic registration is sent to upstream
identically via zlink_set_subscription() on both.
PUB vs XPUB — Key Difference¶
| PUB | XPUB | |
|---|---|---|
| Message publish | zlink_publish() |
zlink_publish() (same) |
| Subscription events | Not exposed | zlink_xpub_recv() |
XPUB can observe which clients subscribe to or unsubscribe from which topics.
XSUB/XPUB Roles in a Proxy¶
A proxy has two separate flows:
%%{init: {'flowchart': {'nodeSpacing': 32, 'rankSpacing': 40, 'padding': 8, 'wrappingWidth': 180}, 'themeVariables': {'fontSize': '18px'}}}%%
flowchart LR
subgraph data ["Data flow (publish)"]
direction LR
P1[PUB] -- publish --> X1[XSUB] == proxy forward ==> X2[XPUB] -- deliver --> S1[SUB]
end
subgraph sub ["Subscription flow (reverse)"]
direction RL
S2[SUB] -. subscribe .-> X3[XPUB] -. propagate .-> X4[XSUB] -. register .-> P2[PUB]
end
Data Flow¶
| Step | Actor | Action | Note |
|---|---|---|---|
| 1 | PUB | zlink_publish(pub, topic, ...) |
Publish data |
| 2 | proxy internal | XSUB internal recv → XPUB internal send | Handled by zlink_proxy() |
| 3 | SUB | zlink_subscribe() |
Final consumption |
Key point: The proxy data relay in
zlink_proxy(xsub, xpub, NULL)uses internal recv/send paths, not the publiczlink_send()/zlink_recv()API. Users never need to call XSUB recv → XPUB send directly.
Subscription Propagation Flow¶
| Step | Actor | Action | API |
|---|---|---|---|
| 1 | SUB | Subscribe → arrives at XPUB via wire | zlink_set_subscription(sub, "weather") |
| 2 | proxy app | Receive subscription event from XPUB | zlink_xpub_recv(xpub, ...) |
| 3 | proxy app | Register on XSUB → propagates to PUB via wire | zlink_set_subscription(xsub, "weather") |
| 4 | PUB | Publish matching data | zlink_publish(pub, "weather", ...) |
| 5 | data flow | XSUB → XPUB → SUB | Handled by zlink_proxy() |
zlink_set_subscription()sends subscription info upstream on the wire identically for both SUB and XSUB. Calling it on XSUB in a proxy is not because "XSUB can send" — the proxy app registers subscription events received from XPUB onto XSUB to propagate them upstream.
Why XSUB/XPUB?¶
| Question | With SUB/PUB | With XSUB/XPUB |
|---|---|---|
| Data pass-through | SUB local filter on — must register subscriptions | XSUB local filter off — passes all |
| Subscription events | PUB does not expose them | XPUB exposes them via zlink_xpub_recv() |
| Proxy suitability | Proxy must manage topics itself | Relay-only — ideal for proxy |
PUB/SUB Socket Public API Summary¶
| Public API | PUB | SUB | XPUB | XSUB |
|---|---|---|---|---|
zlink_publish() |
OK | — | OK | — |
zlink_subscribe() |
— | OK | — | OK |
zlink_set_subscription() |
— | OK | — | OK |
zlink_xpub_recv() |
— | — | OK | — |
| Local filter | N/A | On | N/A | Off |
zlink_send()/zlink_recv()returnZLINK_SUBMIT_NOT_SUPPORTED/ZLINK_RECV_NOT_SUPPORTEDon all 4 PUB/SUB sockets. Usezlink_publish()for publishing andzlink_subscribe()for receiving.Proxy patterns (built-in
zlink_proxy(), manual proxy construction, ROUTER/DEALER broker) are covered in the Proxy Guide.
9. Subscription Frame Format¶
Subscription/unsubscription frames between XPUB/XSUB follow this format:
| Byte | Meaning |
|---|---|
0x01 + topic |
Subscription request |
0x00 + topic |
Unsubscription request |
/* Subscribe from XSUB */
zlink_set_subscription(xsub, "A");
/* Unsubscribe from XSUB */
zlink_unset_subscription(xsub, "A");
XPUB receives subscription frames with zlink_xpub_recv():
void *xpub = zlink_socket(ctx, ZLINK_SOCKET_XPUB);
zlink_bind(xpub, "tcp://*:5557");
const zlink_routing_id_t *source_rid = NULL;
int subscribed = 0;
char topic[256];
size_t topic_len = 0;
zlink_recv_result_t rc = zlink_xpub_recv(
xpub, &source_rid, &subscribed, topic, sizeof(topic), &topic_len, ZLINK_RECV_FLAGS_NONE);
Reference:
core/tests/integration/test_xpub_manual.cpp--subscription1[] = {1, 'A'},unsubscription1[] = {0, 'A'}
10. XPUB Socket Options¶
| Option | Type | Default | Description |
|---|---|---|---|
ZLINK_PUB_OPT_MANUAL |
int | 0 | Enable manual subscription management mode |
ZLINK_PUB_OPT_VERBOSE |
int | 0 | Also surface duplicate subscribes for already-subscribed topics as events |
ZLINK_PUB_OPT_APPROVE_SUBSCRIBE |
binary | -- | (MANUAL mode) Approve the topic subscription for the pipe of the most recently dequeued subscription event |
ZLINK_PUB_OPT_REJECT_SUBSCRIBE |
binary | -- | (MANUAL mode) Reject the topic subscription for the pipe of the most recently dequeued subscription event |
XPUB_MANUAL Mode¶
By default, XPUB processes SUB subscriptions automatically. In MANUAL mode, after receiving a subscription event with zlink_xpub_recv(), the application decides the actual subscription with ZLINK_PUB_OPT_APPROVE_SUBSCRIBE / ZLINK_PUB_OPT_REJECT_SUBSCRIBE through zlink_set_pub_option(). zlink_set_subscription() / zlink_unset_subscription() are SUB/XSUB-only and fail with ZLINK_CONFIG_INVALID_ARGUMENT on an XPUB handle.
/* Enable MANUAL mode */
int manual = 1;
zlink_set_pub_option(xpub, ZLINK_PUB_OPT_MANUAL, &manual, sizeof(manual));
/* zlink_xpub_recv() returns subscribed=1, topic="A"
Then approve a transformed subscription for that subscriber's pipe: */
const char mapped_topic[] = "XA";
zlink_set_pub_option(xpub, ZLINK_PUB_OPT_APPROVE_SUBSCRIBE,
mapped_topic, sizeof(mapped_topic) - 1);
/* Publish */
zlink_msg_t msg_a;
zlink_msg_init_size(&msg_a, 1);
memcpy(zlink_msg_data(&msg_a), "A", 1);
zlink_publish(xpub, NULL, &msg_a, 1, ZLINK_SEND_FLAGS_NONE); /* does not reach the subscriber */
zlink_msg_t msg_xa;
zlink_msg_init_size(&msg_xa, 2);
memcpy(zlink_msg_data(&msg_xa), "XA", 2);
zlink_publish(xpub, NULL, &msg_xa, 1, ZLINK_SEND_FLAGS_NONE); /* subscriber receives this */
Reference:
core/tests/integration/test_xpub_manual.cpp--test_basic(): subscription request for A → transformed to B
11. XPUB/XSUB Usage Patterns¶
Pattern 1: Building a Proxy/Broker¶
Build a PUB/SUB proxy using XSUB (frontend) + XPUB (backend).
/* Proxy frontend: PUBs connect here */
void *xsub = zlink_socket(ctx, ZLINK_SOCKET_XSUB);
zlink_bind(xsub, "tcp://*:5556");
/* Proxy backend: SUBs connect here */
void *xpub = zlink_socket(ctx, ZLINK_SOCKET_XPUB);
zlink_bind(xpub, "tcp://*:5557");
/* Run proxy (forwards messages and subscriptions bidirectionally) */
zlink_proxy(xsub, xpub, NULL);
Pattern 2: MANUAL Mode Proxy (Subscription Transformation)¶
An advanced proxy that transforms or filters subscription requests.
int manual = 1;
zlink_set_pub_option(xpub, ZLINK_PUB_OPT_MANUAL, &manual, sizeof(manual));
for (;;) {
const zlink_routing_id_t *source_rid = NULL;
int subscribed = 0;
char topic[256];
size_t topic_len = 0;
zlink_recv_result_t rc = zlink_xpub_recv(
xpub, &source_rid, &subscribed, topic, sizeof(topic) - 1, &topic_len, ZLINK_RECV_FLAGS_NONE);
if (rc != ZLINK_RECV_OK)
break;
topic[topic_len] = '\0'; /* topic bytes are binary-safe (no NUL) — set_subscription takes a C string */
if (subscribed) {
/* Approve the subscription for the pipe of the event just dequeued */
zlink_set_pub_option(xpub, ZLINK_PUB_OPT_APPROVE_SUBSCRIBE, topic, topic_len);
/* Propagate subscription upstream (XSUB) */
zlink_set_subscription(xsub, topic);
} else {
/* Reject (remove) the subscription for that pipe */
zlink_set_pub_option(xpub, ZLINK_PUB_OPT_REJECT_SUBSCRIBE, topic, topic_len);
zlink_unset_subscription(xsub, topic);
}
}
Reference:
core/tests/integration/test_xpub_manual.cpp--test_xpub_proxy_unsubscribe_on_disconnect()
Pattern 3: Subscription Monitoring¶
Use XPUB to observe which clients subscribe to which topics.
void *xpub = zlink_socket(ctx, ZLINK_SOCKET_XPUB);
zlink_bind(xpub, "tcp://*:5557");
for (;;) {
const zlink_routing_id_t *source_rid = NULL;
int subscribed = 0;
char topic[256];
size_t topic_len = 0;
zlink_recv_result_t rc = zlink_xpub_recv(
xpub, &source_rid, &subscribed, topic, sizeof(topic), &topic_len, ZLINK_RECV_FLAGS_NONE);
if (rc != ZLINK_RECV_OK)
break;
printf("%s: %.*s\n", subscribed ? "New subscription" : "Unsubscription",
(int) topic_len, topic);
}
Pattern 4: Automatic Unsubscribe on Subscriber Disconnect¶
When a SUB disconnects, an unsubscribe frame is automatically delivered to XPUB.
/* After SUB disconnects */
zlink_close(sub);
/* The next zlink_xpub_recv() returns
subscribed=0 and the previously subscribed topic */
Reference:
core/tests/integration/test_xpub_manual.cpp--test_xpub_proxy_unsubscribe_on_disconnect()
12. Caveats¶
Subscription Propagation Timing¶
Subscription messages are propagated asynchronously. Messages published immediately after subscribing may not be received.
zlink_connect(sub, endpoint);
zlink_set_subscription(sub, "topic");
/* Publishing a "topic" message at this point may result in loss */
msleep(100); /* wait for subscription propagation */
/* Messages published after this point can be received */
Subscription Management in XPUB MANUAL Mode¶
In MANUAL mode, if ZLINK_PUB_OPT_APPROVE_SUBSCRIBE is not applied after receiving a subscription event, that subscription is not registered. Subscriptions must be explicitly processed.
Multiple Subscribers → Single XPUB¶
When multiple SUBs subscribe to the same topic, the XPUB subscription is maintained until all SUBs have unsubscribed.
Reference:
core/tests/integration/test_xpub_manual.cpp--test_missing_subscriptions(): processing two subscribers sequentially to prevent omissions
Full language examples¶
zlink::context_t ctx;
zlink::xpub_socket_t publisher (ctx);
zlink::sub_socket_t subscriber (ctx);
zlink::socket_monitor_t pub_monitor = publisher.monitor_open ();
zlink::socket_monitor_t sub_monitor = subscriber.monitor_open ();
publisher.bind ("tcp://127.0.0.1:0");
const std::string endpoint = publisher.options ().last_endpoint ();
assert (!endpoint.empty ());
subscriber.connect (endpoint);
assert (detail::wait_connected (pub_monitor, sub_monitor));
const std::string topic = detail::k_pubsub_topic;
subscriber.set_subscription (topic);
zlink::subscription_event_t event;
assert (publisher.receive_subscription_event (event)
== static_cast<int> (zlink::recv_result_t::ok));
assert (event.subscribed);
assert (event.topic == topic);
const std::string sent = detail::k_pubsub_payload;
zlink::message_t outbound = detail::make_message (sent);
publisher.publish (topic).message (outbound).submit ();
zlink::topic_message_t inbound;
assert (subscriber.subscribe (inbound) == static_cast<int> (zlink::recv_result_t::ok));
assert (inbound.topic () == topic);
assert (inbound.parts ().size () == 1);
const std::string received = inbound.parts ()[0].to_string ();
assert (received == detail::k_pubsub_payload);
inbound.close ();
std::printf ("[pubsub/recv] publish: \"%s/%s\" → subscribe: \"%s/%s\"\n", topic.c_str (),
sent.c_str (), topic.c_str (), received.c_str ());
return 0;
if (!SampleSupport.IsNativeAvailable())
return;
using var ctx = Zlink.CreateContext();
using var publisher = ctx.CreatePubSocket();
using var subscriber = ctx.CreateSubSocket();
string endpoint = SampleSupport.NewEndpoint("tcp", "sample");
using var publisherMonitor = publisher.MonitorOpen(SocketEvent.ConnectionReady);
using var subscriberMonitor = subscriber.MonitorOpen(SocketEvent.ConnectionReady);
publisher.Bind(endpoint);
subscriber.Connect(endpoint);
SampleSupport.WaitConnected(publisherMonitor, subscriberMonitor);
subscriber.SetSubscription("prices");
using (Message message = Message.From("101.25"))
publisher.Publish("prices").Message(message).Submit();
string payload = SampleSupport.SubscribeUtf8(subscriber, out string topic, 2000);
Console.WriteLine(
$"[pubsub/recv] publish: \"prices/101.25\" -> subscribe: \"{topic}/{payload}\"");
SampleSupport.ensureNative();
String endpoint = SampleSupport.tcpEndpoint();
String published = SampleSupport.PUBSUB_TOPIC + "/" + SampleSupport.PUBSUB_PAYLOAD;
try (Context ctx = Zlink.createContext();
PubSocket pub = ctx.createPubSocket();
SubSocket sub = ctx.createSubSocket();
var pubMonitor = pub.monitorOpen(
systems.zlink.contracts.eventing.MonitorEventType.CONNECTION_READY);
var subMonitor = sub.monitorOpen(
systems.zlink.contracts.eventing.MonitorEventType.CONNECTION_READY)) {
pub.bind(endpoint);
sub.setSubscription(SampleSupport.PUBSUB_TOPIC);
sub.connect(endpoint);
SampleSupport.waitPubSubReady(pubMonitor, subMonitor);
try (Message payload = Message.from(SampleSupport.PUBSUB_PAYLOAD)) {
pub.publish(SampleSupport.PUBSUB_TOPIC).message(payload).submit();
}
try (var received = new TopicMessage()) {
if (!sub.subscribe(received, RecvFlags.NONE)) {
throw new IllegalStateException("no pubsub delivery");
}
String value = received.topic() + "/"
+ received.singlePartOrThrow().toUtf8String();
if (!published.equals(value)) {
throw new IllegalStateException("unexpected delivery: " + value);
}
System.out.println("[pubsub/recv] publish: \"" + published
+ "\" \u2192 subscribe: \"" + value + "\"");
}
}
SampleSupport.ensureNative()
val endpoint = SampleSupport.tcpEndpoint()
val published = "${SampleSupport.PUBSUB_TOPIC}/${SampleSupport.PUBSUB_PAYLOAD}"
Zlink.createContext().use { ctx ->
ctx.createPubSocket().use { pub ->
ctx.createSubSocket().use { sub ->
pub.monitorOpen(MonitorEventType.CONNECTION_READY).use { pubMonitor ->
sub.monitorOpen(MonitorEventType.CONNECTION_READY).use { subMonitor ->
pub.bind(endpoint)
sub.setSubscription(SampleSupport.PUBSUB_TOPIC)
sub.connect(endpoint)
SampleSupport.waitPubSubReady(pubMonitor, subMonitor)
Message.from(SampleSupport.PUBSUB_PAYLOAD).use { payload ->
pub.publish(SampleSupport.PUBSUB_TOPIC).message(payload).submit()
}
TopicMessage().use { received ->
check(sub.subscribe(received, RecvFlags.NONE)) { "no pubsub delivery" }
val value = "${received.topic()}/${received.singlePartOrThrow().toUtf8String()}"
check(published == value) { "unexpected delivery: $value" }
println("[pubsub/recv] publish: \"$published\" → subscribe: \"$value\"")
}
}
}
}
}
}
_, endpoint = tcp_endpoint()
with zlink.create_context() as ctx:
with zlink.create_xpub_socket(ctx) as publisher:
with zlink.create_sub_socket(ctx) as subscriber:
with publisher.monitor_open(zlink.MonitorEventMask.CONNECTION_READY) as publisher_monitor:
with subscriber.monitor_open(zlink.MonitorEventMask.CONNECTION_READY) as subscriber_monitor:
publisher.bind(endpoint)
subscriber.connect(endpoint)
subscriber.set_subscription(b"prices")
wait_connected(publisher_monitor, subscriber_monitor)
event = zlink.create_subscription_event()
if not publisher.receive_subscription_event_into(event):
raise AssertionError("missing subscription event")
if not event.subscribed or event.topic != "prices":
raise AssertionError("unexpected subscription event")
publisher.publish(b"prices").message(b"101.25").submit()
received = zlink.create_topic_message()
assert subscriber.subscribe_into(received)
with received:
if received.topic != "prices":
raise AssertionError(f"unexpected pubsub topic: {received.topic!r}")
if received.to_bytes_list() != [b"101.25"]:
raise AssertionError("unexpected pubsub payload")
print('[pubsub/recv] publish: "prices/101.25" → subscribe: "prices/101.25"')
const endpoint = await tcpEndpoint();
const ctx = zlink.createContext();
const pub = zlink.createPubSocket(ctx);
const sub = zlink.createSubSocket(ctx);
try {
const pubMonitor = pub.monitorOpen([zlink.MonitorEventType.ConnectionReady]);
const subMonitor = sub.monitorOpen([zlink.MonitorEventType.ConnectionReady]);
try {
pub.bind(endpoint);
sub.connect(endpoint);
pubMonitor.recv();
subMonitor.recv();
} finally {
pubMonitor.close();
subMonitor.close();
}
const topic = 'prices';
const sent = '101.25';
sub.setSubscription(topic);
const deadline = Date.now() + 5000;
const received = new zlink.TopicMessage();
let hasReceived = false;
while (Date.now() < deadline) {
pub.publish(topic).message(Buffer.from(sent)).submit();
try {
if (sub.subscribe(received, zlink.RecvFlags.DontWait)) {
hasReceived = true;
break;
}
} catch (error) {
if (!(error instanceof zlink.RecvError && error.result === zlink.RecvResult.NoData)) {
throw error;
}
}
await new Promise((resolve) => setTimeout(resolve, 25));
}
assert.equal(hasReceived, true);
try {
const recv = received.parts[0].data().toString();
assert.equal(received.topic, topic);
assert.equal(recv, sent);
console.log(`[pubsub/recv] publish: "${topic}/${sent}" \u2192 subscribe: "${topic}/${recv}"`);
} finally {
received.close();
}
} finally {
sub.close();
pub.close();
ctx.close();
}
const port = await reservePort();
const endpoint = `tcp://127.0.0.1:${port}`;
const ctx = zlink.createContext();
const pub = zlink.createPubSocket(ctx);
const sub = zlink.createSubSocket(ctx);
try {
const pubMonitor = pub.monitorOpen([zlink.MonitorEventType.ConnectionReady]);
const subMonitor = sub.monitorOpen([zlink.MonitorEventType.ConnectionReady]);
try {
pub.bind(endpoint);
sub.connect(endpoint);
pubMonitor.recv();
subMonitor.recv();
} finally {
pubMonitor.close();
subMonitor.close();
}
const topic = 'prices';
const sent = '101.25';
sub.setSubscription(topic);
const deadline = Date.now() + 5000;
const received = new zlink.TopicMessage();
let hasReceived = false;
while (Date.now() < deadline) {
pub.publish(topic).message(Buffer.from(sent)).submit();
try {
if (sub.subscribe(received, zlink.RecvFlags.DontWait)) {
hasReceived = true;
break;
}
} catch (error) {
if (!(error instanceof zlink.RecvError && error.result === zlink.RecvResult.NoData)) {
throw error;
}
}
await new Promise((resolve) => setTimeout(resolve, 25));
}
assert.equal(hasReceived, true);
try {
const recv = received.parts[0].data().toString();
assert.equal(received.topic, topic);
assert.equal(recv, sent);
console.log(`[pubsub/recv] publish: "${topic}/${sent}" → subscribe: "${topic}/${recv}"`);
} finally {
received.close();
}
} finally {
sub.close();
pub.close();
ctx.close();
}
ctx, err := zlink.NewContext()
samplecommon.Must(err)
defer ctx.Close()
publisher, err := ctx.XPubSocket()
samplecommon.Must(err)
defer publisher.Close()
subscriber, err := ctx.SubSocket()
samplecommon.Must(err)
defer subscriber.Close()
pubMon := samplecommon.OpenMonitor(publisher)
defer pubMon.Close()
subMon := samplecommon.OpenMonitor(subscriber)
defer subMon.Close()
endpoint := samplecommon.UniqueTCP("pubsub-recv")
samplecommon.Must(publisher.Bind(endpoint))
samplecommon.Must(subscriber.Connect(endpoint))
samplecommon.WaitConnected(pubMon, subMon)
topic := "prices"
samplecommon.Must(subscriber.SetSubscription(topic))
var event zlink.SubscriptionEvent
_, err = publisher.ReceiveSubscriptionEvent(&event, zlink.RecvFlagsNone)
samplecommon.Must(err)
if !event.Subscribed() || event.Topic() != topic {
samplecommon.Must(fmt.Errorf("unexpected subscription event"))
}
payload := "101.25"
_, err = publisher.Publish(topic).Message(samplecommon.Message(payload)).Submit(context.Background())
samplecommon.Must(err)
var message zlink.TopicMessage
_, err = subscriber.Subscribe(&message, zlink.RecvFlagsNone)
samplecommon.Must(err)
defer message.Close()
part, err := message.SinglePartOrError()
samplecommon.Must(err)
if !bytes.Equal(part.Data(), []byte(payload)) {
samplecommon.Must(fmt.Errorf("unexpected payload %q", string(part.Data())))
}
fmt.Printf("[pubsub/recv] publish: %q -> subscribe: %q\n", topic+"/"+payload, message.Topic()+"/"+string(part.Data()))
let ctx = Context::new().expect("context creation failed");
let endpoint = sample_support::tcp_endpoint();
let pub_sock = ctx.pub_socket().expect("pub socket failed");
let sub_sock = ctx.sub_socket().expect("sub socket failed");
sub_sock
.set_subscription("prices")
.expect("set_subscription failed");
let pub_mon = SocketMonitor::open(&pub_sock).expect("pub monitor open failed");
let sub_mon = SocketMonitor::open(&sub_sock).expect("sub monitor open failed");
pub_sock.bind(&endpoint).expect("bind failed");
sub_sock.connect(&endpoint).expect("connect failed");
sample_support::wait_connected(&[&pub_mon, &sub_mon]);
drop(pub_mon);
drop(sub_mon);
let msg = Message::try_from(b"101.25").expect("message failed");
pub_sock
.publish("prices")
.message(msg)
.submit()
.expect("publish failed");
let mut topic_msg = TopicMessage::empty();
assert!(
sub_sock
.subscribe(&mut topic_msg, RecvFlags::NONE)
.expect("subscribe recv failed")
);
assert_eq!(topic_msg.topic(), "prices");
assert_eq!(topic_msg.parts()[0].as_str().unwrap(), "101.25");
println!(
"[pubsub/recv] publish: \"{}/101.25\" → subscribe: \"{}/{}\"",
topic_msg.topic(),
topic_msg.topic(),
topic_msg.parts()[0].as_str().unwrap()
);