System Configuration

0x00 前言

这篇文章是自己在学习 CFHipsterRef Chapter 11 System Configuration 时做的笔记。

0x01 Sysem Configuration

系统配置(Sysem Configuration)包含用于确定硬件配置和网络状态的 C API。

大多数开发者对框架的曝光是通过 Apple 的示例代码,简称 “Reachability”。旨在演示使用系统配置调用来确定互联网连接,如今却成为成千上万的应用程序的一部分。因此,表明 Reachability 是其自身成功的一个受害者。而不是记录系统配置 API,它无意地使它们废弃。

不幸的后果是,大多数开发者对 reachability 是什么,以及如果使用它一知半解。因此,很多那些在其源码中有 Reachability.m 的应用程序可能实际上比根本没有做任何事情的用户体验更糟。

0x02 Reachability

“我连接到互联网了吗?” 是一个难以回答的问题。

从用户的角度来看,应该很容易,对吧?只需要在 Safari 中输入 apple.com,看看是否有东西加载出来。

0x0201 Determining Network Reachability Synchronously

与任何网络一样,建立 reachability 不应同步进行。然而,为了建立到更复杂的同步使用,看起来像是这样:

1
2
3
4
5
6
7
8
@import SystemConfiguration;

SCNetworkReachabilityRef networkReachability = SCNetworkReachabilityCreateWithName(kCFAllocatorDefault, [@"www.apple.com" UTF8String]);
SCNetworkReachabilityFlags flags = SCNetworkReachabilityGetFlags(networkReachability, &flags);

// Use flags to determine reachability

CFRelease(networkReachability);

SCNetworkReachability 是负责确定网络可达性(Reachability)的数据类型。它可以通过传递主机名(host name)(如上面示例)或一个 sockaddr 地址来创建:

1
2
3
4
5
6
7
8
9
10
11
BOOL ignoreAdHocWiFi = NO;

struct sockaddr_in ipAddress;
bzero(&ipAddress, sizeof(ipAddress));
ipAddress.sin_len = sizeof(ipAddress);
ipAddress.sin_family = AF_INET;
ipAddress.addr.s_addr = htonl(ignoreAdHocWiFi ? INADDR_ANY : IN_LINKLOCALNETNUM);

SCNetworkReachabilityRef networkReachability =
SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault,
(struct sockaddr *)ipAddress);

SCNetworkReachabilityFlags 是一个同步调用,用于确定可用网络接口的可达性。因为有很多不同的因素影响可达性,所以这个函数返回值不是一个简单的 YES / NO,而是一个特征位掩码(bitmask of characteristics):

Reachability Flag Values

Reachable 使用当前的网络配置可以访问指定的节点名称或者地址。
Transient Connection 可以通过一个瞬态连接(如 PPP)访问指定的节点名称或地址。
Connection Required 使用当前的网络配置可以访问指定的节点名称或者地址,但必须先建立连接。
Connection On Traffic 使用当前的网络配置可以访问指定的节点名称或者地址,但必须先建立连接。任何针对指定名称或地址的传输都将启动连接。
Connection On Demand 使用当前的网络配置可以访问指定的节点名称或者地址,但必须先建立连接。
Intervention Required 使用当前的网络配置可以访问指定的节点名称或者地址,但必须先建立连接。另外,将需要某种形式的用户干预来建立此连接,例如提供密码,认证 token 等。
Is Local Address 指定的节点名称或地址是与当前系统上的网络接口相关联的。
Is Direct 网络传输到指定的节点名称或地址将不会通过网关(gateway),而是直接路由(routed)到系统的一个接口上。

尽管网络接口的复杂性非常是有趣的,但它们只是对 app 开发者的学术练习(academic exercise),就像它们的用户,真的喜欢 YESNO 答案。

下面是如何归纳一个复杂的真理:

1
2
3
4
5
6
7
8
9
10
11
12
BOOL isReachable = ((flags & kSCNetworkReachabilityFlagsReachable) != 0);

BOOL needsConnection = ((flags & kSCNetworkReachabilityFlagsConnectionRequired) != 0);

BOOL canConnectionAutomatically =
(((flags & kSCNetworkReachabilityFlagsConnectionOnDemand) != 0) ||
((flags & kSCNetworkReachabilityFlagsConnectionOnTraffic) != 0));

BOOL canConnectWithoutUserInteraction =
(canConnectionAutomatically && (flags & kSCNetworkReachabilityFlagsInterventionRequired) == 0);

BOOL isNetworkReachable = (isReachable && (!needsConnection || canConnectWithoutUserInteraction));

再进一步,reachability flags 也可以用于确定正在使用哪个网络接口:

1
2
3
4
5
6
7
8
9
10
11
12
if (isNetworkReachable == NO) {
// Not Reachable
}

#if TARGET_OS_IPHONE
else if ((flags & kSCNetworkReachabilityFlagsIsWWAN) != 0) {
// Reachable via WWAN
}
#endif
else {
// Reachable via WiFi
}

在主线程上调用 SCNetworkReachabilityFlags 发起一个 30 秒超时的 DNS 查找。这样不好,不要阻塞,同步调用 SCNetworkReachabilityFlags

0x0202 Determining Network Reachability Asynchronously

幸运的是,System Configuration 框架提供了一组用于异步监控 reachability 变化的 API。

首先,定义一个静态回调函数,该函数需要网络 reachability reference,flags 以及要传递的任何附加 context。

