全部產品
Search
文件中心

HTTPDNS:Android端HTTPDNS+HttpURLConnection最佳實務

更新時間:Nov 07, 2025

通過Android SDK接入流程這篇文檔,您已經瞭解了Android SDK匯入、配置、解析IP、應用到網路程式庫和接入驗證的完整流程,本文主要介紹基於 HttpURLConnection 接入HTTPDNS的具體方案。

1. 前言

本文檔主要介紹在 Android 端使用 HttpURLConnection 進行網路請求時,如何在 HTTPS(含 SNI) 情境下整合 HTTPDNS 並實現 “IP 直連” 的方案,如果您對原理感興趣,可以參考IP直串連入HTTPDNS的原理

重要

當前 Android 端主流網路開發架構大多已轉向 OkHttp。OkHttp 原生提供自訂 DNS 服務的介面,可更簡潔、優雅地實現 IP 直連。推薦優先參考Android端HTTPDNS+OkHttp最佳實務進行接入。以下內容僅在您業務情境無法使用 OkHttp 時,提供基於 HttpURLConnection 的替代方案。

2. 接入方案

根據是否涉及 SNI,我們可以分成兩種情境討論:

  1. HTTPS 情境(SNI) 則需要在建立 SSLSocket 時,將原始網域名稱通過 SNI 方式傳遞給服務端,同時也要做好 HostnameVerifier 相關邏輯。

  2. HTTPS 情境(非 SNI) 可以通過 HostnameVerifier 介面,在認證校正時將 IP 還原為原始網域名稱進行驗證。

下面分別介紹這兩種情境下的完整接入流程和參考樣本。

2.1 HTTPS 情境(SNI)

對於部署了多網域名稱認證、需要在握手前通過 SNI 提供網域名稱資訊給伺服器的情境,除了 HostnameVerifier,還需在 建立 SSLSocket 之時 設定正確的 SNI 名稱。為此,需要定製一個 自訂的 SSLSocketFactory,在 createSocket() 方法中進行以下操作:

  1. 替換為 HTTPDNS 解析後的 IP 進行串連。

  2. 調用系統或自訂的 SSLCertificateSocketFactory,在握手前通過 setHostname() 設定 SNI Hostname。

  3. 自行執行認證驗證:將認證校正中的網域名稱從 IP 改回原始網域名稱。

在官方HTTPDNS Android Demo中,我們針對HttpsURLConnection,提供了在SNI情境下使用HTTPDNS的範例程式碼。

自訂 SSLSocketFactory 樣本

class TlsSniSocketFactory constructor(conn: HttpsURLConnection): SSLSocketFactory() {
    private val mConn: HttpsURLConnection

    init {
        mConn = conn
    }

    override fun createSocket(plainSocket: Socket?, host: String?, port: Int, autoClose: Boolean): Socket {
        var peerHost = mConn.getRequestProperty("Host")
        if (peerHost == null) {
            peerHost = host
        }

        val address = plainSocket!!.inetAddress
        if (autoClose) {
            // we don't need the plainSocket
            plainSocket.close()
        }

        // create and connect SSL socket, but don't do hostname/certificate verification yet
        val sslSocketFactory =
            SSLCertificateSocketFactory.getDefault(0) as SSLCertificateSocketFactory
        val ssl = sslSocketFactory.createSocket(address, R.attr.port) as SSLSocket

        // enable TLSv1.1/1.2 if available
        ssl.enabledProtocols = ssl.supportedProtocols

        // set up SNI before the handshake
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            // setting sni hostname
            sslSocketFactory.setHostname(ssl, peerHost)
        } else {
            // No documented SNI support on Android <4.2, trying with reflection
            try {
                val setHostnameMethod = ssl.javaClass.getMethod(
                    "setHostname",
                    String::class.java
                )
                setHostnameMethod.invoke(ssl, peerHost)
            } catch (e: Exception) {
            }
        }

