iOS Reachability

2,766 阅读6分钟

大多数App都严重依赖于网络,一款用户体验良好的的app是必须要考虑网络状态变化的。

为了更好的用户体验,我们会在无网络时展现本地或者缓存的内容,并对用户进行合适的提示。对于网络状态的检测,苹果提供了Reachability,由此也衍生出各种 Reachability 框架,比较著名的有Github上的 tonymillion/Reachability 以及 AFNetworking 中的 AFNetworkReachabilityManager 模块,它们的实现原理基本上都是对苹果公司的SCNetworkReachability API进行的封装。

1、SCNetworkReachability (SystemConfiguration.framework)

0.png

获取网络状态:

@property (nonatomic, strong) dispatch_source_t timer;
@property (nonatomic, assign) SCNetworkReachabilityRef reachability;
@property (nonatomic, strong) dispatch_queue_t serialQueue;
 
-(void)dealloc {
    if (_reachability != NULL) {
        CFRelease(_reachability);
        _reachability = NULL;
    }
}
 
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
     
    //创建零地址,0.0.0.0地址表示查询本机的网络连接状态
    struct sockaddr_in zeroAddress;
    bzero(&zeroAddress, sizeof(zeroAddress));
    zeroAddress.sin_len = sizeof(zeroAddress);
    zeroAddress.sin_family = AF_INET;
     
    _reachability = SCNetworkReachabilityCreateWithAddress(NULL, (struct sockaddr *)&zeroAddress);
    _serialQueue = dispatch_queue_create("com.xmy.serialQueue", DISPATCH_QUEUE_SERIAL);
     
    __weak __typeof(self) weakSelf = self;
    _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
    dispatch_source_set_timer(_timer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0.0 * NSEC_PER_SEC);
    dispatch_source_set_event_handler(_timer, ^{
        __strong __typeof(weakSelf) strongSelf = weakSelf;
        NSLog(@"连接状态: %d", [strongSelf isConnectionAvailable]);
    });
    dispatch_resume(_timer);
     
    [self startMonitor];
}
 
- (BOOL)isConnectionAvailable
{
    SCNetworkReachabilityFlags flags;
    //获取连接的标志
    BOOL didRetrieveFlags = SCNetworkReachabilityGetFlags(_reachability, &flags);
     
    //如果不能获取连接标志,则不能进行网络连接,直接返回
    if (!didRetrieveFlags) {
        NSLog(@"Error. Could not recover network reachability flags");
        return NO;
    }
     
    //根据连接标志进行判断
    BOOL isReachable = ((flags & kSCNetworkFlagsReachable) != 0);
    BOOL needConnection = ((flags & kSCNetworkFlagsConnectionRequired) != 0);
     
    return (isReachable && !needConnection) ? YES : NO;
}
 
- (void)startMonitor
{
    SCNetworkReachabilityContext context = {0, (__bridge void *)self, NULL, NULL, NULL};
    if (SCNetworkReachabilitySetCallback(_reachability, ReachabilityCallback, &context)) {
        // Schedules the given target with the given run loop and mode.
//        SCNetworkReachabilityScheduleWithRunLoop(_reachability, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
         
        // Schedule or unschedule callbacks for the given target on the given dispatch queue.
        SCNetworkReachabilitySetDispatchQueue(_reachability, _serialQueue);
    }
}
 
- (void)stopMonitor
{
    SCNetworkReachabilitySetCallback(_reachability, NULL, NULL);
 
    // Unschedules the given target from the given run loop and mode.
//    SCNetworkReachabilityUnscheduleFromRunLoop(_reachability, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
    SCNetworkReachabilitySetDispatchQueue(_reachability, NULL);
}
 
static void ReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void* info)
{
    NSLog(@"%@, %d, %@", target, flags, info);
}

优点:

  • 使用简单,只有一个类,官方还有Demo,容易上手
  • 灵敏度高,基本网络一有变化,基本马上就能判断出来

