整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

Spring Boot + gRPC 构建可扩展的微

Spring Boot + gRPC 构建可扩展的微服务

文是一份详尽且深入的指南,旨在帮助读者理解并实现将 gRPC 通过 Maven 集成到 SpringBoot 项目中的全过程。文章首先以高度概括的方式探讨了 gRPC 的理论基础,为读者提供了对其核心概念的清晰认识。随后,我们将转向更为具体的实践层面,展示 gRPC 在实际应用中的多种实现方式。

虽然全文篇幅较长,但我们强烈建议您耐心阅读,以确保您能够全面掌握 gRPC 的技术细节和应用场景。通过本文的学习,您将不仅能够理解 gRPC 的工作原理,还能在您的 SpringBoot 项目中成功集成和应用这一强大的通信协议,从而提升系统的性能和效率。

什么是gRPC?

gRPC 是最初由 Google 开发的开源远程过程调用 (RPC) 框架。它是云原生计算基金会 (CNCF) 的一部分,旨在实现微服务架构中的服务之间高效、稳健的通信。以下是 gRPC 的一些关键功能和概念:

1. 协议缓冲区(Protobuf):

  • gRPC 使用 Protocol Buffers 作为其接口定义语言 (IDL)。
  • Protobuf 是一种与语言无关、与平台无关的可扩展机制,用于序列化结构化数据,类似于 XML 或 JSON,但更小、更快、更简单。
  • .proto文件用于定义服务方法和消息格式。

2.基于HTTP/2:

  • gRPC 利用 HTTP/2 作为其传输协议,它允许对 HTTP/1.x 进行许多改进,例如通过单个 TCP 连接复用多个请求、二进制帧和高效的错误处理。

3、四种服务方式:

  • Unary RPC :客户端发送单个请求并获得单个响应。
  • Server Streaming RPC :客户端发送请求并获取响应流。
  • Client Streaming RPC :客户端将一系列消息流式传输到服务器,然后服务器发回单个响应。
  • Bidirectional Streaming RPC :客户端和服务器都独立地向对方发送消息流。

在这个实际的实现中,我们将看到一元实现它等于发送单个请求和响应。我们现在知道 gRPC 使用ProtocolBuffer来定义服务。首先让我们从定义接口定义开始,然后让我们看看项目结构和脚手架。下面的项目结构中的每个模块都是一个 Maven 模块。

模块 3(原型服务):

proto-service模块负责保存与 proto 文件相关的所有内容,并将它们编译成 gRPC 相关的存根和接口。

让我们在src/main/proto文件夹中创建一个Product.proto文件并复制以下内容。

/**
 * @author vaslabs(M K Pavan Kumar)
 * @medium (https://medium.com/@manthapavankumar11)
 */
syntax="proto3";

option java_multiple_files=true;

package com.vaslabs.proto;

message Product {
  int32 product_id=1;
  string name=2;
  string description=4;
  float price=3;
  int32 category_id=5;
}

message ProductList{
  repeated Product product=1;
}

message Category {
  int32 category_id=1;
  string name=2;
}

service ProductService {

  //unary - synchronous
  //request-response stype [not streaming]
  rpc getProductById(Product) returns(Product){}
  rpc getProductByCategoryId(Category) returns(ProductList){}
}

提供的 proto 文件采用 Protocol Buffers 版本 3 ( proto3 ) 的语法编写,是与产品和类别相关的 gRPC 服务的结构化定义。以下是其组成部分的简要说明:

文件头:

  • syntax="proto3"; :指定文件使用proto3语法,Protocol Buffers的最新版本。

选项:

  • option java_multiple_files=true; :指示 Protocol Buffers 编译器为每种消息类型生成单独的 Java 文件,而不是单个文件。

包裹声明:

  • package com.vaslabs.proto; :定义包名称,有助于防止不同项目之间的名称冲突。

消息定义:

  • message Product :定义Product消息,其中包含产品 ID、名称、描述、价格和类别 ID 字段。每个字段都有一个唯一的标签号(例如, product_id=1 )。
  • message ProductList :定义一个ProductList消息,该消息可以包含多个Product消息。 repeated表示它是Product对象的列表。
  • message Category :表示具有类别 ID 和名称的类别。