        // verify hostname and certificate
        val session = ssl.session

        if (!HttpsURLConnection.getDefaultHostnameVerifier()
                .verify(peerHost, session)
        ) throw SSLPeerUnverifiedException(
            "Cannot verify hostname: $peerHost"
        )

        return ssl
    }
}
public class TlsSniSocketFactory extends SSLSocketFactory {
    
    private HttpsURLConnection mConn;
    public TlsSniSocketFactory(HttpsURLConnection conn) {
        mConn = conn;
    }

    @Override
    public Socket createSocket(Socket plainSocket, String host, int port, boolean autoClose) throws IOException {
        String peerHost = mConn.getRequestProperty("Host");
        if (peerHost == null)
            peerHost = host;

        InetAddress address = plainSocket.getInetAddress();
        if (autoClose) {
            // we don't need the plainSocket
            plainSocket.close();
        }
        // create and connect SSL socket, but don't do hostname/certificate verification yet
        SSLCertificateSocketFactory sslSocketFactory = (SSLCertificateSocketFactory) SSLCertificateSocketFactory.getDefault(0);
        SSLSocket ssl = (SSLSocket) sslSocketFactory.createSocket(address, port);

        // enable TLSv1.1/1.2 if available
        ssl.setEnabledProtocols(ssl.getSupportedProtocols());

        // set up SNI before the handshake
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            // setting sni hostname
            sslSocketFactory.setHostname(ssl, peerHost);
        } else {
            // No documented SNI support on Android <4.2, trying with reflection
            try {
                java.lang.reflect.Method setHostnameMethod = ssl.getClass().getMethod("setHostname", String.class);
                setHostnameMethod.invoke(ssl, peerHost);
            } catch (Exception e) {

            }
        }

        // verify hostname and certificate
        SSLSession session = ssl.getSession();

        if (!HttpsURLConnection.getDefaultHostnameVerifier().verify(peerHost, session))
            throw new SSLPeerUnverifiedException("Cannot verify hostname: " + peerHost);

        return ssl;
    }
}

重新導向處理樣本

許多 SNI 情境下的請求可能會經歷多次 HTTP 3xx 重新導向。樣本中也展示了如何在重新導向時再次基於 HTTPDNS 解析新的 Host 並繼續發起請求。

