终端开发经常遇到需要排查 APP 网络状况的场景,例如可能由于某些地区网络运营商的问题,导致客户端某些功能不正常;或者用户自己网络的问题导致无法正常使用我们的后台服务。对此本文提供一套全面的客户端网络诊断方案。

背景

之前的做法非常麻烦:

  1. 用户反馈某一功能异常;
  2. 客服联系该用户;
  3. 客服指导该用户手机上下载网络诊断软件;
  4. 按情况 ping 某域名、traceroute 某域名;
  5. 让用户截图把信息发回来,转交给开发人员分析。

现在在 App 内集成网络诊断功能,用户只需要点击一次,应用内分析出网络情况生成日志文本,之后可以选择微信分享或者直接上传到开发者后台。如视频所示:

1. 检查本地网络环境信息

检查当前是否联网、联网类型、SIM卡网络运营商等基础信息有两种方法:

通过 statusBar 的网络 subview

这种方法的前提是必须保证状态栏不隐藏。

//网络类型
typedef enum {
    NETWORK_TYPE_NONE = 0,
    NETWORK_TYPE_2G = 1,
    NETWORK_TYPE_3G = 2,
    NETWORK_TYPE_4G = 3,
    NETWORK_TYPE_5G = 4,  
    NETWORK_TYPE_WIFI = 5,
} NETWORK_TYPE;

+ (NETWORK_TYPE)getNetworkTypeFromStatusBar
{
    UIApplication *app = [UIApplication sharedApplication];
    NETWORK_TYPE nettype = NETWORK_TYPE_NONE;
    //iOS11
    if ([[app valueForKeyPath:@"_statusBar"] isKindOfClass:NSClassFromString(@"UIStatusBar_Modern")]) {
        NSArray *views = [[[[app valueForKeyPath:@"statusBar"] valueForKeyPath:@"statusBar"] valueForKeyPath:@"foregroundView"] subviews];
        for (UIView *view in views) {
            for (id child in view.subviews) {
                //wifi
                if ([child isKindOfClass:NSClassFromString(@"_UIStatusBarWifiSignalView")]) {
                    nettype = NETWORK_TYPE_WIFI;
                }
                //2G 3G 4G
                if ([child isKindOfClass:NSClassFromString(@"_UIStatusBarStringView")]) {
                    if ([[child valueForKey:@"_originalText"] containsString:@"2G"]) {
                        nettype = NETWORK_TYPE_2G;
                    } else if ([[child valueForKey:@"_originalText"] containsString:@"3G"]) {
                        nettype = NETWORK_TYPE_3G;
                    } else if ([[child valueForKey:@"_originalText"] containsString:@"4G"]) {
                        nettype = NETWORK_TYPE_4G;
                    }
                }
            }
        }
    } else {
        NSArray *subviews = [[[[UIApplication sharedApplication] valueForKey:@"statusBar"]
                              valueForKey:@"foregroundView"] subviews];
        NSNumber *dataNetworkItemView = nil;
        for (id subview in subviews) {
            if ([subview isKindOfClass:[NSClassFromString(@"UIStatusBarDataNetworkItemView") class]]) {
                dataNetworkItemView = subview;
                break;
            }
        }
        NSNumber *num = [dataNetworkItemView valueForKey:@"dataNetworkType"];
        nettype = [num intValue];
    }
   return nettype;
}

通过 CTTelephonyNetworkInfo

这个类是 iOS7 以后才出现的,需要导入CoreTelephony.framework, 用来获取手机的运营商信息和简单的通话信息。

