Grpc在C#中的用法
Grpc:引用官网中的一句话
A high-performance, open-source universal RPC framework
所谓RPC(remote procedure call 远程过程调用)框架实际是提供了一套机制,使得应用程序之间可以进行通信,而且也遵从server/client模型。使用的时候客户端调用server端提供的接口就像是调用本地的函数一样。如下图所示就是一个典型的RPC结构图。
架构图:

下面用demo展示Grpc的基本用法,其中包括普通流模式,客户端流模式,服务端流模式,双向流模式
一.Server
1. 首先新建工程,添加Grpc.AspNetCore包,新建protos文件夹,新建protobuffer文件,文件都是以proto作为后缀,我这里是student.proto
syntax = "proto3"; //指定版本 // 定义命名空间 option csharp_namespace = "Grpc.Server.Demo"; // 指定包名,避免冲突 package user; // 定义Student 的 message类型 message Student { string UserName = 1; int32 Age=2; string addr = 3; } // 公共返回类型 message CommonResponse{ int32 code =1; string msg=2; } // 添加学生时传递的类型 message AddStudentRequest{ Student student=1; } // 查询全部学生,没有条件,但也需要一个空的message message QueryAllStudentRequest { } // 上传图片 message UploadImgRequest{ bytes data = 1; } // 查询学生时传递的类型 message QueryStudentRequest { string UserName=1; } message StudentResponse { Student student =1; } message TokenRequest{ string UserName=1; string UserPwd=2; } message TokenResponse{ string Token =1; } // 约定需要提供的服务方法 service StudentService{ rpc GetToken(TokenRequest) returns (TokenResponse); // 简单模式,查询 rpc GetStudentByUserName(QueryStudentRequest) returns (StudentResponse); // 服务端流模式 rpc GetAllStudent(QueryAllStudentRequest) returns (stream StudentResponse); // 客户端流模式 rpc UploadImg(stream UploadImgRequest) returns (CommonResponse); // 双向流模式 rpc AddManyStudents(stream AddStudentRequest) returns (stream StudentResponse); }
2.这个时候需要右键项目->编辑项目文件,增加ItemGroup,这里很重要,不然后面增加服务class时会提示找不到服务接口
1 2 3 4 5 | <ItemGroup> <Protobuf Include= "Protos\student.proto" GrpcServices= "Server" /> <Protobuf Include= "Protos\greet.proto" GrpcServices= "Server" /> <Protobuf Include= "Protos\Teachers.proto" GrpcServices= "Server" /> </ItemGroup> |
3.新建StudentDemoService类,继承StudentService.StudentServiceBase,其中StudentService是student.proto文件中定义的类名,StudentServiceBase是默认的,即StudentService+Base,然后右键StudentDemoService->快速操作和重构->生成重写,选择需要重构的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | using Grpc.Core; using Grpc.Server.Demo.Permission; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.IdentityModel.Tokens; using System; using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; using System.IO; using System.Linq; using System.Security.Claims; using System.Text; using System.Threading.Tasks; namespace Grpc.Server.Demo.Services { //[Authorize] //[Authorize(Policy = "Permission")] public class StudentDemoService: StudentService.StudentServiceBase { private PermissionRequirement _permissionRequirement; public StudentDemoService(PermissionRequirement permissionRequirement) { _permissionRequirement = permissionRequirement; } public override Task<StudentResponse> GetStudentByUserName(QueryStudentRequest request, ServerCallContext context) { var res = Data.Students.Where(s => s.UserName == request.UserName).FirstOrDefault(); if (res == null ) { return Task.FromResult( new StudentResponse { }); } var studentRes = new StudentResponse { Student = res }; return Task.FromResult(studentRes); } public override async Task GetAllStudent(QueryAllStudentRequest request, IServerStreamWriter<StudentResponse> responseStream, ServerCallContext context) { // 之前整体传过去,现在可以模拟一个一个的传 foreach ( var student in Data.Students) { await responseStream.WriteAsync( new StudentResponse { Student = student }); } } public override async Task<CommonResponse> UploadImg(IAsyncStreamReader<UploadImgRequest> requestStream, ServerCallContext context) { try { var tempData = new List< byte >(); while (await requestStream.MoveNext()) { tempData.AddRange(requestStream.Current.Data); } Console.WriteLine($ "接收到文件大小:{tempData.Count}bytes" ); using FileStream fs = new FileStream( "test.jpg" , FileMode.Create); fs.Write(tempData.ToArray(), 0, tempData.ToArray().Length); return new CommonResponse { Code = 0, Msg = "接收成功" }; } catch (Exception ex) { return new CommonResponse { Code = -1, Msg = "接收失败" }; } } public override async Task AddManyStudents(IAsyncStreamReader<AddStudentRequest> requestStream, IServerStreamWriter<StudentResponse> responseStream, ServerCallContext context) { while (await requestStream.MoveNext()) { var student = requestStream.Current.Student; Data.Students.Add(student); await responseStream.WriteAsync( new StudentResponse { Student = student }) ; } } [AllowAnonymous] public override Task<TokenResponse> GetToken(TokenRequest request, ServerCallContext context) { // 模拟登陆验证 if (request.UserName!= "Code综艺圈" ||request.UserPwd!= "admin123" ) { return Task.FromResult( new TokenResponse { Token = string .Empty }); } // 这里可以根据需要将其权限放在Redis中,每次登陆时都重新存,即登陆获取最新权限 // 这里模拟通过userId从数据库中获取分配的接口权限信息,这里存在内存中 _permissionRequirement.Permissions = new List<PermissionData> { new PermissionData{ UserId= "UserId123456" ,Url= "/user.studentservice/getallstudent" }, new PermissionData{ UserId= "UserId123456" ,Url= "/user.studentservice/addmanystudents" } }; // 当登陆验证通过之后才获取对应的token string token = GenerateToken( "UserId123456" , request.UserName); // 返回效应结果 return Task.FromResult( new TokenResponse { Token=token}); } private string GenerateToken( string userId, string userName) { // 秘钥,这是生成Token需要秘钥,和Startup中用到的一致 string secret = "TestSecretTestSecretTestSecretTestSecret" ; // 签发者,是由谁颁发的,和Startup中用到的一致 string issuer = "TestgRPCIssuer" ; // 接受者,是给谁用的,和Startup中用到的一致 string audience = "TestgRPCAudience" ; // 指定秘钥 var securityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(secret)); // 签名凭据,指定对应的签名算法,结合理论那块看哦~~~ var sigingCredentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256); // 自定义payload信息,每一个claim代表一个属性键值对,就类似身份证上的姓名,出生年月一样 var claims = new Claim[] { new Claim( "userId" , userId), new Claim( "userName" , userName), // claims中添加角色属性,这里的键一定要用微软封装好的ClaimTypes.Role new Claim(ClaimTypes.Role, "Admin" ), new Claim(ClaimTypes.Role, "Maintain" ) }; // 组装生成Token的数据 SecurityToken securityToken = new JwtSecurityToken( issuer: issuer, // 颁发者 audience: audience, // 接受者 claims: claims, //自定义的payload信息 signingCredentials: sigingCredentials, // 凭据 expires: DateTime.Now.AddMinutes(30) // 设置超时时间,30分钟之后过期 ); // 生成Token return new JwtSecurityTokenHandler().WriteToken(securityToken); } } } |
二.Client
1.新建工程添加Google.Protobuf,Grpc.Net.Client,Grpc.Tools包
把刚才服务端新建的student.proto文件复制过来
调用方式:
1 2 3 4 5 | using var channel = GrpcChannel.ForAddress( "https://localhost:5001" ); var grpcClient = new StudentService.StudentServiceClient(channel); //2、客户端一次请求,多次返回 using var respStreamingCall = grpcClient.GetAllStudent( new QueryAllStudentRequest()); |
客户端完整的调用方式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 | using Google.Protobuf; using Grpc.Core; using Grpc.Net.Client; using Grpc.Server.Demo; using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; namespace WinFormsGrpc.Client.Demo { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void Form1_Load( object sender, EventArgs e) { // 允许多线程更新UI Control.CheckForIllegalCrossThreadCalls = false ; } #region 简单模式 private void btn_sample_Click( object sender, EventArgs e) { //1、创建grpc客户端 using var channel = GrpcChannel.ForAddress( "https://localhost:5001" ); var grpcClient = new StudentService.StudentServiceClient(channel); //2、发起请求 var resp = grpcClient.GetStudentByUserName( new QueryStudentRequest { UserName = txt_condition.Text.Trim() }); //3、处理响应结果,将其显示在文本框中 this .txt_result.Text = $ "姓名:{resp?.Student?.UserName},年龄:{resp?.Student?.Age},地址:{resp?.Student?.Addr}" ; } #endregion #region 服务端流模式 private async void btn_server_Click( object sender, EventArgs e) { //用于多线程通知 CancellationTokenSource cts = new CancellationTokenSource(); //1、创建grpc客户端 using var channel = GrpcChannel.ForAddress( "https://localhost:5001" ); var grpcClient = new StudentService.StudentServiceClient(channel); //2、客户端一次请求,多次返回 using var respStreamingCall = grpcClient.GetAllStudent( new QueryAllStudentRequest()); //3、获取响应流 var respStreaming = respStreamingCall.ResponseStream; //4、循环读取响应流,直到读完为止 while (await respStreaming.MoveNext(cts.Token)) { //5、取得每次返回的信息,并显示在文本框中 var student = respStreaming.Current.Student; this .txt_result.Text += $ "姓名:{student.UserName},年龄:{student.Age},地址:{student.Addr}\r\n" ; } } #endregion private async void btn_client_Click( object sender, EventArgs e) { // 用于存放选择的文件路径 string filePath = string .Empty; // 打开文件选择对话框 if ( this .openFileDialog1.ShowDialog() == DialogResult.OK) { filePath = this .openFileDialog1.FileName; } if ( string .IsNullOrEmpty(filePath)) { this .txt_result.Text = "请选择文件" ; return ; } //1、创建grpc客户端 using var channel = GrpcChannel.ForAddress( "https://localhost:5001" ); var grpcClient = new StudentService.StudentServiceClient(channel); //2、读取选择的文件 FileStream fileStream = File.OpenRead(filePath); //3、通过客户端请求流将文件流发送的服务端 using var call = grpcClient.UploadImg(); var clientStream = call.RequestStream; //4、循环发送,指定发送完文件 while ( true ) { // 一次最多发送1024字节 byte [] buffer = new byte [1024]; int nRead = await fileStream.ReadAsync(buffer, 0, buffer.Length); // 直到读不到数据为止,即文件已经发送完成,即退出发送 if (nRead == 0) { break ; } // 5、将每次读取到的文件流通过客户端流发送到服务端 await clientStream.WriteAsync( new UploadImgRequest { Data = ByteString.CopyFrom(buffer) }); } // 6、发送完成之后,告诉服务端发送完成 await clientStream.CompleteAsync(); // 7、接收返回结果,并显示在文本框中 var res = await call.ResponseAsync; this .txt_result.Text = $ "上传返回Code:{res.Code},Msg:{res.Msg}" ; } private async void btn_double_Click( object sender, EventArgs e) { //用于多线程通知 CancellationTokenSource cts = new CancellationTokenSource(); //模拟通过请求流方式保存多个Student,同时通过响应流的方式返回存储结果 List<Student> students = new List<Student> { new Student{ UserName= "Code综艺圈1" , Age=20, Addr= "关注我一块学" }, new Student{ UserName= "综艺圈好酷" , Age=18, Addr= "关注Code综艺圈和我一块学" } }; //1、创建grpc客户端 using var channel = GrpcChannel.ForAddress( "https://localhost:5001" ); var grpcClient = new StudentService.StudentServiceClient(channel); //2、分别取得请求流和响应流 using var call = grpcClient.AddManyStudents(); var requestStream = call.RequestStream; var responseStream = call.ResponseStream; //3、开启一个线程专门用来接收响应流 var taskResp = Task.Run(async () => { while (await responseStream.MoveNext(cts.Token)) { var student = responseStream.Current.Student; // 将接收到结果在文本框中显示 ,多线程更新UI简单处理一下:Control.CheckForIllegalCrossThreadCalls = false; this .txt_result.Text += $ "保存成功,姓名:{student.UserName},年龄:{student.Age},地址:{student.Addr}\r\n" ; } }); //4、通过请求流的方式将多条数据依次传到服务端 foreach ( var student in students) { // 每次发送一个学生请求 await requestStream.WriteAsync( new AddStudentRequest { Student = student }); } //5、传送完毕 await requestStream.CompleteAsync(); await taskResp; } private async Task< string > GetToken() { //1、创建grpc客户端 using var channel = GrpcChannel.ForAddress( "https://localhost:5001" ); var grpcClient = new StudentService.StudentServiceClient(channel); //2、发起请求 var resp = await grpcClient.GetTokenAsync( new TokenRequest { UserName = "Code综艺圈" , UserPwd = "admin123" }); return resp.Token; } private async void btn_server_token_Click( object sender, EventArgs e) { //用于多线程通知 CancellationTokenSource cts = new CancellationTokenSource(); //1、创建grpc客户端 using var channel = GrpcChannel.ForAddress( "https://localhost:5001" ); var grpcClient = new StudentService.StudentServiceClient(channel); // 获取Token var token = await GetToken(); // 将Token封装为Metadat和请求一起发送过去 var headers = new Metadata { { "Authorization" ,$ "Bearer {token}" } }; //2、客户端一次请求,多次返回 using var respStreamingCall = grpcClient.GetAllStudent( new QueryAllStudentRequest(), headers); //3、获取响应流 var respStreaming = respStreamingCall.ResponseStream; //4、循环读取响应流,直到读完为止 while (await respStreaming.MoveNext(cts.Token)) { //5、取得每次返回的信息,并显示在文本框中 var student = respStreaming.Current.Student; this .txt_result.Text += $ "姓名:{student.UserName},年龄:{student.Age},地址:{student.Addr}\r\n" ; } } } } |
本文来自博客园,作者:可乐加冰-Mr-Wang,转载请注明原文链接:https://www.cnblogs.com/helloworld-wang/p/15035652.html
【推荐】还在用 ECharts 开发大屏?试试这款永久免费的开源 BI 工具!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步