fun recursiveRequest(path: String) {
    var conn: HttpURLConnection? = null
    try {
        val url = URL(path)
        conn = url.openConnection() as HttpURLConnection
        // 同步介面擷取IP
        val httpdnsResult = HttpDns.getService(accountID)
            .getHttpDnsResultForHostSync(url.host, RequestIpType.both)
        var ip: String? = null
        if (httpdnsResult.ips != null && httpdnsResult.ips.isNotEmpty()) {
            ip = httpdnsResult.ips[0]
        } else if (httpdnsResult.ipv6s != null && httpdnsResult.ipv6s.isNotEmpty()) {
            ip = httpdnsResult.ipv6s[0]
        }
        if (!TextUtils.isEmpty(ip)) {
            // 通過HTTPDNS擷取IP成功,進行URL替換和HOST頭設定
            val newUrl = path.replaceFirst(url.host.toRegex(), ip!!)
            conn = URL(newUrl).openConnection() as HttpURLConnection
            conn.connectTimeout = 30000
            conn.readTimeout = 30000
            conn.instanceFollowRedirects = false

            // 設定HTTP要求標頭Host域
            conn.setRequestProperty("Host", url.host)
            if (conn is HttpsURLConnection) {
                val httpsURLConnection = conn

                // https情境,認證校正
                httpsURLConnection.hostnameVerifier =
                    HostnameVerifier { _, session ->
                        var host = httpsURLConnection.getRequestProperty("Host")
                        if (null == host) {
                            host = httpsURLConnection.url.host
                        }
                        HttpsURLConnection.getDefaultHostnameVerifier().verify(host, session)
                    }

                // sni情境,建立SSLScocket
                httpsURLConnection.sslSocketFactory = TlsSniSocketFactory(httpsURLConnection)
            }
        }
        val code = conn.responseCode // Network block
        if (code in 300..399) {
            var location = conn.getHeaderField("Location")
            if (location == null) {
                location = conn.getHeaderField("location")
            }
            if (location != null) {
                if (!(location.startsWith("http://") || location
                        .startsWith("https://"))
                ) {
                    //某些時候會省略host,只返回後面的path,所以需要補全url
                    val originalUrl = URL(path)
                    location = (originalUrl.protocol + "://"
                            + originalUrl.host + location)
                }
                recursiveRequest(location)
            }
        } else {
            // redirect finish.
            val dis = DataInputStream(conn.inputStream)
            var len: Int
            val buff = ByteArray(4096)
            val response = StringBuilder()
            while (dis.read(buff).also { len = it } != -1) {
                response.append(String(buff, 0, len))
            }
            Log.d(TAG, "Response: $response")
        }
    } catch (e: MalformedURLException) {
        Log.w(TAG, "recursiveRequest MalformedURLException")
    } catch (e: IOException) {
        Log.w(TAG, "recursiveRequest IOException")
    } catch (e: java.lang.Exception) {
        Log.w(TAG, "unknow exception")
    } finally {
        conn?.disconnect()
    }
}
public void recursiveRequest(String path) {
    HttpURLConnection conn = null;
    try {
        URL url = new URL(path);
        conn = (HttpURLConnection) url.openConnection();
        // 同步介面擷取IP
        HTTPDNSResult httpdnsResult = HttpDns.getService(accountID).getHttpDnsResultForHostSync(url.getHost(), RequestIpType.both);

        String ip = null;
        if (httpdnsResult.getIps() != null && httpdnsResult.getIps().length > 0) {
            ip = httpdnsResult.getIps()[0];
        } else if (httpdnsResult.getIpv6s() != null && httpdnsResult.getIpv6s().length > 0) {
            ip = httpdnsResult.getIpv6s()[0];
        }

        if (!TextUtils.isEmpty(ip)) {
            // 通過HTTPDNS擷取IP成功,進行URL替換和HOST頭設定
            String newUrl = path.replaceFirst(url.getHost(), ip);
            conn = (HttpURLConnection) new URL(newUrl).openConnection();

            conn.setConnectTimeout(30000);
            conn.setReadTimeout(30000);
            conn.setInstanceFollowRedirects(false);

            // 設定HTTP要求標頭Host域
            conn.setRequestProperty("Host", url.getHost());

            if (conn instanceof HttpsURLConnection) {
                final HttpsURLConnection httpsURLConnection = (HttpsURLConnection) conn;

                // 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);
                    }
                });

                // sni情境,建立SSLScocket
                httpsURLConnection.setSSLSocketFactory(new TlsSniSocketFactory(httpsURLConnection));
            }
        }

        int code = conn.getResponseCode();// Network block
        if (code >= 300 && code < 400) {
            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;
                }

                recursiveRequest(location);
            }
        } else {
            // redirect finish.
            DataInputStream dis = new DataInputStream(conn.getInputStream());
            int len;
            byte[] buff = new byte[4096];
            StringBuilder response = new StringBuilder();
            while ((len = dis.read(buff)) != -1) {
                response.append(new String(buff, 0, len));
            }
            Log.d(TAG, "Response: " + response.toString());
        }
    } catch (MalformedURLException e) {
        Log.w(TAG, "recursiveRequest MalformedURLException");
    } catch (IOException e) {
        Log.w(TAG, "recursiveRequest IOException");
    } catch (Exception e) {
        Log.w(TAG, "unknow exception");
    } finally {
        if (conn != null) {
            conn.disconnect();
        }
    }
}

2.2 HTTPS 情境(非 SNI)