缺点:

  • 现在很流行的公用wifi,需要网页鉴权,鉴权之前无法上网,但本地连接已经建立
  • 存在本地网络连接,但信号很差,实际无法连接到服务器情况
  • 能否连接到指定服务器,比如国内访问墙外的服务器

苹果的Reachability有如下说明,告诉我们其能力受限于此:
The SCNetworkReachability programming interface allows an application to determine the status of a system's current network configuration and the reachability of a target host.
A remote host is considered reachable when a data packet, sent by an application into the network stack, can leave the local device. Reachability does not guarantee that the data packet will actually be received by the host.
当应用程序发送到网络堆栈的数据包可以离开本地设备时,就可以认为远程主机是可访问的,不能保证主机是否实际接收到数据包。

2、SimplePing

ping 是 Windows、Unix 、Linux和macOS 等系统下一个常用的命令,利用 ping 命令可以用来测试数据包能否通过IP 协议到达特定主机,并收到主机的应答,以检查网络是否连通和网络连接速度,帮助我们分析和判定网络故障。

SimplePing是苹果封装好的ping的功能,它利用resolve host,create socket(send&recv data),解析ICMP 包验证 checksum 等实现了 ping功能。并且支持iPv4 和 iPv6。

ping 功能使用是 ICMP 协议(Internet Control Message Protocol),ICMP 协议定义了一组错误信息,当路由器或者主机无法成功处理一个IP 封包的时候,能够将错误信息回送给来源主机:

1.png ICMP用途:差错通知、信息查询、重定向等

2.png [1]给送信者的错误通知;[2]送信者的信息查询。

[1]是到IP 数据包被对方的计算机处理的过程中,发生了什么错误时被使用。不仅传送发生了错误这个事实,也传送错误原因等消息。

[2]的信息询问是在送信方的计算机向对方计算机询问信息时被使用。被询问内容的种类非常丰富,他们有目标IP 地址的机器是否存在这种基本确认,调查自己网络的子网掩码,取得对方机器的时间信息等。

Ping实现:

3.png Ping超时原因:

  • 目标服务器不存在
  • 花在数据包交流上的时间太长ping命令认为超时
  • 目标服务器不回答ping命令

SimplePing实现:

4.png SimplePing初始化:

let hostName = "www.baidu.com"
var pinger: SimplePing?
var sendTimer: NSTimer?
 
/// Called by the table view selection delegate callback to start the ping.
func start(forceIPv4 forceIPv4: Bool, forceIPv6: Bool) {
    let pinger = SimplePing(hostName: self.hostName)
    self.pinger = pinger
 
    // By default we use the first IP address we get back from host resolution (.Any)
    // but these flags let the user override that.
    if (forceIPv4 && !forceIPv6) {
        pinger.addressStyle = .ICMPv4
    } else if (forceIPv6 && !forceIPv4) {
        pinger.addressStyle = .ICMPv6
    }
 
    pinger.delegate = self
    pinger.start()
}
 
/// Called by the table view selection delegate callback to stop the ping.
func stop() {
    self.pinger?.stop()
    self.pinger = nil
 
    self.sendTimer?.invalidate()
    self.sendTimer = nil
     
    self.pingerDidStop()
}
 
/// Sends a ping.
/// Called to send a ping, both directly (as soon as the SimplePing object starts up) and
/// via a timer (to continue sending pings periodically).
func sendPing() {
    self.pinger!.sendPingWithData(nil)
}

代理方法:

/// pinger.start()成功之后,解析HostName拿到ip地址后的回调
func simplePing(pinger: SimplePing, didStartWithAddress address: NSData) {
    NSLog("pinging %@", MainViewController.displayAddressForAddress(address))
     
    // Send the first ping straight away.
    self.sendPing()
 
    // And start a timer to send the subsequent pings.
    assert(self.sendTimer == nil)
    self.sendTimer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: #selector(MainViewController.sendPing), userInfo: nil, repeats: true)
}
 
