手把手教你搭建應(yīng)用的網(wǎng)絡(luò)診斷模塊(1)——Ping與TraceRoute

「椎鋒陷陳」微信技術(shù)號現(xiàn)已開通,為了獲得第一手的技術(shù)文章推送,歡迎搜索關(guān)注!

前言

一個App功能的整體表現(xiàn),往往與用戶當(dāng)前的網(wǎng)絡(luò)狀況密不可分。通過為App引入一個輕量級的網(wǎng)絡(luò)診斷模塊,收集那些能夠衡量當(dāng)前網(wǎng)絡(luò)狀況的重要信息,然后在征得用戶同意的情況下,將信息上報(bào)到服務(wù)端進(jìn)行分析,可以有針對性地對網(wǎng)絡(luò)鏈路中的薄弱環(huán)節(jié)進(jìn)行優(yōu)化。

眾所周知,Android系統(tǒng)基于Linux內(nèi)核的,Linux本身就提供了許多可用于檢測網(wǎng)絡(luò)狀況的工具,熟練地運(yùn)用這些工具,可以很輕松地達(dá)到我們網(wǎng)絡(luò)診斷的目的。今天要分享的就是其中的兩個工具,Ping命令與TraceRoute命令。

網(wǎng)絡(luò)工具介紹

Ping

聲吶技術(shù)

「Ping」這個名字源于聲吶技術(shù),聲吶技術(shù)是利用聲波在水中的傳播和反射特性,對水下目標(biāo)進(jìn)行探測、分類、定位和跟蹤的技術(shù)。

概述

Ping命令是用于檢測從源主機(jī)到目標(biāo)主機(jī)是否可達(dá)的工具。

該命令基于ICMP協(xié)議,通過向目標(biāo)主機(jī)發(fā)送指定個數(shù)與大小的回送請求(echo request)數(shù)據(jù)包,并要求目標(biāo)主機(jī)在收到之后返回相應(yīng)的回送應(yīng)答(echo reply)數(shù)據(jù)包,最終結(jié)合數(shù)據(jù)包的往返時(shí)間丟包率來評估網(wǎng)絡(luò)連接狀況。

圖示

ping命令模型

如果用開頭提及的聲吶技術(shù)來類比,就會是這樣的一個對應(yīng)關(guān)系:

對應(yīng)關(guān)系

形式

Ping命令的基本形式如下:

ping [-c 數(shù)據(jù)包個數(shù)] [-s 數(shù)據(jù)包大小] [主機(jī)名/IP地址]

例:

ping -c 5 -s 56 developer.android.google.cn

默認(rèn)情況下,假如不指定數(shù)據(jù)包個數(shù),Ping命令就會連續(xù)發(fā)送數(shù)據(jù)包,如果僅僅是為了進(jìn)行連通性測試,只需要指定3到5個即可。

而假如不指定數(shù)據(jù)包大小,則默認(rèn)是56 bytes。

實(shí)現(xiàn)

Android支持直接使用命令行工具執(zhí)行Ping命令,因此只需設(shè)定好參數(shù),逐行讀取輸出內(nèi)容即可:

/**
 * Ping命令
 */