服务定义:

  • service ProductService :声明一个名为ProductService服务。
  • rpc getProductById(Product) returns(Product){} :定义一元(单个请求-响应)方法getProductById ,该方法将Product消息作为输入并返回Product消息。
  • rpc getProductByCategoryId(Category) returns(ProductList){} :另一个一元方法getProductByCategoryIdCategory消息作为输入并返回ProductList消息。

RPC 类型:

  • RPC 方法是一元的,这意味着它们遵循简单的请求-响应模式,无需流式传输。

让我们使用 maven 插件运行“mvncompile”来编译并获取 gRPC 风格生成的源代码以获取源代码。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.vaslabs</groupId>
        <artifactId>springboot-grpc-demo</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <artifactId>proto-service</artifactId>

    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-stub</artifactId>
            <version>1.56.1</version>
        </dependency>
        <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-protobuf</artifactId>
            <version>1.54.2</version>
        </dependency>
        <dependency>
            <groupId>javax.annotation</groupId>
            <artifactId>javax.annotation-api</artifactId>
            <version>1.3.2</version>
        </dependency>
    </dependencies>

    <build>
        <extensions>
            <extension>
                <groupId>kr.motd.maven</groupId>
                <artifactId>os-maven-plugin</artifactId>
                <version>1.6.1</version>
            </extension>
        </extensions>
        <plugins>
            <plugin>
                <groupId>org.xolstice.maven.plugins</groupId>
                <artifactId>protobuf-maven-plugin</artifactId>
                <version>0.6.1</version>
                <configuration>
                    <protocArtifact>
                        com.google.protobuf:protoc:3.17.3:exe:${os.detected.classifier}
                    </protocArtifact>
                    <pluginId>grpc-java</pluginId>
                    <pluginArtifact>
                        io.grpc:protoc-gen-grpc-java:1.53.0:exe:${os.detected.classifier}
                    </pluginArtifact>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                            <goal>compile-custom</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

模块 2(产品服务)

这是保存原型服务的实现的服务,接收原型请求并将原型响应发送给调用它的客户端。该服务由grpc-spring-boot-starter提供支持

将以下内容复制到该模块的 pom.xml 中。观察 pom 文件,我们已将proto-service模块添加为依赖项,因为我们将在此模块中实现 proto-services。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.vaslabs</groupId>
        <artifactId>springboot-grpc-demo</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <artifactId>product-service</artifactId>

    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.vaslabs</groupId>
            <artifactId>proto-service</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>net.devh</groupId>
            <artifactId>grpc-spring-boot-starter</artifactId>
            <version>2.14.0.RELEASE</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

让我们在src/main/java中创建一个ProductServiceImpl.java类。观察该类扩展了由protoc生成的基类ProductServiceGrpc.ProductServiceImplBase

package org.vaslabs;

import com.vaslabs.proto.Category;
import com.vaslabs.proto.Product;
import com.vaslabs.proto.ProductList;
import com.vaslabs.proto.ProductServiceGrpc;
import io.grpc.stub.StreamObserver;
import net.devh.boot.grpc.server.service.GrpcService;

import java.util.List;

@GrpcService
public class ProductServiceImpl extends ProductServiceGrpc.ProductServiceImplBase {
    @Override
    public void getProductById(Product request, StreamObserver<Product> responseObserver) {
        InMemoryData.getProducts()
                .stream()
                .filter(product -> product.getProductId()==request.getProductId())
                .findFirst()
                .ifPresent(responseObserver::onNext);
        responseObserver.onCompleted();
    }

    @Override
    public void getProductByCategoryId(Category request, StreamObserver<ProductList> responseObserver) {
        List<Product> products=InMemoryData.getProducts()
                .stream()
                .filter(product -> product.getCategoryId()==request.getCategoryId())
                .toList();

        ProductList productList=ProductList.newBuilder().addAllProduct(products).build();

        responseObserver.onNext(productList);
        responseObserver.onCompleted();
    }
}

使用主类,我们可以启动服务ProductServiceApplication.java ,如下所示。

package org.vaslabs;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ProductServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(ProductServiceApplication.class, args);
    }
}

如果要更改默认的 gRPC 服务端口,请在src/main/resources/application.yaml中保留以下内容。

grpc:
  server:
    port: 9090

