苹果示例源码阅读:SimplePing

前言

手机网络连接状态的检测对于 iOS App 开发来说是一个非常基础的需求,在前一篇文章 苹果示例源码阅读:Reachability 我们介绍了如何通过 SCNetworkReachability 提供的一系列 C 函数 API 进行网络连接状态变化的监听。但事实上,此方案能获取的只是设备的本地连接状态,有时它很难为我们检测真正的网络连接状态,如以下场景:

  • 现在很多的公共场所的 WiFi,需要网页登录授权,授权之前无法上网,但本地连接已经建立;
  • 存在了本地网络连接,但信号很差,实际无法连接到服务器;
  • iOS 连接的路由设备本身没有连接外网等。

Ping

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

幸运的是,苹果为我们提供了示例源码:SimplePing,示范了在 iOS 或者 Mac 上如何用 Objective-C / Swift 实现 ping 操作,因此我们也可以通过 ping 来检查手机网络的真实连接状态。事实上,Github 上著名的第三方开源库 RealReachability 也是这么做的。

SimplePing 源码阅读

对于 SimplePing 源码的阅读,我们将分为两部分来介绍。第一部分将结合 SimplePing.h 头文件里声明的方法,介绍如何使用 SimplePing 类封装的方法进行 ping 操作,第二部分(下一篇)将详细介绍 SimplePing.m 里各方法的具体实现细节。

类结构

通过 SimplePing.h 头文件中的声明,我们整理 SimplePing 的类结构如下图所示:

SimplePing 类结构

下面我们一一介绍 SimplePing 类的各个属性、方法以及 delegate 回调方法的含义及作用。

初始化方法

1
2
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithHostName:(NSString *)hostName NS_DESIGNATED_INITIALIZER;

SimplePing 中,禁用了 init 方法,只提供 initWithHostName: 一个方法,它可以初始化一个用于 ping 指定的主机实例对象。其中 hostName 参数可以是主机的 DNS 域名,或者是 IPv4、IPv6 地址的字符串形式。

属性

1
@property (nonatomic, copy, readonly) NSString * hostName;
  • hostName:只读,保存由初始化方法 initWithHostName: 传入的 ping 操作要连接的主机域名或 IP 地址。
1
@property (nonatomic, assign, readwrite) SimplePingAddressStyle addressStyle;
  • addressStyle:主机的 IP 地址类型,如 IPv4IPv6 等,其中 SimplePingAddressStyle 枚举类型的定义如下:
1
2
3
4
5
typedef NS_ENUM(NSInteger, SimplePingAddressStyle) {
SimplePingAddressStyleAny, // IPv4 或 IPv6
SimplePingAddressStyleICMPv4, // IPv4
SimplePingAddressStyleICMPv6 // IPv6
};
1
@property (nonatomic, copy, readonly, nullable) NSData * hostAddress;
  • hostAddress:只读,在 start 方法调用之后,根据 hostName 得到的要 ping 的主机的 IP 地址,它是 struct sockaddr 形式的 NSData 数据。当 SimplePing 实例处于 stopped 状态,或者实例调用了 start 方法,但在 simplePing:didStartWithAddress: 方法被调用之前,hostAddress 的值都是 nil
1
@property (nonatomic, assign, readonly) sa_family_t hostAddressFamily;
  • hostAddressFamily:只读,hostAddress 的地址族,如果 hostAddress 为 nil,则其值为:AF_UNSPEC
1
@property (nonatomic, assign, readonly) uint16_t identifier;
  • identifier:只读,当创建一个 SimplePing 实例对象时,会自动生成一个的随机的标识符,用来唯一标识当前 ping 对象。
1
@property (nonatomic, assign, readonly) uint16_t nextSequenceNumber;
  • nextSequenceNumber:只读,ping 每发送一次数据包都会有一个对应的序列号(sequence number),此值为下一次 ping 操作要发送数据时的序列号,从 0 开始递增,当 ping 成功发送一次数据到主机并收到应答时,该值 +1。而对于本次 ping 的 sequence number 在成功发送数据(request)和成功接收到响应(response)的 delegate 回调方法里都会以方法参数返回,以便进行 ping 操作耗时的计算等等。
1
@property (nonatomic, weak, readwrite, nullable) id<SimplePingDelegate> delegate;
  • delegate:当前对象的回调,delegate 中的回调方法将在对象调用 start 方法所在的线程对应的 run loop 中以默认的 run loop model 执行。

实例方法

1
- (void)start;
  • start 方法:开始一个 ping 操作,在调用此方法前,必须给 SimplePing 实例对象的 delegete 以及其他参数赋值。当 start 方法成功执行时,会回调 delegate 中的 simplePing:didStartWithAddress: 方法,在该回调方法里,就可以通过 sendPingWithData: 开始发送 ICMP 数据包,并等待接受主机应答的数据包。另外需要注意的是,当一个实例已经 started,又一次调用此 start 方法会出错。
1
- (void)sendPingWithData:(nullable NSData *)data;
  • sendPingWithData: 方法:向主机发送特定格式的 ICMP 数据包,调用此方法前必须保证实例已经 started 并且要等待 simplePing:didStartWithAddress: 回调执行才能开始发送数据。参数 data 为要向主机发送的 ICMP 数据包,可以为 nil,默认会发一个标准的 64 byte 数据包。
1
- (void)stop;
  • stop 方法:当结束要 ping 操作时,调用此方法。与 start方法不同的是,当一个实例已经 stopped,再次调用此方法也没事。

