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:
- Bonjour (for Apple platforms)
- Avahi (the most popular implementation on Linux and the other Unixes)
- Android Network Service Discovery (Android's native implementation for Zeroconf)
- Windows DNS-SD (Microsoft's native Zeroconf implemetation for Windows UWP.) (Yes, Blurrr Network Service Discovery allows this to be used in traditional, non-UWP apps.)
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:
- To advertise a service for others to find, use BNSD_RegisterService.
If you want to connect to other services:
- To start discovering other services being advertised on the network, use BNSD_StartDiscovery.
- 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:
"My Cool Service",
"_coolservice._tcp.",
NULL,
NULL,
12345,
0,
NULL,
0,
MyRegisterCallback,
NULL
);
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)
{
{
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
);
}
To discover/browse for services:
"_coolservice._tcp.",
NULL,
0,
MyDiscoveryCallback,
NULL
);
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)
{
{
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
);
}
To resolve a service (i.e. get hostname/IP and port number):
"My Cool Service",
"_coolservice._tcp",
NULL,
0,
5,
MyResolveCallback,
NULL
);
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)
{
{
printf("In MyZeroconfResolveCallback: Got TimeOut for service_type: %s", service_type);
return;
}
{
printf("Resolve had an error\n");
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
);
}
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.
#include "SDL.h"
static Uint32 SDL_BNSD_DISCOVERY_EVENT;
struct DiscoveryPayload
{
char* serviceName;
char* serviceType;
char* domain;
bool isAdded;
int32_t blurrrErrorCode;
uint32_t flags;
int32_t interfaceIndex;
void* userData;
};
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)
{
{
printf("Discovery had an error\n");
return;
}
struct DiscoveryPayload* payload = SDL_calloc(1, sizeof(struct DiscoveryPayload));
payload->discoveryService = zeroconf_discovery;
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);
}
void MySDLThreadDiscoveryCallback(struct DiscoveryPayload* payload)
{
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
);
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;
do
{
the_result = SDL_PollEvent(&the_event);
if(the_result > 0)
{
switch(the_event.type)
{
case SDL_KEYDOWN:
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:
{
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;
"_daap._tcp.",
NULL,
0,
MyDiscoveryCallback,
NULL
);
SDL_Window* the_window = SDL_CreateWindow("BlurrrNetworkServiceDiscovery C",
SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
640, 480,
0
);
while(! s_appDone)
{
main_loop();
}
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.
#include "iup.h"
#include "iupcbs.h"
#include "BlurrrCore.h"
#include <string.h>
#include <stdlib.h>
#include <stddef.h>
struct DiscoveryPayload
{
char* serviceName;
char* serviceType;
char* domain;
bool isAdded;
int32_t blurrrErrorCode;
uint32_t flags;
int32_t interfaceIndex;
void* userData;
};
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)
{
{
IupLog("ERROR", "Discovery had an error\n");
return;
}
struct DiscoveryPayload* payload = calloc(1, sizeof(struct DiscoveryPayload));
payload->discoveryService = zeroconf_discovery;
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;
Ihandle* iup_list = (Ihandle*)user_data;
IupPostMessage(iup_list, NULL, payload, 0);
}
void MyIupMainThreadDiscoveryCallback(Ihandle* ih, char* not_used_char, void* message_data, int not_used_int)
{
struct DiscoveryPayload* payload = (struct DiscoveryPayload*)message_data;
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(payload->serviceName);
free(payload->serviceType);
free(payload->domain);
free(payload);
}
static void IupExitPoint()
{
s_discoveryService = NULL;
IupClose();
}
void IupEntryPoint()
{
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);
Ihandle* iup_list = IupList(NULL);
IupSetAttribute(iup_list, "MULTIPLE", "YES");
IupSetCallback(iup_list, "POSTMESSAGE_CB", (Icallback)MyIupMainThreadDiscoveryCallback);
if(is_init)
{
"_daap._tcp.",
NULL,
0,
MyDiscoveryCallback,
iup_list
);
}
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");
IupSetAttribute(the_dialog, "ICON", "IDI_ICON1");
IupShow(the_dialog);
}
int main(int argc, char* argv[])
{
IupOpen(&argc, &argv);
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:
- Does this printer support color?
- What room is the printer in?
- How many players does your network game service support?
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.
struct MyTXTRecord
{
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)
{
{
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
);
if(txt_record && txt_record_length > 0)
{
uint32_t number_of_entries = 0;
size_t i = 0;
while(i<txt_record_length)
{
size_t next_str_len = (size_t)txt_record[i];
i = i + 1 + next_str_len;
number_of_entries++;
}
struct MyTXTRecord* array_of_keyvalue_entries = (struct MyTXTRecord*)calloc(number_of_entries, sizeof(struct MyTXTRecord));
size_t current_entry = 0;
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;
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++;
break;
}
key_buffer[j] = current_char;
}
if(is_key_state)
{
key_buffer[j] = '\0';
strcpy(array_of_keyvalue_entries[current_entry].keyString, key_buffer);
continue;
}
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';
strcpy(array_of_keyvalue_entries[current_entry].keyString, key_buffer);
strcpy(array_of_keyvalue_entries[current_entry].valueString, value_buffer);
}
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(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:
- Microsoft's native UWP backend for Windows 10 has a bug where it does not unregister services correctly so they appear to still be alive. These services will go away if you try to connect to them and they fail.
- For the native UWP backend, you may see messages in the console from the kernel about exceptions. Despite the scary messages, this does not seem to impact any behavior. (Update: This may be fixed in Windows 10 April 2018 Update Version 1803 Build 17134.1.)
- Using "_services._dns-sd._udp.local." to get the list of all service types on the network does not work with the native Microsoft UWP backend.
Android:
- Re-installing/re-launching an app while currently running (debugging) the same app bypasses the normal unregister/clean up, so services continue to appear to still be alive. Try to unregister and quit normally while developing to avoid this problem.
- Android has a bug where it likes to give you an IPv4 address for the host_target in a resolve callback, instead of the actual host name. https://issuetracker.google.com/issues/80368263. Most network APIs can handle either host names or IP addresses, so it is generally not a showstopper.