class Ping(
    /** 目標(biāo)主機(jī)域名/IP地址  */
    private val host: String,
    /** 數(shù)據(jù)包個數(shù),默認(rèn)連續(xù)發(fā)送  */
    private val count: Int? = null,
    /** 數(shù)據(jù)包大小,單位bytes,默認(rèn)為56 bytes  */
    private val packetSize: Int? = null,
    /** 數(shù)據(jù)包生存時(shí)間 */
    private val ttl: Int? = null,
    /** 超時(shí)間隔,單位s */
    private val deadline: Int? = null
) {

    /**
     * ## 執(zhí)行Ping命令
     * 請注意,ping命令在Linux系統(tǒng)下的參數(shù)與在Windows系統(tǒng)下有差異,需要區(qū)分
     * -c count ping指定次數(shù)后停止ping;
     * -s packetsize 指定每次ping發(fā)送的數(shù)據(jù)字節(jié)數(shù),默認(rèn)為“56字節(jié)”+“28字節(jié)”的ICMP頭,一共是84字節(jié);
     */
    fun execute(callback: ExecuteCallback? = null): String {
        val command = toString()
        // 回調(diào)輸出執(zhí)行的Ping命令
        callback?.onExecuting("% $command\n")

        val result = StringBuilder()
        var process: Process? = null
        var reader: BufferedReader? = null
        try {
            process = Runtime.getRuntime().exec(command)
            reader = BufferedReader(InputStreamReader(process.inputStream))
            // 讀取首行輸出內(nèi)容
            var line = reader.readLine()
            while (line != null) {
                // 回調(diào)執(zhí)行過程的輸出內(nèi)容
                callback?.onExecuting(line)
                // 記錄輸出行到結(jié)果字符串
                result.append(line).append("\n")
                // 讀取下一行輸出內(nèi)容
                line = reader.readLine()
            }
            callback?.onCompleted(result.toString())
            reader.close()
            process.waitFor()
        } catch (e: Exception) {
            e.printStackTrace()
        } finally {
            reader?.close()
            process?.destroy()
        }

        return result.toString()
    }

    /**
     * ## 根據(jù)構(gòu)造字段將實(shí)體轉(zhuǎn)換為具體的Ping命令
     * 判斷各字段非
     */
    override fun toString(): String {
        val stringBuilder = StringBuilder("ping")
        if (count != null) stringBuilder.append(" -c $count")
        if (packetSize != null) stringBuilder.append(" -s $packetSize")
        if (ttl != null) stringBuilder.append(" -t $ttl")
        if (deadline != null) stringBuilder.append(" -w $deadline")
        stringBuilder.append(" $host")
        return stringBuilder.toString()
    }

}

執(zhí)行

Ping命令執(zhí)行結(jié)果.jpg

分析

為了方便進(jìn)行說明,我們在每一個結(jié)果行前添加了一個序號。

整個示例可以分為兩塊區(qū)域,從第1到6行為執(zhí)行過程,從第7到8行為統(tǒng)計(jì)信息。

執(zhí)行過程

第1行表示的是向目標(biāo)主機(jī)發(fā)送了5個56 bytes的數(shù)據(jù)包。

第2-6行數(shù)表示的是每個發(fā)送的回送請求數(shù)據(jù)包的執(zhí)行結(jié)果,其中:

  • [64 bytes]是從目標(biāo)主機(jī)返回的數(shù)據(jù),之所以是64 bytes是由于加多了8 bytes的ICMP報(bào)頭。
  • [icmp_seq]是ICMP報(bào)頭中包含的時(shí)序號,用于確定數(shù)據(jù)包到達(dá)的順序以及判斷數(shù)據(jù)包是否重復(fù)
  • [ttl]是數(shù)據(jù)包的生存時(shí)間,是time to live的縮寫,是為了防止數(shù)據(jù)包在路由選擇的過程中無休止地在網(wǎng)絡(luò)中流動而設(shè)置的。
  • [time]是數(shù)據(jù)包的往返時(shí)間,即從發(fā)送回送請求報(bào)文之后,到接收到回送應(yīng)答報(bào)文之前經(jīng)過的時(shí)間。
統(tǒng)計(jì)信息

第7行表示的是數(shù)據(jù)包的傳輸接收情況以及丟包率,其中:

  • [5 packets transmitted]表示傳輸了5個數(shù)據(jù)包
  • [5 packets received]表示接收了5個數(shù)據(jù)包
  • [0 packet loss]表示數(shù)據(jù)包的丟包率為0%,該數(shù)值越大,表示網(wǎng)絡(luò)狀況越不穩(wěn)定,最高為100%,即目標(biāo)主機(jī)不可達(dá)。

第8行表示的是數(shù)據(jù)包往返時(shí)間的最小值/平均值/最大值,單位為毫秒(ms)。數(shù)值越大,意味著網(wǎng)絡(luò)延遲越嚴(yán)重;最小值與最大值之間的差值越大,意味著網(wǎng)絡(luò)抖動越厲害。

TraceRoute

路由跟蹤.png

兩臺主機(jī)之間的通信,往往需要經(jīng)過很多中間節(jié)點(diǎn),如果其中某個節(jié)點(diǎn)出現(xiàn)問題,可能會導(dǎo)致數(shù)據(jù)無法送達(dá),通過TraceRoute(跟蹤路由)我們可以定位數(shù)據(jù)是在哪個節(jié)點(diǎn)丟失的。

概述

TraceRoute命令是用于定位從源主機(jī)到目標(biāo)主機(jī)所經(jīng)過的路由,以及到達(dá)各個路由的數(shù)據(jù)包往返時(shí)間的工具。