模块 1(产品-客户端)

该模块是一个纯Spring Boot Web客户端,它将以Http形式侦听Web请求,并使用proto请求与产品服务通信并接收proto响应,然后转换为DTO并将其作为Http响应发送给调用客户端。

让我们将以下内容复制到pom.xml,该模块由grpc-client-spring-boot-starter提供支持,以发出启用 grpc 的请求和响应。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.vaslabs</groupId>
        <artifactId>springboot-grpc-demo</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <artifactId>product-client</artifactId>

    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.vaslabs</groupId>
            <artifactId>proto-service</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>net.devh</groupId>
            <artifactId>grpc-client-spring-boot-starter</artifactId>
            <version>2.14.0.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.30</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>3.1.2</version>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

让我们创建一个服务,它将使用生成的存根与 grpc 服务进行通信,如下所示。 src/main/java/org/vaslabs/service/ProductServiceGRPC.java

package org.vaslabs.service;

import com.vaslabs.proto.Category;
import com.vaslabs.proto.Product;
import com.vaslabs.proto.ProductList;
import com.vaslabs.proto.ProductServiceGrpc;
import net.devh.boot.grpc.client.inject.GrpcClient;
import org.springframework.stereotype.Service;

import java.util.List;

import static org.vaslabs.service.helpers.DtoMappingHelper.mapProductListToProductDTO;
import static org.vaslabs.service.helpers.DtoMappingHelper.mapProductToProductDTO;

@Service
public class ProductServiceRPC {

    @GrpcClient("grpc-product-service")
    ProductServiceGrpc.ProductServiceBlockingStub productServiceBlockingStub;

    public org.vaslabs.dto.Product getProductById(int id) {
        Product product=Product.newBuilder().setProductId(id).build();
        Product response=productServiceBlockingStub.getProductById(product);
        return mapProductToProductDTO(response);
    }

    public List<org.vaslabs.dto.Product> getProductByCategoryId(int id) {
        Category category=Category.newBuilder().setCategoryId(id).build();
        ProductList response=productServiceBlockingStub.getProductByCategoryId(category);
        return mapProductListToProductDTO(response);
    }
}

一旦收到原始响应,我们将使用以下辅助方法将其转换为 DTO。 **/helpers/DtoMappingHelper.java

package org.vaslabs.service.helpers;

import com.vaslabs.proto.ProductList;
import org.vaslabs.dto.Product;

import java.util.ArrayList;
import java.util.List;

public class DtoMappingHelper {

    public static List<org.vaslabs.dto.Product> mapProductListToProductDTO(ProductList productList) {
        List<Product> products=new ArrayList<>();
        productList.getProductList().forEach(product -> {
            Product product1=getProduct();
            product1.setId(product.getProductId());
            product1.setCategoryId(product.getCategoryId());
            product1.setName(product.getName());
            product1.setDescription(product.getDescription());
            product1.setPrice(product.getPrice());

            products.add(product1);
        });
        return products;
    }

    public static org.vaslabs.dto.Product mapProductToProductDTO(com.vaslabs.proto.Product product) {
        Product product1=getProduct();
        product1.setId(product.getProductId());
        product1.setCategoryId(product.getCategoryId());
        product1.setName(product.getName());
        product1.setDescription(product.getDescription());
        product1.setPrice(product.getPrice());
        return product1;
    }

    private static Product getProduct(){
        return new Product();
    }
}

**/dto/Product.java

package org.vaslabs.dto;

import lombok.Data;
import lombok.Getter;
import lombok.Setter;

@Data
@Setter
@Getter
public class Product {
    private int id;
    private int categoryId;
    private String name;
    private String description;
    private float price;
}

让我们创建一个名为**/ProductController.java的控制器,如下所示。

package org.vaslabs.controller;

import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.vaslabs.dto.Product;
import org.vaslabs.service.ProductServiceRPC;

import java.util.List;

@RestController
public class ProductController {

    private final ProductServiceRPC productServiceRPC;

    public ProductController(ProductServiceRPC productServiceRPC) {
        this.productServiceRPC=productServiceRPC;
    }

    @GetMapping("/product/{id}")
    public ResponseEntity<Product> getProductById(@PathVariable String id){
        return ResponseEntity.ok().body(productServiceRPC.getProductById(Integer.parseInt(id)));
    }

