1. 程式人生 > >Android多網路機制淺析

Android多網路機制淺析

Android從4.2版本開始,逐步支援了多網路功能。相關的api能夠讓開發者選擇想要的網路裝置訪問,並且各個裝置之間的切換和繫結也越來越方便。

判斷網路連通性機制

從Android4.2.2開始,引入了一個叫“captive portal” detection的機制,用來判斷當前網路是否連線上網際網路,是不是需要身份驗證的公共網路。以5.0版本原始碼中的程式碼為例:

在NetworkMonitor類中的isCaptivePortal方法:

/**
     * Do a URL fetch on a known server to see if we get the data we expect.
     * Returns HTTP response code.
     */
    private int isCaptivePortal() {
        if (!mIsCaptivePortalCheckEnabled) return 204;
        HttpURLConnection urlConnection = null;
        int httpResponseCode = 599;
        try {
            URL url = new URL("http", mServer, "/generate_204");
            if (DBG) {
                log("Checking " + url.toString() + " on " +
                        mNetworkAgentInfo.networkInfo.getExtraInfo());
            }
            urlConnection = (HttpURLConnection) mNetworkAgentInfo.network.openConnection(url);
            urlConnection.setInstanceFollowRedirects(false);
            urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS);
            urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS);
            urlConnection.setUseCaches(false);
            // Time how long it takes to get a response to our request
            long requestTimestamp = SystemClock.elapsedRealtime();
            urlConnection.getInputStream();
            // Time how long it takes to get a response to our request
            long responseTimestamp = SystemClock.elapsedRealtime();
            httpResponseCode = urlConnection.getResponseCode();
            if (DBG) {
                log("isCaptivePortal: ret=" + httpResponseCode +
                        " headers=" + urlConnection.getHeaderFields());
            }
            // NOTE: We may want to consider an "HTTP/1.0 204" response to be a captive
            // portal.  The only example of this seen so far was a captive portal.  For
            // the time being go with prior behavior of assuming it's not a captive
            // portal.  If it is considered a captive portal, a different sign-in URL
            // is needed (i.e. can't browse a 204).  This could be the result of an HTTP
            // proxy server.
            // Consider 200 response with "Content-length=0" to not be a captive portal.
            // There's no point in considering this a captive portal as the user cannot
            // sign-in to an empty page.  Probably the result of a broken transparent proxy.
            // See http://b/9972012.
            if (httpResponseCode == 200 && urlConnection.getContentLength() == 0) {
                if (DBG) log("Empty 200 response interpreted as 204 response.");
                httpResponseCode = 204;
            }
            sendNetworkConditionsBroadcast(true /* response received */, httpResponseCode == 204,
                    requestTimestamp, responseTimestamp);
        } catch (IOException e) {
            if (DBG) log("Probably not a portal: exception " + e);
            if (httpResponseCode == 599) {
                // TODO: Ping gateway and DNS server and log results.
            }
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
        }
        return httpResponseCode;
    }

簡單的說,原理就是訪問google的clients3.google.com/generate_204地址,當返回204程式碼,或者是200並且內容ContentLength是0時,判斷網路已連通。否則就是未連上網際網路,或者需要身份驗證的公共網路。為什麼返回200並且ContentLength為0時也認為是連上網際網路了呢?因為需要身份驗證的系統返回長度不可能是0,可能是因為代理的緣故導致狀態碼錯誤。

使用Android 5.0版本以上原生系統的同學會發現,狀態列上wifi或者cell的圖示上會有個歎號。這是因為谷歌被牆,http的response code自然就不會是204了。

adb shell "settings put global captive_portal_server noisyfox.cn"

也可以完全禁止掉這個檢測:

adb shell "settings put global captive_portal_detection_enabled 0"

但這樣會有一個問題,就是如果連線上一個需要網頁驗證的wifi,就沒有辦法自動跳到登陸介面了。

多網路連線機制

一直以來Android系統的訪問網路的型別都是不可選的,連線WiFi走WiFi,否則走Cellular。但從5.0版本開始引入了多網路連線機制,引用:

Android 5.0 provides new multi-networking APIs that let your app dynamically scan for available networks with specific capabilities, and establish a connection to them. This functionality is useful when your app requires a specialized network, such as an SUPL, MMS, or carrier-billing network, or if you want to send data using a particular type of transport protocol.

在api21中新加入了方法

ConnectivityManager.setProcessDefaultNetwork

可以將程序繫結到特定的網路,這樣即使是wifi開啟的情況下也可以使用Cellular訪問網路了。但是當WiFi連線時,不管WiFi是否聯網,系統預設依然是會選擇走WiFi,只有手動繫結app程序才能切到Cellular網路。

自動切換網路機制

到了Android6.0,在網路方面又有如下變化。如change note中所說,在之前版本的系統中,當連線到WiFi時,其他型別的網路就會斷開;而在6.0中,其他網路不會斷開,雖然會優先從WiFi訪問,但當檢測到連線的WiFi沒有聯網而其他網路(例如Cellular)是聯網的情況下,所有資料的訪問會走到Cellular網路。引用:

This release introduces the following behavior changes to the Wi-Fi and networking APIs.

  • Your apps can now change the state of WifiConfiguration objects only if you created these objects. You are not permitted to modify or deleteWifiConfiguration objects created by the user or by other apps.
  • Previously, if an app forced the device to connect to a specific Wi-Fi network by using enableNetwork() with the disableAllOthers=true setting, the device disconnected from other networks such as cellular data. In This release, the device no longer disconnects from such other networks. If your app’s targetSdkVersion is “20” or lower, it is pinned to the selected Wi-Fi network. If your app’s targetSdkVersion is “21” or higher, use the multinetwork APIs (such as openConnection(), bindSocket(), and the new bindProcessToNetwork() method) to ensure that its network traffic is sent on the selected network.

對大部分開發者來說,這個特性似乎沒什麼用。在大部分情況下,app不需要關心繫統網路如何切換,只要能夠成功訪問並且得到當前訪問的型別就夠了。

但是對某些特殊的應用場景,有了這個特性就慘了。例如:你的app需要訪問到本地的網路服務(例如一臺沒有接入網際網路的路由器),如果cellular是關閉的,那可以正常訪問;而如果cellular是開啟的,所有的資料都預設走到cellular,就無法訪問本地的服務了。

從這個例子看6.0的系統是有些傻,明明可以做到兩個網路同時連通,但卻很“智慧”地選擇了一個連線到網際網路的網路去訪問。當然解決這個問題並不難,使用系統提供的bindsocket和bindprocesstonetwork就夠了。

多網路同時訪問

正如前一節所說,要同時訪問多個網路裝置,需要拿到網路對應的Network,並且跟訪問的socket做繫結。以網路框架Okhttp和主流圖片載入框架picasso與glide為例,用以下幾個步驟便可實現網路繫結:

獲取Network

參照如下的程式碼片段,首先構造一個NetworkRequest.Builder,包含wifi但不包含網路訪問;其次為ConnectivityManager註冊監聽;當onAvailable回撥獲取到Network時,記錄下當前連線的wifi。

final ConnectivityManager connManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkRequest.Builder request = new NetworkRequest.Builder()
                .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
                .removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
connManager.registerNetworkCallback(request.build(), new ConnectivityManager.NetworkCallback() {
            @Override
            public void onAvailable(Network network) {
                NetworkUtil.setNetwork(BindNetActivity.this, network);
                L.d("bind network " + network.toString());
            }

            @Override
            public void onLost(Network network) {
                NetworkUtil.setNetwork(BindNetActivity.this, null);
                try {
                    connManager.unregisterNetworkCallback(this);
                } catch (SecurityException e) {
                    L.d("Failed to unregister network callback");
                }
            }
        });

繫結網路

在setNetwork方法裡,做了一下幾件事。首先記錄下Network,然後更新相關httpclient並繫結網路,然後更新圖片載入並新增繫結網路的註冊。

public static void setNetwork(Context context, Network network) {
    L.d("init network" + network);
    NetworkUtil.network = network;
    BindedHttpClient.getInstance().updateClient(network);
    ImageLoader.initGlide(context);
}            

BindHttpClient 裡的關鍵程式碼如下,可以看到,在updateClient方法中,使用OkHttpClient.Builder的socketFactory方法將建立連線的socketFactory 指定為Network的socketFactory。這樣所有通過OkHttpClient的訪問都會走到指定的Network了。

public OkHttpClient client;
public void updateClient(Network network) {
    OkHttpClient.Builder builder = new OkHttpClient().newBuilder();
    builder.connectTimeout(10, TimeUnit.SECONDS)
            .writeTimeout(30, TimeUnit.SECONDS)
            .readTimeout(30, TimeUnit.SECONDS)
            .connectionPool(new ConnectionPool(0, 5, TimeUnit.MINUTES));
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && network != null) {
        builder.socketFactory(network.getSocketFactory());
    }
    client = builder.build();
}    

ImageLoader裡的關鍵程式碼如下,glide支援圖片載入的自定義註冊。簡單的說,就是可以將載入的地址包裝成modelClass,指定載入後的資料為resourceClass,以及載入工廠factory。

public static void initGlide(Context context) {
    Glide.get(context).register(GlideCameraUrl.class, InputStream.class, new LocalLoaderFactory());
}    

GlideCustomUrl裡面沒什麼,就是繼承了GlideUrl,方便區分是普通的圖片請求,還是特定網路的圖片請求。

public class GlideCustomUrl extends GlideUrl {

    public GlideCustomUrl(URL url) {
        super(url);
    }

    public GlideCustomUrl(String url) {
        super(url);
    }

    public GlideCustomUrl(URL url, Headers headers) {
        super(url, headers);
    }

    public GlideCustomUrl(String url, Headers headers) {
        super(url, headers);
    }
}

LocalLoaderFactory裡的build方法返回了自定義的ModelLoader OkHttpUrlLoader,傳入之前繫結過網路的httpclient作為網路請求,這樣就可以繫結到特定網路了。

public class LocalLoaderFactory implements ModelLoaderFactory<GlideCustomUrl, InputStream> {

    @Override
    public ModelLoader<GlideCustomUrl, InputStream> build(Context context, GenericLoaderFactory factories) {
        return new OkHttpUrlLoader(BindedHttpClient.getInstance().client);
    }

    @Override
    public void teardown() {
    }
}

在OkHttpUrlLoader裡又需要自定義一個DataFetcher,在OkHttpStreamFetcher這裡面才是真正的請求網路資料的部分。

@Override
public DataFetcher<InputStream> getResourceFetcher(GlideCustomUrl model, int width, int height) {
    return new OkHttpStreamFetcher(client, model);
}

對c層socket的繫結

以上寫的都是在java層建立http的繫結,對於有些應用場景需要在C層訪問網路,又如何繫結呢?

參照如下程式碼,linux系統中建立socket會分配對應的fileDescriptor即檔案描述符,這個是一個IO的唯一標識。只要建立完socket之後通過jni呼叫此java方法,將fileDescriptor繫結到相關network中就可以了。

@TargetApi(Build.VERSION_CODES.M)
public static void bindSocketToNetwork(int socketfd) {
    L.d("start bindSocketToNetwork");
    if (network != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        FileDescriptor fileDescriptor = new FileDescriptor();
        try {
            Field field = FileDescriptor.class.getDeclaredField("descriptor");
            field.setAccessible(true);
            field.setInt(fileDescriptor, socketfd);
            // fileDescriptor.sync();

            network.bindSocket(fileDescriptor);
//                bindSocket(socketfd, netId);
            L.d("bindSocketToNetwork success: network" + network + "+socketfd" + socketfd);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

相關原始碼



作者:mqstack
連結:https://www.jianshu.com/p/0042c0e3a15b
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。