delegate 回调方法

  • start 方法执行结果的回调:
1
2
3
4
5
// start 方法成功执行,可在此开始发送数据,其中 address 为主机的 IP 地址;
- (void)simplePing:(SimplePing *)pinger didStartWithAddress:(NSData *)address;

// start 方法执行失败,返回错误信息;
- (void)simplePing:(SimplePing *)pinger didFailWithError:(NSError *)error;
  • sendPingWithData: 方法执行结果的回调,每发送一次数据,都会同步地回调以下两个方法其中一个(除非你在发送途中调用了 stop 方法):
1
2
3
4
5
// 成功发送 ICMP 数据包到指定主机,在此传回已发送的数据包以及本次 ping 对应的序列号;
- (void)simplePing:(SimplePing *)pinger didSendPacket:(NSData *)packet sequenceNumber:(uint16_t)sequenceNumber;

// 发送数据失败,并返回错误信息,绝大部分原因由于 hostName 解析失败。另,当此方法调用时,ping 实例状态会自动转为 `stopped`,不用再显示调用 `stop` 方法;
- (void)simplePing:(SimplePing *)pinger didFailToSendPacket:(NSData *)packet sequenceNumber:(uint16_t)sequenceNumber error:(NSError *)error;
  • 接收到主机返回应答数据的回调:
1
2
3
4
5
// 成功接收到主机回传的与之前发送相匹配的 ICMP 数据包;
- (void)simplePing:(SimplePing *)pinger didReceivePingResponsePacket:(NSData *)packet sequenceNumber:(uint16_t)sequenceNumber;

// 收到的未知的数据包。
- (void)simplePing:(SimplePing *)pinger didReceiveUnexpectedPacket:(NSData *)packet;

注:以上回调方法中的 packet 数据包只包含了 ICMP header 和 sendPingWithData: 中传入的数据,但不包含任何 IP 层的 header。

使用流程

根据苹果提供的 Demo,我们梳理了一下使用 SimplePing 类进行 ping 操作的流程如下图所示:

SimplePing 使用流程

根据上图,我们写了一个简单的使用示例,详见下面代码以及注释,不再赘述。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#import "ViewController.h"
#import "SimplePing.h"

@interface ViewController () <SimplePingDelegate>
@property (nonatomic, strong) SimplePing *pinger;
@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

// (1)
// 初始化一个 SimplePing 实例,
// 注意,这个 pinger 实例不能为临时变量,不然当前函数执行完毕后,pinger 实例就会被释放,那么它的 delegate 将不会执行。
self.pinger = [[SimplePing alloc] initWithHostName:@"www.apple.com"];

// (2)
// 指定 pinger 的 delegate
self.pinger.delegate = self;
// 指定要 ping 的 IP 地址的类型
self.pinger.addressStyle = SimplePingAddressStyleICMPv4;

// (3)
// 调用 start 方法开始 ping
[self.pinger start];
}

#pragma mark - SimplePingDelegate

// (4) start 方法成功执行,可开始发送数据
- (void)simplePing:(SimplePing *)pinger didStartWithAddress:(NSData *)address {
NSLog(@"address: %@", address);

// (5) 调用 sendPingWithData: 方法发送数据
[pinger sendPingWithData:nil]; // data 可传入 nil,此时 ping 发送的数据会有一个默认值。
}

// (4) start 方法执行失败,返回错误信息
- (void)simplePing:(SimplePing *)pinger didFailWithError:(NSError *)error {
NSLog(@"%@", error.localizedDescription);
}

#pragma mark -

// (6) 成功发送数据
- (void)simplePing:(SimplePing *)pinger didSendPacket:(NSData *)packet sequenceNumber:(uint16_t)sequenceNumber {
NSLog(@"didSendPacket: %@", packet);
NSLog(@"identifier: %d", pinger.identifier);
NSLog(@"sequenceNumber: %d", sequenceNumber);
NSLog(@"nextSequenceNumber: %d", pinger.nextSequenceNumber);
}

// (6) 发送数据失败
- (void)simplePing:(SimplePing *)pinger didFailToSendPacket:(NSData *)packet sequenceNumber:(uint16_t)sequenceNumber error:(NSError *)error {
NSLog(@"didFailToSendPacket: %@", error.localizedDescription);
}

#pragma mark -

// (7) 成功接收到之前 pinger 发送的数据
- (void)simplePing:(SimplePing *)pinger didReceivePingResponsePacket:(NSData *)packet sequenceNumber:(uint16_t)sequenceNumber {
NSLog(@"didReceivePingResponsePacket: %@", packet);
NSLog(@"identifier: %d", pinger.identifier);
NSLog(@"sequenceNumber: %d", sequenceNumber);
NSLog(@"nextSequenceNumber: %d", pinger.nextSequenceNumber);
}

// (7) 接收到到未知的数据
- (void)simplePing:(SimplePing *)pinger didReceiveUnexpectedPacket:(NSData *)packet {
NSLog(@"didReceiveUnexpectedPacket: %@", packet);
}

总结

本篇文章只介绍了如何使用苹果提供的示例源码 SimplePing 类初始化一个实例在 iOS 设备上进行 ping 操作,以进行判断网络真实连接状态,在下一篇文章《苹果实例源码阅读:SimplePing(2)》,我们将介绍 SimplePing 类的各个方法的具体内部实现。

Reference