Nest grpc 实践之调用 python ddddocr 库

news2024/9/26 3:25:16

我曾经写过一个项目 ddddocr_server,使用 fastapi 提供 http 接口,以此来调用 ddddocr 库。

其他语言想要调用的话,则是通过 http 协议的方式来调用。然而 http 协议的开销不小,而 Websocket 调用又不灵活,此时针对这种应用场景的最佳选择就是 rpc(Remote Procedure Call 远程过程调用),而这次所要用的技术便是 grpc。

早闻 gRPC 大名,所以这次将使用 nest 通过 grpc 的方式来调用 ddddocr 库来识别验证码。

效果图

Untitled

本文源码 nest-ocr

简单熟悉下 grpc

由于我们的调用方是 nest,因此就很有必要熟悉一下 nest 要如何创建

官方提供了一个 样例,本文便在此基础上进行更改。

首先,在 nest 中 grpc 是以微服务的方式启动的,从代码上也就 3 行便可实现。

const app = await NestFactory.create(AppModule)

app.connectMicroservice <
  MicroserviceOptions >
  {
    transport: Transport.GRPC,
    options: {
      package: 'hero',
      protoPath: join(__dirname, './hero/hero.proto'),
    },
  }

await app.startAllMicroservices()

既然服务有了,那么要如何调用呢?或者说有没有像 http 接口调试工具能够调用 grpc 服务,有很多种 grpc 客户端工具,但这里选择 Postman。

Untitled

创建 API

不过这里先别急着调用,为了后续调试,建议先到工作区的 APIs 中添加一个 API,然后将样例中的 hero.proto 中导入进来

Untitled

导入完毕后将显示如下页面

Untitled

创建 gRPC 客户端

点击工作区旁边的 New 按钮(不是 + 按钮),选择 gRPC

Untitled

在 Enter URL 输入框填写 localhost:5000 (nest grpc 默认地址),这里你也可以选择第一个官方的 gRPC 测试服务,用于看看效果。

Untitled

填写完毕后,你会发现在右侧 Select a method 中并没有看到所定义的两个方法:FindOne,FindMang,这时候我们需要将 hero.proto 文件导入进来,如果你完成了 创建 API 那一步骤,你在右侧便能看到那两个方法

Untitled

此时不妨选择一下 FindOne,然后点击下方 Use Example Message,将 id 填为 1,点击 Invoke,得到的效果图如下。

Untitled

到这里我们就已经搞定了如何调用 grpc 服务,接下来就要自己去实现标题的需求。

Protobuf 消息编码

在 grpc 中,数据传输部分通过 Protobuf(Protocol Buffers)定义

因为从上面服务调用来看,貌似与 http 协议调用不相上下。

其实不然,protobuf 不同于 JSON、XML 数据,是以二进制数据流传输,数据在经 protobuf 序列化后的消息体积很小(传输内容少,传输相对就快)。同时在加上 HTTP/2 协议的加持(底层传输协议,可替换为其他协议),使得 gRPC 的传输性能要优于传统 Restful。

protobuf 对于数据传输的优点有很多,如 **支持流式传输,不过这就不是本文所述的内容了。总之你只要知道 grpc 性能高的原因就是因为 protobuf。**

首先来看 hero.proto 这个文件的内容

syntax = "proto3";

package hero;

service HeroService {
  rpc FindOne (HeroById) returns (Hero);
  rpc FindMany (stream HeroById) returns (stream Hero);
}

message HeroById {
  int32 id = 1;
}

message Hero {
  int32 id = 1;
  string name = 2;
}

不难看出,package 定义包名,service 定义服务,而 message 则是定义数据传输的类型。

客户端与服务端将根据 protobuf 来生成双方交互方式,其中包名决定了双方传输的作用域,service 下的函数就是双方之间的预先定义好要以什么样的数据发送,又以什么样的数据返回。

我个人是觉得没什么特别重点的部分,根据自己的需求然后修改基本数据结构便可。

实践

首先,要明确谁是客户端,谁是服务端。

从 标题 上来看,不难看出是 js(client) ⇒ python(server),也就是 nest 调用 ddddocr 这个库,那么 nest 就应该作为客户端,而 python 作为服务端。

先将整个流程先捋一遍,如图下图示意。

Untitled

用户想要调用 ddddocr 库,最理想的肯定是让用户直接和 python 打交道,但应用(这里指 Web)通常不会使用 python 进行编写,而其他语言(js)想要跨语言调用,这时 rpc 就再适合不过了。

可能会有人说这么操作多此一举,我只能说根据性能和业务为主。相比将 nest 后端服务迁移到 python 上,和在 nest 与 python 之间多层 grpc,在两者的工作量之下我肯定毫不疑问的选择后者。

protobuf 定义

syntax = "proto3";

package ocr;

