Blurrr Network Service Discovery  1.0.0
Blurrr Network Service Discovery Documentation

Introduction:

Blurrr Network Service Discovery (aka BlurrrZeroconf) is a small cross-platform library that wraps the native implementations of each platform's Zeroconf network service discovery services, providing a singular API, in C, with the intent that language bindings can easily be created for any language.

Zeroconf (Zero Configuration Networking) is a protocol on the IETF standards track that provides network service discovery over standard DNS.

RFC 6763: https://tools.ietf.org/html/rfc6763

It is best known as Apple Bonjour, which is Apple's implemention of the Zeroconf protocol. It is also known by other names such as "multicast DNS" (mDNS), "DNS Service Discovery" (DNS-SD), and "DNS-based Service Discovery".

It provides for programs advertising and discovering services over the network, without having to know about IP addresses and ports ahead of time or needing a network adminstrator to configure things. It is designed to be extremely efficient to avoid spamming networks with traffic.

The protocol was pioneered at Apple, and pushed as IETF standard. The protocol has been widely adopted, especially in consumer network devices such as in the entire network printer market.

Blurrr Network Service Discovery wraps the following implementations:

Dependencies:

For easy adoption, this library has no dependencies except for the native Zeroconf services provided by your platform. This documention is written with Blurrr, SDL, and IUP in mind, but this library can be incorporated into any other project.

Usage:

There are only three basic concepts you need to know.

If you want to advertise services for others to connect to:

  1. To advertise a service for others to find, use BNSD_RegisterService.

If you want to connect to other services:

  1. To start discovering other services being advertised on the network, use BNSD_StartDiscovery.
  2. Once you identified a service you want to connect to (via BNSD_StartDiscovery), use BNSD_StartResolve to get the host name/IP address and port number so you can connect to it using your normal networking APIs.

API Documentation

See also
BlurrrNetworkServiceDiscovery.h for the complete API documentation.

Examples:

To register/advertise a new service:

struct BNSD_Registration* registration_service = BNSD_RegisterService(
"My Cool Service", // service_name
"_coolservice._tcp.", // service_type
NULL, // domain
NULL, // host_name
12345, // port
0, // flags
NULL, // txt_record
0, // txt_record_length
MyRegisterCallback,
NULL // user_data
);
// where MyRegisterCallback is defined as:
void MyRegisterCallback(struct BNSD_Registration* zeroconf_registration, const char* service_name, const char* service_type, const char* domain, int32_t blurrr_error_code, uint32_t flags, void* user_data)
{
if(BNSD_ERR_OK != blurrr_error_code)
{
printf("Failed to register service\n");
return;
}
printf("Register service succeeded\n");
printf(
"service_name: %s\n"
"service_type: %s\n"
"domain: %s\n",
service_name,
service_type,
domain
);
}
// Remember to call BNSD_UnregisterService(registration_service); when you are done.

To discover/browse for services:

struct BNSD_Discovery* discovery_service = BNSD_StartDiscovery(
"_coolservice._tcp.", // service_name
NULL, // domain
0, // flags
MyDiscoveryCallback,
NULL // user_data
);
// where MyDiscoveryCallback is defined as:
void MyDiscoveryCallback(struct BNSD_Discovery* zeroconf_discovery, const char* service_name, const char* service_type, const char* domain, bool is_added, int32_t blurrr_error_code, uint32_t flags, int32_t interface_index, void* user_data)
{
if(BNSD_ERR_OK != blurrr_error_code)
{
printf("Discovery had an error\n");
return;
}
printf(
"service_name: %s\n"
"service_type: %s\n"
"domain: %s\n"
"is_added: %d\n",
service_name,
service_type,
domain,
is_added
);
}
// Remember to call BNSD_StopDiscovery(discovery_service); when you are done.

To resolve a service (i.e. get hostname/IP and port number):