    @GetMapping(value="/product/category/{id}", produces=MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<List<Product>> getProductByCategoryId(@PathVariable String id){
        return ResponseEntity.ok().body(productServiceRPC.getProductByCategoryId(Integer.parseInt(id)));
    }
}

在此模块的资源文件夹中,请将以下内容添加到application.yaml文件中

grpc:
  client:
    grpc-product-service:
      address: static://localhost:9090
      negotiationType: plaintext

为此,存根将创建一个名为grpc-product-service通道来与服务进行通信。

执行:

分别运行产品服务和产品客户端,如果下面的所有内容应该是您应该看到的输出。

我使用 HTTPie 来测试服务。

总结

在微服务架构的背景下,采用 gRPC 和 Protobuf 进行服务间通信已被证明是提升效率、稳健性和跨语言互操作性的关键策略。gRPC 以其基于 HTTP/2 的通信机制和对多种编程语言的广泛支持,极大地促进了高性能和低延迟的服务交互,这与微服务架构的分布式特性完美契合。

Protobuf(Protocol Buffers)通过提供一种紧凑的二进制序列化格式,进一步强化了这一优势,确保服务之间交换的数据不仅体积小,而且序列化和反序列化的速度极快。这种高效的序列化机制使得数据传输更为迅速,同时也减少了网络带宽的占用,对于构建现代、高效且有效的微服务生态系统至关重要。

综上所述,gRPC 与 Protobuf 的结合,不仅为微服务架构提供了一种强大的通信工具,还为构建高性能、低延迟的分布式系统奠定了坚实的基础。这种组合无疑成为了推动微服务生态系统发展的强大动力。

来源:https://spring4all.com/forum-post/7462.html

随着.net core3.0的正式发布,gRPC服务被集成到了VS2019。本文主要演示如何对gRPC的服务进行认证授权。

分析

目前.net core使用最广的认证授权组件是基于OAuth2.0协议的IdentityServer4。而gRPC可以与ASP.NET Core Authentication一起使用来实现认证授权功能。本文将创建3个应用程序来完成gRPC的认证授权演示过程。

步骤

Ids4.Server

1.创建一个.net core的webapi
2.nuget引用最新的IdentityServer4的包
<PackageReference Include="IdentityServer4" Version="3.0.1" />

IdentityServer4相关配置,因为是演示所以很简单,生产场景大家根据实际情况配置。

namespace Ids4.Server
{
    public class Config
    {
        public static IEnumerable<IdentityResource> GetIdentityResources()
        {
            return new List<IdentityResource>
            {
                new IdentityResources.OpenId(),
                new IdentityResources.Profile(),
                new IdentityResources.Email(),
            };
        }
        public static IEnumerable<ApiResource> GetApis()
        {
            return new List<ApiResource>
            {
                new ApiResource("api", "Demo API")
                {
                    ApiSecrets={ new Secret("secret".Sha256()) }
                }
            };
        }
        public static IEnumerable<Client> GetClients()
        {
            return new List<Client>
                {
                    new Client
                    {
                        ClientId="client",
                        ClientSecrets={ new Secret("secret".Sha256()) },

                        AllowedGrantTypes=GrantTypes.ClientCredentials,
                        AllowedScopes={ "api" },
                    },
                };
        }
    }
}
4. startup.cs 注入服务
services.AddIdentityServer().AddInMemoryApiResources(Config.GetApis())
    .AddInMemoryIdentityResources(Config.GetIdentityResources())
    .AddInMemoryClients(Config.GetClients())
    .AddDeveloperSigningCredential(persistKey: false); 

5. startup.cs 配置http请求管道
app.UseIdentityServer();
6. 启动服务,使用PostMan进行调试,有返回结果表示服务创建成功
POST /connect/token HTTP/1.1
Host: localhost:5000
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&client_id=client1&client_secret=secret

{
    "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IlVyMmxuM2EwNGhWaGdDdWZTVTNtZVEiLCJ0eXAiOiJhdCtqd3QifQ.eyJuYmYiOjE1NzEzMDkwMTMsImV4cCI6MTU3MTMxMjYxMywiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo1MDAwIiwiYXVkIjoiYXBpIiwiY2xpZW50X2lkIjoiY2xpZW50Iiwic2NvcGUiOlsiYXBpIl19.X4pg9_FbPbWZl814XC0NYWTslfhMG4aXWEyXLrXhIojPJaL7Qvq9ieDF4S7x0psRcClwbwCg81hTrG3j2Cmcl0nzj_Ic7UY8MfN0dvAuy_fJdUf76TX0oOpir3SxgC8gnfaKyEoWmmbIyvwicWbKp9PP-EeTxG6-oMYn6PO22cwRVHDD28ZdEAq2DEkATOh9XPavoi9vGZhPQ1nviKL1K6tcYUGXSQbhWI9ISEqnTHqMX1xA_gcDIAplGvquXmtXdgyTsRoGolEtzDAYVH4sGUb1SpYx2nc8bgl6Qw27fhe0Uy9MR70kQMcEkCTdXLivjYjkuI9_quUyJHzdi5KgnQ",
    "expires_in": 3600,
    "token_type": "Bearer",
    "scope": "api"
}

本篇不对IdentityServer4做更多的讲解,大家可以参考官方文档了解更多。

Grpc.Server

1. 使用vs2019创建gRPC服务端。

2. 不用做任何更改,直接使用默认创建的gRPC服务

Grpc.Client

1. 创建一个控制台程序
2. 引入nuget安装包
<PackageReference Include="Google.Protobuf" Version="3.10.0" />
<PackageReference Include="Grpc.Net.Client" Version="2.23.2" />
<PackageReference Include="Grpc.Tools" Version="2.24.0">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

这3个核心包是客户端必备的,其中grpc.tools帮我们把proto文件转化成C#代码。

3. 创建Protos文件夹
4. 复制Grpc.Server项目Protos文件夹下的greet.proto文件到本项目的Protos文件夹
5. greet.proto文件右键设置gGRC Stub Classes为Client only。

也可以直接使用在项目文件里面代码设置如下:

<ItemGroup>
  <Protobuf Include="Protos\greet.proto" GrpcServices="Client" />
</ItemGroup>
6. gRPC客户端访问服务端代码
var channel=GrpcChannel.ForAddress("https://localhost:5001");
var client=new Greeter.GreeterClient(channel);
var response=client.SayHello(new HelloRequest { Name="World" });
Console.WriteLine(response.Message);

启动gRPC服务端,在启动gRPC客户端控制台打印hello word表示成功。

identityServer接入gRPC是非常容易,和传统webapi差不多。

改造Grpc.Server支持IdentityServer4

1. 引入nuget包
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="3.0.1" />
2. startup.cs 注入服务,和IdentityServer4一样。
services.AddGrpc(x=> x.EnableDetailedErrors=false);
services.AddAuthorization();
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
    .AddIdentityServerAuthentication(options=>
    {
        options.Authority="http://localhost:5000";
        options.RequireHttpsMetadata=false;
    });
3. startup.cs 配置http请求管道
if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints=>
{
    endpoints.MapGrpcService<GreeterService>();

    endpoints.MapGet("/", async context=>
    {
        await context.Response.WriteAsync("Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909");
    });
});
4. 对需要授权的服务打标签[Authorize],可以打在类上也可以打在方法上
[Authorize]
public class GreeterService : Greeter.GreeterBase
{
}

