什么是gRPC以及如何实现一个demo

在当今世界,微服务架构已成为构建可扩展且可靠的应用程序的流行方法。使用微服务,应用程序被分解成更小的、独立的服务,可以单独开发和部署。这种方法需要一个允许这些服务相互对话的通信协议。传统上,REST API 已用于实现此目的,但现在出现了一个名为 gRPC 的新手,它的工作方式类似于普通的 RPC 调用,允许您直接调用在其他机器上实现的方法。

gRPC 简介

gRPC 是 Google 开发的高性能、开源、通用的 RPC(Remote Procedure Call)框架。它建立在较新的 HTTP/2 协议和 Protocol Buffers 之上,使其高效、轻量且易于使用。与传统的 REST API 相比,这也意味着服务之间的通信更快、更高效。

gRPC 还使用 Protocol Buffers(我们在这篇博文中也将其称为 protobufs)——一种与语言无关的数据序列化格式——用于数据序列化和反序列化。数据格式是二进制格式而不是文本格式(在使用 JSON 的传统 REST API 中也是如此),因此占用的内存空间更小,甚至适用于嵌入式系统等低内存设备。
类似地,gRPC 框架附带了协议缓冲区编译器,它可以为您生成数据类(从定义的原型文件,我们将在这篇博文中介绍)减少需要编写的样板代码的数量。

与所有其他技术一样,gRPC 也有不少缺点。对于初学者来说,与传统的 JSON API 相比,学习曲线相当陡峭,新开发人员可能需要一些时间才能完全熟悉。gRPC 还使用更现代的 HTTP/2 协议,该协议对浏览器的支持有限,因此增加了设置开销,并且需要使用代理服务器来连接 HTTP/2 和 HTTP/1 协议。因此,您选择在您的设置中使用 gRPC 归结为您的特定用例。一个好的起点是内部微服务之间的通信,您不需要构建和支持客户端库。

使用Node 和 TypeScript实现一个demo

我们将做一个简单的服务器设置,允许用户登录并对他们的联系人执行 CRUD 操作。

gRPC 和 REST API 之间的一个主要区别是 gRPC 定义了自己的状态代码标准,这与我们大多数人熟悉的传统 HTTP 响应代码不同。这是所有这些状态代码的完整列表。因此,在我们的每个 protofile 定义中,在响应消息类型中,我们将包含一个名为的字段,status我们将使用它来存储标准 HTTP 状态代码。

先决条件

要从原型定义生成数据类,您需要安装 Protocol Buffer Compiler。这是一个方便的链接,告诉您如何去做。如果出现“找不到或不可执行的 protoc-gen-ts 程序”错误,则需要降级 protobuf。这是关于如何执行此操作的另一个方便的链接。

您还需要安装节点。这是下载链接

项目设置

我们的文件夹结构如下:
root
|*src(存放我们的项目源文件)
|__
 protos(我们将在其中定义我们的原型文件,protoc 将在其中生成相关的 ts 文件)
