This page walks through the standalone request-fabric pattern end to end: start a gateway, register a server that handles a request type, write a client that posts a request and waits for the reply — all with cmlabs::DataMessage payloads and no Psyclone runtime. The running example is a generic compute service: clients submit a named processing job with parameters and an optional binary payload; workers process it and reply.
Everything here is plain C++ against the CMSDK library (Compiling and linking with the CMSDK library); each program is an ordinary executable with its own main(). The three roles and their responsibilities are introduced in Using CMSDK without Psyclone; the reference lives in Requests.
The payload: DataMessage
Requests and replies are cmlabs::DataMessage objects (Messaging): one flat allocation with named, typed entries. Set and read fields by name:
DataMessage* msg = new DataMessage();
msg->setString("mode", "fast"); // text
msg->setInt("width", 1024); // 64-bit integer
msg->setDouble("threshold", 0.85); // floating point
msg->setData("payload", buffer, bufferSize); // raw binary block
const char* mode = msg->getString("mode"); // pointer INTO the message
int64 width = msg->getInt("width");
uint32 size;
const char* data = msg->getData("payload", size);
- Warning
- Pointers returned by getString()/getData() point into the message's own memory: they are valid only while the message exists and is not resized by later set*() calls. Copy the bytes out (or use getDataCopy(), which returns a caller-owned heap allocation) if you need them beyond the message's lifetime.
For requests specifically, the static helper cmlabs::RequestClient::CreateRequestMessage() builds a correctly-shaped request from an operation id (HTTP_GET, HTTP_POST, ... from NetworkProtocols.h), a request name and up to 20 key/value string pairs:
DataMessage* msg = RequestClient::CreateRequestMessage(
HTTP_POST, "process", // operation + request name
"mode", "fast", // key/value parameters...
"format", "png");
msg->setData("payload", imageBytes, imageSize); // binary attached normally
The request name (here "process") is the type key of your protocol: the executor uses it to classify short vs long requests (any name registered with RequestExecutor::addLongRequestName() goes to the long queue), and your server code dispatches on it. There is no schema to compile — the message is self-describing and travels verbatim over the wire.
Step 1 — the gateway process
The gateway (cmlabs::RequestGateway) is the router: the only process with a well-known address. Configure it, open ports, then call init():
#include "RequestGateway.h"
using namespace cmlabs;
int main(int argc, char* argv[]) {
RequestGateway* gateway = new RequestGateway(1); // gateway id 1 (rendezvous key, not a port)
gateway->addPort(10000, NOENC); // binary clients + executors
gateway->addPort(8080, NOENC, true); // + HTTP/WebSocket on 8080
if (!gateway->init()) { // start threads + network
printf("Could not start gateway (port in use?)\n");
return 1;
}
printf("Gateway running on ports 10000 and 8080...\n");
while (true)
utils::Sleep(1000); // gateway threads do the work
return 0;
}
Signatures (RequestGateway.h): the constructor is RequestGateway(uint32 id, const char* version = NULL); addPort(uint16 port, uint8 encryption, bool enableHTTP = false, uint32
timeout = 3000) must be called before init(). Encryption is NOENC or SSLENC — for SSL, pass PEM certificate/key paths to init(sslCertPath, sslKeyPath).
Useful configuration before init() (see cmlabs::RequestGateway for all):
- setQueuingParameters(maxQueue, maxProcessing, priorityThreshold) — backpressure: beyond maxQueue pending requests, new ones are rejected.
- setLongRequestLimit(uint32) — cap concurrent long requests per executor.
- setExecutorHeartbeatTimeout(uint32 ms) — how long an executor may go silent before it is declared dead and its queued requests redistributed.
- setWebServerInfo(name, rootdir, indexfile) + addAuthUser(user, pass) — the embedded web server: static files, status pages, basic auth.
- addGateway(id, addr, port) — federate with peer gateways for redundancy.
One process, a handful of lines — that is the entire router. Everything else connects to it.
Step 2 — the server (RequestExecutor)
A server process creates a cmlabs::RequestExecutor, connects it to the gateway, and runs one or more worker threads that pull requests and reply:
#include "RequestExecutor.h"
#include "ThreadManager.h"
using namespace cmlabs;
RequestExecutor* executor;
volatile bool shouldRun = true;
// Worker thread: pull -> process -> reply, forever. The signature matches
// THREAD_FUNCTION so it can be started by ThreadManager::CreateThread();
// arg selects the queue (NULL = short, non-NULL = long).
THREAD_RET THREAD_FUNCTION_CALL workerLoop(void* arg) {
bool isLong = (arg != NULL);
DataMessage *msg, *replyMsg;
while (shouldRun) {
msg = isLong ? executor->waitForLongRequest(50) // blocking, 50ms slices
: executor->waitForShortRequest(50);
if (!msg)
continue; // timeout: loop again
replyMsg = new DataMessage(*msg); // copy the request: this
// preserves the routing reference
const char* req = msg->getString("REQUEST");
if (req && strcmp(req, "process") == 0) {
uint32 size;
const char* payload = msg->getData("payload", size);
// ... do the actual computation on payload/parameters ...
replyMsg->setString("status", "ok");
replyMsg->setInt("bytesProcessed", size);
}
else {
replyMsg->setString("status", "error");
replyMsg->setString("reason", "unknown request");
}
if (!executor->replyToQuery(replyMsg)) // takes ownership on success
delete replyMsg; // ...delete only on failure
// do NOT delete msg: the executor owns it and deletes it itself when the
// reply is dispatched. Deleting it here would be a double-free.
}
return 0;
}
int main(int argc, char* argv[]) {
executor = new RequestExecutor(42, "ComputeServer"); // id + display name
executor->addLongRequestName("batchProcess"); // classify expensive jobs
executor->setLongRequestLimit(2); // at most 2 at once
executor->addGateway(1, "gateway.example.local", 10000); // dial out; reconnects itself
// Start the workers on CMSDK's portable threads (@ref Threading):
// two on the short/interactive queue, one on the long/batch queue.
uint32 tid;
ThreadManager::CreateThread((THREAD_FUNCTION)workerLoop, NULL, tid);
ThreadManager::CreateThread((THREAD_FUNCTION)workerLoop, NULL, tid);
ThreadManager::CreateThread((THREAD_FUNCTION)workerLoop, (void*)1, tid);
while (shouldRun) // main thread just keeps the process alive
utils::Sleep(1000);
executor->shutdownNetwork(); // clean disconnect on exit
return 0;
}
Signatures (RequestExecutor.h): RequestExecutor(uint32 id = 0, const char* name = NULL); addGateway(uint32 id, std::string addr, uint16 port, uint8 encryption =
NOENC) may be repeated for gateway redundancy; DataMessage* waitForShortRequest(uint32 timeoutMS) and DataMessage* waitForLongRequest(uint32 timeoutMS) block up to the timeout and return NULL when nothing arrived; bool replyToQuery(DataMessage* msg) routes the reply back through the gateway.
Key points:
- Short vs long queues. Register expensive request names with addLongRequestName(); the fabric keeps them in a separate queue with a concurrency cap (setLongRequestLimit()), so a batch job can never starve interactive traffic. Run separate worker threads for each queue.
- The reference is the routing key. Every request carries a reference id that lets the gateway match your answer to the waiting client. Building the reply by copying the request (new DataMessage(*msg)) preserves that reference automatically; if you build a fresh message instead you must set it yourself with replyMsg->setReference(msg->getReference()), or the client times out.
- Ownership (footgun). The request returned by waitFor*Request() stays owned by the executor — it is the same object the executor holds in its internal request map and deletes itself once the reply is dispatched. Do not delete it in the worker, or you cause a double-free. The reply passed to replyToQuery() transfers to the executor when the call returns true; if it returns false, the reply is still yours to delete.
- Threading. Public RequestExecutor methods may be called from any thread, and the wait queues support multiple concurrent workers — scale by starting more worker threads (or more server processes; the gateway balances across both). CMSDK's own portable threads via cmlabs::ThreadManager (Threading) work well here, but any thread that can call into the executor will do.
- Connection health. The executor emits heartbeats and reconnects to gateways by itself. shutdownNetwork() disconnects cleanly on exit. Always exit worker loops cooperatively (a shared flag, as above) — killed threads do not unwind.
Step 3 — the client (RequestClient)
A client connects to the gateway and posts requests. postRequest() returns a cmlabs::RequestReply — a future you can block on (waitForResult() / waitForMessage()), poll, or attach a callback to with RequestReply::setCallback():
#include "RequestClient.h"
using namespace cmlabs;
int main(int argc, char* argv[]) {
RequestClient client;
client.addGateway(1, "gateway.example.local", 10000);
if (!client.waitForConnection(5000)) {
printf("No gateway within 5s\n");
return 1;
}
DataMessage* msg = RequestClient::CreateRequestMessage(
HTTP_POST, "process", "mode", "fast");
msg->setData("payload", inputBytes, inputSize);
RequestReply* reply = client.postRequest(msg); // ownership of msg transfers
if (!reply) {
printf("Local send failure\n"); // could not queue the request
return 1;
}
if (reply->waitForResult(3000) == SUCCESS) { // block, 3s timeout
DataMessage* answer = reply->peekReplyMessage(); // owned by reply
printf("status=%s in %u ms\n",
answer->getString("status"), reply->getRequestDurationMS());
} else {
printf("Request failed: %s\n", reply->getStatusText().c_str());
}
client.finishRequest(reply); // release; do NOT touch reply after
return 0;
}
Signatures (RequestClient.h): addGateway(uint32 id, std::string addr, uint16 port, uint8 encryption =
NOENC); bool waitForConnection(uint32 timeoutMS); RequestReply* postRequest(DataMessage* msg) (non-blocking; ownership of msg transfers to the fabric; returns NULL on local failure). A second overload bool postRequest(DataMessage* msg, RequestCallbackFunction callback,
uint32 timeoutMS) is declared but not yet implemented (currently a stub that returns false without taking ownership) — for completion callbacks today, use postRequest(msg) plus RequestReply::setCallback(). RequestStatus waitForResult(uint32 timeoutMS); DataMessage* waitForMessage(uint32 timeoutMS, bool takeMessage = false); bool finishRequest(RequestReply* reply).
Ownership rules — who deletes what
These four rules cover the whole round-trip:
- Request message: ownership transfers to the fabric at postRequest(). Never delete a message you posted.
- RequestReply: owned by the client's pool. Release it with client.finishRequest(reply); never delete it and never touch it after releasing it.
- Reply message via peekReplyMessage(): still owned by the RequestReply — read it before finishRequest(), or take a caller-owned copy with getReplyMessageCopy().
- Reply message via waitForMessage(timeout, true): with takeMessage = true, ownership of the reply DataMessage transfers to you — delete it yourself (and still finishRequest() the reply object).
A compact helper using rule 4:
// Returns the reply message (caller owns it), or NULL on timeout/failure.
DataMessage* sendAndWait(RequestClient& client, DataMessage* msg, uint32 timeoutMS) {
RequestReply* reply = client.postRequest(msg);
if (!reply)
return NULL;
DataMessage* answer = reply->waitForMessage(timeoutMS, true); // take ownership
client.finishRequest(reply);
return answer; // may be NULL: always check
}
Timeouts and errors — always check for NULL
Every blocking call takes an explicit timeout and has a well-defined failure value: postRequest() may return NULL, waitForMessage() returns NULL on timeout or failure, waitForResult() returns the status at return time (TIMEOUT if still pending — see cmlabs::RequestStatus and getStatusText() for readable names). A NULL reply is a normal runtime event — a worker crashed, the gateway shed load (TOOBUSY), the network blipped — and the client must handle it. The client reconnects to gateways automatically and requests survive brief gateway outages by re-queuing, but your code decides whether to retry, degrade or report.
Step 4 — an HTTP/REST front end
Often the fastest path is to let the gateway itself be the HTTP front end: any port added with addPort(port, NOENC, true) accepts HTTP and WebSocket alongside the binary protocol, converts web requests into internal DataMessage requests, routes them to your executors exactly like binary requests, and serialises replies back as XML or JSON (setResponseType("json")). Your server code cannot tell the difference — one worker loop serves native clients, browsers and scripts. Add setWebServerInfo() for static files and addAuthUser() for basic auth.
When you need a custom HTTP service instead (or in addition), embed the CMSDK web server directly with cmlabs::NetworkManager (Networking): implement the cmlabs::NetworkReceiver interface — in particular receiveHTTPRequest(HTTPRequest*, NetworkChannel*, uint64 conid) — and open a listener with createListener(port, encryption, protocol, ...) using the PROTOCOL_HTTP_SERVER flag (NetworkProtocols.h). Protocol flags can be OR'ed, so one port can auto-detect HTTP, WebSocket and the binary message protocol. For SSL, set the certificate first (setSSLCertificate()), then create SSLENC listeners.
To keep a hand-rolled HTTP API disciplined, define it in XML and let cmlabs::RESTParser (RESTAPI.h) do the parsing: it validates each incoming HTTPRequest (or DataMessage) against declared request names and typed parameter sets (integer, float, text, array, image, video, binary; optional vs required) and hands you a cmlabs::RESTRequest with typed getters and a ready getErrorString() when validation fails.
Metrics and time
The fabric keeps rolling round-trip statistics internally, and the same primitives are available to your code: stamp work items with cmlabs::GetTimeNow() (Functions dealing with time; uint64 microseconds, monotonic, cross-machine syncable), accumulate with cmlabs::Stats, and expose windowed rates and latencies with cmlabs::MovingAverage — avg.getAverage(60000, val, count) gives the 60-second rolling average and event count, ideal for a "status" request or an HTTP metrics endpoint. RequestReply::getRequestDuration() / getRequestDurationMS() give per-request round-trip times on the client side.
Platform notes
The API is identical everywhere; the differences live below it. On Windows the socket layer is Winsock (initialised by the library), on Linux/macOS it is POSIX sockets — you never call either directly. Threads are native Win32 threads or pthreads behind cmlabs::ThreadManager and the utils wrappers. Build and link details per platform are in Compiling and linking with the CMSDK library. The only practical differences you will meet are operational: firewall semantics and SSL certificate stores. Binaries on different platforms interoperate freely — a DataMessage is byte-identical on the wire regardless of the sender's OS.
Trying it out
The fabric ships with a self-contained stress test: RequestGateway::UnitTest(port, executorCount, gatewayCount, clientCount,
webCount, webSocketCount, sendfreq, reconnect, payload, processTime,
runtime) spins up a complete in-process topology — gateways, executors, binary/HTTP/WebSocket clients — and runs it at a configurable request rate. It is both a smoke test for your build and a working reference for the whole pattern.
For the same machinery described from the Psyclone side — plus shared memory, SSL setup and the reference-group index — see Going Deeper. Full API reference: cmlabs::RequestClient, cmlabs::RequestReply, cmlabs::RequestExecutor, cmlabs::RequestGateway, cmlabs::DataMessage, cmlabs::NetworkManager (Requests, Networking, Messaging).