Contexts
Contexts in Scalability Protocols provide for isolation of protocol-specific state machines and associated data, allowing multiple concurrent state machines (or transactions) to coexist on a single socket.
For example, a REP server may wish to allow many requests to be serviced concurrently, even though some jobs may take significant time to process. Contexts provide for this ability.
Not all protocols have contexts, because many protocols simply have no state to manage. The following protocols support contexts:
For these protocols, the socket will also have a single, default, context that is used when performing send or receive operations on the socket directly.
Other protocols are stateless, at least with respect to message processing, and have no use for contexts. For the same reason, raw mode sockets do not support contexts.
tip
Developers with experience with [libnanomsg] may be used to using raw sockets for concurrency. Contexts provide a superior solution, as they are much easier to use, less error prone, and allow for easy control of the amount of concurrency used on any given socket.
One drawback of contexts is that they cannot be used with file descriptor polling using
nng_socket_get_recv_poll_fd
or nng_socket_get_send_poll_fd
.
Context Structure
#define NNG_CTX_INITIALIZER // opaque value
typedef struct nng_ctx_s nng_ctx;
The nng_ctx
structure represents context. This is a handle, and
the members of it are opaque. However, unlike a pointer, it is passed by value.
A context may be initialized statically with the NNG_CTX_INITIALIZER
macro,
to ensure that it cannot be confused with a valid open context.
Creating a Context
int nng_ctx_open(nng_ctx *ctxp, nng_socket s);
The nng_ctx_open
function creates a separate context to be used with the socket s,
and returns it at the location pointed by ctxp.
Context Identity
int nng_ctx_id(nng_ctx c);
The nng_ctx_id
function returns a positive identifier for the context c if it is valid.
Otherwise it returns -1
.
A context is considered valid if it was ever opened with nng_ctx_open
function.
Contexts that are allocated on the stack or statically should be initialized with the macro NNG_CTX_INITIALIZER
to ensure that they cannot be confused with a valid context before they are opened.
Closing a Context
int nng_ctx_close(nng_ctx ctx);
The nng_ctx_close
function closes the context ctx.
Messages that have been submitted for sending may be flushed or delivered,
depending upon the transport.
Further attempts to use the context after this call returns will result in NNG_ECLOSED
.
Threads waiting for operations on the context when this
call is executed may also return with an NNG_ECLOSED
result.
note
Closing the socket associated with ctx using nng_socket_close
also closes this context.
Sending Messages
int nng_ctx_sendmsg(nng_ctx ctx, nng_msg *msg, int flags);
void nng_ctx_send(nng_ctx ctx, nng_aio *aio);
These functions (nng_ctx_sendmsg
and nng_ctx_send
) send
messages over the socket s. The differences in their behaviors are as follows.
note
The semantics of what sending a message means varies from protocol to protocol, so examination of the protocol documentation is encouraged. Additionally, some protocols may not support sending at all or may require other pre-conditions first. (For example, REP sockets cannot normally send data until they have first received a request, while SUB sockets can only receive data and never send it.)
nng_ctx_sendmsg
The nng_ctx_sendmsg
function sends the msg over the context ctx.
If this function returns zero, then the socket will dispose of msg when the transmission is complete. If the function returns a non-zero status, then the call retains the responsibility for disposing of msg.
The flags can contain the value NNG_FLAG_NONBLOCK
, indicating that the function should not wait if the socket
cannot accept more data for sending. In such a case, it will return NNG_EAGAIN
.
nng_ctx_send
The nng_ctx_send
function sends a message asynchronously, using the nng_aio
aio, over the context ctx.
The message to send must have been set on aio using the nng_aio_set_msg
function.
If the operation completes successfully, then the context will have disposed of the message.
However, if it fails, then callback of aio should arrange for a final disposition of the message.
(The message can be retrieved from aio with nng_aio_get_msg
.)
Note that callback associated with aio may be called before the message is finally delivered to the recipient. For example, the message may be sitting in queue, or located in TCP buffers, or even in flight.
tip
This is the preferred function to use for sending data on a context. While it does require a few extra
steps on the part of the application, the lowest latencies and highest performance will be achieved by using
this function instead of nng_ctx_sendmsg
.
Receiving Messages
int nng_ctx_recvmsg(nng_ctx ctx, nng_msg **msgp, int flags);
void nng_ctx_recv(nng_ctx ctx, nng_aio *aio);
These functions (, nng_ctx_recvmsg
and nng_ctx_recv
) receive
messages over the context ctx. The differences in their behaviors are as follows.
note
The semantics of what receving a message means varies from protocol to protocol, so examination of the protocol documentation is encouraged. Additionally, some protocols may not support receiving at all or may require other pre-conditions first. (For example, REQ sockets cannot normally receive data until they have first sent a request.)
nng_recvmsg
The nng_ctx_recvmsg
function receives a message and stores a pointer to the nng_msg
for that message in msgp.
The flags can contain the value NNG_FLAG_NONBLOCK
, indicating that the function should not wait if the socket
has no messages available to receive. In such a case, it will return NNG_EAGAIN
.
nng_socket_recv
The nng_ctx_send
function receives a message asynchronously, using the nng_aio
aio, over the context ctx.
On success, the received message can be retrieved from the aio using the nng_aio_get_msg
function.
note
It is important that the application retrieves the message, and disposes of it accordingly. Failure to do so will leak the memory.
tip
This is the preferred function to use for receiving data on a context. While it does require a few extra
steps on the part of the application, the lowest latencies and highest performance will be achieved by using
this function instead of nng_ctx_recvmsg
.
Context Options
int nng_ctx_get_bool(nng_ctx ctx, const char *opt, bool *valp);
int nng_ctx_get_int(nng_ctx ctx, const char *opt, int *valp);
int nng_ctx_get_ms(nng_ctx ctx, const char *opt, nng_duration *valp);
int nng_ctx_get_size(nng_ctx ctx, const char *opt, size_t *valp);
int nng_ctx_set_bool(nng_ctx ctx, const char *opt, int val);
int nng_ctx_set_int(nng_ctx ctx, const char *opt, int val);
int nng_ctx_set_ms(nng_ctx ctx, const char *opt, nng_duration val);
int nng_ctx_set_size(nng_ctx ctx, const char *opt, size_t val);
Some protocols support certain options that affect the behavior of a specific context. For example, most protocols will let you set the defaults timeouts associated with send or receive separately for different contexts.
These functions are used to retrieve or change the value of an option named opt from the context ctx.
The nng_ctx_get_
functions retrieve the value from ctx, and store it in the location valp references.
The nng_ctx_set_
functions change the value for the ctx, taking it from val.
These functions access an option as a specific type. The protocol documentation will have details about which options are available for contexts, whether they can be read or written, and the appropriate type to use.
Examples
These examples show building blocks for a concurrent service based on contexts.
Example 1: Context Echo Server
The following program fragment demonstrates the use of contexts to implement a concurrent REP service that simply echos messages back to the sender.
struct echo_context {
nng_ctx ctx;
nng_aio *aio;
enum { INIT, RECV, SEND } state;
};
void
echo(void *arg)
{
struct echo_context *ec = arg;
switch (ec->state) {
case INIT:
ec->state = RECV;
nng_ctx_recv(ec->ctx, ec->aio);
return;
case RECV:
if (nng_aio_result(ec->aio) != 0) {
// ... handle error
}
// We reuse the message on the ec->aio
ec->state = SEND;
nng_ctx_send(ec->ctx, ec->aio);
return;
case SEND:
if (nng_aio_result(ec->aio) != 0) {
// ... handle error
}
ec->state = RECV;
nng_ctx_recv(ec->ctx, ec->aio);
return;
}
}
Example 2: Starting the Echo Service
Given the above fragment, the following example shows setting up the service.
It assumes that the socket has already been
created and any transports set up as well with functions such as nng_dial
or nng_listen
.
#define CONCURRENCY 1024
static struct echo_context ecs[CONCURRENCY];
void
start_echo_service(nng_socket rep_socket)
{
for (int i = 0; i < CONCURRENCY; i++) {
// error checks elided for clarity
nng_ctx_open(&ecs[i].ctx, rep_socket);
nng_aio_alloc(&ecs[i].aio, echo, ecs+i);
ecs[i].state = INIT;
echo(ecs+i); // start it running
}
}