|__ *services*(我们将在其中定义服务器
|__
 types(我们在其中为不同的数据结构定义必要的接口类型)| __ *utils**(我们将在其中定义可在整个项目中全局重用的实用程序函数)
| _
(我们定义全局常量的地方。即服务器主机等)
|_ index.ts(我们应用程序的入口点。处理初始化函数,如服务器启动和绑定到定义的端口)

随意在您选择的位置(使用打字稿支持)创建一个模仿此文件夹结构的 nodejs 项目。
然后,我们将在您刚刚创建的项目的根目录下创建一个名为(无扩展名)的文件MakeFile,然后复制粘贴以下内容:

``` PROTO_DIR=src/protos

prototypes: grpctoolsnodeprotoc \ --jsout=importstyle=commonjs,binary:${PROTODIR} \ --grpcout=grpcjs:${PROTODIR} \ --plugin=protoc-gen-grpc=which grpc_tools_node_protoc_plugin \ -I ${PROTODIR} \ ${PROTODIR}/*.proto

protoweb: protoc \ --plugin=protoc-gen-ts=./nodemodules/.bin/protoc-gen-ts \ --tsout=grpcjs:${PROTODIR} \ -I ${PROTODIR} \ ${PROTO_DIR}/*.proto

proto: prototypes protoweb ```

这个文件的作用基本上是存放我们与 protoc 编译器相关的 bash 命令的集合,只要我们想从我们的文件make proto生成服务实现,我们就可以通过在终端上运行来方便地执行这些命令。.proto

定义Protofile

第一步是定义协议缓冲区 (.proto) 文件,这些文件定义将用于服务间通信的服务和消息。在此示例中,我们有两个 .proto 文件,一个用于身份验证,另一个用于联系人。
我们将使用 JWT 流程的简单实现来进行用户身份验证。如果您不熟悉 JWT,我建议您在探索代码库之前先阅读一下,因为您可能会迷失在身份验证技术细节中,这不是本文试图探索的内容。

我们的身份验证服务 (at src/protos/auth.proto) 定义如下:

授权协议:

``` syntax = "proto3";

package auth;

service AuthService { rpc Login (LoginRequest) returns (LoginResponse); rpc UserMe (UserRequest) returns (UserResponse); rpc RefreshAccessToken (RefreshAccessTokenRequest) returns (RefreshAccessTokenResponse); }

message UserRequest{}

message UserResponse { int32 status = 1; string error = 2; string username = 4; string id = 5; string password = 6; }

message LoginRequest{ string username = 1; string password = 2; }

message LoginResponse{ int32 status = 1; string error = 2; string jwtToken = 3; string refreshToken = 4; }

message RefreshAccessTokenRequest{ string refreshToken = 1; }

message RefreshAccessTokenResponse{ int32 status = 1; string error = 2; string accessToken = 3; } ```

查看上面的代码片段,定义原型文件非常简单。您首先定义message代表数据结构的 a 。在message定义中,您定义各种字段及其各自的数据类型(对于那些来自动态类型语言的字段,数据类型基本上是一种数据结构。即数组是一种数据类型。字典也是一种数据类型。这也适用于整数、浮点数和字符串,它们也是数据类型)。

在本例中,我们定义了一条AuthService具有三种方法的消息:

  1. Login- 将 ausernamepassword作为输入并使用访问令牌和刷新令牌进行响应。
  2. UserMe- 不接受任何输入(仅访问令牌作为元数据)并使用与登录用户相关的信息进行响应。
  3. RefreshAccessToken- 将刷新令牌作为输入(在登录期间生成)并使用新的访问令牌进行响应。

我们的联系人服务协议文件也遵循类似的模式(在src/protos/contacts.proto)。请随意在示例存储库中探索这一点。

最后,我们make proto从项目的根开始运行,以生成带有必要样板代码的打字稿数据类,我们将在执行服务器实现时与之交互。

服务器实现

在这里,我们将根据我们之前在原型文件中定义的每个方法消息来定义逻辑。我们将从我们的AuthService. 请注意,我将在这里互换使用“方法”和“函数”,但它们都指的是打字稿函数。此外,请随意从存储库
下载文件并运行以加载我们将在此实现中使用的包。package.json``npm install

src/services/authService.ts当我试图解释这个文件的结构,以及我们如何完成实现时,我们将遍历服务实现的不同部分(在)。

正如我相信每种编程语言的规范一样,我们从导入必要的文件开始,这些文件将帮助我们连接我们的服务。

import {sendUnaryData, ServerUnaryCall} from "@grpc/grpc-js"; import {generateAccessToken, generateRefreshToken, getLoggedInUser, refreshToken} from "../utils/auth"; import {User} from "../types/user"; import { LoginRequest, LoginResponse, RefreshAccessTokenRequest, RefreshAccessTokenResponse, UserRequest, UserResponse } from "../protos/auth_pb";

在这里,我们导入定义了@grpc/grpc-jsgRPC 相关核心类的包中定义的核心类。在我们的例子中,我们将只处理一元服务器调用(gRPC 还支持流式调用,我们将在以后的文章中介绍)。

auth.proto然后,我们导入我们在上面(在我们的)中声明的由命令生成的原型文件的打字稿定义make proto

然后我们定义我们将在登录期间使用的模拟用户数据,因为我们不会处理任何用户注册逻辑。

// Mock user data for authentication const USERS = new Map<string, User>(); USERS.set( "admin", { id: 1, username: "admin", password: "admin", } ) USERS.set( "staff", { id: 2, username: "staff", password: "staff", } )

所有方法实现都将遵循类似的模式。它们都有 2 个参数:一个calltype 的参数ServerUnaryCall和一个callbacktype 的参数sendUnaryData。两种类型都采用通用参数。
我们ServerUnaryCall采用 2 个通用参数:第一个用于请求定义,另一个用于响应定义。因此,我将只探索方法userMe来讨论我们如何实现在我们的原型文件中定义的各种方法。
同样,您可以随意浏览示例存储库中的整个文件。

export const userMe = (call: ServerUnaryCall<UserRequest, UserResponse>, callback: sendUnaryData<UserResponse>) => { const response = new UserResponse(); const user = getLoggedInUser(); response.setId(`${user.id}`); response.setStatus(200); response.setError(""); response.setUsername(user.username); response.setPassword(user.password); callback(null, response) }

在进行方法实现时,我们的方法名称应该与我们在 protofiles 中定义的方法名称相对应。这不是一个硬约束,但通常更好,因为乍一看这里的哪个方法对应于定义的协议文件中的哪个方法。在上面的例子中,userMe对应于协议文件UserMe中定义的auth.proto

我们方法中的参数call采用ServerUnaryCallwithUserRequestUserResponse作为通用参数。和类型是从中导入的,是我们UserRequest运行时由 protoc 编译器生成的文件的一部分。这些类定义直接对应于您的协议文件消息定义,一直到数据类型。protoc 编译器在生成源文件时还包括 setter 和 getter 等辅助方法,使您可以方便地操作值。UserResponse``src/protos/auth_pb.ts``make proto

callback参数采用sendUnaryData只有一个泛型参数的类型。通用参数表示我们将返回的响应类型,在本例中为UserResponse。该callback函数还采用一个初始参数(我们已将其传递null为),该参数表示类型错误(如果有)ServerErrorResponse。您可以使用此参数抛出 gRPC 特定错误,即当用户未通过身份验证时。

在方法定义中,我们声明一个response变量并用UserResponse类实例化它。然后我们处理必要的逻辑并相应地设置状态值。我们status在协议文件中定义了变量来存储标准的 HTTP 状态代码,即 200 OK,因为 gRPC 定义了自己的标准状态代码集。如果我们想获取传入请求中附加的任何值,我们也可以通过调用call.request.

我们最后用我们的响应对象和错误(如果有的话)调用回调函数。

这几乎就是在 gRPC 中定义和实现服务器逻辑所需的全部内容。

服务器设置

最后,我们需要启动我们的服务器并将我们的服务实现附加到这个服务器实例。index.ts这将在我们项目根目录的文件中完成。在这里,我们还将介绍一个概念interceptors,它允许我们在将请求和响应传递给我们的服务或将响应返回给客户端之前拦截我们的请求和响应并处理逻辑。在我们的例子中,我们将使用拦截器来检查客户端是否已经通过我们的后端进行身份验证,如果没有并且客户端正在尝试调用需要身份验证的方法,则抛出错误。我们将使用包中的拦截器实现grpcjs-interceptors

``` const interceptors = require('grpcjs-interceptors');

const server = interceptors.serverProxy(new Server());

server.addService(ContactServiceService, { addContact, getContacts, updateContact, deleteContact });

server.addService(AuthServiceService, { login, userMe, refreshAccessToken }); ```

在此代码片段中,我们通过grpcjs-interceptors包实例化我们的服务器,以便我们可以将全局拦截器附加到我们的服务器。然后,我们将刚刚实现的服务添加到我们的服务器实例中,在本例中为 auth 和 contacts 服务。

``` const openEndpoints = [ '/auth.AuthService/Login', '/auth.AuthService/RefreshAccessToken' ] const checkAuthorizationToken = async function (ctx, next, callback) { if (!openEndpoints.includes(ctx.service.path)) { // check if user is authorized to access this route. const metadata = ctx.call.metadata; const authToken = metadata.get("authorization").toString(); const userIsAuthenticated = await isAuthenticated(authToken) if (!userIsAuthenticated) { callback(new Error("Unauthorized.")) return; } } await next()

// do something before response goes back to client } ```

然后我们定义一个变量openEndpoints来存储不需要身份验证的方法定义。此处定义的字符串遵循特定的语法。您从您的原型文件中定义的包名称开始auth,即,然后是正斜杠(/),后跟您的原型文件中定义的服务名称,即,AuthService然后是正斜杠(/),最后是方法名称,同样是您的原型文件中定义的,即Login. 此结构由 gRPC 标准定义,您可以在 console.log 请求的元数据时看到它,在本例中为:console.log(ctx.service.path)
之后,我们定义我们的拦截器函数,该函数将处理逻辑以确保经过身份验证的用户只能访问封闭的端点。在这里,我们定义了一个Error在用户未通过身份验证的情况下将使用 gRPC 特定状态代码触发的对象。然后我们取消该请求并以该错误响应。
如果你想在请求被处理后执行一个功能,你应该在块之后处理逻辑await next()

``` server.use(checkAuthorizationToken);

server.bindAsync(host, ServerCredentials.createInsecure(), (err, port) => { if (err) { console.log(err) return; } server.start(); console.log(listening on ${host}) }); ```

最后,我们告诉我们的服务器使用我们刚刚定义的拦截器。您可以使用该函数将多个拦截器附加到您的服务器server.use(interceptor)
然后我们将我们的服务器绑定到 中定义的主机和端口src/values/globals.ts

结论

gRPC 是一种功能强大且高效的通信协议,非常适合现代微服务架构。它具有多项优势,包括高性能、语言独立性、互操作性、代码生成和流式传输。官方支持 Node.js 和 TypeScript,在现代 Web 应用程序中很容易使用。虽然拦截器的使用乍看之下很复杂,但它是拦截和修改请求和响应的强大工具,可以用来实现复杂的认证和授权逻辑。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: 您可以参考以下示例代码来实现:import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import io.grpc.examples.helloworld.GreeterGrpc; import io.grpc.examples.helloworld.HelloReply; import io.grpc.examples.helloworld.HelloRequest;public class GrpcDemo { public static void main(String[] args) { ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 50051) .usePlaintext() .build(); // 创建一个gRPC客户端 GreeterGrpc.GreeterBlockingStub stub = GreeterGrpc.newBlockingStub(channel); // 创建一个HelloRequest对象 HelloRequest request = HelloRequest.newBuilder() .setName("grpc") .build(); // 发起调用 HelloReply response = stub.sayHello(request); System.out.println(response.getMessage()); } } ### 回答2: 下面是一个使用Java编写的示例代码,实现一个Java服务利用gRPC调用另一个Java服务的演示。 在这个示例中,我们假设存在两个Java服务,一个gRPC服务器(Server),另一个gRPC客户端(Client)。我们将使用protobuf生成的代码来定义gRPC服务。 首先,需要创建一个proto文件来定义我们的服务。 ```protobuf syntax = "proto3"; option java_multiple_files = true; option java_package = "com.example.grpcdemo"; option java_outer_classname = "GreeterProto"; package greeter; service Greeter { rpc SayHello(HelloRequest) returns (HelloResponse); } message HelloRequest { string name = 1; } message HelloResponse { string message = 1; } ``` 然后,我们使用protoc编译器生成Java代码: ``` $ protoc --java_out=./src/main/java ./greeter.proto ``` Server端的示例代码如下: ```java import io.grpc.Server; import io.grpc.ServerBuilder; import io.grpc.stub.StreamObserver; import com.example.grpcdemo.GreeterProto.Greeter; import com.example.grpcdemo.GreeterProto.HelloRequest; import com.example.grpcdemo.GreeterProto.HelloResponse; import java.io.IOException; public class ServerDemo { private Server server; public void start() throws IOException { int port = 50051; server = ServerBuilder.forPort(port) .addService(new GreeterImpl()) .build() .start(); System.out.println("Server started on port " + port); Runtime.getRuntime().addShutdownHook(new Thread(() -> { System.out.println("Shutting down server..."); ServerDemo.this.stop(); })); } public void stop() { if (server != null) { server.shutdown(); } } private static class GreeterImpl extends Greeter { @Override public void sayHello(HelloRequest request, StreamObserver<HelloResponse> responseObserver) { String name = request.getName(); String message = "Hello, " + name + "!"; HelloResponse response = HelloResponse.newBuilder().setMessage(message).build(); responseObserver.onNext(response); responseObserver.onCompleted(); } } public static void main(String[] args) throws IOException { ServerDemo server = new ServerDemo(); server.start(); } } ``` Client端的示例代码如下: ```java import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import io.grpc.stub.StreamObserver; import com.example.grpcdemo.GreeterProto.Greeter; import com.example.grpcdemo.GreeterProto.HelloRequest; import com.example.grpcdemo.GreeterProto.HelloResponse; public class ClientDemo { private final ManagedChannel channel; private final Greeter blockingStub; public ClientDemo(String host, int port) { channel = ManagedChannelBuilder.forAddress(host, port) .usePlaintext() .build(); blockingStub = Greeter.newBlockingStub(channel); } public void sayHello(String name) { HelloRequest request = HelloRequest.newBuilder().setName(name).build(); HelloResponse response = blockingStub.sayHello(request); System.out.println("Greeting: " + response.getMessage()); } public void shutdown() throws InterruptedException { channel.shutdown().awaitTermination(5, TimeUnit.SECONDS); } public static void main(String[] args) throws InterruptedException { ClientDemo client = new ClientDemo("localhost", 50051); client.sayHello("World"); client.shutdown(); } } ``` 以上是一个使用Java编写的示例,展示了一个Java服务如何利用gRPC调用另一个Java服务。在这个示例中,我们定义了一个简单的gRPC服务,然后实现一个gRPC服务器和一个gRPC客户端。服务器通过GreeterImpl类来处理客户端的请求,并返回一个简单的问候消息。客户端通过调用服务器的方法来向服务器发送请求,并输出服务器返回的响应消息。 希望这个示例能够帮助到您理解如何使用Java编写一个使用gRPC调用另一个Java服务的demo。 ### 回答3: 下面是一个使用gRPC调用另一个Java服务的示例: 首先,我们需要定义一个.proto文件,来定义RPC服务和消息格式。假设我们有一个名为"UserService"的服务,它有一个"getUser"方法,用于获取用户信息。我们需要在.proto文件中定义这些内容: ```protobuf syntax = "proto3"; package com.example; service UserService { rpc getUser (UserRequest) returns (UserResponse) {} } message UserRequest { string userId = 1; } message UserResponse { string userName = 1; } ``` 然后,我们使用gRPC的插件生成Java代码。可以使用以下命令生成代码: ```shell $ protoc -I=/path/to/proto/files --java_out=/path/to/output/directory /path/to/proto/files/user_service.proto ``` 生成的代码将包括UserServiceGrpc类和UserRequest/UserResponse类。 接下来,我们创建一个Java服务,并实现UserService中的getUser方法。例如,我们可以创建一个名为"UserServiceImpl"的类: ```java package com.example; import io.grpc.stub.StreamObserver; public class UserServiceImpl extends UserServiceGrpc.UserServiceImplBase { @Override public void getUser(UserRequest request, StreamObserver<UserResponse> responseObserver) { // 在这里编写获取用户信息的逻辑 String userId = request.getUserId(); // 假设这里我们简单返回用户名为userId的用户 UserResponse response = UserResponse.newBuilder() .setUserName(userId) .build(); responseObserver.onNext(response); responseObserver.onCompleted(); } } ``` 最后,我们创建一个用于启动gRPC服务的类,它将监听指定的端口并提供UserService。例如,我们创建一个名为"Server"的类: ```java package com.example; import io.grpc.Server; import io.grpc.ServerBuilder; import java.io.IOException; public class Server { public static void main(String[] args) throws IOException, InterruptedException { int port = 50051; Server server = ServerBuilder.forPort(port) .addService(new UserServiceImpl()) .build(); server.start(); System.out.println("Server started on port " + port); server.awaitTermination(); } } ``` 现在,我们可以编译并运行这两个服务。首先运行Server类启动gRPC服务,然后在另一个Java服务中,我们可以使用gRPC的Stub来调用UserService中的getUser方法: ```java package com.example; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; public class Client { public static void main(String[] args) { String host = "localhost"; int port = 50051; ManagedChannel channel = ManagedChannelBuilder.forAddress(host, port) .usePlaintext() .build(); UserServiceGrpc.UserServiceBlockingStub blockingStub = UserServiceGrpc.newBlockingStub(channel); UserRequest request = UserRequest.newBuilder() .setUserId("123") .build(); UserResponse response = blockingStub.getUser(request); System.out.println("UserName: " + response.getUserName()); channel.shutdown(); } } ``` 这样,我们就完成了一个简单的Java服务利用gRPC调用另一个Java服务的示例。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

youtian.L

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值