「椎鋒陷陳」微信技術號現已開通,為了獲得第一手的技術文章推送,歡迎搜索關注!
前言
一個App功能的整體表現,往往與用戶當前的網路狀況密不可分,通過為App引入一個輕量級的網路診斷模塊,收集那些能夠衡量當前網路狀況的重要資訊,然后在征得用戶同意的情況下,將資訊上報到服務端進行分析,可以有針對性地對網路鏈路中的薄榷訓節進行優化,
眾所周知,Android系統基于Linux內核的,Linux本身就提供了許多可用于檢測網路狀況的工具,熟練地運用這些工具,可以很輕松地達到我們網路診斷的目的,今天要分享的就是其中的兩個工具,Ping命令與TraceRoute命令,
網路工具介紹
Ping

「Ping」這個名字源于聲吶技術,聲吶技術是利用聲波在水中的傳播和反射特性,對水下目標進行探測、分類、定位和跟蹤的技術,
概述
Ping命令是用于檢測從源主機到目標主機是否可達的工具,
該命令基于ICMP協議,通過向目標主機發送指定個數與大小的回送請求(echo request)資料包,并要求目標主機在收到之后回傳相應的回送應答(echo reply)資料包,最終結合資料包的往返時間和丟包率來評估網路連接狀況,
圖示

如果用開頭提及的聲吶技術來類比,就會是這樣的一個對應關系:

形式
Ping命令的基本形式如下:
ping [-c 資料包個數] [-s 資料包大小] [主機名/IP地址]
例:
ping -c 5 -s 56 developer.android.google.cn
默認情況下,假如不指定資料包個數,Ping命令就會連續發送資料包,如果僅僅是為了進行連通性測驗,只需要指定3到5個即可,
而假如不指定資料包大小,則默認是56 bytes,
實作
Android支持直接使用命令列工具執行Ping命令,因此只需設定好引數,逐行讀取輸出內容即可:
/**
* Ping命令
*/
class Ping(
/** 目標主機域名/IP地址 */
private val host: String,
/** 資料包個數,默認連續發送 */
private val count: Int? = null,
/** 資料包大小,單位bytes,默認為56 bytes */
private val packetSize: Int? = null,
/** 資料包生存時間 */
private val ttl: Int? = null,
/** 超時間隔,單位s */
private val deadline: Int? = null
) {
/**
* ## 執行Ping命令
* 請注意,ping命令在Linux系統下的引數與在Windows系統下有差異,需要區分
* -c count ping指定次數后停止ping;
* -s packetsize 指定每次ping發送的資料位元組數,默認為“56位元組”+“28位元組”的ICMP頭,一共是84位元組;
*/
fun execute(callback: ExecuteCallback? = null): String {
val command = toString()
// 回呼輸出執行的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))
// 讀取首行輸出內容
var line = reader.readLine()
while (line != null) {
// 回呼執行程序的輸出內容
callback?.onExecuting(line)
// 記錄輸出行到結果字串
result.append(line).append("\n")
// 讀取下一行輸出內容
line = reader.readLine()
}
callback?.onCompleted(result.toString())
reader.close()
process.waitFor()
} catch (e: Exception) {
e.printStackTrace()
} finally {
reader?.close()
process?.destroy()
}
return result.toString()
}
/**
* ## 根據構造欄位將物體轉換為具體的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()
}
}
執行

分析
為了方便進行說明,我們在每一個結果行前添加了一個序號,
整個示例可以分為兩塊區域,從第1到6行為執行程序,從第7到8行為統計資訊,
執行程序
第1行表示的是向目標主機發送了5個56 bytes的資料包,
第2-6行數表示的是每個發送的回送請求資料包的執行結果,其中:
- [64 bytes]是從目標主機回傳的資料,之所以是64 bytes是由于加多了8 bytes的ICMP報頭,
- [icmp_seq]是ICMP報頭中包含的時序號,用于確定資料包到達的順序以及判斷資料包是否重復,
- [ttl]是資料包的生存時間,是time to live的縮寫,是為了防止資料包在路由選擇的程序中無休止地在網路中流動而設定的,
- [time]是資料包的往返時間,即從發送回送請求報文之后,到接收到回送應答報文之前經過的時間,
統計資訊
第7行表示的是資料包的傳輸接收情況以及丟包率,其中:
- [5 packets transmitted]表示傳輸了5個資料包
- [5 packets received]表示接收了5個資料包
- [0 packet loss]表示資料包的丟包率為0%,該數值越大,表示網路狀況越不穩定,最高為100%,即目標主機不可達,
第8行表示的是資料包往返時間的最小值/平均值/最大值,單位為毫秒(ms),數值越大,意味著網路延遲越嚴重;最小值與最大值之間的差值越大,意味著網路抖動越厲害,
TraceRoute

兩臺主機之間的通信,往往需要經過很多中間節點,如果其中某個節點出現問題,可能會導致資料無法送達,通過TraceRoute(跟蹤路由)我們可以定位資料是在哪個節點丟失的,
概述
TraceRoute命令是用于定位從源主機到目標主機所經過的路由,以及到達各個路由的資料包往返時間的工具,
該命令利用的是IP報頭的TTL值、ICMP超時報文以及ICMP埠不可達報文,TraceRoute每次都向相同的目標主機發送三次設定了相同TTL值的資料包,利用資料包被丟棄時路由器回傳ICMP超時報文獲知路由器的IP地址及資料包的往返時間,
流程
- 首先,向目標主機發送TTL值設為1的資料包,處理該資料包的第一個路由器會將TTL值減1,當TTL值變為0時,該資料包就會被丟棄,并發回一份ICMP超時報文,這樣就得到了該路徑中的第一個路由器的IP地址,
- 接著,發送TTL值設為2的資料包,該資料包在經過第二個路由器時就會被丟棄,這樣就得到了第二個路由器的IP地址,
- 持續這個程序,直至資料包到達目標主機,
- 為了確認資料包是否到達目標主機,TraceRoute使用了一個一般應用程式都不會使用的埠號(30000以上)作為目標埠號,這樣,當資料包到達目標主機時,目標主機就會回傳一個ICMP埠不可達報文,從而讓源主機可以確認資料包已經到達了目標主機,
圖示

形式
TraceRoute命令的基本形式如下:
traceroute [主機名/IP地址]
例:
traceroute developer.android.google.cn
實作
由于Android的非Root設備不支持直接使用命令列工具執行TraceRoute命令,因此我們改成以執行Ping命令并通過限定TTL的方式來模擬TraceRoute的程序,從而達到相等效果,缺點是模擬程序較慢,可能會頻繁出現超時情況,
具體的模擬程序如下:
- 對目標主機執行Ping命令,發送1個TTL值為1的資料包,第一個路由器將TTL值減1變為0,資料包被路由器丟棄,輸出以下結果行:
From 10.0.168.254: icmp_seq=1 Time to live exceeded
- 對該結果行進行正則運算式匹配,提取其中包含的路由器IP地址,如10.0.168.254;
- 對路由器IP地址執行Ping命令,發送3個大小為40 bytes的資料包,資料包到達該路由器,輸出以下結果行:
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
- 對該結果行進行正則運算式匹配,提取其中包含的資料包往返時間;
- 對目標主機再次執行Ping命令,發送1個TTL值設為2的資料包,在經過第二個路由器時被丟棄,同樣從結果行中提取出路由器IP地址,
- 對第二個路由器的IP地址執行Ping命令,同樣從結果行中提取出資料包往返時間,
- 持續這個程序直至資料包到達目標主機,輸出以下結果行:
64 bytes from 113.108.239.226: icmp_seq=1 ttl=115 time=33.0 ms
- 對目標主機IP地址執行Ping命令,同樣從結果行中提取出資料包往返時間,模擬結束,
- 如果程序中資料包超過5s沒有回傳,則會輸出空的結果行,因而提取不出路由器IP地址,轉而輸出[* * *],
- 當躍點數超過設立的最大30個躍點數后仍未到達目標主機,則模擬結束,
相應的流程圖如下:

具體代碼如下:
/**
* TraceRoute命令
* <p>
* 由于Android的非Root設備不支持直接使用命令列工具API執行TraceRoute命令,因此改用執行Ping命令
* 并通過限定TTL引數(IP包被路由器丟棄之前允許通過的最大網段數)來達到相等效果
* 路由器地址通過正則運算式匹配從Ping回應內容中截取,
* 路由耗時通過執行Ping命令前后時間戳對比估算
*/
class TraceRoute(
/** 目標主機域名 */
private val host: String
) {
var TAG = this::class.java.simpleName
companion object {
/** IP包被路由器丟棄之前允許通過的最大網段數 */
const val MAX_HOP = 30
/** 正則運算式-路由器IP地址 */
private const val REGEX_ROUTE_IP = "(?<=From )(?:[0-9]{1,3}\\.){3}[0-9]{1,3}"
/** 正則運算式-目標主機IP地址 */
private const val REGEX_HOST_IP = "(?<=from ).*(?=: icmp_seq=1 ttl=)"
/** 正則運算式-資料包往返時間 */
private const val REGEX_RRT = "(?<=time=).*?ms"
}
/**
* 執行Ping命令模擬TraceRoute流程
* -c count ping指定次數后停止ping;
* -t 設定TTL(Time To Live,生存時間)為指定的值,該欄位指定IP包被路由器丟棄之前允許通過的最大網段數;
*/
fun execute(callback: ExecuteCallback? = null) {
val command = toString();
callback?.onExecuting("% $command\n")
callback?.onExecuting("traceroute to $host, 30 hos max, 40 byte packets\n")
// 當前躍點數
var hop = 1
// 終止標識
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(".")
// 用正則運算式匹配回應內容行
val routerIpMatcher = matchRouterIp(pingResult)
if (routerIpMatcher.find()) { // 匹配到了路由器IP地址,列印路由器IP地址及到達該路由器的耗時
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++
}
}
/**
* 匹配并記錄資料包往返時間
*/
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)
/**
* 匹配資料包往返時間
*/
private fun matchRTT(input: CharSequence) = Pattern.compile(REGEX_RRT).matcher(input)
/**
* 匹配目標主機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"
}
}
執行

分析
第1行表示的是TraceRoute命令向目標主機發送最多30個躍點、40 bytes的資料包,
第2行起表示經過的路由器資訊,其中:
- 最前面的數字表示的是躍點數,與所發送的資料包的TTL值一致
- [172.16.88.1]表示的是經過的路由器IP地址
- [4.69ms 9.29ms 9.24ms]表示的是所發送的三個資料包分別的往返時間
- [*]表示的是沒有應答,當所發送的三個資料包中有任意一個超過5秒沒有應答時,則會以星號表示
如果目標主機可達,則會在到達某一躍點后結束,由此可知經過的路由器數量,如果目標主機不可達,則會在到達第30個躍點后結束,從而可知資料包被送到什么地方,
總結
為了有針對性地對網路進行優化,我們為App引入了一個輕量級的網路診斷模塊,主要借助的是Linux本身提供的檢測網路狀況的工具,在本篇中介紹的是Ping命令和TraceRoute命令,
- Ping命令用于檢測到目標主機是否可達,通過結合資料包的往返時間和丟包率我們能初步地評估網路狀況,包括網路延遲/網路抖動/網路穩定性等情況,
- TraceRoute命令用于定位到目標主機所經過的路由及其耗時,以定位網路故障發生的節點,由于Android的非Root設備不支持直接使用命令列工具執行TraceRoute命令,因此我們改用執行多次Ping命令來模擬TraceRoute的執行流程,
當然,網路狀況的復雜度往往超過我們的想象,還有很多這兩個命令不能覆寫到的故障場景,需要相應的工具才能進行排查,具體可以關注后續推出的文章,
「椎鋒陷陳」微信技術號現已開通,為了獲得第一手的技術文章推送,歡迎搜索關注!
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/292751.html
標籤:其他