let ctNetworkInfo = CTTelephonyNetworkInfo()
    /// 当前蜂窝网络类型
    public func cellularType() -> CellularType {
        switch  ctNetworkInfo.currentRadioAccessTechnology {
        case CTRadioAccessTechnologyEdge, CTRadioAccessTechnologyGPRS, CTRadioAccessTechnologyCDMA1x:
            return ._2G
        case CTRadioAccessTechnologyHSDPA, CTRadioAccessTechnologyWCDMA, CTRadioAccessTechnologyHSUPA, CTRadioAccessTechnologyCDMAEVDORev0, CTRadioAccessTechnologyCDMAEVDORevA, CTRadioAccessTechnologyCDMAEVDORevB, CTRadioAccessTechnologyeHRPD:
            return ._3G
        case CTRadioAccessTechnologyLTE:
            return ._4G
        default:
            return .unknow
        }
    }

    /// 移动运营商类型
    public func mobileCarrierType() -> CarrierType {
        let carrier = ctNetworkInfo.subscriberCellularProvider
        guard let MCC = carrier?.mobileCountryCode, let MNC = carrier?.mobileNetworkCode else {
            return .unknow
        }

        switch Int(MCC) {
        case 460:
            do { //CN
                switch Int(MNC) {
                case 0, 2, 7, 8:    return .chinaMobile
                case 1, 6, 9:       return .chinaUnicom
                case 3, 5, 11:      return .chinaTelecom
                case 20:            return .chinaTietong
                default:            return .unknow
                }
            }
        case 454:
            do {//HK
                switch Int(MNC) {
                case 12, 13:    return .chinaMobileHK
                case 7:         return .chinaUnicomHK
                default:        return .otherHK
                }
            }
        default: return .unknow
        }
    }

    /// 移动运营商名称
    public func mobileCarrierName() -> String {
        if let carrier = ctNetworkInfo.subscriberCellularProvider {
            if let name = carrier.carrierName {
                return name
            }
        }

        return "unknow"
    }

获取系统 WiFi 的相关信息则可以通过系统 sdk 的CaptiveNetwork类:

// MARK: - WIFI
extension NetworkDetector {

    /// 获取WIFI信息
    private func supportedInterfacesNetworkInfo() -> [String: Any]? {
        guard let interfaceList = CNCopySupportedInterfaces() as? [String] else {
            return nil
        }

        for interface in interfaceList {
            if let networkInfo = CNCopyCurrentNetworkInfo(interface as CFString) as? [String: Any],
                let _ = networkInfo[kCNNetworkInfoKeySSID as String] {
                return networkInfo
            }
        }

        return nil
    }

    /// WIFI名称
    public func SSID(_ networkInfo: [String: Any]? = nil) -> String? {
        if let info = (networkInfo ?? supportedInterfacesNetworkInfo()) {
            return info[kCNNetworkInfoKeySSID as String] as? String
        }

        return nil
    }

    /// WIFI Mac地址
    public func BSSID(_ networkInfo: [String: Any]? = nil) -> String? {
        if let info = (networkInfo ?? supportedInterfacesNetworkInfo()) {
            return info[kCNNetworkInfoKeyBSSID as String] as? String
        }

        return nil
    }
}

获取 iOS 设备的ip地址,这方面网上文章很多了,参考这里

2. 检查后台可用性

这一步主要通过 TCP 连接测试目标地址的可用性与连通性。首先给出域名转ip的方法(支持 ipv4 和 ipv6)。

/*!
 * 通过hostname获取ip列表 DNS解析地址
 */
+ (NSArray *)getDNSsWithDormain:(NSString *)hostName{
    NSMutableArray *result = [[NSMutableArray alloc] init];
    NSArray *IPV4DNSs = [self getIPV4DNSWithHostName:hostName];
    if (IPV4DNSs && IPV4DNSs.count > 0) {
        [result addObjectsFromArray:IPV4DNSs];
    }

    //由于在IPV6环境下不能用IPV4的地址进行连接监测
    //所以只返回IPV6的服务器DNS地址
    NSArray *IPV6DNSs = [self getIPV6DNSWithHostName:hostName];
    if (IPV6DNSs && IPV6DNSs.count > 0) {
        [result removeAllObjects];
        [result addObjectsFromArray:IPV6DNSs];
    }

    return [NSArray arrayWithArray:result];
}


