NATS C Client

repository·main·Indexed 19 days ago

https://github.com/nats-io/nats.c

A high-performance C client implementation for the NATS messaging system and NATS Streaming, designed for compatibility with the NATS Go client. It supports core NATS patterns (publish, subscribe, request/reply), JetStream for message streaming (v3.0.0+), and KeyValue stores. The library includes TLS support via OpenSSL and optional performance enhancements via Libsodium.

Tokens
14K
Snippets
41
Records
53
Agent score
18%

What's inside nats.c

  1. Use an event loop library (libuv/libevent) instead of threads

    main

    By default, the NATS library creates a dedicated thread for each connection to handle socket reads. To reduce thread overhead or integrate with existing event-driven architectures, you can use an event loop adapter (e.g., libuv or libevent).

    Integration Steps

    1. Create your event loop instance (e.g., uv_default_loop()).
    2. Use natsOptions_SetEventLoop() to provide the loop and the necessary adapter functions (Attach, Read, Write, Detach).
    3. Run your event loop.

    Critical Warning: Publishing and Request-Reply

    When using an event loop, publishing is asynchronous; data is placed in a buffer and sent when the event loop notifies the library that the socket is writable.

    Do not call blocking or request-reply functions from the thread running the event loop, such as:

    • natsConnection_Request()
    • natsConnection_Flush()
    • natsConnection_FlushTimeout()

    If you call these from the event loop thread, the data may never be sent because the loop is blocked, causing the calls to timeout. For natsConnection_Request(), use natsConnection_PublishRequest() and register a subscriber for the response instead.

    // Example using libuv
    uv_loop_t *uvLoop = uv_default_loop();
    
    natsOptions_SetEventLoop(opts,
                             (void*) uvLoop,
                             natsLibuv_Attach,
                             natsLibuv_Read,
                             natsLibuv_Write,
                             natsLibuv_Detach);
    
    natsConnection_Connect(&conn, opts);
    natsConnection_Subscribe(&sub, conn, subj, onMsg, NULL);
    
    uv_run(uvLoop, UV_RUN_DEFAULT);
  2. Manage socket buffering and latency

    main

    For high throughput, the library uses an internal buffer for socket writes. This reduces system calls but can increase latency.

    • Buffering Behavior: A publish call might return without data actually being sent to the server if the buffer is not yet full. A dedicated flusher thread handles the automatic flush.
    • Configuring Buffer Size: Use natsOptions_SetIOBufSize() to adjust the buffer size.
    • Reducing Latency (Send ASAP): To force every publish call to flush the socket immediately (reducing latency for request/reply patterns), use the natsOptions_SetSendAsap() option when creating the connection.
    • Checking Buffer State: Use natsConnection_Buffered() to query how much data is currently in the buffer.
    • Request/Reply: Note that natsConnection_Request() automatically flushes the buffer in place and does not rely on the flusher thread.
  3. Use Wildcards in Subscriptions

    main

    NATS uses two types of wildcards for subject matching:

    1. * (Token Wildcard): Matches exactly one token at any level.

      • foo.*.baz matches foo.bar.baz and foo.a.baz.
      • It will not match foo.baz or foo.bar.qux.baz.
    2. > (Full Wildcard): Matches any number of tokens at the end of a subject.

      • foo.> matches foo.bar, foo.bar.baz, and foo.a.b.c.d.
      • It must be the last token in the subject.
      • It will not match foo or bar.foo.baz.
    // Matches foo.anytoken.baz
    natsConnection_Subscribe(&sub, nc, "foo.*.baz", onMsg, NULL);
    
    // Matches foo.anything.at.all
    natsConnection_Subscribe(&sub, nc, "foo.>", onMsg, NULL);
  4. Use Durable Queue Groups to maintain state

    main

    Durable queue groups combine the load-balancing benefits of queue groups with the persistence of durable subscriptions. This allows a group to maintain its state even when no members are currently connected.

    Key Behaviors:

    • Creation: Created by providing a DurableName via stanSubOptions_SetDurableName when calling stanConnection_QueueSubscribe.
    • Naming: The server creates a group named DurableName:QueueName. Because of this, the : character is not allowed in the durable name.
    • Persistence: Unlike non-durable groups, the group is not removed when the last member leaves. It is only destroyed when the last member calls Unsubscribe.
    • Resuming: When a new member joins a durable queue group, it resumes from the last position recorded for that group, receiving any unacknowledged messages left by previous members.
    // Create a durable queue subscriber on "foo" for group "bar"
    stanSubOptions *subOpts = NULL;
    stanSubOptions_Create(&subOpts);
    stanSubOptions_SetDurableName(subOpts, "mydurablegroup");
    
    stanConnection_QueueSubscribe(&qsub, "foo", "bar", onMsg, NULL, subOpts);
  5. Use NATS Headers

    main

    Headers allow you to attach metadata to messages without affecting the payload. They function similarly to HTTP headers (key/value pairs where values are arrays of strings).

    Compatibility Note:

    • If you attempt to send headers to a server that does not support them, the call returns NATS_NO_SERVER_SUPPORT.
    • You can check support using natsConnection_HasHeaderSupport(conn).
    • If a server supports headers but sends them to an older client that doesn't, the headers are automatically stripped to ensure compatibility.
  6. Pass context to callbacks using closures

    main

    All NATS library callbacks accept a void *closure parameter. This allows you to pass a pointer to a user-defined object (context) into the callback, which is then returned to you when the callback is triggered. This is the standard way to maintain state or access application data within asynchronous callbacks.

    typedef struct {
        int count;
    } Errors;
    
    Errors asyncErrors;
    memset(&asyncErrors, 0, sizeof(asyncErrors));
    
    // Pass the address of asyncErrors as the closure
    natsOptions_SetErrorHandler(opts, asyncCb, (void*) &asyncErrors);
    
    // Inside the callback:
    static void asyncCb(natsConnection *nc, natsSubscription *sub, natsStatus err, void *closure) {
        Errors *errors = (Errors*) closure;
        errors->count++;
    }
  7. Manage connection reconnections and lifecycle

    main

    The library automatically attempts to reconnect by default.

    • Reconnection Configuration: Use natsOptions_SetAllowReconnect(), natsOptions_SetMaxReconnect(), etc., to control the behavior.
    • URL Randomization: If natsOptions_SetNoRadomize() is not set to true, the list of URLs is randomized on connection. On disconnect, the library tries the next URL in the list.
    • Cluster Awareness: If the NATS server has connect URL advertise enabled, the client will automatically learn about new servers added to the cluster and add them to its connection pool.
    • Buffering during Disconnect: While disconnected, publish and new subscription calls are buffered in memory. When reconnected, this buffer is flushed to the server.
    • Lifecycle Callbacks: To monitor connection state, use:
      • natsOptions_SetDisconnectedCB()
      • natsOptions_SetReconnectedCB()
      • natsOptions_SetClosedCB() (This is a final event; the connection is no longer valid after this is called).
  8. Configure manual message acknowledgments in NATS Streaming

    main

    NATS Streaming provides At-Least-Once delivery. By default, the client library automatically acknowledges messages after the subscriber's handler is invoked.

    To control exactly when a message is acknowledged (e.g., to defer acknowledgment until after heavy I/O is complete), you must:

    1. Enable manual acknowledgment mode using stanSubOptions_SetManualAckMode(subOpts, true).
    2. Set an acknowledgment wait timeout using stanSubOptions_SetAckWait(subOpts, timeout_ms).
    3. Explicitly call stanSubscription_AckMsg(sub, msg) within your message handler.
    // Subscribe with manual ack mode, and set AckWait to 60 seconds
    stanSubOptions_Create(&subOpts);
    stanSubOptions_SetManualAckMode(subOpts, true);
    stanSubOptions_SetAckWait(subOpts, 60000);
    stanConnection_Subscribe(&sub, sc, "foo", onMsg, NULL, subOpts);
    
    // In the callback
    void
    onMsg(stanConnection *sc, stanSubscription *sub, const char *channel, stanMsg *msg, void *closure)
    {
        // ack message before performing I/O intensive operation
        stanSubscription_AckMsg(sub, msg);
    
        printf("Received a message on %s: %.*s\n",
            channel,
            stanMsg_GetDataLength(msg),
            stanMsg_GetData(msg));
    }
  9. Configure NATS Streaming subscription start positions

    main

    NATS Streaming allows clients to start receiving messages from different points in the stream using stanSubOptions. You can create these options using stanSubOptions_Create and then apply one of the following strategies:

    • Last Received: Start with the most recently published value.
    • Deliver All: Receive all messages currently stored in the stream.
    • Specific Sequence: Start at a specific message sequence number.
    • Time Delta: Start at messages that were stored a certain amount of time ago (value in milliseconds).

    After configuring the options, pass the stanSubOptions pointer to stanConnection_Subscribe.

    // Create a Subscription Options:
    stanSubOptions *subOpts = NULL;
    stanSubOptions_Create(&subOpts);
    
    // Subscribe starting with most recently published value
    stanSubOptions_StartWithLastReceived(subOpts);
    
    // OR: Receive all stored messages
    stanSubOptions_DeliverAllAvailable(subOpts);
    
    // OR: Receive messages starting at a specific sequence number
    stanSubOptions_StartAtSequence(subOpts, 22);
    
    // OR: Start at messages that were stored 30 seconds ago. Value is expressed in milliseconds.
    stanSubOptions_StartAtTimeDelta(subOpts, 30000);
    
    // Create the subscription with options
    stanConnection_Subscribe(&sub, sc, "foo", onMsg, NULL, subOpts);
  10. Implement Queue Groups for load balancing

    main

    Queue groups allow you to distribute messages across multiple subscribers. When multiple subscribers join the same queue group (using the same queue name), each message is delivered to only one member of the group.

    Key Behaviors:

    • Automatic Creation: A group is created when the first member joins.
    • Start Position: When a new member joins an existing group, its requested start position is ignored. The member starts receiving messages from the last position in the group.
    • Leaving a Group: Members leave by calling stanSubscription_Unsubscribe or closing their connection. Unacknowledged messages are reassigned to remaining members.
    • Closing a Group: A group is removed from the server once all members have left. The next QueueSubscribe with that name will create a brand new group where start positions will be respected.
    stanConnection_Connect(&sc, "test-cluster", "clientid", NULL);
    
    // Create a queue subscriber on "foo" for group "bar"
    stanConnection_QueueSubscribe(&qsub1, "foo", "bar", onMsg, NULL, NULL);
    
    // Add a second member to the same group
    stanConnection_QueueSubscribe(&qsub2, "foo", "bar", onMsg, NULL, NULL);
    
    // Normal subscribers still work on the same subject
    stanConnection_Subscribe(&sub, "foo", onMsg, NULL, NULL);
  11. Use Durable Subscriptions to resume message processing

    main

    Durable subscriptions allow a client to pick up exactly where it left off in a stream without manually tracking sequence numbers. By assigning a DurableName via stanSubOptions_SetDurableName, the NATS Streaming server tracks the last acknowledged message for that specific clientID + durableName combination.

    Workflow:

    1. Connect with a clientID.
    2. Create subscription options and set a durable name.
    3. Subscribe to a subject.
    4. If the client disconnects, reconnecting with the same clientID and using the same durableName will cause the server to deliver only the messages published since the last acknowledgment.
    stanConnection_Connect(&sc, "test-cluster", "client-123", NULL);
    
    // Create subscription options
    stanSubOptions *subOpts = NULL;
    stanSubOptions_Create(&subOpts);
    
    // Set a durable name
    stanSubOptions_SetDurableName(subOpts, "my-durable");
    
    // Subscribe
    stanConnection_Subscribe(&sub, sc, "foo", onMsg, NULL, subOpts);
  12. Use Queue Groups for Load Balancing

    main

    Queue groups allow you to distribute messages among multiple subscribers. When you subscribe using a queue group name, NATS ensures that each message is delivered to only one subscriber within that group (using queue semantics).

    This is useful for scaling workers: multiple instances of a service can join the same queue group to share the processing load.

    // All subscribers with the name "job_workers" will share the load for subject "foo"
    natsConnection_QueueSubscribe(&sub, nc, "foo", "job_workers", onMsg, NULL);