55.手写实现grpc连接池以及gin和grpc交互

文章目录

  • 一、简介
    • 前置说明
  • 二、敏感词过滤服务
    • 1、定义sensitive.proto文件
    • 2、protoc生成pb.go文件
    • 3、sensitive服务端实现
  • 三、关键词匹配服务
    • 1、编写keywords.proto文件
    • 2、生成pb.go文件
    • 3、keywords服务端实现
  • 四、gin web 路由服务
    • 1、新建grpcpool服务作为gin web服务
    • 2、根据proto文件,分别生成keywords服务和sensitive服务的pb.go文件
  • 五、grpc 连接池实现
    • 1、连接池的实现,通过sync.Pool实现
    • 2、连接池的使用

代码地址: https://gitee.com/lymgoforIT/golang-trick/tree/master/33-grpc-pool

一、简介

当我们在使用需要连接的资源时,一般都应该想到可以通过池化的技术去做一定的性能优化。比如数据库连接池就是最常见的连接池。

在微服务中,服务与服务之间的通信也是需要建立连接的,如果需要频繁的交互,那么 建立连接池就可以避免每次交互都需要新建连接的性能消耗。

本案例就是要手写一个grpc的客户端连接池,整合到gin web服务中,而这个web服务需要频繁调用另外两个grpc远程服务,分别是关键词匹配服务和敏感词过滤服务(当然这里不会有很复杂的匹配和过滤上的业务逻辑,毕竟主要演示的是调用链路),链路大致如下:

在这里插入图片描述

前置说明

  1. 因为本博客主要学习的是连接池的实现方法、grpc服务的开发、gin web服务的开发、以及gin web 服务调用远程grpc服务此外,该案例会包含三个服务,工作中一般这三个服务会在不同的服务器上,这里为了演示,就在同一个代码包下,通过不同的端口号模拟多个服务。
  2. gin web 服务调用grpc服务时,本案例中我们也没有用到服务注册与发现功能,而是在gin web服务中写死了grpc客户端

二、敏感词过滤服务

该服务就一个接口,接收一段文本,然后输出是否包含敏感词,为了简便,我们不真的校验是否包含敏感词,直接返回true即可。

1、定义sensitive.proto文件

syntax  = "proto3";
package sensitive;option go_package = "33-grpc-pool/sensitive/proto";message ValidateRequest{string input = 1;
}message ValidateResponse {bool ok = 1;string word = 2;
}service SensitiveFilter {rpc Validate(ValidateRequest) returns (ValidateResponse);
}

2、protoc生成pb.go文件

 protoc --proto_path=33-grpc-pool/sensitive/proto --go_out=. --go-grpc_out=. 33-grpc-pool/sensitive/proto/sensitive.proto

在这里插入图片描述

3、sensitive服务端实现

编写服务端代码server.go

package serverimport ("context""fmt""golang-trick/33-grpc-pool/sensitive/proto"
)type SensitiveServer struct {proto.UnimplementedSensitiveFilterServer
}func (s SensitiveServer) Validate(ctx context.Context, request *proto.ValidateRequest) (*proto.ValidateResponse, error) {fmt.Printf("%+v\n", request)// 我们直接认为没有敏感词,直接返回true,敏感词为空return &proto.ValidateResponse{Ok:   true,Word: "",}, nil
}

启动服务代码main.go

package mainimport ("flag""fmt""golang-trick/33-grpc-pool/sensitive/proto""golang-trick/33-grpc-pool/sensitive/sensitive-server/server""log""net""google.golang.org/grpc"
)var (port = flag.Int("port", 50051, "")
)func main() {flag.Parse()// 监听端口lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))if err != nil {log.Fatal(err)}// 建立rpc服务,并注册SensitiveServers := grpc.NewServer()proto.RegisterSensitiveFilterServer(s, &server.SensitiveServer{})// 启动服务err = s.Serve(lis)if err != nil {log.Fatal(err)}
}

当前sensitive整体代码结构如下:
在这里插入图片描述

三、关键词匹配服务

关键词匹配 服务,编码与上面的敏感词过滤服务几乎一模一样,主要就是改了proto文件以及服务的实现。但编码流程完全没变的。
我们写完后会发现,keywords服务sensitive服务的代码结构一致。再次强调一下,这里为了演示,所以两个微服务写到了一起,通过端口分为两个微服务启动,实际一般是不同的两个微服务项目,部署到不同的机器上的。
在这里插入图片描述

1、编写keywords.proto文件

