创建带有前端” Vue gRPC-Web”和服务器” Python gRPC”配置的计时器应用程序。


概述

当我寻找一种比REST API更好的API通信方法时,我发现了一种叫做gRPC的东西。它看起来不错,因此在制作一个简单的应用程序时,请记下其使用方式。

首先得出结论,使用Python创建服务器可能有点困难。 ..是。
这是因为我期望的消息完成不起作用。

什么是gRPC

gRPC是RPC的实现之一。

什么是RPC?它是克服REST API弱点的通信标准之一。
它被公认为消除了REST痛苦的备选方法之一。

与REST API的最大区别是必须牢固确定参数和返回值的数据类型。
让我感到高兴的是,当编辑或使用API??参数和返回值时,完成工作(应该)有效。
如果您决定API的结构,则会自动生成数据类型。
因此,在查看规范时,我不必担心该项目是字符类型还是数字类型这一事实,并且我认为它更像是实现=规范。

另外,非常希望能够处理与服务器的双向通信(推送类型)。
您无需再进行投票!

正如RPC(远程过程调用)的名称所暗示的那样,感觉就像可以调用函数而不是REST API一样使用它。

试试看

做什么

让我们做个计时器。
与前端Vue和服务器Python配置进行GRPC通信。
它是可以在没有单独的服务器的情况下创建的内容,但是无论如何,我都会强制使用gRPC。

那是示例程序。

规格

  • 服务器具有计时器功能。
  • 计时器启动命令通过gRPC从前台发送。
  • 服务器每秒通过gRPC将剩余时间发送给前台。
  • 前台在屏幕上显示从服务器发送的剩余时间。

我正在将自己制作的内容上传到GitHub。

尝试过的环境(前提)

  • Windows 10
  • Python 3.6
  • Node.js 12.5.0
  • npm 6.9.0
  • 纱线1.9.4
  • 适用于Windows 2.0.0.3的Docker(特使)

(题外话)我第一次使用Docker,但是我很方便死了。

环境建设

我将放入必要的工具。

安装Vue

正面由Vue制成,因此请安装Vue工具。

1
npm install -g @vue/cli

我在这里指的是。

协议缓冲区编译器准备

gRPC使用一种称为协议缓冲区的接口定义语言来描述API规范。
大致来说,协议缓冲区是一种语言,它允许您通过仅提取SQL的CREATE TABLE语法来定义函数接口。
安装特定于语言的编译器,以将用协议缓冲区编写的数据定义和函数定义编译为每种语言。
这次使用javascript和Python。

安装JavaScript编译器(gRPC-web)

手动插入。

下载编译器本身和javascript插件,并将路径传递到其中包含的可执行文件(protoc.exeprotoc-gen-grpc-web.exe)。

要检查它是否已安装,

1
2
protoc --version
libprotoc 3.8.0

如果变为

,则暂时可以。
尽管它是用于javascript的,但您也可以吐出Typescript定义文件。万岁。

此处的过程基于此处的官方教程。

安装适用于Python的编译器

1
pip install grpcio-tools

要检查它是否已安装,

1
2
python -m grpc_tools.protoc --version
libprotoc 3.8.0

如果它变成

,就可以了。

这一点令人怀疑。

为什么为Web和python安装了不同的编译器? ..
就个人而言,我希望python的插件是类似于protocol-gen-python.exe的插件,并且编译器本身必须相同。
显然,即使使用pip进行安装,protoc编译器的主体(dll版本)也需要单独下载,因此我想知道客户端和服务器之间的版本是否不同。

API实施

我们将实现协议缓冲区。

timer.proto

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
syntax = "proto3";

package timer_with_grpc;

service Timer {
  rpc StartTimer(StartRequest) returns (stream TimerState);  // タイマー開始
  rpc StopTimer(Empty) returns (TimerState); // タイマー停止
}

message StartRequest {
  int32 time = 1;
}

message TimerState {
  bool isRunning = 1;
  int32 leftTime = 2;
}

// 引数空ができないようなので、空用の定義をしておく
message Empty {
}

简单。
基本定义是重写示例,这似乎很有可能。

API接口在

service中定义,参数和返回值的数据结构在message中定义。
此处定义了两个API,StartTimerStopTimer
似乎有一个不需要参数的API,但是

1
rpc StopTimer() returns (TimerState);

当我尝试

时,发生了错误,因此似乎有一些参数。
这次我定义了一个名为Empty的空数据类型。
我不知道这是个好方法。

您还可以通过在返回中添加stream将数据从服务器推送到客户端。我想这样做。

编译协议缓冲区

客户端代码生成(Javascript Typescript定义文件)

1
protoc -I=. timer.proto --js_out=import_style=commonjs:. --grpc-web_out=import_style=commonjs+dts,mode=grpcwebtext:.

timer_pb.js
timer_pb.d.ts
timer_grpc_web_pb.js
timer_grpc_web_pb.d.ts

输出了四个名为

的文件。
我很高兴它甚至可以根据选项正确输出Typescript定义文件。 (添加dts)
我无法正确阅读它,但可以确认timer_grpc_web_pb.d.ts似乎是API客户端接口。

服务器代码生成(Python)

1
python -m grpc_tools.protoc -I. timer.proto --python_out=. --grpc_python_out=.

输出两个文件

timer_pb2.pytimer_pb2_grpc.py
由于timer_pb2_grpc.py具有API实现的模板,因此可以继承它并实现该功能。

服务器端API实施

TimerApi.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import timer_pb2
import timer_pb2_grpc

from concurrent import futures
import time
import grpc


