mikeash.com: just this guy, you know?

Posted at 2013-06-14 13:49 | RSS feed (Full text feed) | Blog Index
Next article: Friday Q&A 2013-06-28: Anatomy of a Compiler Bug
Previous article: Friday Q&A 2013-05-31: C Quiz
Tags: fridayqna networking reachability
Friday Q&A 2013-06-14: Reachability
by Mike Ash  

Networking is playing an ever more important role in application development, and Apple's reachability API is a valuable tool in making network-centric apps play nicely with varying real-world conditions. Today I'm going to give an overview of the reachability API, what it does, and how to use it, a suggestion from reader Manuel Diaz.

Concept
Reachability allows your app to find out whether a remote server is potentially reachable. In short, it tells you whether or not the computer (or iDevice) has a network connection. It also keeps track of the status of the various network connections, and can notify you when they appear or disappear.

Imagine you start an iPhone app in a tunnel with no cell service. It tries to load data, but it fails. It gives you a weird error. You leave the tunnel, but the app is still showing the error. You tap the reload button, hoping that it was just the loss of signal that caused the problem, and it loads.

Now imagine the same scenario, but with an app that uses the reachability API. It notices that there is no internet connection, and doesn't try to load data in the first place. It instead displays an informative message about requiring internet access. Once you leave the tunnel and obtain a signal again, the app automatically loads its data without any intervention, and you go on to use the app.

The second scenario is a lot nicer, and doesn't take too much additional effort to add.

Limitations
Network reachability isn't a panacea and cannot substitute for robust error handling in your networking code.

In particular, the reachability API only accounts for the local environment. If your computer is connected to a Wi-Fi access point, but that access point's internet connection is down, reachability will tell you that yes, you have a network connection. From its perspective, you do. Similarly, you may have a working internet connection, but the target server itself may be offline, or experiencing errors, or slow to respond. Again, reachability will just say yes, you have a connection.

Finally, any pattern where you check for permission and then execute a request is inherently subject to race conditions. Reachability may say that the network connection is up, but then network disconnects before your code makes the actual network request.

All of this is to say that your code must be correct in the absence of reachability, but adopting reachability can greatly enhance usability.

The API
The reachability API is located in the SystemConfiguration framework and is called SCNetworkReachability. It can be found in the documentation under that name. It's a CoreFoundation-like API, so you'll need to do some C.

The basic lifecycle when using the API is this:

  1. Create a reachability object with a given remote host.
  2. Set a callback so it can notify you when things change3
  3. Schedule the reachability object on a runloop or dispatch queue.
  4. If desired, query the reachability object directly.
  5. When done, tear down the object.

Creation
You create a reachability object using the SCNetworkReachabilityCreateWithName call. It takes a hostname in C string format. You can conveniently extract the hostname from a NSURL instance, or hardcode a string, or obtain a hostname any other way that makes sense:

    NSURL *url = ...;
    NSString *host = [url host];
    SCNetworkReachabilityRef reachabilityRef = SCNetworkReachabilityCreateWithName(NULL, [host UF8String]);

Callback
You set a callback on the reachability object using SCNetworkReachabilitySetCallback. This takes a context structure and a C function. Since raw C functions are a little annoying to deal with when the rest of your code is Objective-C, I'll go ahead and write a quick trampoline function to bridge the call over to a block.

The C callback gets three parameters: the reachability object, the new reachability flags, and an info pointer. The reachability object is already accessible, and the info pointer will contain the block, so the block itself will just take the new reachability flags. Here's an example block:

    void (^callbackBlock)(SCNetworkReachabilityFlags) = ^(SCNetworkReachabilityFlags flags) {
        BOOL reachable = (flags & kSCNetworkReachabilityFlagsReachable) != 0;
        NSLog(@"The network is currently %@", reachable ? @"reachable" : @"unreachable");
    };

Next, set up a context structure. It has five fields, a version, which is always 0, an info pointer, a retain and release call, and a copyDescription call. We'll only use version, info, and release. The info pointer will point to the block above, and the release call will just call CFRelease to release the block. The CFBridgingRetain call is used to get ARC to be happy with the type conversion, and to handle memory management correctly:

    SCNetworkReachabilityContext context = {
        .version = 0,
        .info = (void *)CFBridgingRetain(callbackBlock),
        .release = CFRelease
    };

For non-ARC code, the info line can simply be changed to copy the block:

        .info = [callbackBlock copy],

The actual callback function will just extract the block from info and call it:

    static void ReachabilityCallback(SCNetworkReachabilityRef reachabilityRef, SCNetworkReachabilityFlags flags, void *info)
    {
        void (^callbackBlock)(SCNEtworkReachabilityFlags) = (__bridge id)info;
        callbackBlock(flags);
    }

With everything in place, set the callback:

    SCNetworkReachabilitySetCallback(reachabilityRef, ReachabilityCallback, &context);

Scheduling
In order to provide notifications, the reachability object must be scheduled on a runloop or dispatch queue. The callback will be invoked on the specified runloop or queue.

