移动端网络监控实践

本文涉及的产品
.cn 域名,1个 12个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 在移动端应用开发场景下,不可避免的要与网络打交道。有时在网络请求失败时,我们想知道网络的质量;有时需要明确的告知用户当前网络质量(比如游戏场景实时显示延迟)。网络监控离不开最经典的TCP/IP模型,基于模型分层统计网络耗时有助于我们更清晰的了解当前网络质量。

image.png


1. 背景介绍


在移动端应用开发场景下,不可避免的要与网络打交道。有时在网络请求失败时,我们想知道网络的质量;有时需要明确的告知用户当前网络质量(比如游戏场景实时显示延迟)。网络监控离不开最经典的TCP/IP模型,基于模型分层统计网络耗时有助于我们更清晰的了解当前网络质量。


image.png


TCP/IP参考模型中物理层难以在网络层面统计,网络层有Ping工具,传输层有系统提供的Socket接口,应用层最常用的有HTTP、RTMP协议。本文我们介绍ping工具、DNS解析耗时、TCP连接耗时、HTTP建立耗时。


2. ping


ping是基于网络层ICMP协议的,发送的是ICMP回显请求报文。下面我们先了解下ICMP。


2.1 ICMP(Internet控制报文协议)简介


ICMP是IP层的一个组成部分,主要传递差错报文以及其他需要注意的信息。ICMP报文通常被IP层或更高层协议(TCP或UDP)使用。ICMP的正式规范参见RFC 792[Posterl 1981b],ICMP封装在IP数据包内部,格式是20字节的IP首部+ICMP报文。ICMP报文格式如下:


image.png


所有报文的前4个字节都是一样的,但是剩下的其他字节则互不相同。类型字段可以有15个不同的值,以描述特定类型的ICMP报文,某些CIMP报文还是用代码字段的值来进一步描述不同的条件。校验和字段覆盖整个ICMP报文。


不同类型由报文中的类型字段和代码字段来共同决定。报文可以分为查询报文和差错报文,ICMP差错报文有时需要做特殊处理(如在对ICMP差错报文进行响应时,永远不会生成另一份ICMP差错报文)。


对于我们今天用到的ping程序,使用了:


  • 类型为0,代码为0的回显应答的查询报文
  • 类型为8,代码为0的请求回显的查询报文


2.2 Ping程序协议简介


ping的工作原理很简单,一台网络设备发送请求等待另一网络设备的回复,并记录下发送时间。接收到回复之后,就可以计算报文传输时间了。只要接收到回复就表示连接是正常的。耗费的时间喻示了路径长度。重复请求响应的一致性也表明了连接质量的可靠性。因此,ping回答了两个基本的问题:是否有连接?连接的质量如何?


我们称发送回显请求的ping程序为客户,称被ping的主机为服务器。大多数的TCP/IP实现都在内核中直接支持ping服务器,这种服务器不是一个用户进程。ICMP回显请求和回显应答报文如下:


image.png


Unix系统在实现ping程序时是把ICMP报文中的标识符字段置成发送进程的ID号。这样即使在同一台主机上同时运行了多个ping程序实例,ping程序也可以识别出返回的信息。序列号从0开始,每发送一次新的回显请求就加1。


2.3 ping程序命令介绍


ping程序的主要选项:


  1. -c:选项允许用户指定发送报文的数量,例如,ping –c10会发送10个报文然后停止;
  2. -f:选项表明报文发送速率与接收主机能够处理速率相同,这一参数可用于链路压力测试或接口性能比较;
  3. -l:选项用于计数,尽可能快的发送该数量报文,然后恢复正常,该命令用于测试处理泛洪的能力,需要root权限执行;
  4. -i:选项用于用户在两个连续报文之间指定等待秒数。该命令对于将报文间隔开或用在脚本中非常有用。正常情况下,偶然的ping包对数据流的影响是很小的。但重复报文或报文泛洪影响就很大了。因此,使用该选项时需谨慎;
  5. -n:选项将输出限制为数字形式,这在碰见DNS问题时很有用;
  6. -v:显示更详尽输出,较少输出为-q和-Q;
  7. -s:选项指定发送数据的大小。但如果设置的太小,小于8,则报文中就没有空间留给时间戳了。设置报文大小能诊断有路径MTU(Maximum Transmission Unit)设置或分段而导致的问题。如果不使用该选项,ping默认是64字节。


