And if none of the previous choices are to your liking then there is also the theoretical possibility of doing the whole thing from scratch as in the Java case.
For this to be feasible it must be possible to send and receive multicast UDP datagrams at the application level. There is nothing in Foundation or Core Foundation that supports this directly but it is possible at the POSIX level.
1.0 Opening A Multicast UDP Socket Using POSIX System Calls
Opening a multicast UDP socket at the POSIX level is feasible if a little bit fiddly as befits something that needs to be done in C/C++.
It requires three system calls.
1.1 socket
First we need to open a socket using the socket system call which is declared in the header file <sys/socket.h>
like this
int socket(int domain, int type, int protocol);
In our case the domain
argument is AF_INET
because we want to use the socket to communicate using IP over the network.
The type
argument is SOCK_DGRAM
because we want to use the socket to send and receive datagrams.
The protocol
argument is 0
which means we want to use the default protocol given the socket domain and type which should be UDP.
So our code to open the socket looks like this.
...
s = socket(AF_INET, SOCK_DGRAM, 0);
if (s == -1)
{
perror("socket");
return -1;
}
...
1.2 bind
Having opened the socket we need to associate it with a local address and port using the bind system call which is declared in the header file <sys/socket.h>
like this
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
In our case we need to specify an IP address using an instance of struct sockaddr_in
which is defined in the header file
<netinet/in.h>
and then cast appropriately.
On the assumption that there is only one network interface so we do not need to be bound to a specific one we specify the address as INADDR_ANY
.
As we do not need to send and receive on a specific port we specify the port as 0 so the system will allocate a port number for us.
...
struct sockaddr_in inetAddress;
memset(&inetAddress, 0, sizeof(inetAddress));
inetAddress.sin_family = AF_INET;
inetAddress.sin_addr.s_addr = htonl(INADDR_ANY);
inetAddress.sin_port = htons(0);
if (bind(s, (const struct sockaddr*)&inetAddress, sizeof(inetAddress)) == -1)
{
perror("bind");
close(s);
return -1;
}
...
Note
The address and port in the sockaddr_in
struct should be in network byte order hence the use of htonl
and htons
.
As it happens in this case it is not necessary to convert either the address or port from host to network byte order but doing so in all cases helps ensure that you don’t forget in a case where it is necessary.
1.3 setsockopt
Having bound the socket to a local address and port number we need to associate it with the multicast group we wish to send to and receive from.
We do this using the setsockopt system call which is defined like this in the header file <sys/socket.h>
int setsockopt(int socket, int level, int option_name, const void *option_value, socklen_t option_len);
In our case level
is IPPROTO_IP
as we are setting an IP level option.
The option_name is IP_ADD_MEMBERSHIP
indicating that we want to add the socket to the membership of a multicast group.
The option_value is a pointer to an instance of the struct ip_mreq
which specifies the group address,
imr_multiaddr
, and the network interface, imr_interface
.
...
struct ip_mreq imr;
imr.imr_multiaddr.s_addr = group;
imr.imr_interface.s_addr = htonl(INADDR_ANY);
if (setsockopt(s, IPPROTO_IP, IP_ADD_MEMBERSHIP, &imr, sizeof(imr)) == -1)
{
perror("setsockopt");
close(s);
return -1;
}
...
where group
is the multicast group address in network byte order.
As before we assume there is only a single network interface and specify INADDR_ANY
.
Note
The imr_multiaddr
and imr_interface
fields are both of type struct in_addr
and as in the bind case above the addresses must be in network byte order.
2.0 Making A POSIX Socket More iOS Like
Putting to one side for the moment the issue of whether it is actually possible to open a multicast socket there is the question of how it could be used in the context of an iOS application.
Although a POSIX socket is perfectly functional, receiving datagrams using the recvfrom
system call is by default synchronous so using it as is would require at least one separate non-standard thread.
Fortunately the function CFSocketCreateWithNative
can turn a theoretical POSIX socket into a theoretical CFSocket
.
Using CFSocketCreateWithNative
we can specify a function to be called whenever the socket is readable and in return we will get a CFSocketRef
Once we have a theoretical CFSocketRef
we can obtain a CFRunLoopSource
using the function CFSocketCreateRunLoopSource
and then add it to the CFRunLoop
of our choice.
The end result will be that our function is called when the socket is readable so recvfrom
will not block and this will be done in the context of a standard CFRunLoop
.
3.0 But Does It Work ?
Perhaps surprisingly the answer is yes, at least on an iPad running iOS 7.0.4.
DNS messages can be multicast, responses received and services duly discovered.
4.0 The MulticastSocket Class
Here is an initial attempt at encapsulating the multicast socket.
//
// MulticastSocket.m
// XperTakeFive
//
// Created by Simon Lewis on 03/11/2013.
// Copyright (c) 2013 Simon Lewis. All rights reserved.
//
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#import "MulticastSocket.h"
@interface MulticastSocket()
- (void)read;
@end
@implementation MulticastSocket
{
CFSocketRef socketRef;
CFRunLoopSourceRef runLoopSourceRef;
in_addr_t group;
int s;
struct sockaddr_in from;
unsigned char buffer[4096];
}
static void socketCallback(
CFSocketRef theSocketRef,
CFSocketCallBackType theCallbackType,
CFDataRef theAddress,
const void* theData,
void* theInfo)
{
switch (theCallbackType)
{
case kCFSocketReadCallBack:
[(__bridge MulticastSocket*)theInfo read];
break;
default:
NSLog(@"socketCallback: %lu !", theCallbackType);
}
}
- (id)init:(NSString*)theGroup
{
self = [super init];
if (self != nil)
{
group = inet_addr([theGroup UTF8String]);
}
return self;
}
- (void)dealloc
{
if (s != -1)
{
close(s);
CFRelease(socketRef);
CFRelease(runLoopSourceRef);
}
}
- (BOOL)open
{
s = socket(AF_INET, SOCK_DGRAM, 0);
if (s == -1)
{
perror("socket");
return NO;
}
struct sockaddr_in inetAddress;
memset(&inetAddress, 0, sizeof(inetAddress));
inetAddress.sin_family = AF_INET;
inetAddress.sin_addr.s_addr = htonl(INADDR_ANY);
inetAddress.sin_port = htons(0);
if (bind(s, (const struct sockaddr*)&inetAddress, sizeof(inetAddress)) == -1)
{
perror("bind");
return NO;
}
struct ip_mreq imr;
imr.imr_multiaddr.s_addr = group; // in network byte order courtesy of inet_addr
imr.imr_interface.s_addr = htonl(INADDR_ANY);
if (setsockopt(s, IPPROTO_IP, IP_ADD_MEMBERSHIP, &imr, sizeof(imr)) == -1)
{
perror("setsockopt");
return NO;
}
CFSocketContext context;
memset(&context, 0, sizeof(context));
context.info = (__bridge void*)self;
socketRef = CFSocketCreateWithNative(NULL, s, kCFSocketReadCallBack, socketCallback, &context);
if (socketRef == NULL)
{
return NO;
}
runLoopSourceRef = CFSocketCreateRunLoopSource( NULL, socketRef, 0);
if (runLoopSourceRef == NULL)
{
return NO;
}
CFRunLoopAddSource(CFRunLoopGetMain(), runLoopSourceRef, kCFRunLoopDefaultMode);
return YES;
}
- (void)send:(unsigned char*)theData ofLength:(ssize_t)theLength toPort:(uint16_t)thePort
{
struct sockaddr_in inetAddress;
memset(&inetAddress, 0, sizeof(inetAddress));
inetAddress.sin_family = AF_INET;
inetAddress.sin_addr.s_addr = group;
inetAddress.sin_port = ntohs(thePort);
ssize_t nBytes = sendto(s, theData, theLength, 0, (const struct sockaddr*)&inetAddress, sizeof(inetAddress));
if (nBytes == -1)
{
perror("sendto");
}
}
// internal
- (void)read
{
socklen_t length = sizeof(from);
memset(&from, 0, sizeof(from));
ssize_t nBytes = recvfrom(s, buffer, sizeof(buffer), 0, (struct sockaddr*)&from, &length);
if (nBytes != -1)
{
[self.delegate received:buffer ofLength:nBytes from:&from];
}
else
{
perror("recvfrom");
}
}
@end
5.0 And The Rest
The rest is just reading and writing messages which is much the same in Objective-C as it is in Java.
For example, here is the method for reading a message.
...
- (BOOL)readRecord:(Category)theCategory
{
NSString* name = [self readNodeName];
if (name == nil)
{
return NO;
}
if ((offset + HEADER_LENGTH) > length)
{
return NO;
}
uint16_t type = UNSIGNED_SHORT;
uint16_t class = UNSIGNED_SHORT;
uint32_t ttl = UNSIGNED_INT;
uint16_t rdlength = UNSIGNED_SHORT;
[responseHandler
record:
[[ResourceRecord alloc]
init:
name
type:
type
class:
class
ttl:
ttl
data:
[[RecordData alloc]
init:
message
offset:
offset
length:
offset + rdlength]]
category:
theCategory];
offset += rdlength;
return YES;
}
...
Copyright (c) 2013 By Simon Lewis. All Rights Reserved.
Unauthorized use and/or duplication of this material without express and written permission from this blog’s author and owner Simon Lewis is strictly prohibited.
Excerpts and links may be used, provided that full and clear credit is given to Simon Lewis and justanapplication.wordpress.com with appropriate and specific direction to the original content.