之前提到AndroidURLConnection因为不支持ALPN,从而不支持HTTP/2。而OkHttp 2.5以上,是支持ALPNHTTP/2,但对平台有要求,限于Android 5.0以上。那为什么会有平台的差异呢,度娘上有人说,是因为openssl,需要openssl 1.0.2以上才支持TLS,而我手上的Android 4.4用的是openssl 1.0.1e,是不是这个原因呢,我们先从OkHttp入手。

一、相关接口是否存在?

OkHttp的源码okhttp3.internal.platform.Platform.java中 注释中有这么一段:

Access to platform-specific features. … ALPN (Application Layer Protocol Negotiation)

Supported on Android 5.0+. The APIs were present in Android 4.4, but that implementation was unstable.

Supported on OpenJDK 7 and 8 (via the JettyALPN-boot library).

Supported on OpenJDK 9 via SSLParameters and SSLSocket features. …

意思看起来是说,这部分的API4.4里面是存在的,只是执行不稳定???不稳定,那还是有的嘛。继续看Platform.java:

/** Attempt to match the host runtime to a capable Platform implementation. */
private static Platform findPlatform() {
    Platform android = AndroidPlatform.buildIfSupported();
    if (android != null) {
        return android;
    }

    Platform jdk9 = Jdk9Platform.buildIfSupported();

    if (jdk9 != null) {
        return jdk9;
    }

    Platform jdkWithJettyBoot = JdkWithJettyBootPlatform.buildIfSupported();

    if (jdkWithJettyBoot != null) {
        return jdkWithJettyBoot;
    }

    // Probably an Oracle JDK like OpenJDK.
    return new Platform();
}

可以看到,Android平台的适配是通过AndroidPlatform.java实现,在buildIfSupported()方法中:

public static Platform buildIfSupported() {
    // Attempt to find Android 2.3+ APIs.
    try {
        Class<?> sslParametersClass;
        try {
            sslParametersClass = Class.forName("com.android.org.conscrypt.SSLParametersImpl");
        } catch (ClassNotFoundException e) {
            // Older platform before being unbundled.
            sslParametersClass = Class.forName("org.apache.harmony.xnet.provider.jsse.SSLParametersImpl");
        }

        OptionalMethod<Socket> setUseSessionTickets = new OptionalMethod<>(null, "setUseSessionTickets", boolean.class);
        OptionalMethod<Socket> setHostname = new OptionalMethod<>(null, "setHostname", String.class);
        OptionalMethod<Socket> getAlpnSelectedProtocol = null;
        OptionalMethod<Socket> setAlpnProtocols = null;

        // Attempt to find Android 5.0+ APIs.
        try {
            Class.forName("android.net.Network"); // Arbitrary class added in Android 5.0.
            getAlpnSelectedProtocol = new OptionalMethod<>(byte[].class, "getAlpnSelectedProtocol");
            setAlpnProtocols = new OptionalMethod<>(null, "setAlpnProtocols", byte[].class);
        } catch (ClassNotFoundException ignored) {
        }

        return new AndroidPlatform(sslParametersClass, setUseSessionTickets, setHostname, getAlpnSelectedProtocol, setAlpnProtocols);
    } catch (ClassNotFoundException ignored) {
        // This isn't an Android runtime.
    }

    return null;
}

看到没,OkHttp通过查找一个在Android 5.0+才存在的类android.net.Network.java,来判断平台版本,而从指定类中反射出两个方法来,其中关键的是getAlpnSelectedProtocol(), 这里只是缓存了一个可选的方法名、返回值、参数等等,在后面会通过反射调用到,那这个指定的类是什么呢,先别管,我们先把Class.forName("android.net.Network");这行注释掉,让他强制跑在4.4上,看看是否前面说的,API是存在的,但不稳定的,找getAlpnSelectedProtocol()具体调用的地方:

@Override public String getSelectedProtocol(SSLSocket socket) {
    if (getAlpnSelectedProtocol == null) return null;
    if (!getAlpnSelectedProtocol.isSupported(socket)) return null;
    byte[] alpnResult = (byte[]) getAlpnSelectedProtocol.invokeWithoutCheckedException(socket);
    return alpnResult != null ? new String(alpnResult, Util.UTF_8) : null;
}

isSupported()通过反射判断target是否存在相应的方法:

/**
* Returns true if the method exists on the supplied {@code target}.
*/
public boolean isSupported(T target) {
    return getMethod(target.getClass()) != null;
}

