Android端HTTPDNS+gRPC最佳实践

通过Android SDK接入流程这篇文档,您已经了解了Android SDK导入、配置、解析IP、应用到网络库和接入验证的完整流程,本文主要介绍基于gRPC接入HTTPDNS的具体方案。

1. 背景说明

gRPC 是 Google 开源的高性能 RPC 框架,基于 HTTP/2 协议,在微服务架构中广泛使用。它默认通过系统 DNS 解析域名,同时提供了 NameResolver 扩展机制,允许开发者自定义解析逻辑。
在 Android 端,如果你的网络框架使用的是 gRPC,就可以通过实现自定义 NameResolver,将 HTTPDNS 优雅地集成到 gRPC 中,从而替换系统 DNS 进行域名解析。

说明:本方案适用于使用 grpc-okhttp 作为传输层的gRPC客户端(Android推荐方式)。

2. 实现原理

gRPC的域名解析流程如下:

ManagedChannel
    ↓
NameResolver (自定义接入点)
    ↓
LoadBalancer
    ↓
Transport (grpc-okhttp)
    ↓
TCP连接

通过自定义NameResolver,在域名解析阶段接入HTTPDNS,将解析得到的IP列表返回给gRPC,由gRPCLoadBalancer进行负载均衡和故障转移。

3. 集成步骤

3.1 添加依赖

在 build.gradle 中添加gRPC依赖:

dependencies {
    // EMAS HTTPDNS
    implementation 'com.aliyun.ams:alicloud-android-httpdns:x.x.x'
    // gRPC Android
    implementation 'io.grpc:grpc-okhttp:x.x.x'
    implementation 'io.grpc:grpc-stub:x.x.x'
    implementation 'io.grpc:grpc-protobuf-lite:x.x.x'
}

3.2 初始化HTTPDNS

Application中初始化HTTPDNS服务:

public class MyApplication extends Application {
    @Overridepublic void onCreate() {
        super.onCreate();
        
        // 初始化HTTPDNS
        InitConfig config = new InitConfig.Builder()
                .setContext(this)
                .setSecretKey("your_secret_key")  // 可选
                .setEnableCacheIp(true, 24 * 60 * 60 * 1000)  // 启用缓存
                .setEnableExpiredIp(true)  // 允许使用过期IP
                .setTimeoutMillis(2000)
                .build();
        
        HttpDns.init("your_account_id", config);
    }
}

3.3 实现自定义NameResolver

创建 EmasHttpDnsNameResolver 类:

import io.grpc.EquivalentAddressGroup;
import io.grpc.NameResolver;
import io.grpc.Status;
import com.alibaba.sdk.android.httpdns.HttpDns;
import com.alibaba.sdk.android.httpdns.HttpDnsService;
import com.alibaba.sdk.android.httpdns.HTTPDNSResult;
import com.alibaba.sdk.android.httpdns.RequestIpType;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.List;

public class EmasHttpDnsNameResolver extends NameResolver {

    private final String host;
    private final int port;
    private Listener2 listener;

    public EmasHttpDnsNameResolver(String host, int port) {
        this.host = host;
        this.port = port;
    }

    @Overridepublic String getServiceAuthority() {
        return host;
    }

    @Overridepublic void start(Listener2 listener) {
        this.listener = listener;
        resolve();
    }

    @Overridepublic void refresh() {
        resolve();
    }

