VB.net 2010 视频教程 VB.net 2010 视频教程 python基础视频教程
SQL Server 2008 视频教程 c#入门经典教程 Visual Basic从门到精通视频教程
当前位置:
首页 > 编程开发 > c#编程 >
  • C#网络编程之gRPC实战(Protocol Buffers、双向流、拦截器)

第18章 微服务架构基础
18.2 gRPC实战(Protocol Buffers、双向流、拦截器)
一、我踩过的“gRPC坑”:从RESTful到gRPC的性能跃迁与血泪教训
之前做微服务内部通信时用RESTful API,结果订单服务调用库存服务的延迟经常超过500ms,大促时甚至超时——后来换成gRPC,延迟直接降到50ms以内,性能提升了10倍!但刚开始用gRPC时踩了不少坑:
用Protobuf定义消息时,随便改了字段编号,导致老客户端和新服务端通信失败;
双向流聊天功能没处理异常断开,客户端崩vb.net教程C#教程python教程SQL教程access 2010教程溃后服务端一直挂着连接,内存泄漏;
拦截器顺序写错,认证拦截器在日志拦截器之后,导致未认证的请求也被记录日志,浪费资源;
没启用HTTPS,gRPC传输的明文数据被抓包,敏感数据泄露。
这节我把自己从“gRPC小白”到“实战专家”的踩坑经验揉进去,用大白话讲透gRPC的核心原理,结合C#实战代码逐行讲解Protobuf定义、gRPC服务端/客户端实现、双向流实时聊天、拦截器日志与认证,以及Protobuf版本兼容、gRPC性能优化的基础知识,让你的微服务通信既高效又稳定。
二、先搞懂gRPC的核心:“为什么比RESTful快10倍?”
gRPC是Google开发的高性能、开源的远程过程调用(RPC)框架,基于HTTP/2协议和Protocol Buffers(Protobuf)序列化。它比RESTful API快的核心原因有两个:
1.HTTP/2协议:多路复用、二进制帧、头部压缩、服务器推送,比HTTP/1.1效率高很多;
2.Protobuf序列化:二进制格式,体积小(比JSON小3-5倍),序列化/反序列化速度快(比JSON快5-10倍)。
类比:RESTful API就像用快递寄文件,每次寄一个包裹,还要写纸质快递单;gRPC就像用专线寄文件,一次可以寄多个包裹,快递单是电子的,又快又省空间。
三、Protocol Buffers(Protobuf):gRPC的“语言中立的消息格式”
Protobuf是gRPC的核心,是一种语言中立、平台中立的序列化格式,支持C#、Java、Go、Python等多种语言。它的核心是消息定义文件(.proto),用.proto文件定义消息结构,然后用Protobuf编译器生成不同语言的代码。

  1. Protobuf核心语法(拓展知识)
    先写一个简单的.proto文件,定义用户消息和服务:
    proto
	// 指定Protobuf版本,必须写在最前面
	syntax = "proto3";
	
	// 生成代码的命名空间(C#)
	option csharp_namespace = "GrpcDemo.Protos";
	
	// 定义消息结构,类似C#的class
	message UserRequest {
	// 字段格式:字段类型 字段名称 = 字段编号;
	int32 user_id = 1; // 字段编号是唯一的,不能随便改!
	}
	
	message UserResponse {
	int32 user_id = 1;
	string username = 2;
	string email = 3;
	repeated string roles = 4; // repeated表示数组,类似C#的List<string>
	enum Gender {
	UNKNOWN = 0; // 枚举的第一个值必须是0
	MALE = 1;
	FEMALE = 2;
	}
	Gender gender = 5;
	}
	
	// 定义gRPC服务,类似C#的interface
	service UserService {
	// 一元RPC:客户端发一个请求,服务端返回一个响应
	rpc GetUser(UserRequest) returns (UserResponse);
	// 服务端流RPC:客户端发一个请求,服务端返回多个响应
	rpc GetUserStream(UserRequest) returns (stream UserResponse);
	// 客户端流RPC:客户端发多个请求,服务端返回一个响应
	rpc CreateUserStream(stream UserRequest) returns (UserResponse);
	// 双向流RPC:客户端和服务端互相发多个响应
	rpc ChatStream(stream UserRequest) returns (stream UserResponse);
	}