debug模式运行到这里我们发现:

上面是4.4的运行结果,可以执行通过,只是得到的结果alpnResult是空的,而在5.0的机器上,这里是有值的,解析成字符串,是h2

看来正如官方文档所说,getAlpnSelectedProtocol()这个接口在4.4上存在的,只是因为没有得到正确的结果,返回为空,所以unstable?

二、为什么getAlpnSelectedProtocol返回为空?

接下来我们看看是哪个类持有这个接口,debug可以看到:

原来是一个叫com.android.org.conscrypt.OpenSSLSocketImplWrapper的类,那么这个类的源码在那里呢,很遗憾,Android SDK开发包提供下载的sources里面是找不到的。。。它在这里:

所以我们要找conscrypt.jar的源码, OpenSSLSocketImplWrapper其实是OpenSSLSocketImpl的子类:https://android.googlesource.com/platform/external/conscrypt/+/android-5.0.0_r1/src/main/java/org/conscrypt/OpenSSLSocketImpl.java

还有4.4的,我可是翻了很久才找到的: https://android.googlesource.com/platform/external/conscrypt/+/e75878c72b717696d7e4f6cc1052f1cdaca3bda8/src/main/java/org/conscrypt/OpenSSLSocketImpl.java

两者的实现差不多,都包含:

/**
* Returns the protocol agreed upon by client and server, or {@code null} if
* no protocol was agreed upon.
*/
public byte[] getAlpnSelectedProtocol() {
    return NativeCrypto.SSL_get0_alpn_selected(sslNativePointer);
}

从注释可以看到,是需要客户端和服务端都同意ALPN,才会有返回值,具体实现,是个Native方法:

/**
* Returns the selected ALPN protocol. If the server did not select a
* protocol, {@code null} will be returned.
*/
public static native byte[] SSL_get0_alpn_selected(long sslPointer);

那这个native方法指向哪里呢,没错,它指向了openssl

openssl-1.0.2j 下载:https://www.openssl.org/source/openssl-1.0.2j.tar.gz

openssl-1.0.1e 下载:https://www.openssl.org/source/old/1.0.1/openssl-1.0.1e.tar.gz

openssl-1.0.2j\include\openssl\ssl.h中可以找到:

void SSL_get0_alpn_selected(const SSL *ssl, const unsigned char **data,
                            unsigned *len);

而在openssl-1.0.1e\include\openssl\ssl.h中:

我勒个去,Unable to find… 原来这就是unstable的原因啊!!!

三、替换openssl

既然Android 4.4openssl-1.0.1e缺少SSL_get0_alpn_selected ,那我们换成openssl-1.0.2j试试。

编译过程在这里:Android Openssl交叉编译

另外,Openssl官方wiki有个Android环境编译教程: https://wiki.openssl.org/index.php/Android#Acquire_the_Required_Files

可惜编译出来的没法使用:

CANNOT LINK EXECUTABLE: could not load library "libandroid_runtime.so" needed by "app_process"; caused by could not load library "libcrypto.so" needed by "libandroid_runtime.so"; caused by "libcrypto.so" has bad ELF magic

或者caused by "libssl.so" has bad ELF magic

替换完成后debuggetAlpnSelectedProtocol()

alpnResult还是null。。。最终协商结果还是HTTP/1.1:

D/—wyf—: Protocol: http/1.1

四、这到底是为什么

从前面getAlpnSelectedProtocol()方法的注释说明:

/**
* Returns the protocol agreed upon by client and server, or {@code null} if
* no protocol was agreed upon.
*/
public byte[] getAlpnSelectedProtocol() {
    return NativeCrypto.SSL_get0_alpn_selected(sslNativePointer);
}

还有SSL_get0_alpn_selected的注释说明:

/*
* SSL_get0_alpn_selected gets the selected ALPN protocol (if any) from
* |ssl|. On return it sets |*data| to point to |*len| bytes of protocol name
* (not including the leading length-prefix byte). If the server didn't
* respond with a negotiated protocol then |*len| will be zero.
*/
void SSL_get0_alpn_selected(const SSL *ssl, const unsigned char **data, unsigned int *len)
{
    *data = NULL;
    if (ssl->s3)
        *data = ssl->s3->alpn_selected;
    if (*data == NULL)
        *len = 0;
    else
        *len = ssl->s3->alpn_selected_len;
}