syntax  = "proto3";
package sensitive;option go_package = "33-grpc-pool/keywords/proto";message MatchRequest{string input = 1;
}message MatchResponse {bool ok = 1;string word = 2;
}service KeyWordsMatch {rpc Match(MatchRequest) returns (MatchResponse);
}

2、生成pb.go文件

注意命令中的路径和sensitive服务的有所不同,需要修改

protoc --proto_path=33-grpc-pool/keywords/proto --go_out=. --go-grpc_out=. 33-grpc-pool/keywords/proto/keywords.proto

3、keywords服务端实现

编写服务端代码server.go

package serverimport ("context""fmt""golang-trick/33-grpc-pool/keywords/proto"
)type KwServer struct {proto.UnimplementedKeyWordsMatchServer
}func (k KwServer) Match(ctx context.Context, request *proto.MatchRequest) (*proto.MatchResponse, error) {fmt.Printf("%+v\n", request)// 我们直接认为没有敏感词,直接返回true,敏感词为空return &proto.MatchResponse{Ok:   true,Word: "",}, nil
}

服务启动代码main.go

注意端口换为了50052,sensitive服务的是50051

package mainimport ("flag""fmt""golang-trick/33-grpc-pool/keywords/keywords-server/server""golang-trick/33-grpc-pool/keywords/proto""log""net""google.golang.org/grpc"
)var (port = flag.Int("port", 50052, "")
)func main() {flag.Parse()// 监听端口lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))if err != nil {log.Fatal(err)}// 建立rpc服务,并注册SensitiveServers := grpc.NewServer()proto.RegisterKeyWordsMatchServer(s, &server.KwServer{})// 启动服务err = s.Serve(lis)if err != nil {log.Fatal(err)}
}

四、gin web 路由服务

1、新建grpcpool服务作为gin web服务

由于proto文件与上面两个服务的一样的,只是go_package路径需要改变一下,就不重复贴这里了,看下面目录结构即可

在这里插入图片描述

2、根据proto文件,分别生成keywords服务和sensitive服务的pb.go文件

因为gin web 服务相当于路由服务,里面会通过rpc调用远程的两个服务,所以那两个远程服务的客户端代码就是写到gin web 服务中的,因此也需要pb.go文件存根。生成后如下:

keywords客户端存根生成命令

 protoc --proto_path=33-grpc-pool/grpcpool/services/keywords/proto --go_out=. --go-grpc_out=. 33-grpc-pool/grpcpool/services/keywords/proto/keywords.proto

sensitive客户端存根生成命令

protoc --proto_path=33-grpc-pool/grpcpool/services/sensitive/proto --go_out=. --go-grpc_out=. 33-grpc-pool/grpcpool/services/sensitive/proto/sensitive.proto

在这里插入图片描述

五、grpc 连接池实现

1、连接池的实现,通过sync.Pool实现

sync.Pool 知识补充

**结构如下:**主要是关注New字段,是一个方法,需要我们在初始化的时候提供,用于告知如何生成 池中的连接
在这里插入图片描述
Pool具有的方法: 主要关注GetPut方法,用于获取和归还连接。与数据库连接池不太一样,数据库连接池一个连接用完了会自动返回池中,而sync.Pool中的连接用完了,需要我们手动的放回去,故提供了一个Put方法
在这里插入图片描述

定义grpc-client-pool.go文件实现连接池,内容如下

package servicesimport ("log""sync""google.golang.org/grpc""google.golang.org/grpc/connectivity"
)// 注意这里是大写开头,定义的是一个接口
type ClientPool interface {Get() *grpc.ClientConnPut(conn *grpc.ClientConn)
}// 注意这里是小写开头,定义的是结构体,用于实现上面的ClientPool接口
type clientPool struct {pool sync.Pool
}// 获取连接池对象,并定义新建连接的方法,返回ClientPool接口类型
func GetPool(target string, opts ...grpc.DialOption) (ClientPool, error) {return &clientPool{pool: sync.Pool{New: func() any {conn, err := grpc.Dial(target, opts...)if err != nil {log.Fatal(err)}return conn},},}, nil
}// 从连接池中获取一个连接
func (c *clientPool) Get() *grpc.ClientConn {conn := c.pool.Get().(*grpc.ClientConn)// 当连接不可用时,关闭当前连接,并新建一个连接if conn.GetState() == connectivity.Shutdown || conn.GetState() == connectivity.TransientFailure {conn.Close()conn = c.pool.New().(*grpc.ClientConn)}return conn
}// 与数据库连接池不太一样,数据库连接池一个连接用完了会自动返回池中
// 而sync.Pool中的连接用完了,需要我们手动的放回去,故提供一个Put方法
func (c *clientPool) Put(conn *grpc.ClientConn) {// 当连接不可用时,关闭当前连接,并不再放回池中if conn.GetState() == connectivity.Shutdown || conn.GetState() == connectivity.TransientFailure {conn.Close()return}c.pool.Put(conn)
}

