「椎鋒陷陳」微信技術(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

「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ò)連接狀況。
圖示

如果用開頭提及的聲吶技術(shù)來類比,就會是這樣的一個對應(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í)行

分析
為了方便進(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

兩臺主機(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í)間。
流程
- 首先,向目標(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地址。
- 接著,發(fā)送TTL值設(shè)為2的數(shù)據(jù)包,該數(shù)據(jù)包在經(jīng)過第二個路由器時(shí)就會被丟棄,這樣就得到了第二個路由器的IP地址。
- 持續(xù)這個過程,直至數(shù)據(jù)包到達(dá)目標(biāo)主機(jī)。
- 為了確認(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命令的基本形式如下:
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í)情況。
具體的模擬過程如下:
- 對目標(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
- 對該結(jié)果行進(jìn)行正則表達(dá)式匹配,提取其中包含的路由器IP地址,如10.0.168.254;
- 對路由器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
- 對該結(jié)果行進(jìn)行正則表達(dá)式匹配,提取其中包含的數(shù)據(jù)包往返時(shí)間;
- 對目標(biāo)主機(jī)再次執(zhí)行Ping命令,發(fā)送1個TTL值設(shè)為2的數(shù)據(jù)包,在經(jīng)過第二個路由器時(shí)被丟棄,同樣從結(jié)果行中提取出路由器IP地址。
- 對第二個路由器的IP地址執(zhí)行Ping命令,同樣從結(jié)果行中提取出數(shù)據(jù)包往返時(shí)間。
- 持續(xù)這個過程直至數(shù)據(jù)包到達(dá)目標(biāo)主機(jī),輸出以下結(jié)果行:
64 bytes from 113.108.239.226: icmp_seq=1 ttl=115 time=33.0 ms
- 對目標(biāo)主機(jī)IP地址執(zhí)行Ping命令,同樣從結(jié)果行中提取出數(shù)據(jù)包往返時(shí)間,模擬結(jié)束。
- 如果過程中數(shù)據(jù)包超過5s沒有返回,則會輸出空的結(jié)果行,因而提取不出路由器IP地址,轉(zhuǎn)而輸出[* * *]。
- 當(dāng)躍點(diǎn)數(shù)超過設(shè)立的最大30個躍點(diǎn)數(shù)后仍未到達(dá)目標(biāo)主機(jī),則模擬結(jié)束。
相應(yīng)的流程圖如下:

具體代碼如下:
/**
* 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í)行

分析
第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)注!