# APIのロジック
class TimerServicer(timer_pb2_grpc.TimerServicer):
    leftTime = 0
    isRunning = False

    def StartTimer(self, request, context):
        self.leftTime = request.time
        self.isRunning = True

        while self.leftTime > 0:
            if self.isRunning: # 途中でStopTimerされてないかチェック
                yield self.makeTimerState()
                time.sleep(1)
                self.leftTime -= 1
            else:
                return

        self.isRunning = False
        yield self.makeTimerState()

    def StopTimer(self, request, context):
        self.isRunning = False
        return self.makeTimerState()

    def makeTimerState(self):
        return timer_pb2.TimerState(
            isRunning=self.isRunning,
            leftTime=self.leftTime
        )


# サーバーの実行
def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    timer_pb2_grpc.add_TimerServicer_to_server(TimerServicer(), server)
    server.add_insecure_port('0.0.0.0:8082')
    server.start()
    print("Server Start!!")
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        server.stop(0)


if __name__ == '__main__':
    serve()

TimerServicer继承协议编译器输出的API接口
这是添加逻辑的部分。

StartTimer倒计时作为参数接收的秒数。
用StopTimer停止。

客户端实施

在Vue中创建项目

首先,在Vue中创建一个名为client的项目。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ vue create client


Vue CLI v3.8.4
┌───────────────────────────┐
│  Update available: 3.9.2  │
└───────────────────────────┘
? Please pick a preset: Manually select features
? Check the features needed for your project: TS, CSS Pre-processors
? Use class-style component syntax? Yes
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? No    
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Sass/SCSS (with dart-sass)
? Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? No

除了使用Typescript和scss而不是Babel之外,我默认情况下使用它。

尝试编译。

现在您已经为客户准备了一个项目,让我们对其进行编译。
编译命令进入client目录,然后进入

1
yarn serve

那时,我编译了协议缓冲区
timer_pb.js
timer_pb.d.ts
timer_grpc_web_pb.js
timer_grpc_web_pb.d.ts
当我尝试对其进行编译时(在client / src下为4个文件),
我收到以下错误。

1
2
3
4
5
6
7
1:23 Cannot find module 'google-protobuf'.
  > 1 | import * as jspb from "google-protobuf"
      |                       ^
<<中略>>
10:26 Cannot find module 'grpc-web'.
  > 10 | import * as grpcWeb from 'grpc-web';
       |                          ^

我很生气,因为我没有

google-protobufgrpc-web,所以我会乖乖地安装它。

1
yarn add google-protobuf @types/google-protobuf grpc-web

当我再次编译它时,它过去了。

实施

应用程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<template>
    <div id="app">
        <p>
           - TIMER -
        </p>
        <br>
        <p>
            {{isTimerRunning ? "実行中" : "停止中"}}
        </p>
        <p class="time">
            {{leftTime}}
        </p>

        <button @click="startTimer">start</button>
        <button @click="stopTimer">stop</button>
    </div>
</template>

<script lang="ts">
    import {Component, Vue} from 'vue-property-decorator';
    import {TimerClient} from "./timer_grpc_web_pb";
    import {Empty, StartRequest, TimerState} from "./timer_pb";
    import {ClientReadableStream} from "grpc-web";

    @Component({})
    export default class App extends Vue {
        private timerClient: TimerClient;
        private isTimerRunning: boolean = false;
        private leftTime: number = 0;

        constructor() {
            super();
            this.timerClient = new TimerClient('http://' + window.location.hostname + ':8081', null, null);
        }

        private startTimer(): void {
            const request = new StartRequest();
            request.setTime(10);  // 10秒をセット
            const stream: ClientReadableStream<TimerState> = this.timerClient.startTimer(request, {});
            stream.on('data', (response: TimerState) => {
                this.isTimerRunning = response.getIsrunning();
                this.leftTime = response.getLefttime();
            });
        }

        private stopTimer(): void {
            this.timerClient.stopTimer(new Empty(), {}, (err, response: TimerState) => {
                this.isTimerRunning = response.getIsrunning();
                this.leftTime = response.getLefttime();
            });
        }

    }
</script>

<style lang="scss">
    #app {
        font-family: 'Avenir', Helvetica, Arial, sans-serif;
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
        text-align: center;
        color: #2c3e50;
        margin-top: 60px;
    }
    .time{
        margin:0 0 40px 0;
        font-size:148px;
        color:#999;
    }
</style>

Typescript端,使用构造函数实例化API客户端,并使用开始按钮或停止按钮调用相应的API。
计时器启动后,每次时间更改都会从服务器端推送数据,因此秒数将根据该值进行更新。

准备代理服务器

这是官方样品。
老实说我不明白。
我使用了一个名为envoy的代理服务器(这似乎是负载均衡器?)。
当前,似乎无法将gRPC请求直接发送到服务器。 (为什么端口有问题?)

在官方教程中,

envoy.Dockerfile
envoy.yaml
并使用以下命令

1
docker build -t envoy_for_timer -f ./envoy.Dockerfile .

运行

启动服务器应用程序

1
python TimerApi.py

反向代理激活

1
docker run -d -p 8081:8081 envoy_for_timer

前台应用程序启动

1
yarn serve

启动前端应用程序后,

1
2
3
  App running at:
  - Local:   http://localhost:8080/
  - Network: http://192.168.1.22:8080/

如果显示

,则应该可以使用上述地址进行访问。

tello_yolo_camera.gif

印象数

  • 对于那些只接触过REST API的人来说,来自服务器的推入式API接触起来很有趣。
  • 我认为,如果您继续与流连接,则可能要在再次执行时取消前一个。
  • Python版本的信息较少。我选择Python的目的是使用Django作为与数据库的连接,但是我想知道它是否应该用Go编写。
  • 对于Typescript,完成了message变量名的工作。 Python运行不佳。我想知道Python是否没用。 ..

仅此而已。