这个时候我们启动Grpc.Client访问Grpc.Server服务

发现报错401。说明此服务需要携带令牌才能访问。

改造Grpc.Client携带令牌访问

//获取token可以直接使用HttpClient来获取,这里使用IdentityModel来获取token
var httpClient=new HttpClient();
var disco=await httpClient.GetDiscoveryDocumentAsync("http://localhost:5000");
if (!disco.IsError)
{
    var token=await httpClient.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest()
    {
        Address=disco.TokenEndpoint,
        ClientId="client",
        ClientSecret="secret"
    });
    var tokenValue="Bearer " + token.AccessToken;
    var metadata=new Metadata
    {
        { "Authorization", tokenValue }
    };
    var callOptions=new CallOptions(metadata);
    var channel=GrpcChannel.ForAddress("https://localhost:5001");
    var client=new Greeter.GreeterClient(channel);
    var response=client.SayHello(new HelloRequest { Name="World" }, callOptions);
    Console.WriteLine(response.Message);
}

执行程序返回hello world表示成功。

传统调用webapi把token放到Header头的Authorization属性里面,grpc是放到Metadata里面,调用方法的时候传入CallOptions。使用上大同小异。

后记

目前gRPC各个语言的支持都已经很完善,因为跨语言,性能更高的特性非常适合做内网的通信。笔者也将继续对gRPC进行跟进,会尝试将部分的内部服务改造成gRPC,关于gRPC的相关问题也可以留言大家一起讨论。 源代码地址:https://github.com/longxianghui/grpc_ientityserver


