.NET Core 接入 Nacos + gRPC 实战:从零搭建跨语言微服务

技术栈:.NET 8 + nacos-sdk-csharp + Grpc.AspNetCore + NacosNetCore.Extensions

本文从一个真实项目出发,手把手带你完成 .NET Core 接入 Nacos 的服务注册/发现,并基于 gRPC 实现 .NET 与 Java(Dubbo)之间的跨语言互调。所有代码均来自实际项目,可直接复用。


一、背景与痛点

在微服务架构中,Java 生态有 Nacos + Dubbo 的成熟方案,但 .NET 侧接入 Nacos 的资料相对较少。当你的团队需要:

  • .NET 服务注册到 Nacos,让 Java 侧能发现并调用

  • .NET 作为消费者,从 Nacos 发现并调用 Java Dubbo-gRPC 服务

  • REST 和 gRPC 双协议共存,对外提供 HTTP API 的同时支持 gRPC 高性能调用

网上能搜到的文章多是"跑通 Hello World",缺少真实项目的分层设计、自动注册、Proto 生成等工程化实践。本文补上这块空白。


二、最终效果

项目跑起来后,你将获得:

能力说明
服务自动注册到 Nacos启动即注册,无需手动操作
提供 gRPC 服务给 Java 调用Java 通过 Dubbo-gRPC 协议直接调用
提供 REST API 给前端调用标准 HTTP 接口,Swagger 文档自动生成
调用远程 Java gRPC 服务从 Nacos 发现实例,发起 gRPC 调用
业务层与协议层分离加新接口只需写业务逻辑,协议层自动适配
Proto 文件自动生成C# 代码定义接口,一键生成 .proto 交给 Java 团队

三、环境准备

依赖版本说明
.NET SDK8.0+dotnet --version验证
Nacos Server2.2.0+Docker 部署或独立部署均可
IDEVS 2022 / Rider支持 .NET 8 项目

四、项目搭建

4.1 创建项目

dotnet new webapi -n NacosDemo-order --no-https cd NacosDemo-order

4.2 安装 NuGet 包

这是本文用到的核心包,版本亲测可用:

# Nacos SDK dotnet add package nacos-sdk-csharp --version 1.3.10 dotnet add package nacos-sdk-csharp.AspNetCore --version 1.3.10 dotnet add package nacos-sdk-csharp.Extensions.Configuration --version 1.3.10 dotnet add package nacos-sdk-csharp.IniParser --version 1.3.10 dotnet add package nacos-sdk-csharp.YamlParser --version 1.3.10 ​ # Nacos 扩展(简化注册) dotnet add package NacosNetCore.Extensions --version 1.0.4.4 ​ # gRPC dotnet add package Grpc.AspNetCore --version 2.80.0 dotnet add package Grpc.Tools --version 2.80.0 dotnet add package Google.Protobuf --version 3.34.1 dotnet add package Google.Api.CommonProtos --version 2.17.0 ​ # 其他 dotnet add package Swashbuckle.AspNetCore --version 10.1.7 dotnet add package Newtonsoft.Json --version 13.0.4

踩坑提示nacos-sdk-csharp的 ASP.NET Core 集成需要配合NacosNetCore.Extensions使用,后者封装了AddNacosAspNet方法,一行代码搞定注册。如果只用nacos-sdk-csharp.AspNetCore,需要手动配置较多内容。


五、核心配置

5.1 appsettings.json