service OCR {
  rpc Character (CharacterBody) returns (CharacterReply) {}

  // TODO: Add other type, e.g. select, slide, etc.
}

message CharacterBody {
  bytes image = 1;
}

message CharacterReply {
  string result = 1;
  int32 consumedTime = 2;
}

这部分没什么特别好说的,就图片数据以字节数组的方式传递。

nest 部分

由于 nest 作为客户端,事实上示例部分的很多代码都无关了,就比如 main.ts 中用于启动 gRPC 服务的代码,都可以注释掉,因为在这里我们并不打算将 nest 作为服务端。

// app.connectMicroservice<MicroserviceOptions>(grpcClientOptions);
// await app.startAllMicroservices();

最核心的代码,就是定义 client, 如下

@Client({
  transport: Transport.GRPC,
  options: {
    package: ['ocr'],
    protoPath: join(__dirname, './ocr.proto'),
    url: 'localhost:50051', // 这里所定义的是 grpc 服务端地址
  },
})
client: ClientGrpc

这一部分也可以通过构造函数的方式注入,因人而异。 constructor(@**Inject**('OCR_PACKAGE') private readonly client: ClientGrpc) {}

有了这个 client 就能够获取 ocrService 了,完整 ocr.controller.ts 代码如下

import { Body, Controller, OnModuleInit, Post } from '@nestjs/common'
import { Client, ClientGrpc } from '@nestjs/microservices'
import { Observable } from 'rxjs'
import { Character } from './interfaces/character.interface'
import { Reply } from './interfaces/reply.interface'
import { grpcClientOptions } from 'src/grpc-client.options'
import { CharacterDto } from './dtos/character.dto'

interface OCRService {
  Character(image: Character): Observable<Reply>

  // TODO: Add other type, e.g. select, slide, etc.
}

@Controller('ocr')
export class OcrController implements OnModuleInit {
  private ocrService: OCRService

  @Client(grpcClientOptions)
  client: ClientGrpc

  onModuleInit() {
    this.ocrService = this.client.getService('OCR')
  }

  @Post('character')
  character(@Body() dto: CharacterDto): Observable<Reply> {
    // 这里多一步 Base64 将文本解码成图片的操作
    // 主要是根据接口易用性而定,最佳的做法肯定是类似上传文件,直接得到图片二进制数据,省去数据操作步骤
    const buffer = Buffer.from(dto.image, 'base64')

    return this.ocrService.Character({ image: buffer })
  }

  // TODO: Add other type, e.g. select, slide, etc.
}

