聊一聊声明式接口调用与Nacos的结合使用

407 阅读2分钟

背景

对于公司内部的 API 接口,在引入注册中心之后,免不了会用上服务发现这个东西。

现在比较流行的接口调用方式应该是基于声明式接口的调用,它使得开发变得更加简化和快捷。

.NET 在声明式接口调用这一块,有 WebApiClient 和 Refit 可以选择。

前段时间有个群友问老黄,有没有 WebApiClient 和 Nacos 集成的例子。

找了一圈,也确实没有发现,所以只好自己动手了。

本文就以 WebApiClient 为例,简单介绍一下它和 Nacos 的服务发现结合使用。

API接口

基于 .NET 6 创建一个 minimal api。

using Nacos.AspNetCore.V2;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddNacosAspNet(x =>
{
    x.ServerAddresses = new List<string> { "http://localhost:8848/" };
    x.Namespace = "cs";

    // 服务名这一块统一用小写!!
    x.ServiceName = "sample";
    x.GroupName = "Some_Group";
    x.ClusterName = "DC_1";
    x.Weight = 100;
    x.Secure = false;
});

var app = builder.Build();
.
app.MapGet("/api/get", () =>
{
    return Results.Ok("from .net6 minimal API");
});

app.Run("http://*:9991");

这个应用是 provider,在启动的时候,会向 Nacos 进行注册,可以被其他应用发现并调用。

声明式接口调用

这里同样是创建一个 .NET 6 的 WEB API 项目来演示,这里需要引入一个 nuget 包。

<ItemGroup>
    <PackageReference Include="WebApiClientCore.Extensions.Nacos" Version="0.1.0" />
</ItemGroup>

首先来声明一下这个接口。

// [HttpHost("http://192.168.100.100:9991")]
[HttpHost("http://sample")]
public interface ISampleApi : IHttpApi
{
    [HttpGet("/api/get")]
    Task<string> GetAsync();
}

这里其实要注意的就是 HttpHost 这个特性,正常情况下,配置的是具体的域名或者是IP地址。

我们如果需要通过 nacos 去发现这个接口对应的真实地址的话,只需要配置它的服务名就好了。

后面是要进行接口的注册,让这个 ISampleApi 可以动起来。

var builder = WebApplication.CreateBuilder(args);

// 添加 nacos 服务发现模块
// 这里没有把当前服务注册到 nacos,按需调整
builder.Services.AddNacosV2Naming(x =>
{
    x.ServerAddresses = new List<string> { "http://localhost:8848/" };
    x.Namespace = "cs";
});

// 接口注册,启用 nacos 的服务发现功能
// 注意分组和集群的配置
// builder.Services.AddNacosDiscoveryTypedClient<ISampleApi>("Some_Group", "DC_1");
builder.Services.AddNacosDiscoveryTypedClient<ISampleApi>(x => 
{
    // HttpApiOptions
    x.UseLogging = true;
}, "Some_Group", "DC_1");

var app = builder.Build();

app.MapGet("/", async (ISampleApi api) =>
{
    var res = await api.GetAsync();
    return $"client ===== {res}" ;
});

app.Run("http://*:9992");

运行并访问 localhost:9992 就可以看到效果了

从上面的日志看,它请求的是 http://sample/api/get,实际上是 http://192.168.100.220:9991/api/get,刚好这个地址是注册到 nacos 上面的,也就是服务发现是生效了。

info: System.Net.Http.HttpClient.ISampleApi.LogicalHandler[100]
      Start processing HTTP request GET http://sample/api/get
info: System.Net.Http.HttpClient.ISampleApi.ClientHandler[100]
      Sending HTTP request GET http://sample/api/get
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/1.1 GET http://192.168.100.220:9991/api/get - -
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
      Executing endpoint 'HTTP: GET /api/get'

下面来看看 WebApiClientCore.Extensions.Nacos 这个包做了什么。

简单剖析

本质上是加了一个 HttpClientHandler,这个 handler 依赖于 sdk 提供的 INacosNamingService

public static IHttpClientBuilder AddNacosDiscoveryTypedClient<TInterface>(
    this IServiceCollection services,
    Action<HttpApiOptions, IServiceProvider> configOptions,
    string group = "DEFAULT_GROUP",
    string cluster = "DEFAULT")
    where TInterface : class, IHttpApi
{
    NacosExtensions.Common.Guard.NotNull(configOptions, nameof(configOptions));

    return services.AddHttpApi<TInterface>(configOptions)
            .ConfigurePrimaryHttpMessageHandler(provider =>
            {
                var svc = provider.GetRequiredService<INacosNamingService>();
                var loggerFactory = provider.GetService<ILoggerFactory>();

                if (svc == null)
                {
                    throw new InvalidOperationException(
                        "Can not find out INacosNamingService, please register at first");
                }

                return new NacosExtensions.Common.NacosDiscoveryHttpClientHandler(svc, group, cluster, loggerFactory);
            });
}

在 handler 里面重写了 SendAsync 方法,替换了 HttpRequestMessage 的 RequestUri,也就是把服务名换成了真正的服务地址。

protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
    var current = request.RequestUri;
    try
    {
        request.RequestUri = await LookupServiceAsync(current).ConfigureAwait(false);
        var res = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
        return res;
    }
    catch (Exception e)
    {
        _logger?.LogDebug(e, "Exception during SendAsync()");
        throw;
    }
    finally
    {
        // Should we reset the request uri to current here?
        // request.RequestUri = current;
    }
}

具体查找替换逻辑如下:

internal async Task<Uri> LookupServiceAsync(Uri request)
{
    // Call SelectOneHealthyInstance with subscribe
    // And the host of Uri will always be lowercase, it means that the services name must be lowercase!!!!
    var instance = await _namingService
        .SelectOneHealthyInstance(request.Host, _groupName, new List<string> { _cluster }, true).ConfigureAwait(false);

    if (instance != null)
    {
        var host = $"{instance.Ip}:{instance.Port}";

        // conventions here
        // if the metadata contains the secure item, will use https!!!!
        var baseUrl = instance.Metadata.TryGetValue(Secure, out _)
            ? $"{HTTPS}{host}"
            : $"{HTTP}{host}";

        var uriBase = new Uri(baseUrl);
        return new Uri(uriBase, request.PathAndQuery);
    }

    return request;
}

这里是先查询一个健康的实例,如果存在,才会进行组装,这里有一个关于 HTTPS 的约定,也就是元数据里面是否有 Secure 的配置。

大致如下图:

写在最后

声明式的接口调用,对Http接口请求,还是很方便的,可以多试试。

感兴趣的话,欢迎您的加入,一起开发完善。

nacos-sdk-csharp 的地址 :github.com/nacos-group…

nacos-csharp-extensions 的地址: github.com/catcherwong…

本文示例代码的地址 :github.com/catcherwong…

关注我的公众号「不才老黄」,第一时间和您分享老黄的所见所闻。