2.4 Android端执行ping程序


Android系统提供了ping命令行程序,在程序中可以通过popen执行系统自带ping程序,下面是执行ping程序的代码:


int RunPingQuery(int _querycount, int interval/*S*/, int timeout/*S*/, const char* dest, unsigned int packetSize) {
    char cmd[256] = {0};
    int index = snprintf(cmd, 256, "ping -c %d -i %d -w %d", _querycount, interval, timeout);
    if (index < 0 || index >= 256) {
        //sprintf return error
        return -1;
    }
    int tempLen = 0;
    if (packetSize > 0) {
        tempLen = snprintf((char*)&cmd[index], 256 - index, " -s %u  %s", packetSize, dest);
    } else {
        tempLen = snprintf((char*)&cmd[index], 256 - index, " %s", dest);
    }
    if (tempLen < 0 || tempLen >= 256 - index) {
        //sprintf return error
        return -1;
    }
    FILE* pp = popen(cmd, "r");
    if (!pp) {
        //popen error
        return -1;
    }
    std::string pingresult_;
    while (fgets(line, sizeof(line), pp) != NULL) {
        pingresult_.append(line, strlen(line));
    }
    pclose(pp);
    if (pingresult_.empty()) {
        //m_strPingResult is empty
        return -1;
    }
    struct PingStatus pingStatusTemp;  //= {0};notice: cannot initial with = {0},crash
    GetPingStatus(pingStatusTemp);
    if (0 == pingStatusTemp.avgrtt && 0 == pingStatusTemp.maxrtt) {
        //remote host is not available
        return -1;
    }
    return 0;
}
int GetPingStatus(struct PingStatus& _ping_status, std::string pingresult_) {
    if (pingresult_.empty())  return -1;
    _ping_status.res = pingresult_;
    std::vector<std::string> vecPingRes;
    str_split('\n', pingresult_, vecPingRes);
    std::vector<std::string>::iterator iter = vecPingRes.begin();
    for (; iter != vecPingRes.end(); ++iter) {
        if (vecPingRes.begin() == iter) {  // extract ip from the result string and assign to _ping_status.ip
            int index1 = iter->find_first_of("(", 0);
            if (index1 > 0) {
                int index2 = iter->find_first_of(")", 0);
                if (index2 > index1) {
                    int size = index2 - index1 - 1;
                    std::string ipTemp(iter->substr(index1 + 1, size));
                    strncpy(_ping_status.ip, ipTemp.c_str(), (size < 16 ? size : 15));
                }
            }
        }  // end if(vecPingRes.begin()==iter)
        int num = iter->find("packet loss", 0);
        if (num >= 0) {
            int loss_rate = 0;
            int i = 3;
            while (iter->at(num - i) != ' ') {
                loss_rate += ((iter->at(num - i) - '0') * (int)pow(10.0, (double)(i - 3)));
                i++;
            }
            _ping_status.loss_rate  = (double)loss_rate / 100;
        }
        int num2 = iter->find("rtt min/avg/max", 0);
        if (num2 >= 0) {
            int find_begpos = 23;
            int findpos = iter->find_first_of('/', find_begpos);
            std::string sminRTT(*iter, find_begpos, findpos - find_begpos);
            find_begpos = findpos + 1;
            findpos = iter->find_first_of('/', find_begpos);
            std::string savgRTT(*iter, find_begpos, findpos - find_begpos);
            find_begpos = findpos + 1;
            findpos = iter->find_first_of('/', find_begpos);
            std::string smaxRTT(*iter, find_begpos, findpos - find_begpos);
            _ping_status.minrtt = atof(sminRTT.c_str());
            _ping_status.avgrtt = atof(savgRTT.c_str());
            _ping_status.maxrtt = atof(smaxRTT.c_str());
        }
    }
    return 0;
}


2.5 iOS端发送ping指令


iOS端主要通过创建socket发送ICMP执行,主要思路如下:


1.如果设置的是域名,需要将DNS转换为IP;

2.创建socketn = socket(family, type, protocol),family为AF_INET, type为SOCK_DGRAM, protocol为IPPROTO_ICMP;

3.构造ICMP包:


struct icmp {
    u_char    icmp_type;        /* type of message, see below */
    u_char    icmp_code;        /* type sub code */
    u_short    icmp_cksum;        /* ones complement cksum of struct */
    union {
        u_char ih_pptr;            /* ICMP_PARAMPROB */
        struct in_addr ih_gwaddr;    /* ICMP_REDIRECT */
        struct ih_idseq {
            n_short    icd_id;
            n_short    icd_seq;
        } ih_idseq;
        int ih_void;
        /* ICMP_UNREACH_NEEDFRAG -- Path MTU Discovery (RFC1191) */
        struct ih_pmtu {
            n_short ipm_void;
            n_short ipm_nextmtu;
        } ih_pmtu;
        struct ih_rtradv {
            u_char irt_num_addrs;
            u_char irt_wpa;
            u_int16_t irt_lifetime;
        } ih_rtradv;
    } icmp_hun;
#define    icmp_pptr    icmp_hun.ih_pptr
#define    icmp_gwaddr    icmp_hun.ih_gwaddr
#define    icmp_id        icmp_hun.ih_idseq.icd_id
#define    icmp_seq    icmp_hun.ih_idseq.icd_seq
#define    icmp_void    icmp_hun.ih_void
#define    icmp_pmvoid    icmp_hun.ih_pmtu.ipm_void
#define    icmp_nextmtu    icmp_hun.ih_pmtu.ipm_nextmtu
#define    icmp_num_addrs    icmp_hun.ih_rtradv.irt_num_addrs
#define    icmp_wpa    icmp_hun.ih_rtradv.irt_wpa
#define    icmp_lifetime    icmp_hun.ih_rtradv.irt_lifetime
    union {
        struct id_ts {
            n_time its_otime;
            n_time its_rtime;
            n_time its_ttime;
        } id_ts;
        struct id_ip  {
            struct ip idi_ip;
            /* options and then 64 bits of data */
        } id_ip;
        struct icmp_ra_addr id_radv;
        u_int32_t id_mask;
        char    id_data[1];
    } icmp_dun;
#define    icmp_otime    icmp_dun.id_ts.its_otime
#define    icmp_rtime    icmp_dun.id_ts.its_rtime
#define    icmp_ttime    icmp_dun.id_ts.its_ttime
#define    icmp_ip        icmp_dun.id_ip.idi_ip
#define    icmp_radv    icmp_dun.id_radv
#define    icmp_mask    icmp_dun.id_mask
#define    icmp_data    icmp_dun.id_data
};
void __preparePacket(char* _sendbuffer, int& _len) {
    char    sendbuf[MAXBUFSIZE];
    memset(sendbuf, 0, MAXBUFSIZE);
    struct icmp* icmp;
    icmp = (struct icmp*) sendbuf;
    icmp->icmp_type = ICMP_ECHO;
    icmp->icmp_code = 0;
    icmp->icmp_id = getpid() & 0xffff;/* ICMP ID field is 16 bits */
    icmp->icmp_seq = htons(nsent_++);
    memset(&sendbuf[ICMP_MINLEN], 0xa5, DATALEN);   /* fill with pattern */
    struct timeval now;
    (void)gettimeofday(&now, NULL);
    now.tv_usec = htonl(now.tv_usec);
    now.tv_sec = htonl(now.tv_sec);
    bcopy((void*)&now, (void*)&sendbuf[ICMP_MINLEN], sizeof(now));
    _len = ICMP_MINLEN + DATALEN;        /* checksum ICMP header and data */
    icmp->icmp_cksum = 0;
    icmp->icmp_cksum = in_cksum((u_short*) icmp, _len);
    memcpy(_sendbuffer, sendbuf, _len);
}


4.接收ICMP包:


int PingQuery::__recv() {
    char            recvbuf[MAXBUFSIZE];
    char            controlbuf[MAXBUFSIZE];
    memset(recvbuf, 0, MAXBUFSIZE);
    memset(controlbuf, 0, MAXBUFSIZE);
    struct msghdr   msg = {0};
    struct iovec    iov = {0};
    iov.iov_base = recvbuf;
    iov.iov_len = sizeof(recvbuf);
    msg.msg_name = &recvaddr_;
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;
    msg.msg_control = controlbuf;
    msg.msg_namelen = sizeof(recvaddr_);
    msg.msg_controllen = sizeof(controlbuf);
    int n = (int)recvmsg(sockfd_, &msg, 0);
    if (n < 0) {
        return -1;
    }
    //解析消息结构
    return n;
}


