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时会提示找不到服务接口

  <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->快速操作和重构->生成重写,选择需要重构的方法

 

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文件复制过来

 调用方式:

 using var channel = GrpcChannel.ForAddress("https://localhost:5001");
            var grpcClient = new StudentService.StudentServiceClient(channel);

            //2、客户端一次请求,多次返回
            using var respStreamingCall = grpcClient.GetAllStudent(new QueryAllStudentRequest());

 客户端完整的调用方式如下:

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";
            }
        }


    }
}

demo地址

posted @ 2021-07-20 16:31  可乐加冰-Mr-Wang  阅读(3287)  评论(0编辑  收藏  举报