2、连接池的使用

和连接池相关的代码文件如下:
在这里插入图片描述
各个接口,类之间的关系如下:
在这里插入图片描述

首先,由于我们gin web 服务需要调用多个不同rpc服务,每个远程rpc服务,我们都应该建立一个对应的客户端连接池,所以为了统一,建立一个ServiceClient接口,并提供一个默认实现DefaultClient。第二点,建立远程rpc服务的客户端时(我们给sync.PoolNew字段传的函数grpc.Dial(target, opts...)),可能想传入不同的可选项,所以我们提供了一个opts文件,专门存放这些可选性,如安全连接校验等。

client.go

package clientimport ("golang-trick/33-grpc-pool/grpcpool/services""log"
)type ServiceClient interface {GetPool(addr string) services.ClientPool
}type DefaultClient struct {
}func (c *DefaultClient) GetPool(addr string) services.ClientPool {pool, err := services.GetPool(addr, c.getOptions()...)if err != nil {log.Fatal(err)}return pool
}// 还可以有很多其他的实现,比如KeywordsClient,SensitiveClient等,这里为了简单,就只写了DefaultClient

opts.go

package clientimport ("google.golang.org/grpc""google.golang.org/grpc/credentials/insecure"
)func (c *DefaultClient) getOptions() (opts []grpc.DialOption) {opts = make([]grpc.DialOption, 0)opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))return opts
}// 不同的实现可能有不同的opts,比较复杂的时候,还可以考虑使用函数式选项模式

现在可以分别创建keywordssensitive服务对应的客户端连接池对象了,使用单例

33-grpc-pool/grpcpool/services/keywords/client.go

