Blog

The Making of Talkie: Multi-interface broadcasting and multicast

Part 2

TalkieTalkie is my newest product, a Walkie Talkie for iPhone and Mac.

In Part 1 of this series, I wrote about basic broadcasting. This works fine with one network device, but it’s worth discussing how to send through all devices, so you can communicate with others connected via, say, Ethernet and WiFi simultaneously.

So, in Part 2 I’ll write about the approach I took in Talkie for broadcasting from all network devices (a.k.a. network interfaces), so that one can communicate with others connected via WiFi, Ethernet (on a Mac), and any other network devices simultaneously.

Bind them

From Part 1, we have a working broadcast mechanism, but it will only send through the default interface — whatever you’re connected to the network via. This is often sufficient, but if you have more than one device that you communicate through, like Ethernet and WiFi, then you will find that it only works with one.

In order to send through all your connected network interfaces, we need to create one socket for each interface, and bind the socket to its corresponding interface.

Here’s how:

First, we need to obtain a list of all network interfaces with getifaddrs.

#include <ifaddrs.h>
...
struct ifaddrs *addrs;
int result = getifaddrs(&addrs);
if ( result < 0 ) {
  // Error occurred
  return 0;
}

Now, addrs is a list of interfaces that we can iterate over. We now do so, picking out those devices that support broadcasting, and that aren’t loopback or point-to-point devices — loopback is an internal interface that is provided for your computer’s inner dialogue, and point-to-point (ppp) devices include dialup interfaces, 3G modems and the like. We can exclude those guys.

const struct ifaddrs *cursor = addrs;
while ( cursor != NULL ) {
  if ( cursor->ifa_addr->sa_family == AF_INET 
          && !(cursor->ifa_flags & IFF_LOOPBACK) 
          && !(cursor->ifa_flags & IFF_POINTOPOINT) 
          &&  (cursor->ifa_flags & IFF_BROADCAST) ) {
 
    // We will do some stuff in here
 
  }
  cursor = cursor->ifa_next;
}

Now, for each interface that meets our criteria, we create a socket (which we covered in Part 1), then bind the socket to the network interface, to force transmission from that particular device. Finally, as we did in Part 1, we enable broadcasting using setsockopt with SO_BROADCAST.

We want to store the sockets we create in an array, so we can access them later. If we assume a maximum number of interfaces we will support (lets call it kMaxSockets), we can just use an array of that length. So, putting it together:

#define kMaxSockets 16
...
int sock_fds[kMaxSockets];
int number_sockets = 0;
 
while ( cursor != NULL && number_sockets < kMaxSockets ) {
  if ( cursor->ifa_addr->sa_family == AF_INET 
          && !(cursor->ifa_flags & IFF_LOOPBACK) 
          && !(cursor->ifa_flags & IFF_POINTOPOINT) 
          &&  (cursor->ifa_flags & IFF_BROADCAST) ) {
 
    // Create socket
    sock_fds[number_sockets] = socket(AF_INET, SOCK_DGRAM, 0);
    if ( sock_fds[number_sockets] == -1 ) {
      // Error occurred
      return 0;
    }
 
    // Create address from which we want to send, and bind it
    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr = ((struct sockaddr_in *)cursor->ifa_addr)->sin_addr;
    addr.sin_port = htons(0);
 
    int result = bind(sock_fds[number_sockets], (struct sockaddr*)&addr, sizeof(addr));
    if ( result < 0 ) {
      // Error occurred
      return 0;
    }
 
    // Enable broadcast
    int flag = 1;
    result = setsockopt(sock_fds[number_sockets], SOL_SOCKET, SO_BROADCAST, &flag, sizeof(flag));
    if ( result != 0 ) {
      // Error occurred
      return 0;
    }
 
    number_sockets++;
  }
  cursor = cursor->ifa_next;
}

Finally, as before, we can setup a broadcast address to send to, and use sendto to broadcast, this time for each socket we created:

// Initialise broadcast address
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_BROADCAST;
addr.sin_port = htons(kPortNumber);
 
// Send through each interface
int i;
for ( i=0; i<number_sockets; i++ ) {
  int result = sendto(sock_fds[i], data, length, 0, (struct sockaddr*)&addr, sizeof(addr));
  if ( result < 0 ) {
    // Error occurred
    return 0;
  }
}

Note that the receive routine only needs a single socket, as we can receive on any interface when we use INADDR_ANY. So, the receive routine needs no changes from the single-interface version we saw in Part 1.

Here’s the test app with the above modification: broadcast_sample_all_interfaces.c

