Cronet雙網(wǎng)卡邏輯

場(chǎng)景

為了追求極致的用戶(hù)體驗(yàn),每個(gè)app都耗盡腦汁想盡辦法優(yōu)化自身,特別是網(wǎng)絡(luò)卡頓時(shí)候的體驗(yàn),期待在wifi卡頓情況下,通過(guò)白名單控制域名走用戶(hù)的蜂窩網(wǎng)絡(luò)通道。這期主要分享下wifi連接情況下,如何用蜂窩網(wǎng)發(fā)送相應(yīng)的請(qǐng)求。后續(xù)還會(huì)著重分析一個(gè)問(wèn)題,卡了自己好久的問(wèn)題,后面也是看了android官方文檔解釋才明白為啥出這個(gè)問(wèn)題。

介紹

一、系統(tǒng)Api
首先我們要了解下系統(tǒng)的api實(shí)現(xiàn),主要就是Network#bindSocket方法,這個(gè)方法是獲取對(duì)應(yīng)通道的network對(duì)象,再把客戶(hù)端網(wǎng)絡(luò)傳輸?shù)膕ocket綁定到對(duì)應(yīng)通道的network對(duì)象上,從而把對(duì)應(yīng)的網(wǎng)絡(luò)請(qǐng)求切換到不同通道上,實(shí)現(xiàn)雙網(wǎng)卡交互的邏輯。


二、Cronet底層如何實(shí)現(xiàn)雙通道邏輯
了解系統(tǒng)的Api后,我們對(duì)整體android的系統(tǒng)Api有個(gè)認(rèn)知,那接下來(lái)我根據(jù)源碼和大家簡(jiǎn)單分析下Cronet底層socket如何綁定對(duì)應(yīng)的網(wǎng)絡(luò)通道的??聪旅鍯ronet的代碼,可以看出Cronet c++底層也是加載native庫(kù),通過(guò)打開(kāi)Android底層的文件句柄,直接訪(fǎng)問(wèn)Network#bindSocket的C++方法,所以說(shuō)為什么我在第一步先介紹Android系統(tǒng)的Api,各種框架八九不離十都會(huì)和系統(tǒng)的api打交道的,只不過(guò)是分走java層還是走C++層而已。

//大于等于android 6.0
MarshmallowSetNetworkForSocket GetMarshmallowSetNetworkForSocket() {
  // On Android M and newer releases use supported NDK API.
  base::FilePath file(base::GetNativeLibraryName("android"));
  // See declaration of android_setsocknetwork() here:
  // http://androidxref.com/6.0.0_r1/xref/development/ndk/platforms/android-M/include/android/multinetwork.h#65
  // Function cannot be called directly as it will cause app to fail to load on
  // pre-marshmallow devices.
  void* dl = dlopen(file.value().c_str(), RTLD_NOW);
  return reinterpret_cast<MarshmallowSetNetworkForSocket>(
      dlsym(dl, "android_setsocknetwork"));
}

//小于android 6.0
LollipopSetNetworkForSocket GetLollipopSetNetworkForSocket() {
  // On Android L use setNetworkForSocket from libnetd_client.so. Android's netd
  // client library should always be loaded in our address space as it shims
  // socket().
  base::FilePath file(base::GetNativeLibraryName("netd_client"));
  // Use RTLD_NOW to match Android's prior loading of the library:
  // http://androidxref.com/6.0.0_r5/xref/bionic/libc/bionic/NetdClient.cpp#37
  // Use RTLD_NOLOAD to assert that the library is already loaded and avoid
  // doing any disk IO.
  void* dl = dlopen(file.value().c_str(), RTLD_NOW | RTLD_NOLOAD);
  return reinterpret_cast<LollipopSetNetworkForSocket>(
      dlsym(dl, "setNetworkForSocket"));
}

Cronet支持雙通道鏈接
Android 6.0以上的C++方法android_setsocknetwork方法


三、Cronet如何使用的
這里從tcp協(xié)議分析Cronet如何指定網(wǎng)絡(luò)通道。下面代碼可以看出函數(shù)參數(shù)需要一個(gè)叫NetworkHandle一個(gè)對(duì)象,其實(shí)這個(gè)對(duì)象就是Java的Network的id,這個(gè)Android系統(tǒng)api也有方法可以參考copy一份的,那么到這里整個(gè)鏈路都比較清晰了。要Cronet走不同的網(wǎng)絡(luò)通道,首先要獲取對(duì)應(yīng)通道的Network的id,其次通過(guò)id在Cronet決定走tcp/udp協(xié)議后,本地創(chuàng)建的socket綁定對(duì)應(yīng)的id,就可以實(shí)現(xiàn)不同網(wǎng)絡(luò)通道傳輸啦。

//tcp socket bind network
int TCPSocketPosix::BindToNetwork(handle::NetworkHandle network) {
  DCHECK(IsValid());
  DCHECK(!IsConnected());
  VLOG(1) << "cellular BindToNetwork: " << network;
#if defined(OS_ANDROID)
  return android::BindToNetwork(socket_->socket_fd(), network);
#else
  NOTIMPLEMENTED();
  return ERR_NOT_IMPLEMENTED;
#endif  // #if defined(OS_ANDROID)
}

TCP的BindToNetwork的代碼


四、如何獲取對(duì)應(yīng)的Network的id
我這里其實(shí)不是最好的實(shí)現(xiàn),因?yàn)槲乙彩强淳W(wǎng)上和官網(wǎng)的文檔后,只能想到這種方式去獲取。但這個(gè)方式有個(gè)致命缺陷,需要用戶(hù)同意打開(kāi)某個(gè)高級(jí)權(quán)限才可以獲取到對(duì)應(yīng)的Network的id。但是微信是可以做到不需要用戶(hù)打開(kāi)權(quán)限,網(wǎng)絡(luò)差的情況下直接走蜂窩網(wǎng)通道拉取消息,這個(gè)我暫時(shí)沒(méi)想到能咋整。
下面代碼需要申請(qǐng)權(quán)限:
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.WRITE_SETTINGS"/>
主要是WRITE_SETTINGS在高版本手機(jī)需要用戶(hù)主動(dòng)選擇打開(kāi)才行,動(dòng)態(tài)獲取Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS)就可以跳轉(zhuǎn)到設(shè)置頁(yè)面,讓用戶(hù)去打開(kāi)了。

public class NetworkHelper {
    private static final String TAG = "NetworkIdHelper";
    /**
     * Network handle representing the default network. To be used when a network has not been * explicitly set.
     */
    private static final long DEFAULT_NETWORK_HANDLE = -1;
    private static NetworkCallbackImpl _networkCallback = null;
    private static Network _network = null;
    private static long _networkId = DEFAULT_NETWORK_HANDLE;
    private static ConnectivityManager _connectivityManager = null;

    public static void init(Context context) {
        Log.i(TAG, "init");
        _connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
    }

    public static void openMobileNetwork(AvailableNetworkCallback callback) {
        Log.i(TAG, "openMobileNetwork");
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            if (_connectivityManager != null) {
                try {
                    NetworkRequest.Builder builder = new NetworkRequest.Builder();
                    builder.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
                    NetworkRequest request =
                            new NetworkRequest.Builder()
                                    // 設(shè)置指定的?絡(luò)傳輸類(lèi)型(蜂窩傳輸) 等于?機(jī)?絡(luò)
                                    .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
                                    // 設(shè)置感興趣的?絡(luò)功能
                                    .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET).build();
                    _networkCallback = new NetworkCallbackImpl(_connectivityManager, callback);
                    _connectivityManager.requestNetwork(request, _networkCallback);
                } catch (Exception e) {
                    Log.i(TAG, "openMobileNetwork error: " + e.getMessage());
                }
            }
        }
    }

    public static void closeMobileNetwork() {
        Log.i(TAG, "closeMobileNetwork");
        if (_connectivityManager != null) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                _connectivityManager.unregisterNetworkCallback(_networkCallback);
            }
        }
    }

    private static synchronized long getNetworkToNetId() {
        if (_network != null) {
            _networkId = Build.VERSION.SDK_INT >= 23 ? _network.getNetworkHandle() :
                    (long) Integer.parseInt(_network.toString());
        }
        return _networkId;
    }

    public static long networkToNetId() {
        if (_networkId == DEFAULT_NETWORK_HANDLE) {
            _networkId = getNetworkToNetId();
        }
        Log.i(TAG, "cellular networkToNetId _networkId: " + _networkId);
        return _networkId;
    }

    public interface AvailableNetworkCallback {
        void onAvailable(long networkId);
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    private static class NetworkCallbackImpl extends ConnectivityManager.NetworkCallback {
        final ConnectivityManager connectivityManager;
        AvailableNetworkCallback availableNetworkCallback;

        public NetworkCallbackImpl(ConnectivityManager connectivityManager, AvailableNetworkCallback callback) {
            this.connectivityManager = connectivityManager;
            this.availableNetworkCallback = callback;
        }

        @Override
        public void onAvailable(Network network) {
            super.onAvailable(network);
            Log.i(TAG, "Mobile Network Available");
            _network = network;
            networkToNetId();
            if (_networkId != DEFAULT_NETWORK_HANDLE) {
                availableNetworkCallback.onAvailable(_networkId);
            }
        }

        @Override
        public void onLost(Network network) {
            super.onLost(network);
            Log.i(TAG, "Mobile Network onLost");
            _network = null;
        }
    }
}

問(wèn)題

在實(shí)際開(kāi)發(fā)過(guò)程中遇到個(gè)非常棘手問(wèn)題,在某些國(guó)內(nèi)手機(jī)上,獲取到對(duì)應(yīng)的network的id,給了Cronet綁定后,Cronet使用的Android系統(tǒng)回調(diào)會(huì)拋出蜂窩網(wǎng)onLost的回調(diào),然后后續(xù)再繼續(xù)bindSocket的話(huà)就會(huì)一直失敗返回-1,這個(gè)問(wèn)題搞了我一周都沒(méi)找到解決辦法。
后面通過(guò)閱讀官網(wǎng)的文檔才發(fā)現(xiàn),雙通道的實(shí)現(xiàn)需要依賴(lài)手機(jī)打開(kāi)了 始終開(kāi)啟移動(dòng)數(shù)據(jù)設(shè)置這個(gè)開(kāi)關(guān),如果沒(méi)有打開(kāi)這個(gè)配置,一段時(shí)間后移動(dòng)網(wǎng)絡(luò)就會(huì)斷開(kāi)連接,常規(guī)網(wǎng)絡(luò)回調(diào)將收到對(duì) onLost() 的調(diào)用。這些是來(lái)自官網(wǎng)的解釋?zhuān)C明如果不開(kāi)啟這個(gè)開(kāi)關(guān)是無(wú)法實(shí)現(xiàn)雙網(wǎng)卡的邏輯的。
雙網(wǎng)卡官方解答地址

總結(jié)

  1. 遇到問(wèn)題要多點(diǎn)看回android的開(kāi)發(fā)文檔,說(shuō)不定就能找到你的答案了。
  2. 在學(xué)習(xí)中進(jìn)步,在實(shí)踐中收獲知識(shí)。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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