而在之前 http 的方式实现的话,这里 this.ocrService.Character({ image: dto.image }); 所对应的就是例如 fetch(’http://localhost:3002/ocr/character’) ,这里 3002 端口对应的是 python 的 http 服务。

python 部分

服务端部分其实还稍微有些复杂,可能是因为我太久没写 python 的缘故。

在之前是通过 python 来启动一个 http 服务来供其他语言调用,现在有了 gRPC 就完全没必要启动 http 服务。

可以在 这里 下载官方的 python 示例。

先安装 grpc_tools

python3 -m pip install grpcio-tools

接着执行下方指令

python3 -m grpc_tools.protoc -I . --python_out=. --grpc_python_out=. ocr.proto

它将会在下方根据 ocr.proto 生成 ocr_pb2.py 与 ocr_pb2_grpc.py 两个文件,事实上这两个文件都无需改动,你只需要每次修改 .proto 文件后再重新执行上方代码将新的内容复写到文件上便可。

不过要搞清流程,还要是在意这些文件便可。其中在 ocr_pb2_grpc.py ****文件中,你会找到 OCRServicer 类的接口定义。

class OCRServicer(object):
    """Missing associated documentation comment in .proto file."""

    def Character(self, request, context):
        """Missing associated documentation comment in .proto file."""
        context.set_code(grpc.StatusCode.UNIMPLEMENTED)
        context.set_details('Method not implemented!')
        raise NotImplementedError('Method not implemented!')

很显然这是一个接口类,因此我们需要实现它。

而 ocr_pb2.py 内容就不必细看,但后续也需要用到,主要通过 ocr_pb2.CharacterReply 将数据封装返回给客户端。

最终完整的 server.py 内容如下

from concurrent import futures
import time

import grpc
import ocr_pb2
import ocr_pb2_grpc

import ddddocr

ocr = ddddocr.DdddOcr(beta=True)

class OCRServicer(ocr_pb2_grpc.OCRServicer):

    # 这里实现 英数验证码 识别
    def Character(self, request, context):

        t = time.perf_counter()

        result = ocr.classification(request.image)
        consumed_time = int((time.perf_counter() - t)*1000)

        print({'result': result, 'consumedTime': consumed_time})

        # 根据 ocr.proto 的 message CharacterReply 生成的类
        response = ocr_pb2.CharacterReply(
            result=result, consumedTime=consumed_time)
        return response

def serve():
    port = '50051'
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    ocr_pb2_grpc.add_OCRServicer_to_server(OCRServicer(), server)
    server.add_insecure_port('[::]:' + port)
    server.start()
    print("Server started, listening on " + port)
    server.wait_for_termination()

if __name__ == '__main__':
    serve()

此时整个代码的核心流程就已经搞通了,你可以到 nest-ocr 查看源码,先看看用 postman grpc 方式调用,这里 image 为 字节数组(图片的二进制数据)

Untitled

用户以 http 方式访问的效果。

Untitled

结语

时间因素,因此本文最终代码都仅实现 英数字符识别,ddddocr 还支持点选、滑块,如有时间再补充相关代码。

从 http 方式转到 gRPC 无非就是围绕 protobuf 展开,预先定义好 protobuf,然后在此基础上去编写 grpc 客户端(调用方)与服务端(提供方) 的代码。虽然引入了一丝复杂性,但可以有效提高性能。

有时候,为了优化性能,又不想增加硬件开销,我们不得不在代码层面做出一些改进,更换高性能框架便是其中之一。然而事实上,提高性能最快捷的方式就是升级硬件。并发数不足,增加服务器数量是最直接有效的办法。

为了偏薄的性能提升,开发者总能想出诸多的解决方案。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/809659.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【计网】TCP在可靠传输中都干了啥

文章目录 1、概述2、校验和3、序列号和确认应答机制4、重传机制4.1、介绍4.2、超时重传4.3、快速重传 5、滑动窗口协议5.1、介绍5.2、发送方的滑动窗口5.3、接收方的滑动窗口 6、流量控制7、拥塞控制7.1、介绍7.2、慢开始7.3、拥塞避免7.4、快重传和快恢复 1、概述 TCP 是面向…

Day45 算法记录| 动态规划 12

股票问题 309. 买卖股票的最佳时机含冷冻期714.买卖股票的最佳时机含手续费 309. 买卖股票的最佳时机含冷冻期 这个视频讲解的很好 309.最佳买卖股票时机含冷冻期 class Solution {public int maxProfit(int[] prices) {int day prices.length;int [][] dp new int[day][2…

【雕爷学编程】MicroPython动手做(14)——掌控板之OLED屏幕

知识点&#xff1a;什么是掌控板&#xff1f; 掌控板是一块普及STEAM创客教育、人工智能教育、机器人编程教育的开源智能硬件。它集成ESP-32高性能双核芯片&#xff0c;支持WiFi和蓝牙双模通信&#xff0c;可作为物联网节点&#xff0c;实现物联网应用。同时掌控板上集成了OLED…

IOS UICollectionView 设置cell大小不生效问题

代码设置flowLayout.itemSize 单元格并没有改变布局大小&#xff0c; 解决办法如下图&#xff1a;把View flow layout 的estimate size 设置为None&#xff0c;上面设置的itemSize 生效了。

【Spring Boot】实战:实现优雅的数据返回

实战&#xff1a;实现优雅的数据返回 本节介绍如何让前后台优雅地进行数据交互&#xff0c;正常的数据如何统一数据格式&#xff0c;以及异常情况如何统一处理并返回统一格式的数据。 1.为什么要统一返回值 在项目开发过程中经常会涉及服务端、客户端接口数据传输或前后台分…

不同路径 II

一个机器人位于一个 m x n 网格的左上角 &#xff08;起始点在下图中标记为 “Start” &#xff09;。 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角&#xff08;在下图中标记为 “Finish”&#xff09;。 现在考虑网格中有障碍物。那么从左上角到右下角…

Android 面试题 ANR 五

&#x1f525; 什么是 ANR &#x1f525; ANR(Application Not Responding )应用无响应的简称&#xff0c;是为了在 APP卡死时&#xff0c;用户 可以强制退出APP的选择&#xff0c;从而避免卡机无响应问题&#xff0c;这是Android系统的一种自我保护机制。 在Android中&#xf…

Vue基础 --- 动态组件 插槽 自定义指令

1. 动态组件 1.1 什么是动态组件 动态组件指的是动态切换组件的显示与隐藏。 1.2 如何实现动态组件渲染 vue 提供了一个内置的 <component> 组件&#xff0c;专门用来实现动态组件的渲染。示例代码如下&#xff1a; data() { <!-- 1.当前要渲染的组件名称 -->…

ad+硬件每日学习十个知识点(13)23.7.24(MOS管纠错!!!)

1.MOS管到底怎么接&#xff1f;&#xff08;我一直都错了&#xff09; 2.MOS管的非饱和区 答&#xff1a; 3.MOS管的饱和区 答&#xff1a; 4.MOS管的截止区和击穿区 答&#xff1a; 5.做开关&#xff0c;让三极管工作在饱和区&#xff0c;让MOS管工作在非饱和区&…

rocketmq rsqldb 简单记录

GitHub 地址 https://github.com/alibaba/rsqldb/tree/main&#xff0c;是和目前stream sql化看齐的Rocketmq的sql&#xff0c;类似还有kafka的sqlDB 和flink sql。 目前版本0.2 &#xff0c;主要提供rest模式调用&#xff0c;controller类为public class RsqlController支持的…

SQL server 文件占用硬盘过大 日志 读写分离同步文件过大清理 DBCC收缩数据库 分发数据库distribution收缩

一顿操作猛如虎 又省出好几十G硬盘空间 小破站又能蹦跶了 目标&#xff1a;实例库日志压缩清理,分发数据库压缩清理 采用SQL 脚本收缩数据库 截断事务日志 backup log [数据库名] with no_log收缩数据库 dbcc shrinkdatabase ([数据库名]) 4.以上操作都不行的话&#xff0…

联发科CEO:未获准向华为供货,换机潮已过去,手机需求不会更差

据钜亨网报道&#xff0c;联发科近期召开了业绩说明会。蔡力行&#xff0c;该公司副董事长兼首席执行官&#xff0c;表明当前手机市场需求保持稳定&#xff0c;并且随着过去两年用户更换潮的过去&#xff0c;对手机市场明年有一定期望。 根据蔡力行的指示&#xff0c;联发科正在…

计算机和医学的交叉融合到底有多强呢?

目录 简介 人工智能在医学诊断中的应用 计算机辅助药物研发 计算机技术在基因组学研究中的应用 数字病理学 穿戴式医疗设备 虚拟现实&#xff08;VR&#xff09;和增强现实&#xff08;AR&#xff09;技术在医学教育中的应用 机器人手术 区块链技术在医学领域的应用 遥…

论文解读:DeepSort(目标跟踪)

本文来自公众号“AI大道理” —————— ​ 论文原文&#xff1a; https://arxiv.org/abs/1703.07402 SORT是一个比较简单的算法&#xff0c;用FrRCNN做探测&#xff0c;卡尔曼滤波和匈牙利算法做跟踪。 缺点&#xff1a; 线性恒速运动模型可能并不精确&#xff0c;未考虑…

二十二章:通过响应缩放进行不确定性估计,以减轻弱监督语义分割中伪掩码噪声的影响

0.摘要 弱监督语义分割&#xff08;WSSS&#xff09;在不需要密集注释的情况下对对象进行分割。然而&#xff0c;生成的伪掩码存在明显的噪声像素&#xff0c;这导致在这些伪掩码上训练的分割模型表现不佳。但是&#xff0c;很少有研究注意到或解决这个问题&#xff0c;即使在改…

【计算机组成原理】页表结构(虚拟内存的映射)

页表结构 引言简单页表多级页表总结 引言 我们的指令和数据&#xff0c;都必须先加载到内存&#xff0c;才会被CPU拿去执行。但是程序并不能直接访问到物理内存。从这里可以知道&#xff0c;程序是怎么装载到内存中执行的。 我们的内存需要被分成固定大小的页&#xff08;Pag…

docker部署Nacos2获取动态配置的失败的坑

Nacos2获取动态配置的失败的坑在此记录 nacos&#xff1a;2.0 依赖也引入了&#xff0c;配置也正确配置了&#xff0c;该写的注解也写了但是报错 [Nacos Config] config[dataIdxxx.yml, groupDEFAULT_GROUP] is empty 原因&#xff1a; nacos官网解释 给nacos容器增加额外两个…

Spring之IoC源码分析及设计思想(一)——BeanFactory

关于Spring的IOC Spring 是一个开源的 Java 平台&#xff0c;它提供了一种简化应用程序开发的框架。它是一个分层的框架&#xff0c;包括两个主要的内核&#xff1a;控制反转&#xff08;IOC&#xff09;和面向切面编程&#xff08;AOP&#xff09;。IOC 允许应用程序将组件之…

【计算机视觉中的 GAN 】如何稳定GAN训练(3)

一、说明 在上一篇文章中&#xff0c;我们达到了理解未配对图像到图像翻译的地步。尽管如此&#xff0c;在实现自己的超酷深度GAN模型之前&#xff0c;您必须了解一些非常重要的概念。如本文所提的GAN模型新成员的引入&#xff1a;Wasserstein distance&#xff0c;boundary eq…

解读分布式锁(redis实现方案)

1.导读 分布式锁是一种用于分布式系统中的并发控制机制&#xff0c;它用于确保在多个节点或多个进程之间的并发操作中&#xff0c;某些关键资源或代码块只能被一个节点或进程同时访问。分布式锁的目的是避免多个节点同时修改共享资源而导致的数据不一致或冲突的问题。通俗的来…