Again, compile by opening Terminal, and typing make broadcast_sample_all_interfaces or cc -o broadcast_sample_all_interfaces broadcast_sample_all_interfaces.c, then run it with ./broadcast_sample_all_interfaces "Message to send" to send, or just ./broadcast_sample_all_interfaces with no arguments to listen.

You may notice that multiple messages may be received: These have probably arrived via multiple network interfaces, virtual or otherwise. It’s usually a good idea to check for duplicate messages, if this is an issue for program operation, by including a sequence number into the message — this will be discussed in Part 3.

It also may be a good idea to ignore your own messages, which may find their way back to you. One way to accomplish this is to examine the source address (addr in the example above) and compare it with your local interface addresses (stored in addrs, above). If you get a match, the message came from you, and you can drop it.

Multicast

Broadcast is fine when everyone on the local network is interested in what you have to say. If this isn’t the case, though (lets face it, those Chuck Norris jokes aren’t for everyone), effort is wasted delivering to those who aren’t particularly interested.

Multicast works by using a specific address that one ‘subscribes’ to in order to receive messages sent to that address. So, it’s opt-in, allowing for better efficiency and one day, Internet-wide support for ‘to-many’ communications.

Well, in theory. Actually, multicast is still quite new, and for the most part — from what I understand — it behaves pretty much like broadcast on a local area network. However, support can only increase, and given that many services already use it — Multicast DNS (mDNS), also known as Bonjour, being one of the most well-known examples — it seems a good idea to follow their lead. Note also that IPv6, the successor to IP as we know it, and our saviour-to-be from our little Internet overpopulation problem (among other things), doesn’t even have broadcast provisions — the future is all multicast.

So, for these reasons, Talkie speaks multicast, instead of plain ol’ broadcast.

Making use of multicast is relatively straightforward: To receive, you join a multicast group using setsockopt with IP_ADD_MEMBERSHIP, and the address of the multicast group, which is in the range 224.0.0.0-239.255.255.255 (for IPv4, of course). To send, you just use sendto to transmit data to a multicast group address.

Using multicast on all network interfaces is just a little more complicated. Here’s how it’s done with Talkie:

Sending

The send routine is very similar to the one above, using broadcast. However, instead of using bind to specify the outgoing network interface and enabling broadcast, we assign a multicast interface using setsockopt with IP_MULTICAST_IF. And, instead of transmitting to the broadcast address, we transmit to the multicast group address.

Again, we loop through all network interfaces. This time, we pick out those that support multicast (ifa_flags & IFF_MULTICAST):

const struct ifaddrs *cursor = addrs;
while ( cursor != NULL ) {
  if ( cursor->ifa_addr->sa_family == AF_INET 
          && !(cursor->ifa_flags & IFF_LOOPBACK) 
          && !(cursor->ifa_flags & IFF_POINTOPOINT) 
          &&  (cursor->ifa_flags & IFF_MULTICAST) ) {
 
    // We will do some stuff in here
 
  }
  cursor = cursor->ifa_next;
}

And, after creating the socket, we assign the interface:

if ( setsockopt(sock_fds[number_sockets], IPPROTO_IP, IP_MULTICAST_IF, &((struct sockaddr_in *)cursor->ifa_addr)->sin_addr, sizeof(struct in_addr)) != 0  ) {
  // Error occurred
  return 0;
}

Finally, as a nicety, we can disable loopback so that we don’t receive our own messages. This isn’t 100% reliable, as certain network conditions can result in the local machine still receiving its outgoing messages, but it can improve efficiency:

u_char loop = 0;
if ( setsockopt(sock_fds[number_sockets], IPPROTO_IP, IP_MULTICAST_LOOP, &loop, sizeof(loop)) != 0 ) {
  // Error occurred
  return 0;
}

Now that our sockets are set up, we can prepare to send to the multicast address:

#define kMulticastAddress "224.0.0.123"
...
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(kMulticastAddress);
addr.sin_port = htons(kPortNumber);

And, as before, we send through each of the sockets we created:

int i;
for ( i=0; i<number_sockets; i++ ) {
  if ( sendto(sock_fds[i], data, length, 0, (struct sockaddr*)&addr, sizeof(addr)) < 0 ) {
    // Error occurred
    return 0;
  }
}

Receiving

Receiving messages from a multicast group on several network interfaces is a little more involved than doing so with broadcast: We need to subscribe to the multicast group from each network interface, explicitly. If we were to just specify no device in particular, the system would choose a single interface for us, neglecting the others.

Joining the multicast group for each interface takes place in a now-familiar loop:

const struct ifaddrs *cursor = addrs;
while ( cursor != NULL ) {
  if ( cursor->ifa_addr->sa_family == AF_INET 
          && !(cursor->ifa_flags & IFF_LOOPBACK) 
          && !(cursor->ifa_flags & IFF_POINTOPOINT) 
          &&  (cursor->ifa_flags & IFF_MULTICAST) ) {
 
    // We will do some stuff in here
 
  }
  cursor = cursor->ifa_next;
}