// Note: You typically use the values you got from your Discovery callback, instead of hardcoding them like here.
struct BNSD_Resolve* resolve_service = BNSD_StartResolve(
"My Cool Service", // service_name
"_coolservice._tcp", // service_type
NULL, // domain (typically you pass the value from the Discovery callback, which is often "local.", but not always)
0, // flags
5, // time_out in 5 seconds
MyResolveCallback,
NULL // user_data
);
// where MyResolveCallback is defined as:
void MyResolveCallback(struct BNSD_Resolve* zeroconf_resolve, const char* service_name, const char* service_type, const char* domain, const char* full_name, const char* host_target, uint16_t port, const char* txt_record, uint16_t txt_record_length, int32_t blurrr_error_code, uint32_t flags, int32_t interface_index, void* user_data)
{
if(BNSD_ERR_RESOLVE_TIMEOUT == blurrr_error_code)
{
printf("In MyZeroconfResolveCallback: Got TimeOut for service_type: %s", service_type);
BNSD_StopResolve(zeroconf_resolve);
return;
}
else if(BNSD_ERR_OK != blurrr_error_code)
{
printf("Resolve had an error\n");
BNSD_StopResolve(zeroconf_resolve);
return;
}
printf(
"service_name: %s\n"
"service_type: %s\n"
"domain: %s\n"
"full_name: %s\n"
"host_target: %s\n"
"port: %d\n"
service_name,
service_type,
domain,
full_name,
host_target,
port
);
// Note: Typically, the most important values are host_target and port which you will use to connect to your service.
// host_target will be something like "Macbook.local." or "192.168.0.42" or possibly an IPv6 address.
// port will be something like 12345.
// We can stop resolving if we don't need any more callbacks for this.
BNSD_StopResolve(zeroconf_resolve);
}
// Since we disposed of resolve_service in the callback, we must not use that variable any more.
resolve_service = NULL;

Thread Safety:

Most of the callbacks happen on background threads due to different platform implementation details.

But a lot of UI operations need to happen on your originating thread (usually the main UI thread). To do any of these things safely, you should forward events back to your primary thread.

If the things you need to do are safe to do on another thread, (e.g. usually network operations like opening a connection are safe), then you can disregard this warning and do your operations directly in the callback.

SDL Example:

To accomplish this, with SDL, push events into the SDL event queue and then in your main loop (SDL_PollEvent), do your work there.

