Asp-Net-Core学习笔记:gRPC快速入门
前言
此前,我在做跨语言调用时,用的是 Facebook 的 Thrift,挺轻量的,还不错。
Thrift是一种接口描述语言和二进制通讯协议,它被用来定义和创建跨语言的服务。它被当作一个远程过程调用(RPC)框架来使用,是由Facebook为“大规模跨语言服务开发”而开发的。它通过一个代码生成引擎联合了一个软件栈,来创建不同程度的、无缝的跨平台高效服务,可以使用C#、C++(基于POSIX兼容系统)Cappuccino、Cocoa、Delphi、Erlang、Go、Haskell、Java、Node.js、OCaml、Perl、PHP、Python、Ruby和Smalltalk编程语言开发。 2007由Facebook开源,2008年5月进入Apache孵化器, 2010年10月成为Apache的顶级项目。
最近的项目中也有类似的需求,这次打算使用 Google 的 gRPC,原因是 gRPC 知名度也很高,之前一直想用但没有场景,加上 AspNetCore 的文档里有介绍,看起来官方也是推荐使用这种方式进行 RPC 调用。
所以就果断开始了 gRPC 实践,最终做出来的效果还是可以的(这是后话)。
本文主要介绍 C# 与 Python 之间,使用 gRPC 进行跨语言调用。
概念科普
什么是RPC?
远程过程调用(Remote Procedure Call,缩写为 RPC)是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互作用编程。 比如 Java RMI(远程方法调用(Remote
Method Invocation)。能够让在某个java虚拟机上的对象像调用本地对象一样调用另一个java虚拟机中的对象上的方法)。
从上图可以看出, RPC 本身是 client-server模型,也是一种 request-response 协议。
有些实现扩展了远程调用的模型,实现了双向的服务调用,但是不管怎样,调用过程还是由一个客户端发起,服务器端提供响应,基本模型没有变化。
服务的调用过程为:
- client调用client stub,这是一次本地过程调用
- client stub将参数打包成一个消息,然后发送这个消息。打包过程也叫做 marshalling
- client所在的系统将消息发送给server
- server的的系统将收到的包传给server stub
- server stub解包得到参数。 解包也被称作 unmarshalling
- 最后server stub调用服务过程. 返回结果按照相反的步骤传给client
关于 gRPC
gRPC 是一种与语言无关的高性能远程过程调用 (RPC) 框架。
前文有说过,这个是 Google 开发的,以下是 AspNetCore 的文档中,对 gRPC 的介绍。
gRPC 的主要优点是:
- 现代高性能轻量级 RPC 框架。
- 协定优先 API 开发,默认使用协议缓冲区,允许与语言无关的实现。
- 可用于多种语言的工具,以生成强类型服务器和客户端。
- 支持客户端、服务器和双向流式处理调用。
- 使用 Protobuf 二进制序列化减少对网络的使用。
这些优点使 gRPC 适用于:
- 效率至关重要的轻量级微服务。
- 需要多种语言用于开发的 Polyglot 系统。
- 需要处理流式处理请求或响应的点对点实时服务。
gRPC 使用 .proto
文件来定义接口和数据类型,同时提供了通过 .proto
文件生成不同语言调用代码的工具。
项目介绍
本文的项目是使用 C# 调用 Python 写的服务,这个服务是一个大语言模型。
在本项目中,C# 项目作为客户端,Python 项目作为服务端。
编写 .proto
文件
第一步,要先编写用于定义接口和数据结构的 .proto
文件。
将下面代码保存到 chat.proto
文件里,接下来在要接入 gRPC 的每个项目里,都要复制一份这个文件。
syntax = "proto3";
import "google/protobuf/wrappers.proto";
option csharp_namespace = "AIHub.Blazor";
package aihub;
service ChatHub {
rpc Chat (ChatRequest) returns (ChatReply);
rpc StreamingChat (ChatRequest) returns (stream ChatReply);
}
message ChatRequest {
string prompt = 1;
repeated google.protobuf.StringValue history = 2;
int32 max_length = 3;
float top_p = 4;
float temperature = 5;
}
message ChatReply {
string response = 1;
}
这里定义了两个 Chat 方法,其中一个是一元调用,一个是流式输出。
gRPC 服务可以有不同类型的方法,服务发送和接收消息的方式取决于所定义的方法的类型
- 一元 - 调用完就返回,同步方法
- 服务器流式处理 - 服务端流式返回数据
- 客户端流式处理 - 客户端流式写入数据
- 双向流式处理
都挺好理解的,在 proto
定义里面也很易懂,stream
放在哪里,哪里就是流式。
stream
放在请求参数,那客户端输入就是流式的,放在返回值前面,那服务端的返回就是流式。都放就代表双向流式。
接着又定义了用到的数据结构,一个输入参数 ChatRequest
,一个返回参数 ChatReply
。
string prompt = 1;
这里赋值的意思是这个参数的位置,不是定义变量的赋值,第一次用差点整蒙了。
服务端 Python 项目
先在 Python 项目里,把 gRPC 服务器搭建起来。
把上面的 chat.proto
文件,复制到 Python 项目的目录里面。
Python 项目需要安装依赖
pip install grpcio
pip install grpcio-tools
执行命令生成代码
python -m grpc_tools.protoc -I . --python_out=. --pyi_out=. --grpc_python_out=. ./chat.proto
上面的命令,可以根据 proto
文件,生成以下代码
- chat_pb2_grpc.py
- chat_pb2.py
- chat_pb2.pyi
编写服务器代码
from concurrent import futures
import logging
import grpc
import chat_pb2
import chat_pb2_grpc
class Greeter(chat_pb2_grpc.ChatHubServicer):
def Chat(self, request: chat_pb2.ChatRequest, context):
response, history = model.chat(
tokenizer,
request.prompt,
history=[],
max_length=request.max_length,
top_p=request.top_p,
temperature=request.temperature)
return chat_pb2.ChatReply(response=response)
def StreamingChat(self, request: chat_pb2.ChatRequest, context):
past_key_values, history = None, []
current_length = 0
for response, history, past_key_values in model.stream_chat(
tokenizer, request.prompt, history=history,
past_key_values=past_key_values,
return_past_key_values=True):
print(response[current_length:], end="", flush=True)
yield chat_pb2.ChatReply(response=response)
current_length = len(response)
def serve():
port = '50051'
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
chat_pb2_grpc.add_ChatHubServicer_to_server(Greeter(), server)
server.add_insecure_port('[::]:' + port)
server.start()
print("Server started, listening on " + port)
server.wait_for_termination()
if __name__ == '__main__':
logging.basicConfig()
serve()
前面在 proto
里定义的俩方法:Chat
, StreamingChat
要自己实现,这里我直接调用 Transformer ,一元方法就直接返回,流式输出使用 yield 生成结果。
扩展:Python gRPC客户端
本文把 Python 项目作为 gRPC 的服务端供 AspNetCore 项目调用
反过来也是没问题的
Python 作为客户端的代码如下
import logging
import grpc
import chat_pb2
import chat_pb2_grpc
def run():
print("Will try to greet world ...")
with grpc.insecure_channel('localhost:50051') as channel:
stub = chat_pb2_grpc.ChatHubStub(channel)
response = stub.Chat(chat_pb2.ChatRequest(prompt='你好'))
print("Greeter client received: " + response.message)
if __name__ == '__main__':
logging.basicConfig()
run()
客户端 AspNetCore 项目
AspNetCore 与 gRPC 的集成非常容易
先把上面的 proto
文件添加到项目中
在本项目中我放到 Protos/chat.proto
中
先安装 nuget 包
dotnet add Grpc.AspNetCore
然后编辑 .csproj
添加以下配置
<ItemGroup>
<Protobuf Include="Protos\chat.proto" GrpcServices="Both" />
</ItemGroup>
其中 GrpcServices="Both"
表示同时生成服务端和客户端代码
完成之后,Build 项目,然后就会在 obj\Debug\net6.0\Protos
目录下生成对应的代码
不过这些代码不需要直接引用,编译器会自动处理。
客户端
编辑 Program.cs
文件
注册一个 gRPC 客户端
builder.Services.AddGrpcClient<ChatHub.ChatHubClient>(options => {
options.Address = new Uri(builder.Configuration.GetValue<string>("gRPCServer"));
});
如果要注册多个相同的客户端,可以加个名字区分
builder.Services.AddGrpcClient<ChatHub.ChatHubClient>("client1", options => {
options.Address = new Uri(builder.Configuration.GetValue<string>("gRPCServer"));
});
builder.Services.AddGrpcClient<ChatHub.ChatHubClient>("client2", options => {
options.Address = new Uri(builder.Configuration.GetValue<string>("gRPCServer"));
});
使用的时候直接注入就完事了。
如果是命名客户端,就先注入 GrpcClientFactory
对象
var clinet1 = grpcClientFactory.CreateClient<ChatHub.ChatHubClient>("client1");
var clinet2 = grpcClientFactory.CreateClient<ChatHub.ChatHubClient>("client2");
发起一个请求(一元模式)
var request = new ChatRequest { Prompt = "哈喽" };
ChatReply reply = ChatClient.Chat(request);
读取流式返回数据
using (var call = ChatClient.StreamingChat(request)) {
await foreach (var resp in call.ResponseStream.ReadAllAsync()) {
Console.Write(resp.Response);
}
}
简简单单,搞定~
服务端
同样很简单
注册服务、配置中间件
builder.Services.AddGrpc();
app.MapGrpcService<ChatService>();
ChatService 代码
public class ChatService : ChatHub.ChatHubBase {
private readonly ILogger<ChatService> _logger;
public ChatService(ILogger<ChatService> logger) {
_logger = logger;
}
public override Task<ChatReply> Chat(ChatRequest request, ServerCallContext context) {
return new Task<ChatReply>(() => new ChatReply { Response = $"你好,{request.Prompt}" });
}
public override async Task StreamingChat(ChatRequest request, IServerStreamWriter<ChatReply> responseStream,
ServerCallContext context) {
for (var i = 0; i < 5; i++) {
await responseStream.WriteAsync(new ChatReply { Response = $"{i}" });
await Task.Delay(TimeSpan.FromSeconds(1));
}
}
}
同样是把 protobuf 里定义的俩方法实现了一遍。
代码比较易懂,跟上面的 Python 版差不多,这里就不重复解释了。
小结
gRPC 使用起来非常的丝滑,目前来说也没遇到什么坑,可以非常平滑地与已有项目集成,如果有类似的场景,强烈推荐尝试一下 gRPC !
gRPC 的功能很多,本文仅介绍了最基本的使用,更多的请阅读文档,详细有了本文的基础铺垫,读者再阅读文档深入使用时,会比较轻松上手。
PS:最近这个使用到 gRPC 的项目,接下来我会写个小结博客介绍一下~
参考资料
- Thrift 简单介绍 - https://www.jianshu.com/p/8f25d057a5a9
- https://grpc.io/docs/languages/python/quickstart/
- https://learn.microsoft.com/zh-cn/aspnet/core/grpc/
Asp-Net-Core学习笔记:gRPC快速入门的更多相关文章
- Asp.Net Core学习笔记:入门篇
Asp.Net Core 学习 基于.Net Core 2.2版本的学习笔记. 常识 像Django那样自动检查代码更新,自动重载服务器(太方便了) dotnet watch run 托管设置 设置项 ...
- ASP.NET Core 学习笔记 第一篇 ASP.NET Core初探
前言 因为工作原因博客断断续续更新,其实在很早以前就有想法做一套关于ASP.NET CORE整体学习度路线,整体来说国内的环境的.NET生态环境还是相对比较严峻的,但是干一行爱一行,还是希望更多人加入 ...
- Asp.net Core学习笔记
之前记在github上的,现在搬运过来 变化还是很大的,感觉和Nodejs有点类似,比如中间件的使用 ,努力学习ing... 优点 不依赖IIS 开源和跨平台 中间件支持 性能优化 无所不在的依赖注入 ...
- ASP.NET Core 学习笔记 第三篇 依赖注入框架的使用
前言 首先感谢小可爱门的支持,写了这个系列的第二篇后,得到了好多人的鼓励,也更加坚定我把这个系列写完的决心,也能更好的督促自己的学习,分享自己的学习成果.还记得上篇文章中最后提及到,假如服务越来越多怎 ...
- ASP.NET Core 学习笔记 第四篇 ASP.NET Core 中的配置
前言 说道配置文件,基本大多数软件为了扩展性.灵活性都会涉及到配置文件,比如之前常见的app.config和web.config.然后再说.NET Core,很多都发生了变化.总体的来说技术在进步,新 ...
- ASP.NET Core 学习笔记 第五篇 ASP.NET Core 中的选项
前言 还记得上一篇文章中所说的配置吗?本篇文章算是上一篇的延续吧.在 .NET Core 中读取配置文件大多数会为配置选项绑定一个POCO(Plain Old CLR Object)对象,并通过依赖注 ...
- asp.net.core学习笔记1:swagger的使用和webapi接收Jobject对象
环境:asp.net.core 3.1 (一觉醒来官方已经不推荐3.0了,于是没有任何core经验,也只能开始了3.1的开发学习) 由于现有项目前后端分离.微服务化日趋流行,所以上手不采用web应用( ...
- Asp.net core 学习笔记 ( Data protection )
参考 : http://www.cnblogs.com/xishuai/p/aspnet-5-identity-part-one.html http://cnblogs.com/xishuai/p/a ...
- Asp.net core 学习笔记 SignalR
refer : https://kimsereyblog.blogspot.com/2018/07/signalr-with-asp-net-core.html https://github.com/ ...
- Asp.net core (学习笔记 路由和语言 route & language)
https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/routing?view=aspnetcore-2.1 https://doc ...
随机推荐
- Java设计模式 —— 组合模式
11 组合模式 11.1 组合模式概述 Composite Pattern: 组合多个对象形成树形结构以表示具有部分-整体关系的层次结构.组合模式使得客户端可以统一处理单个对象和组合对象. 组合模式关 ...
- 快速搭建一个go语言web后端服务脚手架
快速搭建一个go语言web后端服务脚手架 源码:https://github.com/weloe/go-web-demo web框架使用gin,数据操作使用gorm,访问控制使用casbin 首先添加 ...
- SqlServer 添加字段说明、表说明
1.添加表说明 EXECUTE sp_addextendedproperty N'MS_Description','表说明',N'user',N'dbo',N'table',N'表名',NULL,NU ...
- [C++提高编程] 3.8 set/ multiset 容器
文章目录 3.8 set/ multiset 容器 3.8.1 set基本概念 3.8.2 set构造和赋值 3.8.3 set大小和交换 3.8.4 set插入和删除 3.8.5 set查找和统计 ...
- 手记系列之四 ----- 关于使用MySql的经验
前言 本篇文章主要介绍的关于本人在使用MySql记录笔记的一些使用方法和经验,温馨提示,本文有点长,约1.5w字,几十张图片,建议收藏查看. 一.MySql安装 下载地址:https://dev.my ...
- 搭建SpringBoot项目依赖和配置快速篇
maven依赖及一些配置 这里主要是搭建项目常用到的maven依赖以及搭建项目会需要用到的一些配置文件,可能下面这些依赖还不是很全,但是应该会满足日常大部分的需求了 Spring Spring项目的依 ...
- redis内存突然暴增,排查思路是什么
1 这种暴增的应该还是上次一个群友说的,更多可能是外部因素导致的,应用新上线,定时任务这些,再有就是cat上查是哪些指令多,以及比对和之前的时间的差异 看是否有定时任务 或者 新上线的活动 ,在看下监 ...
- 2021-04-02:给定一个正方形或者长方形矩阵matrix,实现zigzag打印。[[0,1,2],[3,4,5],[6,7,8]]的打印顺序是0,1,3,6,4,2,5,7,8。
2021-04-02:给定一个正方形或者长方形矩阵matrix,实现zigzag打印.[[0,1,2],[3,4,5],[6,7,8]]的打印顺序是0,1,3,6,4,2,5,7,8. 福大大 答案2 ...
- 2021-04-09:rand指针是单链表节点结构中新增的指针,rand可能指向链表中的任意一个节点,也可能指向null。给定一个由Node节点类型组成的无环单链表的头节点 head,请实现一个函数完成这个链表的复制,并返回复制的新链表的头节点。 【要求】时间复杂度O(N),额外空间复杂度O(1) 。
2021-04-09:rand指针是单链表节点结构中新增的指针,rand可能指向链表中的任意一个节点,也可能指向null.给定一个由Node节点类型组成的无环单链表的头节点 head,请实现一个函数完 ...
- 2021-08-09:给定一个有正、有负、有0的数组arr,给定一个整数k,返回arr的子集是否能累加出k。1)正常怎么做?2)如果arr中的数值很大,但是arr的长度不大,怎么做?
2021-08-09:给定一个有正.有负.有0的数组arr,给定一个整数k,返回arr的子集是否能累加出k.1)正常怎么做?2)如果arr中的数值很大,但是arr的长度不大,怎么做? 福大大 答案20 ...