需求
1 客户端功能概述
- 自动监控各APP流量消耗 通过健康监控APP,自动采集DHU系统内各APP的流量信息,包括流量消耗量、日期、前台 or 后台、上行流量 or 下行流量、移动流量 or WLAN流量。将采集到的信息本地缓存后,周期性上传到服务端,服务端对流量数据进行大数据分析。
- 流量异常事件 服务端对上传的流量信息进行大数据分析后,按照自定义规则判定流量异常事件,例如对每个应用设置流量阈值,当流量超过阈值时判定为流量异常事件。服务端将异常事件下发给客户端,客户端收到异常事件消息后,抓取对应logcat日志并回传到服务端。
- 同步服务端监控策略 客户端周期性(待定)拉取服务端配置的监控策略和异常事件信息,并更新本地缓存的策略以及处理流程异常事件。
2 后台服务端 功能概述
-
远程配置监控参数 服务端可配置流量监控参数,包括功能开关(VIN码)、流量信息上传周期、流量异常事件等。
-
流量异常事件 服务端对上传的流量信息进行大数据分析,按照自定义规则判定流量异常事件,例如对每个应用设置流量阈值,当流量超过阈值时判定为流量异常事件,并将异常事件下发给客户端,客户端抓取异常事件日志并回传。
-
流量数据分析 服务端对上传的流量信息进行大数据分析,生成各类报表。例如APP流量排行、车机流量排行、单机流量详情、流量异常事件管理等。
技术调研
调研到两种方式获取流量信息。
1. 读取系统文件
- /proc/net/xt_qtaguid/stats:中包含所有PID(应用)的wlan(WiFi)和rmnet(2G/3G)、lo(本地)流量信息。并区分前台和后台流量。
- /proc/net/dev:统计系统总的流量信息
- /proc/uid_stat/uid/tcp_rcv:单个应用的流量统计信息
读取文件方案适用于Android原生,在车机系统中无相应文件,需继续调研车机系统中是否有类似文件信息。
验证结果
车机系统中无/proc/net/xt_qtaguid/stats文件。
通过Android源码查看,Android系统获取流量信息的方式最终走入native方法,使用的是Linux内核共享内存的方式,无法通过应用层查询到。只能使用方案2,通过AndroidAPI查询。
/**
* Reads the detailed UID stats based on the provided parameters
*
* @param limitUid the UID to limit this query to
* @param limitIfaces the interfaces to limit this query to. Use {@link312 * NetworkStats.INTERFACES_ALL} to select all interfaces
* @param limitTag the tags to limit this query to
* @return the NetworkStats instance containing network statistics at the present time.
*/
public NetworkStats readNetworkStatsDetail(
int limitUid, String[] limitIfaces, int limitTag) throws IOException {
18 // In order to prevent deadlocks, anything protected by this lock MUST NOT call out to other319 // code that will acquire other locks within the system server. See b/134244752.320 synchronized (mPersistentDataLock) {
// Take a reference. If this gets swapped out, we still have the old reference.322 final VpnInfo[] vpnArray = mVpnInfos;
// Take a defensive copy. mPersistSnapshot is mutated in some cases below324 final NetworkStats prev = mPersistSnapshot.clone();
if (USE_NATIVE_PARSING) {
final NetworkStats stats =
new NetworkStats(SystemClock.elapsedRealtime(), 0 /* initialSize */);
if (mUseBpfStats) {
try {
requestSwapActiveStatsMapLocked();
} catch (RemoteException e) {
throw new IOException(e);
}
// Stats are always read from the inactive map, so they must be read after the336 // swap337 if (nativeReadNetworkStatsDetail(stats, mStatsXtUid.getAbsolutePath(), UID_ALL,
INTERFACES_ALL, TAG_ALL, mUseBpfStats) != 0) {
throw new IOException("Failed to parse network stats");
}
// BPF stats are incremental; fold into mPersistSnapshot.343 mPersistSnapshot.setElapsedRealtime(stats.getElapsedRealtime());
mPersistSnapshot.combineAllValues(stats);
} else {
if (nativeReadNetworkStatsDetail(stats, mStatsXtUid.getAbsolutePath(), UID_ALL,
INTERFACES_ALL, TAG_ALL, mUseBpfStats) != 0) {
throw new IOException("Failed to parse network stats");
}
if (SANITY_CHECK_NATIVE) {
final NetworkStats javaStats = javaReadNetworkStatsDetail(mStatsXtUid,
UID_ALL, INTERFACES_ALL, TAG_ALL);
assertEquals(javaStats, stats);
}
mPersistSnapshot = stats;
}
} else {
mPersistSnapshot = javaReadNetworkStatsDetail(mStatsXtUid, UID_ALL, INTERFACES_ALL,
TAG_ALL);
}
NetworkStats adjustedStats = adjustForTunAnd464Xlat(mPersistSnapshot, prev, vpnArray);
// Filter return values366 adjustedStats.filter(limitUid, limitIfaces, limitTag);
return adjustedStats;
}
}
2. 使用Android原生API
Android原生API有TrafficStats(已过时)和NetworkStatsManager,NetworkStatsManager NetworkStatsManager - Android中文版 - API参考文档
提供的API可区分WIFI和MOBILE流量,可区分各UID(应用分配ID)流量,上行和下行流量。但不能满足的需求有以下两点。
-
不能区分前台流量和后台流量
- 解决办法: 通过周期查询前台应用 appProcess.importance == RunningAppProcessInfo.IMPORTANCE_BACKGROUND 判断周期内流量为前台还是后台,
- 缺陷: 周期内的应用前后台变化会导致前后台流量区分不够准确。
-
同UID的应用无法拆开细分(车机内大量系统应用使用shareUID方案,使用此方案的应用UID都为系统UID:1000)。使用系统版本 20.32.20.000001.230322_userdebug 统计,总共具有 android.permission.INTERNET 权限的应用79个,其中使用ShareUID 38个,非ShareUID 41个,意味着系统中将有38个应用无法被流量监控方案覆盖。
- 解决办法: 通过双向认证SDK 统计各应用上行和下行的https流量包大小,然后跨进程发送给健康监控收集数据@贺康 补充双向认证中流量监控方案。
- 缺陷: 双向认证增加跨进程通信和流量统计会变重,统计到的数据只能是https接入双向认证的流量。且HTTP,FTP、UDP等流量无法统计到。目前双向认证的aar包只有我们内部app在使用。
3. 开发aar包,各应用集成,应用层采集上行和下行流量
待定,各业务方需集成多余的包。
4. 通过VPN过滤的方式统计流量
待定,需framework层提供协助,具体总入口为/frameworks/base/services/core/java/com/android/server/ConnectivityService.java 的**linkPropertiesRestrictedForCallerPermissions** 方法。
1724 private LinkProperties linkPropertiesRestrictedForCallerPermissions(
1725 LinkProperties lp, int callerPid, int callerUid) {
1726 if (lp == null) return new LinkProperties();
1727
1728 // Only do a permission check if sanitization is needed, to avoid unnecessary binder calls.1729 final boolean needsSanitization =
1730 (lp.getCaptivePortalApiUrl() != null || lp.getCaptivePortalData() != null);
1731 if (!needsSanitization) {
1732 return new LinkProperties(lp);
1733 }
1734
1735 if (checkSettingsPermission(callerPid, callerUid)) {
1736 return new LinkProperties(lp, true /* parcelSensitiveFields */);
1737 }
1738
1739 final LinkProperties newLp = new LinkProperties(lp);
1740 // Sensitive fields would not be parceled anyway, but sanitize for consistency before the1741 // object gets parceled.1742 newLp.setCaptivePortalApiUrl(null);
1743 newLp.setCaptivePortalData(null);
1744 return newLp;
1745 }
此方法为网络请求进入的framework层总入口,可以根据pid判断对应的包的连接,根据持有的LinkProperties对象能否拿到上下行bytes统计,需要和framework层再深入对接。
设计方案
1. 流程图
加粗部分为周期性任务,需要特别关注性能和优化。
标黄部分为设想优化项。
2. 相关问题和待优化项
- 健康监控流量采集中的各周期和流量变化阈值的设定,还需要写个demo装入实车,收集一些具体使用情况后设计。且需要采用可配置的参数。
- 流量采集误差优化,建议后台根据实际上传总流量和统计到的流量信息,做算法优化。
- 和framework层沟通,能否监听前后台应用切换,不采用轮询方式优化
- wifi状态下,App也可能走的是流量,这个是由Google原生的网络评级决定的,可能产生部分误差
补充
1. 读取系统netstat文件和proc/net/tcp等文件方式获取
netstat -anep
net_anep.txt:
Active Internet connections (established and servers)
Proto Recv-Q Send-Q Local Address Foreign Address State User Inode PID/Program Name
tcp 0 0 127.0.0.1:5354 0.0.0.0:* LISTEN 1000 71150 3263/com.ts.carplay
tcp 0 0 0.0.0.0:7000 0.0.0.0:* LISTEN 1000 73130 3263/com.ts.carplay
tcp 0 0 127.0.0.1:12345 0.0.0.0:* LISTEN 0 85404 6398/admfotaapp
tcp 0 0 0.0.0.0:5000 0.0.0.0:* LISTEN 1000 73132 3263/com.ts.carplay
tcp 0 0 198.18.34.15:50351 198.18.34.2:40500 ESTABLISHED 0 27444 624/vsomeipd
tcp 0 0 127.0.0.1:12345 127.0.0.1:43932 ESTABLISHED 0 102886 6398/admfotaapp
tcp 0 0 198.18.34.15:50366 198.18.34.2:30580 ESTABLISHED 0 53345 624/vsomeipd
tcp 0 0 198.18.34.15:50340 198.18.34.2:30562 ESTABLISHED 0 27474 624/vsomeipd
tcp 70 0 127.0.0.1:53486 127.0.0.1:5354 ESTABLISHED 1000 68590 3263/com.ts.carplay
tcp 0 0 198.18.34.15:50380 198.18.34.2:30521 ESTABLISHED 0 54320 624/vsomeipd
tcp 0 0 127.0.0.1:5354 127.0.0.1:53486 ESTABLISHED 1000 73240 3263/com.ts.carplay
tcp 0 0 198.18.34.15:50346 198.18.34.2:30556 ESTABLISHED 0 27465 624/vsomeipd
tcp 0 0 198.18.34.15:50359 198.18.34.2:30559 ESTABLISHED 0 54350 624/vsomeipd
tcp 0 0 198.18.34.15:50343 198.18.34.2:30579 ESTABLISHED 0 27470 624/vsomeipd
tcp 0 0 198.18.34.15:50666 198.18.34.2:33332 ESTABLISHED 0 54308 624/vsomeipd
tcp 0 0 198.18.34.15:50364 198.18.34.2:30563 ESTABLISHED 0 27439 624/vsomeipd
tcp 0 0 127.0.0.1:53490 127.0.0.1:5354 ESTABLISHED 1000 73244 3263/com.ts.carplay
tcp 0 0 10.167.45.72:33670 36.150.102.196:7824 ESTABLISHED 10042 1332388 13498/com.bilibili.bilithings
tcp 0 0 127.0.0.1:43932 127.0.0.1:12345 ESTABLISHED 0 102882 6371/admsysi4deployapp
tcp 1179448 0 10.167.45.72:52164 114.97.122.72:8000 ESTABLISHED 10042 1438217 13822/com.bilibili.bilithings:ijkservice
tcp 0 0 198.18.34.15:50349 198.18.34.128:40000 ESTABLISHED 0 52069 624/vsomeipd
tcp 0 0 127.0.0.1:12345 127.0.0.1:43918 ESTABLISHED 0 85725 6398/admfotaapp
tcp 0 0 198.18.34.15:50352 198.18.34.2:30999 ESTABLISHED 0 53350 624/vsomeipd
tcp 0 0 198.18.34.15:50341 198.18.34.2:30553 ESTABLISHED 0 46398 624/vsomeipd
tcp 58624 0 10.167.45.72:55636 123.15.88.116:8000 ESTABLISHED 10042 1424024 13822/com.bilibili.bilithings:ijkservice
tcp 0 0 198.18.34.15:43822 198.18.34.2:30558 ESTABLISHED 0 54364 624/vsomeipd
tcp 0 0 198.18.34.15:50348 198.18.34.128:40002 ESTABLISHED 0 52065 624/vsomeipd
tcp 0 0 198.18.34.15:50381 198.18.34.128:40001 ESTABLISHED 0 52061 624/vsomeipd
tcp 0 0 198.18.34.15:45102 198.18.34.2:30538 ESTABLISHED 0 27450 624/vsomeipd
tcp 0 0 198.18.34.15:50369 198.18.34.2:30565 ESTABLISHED 0 50170 624/vsomeipd
tcp 0 0 198.18.34.15:50370 198.18.34.2:30561 ESTABLISHED 0 46372 624/vsomeipd
tcp 0 272 198.18.34.15:53548 198.18.34.2:30590 ESTABLISHED 0 41627 624/vsomeipd
tcp 0 0 127.0.0.1:5354 127.0.0.1:53490 ESTABLISHED 1000 73249 3263/com.ts.carplay
tcp.txt:
sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
0: 0100007F:14EA 00000000:0000 0A 00000000:00000000 00:00000000 00000000 1000 0 71150 1 0000000000000000 100 0 0 10 0
1: 00000000:1B58 00000000:0000 0A 00000000:00000000 00:00000000 00000000 1000 0 73130 1 0000000000000000 100 0 0 10 0
2: 0100007F:3039 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 85404 1 0000000000000000 100 0 0 10 0
3: 00000000:1388 00000000:0000 0A 00000000:00000000 00:00000000 00000000 1000 0 73132 1 0000000000000000 100 0 0 10 0
4: 0F2212C6:C4AF 022212C6:9E34 01 00000000:00000000 02:0000015B 00000000 0 0 27444 2 0000000000000000 20 4 0 10 -1
5: 0100007F:3039 0100007F:AB9C 01 00000000:00000000 00:00000000 00000000 0 0 102886 1 0000000000000000 20 4 30 10 -1
6: 0F2212C6:C4BE 022212C6:7774 01 00000000:00000000 02:0000010E 00000000 0 0 53345 2 0000000000000000 20 0 0 10 -1
7: 482DA70A:C676 A173FC3A:1F40 01 00000000:0000E660 00:00000000 00000000 10042 0 1477214 1 0000000000000000 24 11 0 10 -1
8: 0F2212C6:C4A4 022212C6:7762 01 00000000:00000000 02:00000041 00000000 0 0 27474 2 0000000000000000 51 0 0 10 -1
9: 0100007F:D0EE 0100007F:14EA 01 00000000:00000046 00:00000000 00000000 1000 0 68590 1 0000000000000000 20 5 28 10 -1
10: 0F2212C6:C4CC 022212C6:7739 01 00000000:00000000 02:0000015B 00000000 0 0 54320 2 0000000000000000 20 4 0 10 -1
11: 0100007F:14EA 0100007F:D0EE 01 00000000:00000000 00:00000000 00000000 1000 0 73240 1 0000000000000000 20 4 28 10 -1
12: 0F2212C6:C4AA 022212C6:775C 01 00000000:00000000 02:0000015B 00000000 0 0 27465 2 0000000000000000 20 4 0 10 -1
13: 0F2212C6:C4B7 022212C6:775F 01 00000000:00000000 02:0000008E 00000000 0 0 54350 2 0000000000000000 22 0 0 10 -1
14: 0F2212C6:C4A7 022212C6:7773 01 00000000:00000000 02:0000010E 00000000 0 0 27470 2 0000000000000000 20 0 0 10 -1
15: 0F2212C6:C5EA 022212C6:8234 01 00000000:00000000 02:0000010E 00000000 0 0 54308 2 0000000000000000 20 0 0 10 -1
16: 0100007F:AB8E 0100007F:3039 01 00000000:00000000 00:00000000 00000000 1000 0 85721 1 0000000000000000 20 4 30 10 -1
17: 0F2212C6:C4BC 022212C6:7763 01 00000000:00000000 02:0000010E 00000000 0 0 27439 2 0000000000000000 20 0 0 10 -1
18: 0100007F:D0F2 0100007F:14EA 01 00000000:00000000 00:00000000 00000000 1000 0 73244 1 0000000000000000 20 4 28 10 -1
19: 482DA70A:8386 C4669624:1E90 01 00000000:00000000 00:00000000 00000000 10042 0 1332388 1 0000000000000000 23 4 2 10 -1
20: 0100007F:AB9C 0100007F:3039 01 00000000:00000000 00:00000000 00000000 0 0 102882 1 0000000000000000 20 4 30 10 -1
21: 0F2212C6:C4AD 802212C6:9C40 01 00000000:00000000 02:00000027 00000000 0 0 52069 2 0000000000000000 20 4 0 10 -1
22: 0100007F:3039 0100007F:AB8E 01 00000000:00000000 00:00000000 00000000 0 0 85725 1 0000000000000000 20 4 30 10 -1
23: 0F2212C6:C4B0 022212C6:7917 01 00000000:00000000 02:0000010E 00000000 0 0 53350 2 0000000000000000 20 0 0 10 -1
24: 0F2212C6:C4A5 022212C6:7759 01 00000000:00000000 02:0000015A 00000000 0 0 46398 2 0000000000000000 20 4 0 10 -1
25: 482DA70A:BC80 FC058524:1180 01 00000000:0010297C 00:00000000 00000000 10042 0 1453653 1 0000000000000000 26 4 0 10 -1
26: 0F2212C6:AB2E 022212C6:775E 01 00000000:00000000 02:0000015A 00000000 0 0 54364 2 0000000000000000 20 4 0 10 -1
27: 0F2212C6:C4AC 802212C6:9C42 01 00000000:00000000 02:00000041 00000000 0 0 52065 2 0000000000000000 20 4 0 10 -1
28: 0F2212C6:C4CD 802212C6:9C41 01 00000000:00000000 02:000001C1 00000000 0 0 52061 2 0000000000000000 20 4 1 2 7
29: 0F2212C6:B02E 022212C6:774A 01 00000000:00000000 02:000001C1 00000000 0 0 27450 2 0000000000000000 20 4 0 10 -1
30: 0F2212C6:C4C1 022212C6:7765 01 00000000:00000000 02:0000010E 00000000 0 0 50170 2 0000000000000000 20 0 0 10 -1
31: 0F2212C6:C4C2 022212C6:7761 01 00000000:00000000 02:0000010E 00000000 0 0 46372 2 0000000000000000 20 0 0 10 -1
32: 0F2212C6:D12C 022212C6:777E 01 00000000:00000000 02:00028E76 00000000 0 0 41627 2 0000000000000000 24 0 0 18 18
33: 0100007F:14EA 0100007F:D0F2 01 00000000:00000000 00:00000000 00000000 1000 0 73249 1 0000000000000000 20 4 26 10 -1
netstat中的inode对应tcp文件中的inode,能对应到具体应用(包名和pid),但两个文件的tx和rx数据都不能统计具体流量上行和下行的大小。
2. 修改framework代码
1. Android网络调用时序(TCP)
时序图如下:
2. Android网络调用流程
- TCP包含http和https,https比http仅多一步安全证书校验。
- 应用层调用一般采用三种方式,HttpURLConnection(Android原生),HttpClient(Android老版),OKHttp。
- 应用层的http和https、FTP调用最终都进行到RealConnection,后调用Socket进行connect然后通信。
- UDP也是使用Socket进行通信(待验证)
- Socket中使用BufferSink(OutputStream),BufferSource(InputStream)进行流的操作(Android11)
3. 流量监控前置条件
-
RealConnection、BufferSink、BufferSource都属于aosp-caf/external/okhttp/包下,打包后都在/apex/com.android.art/javalib/okhttp.jar中
-
Android应用使用到okhttp.jar时,使用的是Android系统javalib中找到的第一个同名类
-
且这些类加载和运行在app的进程中
/** Does all the work necessary to build a full HTTP or HTTPS connection on a raw socket. */
private void connectSocket(int connectTimeout, int readTimeout, int writeTimeout,
ConnectionSpecSelector connectionSpecSelector) throws IOException {
rawSocket.setSoTimeout(readTimeout);
try {
Platform.get().connectSocket(rawSocket, route.getSocketAddress(), connectTimeout);
} catch (ConnectException e) {
throw new ConnectException("Failed to connect to " + route.getSocketAddress());
}
source = Okio.buffer(Okio.source(rawSocket));
sink = Okio.buffer(Okio.sink(rawSocket));
if (route.getAddress().getSslSocketFactory() != null) {
connectTls(readTimeout, writeTimeout, connectionSpecSelector);
} else {
protocol = Protocol.HTTP_1_1;
socket = rawSocket;
}
if (protocol == Protocol.SPDY_3 || protocol == Protocol.HTTP_2) {
socket.setSoTimeout(0); // Framed connection timeouts are set per-stream.
FramedConnection framedConnection = new FramedConnection.Builder(true)
.socket(socket, route.getAddress().url().host(), source, sink)
.protocol(protocol)
.build();
framedConnection.sendConnectionPreface();
// Only assign the framed connection once the preface has been sent successfully.
this.framedConnection = framedConnection;
}
}
4. 通过hook Socket方式流量监控方案设计
- 新建两个类BufferSinkStatistics(extents BufferSink),BufferSourceStatistics(extents BufferSource),重写Buffer的in和out方法,添加统计逻辑。这两个类负责统计Socket的上行流量和下行流量。
- 新建两个类中使用反射获取App的包名,将统计数据携带包名跨进程传入server端。
- 替换RealConnection(java.net.Socket)中的BufferSink和BufferSource为重写的两个类,使所有上下行的网络请求流都会进行数据统计。
- 替换后的编译/apex/com.android.art/javalib/okhttp.jar,所有在此系统中运行的应用,使用到http、https、(UDP)的请求,都会纳入统计。
此方案为理论设计,尚待验证。
缺陷:
- 需要频繁进行流量计算,每一个socket都需要计算,计算后还需要进行跨进程通信进行统计。
- 是否会统计到内网上下行流量,需要验证。
参考资料
- Android UID的分配、查看及相关知识:blog.csdn.net/lee17112217…
- 两种流量监控方案:blog.csdn.net/dexFeng/art…
- /proc/net/xt_qtaguid/stats文件监测流量信息:www.cnblogs.com/weilf/p/918…
- TrafficStats方案获取流量信息:blog.csdn.net/weixin_3418…
- 车机网络状态监控实测: blog.csdn.net/android_cai…
- Android网络评分机制:blog.csdn.net/weixin_5001…