對於僅使用單網域名稱認證(即不涉及多網域名稱部署或不需通過 SNI 指明網域名稱)的情境,最主要的改動在於 hostname 驗證。具體原理是:在認證校正流程的第二步,通過 hook 或自行實現 HostnameVerifier,將用來驗證的網域名稱從 IP 改回原始網域名稱即可。

重要 該方案僅適用於非 SNI 情境。如果業務部署了多認證、多網域名稱,請確認是否確實 不需要 SNI。如果遇到類似 SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.請優先排查目標網站是否需要 SNI 支援。

以下樣本基於 HttpURLConnection,示範如何利用 HostnameVerifier 介面完成認證校正。

try {
    val url = "https://140.205.XX.XX/?sprefer=sypc00"
    val connection = URL(url).openConnection() as HttpURLConnection
    connection.setRequestProperty("Host", "m.taobao.com")
    if (connection is HttpsURLConnection) {
        connection.hostnameVerifier = HostnameVerifier { _, session ->
            /*
            * 關於這個介面的說明,官方有文檔描述:
            * This is an extended verification option that implementers can provide.
            * It is to be used during a handshake if the URL's hostname does not match the
            * peer's identification hostname.
            *
            * 使用HTTPDNS後URL裡設定的hostname不是遠端主機名稱(如:m.taobao.com),與憑證發行的域不匹配,
            * Android HttpsURLConnection提供了回調介面讓使用者來處理這種定製化情境。
            * 在確認HTTPDNS返回的來源站點IP與Session攜帶的IP資訊一致後,您可以在回調方法中將待驗證網域名稱替換為原來的真實網域名稱進行驗證。
            *
            */
            HttpsURLConnection.getDefaultHostnameVerifier().verify("m.taobao.com", session)
        }
    }
    connection.connect()
} catch (e: java.lang.Exception) {
    e.printStackTrace()
}
try {
    String url = "https://140.205.XX.XX/?sprefer=sypc00";
    HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();

    connection.setRequestProperty("Host", "m.taobao.com");

    if (connection instanceof HttpsURLConnection) {
        connection.setHostnameVerifier(new HostnameVerifier() {
    
           /*
            * 關於這個介面的說明,官方有文檔描述:
            * This is an extended verification option that implementers can provide.
            * It is to be used during a handshake if the URL's hostname does not match the
            * peer's identification hostname.
            *
            * 使用HTTPDNS後URL裡設定的hostname不是遠端主機名稱(如:m.taobao.com),與憑證發行的域不匹配,
            * Android HttpsURLConnection提供了回調介面讓使用者來處理這種定製化情境。
            * 在確認HTTPDNS返回的來源站點IP與Session攜帶的IP資訊一致後,您可以在回調方法中將待驗證網域名稱替換為原來的真實網域名稱進行驗證。
            *
            */
            @Override
            public boolean verify(String hostname, SSLSession session) {
                return HttpsURLConnection.getDefaultHostnameVerifier().verify("m.taobao.com", session);
            }
        });
    }

    connection.connect();
} catch (Exception e) {
    e.printStackTrace();
}

3. 總結

  1. HTTPDNS 在 HTTPS 下的挑戰

    • 認證校正需要網域名稱匹配;

    • SNI 需要在握手之前提供網域名稱給伺服器。

  2. SNI 情境

    • 必須自訂 SSLSocketFactory,在握手前設定 SNI Hostname,並在 HostnameVerifier 中處理認證校正網域名稱的替換。

  3. 非 SNI 情境

    • 只需通過 HostnameVerifier 把驗證用的網域名稱從 IP 改回原始網域名稱。

  4. 優先使用 OkHttp

至此,您已經瞭解如何在 Android 端通過 HttpURLConnection 實現基於 HTTPDNS 的 HTTPS(含 SNI) “IP 直連” 方案,希望本文能協助您順利完成整合。