原文地址:https://www.cnblogs.com/longxianghui/p/11719190.html

在移动端平台开发中,为了增加代码复用,降低开发成本,通常会需要采用跨平台的开发技术,花椒也不例外。本次新的单品开发,由于时间紧,人员有限,经过调研选型,最终确定了 flutter 方案(具体选型过程不在本文讨论之内)。

为了让客户端更专注业务实现,降低接口联调测试成本,我们选用了 gRPC 方案。gRPC是一个高性能、通用的开源 RPC 框架,由 Google 开发并基于 HTTP/2 协议标准而设计,基于 ProtoBuf(Protocol Buffers)序列化协议开发,且支持当前主流开发语言。gRPC通过定义一个服务并指定一个可以远程调用的带有参数和返回类型的的方法,使客户端可以直接调用不同机器上的服务应用的方法,就像是本地对象一样。在服务端,服务实现这个接口并且运行 gRPC 服务处理客户端调用。在客户端,有一个stub提供和服务端相同的方法。

gRPC

特点

  • 基于标准化的 IDL(ProtoBuf)来生成服务器端和客户端代码,支持多种主流开发语言。同时可以更好的支持团队与团队之间的接口设计,开发,测试,协作等。
  • 基于 HTTP/2 设计,支持双向流,多路复用,头部压缩。
  • 支持流式发送和响应,批量传输数据,提升性能。
  • ProtoBuf 序列化数据抓包、调试难度较大。我们使用服务端注入方式提供了用户或设备过滤,请求及返回值日志捕获,并开发对应后台模拟抓包展示。
  • 相比 JSON, 对前端不够友好。gRPC 生态 提供了 gateway 的方式为 gRPC 服务代理出 RESTful 接口。
  • ProtoBuf 提供了非常强的扩展性,可以为 protoc 开发定制插件,从而扩展 proto 文件的功能及描述性。

gRPC-Web

gRPC-Web 为前端浏览器提供了 Javascript 库用来访问 gRPC 服务,但是需要通过 Envoy 提供代理服务。相比 JSON 的方式对前端不够友好,同时也增加了服务端的部署成本。因此在这次项目中前端未使用 gRPC 服务,而是由 gRPC-Gateway 提供代理的 RESTful 接口。

gRPC-Gateway

grpc-gateway 是 protoc 的一个插件,它能读取 gRPC 的服务定义并生成反向代理服务器,将 RESTful 的 JSON 请求转换为 gRPC 的方式。这样无需太多工作即可实现一套基于 gRPC 服务的 RESTful 接口,方便前端使用调用接口,同时也方便开发过程中通过 Postman/Paw 之类的工具调试接口。

gateway -> gRPC 映射方式:

  • HTTP 源 IP 添加到 gRPC 的 X-Forwarded-For 请求头
  • HTTP 请求 Host 添加到 gRPC 的 X-Forwarded-Host 请求头
  • HTTP 请求头 Authorization 添加到 gRPC 的 authorization 请求头
  • HTTP 请求头带 Grpc-Metadata- 前缀的映射到 gRPC 的 metadata (key 名不带前缀)

例如,gRPC 接口要求的通用的 metadata 参数(如 platform, device_id 等)在 HTTP RESTful 的传递方式如下:

基础库

dart

为了便于客户端调用,连接复用及通用参数传递,我们封装了 dart 的基础库。

BaseClient 维护了针对 HOST 缓存的连接池,同时也提供了接口需要传递的 metadata 信息。

golang

golang 后端服务需要同时支持 gRPC 和 gateway 两种请求方式。为了简化部署和上线依赖,gateway 和 gRPC 的功能放在了一起,并通过拦截器注入对应的功能,主要包括 gRPC 统计,访问日志,接口鉴权,请求参数校验,gateway JSON 编码等。

  • 引用到的 package


  • 开发流程