{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning", "Microsoft.AspNetCore.Hosting.Diagnostics": "Error", "Grpc": "Information" } }, "AllowedHosts": "*", "nacos": { "ServerAddresses": [ "http://xx.xxx.xxx.xxx:8848" ], "Namespace": "dubbo", "ServiceName": "order-service-new", "GroupName": "dubbo_core_group", "UserName": "nacos", "Password": "nacos", "PreferredNetworks": "xx.xxx.", "Port": 5073 }, "Kestrel": { "Endpoints": { "Http": { "Url": "http://0.0.0.0:5073", "Protocols": "Http1AndHttp2" }, "Grpc": { "Url": "http://0.0.0.0:8084", "Protocols": "Http2" } } } }

几个关键配置说明

配置项说明
nacos.NamespaceNacos 命名空间 ID,不是名称!在 Nacos 控制台创建命名空间后获取
nacos.PreferredNetworks多网卡环境下指定注册 IP 的前缀,避免注册了 127.0.0.1 或内网不可达的 IP
nacos.Port注册到 Nacos 的端口号,Java 侧通过这个端口发现你的 HTTP 服务
Kestrel.Endpoints双端口配置:5073 承载 REST + Swagger,8084 专供 gRPC(HTTP/2)

踩坑提示:gRPC 要求 HTTP/2,而浏览器和部分客户端走 HTTP/1.1。Kestrel 的双端口方案让两种协议各走各的,互不干扰。5073 端口设置Http1AndHttp2是为了支持 Swagger UI 和 gRPC-Web。

5.2 csproj 配置 Proto 文件

<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>net8.0</TargetFramework> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> <RootNamespace>NacosDemo_order</RootNamespace> </PropertyGroup> ​ <ItemGroup> <!-- 消费端 proto:生成 gRPC Client --> <Protobuf Include="Protos\Consumer\dictservice.proto" GrpcServices="Client" /> <!-- 生产端 proto:生成 gRPC Server --> <Protobuf Include="Protos\Producer\classification_service_generated.proto" GrpcServices="Server" /> </ItemGroup> ​ <!-- ... NuGet 引用省略 ... --> </Project>

GrpcServices="Client""Server"决定了生成的是客户端桩代码还是服务端基类,别搞反了。


六、Nacos 服务注册

6.1 一行代码注册 Nacos

Program.cs中:

var builder = WebApplication.CreateBuilder(args); ​ // 注册 Nacos —— 核心就这一行 builder.Services.AddNacosAspNet(builder.Configuration, "nacos"); ​ var app = builder.Build(); app.Run();

AddNacosAspNetNacosNetCore.Extensions提供的扩展方法,它会:

  1. 读取appsettings.json"nacos"节点的配置

  2. 向 Nacos Server 注册当前服务实例

  3. 启动心跳保活(Nacos 2.x 使用 gRPC 长连接,不再依赖 HTTP 心跳)

  4. 应用关闭时自动注销

6.2 验证注册成功

启动项目后,打开 Nacos 控制台 → 服务列表,应该能看到:

  • 服务名:order-service-new

  • 分组:dubbo_core_group

  • 命名空间:dubbo

  • 实例 IP 和端口正确

也可以通过 HTTP 接口验证:

curl http://localhost:8848/nacos/v1/ns/instance/list?serviceName=order-service-new&groupName=dubbo_core_group&namespaceId=dubbo

七、gRPC 服务提供(Provider 端)

这部分是 .NET 作为 gRPC 服务提供者,让 Java 侧能通过 Dubbo-gRPC 调用我们。

7.1 定义 Proto 文件

syntax = "proto3"; ​ package com.cn.order.api.dubbo; option csharp_namespace = "NacosDemo_order.Protos"; ​ message ClassificationRequest { string key = 1; } ​ message ClassificationListResponse { repeated ClassificationInfo classifications = 1; } ​ message ClassificationInfo { string id = 1; string name = 2; int32 sort = 3; } ​ message HealthCheckRequest { } ​ message HealthCheckResponse { string status = 1; string message = 2; string version = 3; } ​ service ClassificationDubboService { rpc GetClassificationList(ClassificationRequest) returns (ClassificationListResponse); rpc HealthCheck(HealthCheckRequest) returns (HealthCheckResponse); }

注意package名称必须和 Java Dubbo 侧的包名完全一致(如com.cn.order.api.dubbo),否则 Java 侧通过 Nacos 找到服务后无法正确路由到 gRPC 方法。

7.2 分层架构实现 gRPC 服务

这里我采用了Business 层 + Grpc 层的分层设计:

Services/ ├── Business/ # 业务逻辑层(核心) │ └── ClassificationService # 纯 C# 业务逻辑,无协议依赖 └── Grpc/ # gRPC 协议层(适配器) └── ClassificationGrpcService # 协议转换,调用 Business 层

为什么要分层?因为业务逻辑不应被 gRPC 协议绑架。将来如果要加 REST、GraphQL 或消息队列,只需新增对应的协议适配层,Business 层不动。

接口定义
// Interface/IClassificationService.cs [ProtoService( Package = "com.cn.order.api.dubbo", ServiceName = "ClassificationDubboService", CSharpNamespace = "NacosDemo_order.Protos" )] public interface IClassificationService { Task<ClassificationListResponse> GetClassificationList(ClassificationRequest request); Task<HealthCheckResponse> HealthCheck(HealthCheckRequest request); }
Business 层(纯业务逻辑)
// Services/Business/ClassificationService.cs public class ClassificationService : IClassificationService { public async Task<ClassificationListResponse> GetClassificationList(ClassificationRequest request) { // 模拟数据源 string json = @" [ {""id"":""1"",""name"":""AA"",""sort"":1}, {""id"":""2"",""name"":""BB"",""sort"":2}, {""id"":""3"",""name"":""CC"",""sort"":3}, {""id"":""4"",""name"":""DD"",""sort"":4} ]"; ​ var classificationList = JsonSerializer.Deserialize<List<ClassificationInfo>>(json); if (!string.IsNullOrEmpty(request.key)) { classificationList = classificationList? .Where(x => x.name.Contains(request.key)).ToList(); } ​ return new ClassificationListResponse { classifications = classificationList ?? new List<ClassificationInfo>() }; } ​ public async Task<HealthCheckResponse> HealthCheck(HealthCheckRequest request) { return new HealthCheckResponse { status = "UP", message = "服务运行正常", version = "1.0.0" }; } }
Grpc 层(协议适配)
// Services/Grpc/ClassificationGrpcService.cs public class ClassificationGrpcService : Protos.ClassificationDubboService.ClassificationDubboServiceBase { private readonly IClassificationService _classificationService; private readonly ILogger<ClassificationGrpcService> _logger; ​ public ClassificationGrpcService( IClassificationService classificationService, ILogger<ClassificationGrpcService> logger) { _classificationService = classificationService; _logger = logger; } ​ public override async Task<Protos.ClassificationListResponse> GetClassificationList( Protos.ClassificationRequest request, ServerCallContext context) { try { _logger.LogInformation($"收到 gRPC 调用 GetClassificationList,key={request.Key}"); ​ // DTO 转换 var dtoRequest = new DTO.ClassificationRequest { key = request.Key }; var dtoResponse = await _classificationService.GetClassificationList(dtoRequest); ​ // Proto 转换 var response = new Protos.ClassificationListResponse(); foreach (var item in dtoResponse.classifications) { response.Classifications.Add(new Protos.ClassificationInfo { Id = item.id, Name = item.name, Sort = item.sort }); } ​ return response; } catch (Exception ex) { _logger.LogError(ex, "gRPC GetClassificationList 调用失败"); throw; } } }

Grpc 层只做三件事:接收请求 → 转换 DTO → 调用 Business 层 → 转换响应。不写任何业务逻辑。


八、gRPC 服务消费(Consumer 端)

.NET 同时也是消费者,需要调用 Java 侧的 Dubbo-gRPC 服务(如字典服务)。

8.1 获取 Java 侧的 Proto 文件

从 Java 团队拿到.proto文件后放到Protos/Consumer/目录:

// Protos/Consumer/dictservice.proto syntax = "proto3"; ​ package com.cn.common.api.dubbo; option csharp_namespace = "GrpcServiceDemo"; ​ message DictRequest { int32 tenant_id = 1; string key = 2; } ​ message DictListResponse { repeated DictResponse dicts = 1; } ​ // ... 其他消息定义省略 ... ​ service DictDubboService { rpc getDictsByKey(DictRequest) returns (DictListResponse); // ... 其他方法 ... }

8.2 从 Nacos 发现服务并发起 gRPC 调用

// Controllers/ValuesController.cs [ApiController] [Route("api/[controller]")] public class ValuesController : ControllerBase { private readonly Nacos.V2.INacosNamingService _svc; private readonly ILogger<ValuesController> _logger; ​ public ValuesController( Nacos.V2.INacosNamingService svc, ILogger<ValuesController> logger) { _svc = svc; _logger = logger; } ​ [HttpGet("TestCallBygRPC")] public async Task<string> TestCallBygRPC() { try { // 1. 从 Nacos 获取健康实例 var instance = await _svc.SelectOneHealthyInstance( "providers:com.cn.common.api.dubbo.DictDubboService::", "dubbo_core_group" ); if (instance == null) return "服务实例不可用"; ​ // 2. 拼接 gRPC 地址(注意:用 gRPC 端口,不是 Dubbo 注册的端口) var address = $"http://{instance.Ip}:8084"; var channel = GrpcChannel.ForAddress(address); ​ // 3. 构造请求 var request = new DictRequest { TenantId = 0, Key = "busReqTypeDict" }; ​ // 4. 调用远程服务 var client = new DictDubboService.DictDubboServiceClient(channel); var response = await client.getDictsByKeyAsync(request); ​ return response.ToString(); } catch (RpcException ex) { _logger.LogError(ex, $"gRPC 调用失败:{ex.Status}"); return $"错误:{ex.Status.Detail}"; } } }

几个关键点

  1. 服务名格式:Dubbo 注册到 Nacos 的服务名是providers:{接口全限定名}::这种格式,不是简单的服务名

  2. 端口问题:Nacos 返回的instance.Port是 Dubbo 协议端口,gRPC 通常在不同端口(如 8084),需要硬编码或通过元数据获取

  3. SelectOneHealthyInstance:Nacos SDK 自带负载均衡,自动选择一个健康实例


九、自动注册机制(告别手动配置)

每新增一个 Service 或 gRPC 服务都要手动注册?太累了。通过反射扫描实现自动注册。

9.1 自动注册 Business 层

// Common/ServiceCollectionExtensions.cs public static IServiceCollection AddServicesByAssembly(this IServiceCollection services) { var assembly = Assembly.GetExecutingAssembly(); ​ var serviceTypes = assembly.GetTypes() .Where(t => t.IsClass && !t.IsAbstract && t.Name.EndsWith("Service") && t.GetInterfaces().Any() ).ToList(); ​ foreach (var type in serviceTypes) { var interfaceType = type.GetInterfaces() .FirstOrDefault(i => i.Name == $"I{type.Name}"); if (interfaceType != null) { services.AddScoped(interfaceType, type); } } ​ return services; }

约定规则

  • 类名以Service结尾

  • 实现了对应的I{Name}Service接口

  • 例如ClassificationServiceIClassificationService

9.2 自动映射 gRPC 服务

public static IEndpointRouteBuilder MapGrpcServicesByAssembly(this IEndpointRouteBuilder endpoints) { var assembly = Assembly.GetExecutingAssembly(); ​ var grpcServiceTypes = assembly.GetTypes() .Where(t => t.IsClass && !t.IsAbstract && t.Name.EndsWith("GrpcService") && t.BaseType != null && t.BaseType.Name.EndsWith("ServiceBase") ).ToList(); ​ var mapGrpcServiceMethod = typeof(GrpcEndpointRouteBuilderExtensions) .GetMethods(BindingFlags.Public | BindingFlags.Static) .FirstOrDefault(m => m.Name == "MapGrpcService" && m.IsGenericMethod && m.GetParameters().Length == 1 ); ​ foreach (var serviceType in grpcServiceTypes) { if (mapGrpcServiceMethod != null) { var genericMethod = mapGrpcServiceMethod.MakeGenericMethod(serviceType); genericMethod.Invoke(null, new object[] { endpoints }); } } ​ return endpoints; }

约定规则

  • 类名以GrpcService结尾

  • 继承自 proto 生成的ServiceBase

9.3 Program.cs 一行搞定

// 自动注册所有 Service builder.Services.AddServicesByAssembly(); ​ // ... 其他配置 ... ​ // 自动映射所有 gRPC 服务 app.MapGrpcServicesByAssembly();

之后新增业务,只需要:

  1. I{Name}Service接口

  2. {Name}Service实现类

  3. {Name}GrpcService适配类

零配置,自动生效。


十、Proto 自动生成工具(亮点功能)

这是本项目最值得分享的功能:从 C# 代码反向生成 .proto 文件

传统流程是先写.proto→ 生成 C# 代码 → 写业务逻辑。但在 .NET 先行的项目中,我们往往先设计 C# 接口,再把契约给 Java 团队。手动写.proto容易出错且重复劳动。

10.1 设计思路

C# 接口定义 + DTO → [ProtoService] / [ProtoMessage] 标记 → 运行生成工具 → 输出 .proto 文件

10.2 自定义属性

// Common/ProtoGen/ProtoAttributes.cs ​ /// <summary> /// 标记需要生成 .proto 的服务接口 /// </summary> [AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class)] public class ProtoServiceAttribute : Attribute { public string Package { get; set; } // proto 包名 public string ServiceName { get; set; } // 服务名 public string CSharpNamespace { get; set; } // C# 命名空间 } ​ /// <summary> /// 标记需要生成 proto 消息的类 /// </summary> [AttributeUsage(AttributeTargets.Class | AttributeTargets.Enum)] public class ProtoMessageAttribute : Attribute { } ​ /// <summary> /// 标记字段编号 /// </summary> [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] public class ProtoFieldAttribute : Attribute { public int Number { get; set; } public ProtoFieldAttribute(int number) => Number = number; }

10.3 标记 DTO

// DTO/ProtoMessageClasses.cs [ProtoMessage] public class ClassificationRequest { [ProtoField(1)] public string key { get; set; } } ​ [ProtoMessage] public class ClassificationInfo { [ProtoField(1)] public string id { get; set; } ​ [ProtoField(2)] public string name { get; set; } ​ [ProtoField(3)] public int sort { get; set; } } ​ [ProtoMessage] public class ClassificationListResponse { [ProtoField(1)] public List<ClassificationInfo> classifications { get; set; } }

10.4 核心生成器

生成器通过反射扫描程序集,自动完成:

  1. 找到所有[ProtoService]标记的接口

  2. 收集接口方法中用到的请求/响应类型

  3. 额外收集[ProtoMessage]标记的类型

  4. 生成messageservice定义

  5. C# 类型自动映射为 proto 类型(stringstringintint32List<T>repeated T

  6. 属性名自动转 snake_case(ClassificationListclassification_list

// 核心类型映射 private string GetProtoType(Type type) { if (type == typeof(string)) return "string"; if (type == typeof(int) || type == typeof(int?)) return "int32"; if (type == typeof(long) || type == typeof(long?)) return "int64"; if (type == typeof(bool) || type == typeof(bool?)) return "bool"; if (type == typeof(double) || type == typeof(double?)) return "double"; if (type == typeof(float) || type == typeof(float?)) return "float"; if (type == typeof(byte[])) return "bytes"; if (type.IsEnum) return type.Name; return type.Name; // 自定义消息类型 }

10.5 一键生成

dotnet run -- --generate-proto

输出到Protos/Producer/classification_service_generated.proto,直接交给 Java 团队即可。

10.6 生成结果

syntax = "proto3"; ​ package com.cn.order.api.dubbo; option csharp_namespace = "NacosDemo_order.Protos"; ​ message ClassificationRequest { string key = 1; } ​ message ClassificationListResponse { repeated ClassificationInfo classifications = 1; } ​ message HealthCheckRequest { } ​ message HealthCheckResponse { string status = 1; string message = 2; string version = 3; } ​ message ClassificationInfo { string id = 1; string name = 2; int32 sort = 3; } ​ service ClassificationDubboService { rpc GetClassificationList(ClassificationRequest) returns (ClassificationListResponse); rpc HealthCheck(HealthCheckRequest) returns (HealthCheckResponse); }

十一、Program.cs 完整配置

using Nacos.AspNetCore.V2; using NacosDemo_order.Common; using NacosDemo_order.Tools; ​ // Proto 生成工具入口(独立运行) if (args.Length > 0 && args[0] == "--generate-proto") { ProtoGenerationTool.Run(args); return; } ​ var builder = WebApplication.CreateBuilder(args); ​ // 1. 自动注册 Business 层服务 builder.Services.AddServicesByAssembly(); ​ // 2. 注册控制器和 gRPC builder.Services.AddControllers(); builder.Services.AddGrpc(); builder.Services.AddHealthChecks(); ​ // 3. 注册 Nacos(一行搞定) builder.Services.AddNacosAspNet(builder.Configuration, "nacos"); ​ // 4. 注册 HttpClientFactory builder.Services.AddHttpClient("nacosService"); ​ // 5. Swagger builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(options => { options.SwaggerDoc("v1", new OpenApiInfo { Title = "Order Service API", Version = "v1", Description = "基于 .NET Core + Nacos + gRPC 的订单服务接口文档" }); }); ​ var app = builder.Build(); ​ // 中间件 if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(options => { options.SwaggerEndpoint("/swagger/v1/swagger.json", "Order Service v1"); options.RoutePrefix = string.Empty; }); } ​ app.UseRouting(); app.UseAuthorization(); ​ // 映射控制器 app.MapControllers(); ​ // 自动映射所有 gRPC 服务(无需手动添加) app.MapGrpcServicesByAssembly(); ​ // 健康检查端点 app.MapHealthChecks("/health"); ​ app.Run();

十二、踩坑记录

坑 1:Nacos 注册 IP 不对

现象:Nacos 控制台显示的 IP 是127.0.0.1或某个内网不可达的 IP,Java 侧调不通。

解决:配置PreferredNetworks,指定注册 IP 的前缀匹配:

"PreferredNetworks": "XX.XXX."

SDK 会优先选择 IP 前缀匹配的网卡地址注册。

坑 2:gRPC 端口与 Dubbo 端口混淆

现象:从 Nacos 拿到实例后直接用instance.Port连 gRPC,报连接失败。

解决:Dubbo 注册到 Nacos 的端口是 Dubbo 协议端口(如 20880),gRPC 通常在另一个端口(如 8084)。需要单独约定或通过 Nacos 元数据传递:

// 方式 1:硬编码 gRPC 端口(简单场景) var address = $"http://{instance.Ip}:8084"; ​ // 方式 2:通过元数据获取(推荐) var grpcPort = instance.Metadata.TryGetValue("grpcPort", out var port) ? port : "8084"; var address = $"http://{instance.Ip}:{grpcPort}";

坑 3:Proto 包名不匹配

现象:Java 侧通过 Nacos 找到服务,但 gRPC 调用报UNIMPLEMENTED

解决.proto文件中的package必须与 Java Dubbo 接口的包名完全一致。Java 侧是com.cn.order.api.dubbo,proto 也必须是同样的包名,否则 gRPC 的 service 路径对不上。

坑 4:HTTP/2 和浏览器不兼容

现象:Swagger 页面正常,但 gRPC 调用失败。

解决:Kestrel 双端口方案,HTTP 端口(5073)走Http1AndHttp2,gRPC 专用端口(8084)走纯Http2。这样 Swagger 和 gRPC 各走各的。


十三、Java 侧调用指南

把生成的.proto文件给 Java 团队后,他们需要:

  1. 将 proto 文件放入项目中,使用protoc生成 Java Stub

  2. 通过 Nacos 发现 .NET 服务实例

  3. 使用 Dubbo-gRPC 协议调用

服务发现信息

Nacos 服务名providers:com.cn.order.api.dubbo.ClassificationDubboService::
分组dubbo_core_group
命名空间dubbo
gRPC 端口8084

十四、扩展指南

新增业务服务

只需 3 步:

# 1. 写接口 Interface/IOrderService.cs ​ # 2. 写业务实现 Services/Business/OrderService.cs ​ # 3. 写 gRPC 适配 Services/Grpc/OrderGrpcService.cs

编译启动,自动注册,零配置。

新增 Proto 消费

  1. 把 Java 团队的.proto文件放到Protos/Consumer/

  2. .csproj中添加<Protobuf Include="Protos/Consumer/xxx.proto" GrpcServices="Client" />

  3. 注入INacosNamingService发现服务,用生成的 Client 类发起调用


十五、总结

本文从实战角度,完整覆盖了 .NET Core 接入 Nacos + gRPC 的关键路径:

模块核心要点
Nacos 注册AddNacosAspNet一行搞定,注意PreferredNetworks
gRPC Provider分层架构(Business + Grpc),协议与逻辑解耦
gRPC ConsumerNacos 发现 → 拼地址 → 创建 Client → 调用
自动注册反射扫描,约定优于配置,新增服务零配置
Proto 生成C# 代码 → .proto 文件,跨团队协作利器
双端口方案Kestrel 多 Endpoint,HTTP/1.1 和 HTTP/2 各得其所