通過 JNI 移植一個 tracepath 追蹤路由數(shù)據(jù)鏈給你的應(yīng)用
背景
Linux 的 tracepath 指令可以追蹤數(shù)據(jù)到達(dá)目標(biāo)主機(jī)的路由信息,同時還能夠發(fā)現(xiàn) MTU 值。它跟蹤路徑到目的地,沿著這條路徑發(fā)現(xiàn) MTU。它使用 UDP 端口或一些隨機(jī)端口。它類似于 Traceroute,只是不需要超級用戶權(quán)限,并且沒有花哨的選項(xiàng)。
Android 也是移植的它,其源碼放置位置在platform/external/iputils/tracepath6.c。我們之所以直接移植tracepath6.c而不是tracepath.c的原因是 tracepath6 支持 IPV6 和 IPV4 兩種模式,而tracepath.c僅僅支持 IPV4,所以一把梭后我們直接完美兼容了兩種。
最近剛好在調(diào)研網(wǎng)絡(luò)診斷覆蓋能力,所以順手移植了下它,大致效果如下。
demo 效果
移植后開箱即用地址https://github.com/yanbober/android-tracepath,喜歡就給個小星星唄,一閃一閃亮晶晶。
開始移植
本想撿個現(xiàn)成,去看了platform/external/iputils/tracepath6.c源碼發(fā)現(xiàn)這貨直接是寫死默認(rèn)假定 IPV6 模式的,需要不同模式的話需要自己執(zhí)行命令時傳遞模式參數(shù),不支持自己動態(tài)識別模式,所以需要移植改造。
定義 Java 接口約定
為了方便給 app 使用,需要通過 JNI 包裝到 Java 層接口,約定如下:
- package cn.yan.android.tracepath;
- public final class AndroidTracePath {
- static {
- System.loadLibrary("tracepath-compat");
- }
- private StateListener mStateListener;
- public AndroidTracePath(StateListener stateListener) {
- this.mStateListener = stateListener;
- }
- //業(yè)務(wù)方調(diào)用開始 tracepath 的方法,hostName 是你的域名或者 ip
- public void startTrace(String hostName) {
- nativeInit();
- nativeStartTrace(hostName);
- }
- public native void nativeInit();
- public native void nativeStartTrace(String hostName);
- public void nativeOnStart() {
- if (null != mStateListener) {
- mStateListener.onStart();
- }
- }
- public void nativeOnUpdate(String update) {
- if (null != mStateListener) {
- mStateListener.onUpdate(update);
- }
- }
- public void nativeOnEnd() {
- if (null != mStateListener) {
- mStateListener.onEnd();
- }
- }
- //tracepath 回調(diào)狀態(tài)
- public interface StateListener {
- void onStart();
- void onUpdate(String update);
- void onEnd();
- }
- }
接著就是對應(yīng) JNI 層的接口了,這里沒啥說的,都是老套路,一鍵生成也罷,動態(tài)映射也罷,隨意擺弄,反正最終能調(diào)用到tracepath6.c源碼就行。
我們重點(diǎn)是改造tracepath6.c,由于這玩意默認(rèn)編譯后是一個可執(zhí)行文件,我們通過 cmake 需要當(dāng)作依賴編譯,所以他的 main 方法入口就不再適合我們了,我們需要進(jìn)行改造(換個方法名即可),如下:
- //int main(int argc, char **argv) 替換為 tracepath 函數(shù)
- int tracepath(int argc, char **argv)
這玩意需要給我們的 JNI 包裝接口(apicompat.c)調(diào)用,所以我們給他新建一個頭文件把這個方法報漏一下,如下:
- //tracepath6.h
- #ifndef ANDROIDTRACEPATH_TRACEPATH6_H
- #define ANDROIDTRACEPATH_TRACEPATH6_H
- int tracepath(int argc, char** arg);
- #endif //ANDROIDTRACEPATH_TRACEPATH6_H
手機(jī)連著 ipv4 的網(wǎng)絡(luò)一運(yùn)行,臥槽,跪了,定位代碼發(fā)現(xiàn)tracepath6.c里面是寫死模式的,需要動態(tài)適配,改造點(diǎn)如下:
- ......
- sa_family_t family = AF_UNSPEC; //把這里初值A(chǔ)F_INET6換成AF_UNSPEC
- ......
- int tracepath(int argc, char **argv)
- {
- ......
- memset(&hints, 0, sizeof(hints));
- hints.ai_family = AF_UNSPEC; //把這里family變量換成AF_UNSPEC
- hints.ai_socktype = SOCK_DGRAM;
- hints.ai_protocol = IPPROTO_UDP;
- #ifdef USE_IDN
- hints.ai_flags = AI_IDN;
- #endif
- gai = getaddrinfo(argv[0], pbuf, &hints, &ai0);
- if (gai) {
- fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(gai));
- return 1;
- }
- fd = -1;
- for (ai = ai0; ai; ai = ai->ai_next) {
- //這里一段判斷family的邏輯刪掉
- if (ai->ai_family != AF_INET6 &&
- ai->ai_family != AF_INET)
- continue;
- family = ai->ai_family;
- fd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
- if (fd < 0)
- continue;
- memcpy(&target, ai->ai_addr, sizeof(target));
- targetlen = ai->ai_addrlen;
- break;
- }
- ......
- }
可以看到,貫穿全流程的模式是通過一個全局的 family 變量類型來維護(hù)的,默認(rèn)改為 AF_UNSPEC 后就會自動探測類型,匹配到 AF_INET6 或者 AF_INET 則走自己對應(yīng)邏輯,這樣就能完美兼容 IPV6 和 IPV4 了。
到此運(yùn)行能出結(jié)果了,但是 tracepath 的打印結(jié)果沒法輸出到 Java 層回調(diào)中,我們需要繼續(xù)改造。常規(guī)想法就是一個一個換掉tracepath6.c里面的 printf 函數(shù)為 JNI 回調(diào) Java 方法實(shí)現(xiàn),這樣比較麻煩。我們還是采用了一把梭的模式,如下:
- //tracepath6.c
- #include "./../apicompat.h"
- #define printf(...) callbackOnUpdate(__VA_ARGS__)
如上通過一個宏直接替換 printf 為我們 JNI 接口層的 callbackOnUpdate 函數(shù),這個函數(shù)的作用就是調(diào)用 Java 方法,這樣就能把數(shù)據(jù)傳遞回去了。
到此基本 ok 了,還差最后的優(yōu)化,你也看到了,tracepath6.c編寫初衷是一個可執(zhí)行程序,現(xiàn)在把它移植成 so,所以我們不能在直接 exit 了,相關(guān)地方都需要一把梭的替換為 return 解決問題,到此完美解決所有。