+ (NSArray *)getIPV4DNSWithHostName:(NSString *)hostName
{
    const char *hostN = [hostName UTF8String];
    struct hostent *phot;

    @try {
        phot = gethostbyname(hostN);
    } @catch (NSException *exception) {
        return nil;
    }

    NSMutableArray *result = [[NSMutableArray alloc] init];
    int j = 0;
    while (phot && phot->h_addr_list && phot->h_addr_list[j]) {
        struct in_addr ip_addr;
        memcpy(&ip_addr, phot->h_addr_list[j], 4);
        char ip[20] = {0};
        inet_ntop(AF_INET, &ip_addr, ip, sizeof(ip));

        NSString *strIPAddress = [NSString stringWithUTF8String:ip];
        [result addObject:strIPAddress];
        j++;
    }

    return [NSArray arrayWithArray:result];
}


+ (NSArray *)getIPV6DNSWithHostName:(NSString *)hostName
{
    const char *hostN = [hostName UTF8String];
    struct hostent *phot;

    @try {
        /**
         * 只有在IPV6的网络下才会有返回值
         */
        phot = gethostbyname2(hostN, AF_INET6);
    } @catch (NSException *exception) {
        return nil;
    }

    NSMutableArray *result = [[NSMutableArray alloc] init];
    int j = 0;
    while (phot && phot->h_addr_list && phot->h_addr_list[j]) {
        struct in6_addr ip6_addr;
        memcpy(&ip6_addr, phot->h_addr_list[j], sizeof(struct in6_addr));
        NSString *strIPAddress = [self formatIPV6Address: ip6_addr];
        [result addObject:strIPAddress];
        j++;
    }

    return [NSArray arrayWithArray:result];
}

iOS 实现 socket 编程可以使用CFNetwork, 是基于CFSocket等接口的上层封装,ASIHttpRequest就工作在这一层。不过CFNetwork用起来依旧比较复杂。用的较多的是谷歌封装的一个IM框架CocoaAsyncSocket, 它给 Mac 和 iOS 提供了易于使用的、强大的异步套接字库,向上封装出简单易用的OC接口。省去了我们面向 Socket 以及数据流 Stream 等繁琐复杂的编程,而且支持TCP或者UDP协议,支持IPv4和IPv6。
这里提供一个封装好的 Engine 类,调用方实现 protocol 方法即可。另外支持发送心跳包,用于IM、自建推送等长连接场景。

3. 测试长连接

长连接的实现方式,一种是上面的基于 tcp 的长连接,客户端通常用这种。web的场景则有 http keep-alive 和 websocket 两种。

CocoaAsyncSocket

长连接需要保持心跳,因为国内移动无线网络运营商在链路上一段时间内没有数据通讯后, 会淘汰NAT表中的对应项, 造成链路中断。而国内的运营商一般NAT超时的时间为5分钟,所以通常我们心跳设置的时间间隔为3-5分钟。
每次发送心跳包之前记录时间,在回调didRead data:中计算本次心跳包发起到收到的耗时。

WebSocket

keep-alivewebSocket这两种长连接有什么区别呢?
在 HTTP1.0 中,一次 TCP 连接只能完成1个 HTTP 请求,一个Request 一个Response, 这次HTTP请求就结束了。HTTP1.1中进行了改进,keep-alive connection 允许在一次 TCP 连接中完成多个 HTTP 请求,但因为局限较多,这种仍被称作是伪.长连接,原因如下:

  1. 遵循 Request = Response, 在 HTTP 中永远是一一对应的;
  2. 对每个请求仍然要单独发 Header, 信息交换效率很低;
  3. Response 永远是被动的,不能主动发起。

传统HTTP客户端与服务器请求响应模式如下图所示:

WebSocket 可以看成是 HTTP 协议为了支持长连接所打的一个大补丁,它借用了HTTP的协议来完成一部分握手,但握手成功后就完全按照Websocket协议进行了,相比 keep-alive 建立的是真.长连接

  1. 通过第一个 HTTP request 建立 TCP 连接之后,之后的交换数据都不需要再发 HTTP request 了,提高信息交换效率;
  2. 被动性。服务端可以主动推送信息给客户端。

WebSocket模式客户端与服务器请求响应模式如下图:

iOS 平台常用的一个 WebSocket 框架Starscream, 用法与 CocoaAsyncSocket 类似,建立连接后定时向后台写入一段字符串来做心跳检验。