該命令利用的是IP報(bào)頭的TTL值、ICMP超時(shí)報(bào)文以及ICMP端口不可達(dá)報(bào)文,TraceRoute每次都向相同的目標(biāo)主機(jī)發(fā)送三次設(shè)置了相同TTL值的數(shù)據(jù)包,利用數(shù)據(jù)包被丟棄時(shí)路由器返回ICMP超時(shí)報(bào)文獲知路由器的IP地址及數(shù)據(jù)包的往返時(shí)間。

流程

  1. 首先,向目標(biāo)主機(jī)發(fā)送TTL值設(shè)為1的數(shù)據(jù)包,處理該數(shù)據(jù)包的第一個路由器會將TTL值減1。當(dāng)TTL值變?yōu)?時(shí),該數(shù)據(jù)包就會被丟棄,并發(fā)回一份ICMP超時(shí)報(bào)文,這樣就得到了該路徑中的第一個路由器的IP地址。
  2. 接著,發(fā)送TTL值設(shè)為2的數(shù)據(jù)包,該數(shù)據(jù)包在經(jīng)過第二個路由器時(shí)就會被丟棄,這樣就得到了第二個路由器的IP地址。
  3. 持續(xù)這個過程,直至數(shù)據(jù)包到達(dá)目標(biāo)主機(jī)。
  4. 為了確認(rèn)數(shù)據(jù)包是否到達(dá)目標(biāo)主機(jī),TraceRoute使用了一個一般應(yīng)用程序都不會使用的端口號(30000以上)作為目標(biāo)端口號。這樣,當(dāng)數(shù)據(jù)包到達(dá)目標(biāo)主機(jī)時(shí),目標(biāo)主機(jī)就會返回一個ICMP端口不可達(dá)報(bào)文,從而讓源主機(jī)可以確認(rèn)數(shù)據(jù)包已經(jīng)到達(dá)了目標(biāo)主機(jī)。

圖示

TraceRoute執(zhí)行流程(1).png

形式

TraceRoute命令的基本形式如下:

traceroute [主機(jī)名/IP地址]

例:

traceroute developer.android.google.cn

實(shí)現(xiàn)

由于Android的非Root設(shè)備不支持直接使用命令行工具執(zhí)行TraceRoute命令,因此我們改成以執(zhí)行Ping命令并通過限定TTL的方式來模擬TraceRoute的過程,從而達(dá)到相等效果。缺點(diǎn)是模擬過程較慢,可能會頻繁出現(xiàn)超時(shí)情況。

具體的模擬過程如下:

  1. 對目標(biāo)主機(jī)執(zhí)行Ping命令,發(fā)送1個TTL值為1的數(shù)據(jù)包,第一個路由器將TTL值減1變?yōu)?,數(shù)據(jù)包被路由器丟棄,輸出以下結(jié)果行:
    From 10.0.168.254: icmp_seq=1 Time to live exceeded
  1. 對該結(jié)果行進(jìn)行正則表達(dá)式匹配,提取其中包含的路由器IP地址,如10.0.168.254;
  2. 對路由器IP地址執(zhí)行Ping命令,發(fā)送3個大小為40 bytes的數(shù)據(jù)包,數(shù)據(jù)包到達(dá)該路由器,輸出以下結(jié)果行:
    48 bytes from 211.136.203.125: icmp_seq=1 ttl=251 time=28.2 ms
    48 bytes from 211.136.203.125: icmp_seq=2 ttl=251 time=75.4 ms
    48 bytes from 211.136.203.125: icmp_seq=3 ttl=251 time=33.5 ms
  1. 對該結(jié)果行進(jìn)行正則表達(dá)式匹配,提取其中包含的數(shù)據(jù)包往返時(shí)間;
  2. 對目標(biāo)主機(jī)再次執(zhí)行Ping命令,發(fā)送1個TTL值設(shè)為2的數(shù)據(jù)包,在經(jīng)過第二個路由器時(shí)被丟棄,同樣從結(jié)果行中提取出路由器IP地址。
  3. 對第二個路由器的IP地址執(zhí)行Ping命令,同樣從結(jié)果行中提取出數(shù)據(jù)包往返時(shí)間。
  4. 持續(xù)這個過程直至數(shù)據(jù)包到達(dá)目標(biāo)主機(jī),輸出以下結(jié)果行:
64 bytes from 113.108.239.226: icmp_seq=1 ttl=115 time=33.0 ms
  1. 對目標(biāo)主機(jī)IP地址執(zhí)行Ping命令,同樣從結(jié)果行中提取出數(shù)據(jù)包往返時(shí)間,模擬結(jié)束。
  2. 如果過程中數(shù)據(jù)包超過5s沒有返回,則會輸出空的結(jié)果行,因而提取不出路由器IP地址,轉(zhuǎn)而輸出[* * *]。
  3. 當(dāng)躍點(diǎn)數(shù)超過設(shè)立的最大30個躍點(diǎn)數(shù)后仍未到達(dá)目標(biāo)主機(jī),則模擬結(jié)束。

相應(yīng)的流程圖如下:


PingTraceRoute.png

具體代碼如下:

/**
 * TraceRoute命令
 * <p>
 * 由于Android的非Root設(shè)備不支持直接使用命令行工具API執(zhí)行TraceRoute命令,因此改用執(zhí)行Ping命令
 * 并通過限定TTL參數(shù)(IP包被路由器丟棄之前允許通過的最大網(wǎng)段數(shù))來達(dá)到相等效果
 * 路由器地址通過正則表達(dá)式匹配從Ping響應(yīng)內(nèi)容中截取,
 * 路由耗時(shí)通過執(zhí)行Ping命令前后時(shí)間戳對比估算
 */
class TraceRoute(
    /** 目標(biāo)主機(jī)域名  */
    private val host: String
) {
    var TAG = this::class.java.simpleName

    companion object {
        /** IP包被路由器丟棄之前允許通過的最大網(wǎng)段數(shù)  */
        const val MAX_HOP = 30

        /** 正則表達(dá)式-路由器IP地址 */
        private const val REGEX_ROUTE_IP = "(?<=From )(?:[0-9]{1,3}\\.){3}[0-9]{1,3}"
        /** 正則表達(dá)式-目標(biāo)主機(jī)IP地址 */
        private const val REGEX_HOST_IP = "(?<=from ).*(?=: icmp_seq=1 ttl=)"
        /** 正則表達(dá)式-數(shù)據(jù)包往返時(shí)間 */
        private const val REGEX_RRT = "(?<=time=).*?ms"
    }

    /**
     * 執(zhí)行Ping命令模擬TraceRoute流程
     * -c count ping指定次數(shù)后停止ping;
     * -t 設(shè)置TTL(Time To Live,生存時(shí)間)為指定的值。該字段指定IP包被路由器丟棄之前允許通過的最大網(wǎng)段數(shù);
     */
    fun execute(callback: ExecuteCallback? = null) {
        val command = toString();
        callback?.onExecuting("% $command\n")

        callback?.onExecuting("traceroute to $host, 30 hos max, 40 byte packets\n")

        // 當(dāng)前躍點(diǎn)數(shù)
        var hop = 1
        // 終止標(biāo)識
        var done = false

        while (!done && hop <= MAX_HOP) {
            val pingResult = Ping(host, packetSize = 40, count = 1, ttl = hop, deadline = 5).execute()
            Log.d(TAG, "ping host ip: $pingResult \n\n")

            val lineBuilder = StringBuilder()
            lineBuilder.append(hop).append(".")

            // 用正則表達(dá)式匹配響應(yīng)內(nèi)容行
            val routerIpMatcher = matchRouterIp(pingResult)
            if (routerIpMatcher.find()) {   // 匹配到了路由器IP地址,打印路由器IP地址及到達(dá)該路由器的耗時(shí)
                val routerIp = subRouteIpString(routerIpMatcher)
                lineBuilder.append("\t\t").append(routerIp)

                val pingResult = Ping(host = routerIp, packetSize = 40, count = 3, deadline = 5).execute()
                Log.d(TAG, "ping route ip: $pingResult \n\n")
                matchAndAppendRTT(pingResult, lineBuilder)
            } else {    // 匹配不到
                val hostIpMatcher = matchHostIp(pingResult)
                if(hostIpMatcher.find()) {
                    val hostIp = hostIpMatcher.group()
                    lineBuilder.append("\t\t").append(hostIp)

                    val pingResult = Ping(host = hostIp, packetSize = 40, count = 3, deadline = 5).execute()
                    Log.d(TAG, "ping host ip: $pingResult \n\n")
                    matchAndAppendRTT(pingResult, lineBuilder)
                    done = true
                } else {
                    lineBuilder.append("\t\t *\t\t*\t\t* \t")
                }
            }

            callback?.onExecuting(lineBuilder.toString())

            hop++
        }
    }

    /**
     * 匹配并記錄數(shù)據(jù)包往返時(shí)間
     */
    private fun matchAndAppendRTT(pingResult: String, lineBuilder: StringBuilder) {
        val rttMatcher = matchRTT(pingResult)
        lineBuilder.append("\t\t")
        var i = 0
        while(i < 3) {
            if(rttMatcher.find()) {
                val rtt = rttMatcher.group()
                lineBuilder.append(rtt).append("\t\t")
            } else {
                lineBuilder.append("*").append("\t\t")
            }
            i++
        }
        lineBuilder.append("\t")
    }

    /**
     * 匹配路由器IP地址
     */
    private fun matchRouterIp(input: CharSequence) = Pattern.compile(REGEX_ROUTE_IP).matcher(input)

    /**
     * 匹配數(shù)據(jù)包往返時(shí)間
     */
    private fun matchRTT(input: CharSequence) = Pattern.compile(REGEX_RRT).matcher(input)

    /**
     * 匹配目標(biāo)主機(jī)IP地址
     */
    private fun matchHostIp(input: CharSequence) =  Pattern.compile(REGEX_HOST_IP).matcher(input)

    /**
     * 截取路由器IP字符串
     */
    private fun subRouteIpString(matcher: Matcher): String {
        var pingIp = matcher.group()
        val start = pingIp.indexOf('(')
        if (start >= 0) {
            pingIp = pingIp.substring(start + 1)
        }
        return pingIp
    }

    override fun toString(): String {
        return "traceroute $host"
    }
}