语法逐行讲解:
syntax = "proto3":指定用Protobuf 3版本,比Protobuf 2更简洁,默认值更合理;
option csharp_namespace:生成C#代码的命名空间;
message:定义消息结构,类似C#的class;
字段编号:每个字段必须有唯一的编号(1-536870911),这个编号不能随便改!因为Protobuf是用字段编号来识别字段的,不是字段名称——如果改了字段编号,老客户端和新服务端通信时会解析错误;
repeated:表示数组,类似C#的List;
enum:定义枚举,第一个值必须是0(Protobuf的默认值);
service:定义gRPC服务,里面的方法是RPC方法,支持四种类型:一元RPC、服务端流RPC、客户端流RPC、双向流RPC。
Protobuf版本兼容的核心规则(拓展知识):
1.不要随便改字段编号;
2.可以新增字段,老客户端会忽略新增的字段;
3.可以删除字段,但要把字段编号标记为“已废弃”,不要重复使用;
4.不要把字段类型从int32改成string,会导致解析错误;
5.枚举可以新增值,老客户端会把未知的枚举值解析为0。
我踩过的坑:新增字段时用了已经删除的字段编号,导致老客户端解析新消息时出现乱码——字段编号一旦使用,就永远不要重复使用!
2. 用Protobuf编译器生成C#代码
在Visual Studio中,右键.proto文件,设置“生成操作”为“Protobuf编译器”,“gRPC Stub类”为“服务和客户端”,编译后会自动生成C#代码:
UserService.cs:包含服务端的UserServiceBase抽象类和客户端的UserServiceClient类;
User.cs:包含UserRequest、UserResponse等消息类。
四、gRPC实战1:一元RPC服务端与客户端实现
一元RPC是最基础的RPC类型,客户端发一个请求,服务端返回一个响应,类似RESTful API的GET请求。

  1. 服务端实现(ASP.NET Core)
    csharp
	// UserServiceImpl.cs
	using Grpc.Core;
	using GrpcDemo.Protos;
	
	namespace GrpcDemo.Services;
	
	// 继承自动生成的UserServiceBase,实现GetUser方法
	public class UserServiceImpl : UserService.UserServiceBase
	{
	// 一元RPC方法:客户端发UserRequest,服务端返回UserResponse
	public override Task<UserResponse> GetUser(UserRequest request, ServerCallContext context)
	{
	// 模拟从数据库获取用户信息
	var user = new UserResponse
	{
	UserId = request.UserId,
	Username = "张三",
	Email = "zhangsan@example.com",
	Gender = UserResponse.Types.Gender.MALE
	};
	// 添加角色数组
	user.Roles.Add("admin");
	user.Roles.Add("user");
	
	// 返回响应
	return Task.FromResult(user);
	}
	}

Program.cs配置gRPC服务端:
csharp

	var builder = WebApplication.CreateBuilder(args);
	
	// 添加gRPC服务
	builder.Services.AddGrpc();
	
	var app = builder.Build();
	
	// 映射gRPC服务,必须用MapGrpcService
	app.MapGrpcService<UserServiceImpl>();
	
	// 必须保留HTTP/1.1的健康检查接口,否则gRPC客户端无法连接
	app.MapGet("/", () => "gRPC服务已启动");
	
	app.Run();