    private void resolve() {
        List<EquivalentAddressGroup> addressGroups = new ArrayList<>();

        try {
            // 调用HTTPDNS解析域名
            HttpDnsService httpDnsService = HttpDns.getService("your_account_id");
            HTTPDNSResult result = httpDnsService.getHttpDnsResultForHostSyncNonBlocking(
                host, 
                RequestIpType.auto
            );

            // HTTPDNS返回为空,降级到系统DNS
            if (result == null || 
                (result.getIps() == null || result.getIps().length == 0) &&
                (result.getIpv6s() == null || result.getIpv6s().length == 0)) {
                
                InetAddress[] systemAddrs = InetAddress.getAllByName(host);
                for (InetAddress addr : systemAddrs) {
                    addressGroups.add(new EquivalentAddressGroup(
                        new InetSocketAddress(addr, port)
                    ));
                }
            } else {
                // 添加IPv4地址
                if (result.getIps() != null) {
                    for (String ip : result.getIps()) {
                        addressGroups.add(new EquivalentAddressGroup(
                            new InetSocketAddress(ip, port)
                        ));
                    }
                }
                
                // 添加IPv6地址
                if (result.getIpv6s() != null) {
                    for (String ip : result.getIpv6s()) {
                        addressGroups.add(new EquivalentAddressGroup(
                            new InetSocketAddress(ip, port)
                        ));
                    }
                }
            }

            // 返回解析结果
            ResolutionResult resolutionResult = ResolutionResult.newBuilder()
                    .setAddresses(addressGroups)
                    .build();

            listener.onResult(resolutionResult);

        } catch (Exception e) {
            listener.onError(Status.UNAVAILABLE.withDescription(
                "HTTPDNS resolve failed: " + e.getMessage()
            ));
        }
    }

    @Overridepublic void shutdown() {
        // 清理资源
    }
}

3.4 实现NameResolverProvider

创建 EmasHttpDnsResolverProvider 类:

import io.grpc.NameResolver;
import io.grpc.NameResolverProvider;
import java.net.URI;

public class EmasHttpDnsResolverProvider extends NameResolverProvider {

    private static final String SCHEME = "emashttpdns";

    @Overrideprotected boolean isAvailable() {
        return true;
    }

    @Overrideprotected int priority() {
        return 5;
    }

    @Overridepublic NameResolver newNameResolver(URI targetUri, NameResolver.Args args) {
        if (!SCHEME.equals(targetUri.getScheme())) {
            return null;
        }

        String host = targetUri.getHost();
        int port = targetUri.getPort() == -1 ? 443 : targetUri.getPort();

        return new EmasHttpDnsNameResolver(host, port);
    }

    @Overridepublic String getDefaultScheme() {
        return SCHEME;
    }
}

3.5 注册NameResolver

Application中注册自定义NameResolver:

public class MyApplication extends Application {
    @Overridepublic void onCreate() {
        super.onCreate();
        
        // 初始化HTTPDNS(见3.2)
        // ...
        
        // 注册自定义NameResolver
        NameResolverRegistry.getDefaultRegistry()
                .register(new EmasHttpDnsResolverProvider());
    }
}

3.6 使用gRPC客户端

创建gRPC Channel时,使用自定义scheme:

// 创建Channel
ManagedChannel channel = OkHttpChannelBuilder
        .forTarget("emashttpdns://your-grpc-server.com:443")
        .overrideAuthority("your-grpc-server.com")  // 重要:保证TLS SNI正确
        .useTransportSecurity()  // 启用TLS
        .build();

// 创建Stub并调用
YourServiceGrpc.YourServiceBlockingStub stub = 
    YourServiceGrpc.newBlockingStub(channel);

YourResponse response = stub.yourMethod(request);

4. 验证集成

接入完成后,请参考验证网络库验证成功文档,通过劫持模拟或错误注入测试等方式,验证集成是否成功。

5. 总结

通过实现gRPCNameResolver接口,可以优雅地将HTTPDNS集成到gRPC客户端中,具有以下优势:

  • 实现简洁:只需实现NameResolver接口即可接入HTTPDNS服务

  • 通用性强:适用于各种gRPC场景,兼容证书校验、SNI等安全机制

  • 高可用性:支持多IP自动负载均衡和故障转移,HTTPDNS解析失败时自动降级到系统DNS

  • 性能优化:避免DNS劫持,提升解析速度和成功率