To receive callbacks in the main thread, just schedule the object on the main runloop:

    SCNetworkReachabilityScheduleWithRunLoop(reachabilityRef, CFRunLoopGetMain(), kCFRunLoopDefaultMode);

Or you can do the same thing with the main dispatch queue:

    SCNetworkReachabilitySetDispatchQueue(reachabilityRef, dispatch_queue_get_main());

If you don't want your callbacks tied to the main thread, you could instead pass in a custom dispatch queue or a global queue.

Flags
When your callback is invoked, it will receive the new reachability flags. You can also query the flags at any time directly:

    SCNetworkReachabilityFlags flags = 0;
    bool success = SCNetworkReachabilityGetFlags(reachabilityRef, &flags);
    if(!success) // error getting flags!

There are quite a few potential flags. What do you look at?

The most useful one is the kSCNetworkReachabilityFlagsReachable flag. This flag is set when the destination appears to be reachable, which is usually what you're after.

There are a bunch of informative flags which can tell you about the nature of the reachability described, for example if a connection isn't currently present but will be initiated on demand. See the documentation for a full list and description.

One particularly pertinent flag for iOS development is the kSCNetworkReachabilityFlagsIsWWAN flag. This is set when the target is reachable over the device's cellular connection. You can use this flag to make informed decisions about what sort of data to download, to accommodate the typically slow and expensive nature of these connections.

Note that if you have traffic that absolutely must not be sent over the cellular connection, checking for kSCNetworkReachabilityFlagsIsWWAN is not 100% reliable. The system may think that the destination is reachable through other means, but then later lose that connection and try the cellular network instead. If you need to absolutely prevent this, you can use the setAllowsCellularAccess: method on NSMutableURLRequest.

Cleanup
When done with a reachability object, first unschedule it from the runloop, or clear the dispatch queue:

    // runloop
    SCNetworkReachabilityUnscheduleFromRunLoop(reachabilityRef, CFRunLoopGetMain(), kCFRunLoopDefaultMode);

    // dispatch queue
    SCNetworkReachabilitySetDispatchQueue(reachabilityRef, NULL);

Finally, release the object:

    CFRelease(reachabilityRef);

That's it!

Conclusion
The reachability API is a valuable tool for making a networked app easier and nicer to use. By checking reachability status when making a request, you can provide better error reporting to the user. By watching for reachability changes, you can automatically retry a network request once the user regains connectivity.

That wraps up today's article. Come back soon for the next exciting adventure. Until then, in case you hadn't already heard, Friday Q&A is driven by reader suggestions, so if you have a suggestion for a topic to cover, send it in!

Did you enjoy this article? I'm selling a whole book full of them. It's available for iBooks and Kindle, plus a direct download in PDF and ePub format. It's also available in paper for the old-fashioned. Click here for more information.

Comments:

Steven Fisher at 2013-06-14 14:55:24:
I think you're way off on this one. Reachability is great for diagnostic purposes, but you should never use it before making a request.

Watch for the network to come online and then, sure. But make sure you try that first time.

i run into apps all the time that gate on Reachability, and it just doesn't work.

There was a WWDC session in 2011 about this.

mikeash at 2013-06-14 15:10:37:
Can you elaborate on that? When would reachability say "no" when a request would work?

Robert Vojta at 2013-06-14 16:01:18:
Basically, networking is much more complicated than this simple reachability tool. Sometimes it takes lot of time to set up route/path (especially in cellular networks, …) and reachability can fail. As Steven wrote, reachability is nice, but you can't take it too seriously - I mean, you can't depend on it and stop trying. There also can be firewall/router/... rules where reachability can fail and request succeeds, etc.

