Asp-Net-Core学习笔记:gRPC快速入门

asp,net,core,学习,笔记,grpc,快速,入门 · 浏览次数 : 302

小编点评

# gRPC 代码简介 ## gRPC 使用起来非常丝滑 目前来说,也没遇到任何坑,可以非常平滑地与已有项目集成。 ### 代码解析 * **`Chat`**方法:接收请求,返回响应 * **``StreamingChat`**方法:接收请求,生成响应流 * **```CreateClient`**方法:创建 gRPC 服务端客户端 * **````AddGrpcClient`**方法:添加 gRPC 服务端客户端 * **`````StreamChat`**方法:创建 gRPC 流式输出客户端 ## 代码基础 * **`ILogger`** 用于记录日志 * **``ChatService`** 是 gRPC 服务端类 * **````ChatHubBase`** 是 gRPC 服务端基类 ## 代码应用 * **``MapGrpcService`** 将 gRPC 服务端映射到 gRPC 服务端接口 * **````ChatService`** 接口定义了 **````Chat`** 方法 ## 总结 gRPC 使用起来非常丝滑,代码易懂,逻辑清晰,可以非常平滑地与已有项目集成。 ### 参考资料 * Thrift 简单介绍 - https://www.jianshu.com/p/8f25d057a5a9 * gRPC 使用起来非常丝滑 - https://grpc.io/docs/languages/python/quickstart/

正文

前言

此前,我在做跨语言调用时,用的是 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快速入门相似的内容:

Asp-Net-Core学习笔记:gRPC快速入门

## 前言 此前,我在做跨语言调用时,用的是 Facebook 的 Thrift,挺轻量的,还不错。 >Thrift是一种接口描述语言和二进制通讯协议,它被用来定义和创建跨语言的服务。它被当作一个远程过程调用(RPC)框架来使用,是由Facebook为“大规模跨语言服务开发”而开发的。它通过一个代码

Asp-Net-Core学习笔记:单元测试和集成测试

## 前言 我在使用 AspNetCore 的这段时间内,看了很多开源项目和博客,发现各种 .Net 体系的新技术很多人都有关注和使用,但却很少有人关注测试。 测试是软件生命周期中的一个非常重要的阶段,对于保证软件的可靠性具有极其重要的意义。在应用程序的开发过程中,为了确保它的功能与预期一致,必须对

.NET周刊【7月第1期 2024-07-07】

国内文章 学习.NET 8 MiniApis入门 https://www.cnblogs.com/hejiale010426/p/18280441 MiniApis是ASP.NET Core中的轻量级框架,用最少的代码和配置创建HTTP API。其特点包括简洁明了、性能卓越、灵活多变、易于学习使用,

ASP.NET Core中创建中间件的几种方式

前言 今天我们一起来盘点一下在ASP.NET Core应用程序中添加和创建中间件常见的四种方式。 中间件介绍 ASP.NET Core中间件(Middleware)是用于处理HTTP请求和响应的组件,它们被安排在请求处理管道中,并按顺序执行。中间件的设计是为了使其在请求处理管道中能够以灵活和可扩展的

推荐十个优秀的ASP.NET Core第三方中间件,你用过几个?

ASP.NET Core 作为一个强大的、跨平台的、高性能的开源框架,为开发者提供了丰富的功能和灵活的扩展性。其中,中间件(Middleware)是 ASP.NET Core 架构中的核心组件之一,它负责处理 HTTP 请求和响应的管道,允许开发者在请求和响应之间插入自定义逻辑。随着 ASP.NET

Asp-Net-Core开发笔记:使用原生的接口限流功能

前言 之前介绍过使用 AspNetCoreRateLimit 组件来实现接口限流 从 .Net7 开始,AspNetCore 开始内置限流组件,当时我们的项目还在 .Net6 所以只能用第三方的 现在都升级到 .Net8 了,当然是得来试试这个原生组件 体验后:配置使用都比较简单,不过功能也没有 A

ASP.NET Core如何禁用模型验证(或者从模型状态中移除某些属性)?

这是一篇4年前的文章:【经验分享】在ASP.NET Core中,如果禁用某个请求的模型验证? 事隔多年,又有网友问到这个问题。我就来重新整理一下,顺便扩展一下之前的解决办法。 这是一个来自网友【David】的提问。在 AppBoxCore 项目的新增用户页面,新增一个上传按钮:

Asp-Net-Core开发笔记:进一步实现非侵入性审计日志功能

前言 上次说了利用 AOP 思想实现了审计日志功能,不过有同学反馈还是无法实现完全无侵入,于是我又重构了一版新的。 回顾一下:Asp-Net-Core开发笔记:实现动态审计日志功能 现在已经可以实现对业务代码完全无侵入的审计日志了,在需要审计的接口上加上 [AuditLog] 特性,就可以记录这个接

Asp .Net Core 系列:国际化多语言配置

目录概述术语本地化器IStringLocalizer在服务类中使用本地化IStringLocalizerFactoryIHtmlLocalizerIViewLocalizer资源文件区域性回退配置 CultureProvider内置的 RequestCultureProvider实现自定义 Requ

安全机密管理:Asp.Net Core中的本地敏感数据保护技巧

前言 在我们开发过程中基本上不可或缺的用到一些敏感机密数据,比如SQL服务器的连接串或者是OAuth2的Secret等,这些敏感数据在代码中是不太安全的,我们不应该在源代码中存储密码和其他的敏感数据,一种推荐的方式是通过Asp.Net Core的机密管理器。 机密管理器 在 ASP.NET Core