代码逐行讲解:
UserServiceBase:自动生成的抽象类,包含所有RPC方法的默认实现;
ServerCallContext:包含RPC调用的上下文信息,比如请求头、响应头、取消令牌;
MapGrpcService:把gRPC服务映射到路由,gRPC的默认路由是/服务名称/方法名称,比如/UserService/GetUser;
必须保留HTTP/1.1接口:gRPC用HTTP/2,但客户端连接时会先发送HTTP/1.1的OPTIONS请求,所以必须有一个HTTP/1.1的接口,否则客户端会连接失败。
我踩过的坑:一开始没保留HTTP/1.1接口,客户端一直报“连接失败”,后来查文档才知道gRPC客户端需要先发送OPTIONS请求!
2. 客户端实现(控制台程序)
csharp

	using Grpc.Core;
	using GrpcDemo.Protos;
	
	namespace GrpcClient;
	
	class Program
	{
	static async Task Main(string[] args)
	{
	// 创建gRPC客户端通道,指定服务端地址,启用HTTP/2
	var channel = GrpcChannel.ForAddress("http://localhost:5000");
	// 创建UserService客户端
	var client = new UserService.UserServiceClient(channel);
	
	try
	{
	// 发送一元RPC请求
	var request = new UserRequest { UserId = 1 };
	var response = await client.GetUserAsync(request);
	
	// 打印响应结果
	Console.WriteLine($"用户ID:{response.UserId}");
	Console.WriteLine($"用户名:{response.Username}");
	Console.WriteLine($"邮箱:{response.Email}");
	Console.WriteLine($"性别:{response.Gender}");
	Console.WriteLine($"角色:{string.Join(", ", response.Roles)}");
	}
	catch (RpcException ex)
	{
	// 处理gRPC异常,比如服务端返回的错误码
	Console.WriteLine($"gRPC调用失败:{ex.StatusCode} - {ex.Status.Detail}");
	}
	finally
	{
	// 关闭通道
	await channel.ShutdownAsync();
	}
	}
	}

代码逐行讲解:
GrpcChannel:gRPC客户端的通道,负责管理连接、负载均衡、重试;
UserServiceClient:自动生成的客户端类,包含所有RPC方法的异步和同步版本;
RpcException:gRPC的异常类型,包含错误码(比如StatusCode.NotFound、StatusCode.Unauthenticated)和错误详情;
ShutdownAsync:关闭通道,释放资源,避免内存泄漏。
五、gRPC实战2:双向流实时聊天(客户端+服务端流)
双向流RPC是gRPC的核心优势,支持客户端和服务端互相发送多个消息,适合实时聊天、实时监控、数据上传下载等场景。

  1. 服务端实现双向流聊天
    csharp
	// UserServiceImpl.cs
	public override async Task ChatStream(IAsyncStreamReader<UserRequest> requestStream, IServerStreamWriter<UserResponse> responseStream, ServerCallContext context)
	{
	// 存储所有连接的客户端(简化版,实际项目用ConcurrentDictionary)
	var clients = new List<IServerStreamWriter<UserResponse>>();
	clients.Add(responseStream);
	
	try
	{
	// 监听客户端发送的消息
	await foreach (var request in requestStream.ReadAllAsync(context.CancellationToken))
	{
	// 把消息转发给所有客户端
	foreach (var client in clients)
	{
	var response = new UserResponse
	{
	Username = request.Username,
	Email = request.Email,
	// 把客户端发送的消息作为响应内容
	Roles = { $"消息:{request.UserId}" } // 这里用UserId字段临时存消息内容
	};
	await client.WriteAsync(response);
	}
	}
	}
	catch (RpcException ex) when (ex.StatusCode == StatusCode.Cancelled)
	{
	// 客户端主动断开连接,从列表中移除
	clients.Remove(responseStream);
	Console.WriteLine($"客户端断开连接:{ex.Status.Detail}");
	}
	catch (Exception ex)
	{
	Console.WriteLine($"双向流异常:{ex.Message}");
	}
	}

