Just An Application

November 22, 2013

Service Discovery In Android And iOS: Part Nine – iOS Take Five

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.

Blog at WordPress.com.

%d bloggers like this: