iOS ping - SimplePing 源码解读

ping 的功能

ping 程序对于开发人员来说应该是不会陌生的, ping 通常用来探测主机到主机之间是否可以通信。如果可以 ping 通,意味着可以和该主机建立网络连接,就像这样的。

➜  ~ ping www.qq.com
PING www.qq.com (182.254.34.74): 56 data bytes
64 bytes from 182.254.34.74: icmp_seq=0 ttl=53 time=22.996 ms
64 bytes from 182.254.34.74: icmp_seq=1 ttl=53 time=36.688 ms
64 bytes from 182.254.34.74: icmp_seq=2 ttl=53 time=25.390 ms
64 bytes from 182.254.34.74: icmp_seq=3 ttl=53 time=25.516 ms

如果不能 ping 通,那就意味着无法和该主机建立网络连接,就像下面这样的。

➜  ~ ping www.google.com
PING www.google.com (66.220.147.47): 56 data bytes
Request timeout for icmp_seq 0
Request timeout for icmp_seq 1
Request timeout for icmp_seq 2
Request timeout for icmp_seq 3
Request timeout for icmp_seq 4

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

ICMP 协议

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

  1. 传输线路或者节点故障导致无法到达目的地主机
  2. 路由器封包重组失败
  3. 封包存活时间(Time To Live,TTL)变成 0 (防止封包在网络中永无止境得绕圈)
  4. IP 首部的错误检查码发现错误

iOS SimplePing 的使用

    // 1. 利用 HostName 创建 SimplePing
    SimplePing *pinger = [[SimplePing alloc] initWithHostName:@"www.apple.com"];
    self.pinger = pinger;
    // 2. 指定 IP 地址类型
    if (isIpv4 && !isIpv6) {
        pinger.addressStyle = SimplePingAddressStyleICMPv4;
    }else if (isIpv6 && !isIpv4) {
        pinger.addressStyle = SimplePingAddressStyleICMPv6;
    }
    // 3. 设置 delegate,用于接收回调信息
    pinger.delegate = self;
    // 4. 开始 ping
    [pinger start];

SimplePing 的使用还是非常简单的,

  1. 利用 HostName 创建 SimplePing
  2. 指定 IP 地址类型
  3. 设置 delegate,用于接收回调信息
  4. 开始 ping

delegate 的回调方法体现了 ping 的过程。

// 解析 HostName 拿到 ip 地址之后,发送封包
- (void)simplePing:(SimplePing *)pinger didStartWithAddress:(NSData *)address
{
    NSLog(@"pinging %@", displayAddressForAddress(address));
    [self sendPing];
}
// ping 功能启动失败
- (void)simplePing:(SimplePing *)pinger didFailWithError:(NSError *)error
{
    NSLog(@"failed: %@", shortErrorFromError(error));
    [self stop];
}
// ping 成功发送封包
- (void)simplePing:(SimplePing *)pinger didSendPacket:(NSData *)packet sequenceNumber:(uint16_t)sequenceNumber
{
    NSLog(@"#%u sent", sequenceNumber);
}
// ping 发送封包失败
- (void)simplePing:(SimplePing *)pinger didFailToSendPacket:(NSData *)packet sequenceNumber:(uint16_t)sequenceNumber error:(NSError *)error
{
    NSLog(@"#%u send failed: %@", sequenceNumber,shortErrorFromError(error));
}
// ping 发送封包之后收到响应
- (void)simplePing:(SimplePing *)pinger didReceivePingResponsePacket:(NSData *)packet sequenceNumber:(uint16_t)sequenceNumber
{
    NSLog(@"#%u received, size=%zu", sequenceNumber, packet.length);
}
// ping 接收响应封包发生异常
- (void)simplePing:(SimplePing *)pinger didReceiveUnexpectedPacket:(NSData *)packet
{
    NSLog(@"unexpected packet, size=%zu", packet.length);
}

SimplePing 的流程

流程图

上图是 SimplePing 执行一次 ping IPv4 地址的流程图,
ping 的实现并不负责,一共有以下几个步骤

  1. 解析传入的 HostName,获取第一个可用 IP 地址
  2. 创建传输/接收数据的 socket
  3. 发送数据,封装一个 ICMP 包
  4. 解析目标 IP 传回的 ICMP 包

HostName 的解析

关于 HostName 的解析,SimplePing 采用 CFHost 这个异步 API 方案,通过CFHost解析主机名主要有以下几个步骤:

  1. 通过调用 CFHostCreateWithName 创建一个 CFHostRef 对象。
  2. 调用 CFHostSetClient 并且提供一个上下文对象和回调函数,这个回调函数在解析结束的时候会被调用。
  3. 调用 CFHostScheduleWithRunLoop 用于在 RunLoop 中执行具体的解析操作。
  4. 调用 CFHostStartInfoResolution 来告诉解析器开始解析,把它的第二个参数设置为 kCFHostAddresses 表明你想要返回一个 IP 地址。
  5. 等待解析器调用你的回调函数,通过你的回调函数,调用 CFHostGetAddressing 函数来获取解析结果。这个函数返回 CFDataRef 对象的一个数组,其中的每一个都包含一个 POSIX 的 sockaddr 结构体。

下面的这段代码执行的是 1 - 4 过程

- (void)start {
    Boolean             success;
    CFHostClientContext context = {0, (__bridge void *)(self), NULL, NULL, NULL};
    CFStreamError       streamError;
    
    assert(self.host == NULL);
    assert(self.hostAddress == nil);

    self.host = (CFHostRef) CFAutorelease( CFHostCreateWithName(NULL, (__bridge CFStringRef) self.hostName) );
    assert(self.host != NULL);
    
    CFHostSetClient(self.host, HostResolveCallback, &context);
    
    CFHostScheduleWithRunLoop(self.host, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
    
    success = CFHostStartInfoResolution(self.host, kCFHostAddresses, &streamError);
    if ( ! success ) {
        [self didFailWithHostStreamError:streamError];
    }
}

在系统解析 HostName 成功之后会调用 HostResolveCallback 这个回调,这个回调的作用相当于重定向,将内容从 c 转成适当的 Objective-C 内容。

static void HostResolveCallback(CFHostRef theHost, CFHostInfoType typeInfo, const CFStreamError *error, void *info) {
    // This C routine is called by CFHost when the host resolution is complete. 
    // It just redirects the call to the appropriate Objective-C method.
    SimplePing *    obj;
    obj = (__bridge SimplePing *) info;
    assert([obj isKindOfClass:[SimplePing class]]);
    // 省略代码 ......
    if ( (error != NULL) && (error->domain != 0) ) {
        [obj didFailWithHostStreamError:*error];
    } else {
       // 在这个方法获取 HostName 对应的地址
        [obj hostResolutionDone];
    }
}

调用 CFHostGetAddressing 函数来获取解析结果,这个函数返回一个数组,从这个数组中取得 HostName 对应的 IP。从服务端的角度来说,为了实现负载均衡,一个域名是可以对应多个 IP 的,但是从客户端的角度来说,一个域名就是对应一个 IP。

- (void)hostResolutionDone {
    Boolean     resolved;
    NSArray *   addresses;
    
    // Find the first appropriate address.
    
    addresses = (__bridge NSArray *) CFHostGetAddressing(self.host, &resolved);
    if ( resolved && (addresses != nil) ) {
        resolved = false;
        for (NSData * address in addresses) {
            const struct sockaddr * addrPtr;
            
            addrPtr = (const struct sockaddr *) address.bytes;
            if ( address.length >= sizeof(struct sockaddr) ) {
                switch (addrPtr->sa_family) {
                    case AF_INET: {
                        if (self.addressStyle != SimplePingAddressStyleICMPv6) {
                            self.hostAddress = address;
                            resolved = true;
                        }
                    } break;
                    case AF_INET6: {
                        if (self.addressStyle != SimplePingAddressStyleICMPv4) {
                            self.hostAddress = address;
                            resolved = true;
                        }
                    } break;
                }
            }
            if (resolved) {
                break;
            }
        }
    }

    // We're done resolving, so shut that down.
    
    [self stopHostResolution];
    
    // If all is OK, start the send and receive infrastructure, otherwise stop.
    
    if (resolved) {
        [self startWithHostAddress];
    } else {
        [self didFailWithError:[NSError errorWithDomain:(NSString *)kCFErrorDomainCFNetwork code:kCFHostErrorHostNotFound userInfo:nil]];
    }
}

Socket 操作

HostName 解析成功拿到对应的 IP 之后,SimplePing 调用startWithHostAddress 创建 socket 。

  1. 使用 CFSocketCreateWithNative 创建一个 CFSocket
  2. 使用 CFSocketCreateRunLoopSource 为 CFSocket 创建一个 CFRunLoopSourceRef,
  3. 使用 CFRunLoopAddSource 将 CFRunLoopSourceRef 添加到 RunLoop 的 kCFRunLoopDefaultMode 模式中。
- (void)startWithHostAddress {
 // 省略代码 ......
        CFSocketContext         context = {0, (__bridge void *)(self), NULL, NULL, NULL};
        CFRunLoopSourceRef      rls;
        id<SimplePingDelegate>  strongDelegate;
        
        // Wrap it in a CFSocket and schedule it on the runloop.
        
        self.socket = (CFSocketRef) CFAutorelease( CFSocketCreateWithNative(NULL, fd, kCFSocketReadCallBack, SocketReadCallback, &context) );
        assert(self.socket != NULL);
        
        // The socket will now take care of cleaning up our file descriptor.
        
        assert( CFSocketGetSocketFlags(self.socket) & kCFSocketCloseOnInvalidate );
        fd = -1;
        
        rls = CFSocketCreateRunLoopSource(NULL, self.socket, 0);
        assert(rls != NULL);
        
        CFRunLoopAddSource(CFRunLoopGetCurrent(), rls, kCFRunLoopDefaultMode);
    
        CFRelease(rls);
 // 省略代码 ......
}

在 CFSocketCreateWithNative 的官方文档描述中有提到,CFSocketCreateWithNative 在创建 socket 的时候是有一个复用机制的。

The new CFSocket object, or `NULL` if an error occurred. 
If a CFSocket object already exists for `sock`, 
the function returns the pre-existing object instead of creating a new object; 

封装 ICMP 包

在 socket 创建完成之后,接下来就要开始组装 IP 封包并发送了。组装 IP 封包并发送的过程需要我们手动在这个回调方法触发。

- (void)simplePing:(SimplePing *)pinger didStartWithAddress:(NSData *)address
{
// 调用 - (void)sendPingWithData:(NSData *)data 
}

sendPingWithData 这个方法做的操作是组装 IP 封包然后发送封包,调用这个过程对应的回调方法。发送封包的过程是调用 sendto 方法

- (void)sendPingWithData:(NSData *)data {
    // 省略代码 ......

    // Send the packet.
    
    if (self.socket == NULL) {
        bytesSent = -1;
        err = EBADF;
    } else {
        bytesSent = sendto(
            CFSocketGetNative(self.socket),
            packet.bytes,
            packet.length, 
            0,
            self.hostAddress.bytes, 
            (socklen_t) self.hostAddress.length
        );
        err = 0;
        if (bytesSent < 0) {
            err = errno;
        }
    }
    // 省略代码 ......

组装 IP 封包是调用下面这个方法来完成,这个方法把数据按照 ICMPHeader 结构体的格式进行初始化并返回 IP 封包,关于 ICMPHeader 的结构这里就不再累赘,通过 ICMPHeader 结构体的定义就可以明白。

- (NSData *)pingPacketWithType:(uint8_t)type 
                   payload:(NSData *)payload 
                  requiresChecksum:(BOOL)requiresChecksum ;

解析 ICMP 包

完成了发送操作之后,接下来就是等待 ping 的响应了。当 socket 收到 ping 响应的时候回调 SocketReadCallback ,这个回调的作用相当于重定向,将内容从 c 转成适当的 Objective-C 内容,SocketReadCallback 里面调用了 readData 方法。

static void SocketReadCallback(CFSocketRef s, CFSocketCallBackType type, CFDataRef address, const void *data, void *info) 

在 readData 方法里面做的工作就是读取响应数据,验证响应的数据正确性,执行相应的回调方法。与sendto对应,读取数据使用的是 recvfrom 方法。验证响应数据调用的是下面的方法。

- (BOOL)validatePing4ResponsePacket:(NSMutableData *)packet sequenceNumber:(uint16_t *)sequenceNumberPtr 

这个方法接收 ping 响应数据的时候,会对 ICMP 包进行校验,会跳过 IP 头,毕竟 IP 首部对于 ping 功能来说并不重要,重要的是 ICMP 协议的内容,其中主要验证的字段是 checksum 和 sequenceNumber(iPv6 只需要验证 sequenceNumber)。
停止 ping 的时候需要做一些清理工作,包括 socket 和 CFHost 对应的销毁。
到这里,整个 ping 的基本流程就结束了。

总结

ICMP 协议规定,目的主机必须返回 ICMP 回送应答消息给源主机,如果源主机在一定时间内收到应答,则认为主机可达,而 ping 功能使用的是 ICMP 协议。
SimplePing 实现 ping 操作的原理步骤是这样的,先解析出 HostName 对应的 IP 地址,这个才知道数据包要发送给哪个目的主机,接着构造符合 ICMP 协议格式的数据包并发送,等待目的主机响应。一段时间过后,目的主机响应数据到达源主机,源主机接收响应数据包,验证数据包,然后去掉数据包的 IP 首部,拿到 ICMP 数据。由于个人水平有限,文章若有不对之处恳请指出,我稍作修改,大家共同进步。

参考

  1. https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/NetworkingTopics/Articles/ResolvingDNSHostnames.html#//apple_ref/doc/uid/TP40012543-SW1
  2. https://github.com/iOS-Developer-Documents-Chinese/iOS-Developer-Documents-Chinese/blob/master/Socket/DNS%E4%B8%BB%E6%9C%BA%E5%90%8D%E7%9A%84%E8%A7%A3%E6%9E%90.md
  3. https://www.cnblogs.com/cuihongyu3503319/archive/2012/07/09/2583129.html
  4. http://blog.163.com/qhj4433210@126/blog/static/165975282201592251248584/
  5. http://blog.csdn.net/inject2006/article/details/2139149
  6. https://zhaoxinyu.me/2017-04-12-simple-ping/
  7. https://en.wikipedia.org/wiki/IPv4_header_checksum
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容

  • 1.这篇文章不是本人原创的,只是个人为了对这部分知识做一个整理和系统的输出而编辑成的,在此郑重地向本文所引用文章的...
    SOMCENT阅读 12,977评论 6 174
  • 简介 用简单的话来定义tcpdump,就是:dump the traffic on a network,根据使用者...
    保川阅读 5,903评论 1 13
  • 个人认为,Goodboy1881先生的TCP /IP 协议详解学习博客系列博客是一部非常精彩的学习笔记,这虽然只是...
    贰零壹柒_fc10阅读 5,019评论 0 8
  • 1, 网络是什么 计算机网络的组成组件: 节点 (node):节点主要是具有网络地址 (IP) 的设备之称。 服务...
    求闲居士阅读 1,385评论 0 3
  • 一日一景 露从今夜白,月是故乡明。 玉良画才女,青丹展后人。 潘玉良《月是故乡明》艺术作品展在江苏美术馆陈列馆(国...
    吉光片羽_9bc2阅读 207评论 0 4