// For brevity, this example only shows Discovery.
#include "SDL.h"
static Uint32 SDL_BNSD_DISCOVERY_EVENT;
struct DiscoveryPayload
{
struct BNSD_Discovery* discoveryService;
char* serviceName;
char* serviceType;
char* domain;
bool isAdded;
int32_t blurrrErrorCode;
uint32_t flags;
int32_t interfaceIndex;
void* userData;
};
// This is our callback for BNSD_StartDiscovery().
// Remember, this callback may happen on a background thread.
void MyDiscoveryCallback(struct BNSD_Discovery* zeroconf_discovery, const char* service_name, const char* service_type, const char* domain, bool is_added, int32_t blurrr_error_code, uint32_t flags, int32_t interface_index, void* user_data)
{
// We won't bother forwarding error data to the SDL thread.
if(BNSD_ERR_OK != blurrr_error_code)
{
printf("Discovery had an error\n");
return;
}
// We need to copy all the data to pass to the main SDL thread.
struct DiscoveryPayload* payload = SDL_calloc(1, sizeof(struct DiscoveryPayload));
payload->discoveryService = zeroconf_discovery;
// We must copy the string data since it is not guaranteed to live past this function callback.
payload->serviceName = SDL_strdup(service_name);
payload->serviceType = SDL_strdup(service_type);
payload->domain = SDL_strdup(domain);
payload->isAdded = is_added;
payload->blurrrErrorCode = blurrr_error_code;
payload->flags = flags;
payload->interfaceIndex = interface_index;
payload->userData = user_data;
SDL_Event sdl_event;
SDL_zero(sdl_event);
sdl_event.type = SDL_BNSD_DISCOVERY_EVENT;
sdl_event.user.data1 = payload;
SDL_PushEvent(&sdl_event);
}
// This is our convenience function for handling our Discovery data on the SDL thread.
void MySDLThreadDiscoveryCallback(struct DiscoveryPayload* payload)
{
// Do stuff with the data.
SDL_Log(
"service_name: %s\n"
"service_type: %s\n"
"domain: %s\n"
"is_added: %d\n",
payload->serviceName,
payload->serviceType,
payload->domain,
payload->isAdded
);
// Free all the memory we allocated when done.
SDL_free(payload->serviceName);
SDL_free(payload->serviceType);
SDL_free(payload->domain);
SDL_free(payload);
}
static bool s_appDone = false;
void main_loop()
{
SDL_Event the_event;
int the_result;
bool got_quit = false;
// Pump event loop
do
{
the_result = SDL_PollEvent(&the_event);
if(the_result > 0)
{
switch(the_event.type)
{
case SDL_KEYDOWN:
// Android back key
if(the_event.key.keysym.sym == SDLK_AC_BACK)
{
got_quit = true;
}
else if(the_event.key.keysym.sym == SDLK_ESCAPE)
{
got_quit = true;
}
break;
case SDL_QUIT:
case SDL_APP_TERMINATING:
got_quit = 1;
break;
default:
{
// Because SDL_BNSD_DISCOVERY_EVENT is not a compile-time constant, we must use if-checks
if(the_event.type == SDL_BNSD_DISCOVERY_EVENT)
{
MySDLThreadDiscoveryCallback((struct DiscoveryPayload*)the_event.user.data1);
}
break;
}
}
}
} while(the_result > 0);
s_appDone = got_quit;
}
int main(int argc, char* argv[])
{
void* platform_init_object = NULL;
SDL_Init(SDL_INIT_VIDEO);
#if defined(__ANDROID__)
platform_init_object = SDL_AndroidGetActivity();
#endif
bool is_init = BNSD_Init(platform_init_object);
if(!is_init)
{
SDL_Log("Zeroconf not available. Aborting program.");
return -1;
}
SDL_BNSD_DISCOVERY_EVENT = SDL_RegisterEvents(1);
if(SDL_BNSD_DISCOVERY_EVENT == ((Uint32)-1))
{
SDL_Log("Could not register BSDN Discovery/SDL event type");
return -1;
}
s_appDone = false; // reset global/static for to avoid Android NDK relaunch problems
struct BNSD_Discovery* discovery_service = BNSD_StartDiscovery(
"_daap._tcp.", // service_name (_daap is iTunes music sharing)
NULL, // domain
0, // flags
MyDiscoveryCallback,
NULL // user_data
);
SDL_Window* the_window = SDL_CreateWindow("BlurrrNetworkServiceDiscovery C",
SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
640, 480,
0
);
while(! s_appDone)
{
main_loop();
}
BNSD_StopDiscovery(discovery_service);
SDL_DestroyWindow(the_window);
SDL_Quit();
return 0;
}

IUP Example:

To accomplish this with IUP, use the IupPostMessage API to create a new callback that will happen on the main UI thread.