4. Ping 域名测试

ping 程序是对两台主机之间连通性进行测试的基本工具,基于 ICMP 协议,ICMP 协议定义了一组错误信息,当路由器或者主机无法成功处理一个 IP 封包的时候,能够将错误信息回送给来源主机。
苹果官方有封装 ping 功能的库SimplePing, 支持 iPv4 和 iPv6。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];

需要注意的是,delegate 中的一系列回调方法将在对象调用 start 方法所在的线程对应的 runloop 中以 NSDefaultRunLoopMode 执行,因此为保证接收到回调我们需要加以下 do…while 代码使 ping 程序在当前线程一直执行。

_sendCount = 1;
do {
    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
} while (self.pinger != nil || _sendCount <= MAXCOUNT_PING);

得到最后一个 ping 结果后将 pinger 置为 nil, 或 _sendCount 达到最大值,从而结束 while 循环。

self.pinger = nil;
_sendCount = MAXCOUNT_PING + 1;

提供一个封装好的NetPing类,通过模拟 shell 命令 ping 的过程,监控目标主机是否连通,并通过解析 ICMP 包,输出每次 ping 的域名ip、数据大小、icmp_seq、time 等信息,从而完整展示 ping 的结果。每个域名连续执行五次,因为每次的速度不一致,可以观察其平均速度来判断网络情况。

Ping: www.qq.com 
64 bytes from 182.254.34.74: icmp_seq=0 type=ICMPv4TypeEchoReply time=22.996 ms
64 bytes from 182.254.34.74: icmp_seq=1 type=ICMPv4TypeEchoReply time=36.688 ms
64 bytes from 182.254.34.74: icmp_seq=2 type=ICMPv4TypeEchoReply time=25.390 ms
64 bytes from 182.254.34.74: icmp_seq=3 type=ICMPv4TypeEchoReply time=25.516 ms
64 bytes from 182.254.34.74: icmp_seq=4 type=ICMPv4TypeEchoReply time=28.377 ms

在发送 ping 数据时为什么初始化一个NSTimer?, 因为如果 ping 失败,也就是发送的测试报文成功,但一直没收到响应的报文,此时却不会有任何的回调方法告知我们,因此加一个 timer 进行延时判断。如果3s内无 response 则输出 timeout, 并主动重试(当 ping 次数未超限)。 如果是在 delegate 回调中触发了 sendPing(ping命令发生错误、发送ping数据失败、成功接收到 PingResponse 数据、接收到错误的 PingResponse 数据),则会及时将 timer 关掉。

/*
 * 发送Ping数据,pinger会组装一个ICMP控制报文的数据发送过去
 *
 */
- (void)sendPing
{
    if (timer) {
        [timer invalidate];
    }
    if (_sendCount > MAXCOUNT_PING) {
        _sendCount++;
        self.pinger = nil;
        if (self.delegate && [self.delegate respondsToSelector:@selector(netPingDidEnd)]) {
            [self.delegate netPingDidEnd];
        }
    }

    else {
        assert(self.pinger != nil);
        _sendCount++;
        _startTime = [NetTimer getMicroSeconds];
        if (_isLargePing) {
            NSString *testStr = @"";
            for (int i=0; i<408; i++) {
                testStr = [testStr stringByAppendingString:@"abcdefghi "];
            }
            testStr = [testStr stringByAppendingString:@"abcdefgh"];
            NSData *data = [testStr dataUsingEncoding:NSASCIIStringEncoding];
            [self.pinger sendPingWithData:data];
        } else {
            [self.pinger sendPingWithData:nil];
        }
        timer = [NSTimer scheduledTimerWithTimeInterval:3.0
                                                 target:self
                                               selector:@selector(pingTimeout:)
                                               userInfo:[NSNumber numberWithInt:_sendCount]
                                                repeats:NO];
    }
}