2.6 ping网络延迟的相关参考


ping外网小于50ms,网络的延迟就算良好,是正常的。


一般来说,网络的延迟PING值越低,速度会越快;但是网络的速度与网络延迟这二者之间没有必然的联系,以下是ping网络延迟的相关参考数据:


  • ping网络:1到30ms:速度极快,几乎察觉不出有延迟,玩任何游戏速度都特别顺畅;~
  • ping网络:31到50ms:速度良好,可以正常游戏浏览网页,没有明显的延迟情况;~
  • ping网络:51到100ms:速度普通,对抗类游戏在一定水平以上能感觉出延迟,偶尔感觉到停顿;
  • ping网络:100ms到200ms:速度较差,无法正常游玩对抗类游戏,有明显的卡顿现象,偶尔出现丢包和掉线现象。


3. dns解析耗时


在我们创建socket前,会有一个域名转IP的解析过程。域名系统是一种用于TCP/IP应用程序的分布式数据库,提供了域名和IP地址之间的转换及有关电子邮件的选路信息。在Unix主机中,通过两个库函数gethostbyname和gethostbyaddr来访问的。前者接收主机名字返回IP地址,后者接收IP地址来寻找主机名字。


我们知道域名解析要访问域名服务,连接域名服务是基于UDP还是TCP呢?DNS名字服务器使用的熟知端口号是53,通过tcpdump观察到所有例子都是采用UDP,为什么采用的是UDP呢?


当名字解析器发出一个查询请求,并且返回响应中的TC(删减标志)比特被设置为1时,它就意味着响应的长度超过了512个字节,而仅返回前512个字节。在遇到这种情况时,名字解析器通过使用TCP重发原来的查询请求,它将允许返回的响应超过512个字节。TCP能将用户的数据流分为一些报文段,它就能用多个报文段来传送任意长度的用户数据。


我们要统计DNS解析延时就需要自己创建socket,发送DNS报文并获取响应计算耗时。创建socket需要知道DNS服务地址,怎么获取DNS地址呢?


一种常见的方法通过获取手机配置文件获取:


char buf1[PROP_VALUE_MAX];
char buf2[PROP_VALUE_MAX];
__system_property_get("net.dns1", buf1);
__system_property_get("net.dns2", buf2);


这种方式高版本获取不到DNS服务地址,部分高版本手机可通过下面方法获取:


char buf3[1024];
    __system_property_get("ro.config.dnscure_ipcfg", buf3);
    std::string dnsCureIPCfgStr(buf3);
    if (!dnsCureIPCfgStr.empty()) {
        const std::vector<std::string> &kVector = splitstr(dnsCureIPCfgStr, '|');
        if (kVector.size() > 2) {
            const std::vector<std::string> &kVector2 = splitstr(dnsCureIPCfgStr, ';');
            if (kVector2.size() > 2) {
                _dns_servers.push_back(kVector2[0]);  // 主DNS
                _dns_servers.push_back(kVector2[1]);  // 备DNS
                return;
            }
        }
    }


该方法获取到DNS列表,以逗号分隔地址列表,内网外网通过|区分。


通过ConnectivityManager获取:


private static String[] getDnsFromConnectionManager(Context context) {
    LinkedList<String> dnsServers = new LinkedList<>();
    if (Build.VERSION.SDK_INT >= 21 && context != null) {
      ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(context.CONNECTIVITY_SERVICE);
      if (connectivityManager != null) {
        NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo();
        if (activeNetworkInfo != null) {
          for (Network network : connectivityManager.getAllNetworks()) {
            NetworkInfo networkInfo = connectivityManager.getNetworkInfo(network);
            if (networkInfo != null && networkInfo.getType() == activeNetworkInfo.getType()) {
              LinkProperties lp = connectivityManager.getLinkProperties(network);
              for (InetAddress addr : lp.getDnsServers()) {
                dnsServers.add(addr.getHostAddress());
              }
            }
          }
        }
      }
    }
    return dnsServers.isEmpty() ? new String[0] : dnsServers.toArray(new String[dnsServers.size()]);
  }


获取到的是内网DNS地址。在Android端获取DNS服务地址需要考虑到Android品牌及系统的兼容性。