Note
IupPostMessage is currently an API proposal (from Blurrr SDK) to provide a mechanism to deal with threads in IUP. Blurrr ships with an implementation of the proposal. The final API is subject to change.
// For brevity, this example only shows Discovery.
#include "iup.h"
#include "iupcbs.h"
#include "BlurrrCore.h"
#include <string.h>
#include <stdlib.h>
#include <stddef.h>
struct DiscoveryPayload
{
struct BNSD_Discovery* discoveryService;
char* serviceName;
char* serviceType;
char* domain;
bool isAdded;
int32_t blurrrErrorCode;
uint32_t flags;
int32_t interfaceIndex;
void* userData;
};
// This is our callback for BNSD_StartDiscovery().
// Remember, this callback may happen on a background thread.
void MyDiscoveryCallback(struct BNSD_Discovery* zeroconf_discovery, const char* service_name, const char* service_type, const char* domain, bool is_added, int32_t blurrr_error_code, uint32_t flags, int32_t interface_index, void* user_data)
{
// We won't bother forwarding error data to the main UI thread.
if(BNSD_ERR_OK != blurrr_error_code)
{
IupLog("ERROR", "Discovery had an error\n");
return;
}
// We need to copy all the data to pass to the main SDL thread.
struct DiscoveryPayload* payload = calloc(1, sizeof(struct DiscoveryPayload));
payload->discoveryService = zeroconf_discovery;
// We must copy the string data since it is not guaranteed to live past this function callback.
payload->serviceName = strdup(service_name);
payload->serviceType = strdup(service_type);
payload->domain = strdup(domain);
payload->isAdded = is_added;
payload->blurrrErrorCode = blurrr_error_code;
payload->flags = flags;
payload->interfaceIndex = interface_index;
payload->userData = user_data;
// We passed in an Iup List as the user data. Use it to post a message to the main UI thread.
Ihandle* iup_list = (Ihandle*)user_data;
IupPostMessage(iup_list, NULL, payload, 0);
}
// This is our IupPostMessage callback on the main UI thread.
void MyIupMainThreadDiscoveryCallback(Ihandle* ih, char* not_used_char, void* message_data, int not_used_int)
{
struct DiscoveryPayload* payload = (struct DiscoveryPayload*)message_data;
// Do stuff with the data.
// Ideally, we should add the entry to our Iup List (the Ihandle* ih).
BlurrrLog_SysLog(
"service_name: %s\n"
"service_type: %s\n"
"domain: %s\n"
"is_added: %d\n",
payload->serviceName,
payload->serviceType,
payload->domain,
payload->isAdded
);
// Free all the memory we allocated when done.
free(payload->serviceName);
free(payload->serviceType);
free(payload->domain);
free(payload);
}
static struct* BNSD_Discovery* s_discoveryService = NULL;
// This function is called when the program exits.
static void IupExitPoint()
{
BNSD_StopDiscovery(s_discoveryService);
s_discoveryService = NULL;
IupClose();
}
// This function is the starting point for your code.
void IupEntryPoint()
{
// This tells IUP to call IupExitPoint on exit.
IupSetFunction("EXIT_CB", (Icallback)IupExitPoint);
IupSetInt(NULL, "UTF8MODE", 1);
BlurrrCore_Init();
void* platform_init_object = NULL;
#if defined(__ANDROID__)
platform_init_object = BlurrrPlatformAndroid_GetApplicationContext();
#endif
bool is_init = BNSD_Init(platform_init_object);
// For brevity, we're not going to do anything interesting with the UI.
// Ideally, we should put results in an IupList or IupTree.
Ihandle* iup_list = IupList(NULL);
IupSetAttribute(iup_list, "MULTIPLE", "YES");
IupSetCallback(iup_list, "POSTMESSAGE_CB", (Icallback)MyIupMainThreadDiscoveryCallback);
if(is_init)
{
s_discoveryService = BNSD_StartDiscovery(
"_daap._tcp.", // service_name (_daap is iTunes music sharing)
NULL, // domain
0, // flags
MyDiscoveryCallback,
iup_list // user_data
);
}
else
{
BlurrrLog_SysLog("Zeroconf not available");
}
Ihandle* the_dialog = IupDialog(iup_list);
IupSetAttribute(the_dialog, "TITLE", "Blurrr Network Service Discovery");
IupSetAttribute(the_dialog, "SIZE", "HALFxHALF");
// For Microsoft Windows: This will set the title bar icon to the application icon.
IupSetAttribute(the_dialog, "ICON", "IDI_ICON1");
IupShow(the_dialog);
}
// Do not modify main(). Not all platforms use main as the entry point so your changes may have no effect.
int main(int argc, char* argv[])
{
// Remember: Not all platforms run main.
IupOpen(&argc, &argv); // removed because IupLua is presumably doing this.
IupSetFunction("ENTRY_POINT", (Icallback)IupEntryPoint);
IupMainLoop();
return 0;
}

TXT Records:

DNS TXT records are a way of broadcasting metadata with an advertised service. Some examples are:

All the BNSD backends support TXT records, with the caveat that Android 5.0+ is required to support TXT records. (Older versions will ignore TXT records.)

TXT Record Parsing:

TXT records are in DNS TXT record format where each field entry starts with the number of bytes (an actual number, not a string) followed by that number characters, then repeat. (e.g. "\xfMyKey1=MyValue1\xfMyKey2=MyValue2" where "\xf" is hex for 15).

These are not proper C strings. They are not NULL terminated. Use the provided txt_record_length and do not use string functions like strlen.

Here is an example on how to parse a TXT record and break it up into key-value pairs.