- (void)pingTimeout:(NSTimer *)index
{
    if ([[index userInfo] intValue] == _sendCount && _sendCount <= MAXCOUNT_PING + 1 &&
        _sendCount > 1) {
        NSString *timeoutLog =
            [NSString stringWithFormat:@"ping: cannot resolve %@: TimeOut", _hostAddress];
        if (self.delegate && [self.delegate respondsToSelector:@selector(appendPingLog:)]) {
            [self.delegate appendPingLog:timeoutLog];
        }
        [self sendPing];
    }
}

5. Traceroute 测试

与 Ping 一样,Traceroute 也是基于 ICMP 协议的常规网络分析工具。数据从主机发送到目标服务器要经过层层路由转发,Traceroute 用来侦测到目的主机之间所经路由。带 -R 参数的 ping 命令也可以记录路由过程,但是因为 IP 数据报头的长度限制(最多能保存9个IP地址),ping 不能完全的记录下所经过的路由器,traceroute 正好就填补了这个缺憾。工作原理很简单:

  1. 发送一份 TTL == 1 的 IP 数据报给目的主机,经过第一个路由器时,TTL 值被减为 0,则第一个路由器丢弃该数据报,并返回一份 ICMP超时报文,于此得到了路径中第一个路由器的地址;
  2. 再发送一份 TTL 值为 2 的数据报,便可得到第二个路由器的地址;
  3. 以此类推,一直到到达目的主机为止,这样便记录下了路径上所有的路由 IP。

基于 UDP 实现

我们如何知道数据报何时到达目的主机呢?在基于UDP的方案中,traceroute 使用了一个大于30000的端口号,服务器在收到这个数据包的时候会返回一个端口不可达的ICMP错误信息,客户端通过判断收到的错误信息是TTL超时还是端口不可达来判断数据包是否到达目标主机。Linux 和 MacOS 平台上的 traceroute 都是基于该原理实现的。然而使用过程中会发现这种方法并不太靠谱。TTL增加到一定大小后就拿不到返回的数据包了,也无法知道什么时候到达目的主机。

原因可以看下这篇文章的分析:

使用 UDP 的 traceroute,失败还是比较常见的。这常常是由于,在运营商的路由器上,UDP与ICMP的待遇大不相同。为了利于troubleshooting,ICMP ECHO Request/Reply 是不会封的,而UDP则不同。UDP常被用来做网络攻击,因为UDP无需连接,因而没有任何状态约束它,比较方便攻击者伪造源IP、伪造目的端口发送任意多的UDP包,长度自定义。所以运营商为安全考虑,对于UDP端口常常采用白名单ACL,就是只有ACL允许的端口才可以通过,没有明确允许的则统统丢弃。比如允许DNS/DHCP/SNMP等。

目前网上许多开源的iOS traceroute库基本都是UDP的方案,使用起来并不靠谱,为此实现一套基于 ICMP 的方案。

基于 ICMP 实现

整体流程如下:

// 1. 创建套接字
int send_sock = socket(remoteAddr->sa_family, SOCK_DGRAM, isIPv6 ? IPPROTO_ICMPV6 : IPPROTO_ICMP);

// 2. 最多尝试30跳
int ttl = 1;
bool finished = false; // 是否抵达目标主机

while(ttl <= 30 && !finished) {
    // 3. 设置TTL,下一跳TTL递增
    setsockopt(sock, IPPROTO_IP, IP_TTL, &ttl, sizeof(ttl));
    ++ttl;

    // 4. 构建icmp报文
    packetData = makeICMPPacket(...);

    // 5. 连续发送3个ICMP报文,记录往返时长
    for (0...3) {

        // 6. 发送icmp报文
        sendto(...);

        // 7. 接收icmp数据
        ssize_t resultLen = recvfrom(...);

        if (resultLen >= 0) {
            // 8. 解析数据包
            ICMPPacket *packet = unpack(...);

            if (icmp_timeout) {
                // 到达中间节点
            } else if (icmp_EchoReply && ip == 目标主机ip) {
                // 到达目标服务器,traceroute 结束
            } else {
                // 失败
            }
        }
    }
}

从伪代码看出,关键在于如何创建和发送ICMP数据,以及如何从接收到的ip数据包中解析ICMP。

ICMP 数据创建


