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

news2025/2/2 22:54:40

文章目录

  • 一、简介
    • 前置说明
  • 二、敏感词过滤服务
    • 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 server

import (
	"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 main

import (
	"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服务,并注册SensitiveServer
	s := 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 server

import (
	"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 main

import (
	"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服务,并注册SensitiveServer
	s := 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 services

import (
	"log"
	"sync"

	"google.golang.org/grpc"
	"google.golang.org/grpc/connectivity"
)

// 注意这里是大写开头,定义的是一个接口
type ClientPool interface {
	Get() *grpc.ClientConn
	Put(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 client

import (
	"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 client

import (
	"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 keywords

import (
	"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的GetPool
		pool = c.GetPool(kwAddr)
	})
	return pool
}

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

package sensitive

import (
	"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的GetPool
		pool = c.GetPool(sensitiveAddr)
	})
	return pool
}

gin web启动函数main.go

package main

import (
	"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 controllers

import (
	"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.coloradmin.cn/o/1300492.html

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

相关文章

.net core提示The xx field is required,One or more validation errors occurred

访问接口时缺少model中的参数时,会提示: The xx field is required One or more validation errors occurred原因是.net core webapi默认参数为不可空,因此会验证并报错。 解决方案: 在项目的.csproj中,修改Nullable…

2023团体程序设计天梯赛——模拟赛和总决赛题

M-L1-1 嫑废话上代码 Linux 之父 Linus Torvalds 的名言是:“Talk is cheap. Show me the code.”(嫑废话,上代码)。本题就请你直接在屏幕上输出这句话。 输入格式: 本题没有输入。 输出格式: 在一行中输出…

华为新款笔记本搭载5nm麒麟芯片,来源成谜,可能让大家失望了~

近日,华为公司悄悄推出了一款基于国产技术打造的全新商用笔记本——华为擎云L540。目前,华为擎云L540在京东平台悄然上线的,尚未在华为官方渠道公开售卖。华为擎云L540搭载了麒麟9006C处理器,采用先进的5nm制程工艺,8 …

大数据HCIE成神之路之数据预处理(1)——缺失值处理

缺失值处理 1.1 删除1.1.1 实验任务1.1.1.1 实验背景1.1.1.2 实验目标1.1.1.3 实验数据解析 1.1.2 实验思路1.1.3 实验操作步骤1.1.4 结果验证 1.2 填充1.2.1 实验任务1.2.1.1 实验背景1.2.1.2 实验目标1.2.1.3 实验数据解析 1.2.2 实验思路1.2.3 实验操作步骤1.2.4 结果验证 1…

JAVA高级(后端需深入移步)

单元测试:使用Junit单元测试框架 使用Junit单元测试: 通过左侧的对❌来进行提示 Junit框架的常见注解: 反射(用于框架,也是最重要):展示框架的成员信息 由于是用于对象,即使在获取…

PHP基础 - 类型比较

在 PHP 中,作为一种弱类型语言,它提供了松散比较和严格比较两种方式来比较变量的值和类型。 松散比较: 使用两个等号(==)进行比较,只会比较变量的值,而不会考虑它们的数据类型。例如: $a = 5; // 整数 $b = 5; // 字符串if ($a == $b) {echo "相等"; // 输…

Go性能分析工具

前言 作为后端研发,性能分析是我们在研发过程中必然会会遇到的环节,接口耗时、堆栈溢出、内存泄露等等。所谓工欲善其事必先利其器,之前在java中我们是使用arthas这一大神器,不得不说确实好用,想了解arthas的可以看下…

Zookeeper单机模式搭建

1、下载 ​wget https://dlcdn.apache.org/zookeeper/zookeeper-3.6.3/apache-zookeeper-3.6.3-bin.tar.gz 2、解压 tar -zxvf apache-zookeeper-3.6.3-bin.tar.gz 3、进入 apache-zookeeper-3.6.3-bin目录下,创建data cd apache-zookeeper-3.6.3-bin mkdir da…

面试官:性能测试瓶颈调优你是真的会吗?

引言:性能瓶颈调优 在实际的性能测试中,会遇到各种各样的问题,比如 TPS 压不上去等,导致这种现象的原因有很多,测试人员应配合开发人员进行分析,尽快找出瓶颈所在。 理想的性能测试指标结果可能不是很高&…

CGAL的2D符合规定的三角剖分和网格

1、符合规定的三角剖分 1.1、定义 如果三角形的任何面的外接圆在其内部不包含顶点,则该三角形是 Delaunay 三角形。 约束 Delaunay 三角形是一种尽可能接近 Delaunay 的约束三角形。 约束 Delaunay 三角形的任何面的外接圆在其内部不包含从该面可见的数据点。 如果…

Leetcode—1768.交替合并字符串【简单】

2023每日刷题&#xff08;五十五&#xff09; Leetcode—1768.交替合并字符串 实现代码 class Solution { public:string mergeAlternately(string word1, string word2) {int len1 word1.size(), len2 word2.size();string ans;for(int i 0; i < len1 || i < len2;…

编译Android14 AOSP原生代码并运行X86模拟器镜像过程记录

最近在研究Android Entreprise部分的特性&#xff0c;需要在Android手机上分析WorkProfile相关的源码&#xff0c;因为新买的Pixel样机还未到货&#xff0c;看了几天Android源码&#xff0c;迫切需要上真机对比分析。 又听说最近几年Android模拟器已经有些进步&#xff0c;至少…

IDEA远程调试与JDWP调试端口RCE漏洞

文章目录 前言Docker远程调试Java调试原理远程调试实践 JDWP端口RCE调试端口探测调试端口利用 总结 前言 在对一些 Java CVE 漏洞的调试分析过程中&#xff0c;少不了需要搭建漏洞环境的场景&#xff0c;但是本地 IDEA 搭建的话既麻烦&#xff08;通过 pom.xml 导入各种漏洞组…

散列卡片悬停变为整齐列表

效果展示 CSS 知识点 transform 属性运用 页面整体布局 <ul><li><div class"box"><img src"./user1.jpg" /><div class"content"><h4>Hamidah</h4><p>commented on your photo.<br />…

每日OJ题_算法_双指针④_力扣11. 盛最多水的容器

目录 力扣11. 盛最多水的容器 解析代码 力扣11. 盛最多水的容器 11. 盛最多水的容器 - 力扣&#xff08;LeetCode&#xff09; 难度 中等 给定一个长度为 n 的整数数组 height 。有 n 条垂线&#xff0c;第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。 找出其中的两…

APP备案(Android) - 获取签名证书公钥、MD5

因为近期刚针对各应用平台对APP备案时间节点要求进行了统一整理&#xff0c;然后隔天就被要求提供一下app相关的的公钥和MD5&#xff0c;虽然很快就解决了这个事情&#xff0c;但忍不住又稍微衍生了一下&#xff0c;但行小步&#xff0c;莫问远方吧 关联Blog APP备案(Android)…

【Spring Boot 源码学习】ApplicationListener 详解

Spring Boot 源码学习系列 ApplicationListener 详解 引言往期内容主要内容1. 初识 ApplicationListener2. 加载 ApplicationListener3. 响应应用程序事件 总结 引言 书接前文《初识 SpringApplication》&#xff0c;我们从 Spring Boot 的启动类 SpringApplication 上入手&am…

harmonyOS创建低的代码开发模式项目 带你基本不写代码完成一个界面跳转的小案例

之前 我们讲了 JavaScript类Web开发模式和ArkTS开发模式 但是 有人就会说 我一点代码基础都没有 难道就不能开发鸿蒙了吗&#xff1f; 其实也是可以的 本文来讲述一下低代码开发模式 我们先打开编辑器 先创建一个项目 默认模板 直接下一步 这里配置中 我们输入名称 然后选择…

【Vulnhub 靶场】【hacksudo: ProximaCentauri】【简单 - 中等】【20210608】

1、环境介绍 靶场介绍&#xff1a;https://www.vulnhub.com/entry/hacksudo-proximacentauri,709/ 靶场下载&#xff1a;https://download.vulnhub.com/hacksudo/hacksudo-ProximaCentauri.zip 靶场难度&#xff1a;简单 - 中等 发布日期&#xff1a;2021年06月08日 文件大小&…

龙迅LT9721 MIPIDSI/CSI/HDMI桥接到TYPE-C/DP 支持高达4K30HZ的分辨率

Lontium LT9721 LT9721描述&#xff1a; Lontium LT9721是MIPI/HDMI到DP转换器&#xff0c;内部有C型替代模式开关和PD控制器。 对于MIPI DSI输入&#xff0c;LT9721具有一个单端口MIPI DSI接收器&#xff0c;具有1个时钟通道和4个数据通道&#xff0c;每个数据通道的最大运行频…