背景
對於公司內部的 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 的地址 :https://github.com/nacos-group/nacos-sdk-csharp
nacos-csharp-extensions 的地址: https://github.com/catcherwong/nacos-csharp-extensions
本文示例程式碼的地址 :https://github.com/catcherwong-archive/2021/tree/main/WebApiClientCoreWithNacos