/// pinger.start()功能启动失败的回调
func simplePing(pinger: SimplePing, didFailWithError error: NSError) {
    NSLog("failed: %@", MainViewController.shortErrorFromError(error))
     
    self.stop()
}
 
/// sendPingWithData发送数据成功
func simplePing(pinger: SimplePing, didSendPacket packet: NSData, sequenceNumber: UInt16) {
    NSLog("#%u sent", sequenceNumber)
}
 
/// sendPingWithData发送数据失败,并返回错误信息
func simplePing(pinger: SimplePing, didFailToSendPacket packet: NSData, sequenceNumber: UInt16, error: NSError) {
    NSLog("#%u send failed: %@", sequenceNumber, MainViewController.shortErrorFromError(error))
}
 
/// ping发送后收到响应
func simplePing(pinger: SimplePing, didReceivePingResponsePacket packet: NSData, sequenceNumber: UInt16) {
    NSLog("#%u received, size=%zu", sequenceNumber, packet.length)
}
 
/// ping接收响应封包发生异常
func simplePing(pinger: SimplePing, didReceiveUnexpectedPacket packet: NSData) {
    NSLog("unexpected packet, size=%zu", packet.length)
}

如代码所示,每隔一段时间就ping下host,看看是否畅通无阻,因此ping不可能做到及时判断网络变化,会有一定的延迟:
利用Reachability判断当前设备是否联网,利用SimplePing来检查服务器是否连通。

3、RealReachability (Star: 3k)

5.png

4、扩展:traceroute

由于ping命令不一定能判断对方是否存在,为了查看主机及目标主机之间的路由路径,我们使用traceroute 命令。它与ping 并列,也是ICMP 的典型实现之一。

traceroute是利用增加存活时间(TTL)值来实现功能的。每当一个icmp包经过一个路由器时,其存活时间值就会减1,当其存活时间为0时,路由器便会取消包发送,并发送一个ICMP TTL超时封包给原封包发出者。

6.png

7.png 命令行测试:

测试1:
> traceroute -I baidu.com
traceroute: Warning: baidu.com has multiple addresses; using 220.181.38.148
traceroute to baidu.com (220.181.38.148), 64 hops max, 72 byte packets
 1  172.25.62.254 (172.25.62.254)  2.198 ms  1.690 ms  1.437 ms
 2  172.25.100.17 (172.25.100.17)  2.175 ms  1.795 ms  1.769 ms
 3  * * *
 4  * * *
 5  * * *
 6  * * *
 7  * * *
 8  * * *
 9  * * *
10  * * *
11  * * *
12  * * *
13  * * *
14  * * *
15  * * *
16  220.181.38.148 (220.181.38.148)  29.700 ms  29.135 ms  29.127 ms

测试2:
> traceroute -I baidu.com
traceroute: Warning: baidu.com has multiple addresses; using 39.156.69.79
traceroute to baidu.com (39.156.69.79), 64 hops max, 72 byte packets
 1  172.25.62.254 (172.25.62.254)  3.339 ms  1.993 ms  4.845 ms
 2  172.25.100.17 (172.25.100.17)  2.146 ms  1.792 ms  1.971 ms
 3  * * *
 4  * * *
 5  * * *
 6  * * *
 7  * * *
 8  * * *
 9  * * *
10  * * *
11  * * *
12  * * *
13  * * *
14  * * *
15  * * *
16  * * *
17  * * *
18  39.156.69.79 (39.156.69.79)  29.015 ms  27.569 ms  28.232 ms

net-diagnosis (Star: 0.3k)
通过集成net-diagnosis,您可以轻松地在iOS上实现ping / traceroute /移动公共网络信息/端口扫描等网络诊断相关的功能。

8.png

9.png

参考:

  1. 完全理解icmp协议
  2. 在 iOS 平台实现Ping 和 traceroute
  3. Traceroute(路由追踪)的原理及实现