4. tcp连接耗时统计


TCP耗时从socket创建到连接、收发消息耗时:


  1. 创建socketsocket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  2. 建立连接:int connectRet = connect(fsocket, (sockaddr*)&_addr, sizeof(_addr));
  3. 发送测试指令:send
  4. 接收消息:recv


5. 总结


本文讨论了统计移动端网络耗时网络质量的主要方法:ping耗时、DNS耗时、TCP连接耗时等。在移动端要考虑到获取DNS服务地址的兼容性、tcp socket读写次数等策略,以及简要介绍了网络质量评估方法。

目录
相关文章
|
10天前
|
机器学习/深度学习 人工智能 自然语言处理
深度学习中的卷积神经网络:从理论到实践
【10月更文挑战第35天】在人工智能的浪潮中,深度学习技术以其强大的数据处理能力成为科技界的宠儿。其中,卷积神经网络(CNN)作为深度学习的一个重要分支,在图像识别和视频分析等领域展现出了惊人的潜力。本文将深入浅出地介绍CNN的工作原理,并结合实际代码示例,带领读者从零开始构建一个简单的CNN模型,探索其在图像分类任务中的应用。通过本文,读者不仅能够理解CNN背后的数学原理,还能学会如何利用现代深度学习框架实现自己的CNN模型。
|
10天前
|
数据采集 网络协议 算法
移动端弱网优化专题(十四):携程APP移动网络优化实践(弱网识别篇)
本文从方案设计、代码开发到技术落地,详尽的分享了携程在移动端弱网识别方面的实践经验,如果你也有类似需求,这篇文章会是一个不错的实操指南。
26 1
|
16天前
|
数据采集 存储 XML
Python实现网络爬虫自动化:从基础到实践
本文将介绍如何使用Python编写网络爬虫,从最基础的请求与解析,到自动化爬取并处理复杂数据。我们将通过实例展示如何抓取网页内容、解析数据、处理图片文件等常用爬虫任务。
|
1月前
|
弹性计算 人工智能 运维
Terraform从入门到实践:快速构建你的第一张业务网络(上)
本次分享主题为《Terraform从入门到实践:快速构建你的第一张业务网络》。首先介绍如何入门和实践Terraform,随后演示如何使用Terraform快速构建业务网络。内容涵盖云上运维挑战及IaC解决方案,并重磅发布Terraform Explorer产品,旨在降低使用门槛并提升用户体验。此外,还将分享Terraform在实际生产中的最佳实践,帮助解决云上运维难题。
124 1
Terraform从入门到实践:快速构建你的第一张业务网络(上)
|
30天前
|
机器学习/深度学习 人工智能 监控
深入理解深度学习中的卷积神经网络(CNN):从原理到实践
【10月更文挑战第14天】深入理解深度学习中的卷积神经网络(CNN):从原理到实践
84 1
|
1月前
|
机器学习/深度学习 存储 自然语言处理
从理论到实践:如何使用长短期记忆网络(LSTM)改善自然语言处理任务
【10月更文挑战第7天】随着深度学习技术的发展,循环神经网络(RNNs)及其变体,特别是长短期记忆网络(LSTMs),已经成为处理序列数据的强大工具。在自然语言处理(NLP)领域,LSTM因其能够捕捉文本中的长期依赖关系而变得尤为重要。本文将介绍LSTM的基本原理,并通过具体的代码示例来展示如何在实际的NLP任务中应用LSTM。
71 4
|
13天前
|
边缘计算 5G 数据处理
5G网络能耗管理:绿色通信的实践
【10月更文挑战第30天】
33 0
|
1月前
|
自动驾驶 物联网 5G
5G网络的演进:从理论到实践
【10月更文挑战第3天】5G网络作为新一代移动通信技术,不仅在理论上实现了重大突破,而且在实践中也展现出了强大的生命力。本文将围绕5G网络的演进,从理论基础到实际应用,探讨5G技术的发展和实践案例,同时提供代码示例以供参考。
103 6
|
1月前
|
机器学习/深度学习 算法 数据建模
计算机前沿技术-人工智能算法-生成对抗网络-算法原理及应用实践
计算机前沿技术-人工智能算法-生成对抗网络-算法原理及应用实践
25 0
|
1月前
|
物联网 5G 调度