For each network device, we use the IP_ADD_MEMBERSHIP property with setsockopt to join — thereby subscribing to any messages sent to the multicast group address that reach that network interface.

First, we prepare the join request structure. This provides the multicast group address, and the network interface:

struct ip_mreq multicast_req;
memset(&multicast_req, 0, sizeof(multicast_req));
multicast_req.imr_multiaddr.s_addr = inet_addr(kMulticastAddress);
multicast_req.imr_interface = ((struct sockaddr_in *)cursor->ifa_addr)->sin_addr;

Now we use this structure to join the group:

if ( setsockopt(sock_fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &multicast_req, sizeof(multicast_req)) < 0 ) {
  // Error occurred
  return 0;
}

Now, a caveat: While it’s perfectly legal to join the same multicast group on more than one network interface, and up to 20 memberships may be added to the same socket (see ip(4)), for some reason, OS X spews ‘Address already in use’ errors when we actually attempt it.

As a workaround, we can ‘drop’ the membership first, which would normally have no effect, as we have not yet joined on this interface. However, it enables us to perform the subsequent join, without dropping prior memberships.

So, before we perform the above IP_ADD_MEMBERSHIP, we do a IP_DROP_MEMBERSHIP first:

setsockopt(sock_fd, IPPROTO_IP, IP_DROP_MEMBERSHIP, &multicast_req, sizeof(multicast_req));

This sets up our socket to receive messages sent to the multicast group that are received via any interface.

Here it is all put together: multicast_sample.c

Compile by opening Terminal, and typing make multicast_sample or cc -o multicast_sample multicast_sample .c, then run it with ./multicast_sample "Message to send" to send, or just ./multicast_sample with no arguments to listen.

Still to come

So, now we mostly have networking covered. There’s one obvious omission, though, for an iPhone app: Bluetooth. In Part 3, I’ll discuss how to perform communications over Bluetooth on the iPhone, in a way that’s compatible with the above generic network communications. I’ll also talk about how to connect to other devices automatically, without user intervention — This is one particularly popular feature of Talkie that allows it to behave more like a real walkie-talkie.

I promised in Part 1 that I’d talk about packet formats. We’ve covered a lot of ground in Part 2, however, so it shall be postponed to Part 3 — I’ll discuss how to ensure you get messages in the correct order by using sequence numbers, as well as providing for versioning and a few other bits and pieces.

, , , , , , , , , , . Bookmark the permalink. Both comments and trackbacks are currently closed.

5 Comments

  1. Lassic
    Posted March 1, 2012 at 3:10 pm | Permalink

    Great article, did you ever get around to writing Part3 about Bluetooth? I’m very interested in that follow up.

    Cheers!

    • Posted March 1, 2012 at 4:35 pm | Permalink

      Thanks, Lassic =) I’m afraid I flaked out on that one! Never did get around to it…. GameKit’s what you want, though, and it’s pretty good!

  2. Jan Cássio
    Posted July 17, 2012 at 4:03 pm | Permalink

    Hi Michael, this is very nice article.

    I’m working on a iOS app where a device needs transmit sound and video to another iOS device, it’s very similar to make a chat with voice and video, but the other device just receive the tranmission, not need to transmit.

    I do some researches and I implemented a TCP socket using CFNetwork to receive camera frames and some other commands from master device, this works fine, I just need to optimize image encoding to improve performance, but no other problems here.

    My problem is about audio, it’s a very complicated topic on iOS and I read some articles about (your post about Remote I/O too, where I implemented a simple test app to listen microphone and plays in same time in the same device, to avoid the sound feedback, I just put a earphone, very nice article too, thanks for share this things!), my problem is how to pack sound unit bytes and plays in another device.

    I not sure how to do this, and is very complicated to find a good explanation about this. Can you help me with some ideas?

    I think about to use GKVoiceChatService, but the bad thing here is if I use then, the GKSession opens an alert asking to user to authorize a connection with a device, I want to avoid any authorization and start voice transmission when user starts then.

    Anyway, I really grateful with your attention to explain clearly about Remote I/O and this post series too.

    Cheers,

  3. Jim
    Posted August 8, 2012 at 12:08 pm | Permalink

    Hello Michael. I have a similar app that uses multicasting. I was wondering if you were able to run the app using the iOS simulator? Mine fails on bind() when using the simulator and I have not been able to find a work around yet.

    • Posted August 8, 2012 at 1:54 pm | Permalink

      Hey Jim – Yes, but that was quite a while ago; there’s a chance Apple’s changed something since then.