Applications are what your code runs in. An application should run in its own namespace and respect the internal namespace of the server. Applications may be written in any language that has C/C++ bindings.
Applications are responsible for running any internal threads they might need, maintaining any connections to external services, and not bringing the server down when they fail. Applications will receive only two general callbacks from the server during their typical lifetime, and a possible third callback when running unit tests.
The first callback to the application will be right after it’s loaded into memory: it will be told to initialize itself. During this time, the application should listen for all events it’s interested in, setup any internal structures, and run any threads necessary to do its job. Should the application fail to initialize, it should inform the server by returning FALSE, indicating it failed.
Note
Initialization is optional, but if you don’t implement it, you can’t really listen for events, and that’s rather pointless.
The only other time the application will receive a general callback is when the server is exiting. At this point, the application MUST shut down its internal threads, close any external connections, and free any resources.
Since the server is built entirely around events, it might be useful to know to know how to register them. The call is incredibly simple:
// EV_PREFIX is usually defined as some namespace for your application, like: #define EV_PREFIX "/my-app-prefix"
struct event *myev = evs_add_handler(EV_PREFIX, "/my-event", _myev_handler, _myev_on, _myev_off, FALSE);
This function returns an struct event, which is used to broadcast events to all clients subscribed to the event.
Important
This function MUST execute as fast as possible as it runs in one of the server’s main threads. Any background operations, long processing, socket operations, or anything that could potentially block should be done in another thread. Return EVS_STATUS_HANDLED for these operations, and schedule them to run in another thread.
It is completely up to the event handler if it wants to handle events with these path segments, and it may reject any event with segments it does not like. Extra event segments go to the handler and callbacks in a variable called ev_extra, or located inside struct evs_on_info as ev_extra.
Note
QuickIO reserves the /qio namespace for itself and its internal events. Applications MAY NEVER create events in this namespace.
When a client sends an event to the server, it is routed to the registered handler. The handler callback looks like:
typedef enum evs_status (*evs_handler_fn)(
struct client *client,
const gchar *ev_extra,
const evs_cb_t client_cb,
gchar *json);
This function is expected to return a status, as described above, and is given the following:
So what if you have an event subscription callback that does asynchronous verification of events, and you want to send the proper callbacks? There are a set of callback functions to help you out here.
evs_on_cb()
evs_on_info_copy()
evs_on_info_free()
Client callbacks are simple enough to understand, but what about when you want to be able to have a callback on the server? The easiest way to see these is to look at some code:
// Whatever data needs to be passed to callback handler
struct callback_data {
int id;
gchar *name;
float percent;
};
enum status _callback(
struct client *client,
const void *data,
const evs_cb_t client_cb,
gchar *json)
{
return CLIENT_GOOD;
}
void _free(void *data)
{
struct callback_data *d = data;
g_free(d->name);
g_slice_free1(sizeof(*d), d);
}
static enum evs_status _handler(
struct client *client,
const gchar *ev_extra G_GNUC_UNUSED,
const evs_cb_t client_cb,
gchar *json)
{
struct callback_data *d = g_slice_alloc0(sizeof(*d));
d->name = g_strdup("test");
evs_cb_with_cb(client, client_cb, json, _callback, d, _free);
return EVS_STATS_HANDLED;
}
... snip ...
evs_add_handler(EV_PREFIX, "/event", _hander, evs_no_on, NULL, FALSE);
... snip ...
The event flow will be as follows:
Note
A server callback might never be called: a client has a limited number of server callbacks it can have registered simultaneously, so if it exceeds the number its allowed, then old callbacks will be culled to make room for the new. Chances are this will never happen, but it is a possibility.
In this example, we (fakely) track the population of the world, sending out an updated population event every time a baby is born.
You’ll probably want to check out the Doxygen API docs just to see what is possible
Enough talk, let’s see what we can do here!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | // This file includes everything you need to write an app for QuickIO
#include "quickio.h"
/**
* All events in this application are prefixed to /skeleton
*/
#define EV_PREFIX "/skeleton"
/**
* The structure representing the events in the queue.
*/
struct pending {
/**
* The number of babies born in this event.
*/
gint64 babies_born;
/**
* A reference to the client, for the callback
*/
struct client *client;
/**
* For if we want to send a callback
*/
evs_cb_t client_cb;
};
/**
* The thread for running all async tasks; will be joined on exit.
*/
static GThread *_thread;
/**
* Where all events are sent for processing; in this application, it's not
* really necessary as we're just processing integer increments, but it's
* a good way to show how to do things asynchronously.
*/
static GAsyncQueue *_events;
/**
* The handler for population events
*/
struct event *_ev_population;
/**
* The population of the earth, as is known right now. This value is only
* ever updated inside the skeleton thread.
*/
static gint64 _population;
/**
* If you need external configuration for an app, it's already provided for you.
* Check out quick-event's config documentation for all options.
*/
static struct qev_cfg _cfg[] = {
{ .name = "population",
.description = "How many people there are on earth",
.type = QEV_CFG_INT64,
.val.i64 = &_population,
.defval.i64 = 7000000000,
.validate = NULL,
.cb = NULL,
.read_only = TRUE,
},
};
static enum evs_status _ev_baby_born_handler(
struct client *client,
const gchar *ev_extra G_GNUC_UNUSED,
const evs_cb_t client_cb,
gchar *json)
{
struct pending *p;
gint64 babies_born;
enum qev_json_status status;
// This will modify the contents of `json`
status = qev_json_unpack(json, NULL, "%d", &babies_born);
if (status != QEV_JSON_OK) {
evs_err_cb(client, client_cb, CODE_BAD, "could not parse JSON", NULL);
goto out;
}
if (babies_born <= 0) {
// Should probably give a better error message here
evs_err_cb(client, client_cb, CODE_BAD, "invalid number of babies born", NULL);
goto out;
}
p = g_slice_alloc(sizeof(*p));
p->babies_born = babies_born;
p->client = qev_ref(client);
p->client_cb = client_cb;
g_async_queue_push(_events, p);
out:
return EVS_STATUS_HANDLED;
}
static void* _run(void *nothing G_GNUC_UNUSED)
{
GString *buff = qev_buffer_get();
while (TRUE) {
struct pending *p = g_async_queue_pop(_events);
// Once we get a NULL, assume we're exiting. The server finishes all
// events before calling exit, so no more events will ever be in the
// queue.
if (p == NULL) {
break;
}
_population += p->babies_born;
// Send the client a callback asynchronously.
evs_cb(p->client, p->client_cb, NULL);
qev_unref(p->client);
g_slice_free1(sizeof(*p), p);
// Broadcast the new world population to everyone
qev_buffer_clear(buff);
qev_json_pack(buff, "%d", _population);
evs_broadcast(_ev_population, NULL, buff->str);
}
qev_buffer_put(buff);
return NULL;
}
static gboolean _app_init()
{
// Read any configuration values set straight into our variables
qev_cfg_add("skeleton", _cfg, G_N_ELEMENTS(_cfg));
_events = g_async_queue_new();
_thread = g_thread_new("skeleton", _run, NULL);
// Will be automatically configured with the rest of quick-event's
// configuration loading
qev_cfg_add("skeleton", _cfg, G_N_ELEMENTS(_cfg));
// No one may subscribe to this event
evs_add_handler(EV_PREFIX, "/baby-born",
_ev_baby_born_handler, evs_no_on, NULL, FALSE);
// Anyone may subscribe to this event
_ev_population = evs_add_handler(EV_PREFIX, "/population",
NULL, NULL, NULL, FALSE);
return TRUE;
}
static gboolean _app_exit()
{
g_async_queue_push(_events, NULL);
g_thread_join(_thread);
_thread = NULL;
g_async_queue_unref(_events);
_events = NULL;
return TRUE;
}
static gboolean _app_test()
{
// Used to run test cases on the application
return TRUE;
}
/**
* Use this macro so that when the server loads the app, it knows what to call.
*/
QUICKIO_APP(
_app_init,
_app_exit);
/**
* If you implement test cases, you're also going to need this macro.
*/
QUICKIO_APP_TEST(_app_test);
|