图为ICMP数据结构,据此定义 ICMPPacket 数据结构:

typedef struct ICMPPacket {
    uint8_t     type; // 类型
    uint8_t     code; // 类型代码
    uint16_t    checksum; // 校验码
    uint16_t    identifier; // ID
    uint16_t    sequenceNumber; // 序列号
    // data...
} ICMPPacket;

ICMPv4TypeEchoRequest 类型=8,代码=0,创建好 ICMP 报文直接发送就可以了,系统会自动加上IP头部。

ICMP 数据解析

typedef struct IPv4Header {
    uint8_t versionAndHeaderLength; // 版本和首部长度
    uint8_t serviceType;
    uint16_t totalLength; 
    uint16_t identifier;
    uint16_t flagsAndFragmentOffset;
    uint8_t timeToLive;
    uint8_t protocol; // 协议类型,1表示ICMP
    uint16_t checksum;
    uint8_t sourceAddress[4];
    uint8_t destAddress[4];
    // options...
    // data...
} IPv4Header;

根据ip数据包结构的关键字段,我们可以获取到 ICMP 数据包,再根据上面的类型字段判断是CMPv4TypeTimeOut(11) 还是
ICMPv4TypeEchoReply(0),从而得知当前所处路由位置。

对比

www.baidu.com 为例,比较两种方案的 traceroute 结果,


UDP 方案从ttl==13之后都不再收到服务器返回,也无法得知什么时候到达baidu, ICMP 则完整地输出路由表。
两种方案都会出现某一跳显示星号的情况,代表那一跳路由器隐藏了自己位置。并不是所有网关都会如实返回ICMP超时报文。出于安全性考虑,大多数防火墙以及启用了防火墙功能的路由器缺省配置为不返回各种ICMP报文,因此traceroute程序不一定能拿到所有的沿途网关地址。
总结一下:

  1. UDP模式:UDP探测数据包(目标端口大于30000) -> 中间网关发回 ICMP TTL Timout -> 目标主机发回 ICMP Destination Unreachable
  2. ICMP模式:ICMPEchoRequest 探测数据包 -> 中间网关发回 ICMP TTL Timout -> 目标主机发回 ICMPEchoReply

6. 文件下载测试

这里提供一个基于 URLSession 实现的简单的下载器,在下载成功的回调中计算文件下载速度,网上相关教程很多,不赘述了。

总结

至此,开发者诊断 iOS APP 网络状况所需要的功能基本都包括了,该工具完整诊断结果如下:

Step1:-------------> 检查当前网络 <---------------
当前是否联网:已联网
当前联网类型:wifi
SSID:Tencent-StaffWiFi
当前本机IP: 10.70.75.98
本地网关: fe80:c::
本地DNS: 10.11.10.12, 10.6.210.85, 10.14.12.239

Step2:-------------> 检查后台可用性 <---------------
Server: nggws.starrobot.qq.com,port:443
isConnected: true, isAvailable: true
Server: nggws.starrobot.qq.com,port:80
isConnected: true, isAvailable: true
域名转IP: 14.17.41.219

Server: nggws.robot.qq.com,port:443
isConnected: true, isAvailable: true
Server: nggws.robot.qq.com,port:80
isConnected: true, isAvailable: true
域名转IP: 183.61.51.41

Server: qdtts.qq.com,port:443
isConnected: true, isAvailable: true
Server: qdtts.qq.com,port:80
isConnected: true, isAvailable: true
域名转IP: 58.60.9.100

Server: yingyongbao.soundai.cn,port:443
isConnected: true, isAvailable: true
域名转IP: 182.61.53.149

Step3:-------------> TestWebSocket <---------------
wss://nggws.starrobot.qq.com:443, 连接成功
心跳包[1]发起到收到耗时:10ms
心跳包[2]发起到收到耗时:25ms
心跳包[3]发起到收到耗时:12ms
ws://nggws.starrobot.qq.com:80, 连接成功
心跳包[1]发起到收到耗时:17ms
心跳包[2]发起到收到耗时:16ms
心跳包[3]发起到收到耗时:11ms
wss://nggws.robot.qq.com:443, 连接成功
心跳包[1]发起到收到耗时:14ms
心跳包[2]发起到收到耗时:13ms
心跳包[3]发起到收到耗时:15ms
ws://nggws.robot.qq.com:80, 连接成功
心跳包[1]发起到收到耗时:11ms
心跳包[2]发起到收到耗时:13ms
心跳包[3]发起到收到耗时:18ms