1
2
3
4
5
static void ReachabilityCallback(SCNetworkReachabilityRef target,
SCNetworkConnectionFlags flags,
void *context) {
// ...
}

一旦声明了回调函数,它就可以被设置到 SCNetworkReachabilityRef 上,然后在 runloop 上调度:

1
2
3
4
5
SCNetworkReachabilityContext context = {0, NULL, NULL, NULL, NULL};

SCNetworkReachabilitySetCallback(networkReachability, ReachabilityCallback, &context);

SCNetworkReachabilityScheduleWithRunLoop(reachability, CFRunLoopGetMain(), kCFRunLoopCommonModes);

现在,每当设置的网络 reachability 发生变化,ReachabilityCallback 函数就会被调用。然后,可以通过发布到 NSNotificationQueue,或通过调用一个 block 传递到 SCNetworkReachabilityContext 参数中,将此信息传达给应用程序。

0x03 Network Configuration Settings

原谅有人认为 System Configuration 是一个 one-trick pony。几乎每个对该框架的提及都是在引用网络 reachability,也就是它本身是一个以该死的 Reachability 例子为主导的话题。

更模糊的功能与查询系统的动态存储有关:SCDynamicStore。这些 API 仅在 OS X 上可用。

0x0301 Query

SCDynamicStoreRef 为当前系统配置提供了一个键-值接口,该接口由 configd 守护程序(daemon)管理。

可用使用 SCDynamicStoreCopyKeyList 获取可用的 key 列表:

1
2
3
SCDynamicStoreContext context = { 0, NULL, NULL, NULL, NULL };
SCDynamicStoreRef store = SCDynamicStoreCreate(NULL, NULL, nil, &context);
NSArray *keys = (__bridge_transfer NSArray *)SCDynamicStoreCopyKeyList(store, CFSTR(".+"));
  • Setup:/ Network/Service/…IPv4,
  • Setup:/ Network/Service/…IPv6,
  • … 100+ Items …
  • State:/Network/Interface/p2p0/Link,
  • State:/Network/Interface/lo0/IPv6,
  • State:/IOKit/LowBatteryWarning,
  • State:/Network/MulticastDNS,
  • State:/Network/Global/Proxies,
  • State:/Network/Interface/bridge0/Link

注:完整的 key 列表可以查看 Apple 的 System Configuration Programming Guide

有了 key 的列表,SCDynamicStore 的目的变得更清晰。IP 地址和网络接口更改时应用程序能够收到通知,甚至当设备电池过低时也能收到通知。

这里与用于监控网络可达性的语义相同。

例如,可以使用 State:/Network/Interface key 找到可用网络接口的列表:

1
2
3
4
SCDynamicStoreContext context = { 0, NULL, NULL, NULL, NULL };
SCDynamicStoreRef store = SCDynamicStoreCreate(NULL, NULL, nil, &context);
CFPropertyListRef propertyList = SCDynamicStoreCopyValue(store, CFSTR("State:/Network/Interface"));
NSArray *interfaces = (__bridge NSArray *)CFDictionaryGetValue(propertyList, CFSTR("Interfaces"));
  • lo0
  • gif0
  • stf0
  • en0
  • en1
  • en2
  • bridge0
  • p2p0
  • utun0

0x0302 Monitoring

动态存储的真正用处是能够监控更改,以便应用程序可以立即响应如 AirPort 被开启或本地 IP 地址发生变化等事情。

在某种程度上,这只是一个监控网络可达性的更广泛的规划(formulation)。创建一个动态存储,监听一个特定的网络服务实体 key,并且设置一个回调函数指针:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static void Callback(SCDynamicStoreRef store, CFArrayRef changedKeys, void *info) {
if (info) {
((void (^)())info)(changedKeys);
}
}

id callback = ^(NSArray *changedKeys) {
// ...
}

SCDynamicStoreContext context = {0, (__bridge void *)callback, NULL, NULL, NULL};

SCDynamicStoreRef store = SCDnamicSoreCreate(NULL, CFSTR("IPv4AddressMonitor"), Callback, &context);

NSString *ipv4 =
(NSString *)SCDynamicStoreKeyCreateNetworkServiceEntity(NULL,
kSCDynamicStoreDomainState,
kSCCompAnyRegex,
kSCEntNetIPv4);

SCDynamicStoreSetNotificationKeys(store, NULL, @[ipv4]);
SCDynamicStoreSetDispatchQueue(store, dispatch_get_main_queue());

语言是我们特有的意识的本质。这就是我们如何思考。这就是为什么我们完全可以思考。

我们依据名词来理解世界,用动词浏览它。形容词将将离散观察结果集中到感觉和判断中。我们通过前辈们的书面记录作为一个社会进步。最后,这将会是我们自己的文字,比我们更久。

我们能够交流完全是一个奇迹。这些想法可以心灵(mind)转移到另一个心灵,是超越信仰的 —— 但是很好理解,它几乎没有记录第二个想法。通过沟通,我们创造彼此之间的理解。我们唤起同情,并扩大我们的道德考虑边界给别人。我们的世界变得更大。

然而,当语言的距离太难以克服,沟通失败时,它变得难以同情。一种别样的感觉。无论是个人的还是国际的,沟通失败仍然是冲突的主要原因。

作为一项消除语言和文化距离的技术,有一个强有力的案例表明,Unicode 是为我们的物种创造的最重要的技术之一。