Skip to content

gRPC 远程调用

一、gRPC 基础

1. 什么是 gRPC?

gRPC 是一个高性能、开源的远程过程调用(RPC)框架,由 Google 开发。

核心特性:

  • 基于 HTTP/2 - 支持多路复用、流式传输
  • Protocol Buffers - 高效的二进制序列化
  • 多语言支持 - 支持多种编程语言
  • 流式处理 - 支持客户端流、服务端流、双向流
  • 类型安全 - 强类型接口定义

2. gRPC vs REST

特性gRPCREST
协议HTTP/2HTTP/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 快?

  1. 二进制序列化 - Protocol Buffers 比 JSON 更小、更快
  2. HTTP/2 - 多路复用,减少连接数
  3. 流式传输 - 支持实时数据流
  4. 代码生成 - 编译时优化

Q2: gRPC 的适用场景?

  • 微服务间通信 - 高性能服务调用
  • 实时数据流 - 股票行情、游戏状态同步
  • 移动应用 - 减少带宽消耗
  • 云原生应用 - Kubernetes、Docker 环境

Q3: 如何处理版本兼容?

策略:

  1. 字段编号不变 - 已使用的字段编号不要改变
  2. 添加新字段 - 使用新的字段编号
  3. 标记废弃字段 - 使用 deprecated 关键字
  4. 向后兼容 - 新版本服务支持旧版本客户端
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
  • 浏览器直接调用
  • 简单场景
  • 需要人类可读的格式

十一、最佳实践

  1. 使用流式处理 - 适合大数据量传输
  2. 连接复用 - 避免频繁创建连接
  3. 错误处理 - 正确处理 RpcException
  4. 超时配置 - 避免请求无限等待
  5. 认证授权 - 保护 gRPC 服务
  6. 监控日志 - 记录请求和响应
  7. 版本管理 - 保持向后兼容
  8. 压缩 - 减少网络传输
  9. 不要忽略错误 - 正确处理异常
  10. 不要过度使用流 - 简单场景用一元 RPC

基于 VitePress 构建 | Copyright © 2026-present