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

demo地址

posted @   可乐加冰-Mr-Wang  阅读(3463)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示