執(zhí)行

TraceRoute命令執(zhí)行結(jié)果.jpg

分析

第1行表示的是TraceRoute命令向目標(biāo)主機(jī)發(fā)送最多30個躍點(diǎn)、40 bytes的數(shù)據(jù)包。

第2行起表示經(jīng)過的路由器信息,其中:

  • 最前面的數(shù)字表示的是躍點(diǎn)數(shù),與所發(fā)送的數(shù)據(jù)包的TTL值一致
  • [172.16.88.1]表示的是經(jīng)過的路由器IP地址
  • [4.69ms 9.29ms 9.24ms]表示的是所發(fā)送的三個數(shù)據(jù)包分別的往返時(shí)間
  • [*]表示的是沒有應(yīng)答,當(dāng)所發(fā)送的三個數(shù)據(jù)包中有任意一個超過5秒沒有應(yīng)答時(shí),則會以星號表示

如果目標(biāo)主機(jī)可達(dá),則會在到達(dá)某一躍點(diǎn)后結(jié)束,由此可知經(jīng)過的路由器數(shù)量,如果目標(biāo)主機(jī)不可達(dá),則會在到達(dá)第30個躍點(diǎn)后結(jié)束,從而可知數(shù)據(jù)包被送到什么地方。

總結(jié)

為了有針對性地對網(wǎng)絡(luò)進(jìn)行優(yōu)化,我們?yōu)锳pp引入了一個輕量級的網(wǎng)絡(luò)診斷模塊,主要借助的是Linux本身提供的檢測網(wǎng)絡(luò)狀況的工具,在本篇中介紹的是Ping命令和TraceRoute命令。

  • Ping命令用于檢測到目標(biāo)主機(jī)是否可達(dá),通過結(jié)合數(shù)據(jù)包的往返時(shí)間和丟包率我們能初步地評估網(wǎng)絡(luò)狀況,包括網(wǎng)絡(luò)延遲/網(wǎng)絡(luò)抖動/網(wǎng)絡(luò)穩(wěn)定性等情況。
  • TraceRoute命令用于定位到目標(biāo)主機(jī)所經(jīng)過的路由及其耗時(shí),以定位網(wǎng)絡(luò)故障發(fā)生的節(jié)點(diǎn)。由于Android的非Root設(shè)備不支持直接使用命令行工具執(zhí)行TraceRoute命令,因此我們改用執(zhí)行多次Ping命令來模擬TraceRoute的執(zhí)行流程。

當(dāng)然,網(wǎng)絡(luò)狀況的復(fù)雜度往往超過我們的想象,還有很多這兩個命令不能覆蓋到的故障場景,需要相應(yīng)的工具才能進(jìn)行排查,具體可以關(guān)注后續(xù)推出的文章。

「椎鋒陷陳」微信技術(shù)號現(xiàn)已開通,為了獲得第一手的技術(shù)文章推送,歡迎搜索關(guān)注!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容