代码逐行讲解:
IAsyncStreamReader:客户端发送的消息流,用await foreach监听;
IServerStreamWriter:服务端发送的消息流,用WriteAsync发送消息;
ServerCallContext.CancellationToken:监听客户端取消请求的令牌,客户端断开连接时会触发;
ReadAllAsync:异步读取客户端发送的所有消息,直到客户端断开连接。
我踩过的坑:一开始没处理客户端断开连接的异常,服务端会一直报错,后来用CancellationToken监听取消事件,及时从客户端列表中移除断开的连接。
2. 客户端实现双向流聊天
csharp

	// GrpcClient/Program.cs
	static async Task ChatStreamAsync(UserService.UserServiceClient client)
	{
	// 创建双向流调用
	using var call = client.ChatStream();
	
	// 启动一个任务,监听服务端发送的消息
	var responseTask = Task.Run(async () =>
	{
	await foreach (var response in call.ResponseStream.ReadAllAsync())
	{
	Console.WriteLine($"
[{response.Username}]:{response.Roles[0]}");
	}
	});
	
	// 从控制台输入消息,发送给服务端
	Console.WriteLine("请输入消息(输入exit退出):");
	while (true)
	{
	var input = Console.ReadLine();
	if (input == "exit")
	{
	break;
	}
	
	// 发送消息给服务端,用UserId字段临时存消息内容
	var request = new UserRequest
	{
	UserId = int.Parse(input.GetHashCode().ToString()), // 临时用哈希值作为UserId
	Username = "客户端1"
	};
	await call.RequestStream.WriteAsync(request);
	}
	
	// 关闭请求流,通知服务端客户端断开连接
	await call.RequestStream.CompleteAsync();
	// 等待响应任务完成
	await responseTask;
	}

代码逐行讲解:
client.ChatStream():创建双向流调用,返回AsyncDuplexStreamingCall<UserRequest, UserResponse>对象;
ResponseStream.ReadAllAsync():监听服务端发送的消息;
RequestStream.WriteAsync():发送消息给服务端;
RequestStream.CompleteAsync():关闭请求流,通知服务端客户端断开连接。
六、gRPC实战3:拦截器(日志、认证、限流)
gRPC拦截器类似ASP.NET Core的中间件,支持在RPC调用前后执行逻辑,比如日志、认证、限流、重试。

  1. 日志拦截器实现
    csharp
	// LogInterceptor.cs
	using Grpc.Core;
	using Grpc.Core.Interceptors;
	
	namespace GrpcDemo.Interceptors;
	
	public class LogInterceptor : Interceptor
	{
	private readonly ILogger<LogInterceptor> _logger;
	
	public LogInterceptor(ILogger<LogInterceptor> logger)
	{
	_logger = logger;
	}
	
	// 拦截一元RPC方法
	public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(TRequest request, ServerCallContext context, UnaryServerMethod<TRequest, TResponse> continuation)
	{
	// RPC调用前:记录请求信息
	var startTime = DateTime.UtcNow;
	_logger.LogInformation("开始处理RPC请求:{Method},请求内容:{Request}", context.Method, request);
	
	try
	{
	// 调用实际的RPC方法
	var response = await continuation(request, context);
	// RPC调用后:记录响应信息和耗时
	var elapsed = DateTime.UtcNow - startTime;
	_logger.LogInformation("RPC请求处理完成:{Method},耗时:{Elapsed}ms,响应内容:{Response}", context.Method, elapsed.TotalMilliseconds, response);
	return response;
	}
	catch (RpcException ex)
	{
	// 记录异常信息
	_logger.LogError(ex, "RPC请求处理失败:{Method},错误码:{StatusCode}", context.Method, ex.StatusCode);
	throw;
	}
	}
	
	// 拦截双向流RPC方法(其他类型的RPC方法类似)
	public override async Task DuplexStreamingServerHandler<TRequest, TResponse>(IAsyncStreamReader<TRequest> requestStream, IServerStreamWriter<TResponse> responseStream, ServerCallContext context, DuplexStreamingServerMethod<TRequest, TResponse> continuation)
	{
	_logger.LogInformation("开始处理双向流RPC请求:{Method}", context.Method);
	try
	{
	await continuation(requestStream, responseStream, context);
	_logger.LogInformation("双向流RPC请求处理完成:{Method}", context.Method);
	}
	catch (RpcException ex)
	{
	_logger.LogError(ex, "双向流RPC请求处理失败:{Method},错误码:{StatusCode}", context.Method, ex.StatusCode);
	throw;
	}
	}
	}

代码逐行讲解:
Interceptor:gRPC拦截器的基类,需要重写对应类型的RPC方法;
UnaryServerHandler:拦截一元RPC方法,continuation是实际的RPC方法;
ServerCallContext:包含RPC调用的上下文信息,比如方法名称、请求头;
日志记录:记录请求开始时间、请求内容、响应内容、耗时、异常信息。
2. 注册拦截器到服务端
csharp

	// Program.cs
	builder.Services.AddGrpc(options =>
	{
	// 添加日志拦截器,拦截器顺序很重要:先执行的拦截器先处理请求
	options.Interceptors.Add<LogInterceptor>();
	});

拦截器顺序的坑:如果同时有认证拦截器和日志拦截器,要把认证拦截器放在前面,这样未认证的请求会被直接拒绝,不会被日志拦截器记录,减少日志量。
3. 客户端拦截器实现认证
csharp

	// AuthInterceptor.cs
	using Grpc.Core;
	using Grpc.Core.Interceptors;
	
	namespace GrpcClient.Interceptors;
	
	public class AuthInterceptor : Interceptor
	{
	private readonly string _token;
	
	public AuthInterceptor(string token)
	{
	_token = token;
	}
	
	public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(TRequest request, ClientInterceptorContext<TRequest, TResponse> context, AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
	{
	// 在请求头中添加认证Token
	var headers = new Metadata();
	headers.Add("Authorization", $"Bearer {_token}");
	var newContext = new ClientInterceptorContext<TRequest, TResponse>(
	context.Method,
	context.Host,
	new CallOptions(headers, context.Options.Deadline, context.Options.CancellationToken)
	);
	// 继续执行RPC调用
	return continuation(request, newContext);
	}
	}

客户端使用拦截器:
csharp

	// GrpcClient/Program.cs
	var channel = GrpcChannel.ForAddress("http://localhost:5000");
	// 创建客户端时添加认证拦截器
	var client = new UserService.UserServiceClient(channel.Intercept(new AuthInterceptor("your-jwt-token")));

七、gRPC性能优化与最佳实践

  1. 性能优化技巧(拓展知识)
    启用HTTP/2多路复用:gRPC默认用HTTP/2,不要改成HTTP/1.1;
    用Protobuf压缩:在服务端和客户端启用Gzip压缩,减少消息体积;
    csharp
	// 服务端启用压缩
	builder.Services.AddGrpc(options =>
	{
	options.ResponseCompressionAlgorithm = "gzip";
	options.ResponseCompressionLevel = System.IO.Compression.CompressionLevel.Optimal;
	});
	// 客户端启用压缩
	var channel = GrpcChannel.ForAddress("http://localhost:5000", new GrpcChannelOptions
	{
	CompressionProviders = { new GzipCompressionProvider() }
	});

设置合理的超时时间:避免RPC调用一直挂着,比如设置10秒超时;
csharp

	var callOptions = new CallOptions(deadline: DateTime.UtcNow.AddSeconds(10));
	var response = await client.GetUserAsync(request, callOptions);

用连接池:gRPC客户端默认用连接池,不要频繁创建和销毁通道;
避免大消息:Protobuf适合小消息,大消息(比如超过10MB)建议用分块传输。
2. 最佳实践
用HTTPS传输:gRPC传输的是二进制数据,虽然Protobuf是二进制,但还是要启用HTTPS,防止数据被抓包;
处理RPC异常:必须捕获RpcException,处理不同的错误码(比如NotFound、Unauthenticated);
拦截器不要做耗时操作:拦截器是同步执行的,耗时操作会阻塞RPC调用;
监控gRPC性能:用Prometheus、Grafana监控RPC调用的QPS、响应时间、错误率;
版本兼容:严格遵守Protobuf的版本兼容规则,不要随便改字段编号。
八、总结:gRPC vs RESTful API的适用场景对比

特性 gRPC RESTful API
性能 快(Protobuf+HTTP/2) 慢(JSON+HTTP/1.1)
消息格式 Protobuf(二进制,强类型) JSON(文本,弱类型)
流支持 支持双向流、服务端流、客户端流 不支持(需要WebSocket)
语言支持 多语言(C#、Java、Go等) 多语言
易用性 中等(需要写.proto文件) 简单(直接写API)
适用场景 微服务内部通信、实时流、高性能场景 对外API、浏览器访问

最佳实践:
1.微服务内部通信用gRPC,提高性能;
2.对外API用RESTful API,方便浏览器和第三方客户端访问;
3.实时场景(聊天、监控)用gRPC双向流;
4.严格遵守Protobuf版本兼容规则,避免通信失败;
5.用拦截器统一处理日志、认证、限流。
下一节我们会学习微服务的消息队列实战:RabbitMQ、Kafka,让你的微服务之间通信既解耦又可靠。

 本站原创,转载请注明出处:https://www.xin3721.com/ArticlecSharp/c49544.html


相关教程