为了提高开发效率,方便维护及模块复用,服务端按功能进行组件化开发。每个组件可以单独运行一个服务,也可以和其它组件共同组成一个服务。每个组件都需要实现 Component 接口:

对应组件开发完成后,需要开发对应的服务容器,步骤如下。

  • 初始化 base package
1 base.Init(context.TODO(), cfg, &global.Callback{
2 Authenticator: &auth.Callback{},
3 LogCapture: &log.Capture{},
4 })
  • 如需对外提供服务,需要提供端口及 TLS 证书


base.DefaultServer.AddPublicServer(rpcPort, gatewayPort, setting.TLSConfig)

  • 组件注册
1 base.DefaultServer.RegisterComponent(&user.Component{})
2 base.DefaultServer.RegisterComponent(&push.Component{})
3 ...
  • 监听服务


base.DefaultServer.Serve()

接口定义及实现

proto 规范

gRPC 基于标准化的 IDL(ProtoBuf)来生成服务器端和客户端代码,我们决定将所有的接口描述及文档说明都放到 proto 文件中,便于查看及修改。对 proto 的接口描述及注释的规范如下:

代码生成

golang

 1 gengo:
 2 @protoc -Iproto \
 3 -I${GOPATH}/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
 4 -I${GOPATH}/src/github.com/lnnujxxy/protoc-gen-validate \
 5 -I${GOPATH}/src/github.com/youlu-cn/grpc-gen/protoc-gen-auth \
 6 --go_out=plugins=grpc:go/pb \
 7 --grpc-gateway_out=logtostderr=true:go/pb \
 8 --validate_out="lang=go:go/pb" \
 9 --auth_out="lang=go:go/pb" \
10 proto/*.proto
  • SDK 引入


golang 使用 go mod 的方式直接引入 pb 生成的 .go 文件

dart

  • SDK 引入


修改 pubspec.yaml,执行 flutter packages get 或 flutter packages upgrade

 1 dependencies:
 2 flutter:
 3 sdk: flutter
 4
 5 protobuf: ^0.13.4
 6 grpc: ^1.0.1
 7 user:
 8 git:
 9 url: git@github.com:project/repo.git
10 path: dart/user
  • 已知问题:
  1. dart 在对 protobuf 生成的类型做 json 编码时,json 中的 key 是字段号而非名字,导致无法与其它语言交互。ISSUE (https://github.com/dart-lang/protobuf/issues/220)


文档生成

gRPC gateway 提供了通过 proto 文件生成 swagger API 文档,缺点是只支持 gateway 的 RESTful 接口,并且默认的展示方式有点不符合我们的常规文档使用方式。

我们基于 protoc 插件开发了 protoc-gen-markdown 工具,可以由 proto 文件生成 markdown 文档,提供 gRPC 接口描述,以及 RESTful 接口描述及 JSON 示例,提供全文目录,支持锚点导航等。生成方式如下:

1gendoc:
2 @protoc -Iproto \
3 -I${GOPATH}/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
4 -I${GOPATH}/src/github.com/lnnujxxy/protoc-gen-validate \
5 -I${GOPATH}/src/github.com/youlu-cn/grpc-gen/protoc-gen-auth \
6 --markdown_out=":doc" \
7 proto/*.proto

文档会在对应路径生成接口列表 README.md,以及每个 protobuf 对应的接口文档。

调试

传统的 RESTful 接口在调试及问题排查时,可以通过抓包或者 MitM(中间人攻击)的方式,配置也比较容易。而 gRPC 因为使用了 HTTP2 及 protobuf 二进制流,抓包及数据流反解难度相对较高,调试及问题排查时会比较复杂。为了解决这个问题,我们通过服务端注入的方式,配合查询后台过滤对应的请求日志,从而实现如下类似抓包的效果。



后续计划

  1. gRPC Streaming
  2. 框架层集成全链路 Trace 支持
  3. 迭代优化框架,提供对应脚手架,简化新组件/服务创建及开发流程

Go语言中文网,致力于每日分享编码、开源等知识,欢迎关注我,会有意想不到的收获!

本文由花椒服务端团队原创授权发布