Step4:---------------> PING域名测试 <---------------
PING: [    nggws.starrobot.qq.com    ]
64 bytes from 14.17.41.219 icmp_seq=#0 time=14ms
64 bytes from 14.17.41.219 icmp_seq=#1 time=5ms
64 bytes from 14.17.41.219 icmp_seq=#2 time=14ms
64 bytes from 14.17.41.219 icmp_seq=#3 time=4ms
64 bytes from 14.17.41.219 icmp_seq=#4 time=8ms

PING: [    nggws.robot.qq.com    ]
64 bytes from 183.61.51.41 icmp_seq=#0 time=11ms
64 bytes from 183.61.51.41 icmp_seq=#1 time=7ms
64 bytes from 183.61.51.41 icmp_seq=#2 time=10ms
64 bytes from 183.61.51.41 icmp_seq=#3 time=12ms
64 bytes from 183.61.51.41 icmp_seq=#4 time=11ms

PING: [    qdtts.qq.com    ]
64 bytes from 58.60.9.100 icmp_seq=#0 time=7ms
64 bytes from 58.60.9.100 icmp_seq=#1 time=8ms
64 bytes from 58.60.9.100 icmp_seq=#2 time=5ms
64 bytes from 58.60.9.100 icmp_seq=#3 time=9ms
64 bytes from 58.60.9.100 icmp_seq=#4 time=6ms

PING: [    yingyongbao.soundai.cn    ]
64 bytes from 182.61.53.149 icmp_seq=#0 time=13ms
64 bytes from 182.61.53.149 icmp_seq=#1 time=18ms
64 bytes from 182.61.53.149 icmp_seq=#2 time=17ms
64 bytes from 182.61.53.149 icmp_seq=#3 time=16ms
64 bytes from 182.61.53.149 icmp_seq=#4 time=18ms

PING: [    www.baidu.com    ]
64 bytes from 14.215.177.39 icmp_seq=#0 time=11ms
64 bytes from 14.215.177.39 icmp_seq=#1 time=9ms
64 bytes from 14.215.177.39 icmp_seq=#2 time=11ms
64 bytes from 14.215.177.39 icmp_seq=#3 time=16ms
64 bytes from 14.215.177.39 icmp_seq=#4 time=10ms

Step5:---------------> traceroute 测试 <---------------
开始traceroute...
1   10.70.75.2        5.86ms    3.17ms    8.69ms  
2   10.14.61.41       3.94ms    7.24ms    5.30ms  
3   10.14.60.30       9.65ms    2.92ms    9.28ms  
4   10.14.61.60       3.01ms    3.72ms    6.01ms  
5   14.17.22.1        6.59ms    9.81ms    4.17ms  
6   10.200.102.1      4.57ms    4.17ms    6.41ms  
7   ***********       ----ms    ----ms    ----ms  
8   14.17.0.97        14.05ms   8.24ms    5.05ms  
9   119.147.220.157   9.93ms    5.11ms    4.47ms  
10  113.96.4.118      18.49ms   14.68ms   9.45ms  
11  219.135.96.86     58.19ms   -----ms   22.62ms 
12  14.29.117.238     12.20ms   8.35ms    -----ms 
13  ***********       -----ms   -----ms   -----ms 
14  14.215.177.38     10.80ms   7.38ms    8.88ms  

Step6.1:--------> 文件下载测试[http] <--------
下载成功:19KB, 速度:54.11KB/S, 总耗时:356ms
Step6.2:--------> 文件下载测试[https] <--------
下载成功:19KB, 速度:47.33KB/S, 总耗时:407ms

Step7:----> 网络诊断结束,点[复制]按钮发送给客服,谢谢 <----