Bootstrap

Android网络代理原理及实现

网络代理简介

代理典型的分为三种类型:

正向代理

缓存服务器使用的代理机制最早是放在客户端一侧的,是代理的原型,称为正向代理。其目的之一
是缓存,另一目的是用来实现防火墙(阻止互联网与公司内网之间的包,同时要让必要的包通过)。
组网图如下所示:

forward_proxy

在使用正向代理时,一般需要在浏览器的设置窗口中的代理服务器一栏中填写正向代理的IP地址。
当设置了正向代理时。浏览器会忽略网址栏的内容,直接将所有请求发给正向代理(Http 的URI
部分从原来的文件路径改成http的完整路径,用于转发)。请求包中的URI如下所示:

forward_proxy_http

反向代理

通过将请求消息中的URI中的目录名与Web服务器进行关联,使得代理能够转发一般的不包含完整网
址的请求消息。这种方式称为反向代理(客户端不需要进行代理配置,地址栏中域名通过DNS服务器
解析成对应的为代理服务器地址,代理服务器根据请求消息中的URI的目录名转发给相应的服务器处
理)。这种方式一般应用于缓存服务器,分布式服务器场景。

reverse_proxy

透明代理

通过请求消息包头部中的IP地址,进行拦截转发,这种方式成为透明代理。透明代理需要对请求消
息进行拦截,如果请求消息到目的服务器有多条路径,就需要在每条路径中设置透明代理,通常为
了方便,需要把组网设计成只有一条路径,并在该条路径中进行拦截。比如在连接互联网的接入网
入口处可设置透明代理。

客户需求Http/Https Proxy业务

客户需求的Http/Https代理业务,属于客户端一侧的代理,即我们上述介绍的正向代理。
Android原生框架已支持http/https代理功能,博主基于原生框架接口实现对所有网络
进行代理配置。

框架实现

Android中的几个Proxy概念:

名称描述相关接口
GlobalProxy全局代理,对所有网络(以太网,WiFi,vpn等)生效。ConnectivityManager.getGlobalProxy()
ConnectivityManager.setGlobalProxy(ProxyInfo p)
NetWorkProxy存在全局代理,则全局代理即为该网络的代理,否则为该network特定的http代理。
说明:应用可以绑定指定的网络,当未配置全局代理时,各个应用的代理可以不同。
ConnectivityManager.getProxyForNetwork(Network network)
ConnectivityManager.getBoundNetworkForProcess()
ConnectivityManager.bindProcessToNetwork(Network network)
DefaultProxy默认代理为系统环境生效代理。当指定全局代理时,会将全局代理应用到默认代理。没有指定全局代理时,默认网络(首选网络)的代理会应用到默认代理。ConnectivityManager.getDefaultProxy()

本文的Http/Https代理业务,针对的是GlobalProxy的配置,对所有网络生效。
相关的配置项:

名称描述Settings provider 中使用的Keynvram 的Pvalue备注
proxy host代理服务器地址Settings.Global.GLOBAL_HTTP_PROXY_HOST以"host:port"形式持久存储
proxy port代理服务器端口Settings.Global.GLOBAL_HTTP_PROXY_PORT以"host:port"形式持久存储
exclusion list不使用代理的网址列表Settings.Global.GLOBAL_HTTP_PROXY_EXCLUSION_LIST持久存储
PAC(Proxy Auto Config)代理配置脚本文件,可以实现对符合一定规则的url配置相应的代理服务器,实现多代理服务器的大型代理网络,具体可以参考 PAC(代理自动配置)_百度百科, PAC自动代理文件格式,教你如何写PAC文件Settings.Global.GLOBAL_HTTP_PROXY_PAC博主目前不支持
Global Proxy配置实现架构

在这里插入图片描述

http/https 客户端请求Proxy应用详解
方式一 基于Android ojluni实现
  • ojluni(OpenJDK Lang,Util,Net,IO)库提供了URL,HttpURLConnection等类用于建立http连接。
  • 最原始的这个Java lib库,需要应用自己调用ProxySelector,对URL进行处理,并用处理后的URI进行
    请求。
  • 在Android4.4中,框架层引入了okhttp(代码目录:android/external/okhttp),并基于okhttp实现了HttpHandlerHttpsHandler(代码目录:android/external/okhttp/android/main/java/com/squareup/okhttp),
    这两个类实现了URLStreamHandler
  • 在ojluni中Android利用反射机制,当解析出协议为http或者https时,将http请求或https请求的处理实现交给okhttp的HttpHandlerHttpsHandler,并且没有携带指定的proxy。

android/libcore/ojluni/src/main/java/java/net/URL.java

	       static URLStreamHandler getURLStreamHandler(String protocol) {

		    /*此处略去其他代码*/
		    // Fallback to built-in stream handler.
		    // Makes okhttp the default http/https handler
		    if (handler == null) {
		        try {
		            // BEGIN Android-changed
		            // Use of okhttp for http and https
		            // Removed unnecessary use of reflection for sun classes
		            if (protocol.equals("file")) {
		                handler = new sun.net.www.protocol.file.Handler();
		            } else if (protocol.equals("ftp")) {
		                handler = new sun.net.www.protocol.ftp.Handler();
		            } else if (protocol.equals("jar")) {
		                handler = new sun.net.www.protocol.jar.Handler();
		            } else if (protocol.equals("http")) {
		                handler = (URLStreamHandler)Class.
		                    forName("com.android.okhttp.HttpHandler").newInstance();
		            } else if (protocol.equals("https")) {
		                handler = (URLStreamHandler)Class.
		                    forName("com.android.okhttp.HttpsHandler").newInstance();
		            }
		            // END Android-changed
		        } catch (Exception e) {
		            throw new AssertionError(e);
		        }
		    }
		}

  • 在okhttp的HttpHandlerHttpsHandler中如果没有指定proxy,则使用的是默认代理选择器(ProxySelector.getDefault())。

  • ProxySelector.getDefault(),实例为ojluni中的“sun.net.spi.DefaultProxySelector”,其根据系统环境中的http/https代理相关属性对URL进行解析匹配判断处理。

  • 系统环境中的http/https代理相关属性如下:

    属性名称httphttps
    代理服务器地址http.proxyHosthttps.proxyHost
    代理服务器端口http.proxyPorthttps.proxyPort
    不使用代理的网址列表http.nonProxyHostshttps.nonProxyHosts
  • 从上述实现中,可以看到,即便设置了全局代理(并应用到了系统默认代理),无法保证所有请求都走的全局代理。系统默认代理在全局代理设置结束后仍可以变更。

Demo1 此处仅示范最简单的同步请求:

	    private void demoHttpURLConnection() {
		new Thread() {
		    @Override
		    public void run() {
		        /*当我们设置页面修改代理参数==>nvram 变更==>SystemManager收到广播 ==>
		          调用ConnectivityService.setGlobalProxy==>发出广播==》默认代理参数变更*/

		        //此处获取到的默认代理参数与设置的一致(http.nonProxyHosts分隔符变更为"|")
		        String host = System.getProperty("http.proxyHost");
		        String port = System.getProperty("http.proxyPort");
		        String nonProxyHosts = System.getProperty("http.nonProxyHosts");

	    //                //假如我们在此处修改默认代理参数,那么接下来的http请求也就变更
	    //                System.setProperty("http.proxyHost", "");
	    //                System.setProperty("http.proxyPort", "");
	    //                System.setProperty("http.nonProxyHosts", "");

		        Log.d(TAG, "host: " + host);
		        Log.d(TAG, "port: " + port);
		        Log.d(TAG, "nonProxyHosts: " + nonProxyHosts);

		        //以下请求基于默认代理参数进行
		        // 也就是"http.proxyHost","http.proxyPort","http.nonProxyHosts"只要确保正确,则代理请求正确
		        try {
		            URL url = new URL("http://192.168.xx.xx/cer/cert.pem");
		            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
		            connection.setConnectTimeout(5000);
		            int code = connection.getResponseCode();
		            if (code == 200) {
		                InputStream inputStream = connection.getInputStream();
		                String ret = inputStreamString(inputStream);
		                Log.d(TAG, "success.. ret:" + ret);
		            } else {
		                Log.e(TAG, "fail..");
		            }
		        } catch (Exception e) {
		            e.printStackTrace();
		            Log.e(TAG, "e:" + e.getMessage());
		        }
		    }
		}.start();
	    }
方式二 基于okhttp实现
  • okhttp因为设计强大,易用,问题少,因此Android框架中也将其引入,第三方应用中使用也很广泛。
  • okhttp为开源http客户端,因此在Http代理的处理上设计为:

    (1)直接设置代理的host port,使当前请求直接走指定的代理

    (2)实现一个ProxySelector,对URL满足一定规则进行匹配处理,选择不同的代理

    (3)不设置代理或ProxySelector,okhttp内部直接使用默认代理服务器。

android/external/okhttp/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java

		 OkHttpClient copyWithDefaults() {
		    OkHttpClient result = new OkHttpClient(this);
		    if (result.proxySelector == null) {
		      result.proxySelector = ProxySelector.getDefault();
		    }
		    /*省略其他代码*/
		    return result;
		  }

		  @android/external/okhttp/okhttp-urlconnection/src/main/java/com/squareup/okhttp/OkUrlFactory.java
		  HttpURLConnection open(URL url, Proxy proxy) {
		      /*省略其他代码*/
		      OkHttpClient copy = client.copyWithDefaults();
		      copy.setProxy(proxy);
		      /*省略其他代码*/
		   }
  • 上述okhttp的实现说明,使用okhttp进行请求也无法保证所有请求都走了全局代理(全局代理已应用到了默认代理),okhttp的单个请求可以特别指定它的代理服务器。

Demo2 此处仅示范最简单的同步请求:

    private void demoOkhttp() {
        new Thread() {
            @Override
            public void run() {
                try {
                    //当指定了Proxy,则proxySelector会失效。不指定Proxy和proxySelector,走默认代理(同方式一,只要确保系统默认代理参数正确即可)
                    OkHttpClient client = new OkHttpClient.Builder().proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("192.168.128.81", 808))).proxySelector(ProxySelector.getDefault()).build();//创建OkHttpClient对象

                    Request request = new Request.Builder()
                            .url("http://192.168.xx.xx/cer/cert.pem")//请求接口。如果需要传参拼接到接口后面。
                            .build();//创建Request 对象
                    Response response = null;
                    response = client.newCall(request).execute();//得到Response 对象
                    if (response.isSuccessful()) {
                        Log.d(TAG, "response.code()==" + response.code());
                        Log.d(TAG, "response.message()==" + response.message());
                        Log.d(TAG, "res==" + response.body().string());
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }.start();
    }
方式三 基于apache-http实现
  • apache-http 也是属于使用比较广泛的开源http客户端。Android系统中也有引入(代码目录:android/external/apache-http),并在其基础上对代理相关代码做了一定的修改。
  • 第三方应用使用时在moudle的gradle中增加以下,即可引入Android系统中的apache-http
        android {
            useLibrary 'org.apache.http.legacy'
        }
  • Android修改代理主要实现:

    (1)设置指定代理时,直接使用指定代理

    (2)未设置指定代理时,使用默认代理

android/external/apache-http/src/org/apache/http/impl/client/DefaultHttpClient.java

        @Override
        protected HttpRoutePlanner createHttpRoutePlanner() {
            // BEGIN android-changed
            //     Use the proxy specified by system properties
            return new ProxySelectorRoutePlanner(getConnectionManager().getSchemeRegistry(), null);
            // END android-changed
        }

        @android/external/apache-http/src/org/apache/http/impl/conn/ProxySelectorRoutePlanner.java
        public HttpRoute determineRoute(HttpHost target,
                                HttpRequest request,
                                HttpContext context)
            throws HttpException {
            /*省略其他代码*/

            // BEGIN android-changed
            //     If the client or request explicitly specifies a proxy (or no
            //     proxy), prefer that over the ProxySelector's VM-wide default.
            HttpHost proxy = (HttpHost) request.getParams().getParameter(ConnRoutePNames.DEFAULT_PROXY);
            if (proxy == null) {
                proxy = determineProxy(target, request, context);
            } else if (ConnRouteParams.NO_HOST.equals(proxy)) {
                // value is explicitly unset
                proxy = null;
            }
            // END android-changed

            /*省略其他代码*/
            return route;
            }

             protected HttpHost determineProxy(HttpHost    target,
                                      HttpRequest request,
                                      HttpContext context)
                throws HttpException {

                // the proxy selector can be 'unset', so we better deal with null here
                ProxySelector psel = this.proxySelector;
                if (psel == null)
                psel = ProxySelector.getDefault();
                if (psel == null)
                return null;

                URI targetURI = null;
                try {
                targetURI = new URI(target.toURI());
                } catch (URISyntaxException usx) {
                throw new HttpException
                    ("Cannot convert host to URI: " + target, usx);
                }
                List<Proxy> proxies = psel.select(targetURI);

                Proxy p = chooseProxy(proxies, target, request, context);

                HttpHost result = null;
                if (p.type() == Proxy.Type.HTTP) {
                // convert the socket address to an HttpHost
                if (!(p.address() instanceof InetSocketAddress)) {
                    throw new HttpException
                    ("Unable to handle non-Inet proxy address: "+p.address());
                }
                final InetSocketAddress isa = (InetSocketAddress) p.address();
                // assume default scheme (http)
                result = new HttpHost(getHost(isa), isa.getPort());
                }

                return result;
            }
  • 上述代理实现说明,使用apache-http进行请求也无法保证所有请求都走了全局代理(全局代理已应用到了默认代理),apache-http的单个请求可以特别指定它的代理服务器。

Demo3 此处仅示范最简单的同步请求:

      private void demoApacheHttp() {
	    new Thread() {
		@Override
		public void run() {
		    try {
			HttpClient client = new DefaultHttpClient();
			HttpGet request = new HttpGet("http://192.168.xx.xx/cer/cert.pem");
			//当指定了Proxy,则直接使用该代理。不指定Proxy则走默认代理(同方式一,只要确保系统默认代理参数正确即可)
			HttpHost proxy = new HttpHost("192.168.xx.xx", 808);
			request.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy);

			HttpResponse response = client.execute(request);
			if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
			    String ret = EntityUtils.toString(response.getEntity());
			    Log.d(TAG, "success.." + ret);
			}

		    } catch (ClientProtocolException e) {
			e.printStackTrace();
		    } catch (IOException e) {
			e.printStackTrace();
		    }
		}
	    }.start();
	}

方式四 基于Android Network实现
  • 前述三种方式,如果没有指定特殊的代理,则最终使用的都是系统环境的默认代理。而Default Proxy可以直接通过System.setProperty来修改,这将导致Android无法进行维护。
  • 因此Android框架层中维护了Global Proxy,当Global Proxy变更则同时更新Default Proxy。按照目前设计思路,Android应该希望每个应用均可以指定特定的网络进行请求,
    且各个网络代理可以不同。因此Android的Network.java也提供了单独的接口进行网络连接。
  • Android Network.java的代理实现设计也非常简单:

    (1)直接使用openConnection(URL url, java.net.Proxy proxy)对某个请求指定特殊的代理。内部实现直接利用okhttp进行请求。

android/frameworks/base/core/java/android/net/Network.java

public URLConnection openConnection(URL url, java.net.Proxy proxy) throws IOException {
                if (proxy == null) throw new IllegalArgumentException("proxy is null");
                maybeInitHttpClient();
                String protocol = url.getProtocol();
                OkUrlFactory okUrlFactory;
                // TODO: HttpHandler creates OkUrlFactory instances that share the default ResponseCache.
                // Could this cause unexpected behavior?
                if (protocol.equals("http")) {
                    okUrlFactory = HttpHandler.createHttpOkUrlFactory(proxy);
                } else if (protocol.equals("https")) {
                    okUrlFactory = HttpsHandler.createHttpsOkUrlFactory(proxy);
                } else {
                    // OkHttp only supports HTTP and HTTPS and returns a null URLStreamHandler if
                    // passed another protocol.
                    throw new MalformedURLException("Invalid URL or unrecognized protocol " + protocol);
                }
                OkHttpClient client = okUrlFactory.client();
                client.setSocketFactory(getSocketFactory()).setConnectionPool(mConnectionPool);
                // Let network traffic go via mDns
                client.setDns(mDns);

                return okUrlFactory.open(url);
            }
(2)使用openConnection(URL url),使用当前网络的代理(从实现来看,单个网络应该只支持设置代理域名和端口,不支持不进行代理网址的过滤,如果设置了全局代理,利用 Network.java的这个接口进行请求,同样不支持不进行代理网址的过滤)

android/frameworks/base/core/java/android/net/Network.java

public URLConnection openConnection(URL url) throws IOException {
                final ConnectivityManager cm = ConnectivityManager.getInstanceOrNull();
                if (cm == null) {
                    throw new IOException("No ConnectivityManager yet constructed, please construct one");
                }
                /* note:此处我们可以看出,该实现没有完全确定,后续可能不支持全局代理替换单个Network代理。*/
                // TODO: Should this be optimized to avoid fetching the global proxy for every request?
                final ProxyInfo proxyInfo = cm.getProxyForNetwork(this);
                java.net.Proxy proxy = null;
                if (proxyInfo != null) {
                    proxy = proxyInfo.makeProxy();
                } else {
                    proxy = java.net.Proxy.NO_PROXY;
                }
                return openConnection(url, proxy);
            }
  • 上述实现中,也能看到单个网络的请求也可指定特殊代理。如果不指定代理,则使用的是全局代理。此处存在差异是该全局代理却不支持不进行代理的网址的过滤。(可能是Android的bug,只要代理传null,其实就和其他的方式一样使用默认代理了)

Demo4 此处仅示范最简单的同步请求:

    private void demoAndroidNetwork() {
            new Thread() {
                @Override
                public void run() {
                    try {
                        URL url = new URL("http://192.168.xx.xx/cer/cert.pem");

                        ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
                        Network defNet = cm.getActiveNetwork();
                        Log.e(TAG, "defNet.." + defNet);
                        HttpURLConnection connection = (HttpURLConnection) defNet.openConnection(url);
                        connection.setConnectTimeout(5000);
                        int code = connection.getResponseCode();
                        if (code == 200) {
                            InputStream inputStream = connection.getInputStream();
                            String ret = inputStreamString(inputStream);
                            Log.e(TAG, "success.." + ret);

                        } else {
                            Log.e(TAG, "fail..");
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                        Log.e(TAG, "e:" + e.getMessage());
                    }
                }
            }.start();
        }
相关代码路径
模块路径说明
Android frameworkandroid/frameworks/base/telecomm/java/android/telecom/ConnectionService.java
android/frameworks/base/core/java/android/net/Network.java
Global Proxy维护,以及提供单个Network的网络连接接口
ojluniandroid/libcore/ojluniOpenJDK Lang,Util,Net,IO库,默认代理选择器的实现在此
okhttpandroid/external/okhttp框架层实际使用了okhttp进行http请求
apache-httpandroid/external/apache-http第三方应用可以引用Android系统中的apache-http,Android在apache-http中修改了代理的实现
遗留问题说明
  • 上述方式四,也就是使用指定Network进行网络请求,在设置了全局代理的情况时,全局代理的不进行代理的域名列表将不生效。
    该问题可能为Android的bug,Android后续也可能不会对在此接口应用全局代理。目前未做修改,如有必要,我认为修改为和其他方式一样,直接使用默认代理。
  • 上述前三种方式,均存在默认代理被修改的风险(通过System.setProperty),此为Android系统整体的问题,后续可能SELINUX加强后,对这个属性进行保护,也许能进行规避。目前未做修改。
其他调试经验
  • 获取global proxy信息
        settings get global global_http_proxy_host
        settings get global global_http_proxy_port
        settings get global global_http_proxy_exclusion_list

;