前言

此前,我在做跨语言调用时,用的是 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 协议。

有些实现扩展了远程调用的模型,实现了双向的服务调用,但是不管怎样,调用过程还是由一个客户端发起,服务器端提供响应,基本模型没有变化。

服务的调用过程为:

  1. client调用client stub,这是一次本地过程调用
  2. client stub将参数打包成一个消息,然后发送这个消息。打包过程也叫做 marshalling
  3. client所在的系统将消息发送给server
  4. server的的系统将收到的包传给server stub
  5. server stub解包得到参数。 解包也被称作 unmarshalling
  6. 最后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 的项目,接下来我会写个小结博客介绍一下~

参考资料

Asp-Net-Core学习笔记:gRPC快速入门的更多相关文章

  1. Asp.Net Core学习笔记:入门篇

    Asp.Net Core 学习 基于.Net Core 2.2版本的学习笔记. 常识 像Django那样自动检查代码更新,自动重载服务器(太方便了) dotnet watch run 托管设置 设置项 ...

  2. ASP.NET Core 学习笔记 第一篇 ASP.NET Core初探

    前言 因为工作原因博客断断续续更新,其实在很早以前就有想法做一套关于ASP.NET CORE整体学习度路线,整体来说国内的环境的.NET生态环境还是相对比较严峻的,但是干一行爱一行,还是希望更多人加入 ...

  3. Asp.net Core学习笔记

    之前记在github上的,现在搬运过来 变化还是很大的,感觉和Nodejs有点类似,比如中间件的使用 ,努力学习ing... 优点 不依赖IIS 开源和跨平台 中间件支持 性能优化 无所不在的依赖注入 ...

  4. ASP.NET Core 学习笔记 第三篇 依赖注入框架的使用

    前言 首先感谢小可爱门的支持,写了这个系列的第二篇后,得到了好多人的鼓励,也更加坚定我把这个系列写完的决心,也能更好的督促自己的学习,分享自己的学习成果.还记得上篇文章中最后提及到,假如服务越来越多怎 ...

  5. ASP.NET Core 学习笔记 第四篇 ASP.NET Core 中的配置

    前言 说道配置文件,基本大多数软件为了扩展性.灵活性都会涉及到配置文件,比如之前常见的app.config和web.config.然后再说.NET Core,很多都发生了变化.总体的来说技术在进步,新 ...

  6. ASP.NET Core 学习笔记 第五篇 ASP.NET Core 中的选项

    前言 还记得上一篇文章中所说的配置吗?本篇文章算是上一篇的延续吧.在 .NET Core 中读取配置文件大多数会为配置选项绑定一个POCO(Plain Old CLR Object)对象,并通过依赖注 ...

  7. asp.net.core学习笔记1:swagger的使用和webapi接收Jobject对象

    环境:asp.net.core 3.1 (一觉醒来官方已经不推荐3.0了,于是没有任何core经验,也只能开始了3.1的开发学习) 由于现有项目前后端分离.微服务化日趋流行,所以上手不采用web应用( ...

  8. Asp.net core 学习笔记 ( Data protection )

    参考 : http://www.cnblogs.com/xishuai/p/aspnet-5-identity-part-one.html http://cnblogs.com/xishuai/p/a ...

  9. Asp.net core 学习笔记 SignalR

    refer : https://kimsereyblog.blogspot.com/2018/07/signalr-with-asp-net-core.html https://github.com/ ...

  10. Asp.net core (学习笔记 路由和语言 route & language)

    https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/routing?view=aspnetcore-2.1 https://doc ...

随机推荐

  1. ASP.NET Core - 缓存之内存缓存(下)

    话接上篇 [ASP.NET Core - 缓存之内存缓存(上)],所以这里的目录从 2.4 开始. 2.4 MemoryCacheEntryOptions MemoryCacheEntryOption ...

  2. opengauss配置远程白名单

    DB_VERSION:openGauss 3.0.3 1.允许192.168网段用户使用jack用户登陆 --创建只读账号 CREATE USER jack WITH MONADMIN passwor ...

  3. 最新升级优化 shopee|美客多 Mercadolibre|shopfiy|lazada|独立货代贴单系统 可规模化的贴单打单系统 源码下载独立部署

    七想网络 跨境猴 最新优化改进版本的 虾皮代打包-虾皮代贴单 独立部署源码版本货代贴单系统 介绍: 台湾海外仓_shopee货代_虾皮物流–虾皮代贴单 虾皮代打包-虾皮代贴单-虾皮货代平台 shope ...

  4. Django笔记二十三之case、when操作条件表达式搜索、更新等操作

    本文首发于公众号:Hunter后端 原文链接:Django笔记二十三之条件表达式搜索.更新等操作 这一篇笔记将介绍条件表达式,就是如何在 model 的使用中根据不同的条件筛选数据返回. 这个操作类似 ...

  5. MySQL MHA信息的收集【Filebeat+logstash+MySQL】

    一.项目背景 随着集团MHA集群的日渐增长,MHA管理平台话越来越迫切.而MHA平台的建设第一步就是将这些成百上千套的MHA集群信息收集起来,便于查询和管理. MHA主要信息如下: (1)基础配置信息 ...

  6. 04-webpack初体验

    /** * index.js: webpack入口起点文件 * * 1.运行指令: * 开发环境:webpack ./src/index.js -o ./build --mode=developmen ...

  7. xcode历史版本下载

    一.背景 较早之前做过一个项目,当时使用swift 3.x开发. 项目结束后就没再有新需求与更新. 但最近呢需要对项目的某些功能进行调整,项目又重新被拾了起来. 我们知道现在的swift 版本已经到了 ...

  8. 如何通过C#/VB.NET 代码调整PDF文档的页边距

    PDF边距是页面主要内容区域和页面边缘之间的距离.与Word页边距不同,PDF文档的页边距很难更改.因为Adobe没有提供操作页边距的直接方法.但是,您可以通过缩放页面内容来改变页边距.本文将介绍如何 ...

  9. 极速进化,光速转录,C++版本人工智能实时语音转文字(字幕/语音识别)Whisper.cpp实践

    业界良心OpenAI开源的Whisper模型是开源语音转文字领域的执牛耳者,白璧微瑕之处在于无法通过苹果M芯片优化转录效率,Whisper.cpp 则是 Whisper 模型的 C/C++ 移植版本, ...

  10. 【Visual Leak Detector】在 VS 高版本中使用 VLD

    说明 使用 VLD 内存泄漏检测工具辅助开发时整理的学习笔记. 本篇介绍如何在 VS 高版本中使用 vld2.5.1.同系列文章目录可见 <内存泄漏检测工具>目录 目录 说明 1. 使用前 ...