// This is just a helper struct to help demonstrate parsing the txt_record.
struct MyTXTRecord
{
// There are not supposed to be more than 255 characters in a single DNS TXT record string.
char keyString[256];
char valueString[256];
};
void MyResolveCallback(struct BNSD_Resolve* zeroconf_resolve, const char* service_name, const char* service_type, const char* domain, const char* full_name, const char* host_target, uint16_t port, const char* txt_record, uint16_t txt_record_length, int32_t blurrr_error_code, uint32_t flags, int32_t interface_index, void* user_data)
{
if(BNSD_ERR_RESOLVE_TIMEOUT == blurrr_error_code)
{
// If you have any other pointers to the zeroconf_resolve outside this function,
// they are now dangling pointers.
BNSD_StopResolve(zeroconf_resolve);
return;
}
printf("In MyResolveCallback:\n"
"service_name:%s\n"
"service_type:%s\n"
"domain:%s\n"
"full_name:%s\n"
"host_target:%s\n"
"port:%d\n"
"blurrr_error_code:%d\n"
"interface_index:%d\n",
service_name,
service_type,
domain,
full_name,
host_target,
port,
blurrr_error_code,
interface_index
);
// Example of parsing txt_record
if(txt_record && txt_record_length > 0)
{
uint32_t number_of_entries = 0;
size_t i = 0;
// run through the entire txt_record array and count how many entries there are
while(i<txt_record_length)
{
size_t next_str_len = (size_t)txt_record[i];
i = i + 1 + next_str_len;
number_of_entries++;
}
// Now we'll create an array to hold all the key/value pairs in the txt_record
struct MyTXTRecord* array_of_keyvalue_entries = (struct MyTXTRecord*)calloc(number_of_entries, sizeof(struct MyTXTRecord));
size_t current_entry = 0;
// Now run through the txt_record array again and break up the fields
for(i=0, current_entry=0; i<txt_record_length; current_entry++)
{
size_t next_str_len = (size_t)txt_record[i];
i++;
char key_buffer[256];
char value_buffer[256];
memset(key_buffer, 0, 256);
memset(value_buffer, 0, 256);
bool is_key_state = true;
// The equal sign does not count as part of the key=value length
size_t j=0;
size_t k=0;
for(j=0; ((j<next_str_len) && (j<257)); j++)
{
char current_char = txt_record[i];
i++;
if(current_char == '=')
{
is_key_state = false;
k=i+1;
key_buffer[j] = '\0';
j++; // because we are breaking out, j won't get incremented, but we need to continue the count in the following loop so we need to manually increment here.
break;
}
key_buffer[j] = current_char;
}
// Make sure we got an = character. If not, skip this or try using ""?
if(is_key_state)
{
// if we are here, the key_buffer was not NULL terminated.
key_buffer[j] = '\0';
// We know our string lengths and buffer sizes are okay for plain strcpy.
strcpy(array_of_keyvalue_entries[current_entry].keyString, key_buffer);
// valueString will be "" since we left it untouched
continue; // go to next loop
}
// Using j here is not a bug. We are continuing on the character count because the combined key=value makes up the entire string length.
for(k=0; ((j<next_str_len) && (j<257)); j++, k++)
{
char current_char = txt_record[i];
i++;
value_buffer[k] = current_char;
}
value_buffer[k] = '\0';
// We know our string lengths and buffer sizes are okay for plain strcpy.
strcpy(array_of_keyvalue_entries[current_entry].keyString, key_buffer);
strcpy(array_of_keyvalue_entries[current_entry].valueString, value_buffer);
}
// Now we have our array of key/value entries.
// Print them out.
printf("TXT Record contains the following:");
for(current_entry=0; current_entry<number_of_entries; current_entry++)
{
printf("%s = %s", array_of_keyvalue_entries[current_entry].keyString, array_of_keyvalue_entries[current_entry].valueString);
}
// free the memory now that we're done
free(array_of_keyvalue_entries);
}

Additional Notes:

For mobile platforms, when the application is suspended, you should stop all running services. (Stop advertising, stop discovering, stop resolving.)

Known Issues:

Windows:

Android: