全部产品

Android端HTTPDNS+Weex最佳实践

由于WebView并未暴露出设置DNS的接口,因而在WebView场景下使用HTTPDNS存在很多限制,但如果接入WEEX,则可以较好地植入HTTPDNS,本文主要介绍在WEEX场景下接入HTTPDNS的方案细节。

注意

当前最佳实践文档只针对结合使用时,如何使用HTTPDNS解析出的IP,关于HTTPDNS本身的解析服务,请先查看Android SDK开发指南

代码示例

HTTPDNS+Weex方案详细代码参见Weex+HTTPDNS Android Demo

概述

WEEX运行时环境下,所有的逻辑最终都会转换到Native Runtime中执行,网络请求也不例外。同时WEEX也提供了自定义相应实现的接口,通过重写网络请求适配器,我们可以较为简单地接入HTTPDNS。在WEEX运行环境中,主要有两种网络请求:

  • 通过Stream进行的网络请求

  • 标签指定的加载图片的网络请求

Stream网络请求+HTTPDNS

Stream网络请求在Android端最终会通过DefaultWXHttpAdapter完成,同时WEEX也提供了相应的接口自定义网络请求适配器。

具体的逻辑如下:

  1. 创建自定义网络请求适配器,实现IWXHttpAdapter接口

    public class WXHttpdnsAdatper implements IWXHttpAdapter {
          @Override
        public void sendRequest(final WXRequest request, final OnHttpListener listener) {
          ......
        }
    }

    该接口需要实现sendRequest方法,该方法传入一个WXRequest对象的实例,该对象提供了以下信息:

    public class WXRequest {
      // The request parameter
      public Map<String, String> paramMap;
      // The request URL
      public String url;
      // The request method
      public String method;
      // The request body
      public String body;
      // The request time out
      public int timeoutMs = WXRequest.DEFAULT_TIMEOUT_MS;
      // The default timeout
      public static final int DEFAULT_TIMEOUT_MS = 3000;
    }

    通过该对象我们可以获取到请求报头、URL、方法以及body。

  2. WEEX初始化时注册自定义网络适配器,替换默认适配器:

    InitConfig config=new InitConfig.Builder()
                    .setHttpAdapter(new WXHttpdnsAdatper()) // 注册自定义网络请求适配器
                      ......
                    .build();
            WXSDKEngine.initialize(this,config);

    之后左右的网络请求都会通过WXHttpdnsAdatper实现,所以只需要在WXHttpdnsAdapter中植入HTTPDNS逻辑即可,具体逻辑可以参考如下代码:

    public class WXHttpdnsAdatper implements IWXHttpAdapter {
      ......
        private void execute(Runnable runnable){
            if(mExecutorService==null){
                mExecutorService = Executors.newFixedThreadPool(3);
            }
            mExecutorService.execute(runnable);
        }
    
        @Override
        public void sendRequest(final WXRequest request, final OnHttpListener listener) {
            if (listener != null) {
                listener.onHttpStart();
            }
    
            Log.e(TAG, "URL:" + request.url);
            execute(new Runnable() {
                @Override
                public void run() {
                    WXResponse response = new WXResponse();
                    WXHttpdnsAdatper.IEventReporterDelegate reporter = getEventReporterDelegate();
                    try {
                          // 创建连接
                        HttpURLConnection connection = openConnection(request, listener);
                        reporter.preConnect(connection, request.body);
                      ......
                    } catch (IOException |IllegalArgumentException e) {
                      ......
                    }
                }
            });
        }
    
        private HttpURLConnection openConnection(WXRequest request, OnHttpListener listener) throws IOException {
            URL url = new URL(request.url);
              // 创建一个植入HTTPDNS的连接
            HttpURLConnection connection = openHttpDnsConnection(request, request.url, listener, null);
            return connection;
        }
    
    
    
        private HttpURLConnection openHttpDnsConnection(WXRequest request, String path, OnHttpListener listener, String reffer) {
            HttpURLConnection conn = null;
            URL url = null;
            try {
                url = new URL(path);
                conn = (HttpURLConnection) url.openConnection();
               // 创建一个接入httpdns的连接
                HttpURLConnection tmpConn = httpDnsConnection(url, path);
              ......
    
                int code = conn.getResponseCode();// Network block
                Log.e(TAG, "code:" + code);
                // SNI场景下通常涉及重定向,重新建立新连接
                if (needRedirect(code)) {
                    Log.e(TAG, "need redirect");
                    String location = conn.getHeaderField("Location");
                    if (location == null) {
                        location = conn.getHeaderField("location");
                    }
    
                    if (location != null) {
                        if (!(location.startsWith("http://") || location
                                .startsWith("https://"))) {
                            //某些时候会省略host,只返回后面的path,所以需要补全url
                            URL originalUrl = new URL(path);
                            location = originalUrl.getProtocol() + "://"
                                    + originalUrl.getHost() + location;
                        }
                        Log.e(TAG, "code:" + code + "; location:" + location + "; path" + path);
                        return openHttpDnsConnection(request, location, listener, path);
                    } else {
                        return conn;
                    }
                } else {
                    // redirect finish.
                    Log.e(TAG, "redirect finish");
                    return conn;
                }
            }
            ......
            return conn;
        }
    
        private HttpURLConnection httpDnsConnection(URL url, String path) {
            HttpURLConnection conn = null;
            // 通过HTTPDNS SDK接口获取IP
            String ip = HttpDnsManager.getInstance().getHttpDnsService().getIpByHostAsync(url.getHost());
            if (ip != null) {
                // 通过HTTPDNS获取IP成功,进行URL替换和HOST头设置
                Log.d(TAG, "Get IP: " + ip + " for host: " + url.getHost() + " from HTTPDNS successfully!");
                String newUrl = path.replaceFirst(url.getHost(), ip);
                try {
                    conn = (HttpURLConnection) new URL(newUrl).openConnection();
                } catch (IOException e) {
                    return null;
                }
    
                // 设置HTTP请求头Host域
                conn.setRequestProperty("Host", url.getHost());
    
                // HTTPS场景
                if (conn instanceof HttpsURLConnection) {
                    final HttpsURLConnection httpsURLConnection = (HttpsURLConnection)conn;
                    WXTlsSniSocketFactory sslSocketFactory = new WXTlsSniSocketFactory((HttpsURLConnection) conn);
    
                    // SNI场景,创建SSLScocket解决SNI场景下的证书问题
                    conn.setInstanceFollowRedirects(false);
                    httpsURLConnection.setSSLSocketFactory(sslSocketFactory);
                    // HTTPS场景,证书校验
                    httpsURLConnection.setHostnameVerifier(new HostnameVerifier() {
                        @Override
                        public boolean verify(String hostname, SSLSession session) {
                            String host = httpsURLConnection.getRequestProperty("Host");
                            Log.e(TAG, "verify host:" + host);
                            if (null == host) {
                                host = httpsURLConnection.getURL().getHost();
                            }
                            return HttpsURLConnection.getDefaultHostnameVerifier().verify(host, session);
                        }
                    });
                }
            } else {
                Log.e(TAG, "no corresponding ip found, return null");
                return null;
            }
    
            return conn;
        }
    }

<image>网络请求+HTTPDNS

WEEX并没有提供默认的图片适配器实现,所以用户必须自行实现才能完成图片请求逻辑,具体步骤分为以下几步:

  1. 自定义图片请求适配器,实现IWXImgLoaderAdapter接口

    public class HttpDnsImageAdapter implements IWXImgLoaderAdapter {
        @Override
        public void setImage(final String url, final ImageView view, WXImageQuality quality, final WXImageStrategy strategy) {
          ......
        }
    }
  2. WEEX初始化时注册该图片适配器:

        private void initWeex() {
            InitConfig config=new InitConfig.Builder()
                    .setImgAdapter(new HttpDnsImageAdapter())
                    .build();
            WXSDKEngine.initialize(this,config);
              ......
        }

    所以同WXHttpdnsAdatper一样,我们只需在HttpDnsImageAdapter植入HTTPDNS逻辑即可。具体代码可参考:

    /**
     * Created by liyazhou on 2017/10/22.
     */
    
    public class WXHttpDnsImageAdapter implements IWXImgLoaderAdapter {
    
        @Override
        public void setImage(final String url, final ImageView view, WXImageQuality quality, final WXImageStrategy strategy) {
            Log.e(TAG, "img url:" + url);
            execute(new Runnable() {
                @Override
                public void run() {
                  ......
                    HttpURLConnection conn = null;
                    try {
                         conn = createConnection(url);
                         ....
                         // 将得到的数据转化成InputStream
                         InputStream is = conn.getInputStream();
                         // 将InputStream转换成Bitmap
                         final Bitmap bitmap = BitmapFactory.decodeStream(is);
                         WXSDKManager.getInstance().postOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                view.setImageBitmap(bitmap);
                            }
                         }, 0);
                          ......
                }
            });
        }
    
        protected HttpURLConnection createConnection(String originalUrl) throws IOException {
            mHttpDnsService = HttpDnsManager.getInstance().getHttpDnsService();
            if (mHttpDnsService == null) {
                URL url = new URL(originalUrl);
                return (HttpURLConnection) url.openConnection();
            } else {
                return httpDnsRequest(originalUrl, null);
            }
        }
    
        private HttpURLConnection httpDnsRequest(String path, String reffer) {
            HttpURLConnection httpDnsConn = null;
            HttpURLConnection originalConn = null;
            URL url = null;
            try {
                url = new URL(path);
                originalConn = (HttpURLConnection) url.openConnection();
                // 异步接口获取IP
                String ip = HttpDnsManager.getInstance().getHttpDnsService().getIpByHostAsync(url.getHost());
                if (ip != null) {
                    // 通过HTTPDNS获取IP成功,进行URL替换和HOST头设置
                    Log.d(TAG, "Get IP: " + ip + " for host: " + url.getHost() + " from HTTPDNS successfully!");
                    String newUrl = path.replaceFirst(url.getHost(), ip);
                    httpDnsConn = (HttpURLConnection) new URL(newUrl).openConnection();
    
                    // 设置HTTP请求头Host域
                    httpDnsConn.setRequestProperty("Host", url.getHost());
    
                    // HTTPS场景
                    if (httpDnsConn instanceof HttpsURLConnection) {
                        final HttpsURLConnection httpsURLConnection = (HttpsURLConnection)httpDnsConn;
                        WXTlsSniSocketFactory sslSocketFactory = new WXTlsSniSocketFactory((HttpsURLConnection) httpDnsConn);
    
                        // sni场景,创建SSLScocket解决SNI场景下的证书问题
                        httpsURLConnection.setSSLSocketFactory(sslSocketFactory);
                        // https场景,证书校验
                        httpsURLConnection.setHostnameVerifier(new HostnameVerifier() {
                            @Override
                            public boolean verify(String hostname, SSLSession session) {
                                String host = httpsURLConnection.getRequestProperty("Host");
                                if (null == host) {
                                    host = httpsURLConnection.getURL().getHost();
                                }
                                return HttpsURLConnection.getDefaultHostnameVerifier().verify(host, session);
                            }
                        });
                    }
                } else {
                    return originalConn;
                }
    
              ......
    
                int code = httpDnsConn.getResponseCode();// Network block
                if (needRedirect(code)) {
                    String location = httpDnsConn.getHeaderField("Location");
                    if (location == null) {
                        location = httpDnsConn.getHeaderField("location");
                    }
    
                    if (location != null) {
                        if (!(location.startsWith("http://") || location
                                .startsWith("https://"))) {
                            //某些时候会省略host,只返回后面的path,所以需要补全url
                            URL originalUrl = new URL(path);
                            location = originalUrl.getProtocol() + "://"
                                    + originalUrl.getHost() + location;
                        }
                        Log.e(TAG, "code:" + code + "; location:" + location + "; path" + path);
                        return httpDnsRequest(location, path);
                    } else {
                        return originalConn;
                    }
                }
            return originalConn;
        }
    }