Chris L at 2013-06-14 16:05:47:
I know Apple has always recommended that you try making a network request first, and only check reachability if and when it fails. So this is what I always do, though I haven't personally run into a situation where it wouldn't work the other way. (Perhaps VPN on demand, or some other situation where the network isn't up until you request something.)

Also it's grounds for app store rejection if your app fails but doesn't tell the user they need a network connection to continue.

Cheers

Steven Fisher at 2013-06-14 18:41:14:
Reachability tells you that networking is connected. You may be disconnected due to being in a lower power state. If only you tried, the networking would power up and you'd be connected.

Steven Fisher at 2013-06-14 18:44:33:
Basically, I use Reachability in two ways:

1. I use it to provide more guidance on why my connection failed after it fails. At this point, I know that I'm as close to a connection as I can be. And Reachability can help me answer "So why didn't it work?"
2. I use it to determine when I should retry. When Reachability says the network is up, it was definitely (at that moment, anyway) up.

Roger at 2013-06-15 00:20:23:
My pet peeve is being unable to find out if the user is roaming. There are circumstances where I don't want to do networking even if things are reachable.

Vlad at 2013-06-15 13:36:59:
There is a thing about SCNetworkReachabilityGetFlags() that I think it worth mentioning: if reachabilityRef is not scheduled in a run loop then SCNetworkReachabilityGetFlags will operate in synchronous mode, blocking current run loop until it determines the reachability of host. If the run loop is main loop (which is usually the case) then there is a risk of watchdog killing your app.
This is similar like doing [NSData dataWithContentsOfURL:] and other related and seemingly innocent methods. While this is not clearly explained in SCNetworkReachability Reference it is better described in http://developer.apple.com/library/ios/#qa/qa1693/_index.html .

Pierre Lebeaupin at 2013-06-16 12:54:50:
In support of Steven Fisher, I think I do remember reading or hearing at WWDC to indeed only query Reachability after a first attempt that failed, because Reachability not only reports false positive but false negatives as well, though I can't exactly remember what those false negatives were. It's kind of like filesystem accesses: I try, then handle failure.

But on top of failure reason and when to reattempt connection, I also used Reachability to know whether the particular access I was doing (reading a video) was being done cellular or WiFi, not for internal decisions as this is not reliable enough, but for user information: this may be different from what's shown in the status bar as that may show WiFi, but that WiFi is actually a private network and my app is actually using cellular.

Also, I don't know if it is related, but if the network issue is deep in the ISP network Reachability won't be able to tell you when access is back, so it is still necessary to allow the user to manually retry.

David Dancy at 2013-06-17 04:50:16:
I'm using Reachability to determine when to switch on/off some extra functionality in my app. The app has features that only apply when connected to a special web server, and I depend on Reachability to tell me whether the server is available so I can go ahead and switch them on. If I used the "try first, then diagnose failure" method, it seems to me that I'd have to implement some kind of polling which would waste the battery. The special web server isn't often available so Reachability seems like as good a trigger as any to determine when an attempt at talking to the special web server would be worthwhile. (Of course I also handle failures in actual transmissions, but that's a separate issue in my case). I admit that allowing the user to initiate the conversation with the special web server would also work, but it's not part of the design for the app.
One frustration I have though is that I can't find a way (yet - still scouring the docs) to force a network communication to use the Cellular radio instead of WiFi. The idea is to be connected on local WiFi to my special web server, but still able to talk over the Cell connection to other web connections. Normally I'd allow the OS to determine how to do this, but sometimes the WiFi is pre-configured in Infrastructure mode and NSURLConnection isn't clever enough to work out what's going on. I can detect the configuration using Reachability flags (I think) but I can't get around it to force other traffic onto the Cell-based internet connection. Is this even possible?

Nolan at 2013-06-17 15:05:22:
I think Steven's point is very valid and nobody should be gating code based on reachability. However, gating UI is another issue. Apple themselves use reachability to update their app's UI such that if there is not network (per reachability) they block UI in apps that require an internet connect - however behind the scenes, the app is still making network requests as normal. This shows the usefulness of providing users feedback based on reachability without relying on in for you application's logic. Look at Apple's iTunes and Apple Store apps for examples of this.

Konstantin at 2013-06-21 18:47:30:
Steven is right, the Internet is way more complicated than reachability API suggests. The server you're trying to connect to may be reachable from the point of view of TCP/IP, but not actually respond (web server/backend problems). The server may appear unreachable because of access points' authentication, firewalls, cell company restrictions, weird VPN setups and what not, but will respond when you ask it.

The only real way to check it is to make a request using the protocol you normally use, including the necessary authentication parameters if any. Reachability API is just a quick and dirty test for trivial connectivity issues. It can provide useful information, but you cannot trust it as a one-stop solution, even for diagnostic purposes.

Greg Combs at 2013-07-09 16:29:12:
Vlad is absolutely spot-on. When your connectivity is on the fringes of reachability, SCNetworkReachabilityGetFlags() will definitely put a damper on user-experience. The best way I know to approach this is to make sure your code actually only calls GetFlags once (but not on the main thread) and then monitor that address for reachability, reusing the flags that you get back.

I've seen a ton of folks on GitHub use reachability helpers that call GetFlags any time they check if a host is online (even if it's one that they've previously checked on).

Zav at 2013-08-22 14:19:48:
Vlad, your link is no longer valid:

http://developer.apple.com/library/ios/#qa/qa1693/_index.html

This link works though (remove the # in the URL):

https://developer.apple.com/library/ios/qa/qa1693/_index.html

Here is a related link with more info that Apple also recommends:

https://developer.apple.com/library/ios/technotes/tn2277/_index.html

Oleg at 2014-06-30 09:51:16:
What bothers me is the potential race condition that can occur when I call SCNetworkReachabilityGetFlags in a helper thread, while the SCNetworkReachabilityScheduleWithRunLoop has already been set in the main run loop.

There are going to be two different threads both checking for network reachability at the same time and returning their result to the main thread. If network status changes somewhere in between, the events may appear in the run loop queue in an incorrect order.

Is there a way to guarantee the correct event order? Ideally, to make the SCNetworkReachabilityScheduleWithRunLoop to trigger events upon request (not only upon actual network status change)


Comments RSS feed for this page

Add your thoughts, post a comment:

Spam and off-topic posts will be deleted without notice. Culprits may be publicly humiliated at my sole discretion.

Name:
Web site:
Comment:
Formatting: <i> <b> <blockquote> <code>. URLs are automatically hyperlinked.
Code syntax highlighting thanks to Pygments.
Hosted at DigitalOcean.