package keywordsimport ("golang-trick/33-grpc-pool/grpcpool/services""golang-trick/33-grpc-pool/grpcpool/services/client""sync"
)//	注意是小写的,因为一个gin web服务,我们只希望它对一个grpc服务持有的连接池是一个单例
// 因此小写,避免其他地方可以构造这个结构体的对象。然后这里通过once控制是单例
type kwClient struct {// 内嵌client.DefaultClient,从而实现了ServiceClient接口// 如果有其他实现,比如KeywordsClient ,那么内嵌KeywordsClient即可client.DefaultClient
}var pool services.ClientPool
var once sync.Once// 实际工作中,这里应该用服务的注册与发现机制,这里只是会了简单演示,所以写死了服务端的地址
var kwAddr = "localhost:50052"func GetKwClientPool() services.ClientPool {once.Do(func() {c := &kwClient{}// 实际调用的是内嵌的DefaultClient的GetPoolpool = c.GetPool(kwAddr)})return pool
}

33-grpc-pool/grpcpool/services/sensitive/client.go

package sensitiveimport ("golang-trick/33-grpc-pool/grpcpool/services""golang-trick/33-grpc-pool/grpcpool/services/client""sync"
)//	注意是小写的,因为一个gin web服务,我们只希望它对一个grpc服务持有的连接池是一个单例
// 因此小写,避免其他地方可以构造这个结构体的对象。然后这里通过once控制是单例
type sensitiveClient struct {// 内嵌client.DefaultClient,从而实现了ServiceClient接口// 如果有其他实现,比如SensitiveClient ,那么内嵌SensitiveClient即可client.DefaultClient
}var pool services.ClientPool
var once sync.Once// 实际工作中,这里应该用服务的注册与发现机制,这里只是会了简单演示,所以写死了服务端的地址
var sensitiveAddr = "localhost:50051"func GetSensitiveClientPool() services.ClientPool {once.Do(func() {c := &sensitiveClient{}// 实际调用的是内嵌的DefaultClient的GetPoolpool = c.GetPool(sensitiveAddr)})return pool
}

gin web启动函数main.go

package mainimport ("golang-trick/33-grpc-pool/grpcpool/controllers""github.com/gin-gonic/gin"
)func main() {r := gin.Default()r.GET("/ping", controllers.Ping)r.Run()
}

ping函数中通过客户端连接池调用远程服务

package controllersimport ("context""fmt""golang-trick/33-grpc-pool/grpcpool/services/keywords"kwProto "golang-trick/33-grpc-pool/grpcpool/services/keywords/proto""golang-trick/33-grpc-pool/grpcpool/services/sensitive"sensitiveProto "golang-trick/33-grpc-pool/grpcpool/services/sensitive/proto""net/http""github.com/gin-gonic/gin"
)func Ping(ctx *gin.Context) {// 建立一个sensitive服务的客户端单例连接,并调用sensitive远程rpc服务的Validate接口spool := sensitive.GetSensitiveClientPool()sconn := spool.Get()// 注意用完后需要将连接手动放回连接池defer spool.Put(sconn)sensitiveClient := sensitiveProto.NewSensitiveFilterClient(sconn)sIn := &sensitiveProto.ValidateRequest{Input: "今天天气很好"}sensitiveRes, err := sensitiveClient.Validate(context.Background(), sIn)fmt.Printf("%+v    %+v  \n", sensitiveRes, err)// 建立一个keywords服务的客户端单例连接,并调用keywords远程rpc服务的Match接口kpool := keywords.GetKwClientPool()kconn := kpool.Get()// 注意用完后需要将连接手动放回连接池defer kpool.Put(kconn)keywordsClient := kwProto.NewKeyWordsMatchClient(kconn)kIn := &kwProto.MatchRequest{Input: "今天天气很好"}keywordsRes, err := keywordsClient.Match(context.Background(), kIn)fmt.Printf("%+v    %+v  \n", keywordsRes, err)ctx.JSON(http.StatusOK, gin.H{"message": "pong",})
}

测试:
启动keywords服务和sensitive服务,以及gin web服务,然后访问http://localhost:8080/ping

在这里插入图片描述
终端也可以看到两个远程服务都调用成功啦
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

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

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

相关文章

GEE影像升尺度(10m->250m)

GEE影像升尺度(10m->250m) 代码 var ext /* color: #d63000 *//* shown: false *//* displayProperties: [{"type": "rectangle"}] */ee.Geometry.Polygon([[[108.74625980473367, 28.562445155322063],[108.74625980473367, …

Day56力扣打卡

打卡记录 数对统计&#xff08;DP状态压缩&#xff09; 参考文献 #include <bits/stdc.h>using namespace std;void solve(){int n;cin >> n;map<int, int> mapp;vector<int> a(n);for (auto& x : a){cin >> x;mapp[x] ;}vector<array&…

使用WebyogSQLyog使用数据库

数据库 实现数据持久化到本地&#xff1a; 使用完整的管理系统统一管理&#xff0c; 数据库&#xff08;DateBase&#xff09;&#xff1a; 为了方便数据存储和管理&#xff08;增删改查&#xff09;&#xff0c;将数据按照特定的规则存储起来 安装WebyogSQLyog -- 创建数…

101基于matlab的极限学习机ELM算法进行遥感图像分类

基于matlab的极限学习机ELM算法进行遥感图像分类&#xff0c;对所获取的遥感图片进行初步分类和最终分类。数据可更换自己的&#xff0c;程序已调通&#xff0c;可直接运行。

如何使用 Explain 分析 SQL 语句?

如何使用 Explain 分析 SQL 语句&#xff1f; MySQL中EXPLAIN命令是我们分析和优化SQL语句的利器。 如何使用EXPLAIN来分析SQL语句&#xff0c;接下来有15个例子&#xff0c;一起学习呗 1. EXPLAIN的基本使用 EXPLAIN可以用于分析MySQL如何执行一个SQL查询&#xff0c;包括如…

python+gdal地理坐标转投影坐标

1 前言 地理坐标系&#xff0c;是使用三维球面来定义地球表面位置&#xff0c;以实现通过经纬度对地球表面点位引用的坐标系。 地理坐标系经过地图投影操作后就变成了投影坐标系。而地图投影是按照一定的数学法则将地球椭球面上点的经维度坐标转换到平面上的直角坐标。 2 流程…

基于STM32的四位数码管计数器设计与实现

✅作者简介&#xff1a;热爱科研的嵌入式开发者&#xff0c;修心和技术同步精进&#xff0c; 代码获取、问题探讨及文章转载可私信。 ☁ 愿你的生命中有够多的云翳,来造就一个美丽的黄昏。 &#x1f34e;获取更多嵌入式资料可点击链接进群领取&#xff0c;谢谢支持&#xff01;…

Docker Compose(容器编排)——9

目录 什么是 Docker Compose生活案例为什么要 Docker ComposeDocker Compose 的安装Docker Compose 的功能Docker Compose 使用场景Docker Compose 文件&#xff08;docker-compose.yml&#xff09; 文件语法版本文件基本结构及常见指令Docker Compose 命令清单 命令清单如下命…

C++11(下)

可变参数模板 C11的新特性可变参数模板能够创建可以接受可变参数的函数模板和类模板. 相比C98/03, 类模版和函数模版中只能含固定数量的模版参数, 可变模版参数无疑是一个巨大的改进, 然而由于可变模版参数比较抽象, 使用起来需要一定的技巧, 所以这块还是比较晦涩的.掌握一些基…

Vue 3项目的运行过程

概述&#xff1a; 使用Vite构建Vue 3项目后&#xff0c;当执行yarn dev命令启动服务时&#xff0c;项目就会运行起来&#xff0c;该项目会通过src\main.js文件将src\App.vue组件渲染到index.html文件的指定区域。 文件介绍&#xff1a; src\App.vue文件 Vue 3项目是由各种组件…

Spring Boot的日志

打印日志 打印日志的步骤: • 在程序中得到日志对象. • 使用日志对象输出要打印的内容 在程序中得到日志对象 在程序中获取日志对象需要使用日志工厂LoggerFactory,代码如下: package com.example.demo;import org.slf4j.Logger; import org.slf4j.LoggerFactory;public c…

STM32——继电器

继电器工作原理 单片机供电 VCC GND 接单片机&#xff0c; VCC 需要接 3.3V &#xff0c; 5V 不行&#xff01; 最大负载电路交流 250V/10A &#xff0c;直流 30V/10A 引脚 IN 接收到 低电平 时&#xff0c;开关闭合。

从Centos-7升级到Centos-Stream-8

如果在正式环境升级&#xff0c;请做好数据备份以及重要配置备份&#xff01;因为升级会造一部分应用被卸载。 注意&#xff1a;升级前请备份好数据&#xff0c;升级可能会导致ssh的root用户无法登陆、网卡名称发生改变、引导丢失无法开机等问题。 1.安装epel源 yum -y install…

【Spring教程20】Spring框架实战:AOP(面对切面编程)知识总结

欢迎大家回到《Java教程之Spring30天快速入门》&#xff0c;本教程所有示例均基于Maven实现&#xff0c;如果您对Maven还很陌生&#xff0c;请移步本人的博文《如何在windows11下安装Maven并配置以及 IDEA配置Maven环境》&#xff0c;本文的上一篇为《利用 AOP通知获取数据代码…

软件测试(接口测试业务场景测试)

软件测试 手动测试 测试用例8大要素 编号用例名称&#xff08;标题&#xff09;模块优先级预制条件测试数据操作步骤预期结果 接口测试&#xff08;模拟http请求&#xff09; 接口用例设计 防止漏测方便分配工具&#xff0c;评估工作量和时间接口测试测试点 功能 单接口业…

利用Microsoft Visual Studio Installer Projects打包安装包

利用Microsoft Visual Studio Installer Projects打包安装包 具体步骤步骤1&#xff1a;安装扩展步骤2&#xff1a;创建 Setup 项目步骤3&#xff1a;设置属性步骤4&#xff1a;添加输出步骤5&#xff1a;添加文件步骤6&#xff1a;添加桌面快捷方式步骤7&#xff1a;添加菜单快…

【Table/SQL Api】Flink Table/SQL Api表转流读取MySQL

引入依赖 jdbc依赖 flink-connector-jdbc mysql-jdbc-driver 操作mysql数据库 <!-- Flink-Connector-Jdbc --><dependency><groupId>org.apache.flink</groupId><artifactId>flink-connector-jdbc_${scala.binary.version}</artifactId>…

Ubuntu上安装 Git

在 Ubuntu 上安装 Git 可以通过包管理器 apt 进行。以下是在 Ubuntu 上安装 Git 的步骤&#xff1a; 打开终端。你可以按 Ctrl Alt T 组合键来打开终端。 运行以下命令以确保你的系统的软件包列表是最新的&#xff1a; sudo apt update 安装 Git&#xff1a; sudo apt inst…

RT-DERT改进策略:AKConv即插即用,轻松涨点

摘要 提出了一种算法&#xff0c;用于生成任意尺寸卷积核的初始采样坐标。与常规卷积核相比&#xff0c;提出的AKConv实现了不规则卷积核的函数来提取特征&#xff0c;为各种变化目标提供具有任意采样形状和尺寸的卷积核&#xff0c;弥补了常规卷积的不足。在COCO2017和VisDro…

Anaconda文件目录(打开默认路径)更改

Anaconda 文件默认目录更改 每次打开 Anaconda 都在C盘怎么办&#xff0c;如何改为D盘或是其他盘符位置&#xff1f; 可以进行下述操作。 1. 单次修改路径 单次修改路径&#xff1a;在 exe 文件(Anaconda Prompt (Anaconda_py))中写入下面代码&#xff1a; jupyter notebook …