Monitoring Socket Viability

This thread has been locked by a moderator.

Working a DTS incident today, someone asked me exactly how you use SCNetworkReachabilityCreateWithAddressPair to monitor a socket’s viability (per the advice in TN3151). I thought I’d posted code for that many times but, on searching my records, it turns out that I haven’t. Ever. Weird!

Anyway, this post rectifies that. If you have questions or comments, start a new thread here on DevForums. Tag it with System Configuration so that I see it.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"


Monitoring Socket Viability

The BSD Sockets best practices section of TN3151 Choosing the right networking API says:

Once you’ve established a connection, use SCNetworkReachabilityCreateWithAddressPair to monitor its viability. Without this, your program won’t notice that a connection is stuck due to a TCP/IP stack reconfiguration.

While this is correct, it’s not exactly expansive. This post shows how you might use that call to monitor the viability of a socket. It explains this technique using a Swift example, but there’s a C version of the code below for those with a “sad devotion to that ancient religion” (-:

Seriously though, this code is very specific to Apple platforms, where you also have access to Network framework, and that has built-in support for this sort of thing. Only use this code if you absolutely must stick with BSD Sockets.

IMPORTANT The Swift code in this post relies on the QSocket2023 library from Calling BSD Sockets from Swift.

To start, I assume you have a class like this:

final class QConnection {
    private var socket: FileDescriptor
    private var queue: DispatchQueue
    private var targetQ: SCNetworkReachability?
    
    … initialiser elided …
}

Each instance of this class ‘owns’ the socket. It uses a Dispatch queue that serialises all access to the instance. Finally, the targetQ property holds a reachability object that monitor’s the socket’s viability.

Once an instance has connected its socket, it calls this method to start monitoring its viability:

extension QConnection {

    fileprivate func startMonitoring() throws {
        let local = try socket.getSockName()
        let remote = try socket.getPeerName()

        let target = try scCall {
            try QSockAddr.withSockAddr(address: local.address, port: local.port) { saLocal, _ in
                try QSockAddr.withSockAddr(address: remote.address, port: remote.port) { saRemote, _ in
                    SCNetworkReachabilityCreateWithAddressPair(nil, saLocal, saRemote)
                }
            }
        }

        var context = SCNetworkReachabilityContext()
        context.info = Unmanaged.passUnretained(self).toOpaque()
        try scCall{ SCNetworkReachabilitySetCallback(target, { target, flags, info in
            let obj = Unmanaged<QConnection>.fromOpaque(info!).takeUnretainedValue()
            let isViable = flags.contains(.reachable)
            obj.viabilityDidChange(isViable)
        }, &context) }
        try! scCall{ SCNetworkReachabilitySetDispatchQueue(target, self.queue) }

        self.targetQ = target
    }
}

This creates a reachability object with the socket’s local and remote addresses. It then sets up a callback on that object that runs on the instance’s Dispatch queue. That callback calls viabilityDidChange(_:) when the reachability flags change.

Here’s the viabilityDidChange(_:) method:

extension QConnection {

    fileprivate func viabilityDidChange(_ isViable: Bool) {
        dispatchPrecondition(condition: .onQueue(self.queue))
        print("isViable: \(isViable)")
    }
}

This just prints the new value. In a real product you would:

  • Merge duplicate changes.

  • Debounce the change. The way that reachability works, you might see transient events, and it’s best to ignore those.

  • If the socket is non-viable for too long, start the process of closing it down.

Finally, here’s the code to stop viability monitoring.

extension QConnection {

    fileprivate func stopMonitoring() {
        guard let target = self.targetQ else { return }
        self.targetQ = nil
        
        try! scCall { SCNetworkReachabilitySetCallback(target, nil, nil) }
        try! scCall { SCNetworkReachabilitySetDispatchQueue(target, nil) }
    }

}

Note The snippets above assume two simple helper routines that turn System Configuration framework errors into Swift errors:

func scCall(_ body: () throws -> Bool) throws {
    let ok = try body()
    guard ok else {
        throw SCCopyLastError()
    }
}

func scCall<Result>(_ body: () throws -> Result?) throws -> Result {
    guard let result = try body() else {
        throw SCCopyLastError()
    }
    return result
}

And for those of you working with a C-based language, here’s pretty much the same code in plain ol’ C:

struct QConnection {
    int sock;
    dispatch_queue_t queue;
    SCNetworkReachabilityRef target;
};
typedef struct QConnection QConnection;

static bool _QConnectionStartMonitoring(QConnection * connection) {
    struct sockaddr_storage local = {};
    socklen_t localLen = sizeof(local);
    bool ok = getsockname(connection->sock, (struct sockaddr *) &local, &localLen) >= 0;
    if ( ! ok ) { return false; }

    struct sockaddr_storage remote = {};
    socklen_t remoteLen = sizeof(remote);
    ok = getpeername(connection->sock, (struct sockaddr *) &remote, &remoteLen) >= 0;
    if ( ! ok ) { return false; }

    SCNetworkReachabilityRef target = SCNetworkReachabilityCreateWithAddressPair(
        NULL,
        (const struct sockaddr *) &local,
        (const struct sockaddr *) &remote
    );
    if (target == NULL) { return false; }
    
    SCNetworkReachabilityContext context = { .info = connection };
    ok = SCNetworkReachabilitySetCallback(target, _QConnectionReachabilityCallback, &context);
    if ( ! ok ) {
        CFRelease(target);
        return false;
    }
    
    ok = SCNetworkReachabilitySetDispatchQueue(target, connection->queue);
    if ( ! ok ) {
        ok = SCNetworkReachabilitySetCallback(target, NULL, NULL);
        assert(ok);
        CFRelease(target);
        return false;
    }
    
    connection->target = target;
    return true;
}

static void _QConnectionReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void * info) {
    #pragma unused(target)
    #pragma unused(info)
    QConnection * connection = (QConnection *) info;
    bool isViable = (flags & kSCNetworkReachabilityFlagsReachable) != 0;
    _QConnectionViabilityDidChange(connection, isViable);
}

static void _QConnectionViabilityDidChange(QConnection * connection, bool isViable) {
    dispatch_assert_queue(connection->queue);
    fprintf(stderr, "isViable: %d\n", isViable);
}

static void _QConnectionStopMonitoring(QConnection * connection) {
    SCNetworkReachabilityRef target = connection->target;
    if (target == NULL) { return; }
    connection->target = NULL;
    
    bool ok = SCNetworkReachabilitySetCallback(target, NULL, NULL);
    assert(ok);
    ok = SCNetworkReachabilitySetDispatchQueue(target, NULL);
    assert(ok);
    CFRelease(target);
}
Up vote post of eskimo
323 views