ByteString
google.protobuf.BytesValue
字节
Protobuf 支持标量值类型为 bytes
的二进制有效负载。 C# 中生成的属性使用 ByteString
作为属性类型。
使用 ByteString.CopyFrom(byte[] data)
从字节数组创建新实例:
var data = await File.ReadAllBytesAsync(path);
var payload = new PayloadResponse();
payload.Data = ByteString.CopyFrom(data);
使用 ByteString.Span
或 ByteString.Memory
直接访问 ByteString
数据。 或调用 ByteString.ToByteArray()
将实例转换回字节数组:
var payload = await client.GetPayload(new PayloadRequest());
await File.WriteAllBytesAsync(path, payload.Data.ToByteArray());
集合
列表
Protobuf 中,在字段上使用 repeated
前缀关键字指定列表。 以下示例演示如何创建列表:
message Person {
// ...
repeated string roles = 8 ;
}
在生成的代码中,repeated
字段由 Google.Protobuf.Collections.RepeatedField<T>
泛型类型表示。
public class Person
{
// ...
public RepeatedField<string > Roles { get ; }
}
RepeatedField<T>
实现了 IList 。 因此你可使用 LINQ 查询,或者将其转换为数组或列表。 RepeatedField<T>
属性没有公共 setter。 项应添加到现有集合中。
var person = new Person();
person.Roles.Add( " user " );
var roles = new [] { " admin " , " manager " };
person.Roles.Add(roles);
var list = roles.ToList();
字典
.NET IDictionary 类型在 Protobuf 中使用 map<key_type, value_type>
表示。
ProtoBuf复制
message Person {
// ...
map<string , string > attributes = 9 ;
}
在生成的 .NET 代码中,map
字段由 Google.Protobuf.Collections.MapField<TKey, TValue>
泛型类型表示。 MapField<TKey, TValue>
实现了 IDictionary。 与 repeated
属性一样,map
属性没有公共 setter。 项应添加到现有集合中。
var person = new Person();
// 添加一项
person.Attributes[" created_by " ] = " James " ;
// 添加多项
var attributes = new Dictionary<string , string >
{
[ " last_modified " ] = DateTime.UtcNow.ToString()
};
person.Attributes.Add(attributes);
4. 创建 gRPC 服务和方法
本文档介绍如何以 C# 创建 gRPC 服务和方法。 包括:
如何在 .proto
文件中定义服务和方法。
使用 gRPC C# 工具生成的代码。
实现 gRPC 服务和方法。
创建新的 gRPC 服务
设置appsetting.json
" Kestrel " : {
" EndpointDefaults " : {
" Protocols " : " Http2 "
}
}
或者Program.cs 中配置如下代码:
// Gprc 需要 Http2.0
builder.WebHost.UseKestrel(p =>
{
p.ConfigureEndpointDefaults(opt =>
{
opt.Protocols = HttpProtocols.Http2;
});
});
引用包:
Grpc.AspNetCore 2.50 .0
服务和消息是在 .proto
文件中定义的。 然后,C# 工具从 .proto
文件生成代码。 对于服务器端资产,将为每个服务生成一个抽象基类型,同时为所有消息生成类。
以下 .proto
文件:
syntax = " proto3 " ;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}
message HelloRequest {
string name = 1 ;
}
message HelloReply {
string message = 1 ;
}
C# 工具生成 C# GreeterBase
基类型:
public abstract partial class GreeterBase
{
public virtual Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
{
throw new RpcException(new Status(StatusCode.Unimplemented, "" ));
}
}
public class HelloRequest
{
public string Name { get ; set ; }
}
public class HelloReply
{
public string Message { get ; set ; }
}
默认情况下,生成的 GreeterBase
不执行任何操作。 它的虚拟 SayHello
方法会将 UNIMPLEMENTED
错误返回到调用它的任何客户端。 为了使服务有用,应用必须创建 GreeterBase
的具体实现:
public class GreeterService : GreeterBase
{
public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
{
return Task.FromResult(new HelloReply { Message = $" Hello {request.Name} " });
}
}
ServerCallContext
提供服务器端调用的上下文。
服务实现已注册到应用。 如果服务由 ASP.NET Core gRPC 托管,则应使用 MapGrpcService
方法将其添加到路由管道。
app.MapGrpcService<GreeterService>();
实现 gRPC 方法
gRPC 服务可以有不同类型的方法。 服务发送和接收消息的方式取决于所定义的方法的类型。 gRPC 方法类型如下:
一元
服务器流式处理
客户端流式处理
双向流式处理
流式处理调用是使用 stream
关键字在 .proto
文件中指定的。 stream
可以放置在调用的请求消息和/或响应消息中。
syntax = " proto3 " ;
import " google/protobuf/empty.proto " ; // 无参包
service ExampleService {
// 无参方法
rpc GetPerson1(google.protobuf.Empty) returns (PersonResponse);
// 一元
rpc UnaryCall (ExampleRequest) returns (ExampleResponse);
// 服务器流式处理
rpc StreamingFromServer (ExampleRequest) returns (stream ExampleResponse);
// 客户端流式处理
rpc StreamingFromClient (stream ExampleRequest) returns (ExampleResponse);
// 双向流式处理
rpc StreamingBothWays (stream ExampleRequest) returns (stream ExampleResponse);
}
每个调用类型都有不同的方法签名。 在具体实现中替代从抽象基本服务类型生成的方法,可确保使用正确的参数和返回类型。
一元方法
一元方法将请求消息作为参数,并返回响应。 返回响应时,一元调用完成。
public override Task<ExampleResponse> UnaryCall(ExampleRequest request,
ServerCallContext context)
{
var response = new ExampleResponse();
return Task.FromResult(response);
}
一元调用与 Web API 控制器上的操作最为相似。 gRPC 方法与操作的一个重要区别是,gRPC 方法无法将请求的某些部分绑定到不同的方法参数。 对于传入请求数据,gRPC 方法始终有一个消息参数。 通过在请求消息中设置多个值字段,仍可以将多个值发送到 gRPC 服务:
message ExampleRequest {
int32 pageIndex = 1 ;
int32 pageSize = 2 ;
bool isDescending = 3 ;
}
服务器流式处理方法
服务器流式处理方法将请求消息作为参数。 由于可以将多个消息流式传输回调用方,因此可使用 responseStream.WriteAsync
发送响应消息。 当方法返回时,服务器流式处理调用完成。
public override async Task StreamingFromServer(ExampleRequest request,
IServerStreamWriter <ExampleResponse> responseStream, ServerCallContext context)
{
for (var i = 0 ; i < 5 ; i++)
{
await responseStream.WriteAsync(new ExampleResponse());
await Task.Delay(TimeSpan.FromSeconds(1 ));
}
}
服务器流式处理方法启动后,客户端无法发送其他消息或数据。 某些流式处理方法设计为永久运行。 对于连续流式处理方法,客户端可以在不再需要调用时将其取消。 当发生取消时,客户端会将信号发送到服务器,并引发 ServerCallContext.CancellationToken 。 应在服务器上通过异步方法使用 CancellationToken
标记,以实现以下目的:
所有异步工作都与流式处理调用一起取消。
该方法快速退出。
public override async Task StreamingFromServer(ExampleRequest request,
IServerStreamWriter <ExampleResponse> responseStream, ServerCallContext context)
{
while (!context.CancellationToken.IsCancellationRequested)
{
await responseStream.WriteAsync(new ExampleResponse());
await Task.Delay(TimeSpan.FromSeconds(1 ), context.CancellationToken);
}
}
客户端流式处理方法
客户端流式处理方法在该方法没有接收消息的情况下启动。 requestStream
参数用于从客户端读取消息。 返回响应消息时,客户端流式处理调用完成:
public override async Task<ExampleResponse> StreamingFromClient(
IAsyncStreamReader <ExampleRequest> requestStream, ServerCallContext context)
{
while (await requestStream.MoveNext())
{
var message = requestStream.Current;
// ...
}
return new ExampleResponse();
}
如果使用 C# 8 或更高版本,则可使用 await foreach
语法来读取消息。 IAsyncStreamReader<T>.ReadAllAsync()
扩展方法读取请求数据流中的所有消息:
public override async Task<ExampleResponse> StreamingFromClient(
IAsyncStreamReader <ExampleRequest> requestStream, ServerCallContext context)
{
await foreach (var message in requestStream.ReadAllAsync())
{
// ...
}
return new ExampleResponse();
}
双向流式处理方法
双向流式处理方法在该方法没有接收到消息的情况下启动。 requestStream
参数用于从客户端读取消息。 该方法可选择使用 responseStream.WriteAsync
发送消息。 当方法返回时,双向流式处理调用完成:
public override async Task StreamingBothWays(IAsyncStreamReader<ExampleRequest> requestStream,
IServerStreamWriter <ExampleResponse> responseStream, ServerCallContext context)
{
await foreach (var message in requestStream.ReadAllAsync())
{
await responseStream.WriteAsync(new ExampleResponse());
}
}
前面的代码:
可以支持更复杂的方案,例如同时读取请求和发送响应:
public override async Task StreamingBothWays(IAsyncStreamReader<ExampleRequest> requestStream,
IServerStreamWriter <ExampleResponse> responseStream, ServerCallContext context)
{
// 读取后台任务中的请求。
var readTask = Task.Run(async () =>
{
await foreach (var message in requestStream.ReadAllAsync())
{
// Process request.
}
});
// 发送响应,直到客户端发出完成的信号。
while (!readTask.IsCompleted)
{
await responseStream.WriteAsync(new ExampleResponse());
await Task.Delay(TimeSpan.FromSeconds(1 ), context.CancellationToken);
}
}
在双向流式处理方法中,客户端和服务可在任何时间互相发送消息。 双向方法的最佳实现根据需求而有所不同。
访问 gRPC 请求标头
请求消息并不是客户端将数据发送到 gRPC 服务的唯一方法。 标头值在使用 ServerCallContext.RequestHeaders
的服务中可用。
public override Task<ExampleResponse> UnaryCall(ExampleRequest request, ServerCallContext context)
{
var userAgent = context.RequestHeaders.GetValue(" user-agent " );
// ...
return Task.FromResult(new ExampleResponse());
}
多线程处理
实现使用多个线程的 gRPC 流式处理方法有一些重要的注意事项。
IAsyncStreamReader<TMessage>
和 IServerStreamWriter<TMessage>
一次只能由一个线程使用。 对于流式处理 gRPC 方法,多个线程无法使用 requestStream.MoveNext()
同时读取新消息。 多个线程无法使用 responseStream.WriteAsync(message)
同时写入新消息。
多个线程能够与 gRPC 方法实现交互的一种安全方法是将生成方-使用者模式与 System.Threading.Channels 配合使用。
public override async Task DownloadResults(DataRequest request,
IServerStreamWriter <DataResult> responseStream, ServerCallContext context)
{
var channel = Channel.CreateBounded<DataResult>(new BoundedChannelOptions(capacity: 5 ));
var consumerTask = Task.Run(async () =>
{
// 从通道中消费消息并写入响应流
await foreach (var message in channel.Reader.ReadAllAsync())
{
await responseStream.WriteAsync(message);
}
});
var dataChunks = request.Value.Chunk(size: 10 );
// 从多个线程向通道写入消息
await Task.WhenAll(dataChunks.Select(
async c =>
{
var message = new DataResult { BytesProcessed = c.Length };
await channel.Writer.WriteAsync(message);
}));
// 完成写作,等待消费者完成
channel.Writer.Complete();
await consumerTask;
}
备注
双向流式处理方法采用 IAsyncStreamReader<TMessage>
和 IServerStreamWriter<TMessage>
作为自变量。 在彼此独立的线程上使用这些类型是安全的。
5. gRPC 客户端
.NET 客户端调用 gRPC
Grpc.Net.Client NuGet 包提供了 .NET gRPC 客户端库。 本文档介绍如何执行以下操作:
配置 gRPC 客户端
需要安装的包:
Grpc.Net.Client 2.50 .0
Google.Protobuf 3.20 .0
Grpc.Tools 2.50 .0
gRPC 客户端是从 .proto
文件生成的具体客户端类型。 具体 gRPC 客户端具有转换为 .proto
文件中 gRPC 服务的方法。 例如,名为 Greeter
的服务生成 GreeterClient
类型(包含调用服务的方法)。
gRPC 客户端是通过通道创建的。 首先使用 GrpcChannel.ForAddress
创建一个通道,然后使用该通道创建 gRPC 客户端:
var channel = GrpcChannel.ForAddress(" https://localhost:5001 " );
var client = new Greet.GreeterClient(channel);
通道表示与 gRPC 服务的长期连接。 创建通道后,进行配置,使其具有与调用服务相关的选项。 例如,可在 GrpcChannelOptions
上指定用于调用的 HttpClient
、发收和接收消息的最大大小以及记录日志,并将其与 GrpcChannel.ForAddress
一起使用。 有关选项的完整列表,请参阅 客户端配置选项
。
var channel = GrpcChannel.ForAddress(" https://localhost:5001 " );
var greeterClient = new Greet.GreeterClient(channel);
var counterClient = new Count.CounterClient(channel);
配置 TLS
gRPC 客户端必须使用与被调用服务相同的连接级别安全性 。 gRPC 客户端传输层安全性 (TLS) 是在创建 gRPC 通道时配置的。 如果在调用服务时通道和服务的连接级别安全性不一致,gRPC 客户端就会抛出错误。
若要将 gRPC 通道配置为使用 TLS,请确保服务器地址以 https
开头。 例如,GrpcChannel.ForAddress("https://localhost:5001")
使用 HTTPS 协议。 gRPC 通道自动协商由 TLS 保护的连接,并使用安全连接进行 gRPC 调用。
若要调用不安全的 gRPC 服务,请确保服务器地址以 http
开头。 例如,GrpcChannel.ForAddress("http://localhost:5000")
使用 HTTP 协议。 在 .NET Core 3.1 中,必须进行其他配置,才能使用 .NET 客户端调用不安全的 gRPC 服务。
// Net Core 3.1 支持HTTP
AppContext.SetSwitch(
" System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport " , true );
var channel = GrpcChannel.ForAddress(" http://localhost:5000 " );
var client = new Greet.GreeterClient(channel);
客户端性能
通道及客户端性能和使用情况:
创建通道成本高昂。 重用 gRPC 调用的通道可提高性能。
gRPC 客户端是使用通道创建的。 gRPC 客户端是轻型对象,无需缓存或重用。
可从一个通道创建多个 gRPC 客户端(包括不同类型的客户端)。
通道和从该通道创建的客户端可由多个线程安全使用。
从通道创建的客户端可同时进行多个调用。
GrpcChannel.ForAddress
不是创建 gRPC 客户端的唯一选项。 如果要从 ASP.NET Core 应用调用 gRPC 服务,请考虑 gRPC 客户端工厂集成 。 gRPC 与 HttpClientFactory
集成是创建 gRPC 客户端的集中式操作备选方案。
一元调用
一元调用从客户端发送请求消息开始。 服务结束后,返回响应消息。
var client = new Greet.GreeterClient(channel);
var response = await client.SayHelloAsync(new HelloRequest { Name = " World " });
Console.WriteLine( " Greeting: " + response.Message);
// Greeting: Hello World
.proto
文件中的每个一元服务方法将在用于调用方法的具体 gRPC 客户端类型上产生两个 .NET 方法:异步方法和阻塞方法。 例如,GreeterClient
具有两种调用 SayHello
的方法:
服务器流式处理调用
服务器流式处理调用从客户端发送请求消息开始。 ResponseStream.MoveNext()
读取从服务流式处理的消息。 ResponseStream.MoveNext()
返回 false
时,服务器流式处理调用已完成。
var client = new Greet.GreeterClient(channel);
using var call = client.SayHellos(new HelloRequest { Name = " World " });
while (await call.ResponseStream.MoveNext())
{
Console.WriteLine( " Greeting: " + call.ResponseStream.Current.Message);
// "Greeting: Hello World" is written multiple times
}
如果使用 C# 8 或更高版本,则可使用 await foreach
语法来读取消息。 IAsyncStreamReader<T>.ReadAllAsync()
扩展方法读取响应数据流中的所有消息:
var client = new Greet.GreeterClient(channel);
using var call = client.SayHellos(new HelloRequest { Name = " World " });
await foreach (var response in call.ResponseStream.ReadAllAsync())
{
Console.WriteLine( " Greeting: " + response.Message);
// "Greeting: Hello World" is written multiple times
}
客户端流式处理调用
客户端无需发送消息即可开始客户端流式处理调用 。 客户端可选择使用 RequestStream.WriteAsync
发送消息。 客户端发送完消息后,应调用 RequestStream.CompleteAsync()
来通知服务。 服务返回响应消息时,调用完成。
var client = new Counter.CounterClient(channel);
using var call = client.AccumulateCount();
for (var i = 0 ; i < 3 ; i++)
{
await call.RequestStream.WriteAsync(new CounterRequest { Count = 1 });
}
// 客户端发完消息,通知服务端返回数据
await call.RequestStream.CompleteAsync();
var response = await call;
Console.WriteLine($ " Count: {response.Count} " );
// Count: 3
双向流式处理调用
客户端无需发送消息即可开始双向流式处理调用 。 客户端可选择使用 RequestStream.WriteAsync
发送消息。 使用 ResponseStream.MoveNext()
或 ResponseStream.ReadAllAsync()
可访问从服务流式处理的消息。 ResponseStream
没有更多消息时,双向流式处理调用完成。
var client = new Echo.EchoClient(channel);
using var call = client.Echo();
Console.WriteLine( " Starting background task to receive messages " );
var readTask = Task.Run(async () =>
{
await foreach (var response in call.ResponseStream.ReadAllAsync())
{
Console.WriteLine(response.Message);
// Echo messages sent to the service
}
});
Console.WriteLine( " Starting to send messages " );
Console.WriteLine( " Type a message to echo then press enter. " );
while (true )
{
var result = Console.ReadLine();
if (string .IsNullOrEmpty(result))
{
break ;
}
await call.RequestStream.WriteAsync(new EchoMessage { Message = result });
}
Console.WriteLine( " Disconnecting " );
await call.RequestStream.CompleteAsync();
await readTask;
为获得最佳性能并避免客户端和服务中出现不必要的错误,请尝试正常完成双向流式调用。 当服务器已读取请求流且客户端已读取响应流时,双向调用正常完成。 前面的示例调用就是一个正常结束的双向调用。 在调用中,客户端:
通过调用 EchoClient.Echo
启动新的双向流式调用。
使用 ResponseStream.ReadAllAsync()
创建用于从服务中读取消息的后台任务。
使用 RequestStream.WriteAsync
将消息发送到服务器。
使用 RequestStream.CompleteAsync()
通知服务器它已发送消息。
等待直到后台任务已读取所有传入消息。
双向流式处理调用期间,客户端和服务可在任何时间互相发送消息。 与双向调用交互的最佳客户端逻辑因服务逻辑而异。
访问 gRPC 标头
gRPC 调用返回响应头。 HTTP 响应头传递与返回的消息不相关的调用的名称/值元数据。
标头可通过 ResponseHeadersAsync
进行访问,它会返回元数据的集合。 标头通常随响应消息一起返回;因此,必须等待它们返回。
var client = new Greet.GreeterClient(channel);
using var call = client.SayHelloAsync(new HelloRequest { Name = " World " });
var headers = await call.ResponseHeadersAsync;
var myValue = headers.GetValue(" my-trailer-name " );
var response = await call.ResponseAsync;
使用 ResponseHeadersAsync
时:
必须等待 ResponseHeadersAsync
的结果才能获取标头集合。
无需在 ResponseAsync
(或流式处理时的响应流)之前访问。 如果已返回响应,则 ResponseHeadersAsync
立即返回标头。
如果存在连接或服务器错误,并且 gRPC 调用未返回标头,将引发异常。
配置截止时间
建议配置 gRPC 调用截止时间,因为它提供调用时间的上限。 它能阻止异常运行的服务持续运行并耗尽服务器资源。 截止时间对于构建可靠应用非常有效。
配置 CallOptions.Deadline
以设置 gRPC 调用的截止时间:
var client = new Greet.GreeterClient(channel);
try
{
var response = await client.SayHelloAsync(
new HelloRequest { Name = " World " },
deadline: DateTime.UtcNow.AddSeconds( 5 ));
// Greeting: Hello World
Console.WriteLine(" Greeting: " + response.Message);
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.DeadlineExceeded)
{
Console.WriteLine( " Greeting timeout. " );
}
客户端工厂
gRPC 与 HttpClientFactory
的集成提供了一种创建 gRPC 客户端的集中方式。 它可用作配置独立 gRPC 客户端实例
(上一节内容)的替代方法。 Grpc.Net.ClientFactory NuGet 包中提供了工厂集成。
工厂具有以下优势:
提供了用于配置逻辑 gRPC 客户端实例的中心位置。
可管理基础 HttpClientMessageHandler
的生存期。
在 ASP.NET Core gRPC 服务中自动传播截止时间和取消。
注册 gRPC 客户端
若要注册 gRPC 客户端,可在 Program.cs
中的应用入口点处的 WebApplicationBuilder 的实例中使用通用的 AddGrpcClient
扩展方法,并指定 gRPC 类型化客户端类和服务地址:
builder.Services.AddGrpcClient<UserService.UserServiceClient>(p =>
{
p.Address = new Uri(" http://localhost:5227 " );
});
gRPC 客户端类型通过依赖项注入 (DI) 注册为暂时性。 现在可以在由 DI 创建的类型中直接注入和使用客户端。 ASP.NET Core MVC 控制器、SignalR 中心和 gRPC 服务是可以自动注入 gRPC 客户端的位置:
[Route(" [controller]/[action] " )]
[ApiController]
public class UserController:ControllerBase
{
private readonly UserService.UserServiceClient _userClient;
public UserController(UserService.UserServiceClient userClient)
{
_userClient = userClient;
}
[HttpGet]
public IActionResult GetUserList()
{
return Ok(_userClient.GetUserList(new UserRequest()));
}
}
调用凭据
使用 AddCallCredentials
方法将身份验证标头添加到 gRPC 调用, 此方法在 Grpc.Net.ClientFactory 版本 2.46.0 以上版本中可用。
builder.Services.AddHttpContextAccessor();
// 客户端配置JWT
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(p =>
{
// ...
});
builder.Services.AddAuthorization();
builder.Services
.AddGrpcClient <Greeter.GreeterClient>(o =>
{
o.Address = new Uri(" https://localhost:5001 " );
})
// 添加调用凭据
.AddCallCredentials(async (context, metadata) =>
{
var serviceProvider = builder.Services.BuildServiceProvider();
var httpContextAccessor = serviceProvider.GetService<IHttpContextAccessor>();
// 获取客户端的token
var accessToken = await httpContextAccessor.HttpContext.GetTokenAsync(" access_token " );
if (string .IsNullOrWhiteSpace(accessToken))
{
// 如果客户端没有拿到token ,直接从授权中心拿Token(这一步通常用不上)
var channel = GrpcChannel.ForAddress(" http://localhost:5015 " );
accessToken = new TokenService.TokenServiceClient(channel).GetToken(new ()
{
UserName = " admin " ,
Pwd = " 123 "
}).AccessToken;
channel.Dispose();
}
if (!string .IsNullOrEmpty(accessToken))
{
metadata.Add( " Authorization " , $" Bearer {accessToken} " );
}
}).ConfigureChannel(o => o.UnsafeUseInsecureChannelCallCredentials = true );
var app = builder.Build();
// ...
app.UseAuthentication();
app.UseAuthorization();
// ...
有关配置调用凭据的详细信息,请参gRPC身份验证和授权
。
截止时间与取消
截止时间和取消功能是 gRPC 客户端用来中止进行中调用的功能。 本文介绍截止时间和取消功能非常重要的原因,以及如何在 .NET gRPC 应用中使用它们。
截止时间
截止时间功能让 gRPC 客户端可以指定等待调用完成的时间。 超过截止时间时,将取消调用。 设定一个截止时间非常重要,因为它将提供调用可运行的最长时间。 它能阻止异常运行的服务持续运行并耗尽服务器资源。 截止时间对于构建可靠应用非常有效,应该进行配置。
截止时间配置:
在进行调用时,使用 CallOptions.Deadline
配置截止时间。
没有截止时间默认值。 gRPC 调用没有时间限制,除非指定了截止时间。
截止时间指的是超过截止时间的 UTC 时间。 例如,DateTime.UtcNow.AddSeconds(5)
是从现在起 5 秒的截止时间。
如果使用的是过去或当前的时间,则调用将立即超过截止时间。
截止时间随 gRPC 调用发送到服务,并由客户端和服务独立跟踪。 gRPC 调用可能在一台计算机上完成,但当响应返回给客户端时,已超过了截止时间。
如果超过了截止时间,客户端和服务将有不同的行为:
客户端将立即中止基础的 HTTP 请求并引发 DeadlineExceeded
错误。 客户端应用可以选择捕获错误并向用户显示超时消息。
在服务器上,将中止正在执行的 HTTP 请求,并引发 ServerCallContext.CancellationToken 。 尽管中止了 HTTP 请求,gRPC 调用仍将继续在服务器上运行,直到方法完成。 将取消令牌传递给异步方法,使其随调用一同被取消,这非常重要。 例如,向异步数据库查询和 HTTP 请求传递取消令牌。 传递取消令牌让取消的调用可以在服务器上快速完成,并为其他调用释放资源。
配置 CallOptions.Deadline
以设置 gRPC 调用的截止时间:
var client = new Greet.GreeterClient(channel);
try
{
var response = await client.SayHelloAsync(
new HelloRequest { Name = " World " },
deadline: DateTime.UtcNow.AddSeconds( 5 ));
// Greeting: Hello World
Console.WriteLine(" Greeting: " + response.Message);
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.DeadlineExceeded)
{
Console.WriteLine( " Greeting timeout. " );
}
在 gRPC 服务中使用 ServerCallContext.CancellationToken
:
public override async Task<HelloReply> SayHello(HelloRequest request,
ServerCallContext context)
{
var user = await _databaseContext.GetUserAsync(request.Name,
context.CancellationToken);
return new HelloReply { Message = " Hello " + user.DisplayName };
}
截止时间和重试
当 gRPC 调用配置了故障处理
和截止日期时,截止日期会跟踪 gRPC 调用的所有重试时间。 如果超过了截止时间,gRPC 调用会立即中止底层 HTTP 请求,跳过任何剩余的重试,并引发 DeadlineExceeded
错误。
传播截止时间
需要安装包:
Grpc.AspNetCore.Server.ClientFactory 2.50 .0
从正在执行的 gRPC 服务进行 gRPC 调用时,应传播截止时间。 例如:
客户端应用调用带有截止时间的 FrontendService.GetUser
。
FrontendService
调用 UserService.GetUser
。 客户端指定的截止时间应随新的 gRPC 调用进行指定。
UserService.GetUser
接收截止时间。 如果超过了客户端应用的截止时间,将正确超时。
调用上下文将使用 ServerCallContext.Deadline
提供截止时间:
public override async Task<UserResponse> GetUser(UserRequest request,
ServerCallContext context)
{
var client = new User.UserServiceClient(_channel);
var response = await client.GetUserAsync(
new UserRequest { Id = request.Id },
deadline: context.Deadline);
return response;
}
手动传播截止时间可能会很繁琐。 截止时间需要传递给每个调用,很容易不小心错过。 gRPC 客户端工厂提供自动解决方案。 指定 EnableCallContextPropagation
(需要安装包: Grpc.AspNetCore.Server.ClientFactory ):
services
.AddGrpcClient <User.UserServiceClient>(o =>
{
o.Address = new Uri(" https://localhost:5001 " );
})
.EnableCallContextPropagation();
默认情况下,如果客户端在 gRPC 调用的上下文之外使用,EnableCallContextPropagation
将引发错误。 此错误旨在提醒你没有要传播的调用上下文。 如果要在调用上下文之外使用客户端,请使用 SuppressContextNotFoundErrors
在配置客户端时禁止显示该错误:
builder.Services
.AddGrpcClient <Greeter.GreeterClient>(o =>
{
o.Address = new Uri(" https://localhost:5001 " );
})
.EnableCallContextPropagation(o => o.SuppressContextNotFoundErrors = true );
故障处理与重试
gRPC 重试是一项功能,允许 gRPC 客户端自动重试失败的调用。 本文介绍如何配置重试策略,以便在 .NET 中创建可复原的容错 gRPC 应用。
gRPC 重试需要 Grpc.Net.Client 2.36.0 或更高版本。
暂时性故障处理
暂时性故障可能会中断 gRPC 调用。 暂时性故障包括:
暂时失去网络连接。
服务暂时不可用。
由于服务器负载导致超时。
gRPC 调用中断时,客户端会引发 RpcException
并提供有关错误的详细信息。 客户端应用必须捕获异常并选择如何处理错误。
var client = new Greeter.GreeterClient(channel);
try
{
var response = await client.SayHelloAsync(
new HelloRequest { Name = " .NET " });
Console.WriteLine( " From server: " + response.Message);
}
catch (RpcException ex)
{
// Write logic to inspect the error and retry
// if the error is from a transient fault.
}
在整个应用中复制重试逻辑是非常冗长的代码,容易出错。 幸运的是,.NET gRPC 客户端拥有自动重试的内置支持。
配置 gRPC 重试策略
重试策略在创建 gRPC 通道时配置一次:
var defaultMethodConfig = new MethodConfig
{
Names = { MethodName.Default },
RetryPolicy = new RetryPolicy
{
MaxAttempts = 5 ,
InitialBackoff = TimeSpan.FromSeconds(1 ),
MaxBackoff = TimeSpan.FromSeconds(5 ),
BackoffMultiplier = 1.5 ,
RetryableStatusCodes = { StatusCode.Unavailable }
}
};
var channel = GrpcChannel.ForAddress(" https://localhost:5001 " , new GrpcChannelOptions
{
ServiceConfig = new ServiceConfig { MethodConfigs = { defaultMethodConfig } }
});
上述代码:
创建一个 MethodConfig
。 重试策略可以按方法配置,而方法可以使用 Names
属性进行匹配。 此方法配置有 MethodName.Default
,因此它将应用于此通道调用的所有 gRPC 方法。
配置重试策略。 此策略将指示客户端自动重试状态代码为 Unavailable
的失败 gRPC 调用。
通过设置 GrpcChannelOptions.ServiceConfig
,将创建的通道配置为使用该重试策略。
随着该通道一起创建的 gRPC 客户端将自动重试失败的调用:
var client = new Greeter.GreeterClient(channel);
var response = await client.SayHelloAsync(
new HelloRequest { Name = " .NET " });
Console.WriteLine( " From server: " + response.Message);
gRPC 重试选项
下表描述了用于配置 gRPC 重试策略的选项:
选项 描述
MaxAttempts
最大调用尝试次数,包括原始尝试。 此值受 GrpcChannelOptions.MaxRetryAttempts
(默认值为 5)的限制。 必须为该选项提供值,且值必须大于 1。
InitialBackoff
重试尝试之间的初始退避延迟。 介于 0 与当前退避之间的随机延迟确定何时进行下一次重试尝试。 每次尝试后,当前退避将乘以 BackoffMultiplier
。 必须为该选项提供值,且值必须大于 0。
MaxBackoff
最大退避会限制指数退避增长的上限。 必须为该选项提供值,且值必须大于 0。
BackoffMultiplier
每次重试尝试后,退避将乘以该值,并将在乘数大于 1 的情况下以指数方式增加。 必须为该选项提供值,且值必须大于 0。
RetryableStatusCodes
状态代码的集合。 具有匹配状态的失败 gRPC 调用将自动重试。 有关状态代码的更多信息,请参阅状态代码及其在 gRPC 中的用法 。 至少需要提供一个可重试的状态代码。
重试何时有效
满足以下条件时,将重试调用:
在以下两种情况下,将提交 gRPC 调用:
无论状态代码是什么或以前的尝试次数是多少,提交的调用都无法重试。
流式处理调用
流式处理调用可以与 gRPC 重试一起使用,但在将它们一起使用时,务必注意以下事项:
客户端负载均衡
客户端负载均衡功能允许 gRPC 客户端以最佳方式在可用服务器之间分配负载。 本文介绍了如何配置客户端负载均衡,以在 .NET 中创建可缩放的高性能 gRPC 应用。
使用客户端负载均衡需要具备以下组件:
配置客户端负载均衡
客户端负载均衡是在创建通道时配置的。 使用负载均衡时需要考虑两个组件:
解析程序和负载均衡器的内置实现包含在 Grpc.Net.Client
中。 也可以通过编写自定义解析程序和负载均衡器 来扩展负载均衡。
地址、连接和其他负载均衡状态存储在 GrpcChannel
实例中。 在进行 gRPC 调用时,必须重用通道,以使负载均衡正常工作。
配置解析程序
解析程序是使用创建通道时所用的地址配置的。
Scheme 类型 说明
dns
DnsResolverFactory
通过查询 DNS 地址记录 的主机名来解析地址。
static
StaticResolverFactory
解析应用已指定的地址。 如果应用已经知道它调用的地址,则建议使用。
通道不会直接调用与解析程序匹配的 URI。 而是创建一个匹配的解析程序,用它来解析地址。
例如,使用 GrpcChannel.ForAddress("dns:///my-example-host", new GrpcChannelOptions { Credentials = ChannelCredentials.Insecure })
:
dns
方案映射到 DnsResolverFactory
。 为通道创建 DNS 解析程序的一个新实例。
解析程序对 my-example-host
进行 DNS 查询,并获得两个结果:localhost:80
和 localhost:81
。
负载均衡器使用 localhost:80
和 localhost:81
创建连接并进行 gRPC 调用。
DnsResolverFactory
DnsResolverFactory
创建一个解析程序,旨在从外部源获取地址。 DNS 解析通常用于对具有 Kubernetes 无外设服务 的 Pod 实例进行负载均衡。
var channel = GrpcChannel.ForAddress(
" dns:///my-example-host " ,
new GrpcChannelOptions { Credentials = ChannelCredentials.Insecure });
var client = new Greet.GreeterClient(channel);
var response = await client.SayHelloAsync(new HelloRequest { Name = " world " });
前面的代码:
DNS 地址缓存
在使用负载均衡时,性能非常重要。 通过缓存地址,从 gRPC 调用中消除了解析地址的延迟。 进行第一次 gRPC 调用时,将调用解析程序,后续调用将使用缓存。
如果连接中断,则会自动刷新地址。 如果是在运行时更改地址,那么刷新非常重要。 例如,在 Kubernetes 中,重启的 Pod 会触发 DNS 解析程序刷新并获取 Pod 的新地址。
默认情况下,如果连接中断,则会刷新 DNS 解析程序。 DNS 解析程序还可以根据需要定期刷新。 这对于快速检测新的 pod 实例很有用。
services.AddSingleton<ResolverFactory>( sp => new DnsResolverFactory(refreshInterval: TimeSpan.FromSeconds(30)));
上面的代码创建具有刷新间隔的 DnsResolverFactory
,并将其注册到依赖关系注入。
StaticResolverFactory
静态解析程序由 StaticResolverFactory
提供。 此解析程序:
不调用外部源。 相反,客户端应用会配置地址。
适用于应用已经知道它调用的地址的情况。
var factory = new StaticResolverFactory(addr => new []
{
new BalancerAddress(" localhost " , 80 ),
new BalancerAddress(" localhost " , 81 )
});
var services = new ServiceCollection();
services.AddSingleton <ResolverFactory>(factory);
var channel = GrpcChannel.ForAddress(
" static:///my-example-host " ,
new GrpcChannelOptions
{
Credentials = ChannelCredentials.Insecure,
ServiceProvider = services.BuildServiceProvider()
});
var client = new Greet.GreeterClient(channel);
上述代码:
本示例为 DI 创建了一个新的 ServiceCollection 。 假设一个应用已设置 DI,比如一个 ASP.NET Core 网站。 在这种情况下,类型应被注册到现有的 DI 实例。 GrpcChannelOptions.ServiceProvider
是通过从 DI 获取 IServiceProvider 来配置的。
配置负载均衡器
负载均衡器是使用 ServiceConfig.LoadBalancingConfigs
集合在 service config
中指定的。 两个负载均衡器都是内置的,映射到负载均衡器配置名称:
名称 类型 说明
pick_first
PickFirstLoadBalancerFactory
尝试连接到地址,直到成功建立连接。 gRPC 调用都是针对第一次成功连接进行的。
round_robin
RoundRobinLoadBalancerFactory
尝试连接到所有地址。 gRPC 调用是使用轮循机制 逻辑分布在所有成功的连接上的。
service config
是“service configuration”的缩写形式,用 ServiceConfig
类型表示。 有几种方法可以让通道获取配置了负载均衡器的 service config
:
当使用 GrpcChannelOptions.ServiceConfig
创建通道时,应用可以指定 service config
。
或者,解析程序可以为通道解析 service config
。 此功能允许外部源指定其调用方应如何执行负载均衡。 解析程序是否支持解析 service config
取决于解析程序实现。 使用 GrpcChannelOptions.DisableResolverServiceConfig
禁用此功能。
如果未提供 service config
,或者 service config
没有配置负载均衡器,则通道默认为 PickFirstLoadBalancerFactory
。
var channel = GrpcChannel.ForAddress(
" dns:///my-example-host " ,
new GrpcChannelOptions
{
Credentials = ChannelCredentials.Insecure,
ServiceConfig = new ServiceConfig { LoadBalancingConfigs = { new RoundRobinConfig() } }
});
var client = new Greet.GreeterClient(channel);
var response = await client.SayHelloAsync(new HelloRequest { Name = " world " });
前面的代码:
配置通道凭据
通道必须知道是否使用传输安全性 发送了 gRPC 调用。 http
和 https
不再是地址的一部分,方案现在指定一个解析程序,使得在使用负载均衡时必须对通道选项配置 Credentials
。
var channel = GrpcChannel.ForAddress(
" dns:///my-example-host " ,
new GrpcChannelOptions { Credentials = ChannelCredentials.Insecure });
var client = new Greet.GreeterClient(channel);
var response = await client.SayHelloAsync(new HelloRequest { Name = " world " });
6. gRPC JSON 转码
gRPC 是一种高性能远程过程调用 (RPC) 框架。 gRPC 使用 HTTP/2、流式传输、Protobuf 和消息协定来创建高性能的实时服务。
gRPC 有一个限制,即不是所有平台都可以使用它。 浏览器并不完全支持 HTTP/2,这使得 REST API 和 JSON 成为将数据引入浏览器应用的主要方式。 尽管 gRPC 带来了很多好处,REST API 和 JSON 在新式应用中仍发挥着重要作用。 构建 gRPC 和 JSON Web API 给应用开发增加了不必要的开销。
gRPC JSON 转码是为 gRPC 服务创建 RESTful JSON API 的 ASP.NET Core 的扩展。 配置转码后,应用可以使用熟悉的 HTTP 调用 gRPC 服务:
HTTP 谓词
URL 参数绑定
JSON 请求/响应
gRPC 仍然可以用来调用服务。
使用步骤
将包引用添加到 Microsoft.AspNetCore.Grpc.JsonTranscoding
。
通过添加 AddJsonTranscoding
,在服务器启动代码中注册转码:在 Program.cs
文件中,将 builder.Services.AddGrpc();
更改为 builder.Services.AddGrpc().AddJsonTranscoding();
。
在包含 .csproj
文件的项目目录中创建目录结构 /google/api
。
将 google/api/http.proto
和 google/api/annotations.proto
文件添加到 /google/api
目录中。
用 HTTP 绑定和路由在 .proto
文件中注释 gRPC 方法:
syntax = " proto3 " ;
option csharp_namespace = " GrpcServiceTranscoding " ;
import " google/api/annotations.proto " ;
package greet;
// 定义Greeter服务
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {
option (google.api.http) = {
get : " /v1/greeter/{name} "
};
}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1 ;
}
// The response message containing the greetings.
message HelloReply {
string message = 1 ;
}
SayHello
gRPC 方法现在可以作为 gRPC 和 JSON Web API 调用:
HTTP 协议
.NET SDK 中包含的 ASP.NET Core gRPC 服务模板创建仅针对 HTTP/2 配置的应用。 当应用仅支持传统的 gRPC over HTTP/2 时,HTTP/2 是很好的默认设置。 但是,转码同时适用于 HTTP/1.1 和 HTTP/2。 某些平台(如 UWP 或 Unity)无法使用 HTTP/2。 若要支持所有客户端应用,请将服务器配置为启用 HTTP/1.1 和 HTTP/2。
更新 appsettings.json
中的默认协议:
{
" Kestrel " : {
" EndpointDefaults " : {
" Protocols " : " Http1AndHttp2 "
}
}
}
或者,在启动代码中配置 Kestrel 终结点
builder.WebHost.UseKestrel(p =>
{
p.ConfigureEndpointDefaults(opt =>
{
opt.Protocols = HttpProtocols.Http1AndHttp2;
});
});
HTTP 规则
gRPC JSON 转码从 gRPC 方法创建 RESTful JSON Web API。 它使用用于自定义如何将 RESTful API 映射到 gRPC 方法的注释和选项。
gRPC 方法必须在支持转码之前使用 HTTP 规则进行注释。 HTTP 规则包括有关将 gRPC 方法作为 RESTful API 调用的信息,例如 HTTP 方法和路由。
import " google/api/annotations.proto " ;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {
option (google.api.http) = {
get : " /v1/greeter/{name} "
};
}
}
HTTP 规则:
HTTP 方法
通过将路由设置为匹配的 HTTP 方法字段名称来指定 HTTP 方法:
get
put
post
delete
patch
custom
字段适用于其他 HTTP 方法。
在以下示例中,CreateAddress
方法使用指定的路由映射到 POST
:
service Address {
rpc CreateAddress (CreateAddressRequest) returns (CreateAddressReply) {
option (google.api.http) = {
post: " /v1/address " ,
body: " * "
};
}
}
生成Swagger
OpenAPI (Swagger) 是一个与语言无关的规范,用于描述 REST API。 gRPC JSON 转码支持从转码的 RESTful API 生成 OpenAPI。 Microsoft.AspNetCore.Grpc.Swagger
包:
若要使用 gRPC JSON 转码启用 OpenAPI,请执行以下操作:
将包引用添加到 Microsoft.AspNetCore.Grpc.Swagger
。 版本必须为 0.3.0-xxx 或更高版本。
在启动时配置 Swashbuckle。 AddGrpcSwagger
方法将 Swashbuckle 配置为包含 gRPC 终结点。
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddGrpc().AddJsonTranscoding(); // JSON 转码
builder.Services.AddGrpcSwagger();
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc( " v1 " ,
new OpenApiInfo { Title = " gRPC transcoding " , Version = " v1 " });
});
var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint( " /swagger/v1/swagger.json " , " My API V1 " );
});
app.MapGrpcService <GreeterService>();
app.Run();
Swagger 文档注释
根据 .proto
协定中的注释生成 OpenAPI 说明,如以下示例所示:
ProtoBuf复制
// My amazing greeter service.
service Greeter {
// Sends a greeting.
rpc SayHello (HelloRequest) returns (HelloReply) {
option (google.api.http) = {
get : " /v1/greeter/{name} "
};
}
}
message HelloRequest {
// Name to say hello to.
string name = 1 ;
}
message HelloReply {
// Hello reply message.
string message = 1 ;
}
若要启用 gRPC OpenAPI 注释,请执行以下操作:
使用 <GenerateDocumentationFile>true</GenerateDocumentationFile>
启用服务器项目中的 XML 文档文件。
配置 AddSwaggerGen
以读取生成的 XML 文件。 将 XML 文件路径传递到 IncludeXmlComments
和 IncludeGrpcXmlComments
,如以下示例所示:
builder.Services.AddSwaggerGen(c =>
{
var filePath = Path.Combine(AppContext.BaseDirectory,
$ " {Assembly.GetExecutingAssembly().GetName().Name}.xml " );
c.IncludeXmlComments(filePath);
c.IncludeGrpcXmlComments(filePath, includeControllerXmlComments: true );
}).AddGrpcSwagger();
若要确认 Swashbuckle 为 RESTful gRPC 服务生成带说明的 OpenAPI ,请启动应用并导航到 Swagger UI 页面:
7. gRPC 配置
配置服务选项
gRPC 服务在 Startup.cs
中使用 AddGrpc
进行配置。 配置选项位于 Grpc.AspNetCore.Server
包中。
下表描述了用于配置 gRPC 服务的选项:
选项 默认值 说明
MaxSendMessageSize
null
可以从服务器发送的最大消息大小(以字节为单位)。 尝试发送超过配置的最大消息大小的消息会导致异常。 设置为 null
时,消息的大小不受限制。
MaxReceiveMessageSize
4 MB
可以由服务器接收的最大消息大小(以字节为单位)。 如果服务器收到的消息超过此限制,则会引发异常。 增大此值可使服务器接收更大的消息,但可能会对内存消耗产生负面影响。 设置为 null
时,消息的大小不受限制。
EnableDetailedErrors
false
如果为 true
,则当服务方法中引发异常时,会将详细异常消息返回到客户端。 默认值为 false
。 将 EnableDetailedErrors
设置为 true
可能会泄漏敏感信息。
CompressionProviders
gzip
用于压缩和解压缩消息的压缩提供程序的集合。 可以创建自定义压缩提供程序并将其添加到集合中。 默认已配置提供程序支持 gzip 压缩。
ResponseCompressionAlgorithm
null
压缩算法用于压缩从服务器发送的消息。 该算法必须与 CompressionProviders
中的压缩提供程序匹配。 若要使算法可压缩响应,客户端必须通过在 grpc-accept-encoding 标头中进行发送来指示它支持算法。
ResponseCompressionLevel
null
用于压缩从服务器发送的消息的压缩级别。
Interceptors
None
随每个 gRPC 调用一起运行的侦听器的集合。 侦听器按注册顺序运行。 全局配置的侦听器在为单个服务配置的侦听器之前运行。 侦听器默认为每个请求设置生存期。 将调用侦听器构造函数,并从依赖关系注入 (DI) 解析参数。 还可以向 DI 注册侦听器类型,以重写其创建方式及其生存期。 与 ASP.NET Core 中间件相比,侦听器会提供类似的功能。 有关详细信息,请参阅 gRPC 侦听器与中间件 。
IgnoreUnknownServices
false
如果为 true
,则对未知服务和方法的调用不会返回 UNIMPLEMENTED 状态,并且请求会传递到 ASP.NET Core 中的下一个注册中间件。
可以通过在 Startup.ConfigureServices
中向 AddGrpc
调用提供选项委托,为所有服务配置选项:
public void ConfigureServices(IServiceCollection services)
{
services.AddGrpc(options =>
{
options.EnableDetailedErrors = true ;
options.MaxReceiveMessageSize = 2 * 1024 * 1024 ; // 2 MB
options.MaxSendMessageSize = 5 * 1024 * 1024 ; // 5 MB
});
}
用于单个服务的选项会替代 AddGrpc
中提供的全局选项,可以使用 AddServiceOptions<TService>
进行配置:
public void ConfigureServices(IServiceCollection services)
{
services.AddGrpc().AddServiceOptions <MyService>(options =>
{
options.MaxReceiveMessageSize = 2 * 1024 * 1024 ; // 2 MB
options.MaxSendMessageSize = 5 * 1024 * 1024 ; // 5 MB
});
}
服务侦听器默认为每个请求设置生存期。 使用 DI 注册侦听器类型会覆盖创建侦听器的方式及其生存期。
public void ConfigureServices(IServiceCollection services)
{
services.AddGrpc(options =>
{
options.Interceptors.Add <LoggingInterceptor>();
});
services.AddSingleton <LoggingInterceptor>();
}
配置客户端选项
gRPC 客户端配置在 GrpcChannelOptions
中进行设置。 配置选项位于 Grpc.Net.Client
包中。
下表描述了用于配置 gRPC 通道的选项:
选项 默认值 说明
HttpHandler
新实例
用于进行 gRPC 调用的 HttpMessageHandler
。 可以将客户端设置为配置自定义 HttpClientHandler
,或将附加处理程序添加到 gRPC 调用的 HTTP 管道。 如果未指定 HttpMessageHandler
,则会通过自动处置为通道创建新 HttpClientHandler
实例。
HttpClient
null
用于进行 gRPC 调用的 HttpClient
。 此设置是 HttpHandler
的替代项。
DisposeHttpClient
false
如果设置为 true
且指定了 HttpMessageHandler
或 HttpClient
,则在处置 GrpcChannel
时,将分别处置 HttpHandler
或 HttpClient
。
LoggerFactory
null
客户端用于记录有关 gRPC 调用的信息的 LoggerFactory
。 可以通过依赖项注入来解析或使用 LoggerFactory.Create
来创建 LoggerFactory
实例。
MaxSendMessageSize
null
可以从客户端发送的最大消息大小(以字节为单位)。 尝试发送超过配置的最大消息大小的消息会导致异常。 设置为 null
时,消息的大小不受限制。
MaxReceiveMessageSize
4 MB
可以由客户端接收的最大消息大小(以字节为单位)。 如果客户端收到的消息超过此限制,则会引发异常。 增大此值可使客户端接收更大的消息,但可能会对内存消耗产生负面影响。 设置为 null
时,消息的大小不受限制。
Credentials
null
一个 ChannelCredentials
实例。 凭据用于将身份验证元数据添加到 gRPC 调用。
CompressionProviders
gzip
用于压缩和解压缩消息的压缩提供程序的集合。 可以创建自定义压缩提供程序并将其添加到集合中。 默认已配置提供程序支持 gzip 压缩。
ThrowOperationCanceledOnCancellation
false
如果设置为 true
,则在取消调用或超过其截止时间时,客户端将引发 OperationCanceledException 。
UnsafeUseInsecureChannelCallCredentials
false
如果设置为 true
,则 CallCredentials
应用于不安全通道发出的 gRPC 调用。 通过不安全的连接发送身份验证标头具有安全隐患,不应在生产环境中执行。
MaxRetryAttempts
5
最大重试次数。 该值限制服务配置中指定的任何重试和 hedging 尝试值。单独设置该值不会启用重试。 重试在服务配置中启用,可以使用 ServiceConfig
来启用。 null
值会删除最大重试次数限制。 有关重试的详细信息,请参阅故障处理与重试
MaxRetryBufferSize
16 MB
在重试或 hedging 调用时,可用于存储发送的消息的最大缓冲区大小(以字节为单位)。 如果超出了缓冲区限制,则不会再进行重试,并且仅保留一个 hedging 调用,其他 hedging 调用将会取消。 此限制将应用于通过通道进行的所有调用。 值 null
移除最大重试缓冲区大小限制。
MaxRetryBufferPerCallSize
1 MB
在重试或 hedging 调用时,可用于存储发送的消息的最大缓冲区大小(以字节为单位)。 如果超出了缓冲区限制,则不会再进行重试,并且仅保留一个 hedging 调用,其他 hedging 调用将会取消。 此限制将应用于一个调用。 值 null
移除每个调用的最大重试缓冲区大小限制。
ServiceConfig
null
gRPC 通道的服务配置。 服务配置可以用于配置 gRPC 重试。
下面的代码:
设置通道上发送和接收的最大消息大小。
创建客户端。
static async Task Main(string [] args)
{
var channel = GrpcChannel.ForAddress(" https://localhost:5001 " , new GrpcChannelOptions
{
MaxReceiveMessageSize = 5 * 1024 * 1024 , // 5 MB
MaxSendMessageSize = 2 * 1024 * 1024 // 2 MB
});
var client = new Greeter.GreeterClient(channel);
var reply = await client.SayHelloAsync(
new HelloRequest { Name = " GreeterClient " });
Console.WriteLine( " Greeting: " + reply.Message);
}
请注意,未使用 GrpcChannelOptions
配置客户端侦听器。 相反,客户端侦听器是使用带有通道的 Intercept
扩展方法配置的。 此扩展方法位于 Grpc.Core.Interceptors
命名空间中。
static async Task Main(string [] args)
{
var channel = GrpcChannel.ForAddress(" https://localhost:5001 " );
var callInvoker = channel.Intercept(new LoggingInterceptor());
var client = new Greeter.GreeterClient(callInvoker);
var reply = await client.SayHelloAsync(
new HelloRequest { Name = " GreeterClient " });
Console.WriteLine( " Greeting: " + reply.Message);
}
8. 身份验证和授权
gRPC 可与 ASP.NET Core 身份验证 配合使用,将用户与每个调用关联。
以下是使用 gRPC 和 ASP.NET Core 身份验证的 Program.cs
的示例:
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapGrpcService <GreeterService>();
备注
注册 ASP.NET Core 身份验证中间件的顺序很重要。 始终在 UseRouting
之后和 UseEndpoints
之前调用 UseAuthentication
和 UseAuthorization
。
应用在调用期间使用的身份验证机制需要进行配置。 身份验证配置已添加到 Program.cs
中,并因应用使用的身份验证机制而异。
设置身份验证后,可通过 ServerCallContext
使用 gRPC 服务方法访问用户。
public override Task<BuyTicketsResponse> BuyTickets(
BuyTicketsRequest request, ServerCallContext context)
{
var user = context.GetHttpContext().User;
// ... access data from ClaimsPrincipal ...
}
持有者令牌身份验证
客户端可提供用于身份验证的访问令牌。 服务器验证令牌并使用它来标识用户。
在服务器上,使用 JWT 持有者中间件 配置持有者令牌身份验证。
在 .NET gRPC 客户端中,令牌可通过 Metadata
集合与调用一起发送。 Metadata
集合中的条目以 HTTP 标头的形式与 gRPC 调用一起发送:
public bool DoAuthenticatedCall(
Ticketer.TicketerClient client, string token)
{
var headers = new Metadata();
headers.Add( " Authorization " , $" Bearer {token} " );
var request = new BuyTicketsRequest { Count = 1 };
var response = await client.BuyTicketsAsync(request, headers);
return response.Success;
}
在通道上配置 ChannelCredentials
是通过 gRPC 调用将令牌发送到服务的备用方法。 ChannelCredentials
可包含 CallCredentials
,这使得能够自动设置 Metadata
。 CallCredentials
在每次进行 gRPC 调用时运行,因而无需在多个位置编写代码用于自行传递令牌。
备注
仅当通道通过 TLS 进行保护时,才应用 CallCredentials
。 通过不安全的连接发送身份验证标头具有安全隐患,不应在生产环境中执行。 应用可以配置通道以忽略此行为,并通过在通道上设置 CallCredentials
始终使用 UnsafeUseInsecureChannelCallCredentials
。
以下示例中的凭据将通道配置为随每个 gRPC 调用发送令牌:
private static GrpcChannel CreateAuthenticatedChannel(string address)
{
var credentials = CallCredentials.FromInterceptor((context, metadata) =>
{
if (!string .IsNullOrEmpty(_token))
{
metadata.Add( " Authorization " , $" Bearer {_token} " );
}
return Task.CompletedTask;
});
var channel = GrpcChannel.ForAddress(address, new GrpcChannelOptions
{
Credentials = ChannelCredentials.Create(new SslCredentials(), credentials)
});
return channel;
}
gRPC 客户端工厂的持有者令牌
gRPC 客户端工厂可以创建使用 AddCallCredentials
发送持有者令牌的客户端。 此方法在 Grpc.Net.ClientFactory 版本 2.46.0 或更高版本中可用。
传递给 AddCallCredentials
的委托针对每个 gRPC 调用执行:
builder.Services
.AddGrpcClient <Greeter.GreeterClient>(o =>
{
o.Address = new Uri(" http://localhost:5001 " );
})
.AddCallCredentials( async (context, metadata) =>
{
var serviceProvider = builder.Services.BuildServiceProvider();
var httpContextAccessor = serviceProvider.GetService<IHttpContextAccessor>();
// 获取客户端的token
var accessToken = await httpContextAccessor.HttpContext.GetTokenAsync(" access_token " );
if (!string .IsNullOrEmpty(accessToken))
{
metadata.Add( " Authorization " , $" Bearer {accessToken} " );
}
}).ConfigureChannel(o => o.UnsafeUseInsecureChannelCallCredentials = true );
依赖项注入 (DI) 可以与 AddCallCredentials
结合使用。 重载将 IServiceProvider
传递给委托,该委托可用于获取使用范围内服务和暂时性服务从 DI 构建的服务 。
请考虑具有以下特征的应用:
用于获取持有者令牌的用户定义的 ITokenProvider
。 在具有作用域生存期的 DI 中注册 ITokenProvider
。
gRPC 客户端工厂配置为创建注入到 gRPC 服务和 Web API 控制器中的客户端。
gRPC 调用应使用 ITokenProvider
获取持有者令牌。
public interface ITokenProvider
{
Task <string > GetTokenAsync();
}
public class AppTokenProvider : ITokenProvider
{
private string _token;
public async Task<string > GetTokenAsync()
{
if (_token == null )
{
// App code to resolve the token here.
}
return _token;
}
}
builder.Services.AddScoped <ITokenProvider, AppTokenProvider>();
builder.Services
.AddGrpcClient <Greeter.GreeterClient>(o =>
{
o.Address = new Uri(" https://localhost:5001 " );
})
.AddCallCredentials( async (context, metadata, serviceProvider) =>
{
var provider = serviceProvider.GetRequiredService<ITokenProvider>();
var token = await provider.GetTokenAsync();
metadata.Add( " Authorization " , $" Bearer {token} " );
}));
前面的代码:
定义 ITokenProvider
和 AppTokenProvider
。 这些类型处理解析 gRPC 调用的身份验证令牌。
在范围内的生存期内向 DI 注册 AppTokenProvider
类型。 AppTokenProvider
缓存令牌,以便只需要范围内的第一个调用来计算它。
向客户端工厂注册 GreeterClient
类型。
为此客户端配置 AddCallCredentials
。 每次发出调用并将 ITokenProvider
返回的令牌添加到元数据时,都会执行委托。
客户端证书身份验证
客户端还可以提供用于身份验证的客户端证书。 证书身份验证 在 TLS 级别发生,远在到达 ASP.NET Core 之前。 当请求进入 ASP.NET Core 时,可借助客户端证书身份验证包 将证书解析为 ClaimsPrincipal
。
备注
将服务器配置为接受客户端证书。 有关在 Kestrel、IIS 和 Azure 中接受客户端证书的信息,请参阅在 ASP.NET Core 中配置证书身份验证 。
在 .NET gRPC 客户端中,客户端证书已添加到 HttpClientHandler
中,后者之后用于创建 gRPC 客户端:
public Ticketer.TicketerClient CreateClientWithCert(
string baseAddress,
X509Certificate2 certificate)
{
// Add client cert to the handler
var handler = new HttpClientHandler();
handler.ClientCertificates.Add(certificate);
// Create the gRPC channel
var channel = GrpcChannel.ForAddress(baseAddress, new GrpcChannelOptions
{
HttpHandler = handler
});
return new Ticketer.TicketerClient(channel);
}
授权用户访问服务
默认情况下,未经身份验证的用户可以调用服务中的所有方法。 若要要求进行身份验证,请将 [Authorize]
特性应用于服务:
[Authorize]
public class TicketerService : Ticketer.TicketerBase
{
}
可使用 [Authorize]
特性的构造函数参数和属性将访问权限仅限于匹配特定授权策略 的用户。 例如,如果有一个名为 MyAuthorizationPolicy
的自定义授权策略,请使用以下代码确保仅匹配该策略的用户才能访问服务:
[Authorize(" MyAuthorizationPolicy " )]
public class TicketerService : Ticketer.TicketerBase
{
}
各个服务方法也可以应用 [Authorize]
特性。 如果当前用户与同时 应用于方法和类的策略不匹配,则会向调用方返回错误:
[Authorize]
public class TicketerService : Ticketer.TicketerBase
{
public override Task<AvailableTicketsResponse> GetAvailableTickets(
Empty request, ServerCallContext context)
{
// ... buy tickets for the current user ...
}
[Authorize( " Administrators " )]
public override Task<BuyTicketsResponse> RefundTickets(
BuyTicketsRequest request, ServerCallContext context)
{
// ... refund tickets (something only Administrators can do) ..
}
}
2. Feign 组件
github 文档:SummerBoot/README.zh-cn.md at master · TripleView/SummerBoot · GitHub
需要安装包:
SummerBoot 2.0.2
SummerBoot 是将SpringBoot的先进理念与C#的简洁优雅合二为一,声明式编程,专注于”做什么”而不是”如何去做”。在更高层面写代码,更关心的是目标,而不是底层算法实现的过程,SummerBoot,致力于打造一个人性化框架,让.net开发变得更简单优雅。
SummerBoot 框架中内容较多,我们重点使用框架中提供的Feign组件。
个人觉得比gRPC 使用更简单一点,个人还是比较推荐
1. Feign 简介
Feign 是一种声明式服务调用组件 , 我们只需要声明一个接口并通过注解进行简单的配置(类似于 Dao 接口上面的 Mapper 注解一样)即可实现对 HTTP 接口的绑定。 通过 Feign,我们可以像调用本地方法一样来调用远程服务,而完全感觉不到这是在进行远程调用。
它(Feign)能做什么?
自定义拦截器(AOP)
封装了Http远程调用过程
微服务接入Nacos
可结合Polly做降级处理
结合JWT做授权与鉴权
请求方式
方式 特性(注解)
HttpGet
[GetMapping]
HttpPut
[PutMapping]
HttpDelete
[DeleteMapping]
HttpPost
[PostMapping]
2. 如何使用
封装Http调用
feign底层基于httpClient。
1.注册服务
builder.Services.AddSummerBoot();
builder.Services.AddSummerBootFeign();
2.定义接口
[FeignClient(Url = " http://localhost:5061 " )]
public interface UserClient
{
[GetMapping( " /User/GetUserList " )]
Task <List<UserInfo>> GetUserList();
}
提示
Feign 组件会由FeignProxyBuilder
类自动为接口生成实现代理类,注意,此版本中接口必须定义为Task<> 异步方法。
定义一个接口,并且在接口上添加FeignClient注解,FeignClient注解里可以自定义http接口url的公共部分-url(整个接口请求的url由FeignClient里的url加上方法里的path组成),是否忽略远程接口的https证书校验-IsIgnoreHttpsCertificateValidate,接口超时时间-Timeout(单位s),自定义拦截器-InterceptorType。
[FeignClient(
Url = " http://localhost:5001/home "
, IsIgnoreHttpsCertificateValidate = true
, InterceptorType = typeof (MyRequestInterceptor)
,Timeout = 100 )
]
public interface ITestFeign
{
[GetMapping( " /query " )]
Task <Test> TestQuery([Query] Test tt);
[GetMapping( " /addTest " )]
Task <Test> AddTest([Body] TestBo bo);
}
[Body]
请求体中支持Json与Form两种格式,默认为Json, 如果需要设置为Form提交,则修改为:
[PostMapping(" /User/AddUser " )]
Task <List<UserInfo>> AddUser([Body(BodySerializationKind.Form)] UserInfo bo);
3.服务调用
[Route(" [controller]/[action] " )]
[ApiController]
public class RpcController:ControllerBase
{
private readonly IUserClient _userClient;
public RpcController( IUserClient userClient)
{
_userClient = userClient;
}
[HttpGet]
public async Task<List<UserInfo>> GetUserList()
{
return await _userClient.GetUserList();
}
}
读取配置
同时,url和path可以通过读取配置获取,配置项通过${}包裹,配置的json如下:
{
" configurationTest " : {
" url " : " http://localhost:5001/home " ,
" path " : " /query "
}
}
接口如下:
[FeignClient(Url = " ${configurationTest:url} " )]
public interface ITestFeignWithConfiguration
{
[GetMapping( " ${configurationTest:path} " )]
Task <Test> TestQuery([Query] Test tt);
}
3. 设置请求头(header)
接口上可以选择添加Headers注解,代表这个接口下所有http请求都带上注解里的请求头。Headers的参数为变长的string类型的参数,同时Headers也可以添加在方法上,代表该方法调用的时候,会加该请求头,接口上的Headers参数可与方法上的Headers参数互相叠加,同时headers里可以使用变量,变量的占位符为{{}},如
[FeignClient(Url = " http://localhost:5001/home " , IsIgnoreHttpsCertificateValidate = true , InterceptorType = typeof (MyRequestInterceptor),Timeout = 100 )]
[Headers( " a:a " ," b:b " )]
public interface ITestFeign
{
[GetMapping( " /testGet " )]
Task <Test> TestAsync();
[GetMapping( " /testGetWithHeaders " )]
[Headers( " c:c " )]
Task <Test> TestWithHeadersAsync();
// header替换
[Headers(" a:{{methodName}} " )]
[PostMapping( " /abc " )]
Task <Test> TestHeaderAsync(string methodName);
}
await TestFeign.TestAsync()
>>> get , http:// localhost:5001/home/testGet,header为 "a:a" 和 "b:b"
await TestFeign.TestWithHeadersAsync()
>>> get , http:// localhost:5001/home/testGetWithHeaders,header为 "a:a" ,"b:b"和 "c:c"
await TestFeign.TestHeaderAsync(" abc " );
>>> post, http:// localhost:5001/home/abc,同时请求头为 "a:abc"
[Headers(" a:a " ," b:b " )]
冒号左边是键名称,右边是键值
4. 自定义拦截器
自定义拦截器对接口下的所有方法均生效,拦截器的应用场景主要是在请求前做一些操作,比如请求第三方业务接口前,需要先登录第三方系统,那么就可以在拦截器里先请求第三方登录接口,获取到凭证以后,放到header里,拦截器需要实现IRequestInterceptor接口,例子如下
// 先定义一个用来登录的loginFeign客户端
[FeignClient(Url = " http://localhost:5001/login " , IsIgnoreHttpsCertificateValidate = true ,Timeout = 100 )]
public interface ILoginFeign
{
[PostMapping( " /login " )]
Task <LoginResultDto> LoginAsync([Body()] LoginDto loginDto );
}
// 接着自定义登录拦截器
public class LoginInterceptor : IRequestInterceptor
{
private readonly ILoginFeign loginFeign;
private readonly IConfiguration configuration;
public LoginInterceptor(ILoginFeign loginFeign, IConfiguration configuration)
{
this .loginFeign = loginFeign;
this .configuration = configuration;
}
public async Task ApplyAsync(RequestTemplate requestTemplate)
{
var username = configuration.GetSection(" username " ).Value;
var password = configuration.GetSection(" password " ).Value;
var loginResultDto = await this .loginFeign.LoginAsync(new LoginDto(){Name = username,Password = password});
if (loginResultDto != null )
{
requestTemplate.Headers.Add( " Authorization " , new List<string >() { " Bearer " +loginResultDto.Token });
}
await Task.CompletedTask;
}
}
// 定义访问业务接口的testFegn客户端,在客户端上定义拦截器为loginInterceptor
[FeignClient(Url = " http://localhost:5001/home " , IsIgnoreHttpsCertificateValidate = true , InterceptorType = typeof (LoginInterceptor),Timeout = 100 )]
public interface ITestFeign
{
[GetMapping( " /testGet " )]
Task <Test> TestAsync();
}
await TestFeign.TestAsync();
>>> get to http:// localhost:5001/home/testGet,header为 "Authorization:Bearer abc"
忽略拦截器
有时候我们接口中的某些方法,是不需要拦截器的,那么就可以在方法上添加注解IgnoreInterceptor,那么该方法发起的请求,就会忽略拦截器,如
// 定义访问业务接口的testFegn客户端,在客户端上定义拦截器为loginInterceptor
[FeignClient(Url = " http://localhost:5001/home " , IsIgnoreHttpsCertificateValidate = true , InterceptorType = typeof (LoginInterceptor),Timeout = 100 )]
public interface ITestFeign
{
[GetMapping( " /testGet " )]
[IgnoreInterceptor]
Task <Test> TestAsync();
}
await TestFeign.TestAsync();
>>> get to http:// localhost:5001/home/testGet,没有header
5. 微服务-接入nacos
目前Feign组件只支持Nacos。
1.配置文件里添加nacos配置
注意:这里的nacos 配置与原生的nacos配置不太一样,SummberBoot会集成Nacos服务注册,所以若考虑接入SummberBoot中的Nacos,则原生的Nacos服务注入则应去掉。
" nacos " : {
// --------使用nacos则serviceAddress和namespaceId必填------
// nacos服务地址,如http: // 172.16.189.242:8848
" serviceAddress " : " http://172.16.189.242:8848/ " ,
// 命名空间id,如832e754e-e845-47db-8acc-46ae3819b638或者public
" namespaceId " : " dfd8de72-e5ec-4595-91d4-49382f500edf " ,
// --------如果只是访问nacos中的微服务,则仅配置lbStrategy即可,defaultNacosGroupName和defaultNacosNamespaceId选填------
// 客户端负载均衡算法,一个服务下有多个实例,lbStrategy用来挑选服务下的实例,默认为Random(随机),也可以选择WeightRandom(根据服务权重加权后再随机)
" lbStrategy " : " Random " ,
// defaultNacosGroupName,选填,为FeignClient注解中NacosGroupName的默认值,为空则默认为DEFAULT_GROUP
" defaultNacosGroupName " : "" ,
// defaultNacosNamespaceId,选填,为FeignClient注解中NacosNamespaceId的默认值,为空则默认为public
" defaultNacosNamespaceId " : "" ,
// --------如果需要使用nacos配置中心,则ConfigurationOption必填,允许监听多个配置------
" configurationOption " : [
{
" namespaceId " : " f3dfa56a-a72c-4035-9612-1f9a8ca6f1d2 " ,
// 配置的分组
" groupName " : " DEFAULT_GROUP " ,
// 配置的dataId,
" dataId " : " def "
},
{
" namespaceId " : " public " ,
// 配置的分组
" groupName " : " DEFAULT_GROUP " ,
// 配置的dataId,
" dataId " : " abc "
}
],
// -------如果是要将本应用注册为服务实例,则全部参数均需配置--------------
// 是否要把应用注册为服务实例
" registerInstance " : true ,
// 要注册的服务名
" serviceName " : " test " ,
// 服务的分组名
" groupName " : " DEFAULT_GROUP " ,
// 权重,一个服务下有多个实例,权重越高,访问到该实例的概率越大,比如有些实例所在的服务器配置高,那么权重就可以大一些,多引流到该实例,与上面的参数lbStrategy设置为WeightRandom搭配使用
" weight " : 1 ,
// 本应用对外的网络协议,http或https
" protocol " : " http " ,
// 本应用对外的端口号,比如5000
" port " : 5000
}
2.接入nacos服务中心
// 抛弃原生Nacos注册
// builder.Services.AddNacosAspNet(builder.Configuration);
builder.Services.AddSummerBoot();
builder.Services.AddSummerBootFeign(p =>
{
p.AddNacos(builder.Configuration); // Feign组件接入Nacos
});
3.定义接口
namespace NetCloud.RpcClient
{
[FeignClient(
ServiceName = " NetCloud.Nacos.UserService "
, NacosNamespaceId = " NetCloud "
, NacosGroupName = " DEFAULT_GROUP "
, MicroServiceMode = true
)
]
public interface IUserServiceClient
{
[GetMapping( " /User/GetUserList " )]
Task <List<UserInfo>> GetUserList();
}
}
同时ServiceName,NacosGroupName,NacosNamespaceId也支持从配置文件中读取,如
{
" ServiceName " : " NetCloud.Nacos.UserService " ,
" NacosGroupName " : " DEFAULT_GROUP " ,
" NacosNamespaceId " : " NetCloud "
}
[FeignClient( ServiceName = " ${ServiceName} " , MicroServiceMode = true ,NacosGroupName = " ${NacosGroupName} " , NacosNamespaceId = " ${NacosNamespaceId} " )]
public interface IFeignService
{
[GetMapping( " /home/index " )]
Task <string > TestGet();
}
视频配套链接:课程简介 (cctalk.com)
海阔平鱼跃,天高任我行,给我一片蓝天,让我自由翱翔。