gRPC 远程调用
一、gRPC 基础
1. 什么是 gRPC?
gRPC 是一个高性能、开源的远程过程调用(RPC)框架,由 Google 开发。
核心特性:
- ✅ 基于 HTTP/2 - 支持多路复用、流式传输
- ✅ Protocol Buffers - 高效的二进制序列化
- ✅ 多语言支持 - 支持多种编程语言
- ✅ 流式处理 - 支持客户端流、服务端流、双向流
- ✅ 类型安全 - 强类型接口定义
2. gRPC vs REST
| 特性 | gRPC | REST |
|---|---|---|
| 协议 | HTTP/2 | HTTP/1.1 |
| 数据格式 | Protocol Buffers(二进制) | JSON(文本) |
| 性能 | 高(二进制、多路复用) | 较低 |
| 浏览器支持 | 有限(需要 gRPC-Web) | 完全支持 |
| 流式传输 | 原生支持 | 有限(SSE、WebSocket) |
| 代码生成 | 自动生成客户端/服务端代码 | 手动编写 |
| 适用场景 | 微服务间通信 | Web API、移动应用 |
3. 为什么选择 gRPC?
优势:
- ✅ 高性能 - 二进制序列化,体积小,速度快
- ✅ 强类型 - 编译时类型检查,减少错误
- ✅ 流式处理 - 支持实时数据流
- ✅ 多语言 - 统一的接口定义,跨语言调用
- ✅ 代码生成 - 自动生成客户端和服务端代码
劣势:
- ❌ 浏览器支持有限 - 需要 gRPC-Web
- ❌ 调试相对困难 - 二进制格式不易阅读
- ❌ 学习曲线 - 需要了解 Protocol Buffers
二、Protocol Buffers
1. 什么是 Protocol Buffers?
Protocol Buffers(protobuf) 是 Google 开发的一种语言无关、平台无关的序列化数据结构的方法。
特点:
- ✅ 高效 - 比 JSON、XML 更小、更快
- ✅ 跨语言 - 支持多种编程语言
- ✅ 向后兼容 - 支持字段添加和删除
- ✅ 强类型 - 编译时类型检查
2. .proto 文件定义
示例:
protobuf
syntax = "proto3";
package user;
// 用户服务
service UserService {
// 获取用户(一元 RPC)
rpc GetUser (GetUserRequest) returns (User);
// 创建用户
rpc CreateUser (CreateUserRequest) returns (User);
// 获取用户列表(服务端流)
rpc GetUsers (GetUsersRequest) returns (stream User);
// 批量创建用户(客户端流)
rpc CreateUsers (stream CreateUserRequest) returns (CreateUsersResponse);
// 聊天(双向流)
rpc Chat (stream ChatMessage) returns (stream ChatMessage);
}
// 消息定义
message GetUserRequest {
int32 id = 1;
}
message CreateUserRequest {
string name = 1;
string email = 2;
int32 age = 3;
}
message User {
int32 id = 1;
string name = 2;
string email = 3;
int32 age = 4;
}
message GetUsersRequest {
int32 page = 1;
int32 page_size = 2;
}
message CreateUsersResponse {
int32 count = 1;
repeated User users = 2;
}
message ChatMessage {
string user = 1;
string message = 2;
}3. 数据类型
标量类型:
int32,int64- 整数uint32,uint64- 无符号整数float,double- 浮点数bool- 布尔值string- 字符串bytes- 字节数组
复合类型:
message- 消息类型enum- 枚举类型repeated- 数组/列表map- 映射
三、.NET Core 中使用 gRPC
1. 创建 gRPC 服务
安装工具:
bash
dotnet add package Grpc.AspNetCore
dotnet add package Grpc.Tools创建 .proto 文件:
protobuf
syntax = "proto3";
package greet;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}项目文件配置:
xml
<ItemGroup>
<Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
</ItemGroup>实现服务:
csharp
using Grpc.Core;
using Greet;
namespace MyGrpcService.Services;
public class GreeterService : Greeter.GreeterBase
{
public override Task<HelloReply> SayHello(
HelloRequest request,
ServerCallContext context)
{
return Task.FromResult(new HelloReply
{
Message = $"Hello {request.Name}"
});
}
}Program.cs 配置:
csharp
var builder = WebApplication.CreateBuilder(args);
// 添加 gRPC 服务
builder.Services.AddGrpc();
var app = builder.Build();
// 映射 gRPC 服务
app.MapGrpcService<GreeterService>();
app.Run();2. 创建 gRPC 客户端
项目文件配置:
xml
<ItemGroup>
<Protobuf Include="Protos\greet.proto" GrpcServices="Client" />
</ItemGroup>客户端代码:
csharp
using Grpc.Net.Client;
using Greet;
var channel = GrpcChannel.ForAddress("https://localhost:5001");
var client = new Greeter.GreeterClient(channel);
var request = new HelloRequest { Name = "World" };
var response = await client.SayHelloAsync(request);
Console.WriteLine($"Response: {response.Message}");
await channel.ShutdownAsync();3. 依赖注入客户端
csharp
// Program.cs
builder.Services.AddGrpcClient<Greeter.GreeterClient>(options =>
{
options.Address = new Uri("https://localhost:5001");
});
// 使用
public class MyService
{
private readonly Greeter.GreeterClient _client;
public MyService(Greeter.GreeterClient client)
{
_client = client;
}
public async Task<string> SayHelloAsync(string name)
{
var request = new HelloRequest { Name = name };
var response = await _client.SayHelloAsync(request);
return response.Message;
}
}四、流式处理
1. 服务端流(Server Streaming)
定义:
protobuf
service UserService {
rpc GetUsers (GetUsersRequest) returns (stream User);
}服务端实现:
csharp
public override async Task GetUsers(
GetUsersRequest request,
IServerStreamWriter<User> responseStream,
ServerCallContext context)
{
var users = await _userRepository.GetUsersAsync(request.Page, request.PageSize);
foreach (var user in users)
{
await responseStream.WriteAsync(new User
{
Id = user.Id,
Name = user.Name,
Email = user.Email
});
await Task.Delay(100); // 模拟延迟
}
}客户端调用:
csharp
using var call = client.GetUsers(new GetUsersRequest { Page = 1, PageSize = 10 });
await foreach (var user in call.ResponseStream.ReadAllAsync())
{
Console.WriteLine($"User: {user.Name}");
}2. 客户端流(Client Streaming)
定义:
protobuf
service UserService {
rpc CreateUsers (stream CreateUserRequest) returns (CreateUsersResponse);
}服务端实现:
csharp
public override async Task<CreateUsersResponse> CreateUsers(
IAsyncStreamReader<CreateUserRequest> requestStream,
ServerCallContext context)
{
var users = new List<User>();
await foreach (var request in requestStream.ReadAllAsync())
{
var user = await _userRepository.CreateAsync(new User
{
Name = request.Name,
Email = request.Email,
Age = request.Age
});
users.Add(user);
}
return new CreateUsersResponse
{
Count = users.Count,
Users = { users }
};
}客户端调用:
csharp
using var call = client.CreateUsers();
for (int i = 0; i < 10; i++)
{
await call.RequestStream.WriteAsync(new CreateUserRequest
{
Name = $"User{i}",
Email = $"user{i}@example.com",
Age = 20 + i
});
}
await call.RequestStream.CompleteAsync();
var response = await call;
Console.WriteLine($"Created {response.Count} users");3. 双向流(Bidirectional Streaming)
定义:
protobuf
service ChatService {
rpc Chat (stream ChatMessage) returns (stream ChatMessage);
}服务端实现:
csharp
public override async Task Chat(
IAsyncStreamReader<ChatMessage> requestStream,
IServerStreamWriter<ChatMessage> responseStream,
ServerCallContext context)
{
await foreach (var message in requestStream.ReadAllAsync())
{
Console.WriteLine($"Received: {message.User} - {message.Message}");
// 回显消息
await responseStream.WriteAsync(new ChatMessage
{
User = "Server",
Message = $"Echo: {message.Message}"
});
}
}客户端调用:
csharp
using var call = client.Chat();
// 发送消息
var sendTask = Task.Run(async () =>
{
for (int i = 0; i < 10; i++)
{
await call.RequestStream.WriteAsync(new ChatMessage
{
User = "Client",
Message = $"Message {i}"
});
await Task.Delay(1000);
}
await call.RequestStream.CompleteAsync();
});
// 接收消息
var receiveTask = Task.Run(async () =>
{
await foreach (var message in call.ResponseStream.ReadAllAsync())
{
Console.WriteLine($"Received: {message.User} - {message.Message}");
}
});
await Task.WhenAll(sendTask, receiveTask);五、拦截器和中间件
1. 服务端拦截器
csharp
public class LoggingInterceptor : Interceptor
{
private readonly ILogger<LoggingInterceptor> _logger;
public LoggingInterceptor(ILogger<LoggingInterceptor> logger)
{
_logger = logger;
}
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request,
ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation)
{
_logger.LogInformation($"Received request: {typeof(TRequest).Name}");
var stopwatch = Stopwatch.StartNew();
var response = await base.UnaryServerHandler(request, context, continuation);
stopwatch.Stop();
_logger.LogInformation($"Request completed in {stopwatch.ElapsedMilliseconds}ms");
return response;
}
}
// 注册
builder.Services.AddGrpc(options =>
{
options.Interceptors.Add<LoggingInterceptor>();
});2. 客户端拦截器
csharp
public class LoggingClientInterceptor : Interceptor
{
private readonly ILogger<LoggingClientInterceptor> _logger;
public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
TRequest request,
ClientInterceptorContext<TRequest, TResponse> context,
AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
{
_logger.LogInformation($"Sending request: {typeof(TRequest).Name}");
var call = continuation(request, context);
return new AsyncUnaryCall<TResponse>(
HandleResponse(call.ResponseAsync),
call.ResponseHeadersAsync,
call.GetStatus,
call.GetTrailers,
call.Dispose);
}
private async Task<TResponse> HandleResponse<TResponse>(Task<TResponse> task)
{
var response = await task;
_logger.LogInformation($"Received response: {typeof(TResponse).Name}");
return response;
}
}
// 使用
var channel = GrpcChannel.ForAddress("https://localhost:5001");
var invoker = channel.Intercept(new LoggingClientInterceptor());
var client = new Greeter.GreeterClient(invoker);六、认证和授权
1. JWT 认证
服务端配置:
csharp
builder.Services.AddGrpc(options =>
{
options.EnableDetailedErrors = true;
});
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://localhost:5001";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
});
builder.Services.AddAuthorization();
// 使用
[Authorize]
public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
{
var user = context.GetHttpContext().User;
return Task.FromResult(new HelloReply { Message = $"Hello {user.Identity.Name}" });
}客户端配置:
csharp
var credentials = CallCredentials.FromInterceptor((context, metadata) =>
{
metadata.Add("Authorization", $"Bearer {token}");
return Task.CompletedTask;
});
var channel = GrpcChannel.ForAddress("https://localhost:5001");
var client = new Greeter.GreeterClient(channel.WithCallCredentials(credentials));2. 证书认证
csharp
var handler = new HttpClientHandler();
handler.ClientCertificates.Add(certificate);
var channel = GrpcChannel.ForAddress("https://localhost:5001", new GrpcChannelOptions
{
HttpHandler = handler
});七、错误处理
1. 状态码
csharp
public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
{
if (string.IsNullOrEmpty(request.Name))
{
throw new RpcException(new Status(StatusCode.InvalidArgument, "Name is required"));
}
if (request.Name == "Error")
{
throw new RpcException(new Status(StatusCode.Internal, "Internal error"));
}
return Task.FromResult(new HelloReply { Message = $"Hello {request.Name}" });
}状态码类型:
OK- 成功InvalidArgument- 无效参数NotFound- 未找到AlreadyExists- 已存在PermissionDenied- 权限拒绝Unauthenticated- 未认证Internal- 内部错误Unavailable- 服务不可用
2. 客户端错误处理
csharp
try
{
var response = await client.SayHelloAsync(new HelloRequest { Name = "World" });
}
catch (RpcException ex)
{
switch (ex.StatusCode)
{
case StatusCode.InvalidArgument:
Console.WriteLine("Invalid argument");
break;
case StatusCode.NotFound:
Console.WriteLine("Not found");
break;
default:
Console.WriteLine($"Error: {ex.Status.Detail}");
break;
}
}八、性能优化
1. 连接复用
csharp
// 单例 Channel
public class GrpcClientFactory
{
private static GrpcChannel _channel;
public static GrpcChannel GetChannel()
{
if (_channel == null)
{
_channel = GrpcChannel.ForAddress("https://localhost:5001");
}
return _channel;
}
}2. 压缩
csharp
// 服务端启用压缩
builder.Services.AddGrpc(options =>
{
options.ResponseCompressionLevel = CompressionLevel.Fastest;
});
// 客户端启用压缩
var channel = GrpcChannel.ForAddress("https://localhost:5001", new GrpcChannelOptions
{
CompressionProviders = new List<ICompressionProvider>
{
new GzipCompressionProvider(CompressionLevel.Fastest)
}
});3. 超时配置
csharp
var callOptions = new CallOptions(deadline: DateTime.UtcNow.AddSeconds(5));
var response = await client.SayHelloAsync(request, callOptions);九、gRPC-Web
1. 配置 gRPC-Web
安装包:
bash
dotnet add package Grpc.AspNetCore.Web配置:
csharp
builder.Services.AddGrpc();
var app = builder.Build();
app.UseGrpcWeb(); // 启用 gRPC-Web
app.MapGrpcService<GreeterService>().EnableGrpcWeb();
app.Run();2. 客户端调用
javascript
// 使用 grpc-web
import { GreeterClient } from './greet_grpc_web_pb';
import { HelloRequest } from './greet_pb';
const client = new GreeterClient('https://localhost:5001');
const request = new HelloRequest();
request.setName('World');
client.sayHello(request, {}, (err, response) => {
if (err) {
console.error(err);
} else {
console.log(response.getMessage());
}
});十、常见面试题
Q1: gRPC 为什么比 REST 快?
- 二进制序列化 - Protocol Buffers 比 JSON 更小、更快
- HTTP/2 - 多路复用,减少连接数
- 流式传输 - 支持实时数据流
- 代码生成 - 编译时优化
Q2: gRPC 的适用场景?
- ✅ 微服务间通信 - 高性能服务调用
- ✅ 实时数据流 - 股票行情、游戏状态同步
- ✅ 移动应用 - 减少带宽消耗
- ✅ 云原生应用 - Kubernetes、Docker 环境
Q3: 如何处理版本兼容?
策略:
- 字段编号不变 - 已使用的字段编号不要改变
- 添加新字段 - 使用新的字段编号
- 标记废弃字段 - 使用
deprecated关键字 - 向后兼容 - 新版本服务支持旧版本客户端
protobuf
message User {
int32 id = 1;
string name = 2;
string email = 3;
int32 age = 4 [deprecated = true]; // 废弃字段
string phone = 5; // 新字段
}Q4: gRPC 如何实现负载均衡?
客户端负载均衡:
csharp
var channels = new[]
{
GrpcChannel.ForAddress("https://service1:5001"),
GrpcChannel.ForAddress("https://service2:5001"),
GrpcChannel.ForAddress("https://service3:5001")
};
var random = new Random();
var channel = channels[random.Next(channels.Length)];
var client = new Greeter.GreeterClient(channel);服务发现集成:
- 使用 Consul、Eureka 等服务发现
- 动态获取服务地址列表
- 实现客户端负载均衡
Q5: gRPC 和 REST 如何选择?
选择 gRPC:
- 微服务间通信
- 高性能要求
- 实时数据流
- 强类型需求
选择 REST:
- Web API
- 浏览器直接调用
- 简单场景
- 需要人类可读的格式
十一、最佳实践
- ✅ 使用流式处理 - 适合大数据量传输
- ✅ 连接复用 - 避免频繁创建连接
- ✅ 错误处理 - 正确处理 RpcException
- ✅ 超时配置 - 避免请求无限等待
- ✅ 认证授权 - 保护 gRPC 服务
- ✅ 监控日志 - 记录请求和响应
- ✅ 版本管理 - 保持向后兼容
- ✅ 压缩 - 减少网络传输
- ❌ 不要忽略错误 - 正确处理异常
- ❌ 不要过度使用流 - 简单场景用一元 RPC