可以看到,是需要客户端和服务端都同意ALPN协商,而SSL_get0_alpn_selected只是从协商结果中判断是否能使用HTTP/2,难道不是openssl的问题?那问题出在哪呢

五、TLS链接建立和ALPN协商过程

看来有必要从头了解一下HTTP/2请求过程,我们在浏览器上访问一下HTTP/2网站,抓包看看:

首先,ClientServer发送一个ClientHello消息,说明它支持的密码算法列表、压缩方法及最高协议版本,以及稍后将被使用的随机数:

TLSv1.2中,会有个扩展字段Extension,通过ALPN扩展列出了自己支持的各种应用层协议,比如h2http/1.1

然后服务端会在Server Hello中选取所支持的加密算法和密钥大小,以及支持的协议,如果服务端支持HTTP/2,指定ALPN的协商结果为h2就可以了;如果服务端不支持HTTP/2,就会从客户端的 ALPN支持列表中选一个自己可以支持的。比如下面这图,服务端选择了降级成HTTP/1.1

这就是TLS链接建立ALPN协商的过程。

其中ALPN被包含了在TLS v1.22Extension字段中,查看TLS wiki,在1.2以前的版本是没有Extension字段的,看来问题的关键在于TLS的版本。

六、TLS支持

我们来看看Android所支持的TLS版本:https://developer.android.com/reference/javax/net/ssl/SSLSocket.html

好吧,API 164.1)就提供支持,但API 205.0)才默认打开。。。

那能不能手动打开呢,先看一下执行结果:

5.0:在初始化的ssLSocketFactory的时候,sslParameters里面enabledProtocls列表包含了TLSv1.2

4.4:只有TLSv1

再看初始化sslParameters的源码:

5.0下: com.android.org.conscrypt.SSLParametersImpl.java:

protected SSLParametersImpl(KeyManager[] kms, TrustManager[] tms, SecureRandom sr, ClientSessionContext clientSessionContext, ServerSessionContext serverSessionContext) throws KeyManagementException {
    this.serverSessionContext = serverSessionContext;
    this.clientSessionContext = clientSessionContext;
    // initialize key managers
    if (kms == null) {
        x509KeyManager = getDefaultX509KeyManager();
        // There's no default PSK key manager
        pskKeyManager = null;
    } else {
        x509KeyManager = findFirstX509KeyManager(kms);
        pskKeyManager = findFirstPSKKeyManager(kms);
    }
    // initialize x509TrustManager
    if (tms == null) {
        x509TrustManager = getDefaultX509TrustManager();
    } else {
        x509TrustManager = findFirstX509TrustManager(tms);
    }
    // initialize secure random
    // We simply use the SecureRandom passed in by the caller. If it's
    // null, we don't replace it by a new instance. The native code below
    // then directly accesses /dev/urandom. Not the most elegant solution,
    // but faster than going through the SecureRandom object.
    secureRandom = sr;
    // initialize the list of cipher suites and protocols enabled by default
    enabledProtocols = getDefaultProtocols();
    boolean x509CipherSuitesNeeded = (x509KeyManager != null) || (x509TrustManager != null);
    boolean pskCipherSuitesNeeded = pskKeyManager != null;
    enabledCipherSuites = getDefaultCipherSuites(x509CipherSuitesNeeded, pskCipherSuitesNeeded);
}


private static String[] getDefaultProtocols() {
    return NativeCrypto.DEFAULT_PROTOCOLS.clone();
}

com.android.org.conscrypt. NativeCrypto.java:

public static final String[] DEFAULT_PROTOCOLS = new String[] {
    SUPPORTED_PROTOCOL_SSLV3,
    SUPPORTED_PROTOCOL_TLSV1,
    SUPPORTED_PROTOCOL_TLSV1_1,
    SUPPORTED_PROTOCOL_TLSV1_2,
};

4.4下:

com.android.org.conscrypt.SSLParametersImpl.java:

// protocols available for SSL connection
private String[] enabledProtocols = ProtocolVersion.supportedProtocols;

com.android.org.conscrypt. ProtocolVersion.java:

/**
* Protocols supported by this provider implementation
*/
public static final String[] supportedProtocols = new String[] { "TLSv1", "SSLv3" };

所以不管是4.4还是5.0,支持的TLS版本都被写死在了外部扩展库conscrypt.jar里面。

所以看来需要重新编译Android 4.4的源码才能解决了。。。