不基于Gin手撸一个RPC服务

目标

实现一个GRPC框架,可以通过grpc-ui来对接口进行访问。也可以使用client来直接调用服务端服务

准备(这边以Mac系统举例)

安装homebrew(如果没有安装的话)

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

安装PostgresSql(熟悉Mysql的也可以用mysql代替)

brew install postgresqlpsql --version 

安装protobuf 相关组件

brew install protobufbrew install protoc-gen-gobrew install protoc-gen-go-grpcprotoc --versionprotoc-gen-go --versionprotoc-gen-go-grpc --version

安装grpc-ui

go install github.com/fullstorydev/grpcui/cmd/grpcui@latest

第一步 定义接口(.proto文件)

初始化一个go工程,我这边命名为school-rpc
创建一个protobuf文件夹,定义一个student.proto文件,该文件后续会由编译脚本进行执行,用于生成pb.go 以及grpc_pb.go 文件

syntax = "proto3";option go_package = "./protobuf/student";
package school_rpc_service.student;message StudentListRequest{uint32 pageSize = 1;uint32 pageNo = 2;
}message CreateStudentRequest{string name = 1;uint32 age = 2;uint32 gender = 3;string  mobile = 4;string className = 5;uint32 grade =6;
}message UpdateStudentRequest{uint64  id = 1;string name = 2;uint32 age = 3;uint32 gender = 4;string  mobile = 5;string className = 6;uint32 grade =7;
}message StudentListResponse {string code = 1;string msg = 2;repeated Student studentList = 3;
}message CreateStudentResponse {string code = 1;string msg = 2;int64 id = 3;
}message UpdateStudentResponse {string code = 1;string msg = 2;}message Student {string id = 1;          // 学生 IDstring name = 2;        // 学生姓名uint32 age = 3;          // 学生年龄uint32 gender = 4;string  mobile = 5;string className = 6;uint32 grade =7;
}service StudentService {rpc studentList(StudentListRequest) returns (StudentListResponse){}rpc createStudent(CreateStudentRequest) returns (CreateStudentResponse){}rpc updateStudent(UpdateStudentRequest) returns (UpdateStudentResponse){}
}

第二步, 定义编脚本(compile.sh)

上一步中,我们已经定义好了.proto 文件,现在需要来处理一下编译的脚本
如果,在这一步中,如果我们没有安装protobuf 相关的组件,请参考准备阶段的流程,进行安装。
完成安装后,我们在项目的bin目录下,创建一个compile.sh 文件
compile.sh:

#!/bin/bashfunction exit_if() {extcode=$1msg=$2if [ $extcode -ne 0 ]thenif [ "msg$msg" != "msg" ]; thenecho $msg >&2fiexit $extcodefi
}echo $GOPATH;if [ ! -f $GOPATH/bin/protoc-gen-go ]
thenecho 'No plugin for golang installed, skip the go installation' >&2echo 'try go get github.com/golang/protobuf/protoc-gen-go' >&2
elseecho Compiling go interfaces...export GO_PATH=$GOPATHexport GOBIN=$GOPATH/binexport PATH=$PATH:$GOPATH/binprotoc -I ./ --go_out=./ --go-grpc_out=require_unimplemented_servers=false:. protobuf/*.protoexit_if $?echo Done
fi

第三步 编写MakeFile

来验证compile.sh 脚本是否能够成功编译成.go代码,以及生成执行文件

school-rpc:./bin/compile.shenv GO111MODULE=on go build $(LDFLAGS)
.PHONY: school-rpcclean:rm school-rpctest:go test -v ./...lint:golangci-lint run ./...

同步写一个main.go , main方法中,随便打印一行hello world 即可
此时的整个目录结构如下

image.png


Makefile ,main.go ,go.mod , compile.sh,student.proto 一共5个文件,结构还是比较清晰
然后,唤起一个终端,使用 "make" 命令

image.png


如上提示即代表成功,成功后的目录结构下多了school-rpc执行文件 和 student.pb.go & student_grpc.pb.go

image.png

第四步 初始化Sql相关

我们在工程的目录下,创建一个migration的目录,用于存放初始化sql

CREATE TABLE IF NOT EXISTS students (id BIGSERIAL PRIMARY KEY,name VARCHAR(100),age INT,gender INT,mobile VARCHAR(100),class_name VARCHAR(100),grade INT
);CREATE TABLE IF NOT EXISTS clazz (id BIGSERIAL PRIMARY KEY,name VARCHAR(100),grade INT
)

然后,我们期望在执行makefile的之后,能够根据命令参数,来执行具体的操作。因此我们需要在项目中增加一个cmd的目录,并在其中增加一个cli.go

// 具体执行migration的方法
func runMigrations(ctx *cli.Context) error {ctx.Context = opio.CancelOnInterrupt(ctx.Context)cfg := config.NewConfig(ctx)db, err := database.NewDB(ctx.Context, cfg.Database)if err != nil {return err}defer func(db *database.DB) {err := db.Close()if err != nil {}}(db)err = db.ExecuteSQLMigration(cfg.Migrations)if err != nil {return err}return nil
}
// 创建一个cli App实例,其中包含了执行migrations包下面的sql
func NewCli(GitCommit string, GitData string) *cli.App {flags := flags2.Flagsreturn &cli.App{Version:              params.VersionWithCommit(GitCommit, GitData), // 将git提交信息,和版本信息组合在一起生产版本信息Description:          "An exchange school services with rpc and rest api server",EnableBashCompletion: true,Commands: []*cli.Command{{Name:        "migrate",Flags:       flags,Description: "Run database migrations",Action:      runMigrations,},},}
}

然后,我们在db里面把该有的逻辑进行一下补充

func (db *DB) ExecuteSQLMigration(migrationsFolder string) error {err := filepath.Walk(migrationsFolder, func(path string, info os.FileInfo, err error) error {if err != nil {return errors.Wrap(err, fmt.Sprintf("Failed to process migration file: %s", path))}if info.IsDir() {return nil}fileContent, readErr := os.ReadFile(path)if readErr != nil {return errors.Wrap(readErr, fmt.Sprintf("Error reading SQL file: %s", path))}execErr := db.gorm.Exec(string(fileContent)).Errorif execErr != nil {return errors.Wrap(execErr, fmt.Sprintf("Error executing SQL script: %s", path))}return nil})return err
}

如此,我们就把通过命令行,执行初始化sql语句的逻辑给写好了
最后,我们把main.go 移动到/cmd 目录下,同时,添加命令行相关的代码,用于执行相关的命令行

func main() {log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelInfo, true)))app := NewCli(GitCommit, GitData)// 这个方法的作用是否是增加一个信号中断处理器,用于通知给上下文ctx := opio.WithInterruptBlocker(context.Background())// 真正执行的是command.go Run方法if err := app.RunContext(ctx, os.Args); err != nil {log.Error("Application failed")os.Exit(1)}
}

并且将makefile也一并进行修改

school-rpc:./bin/compile.shenv GO111MODULE=on go build -v -o school-rpc $(LDFLAGS) ./cmdclean:rm school-rpctest:go test -v ./...lint:golangci-lint run ./...

第五步 通过flag将配置文件配置好之后,即可初始化数据库

const evnVarPrefix = "SCHOOL"func prefixEnvVars(name string) []string {return []string{evnVarPrefix + "_" + name}
}var (MigrationsFlag = &cli.StringFlag{Name:    "migrations-dir",Value:   "./migrations",Usage:   "path for database migrations",EnvVars: prefixEnvVars("MIGRATIONS_DIR"),}// RpcHostFlag RPC ServiceRpcHostFlag = &cli.StringFlag{Name:     "rpc-host",Usage:    "The port of the rpc",EnvVars:  prefixEnvVars("RPC_HOST"),Required: true,}// RpcPortFlagRpcPortFlag = &cli.IntFlag{Name:     "rpc-port",Usage:    "The port of the rpc",EnvVars:  prefixEnvVars("RPC_PORT"),Value:    8987,Required: true,}// MetricsHostFlag MetricsMetricsHostFlag = &cli.StringFlag{Name:     "metrics-host",Usage:    "The port of the metrics",EnvVars:  prefixEnvVars("METRICS_PORT"),Required: true,}MetricsPortFlag = &cli.IntFlag{Name:     "metrics-port",Usage:    "The port of the metrics",EnvVars:  prefixEnvVars("METRICS_PORT"),Value:    7214,Required: true,}// DbHostFlag DatabaseDbHostFlag = &cli.StringFlag{Name:     "master-db-host",Usage:    "The hostname of the database master",EnvVars:  prefixEnvVars("DB_HOST"),Required: true,}DbPortFlag = &cli.IntFlag{Name:     "master-db-port",Usage:    "The port of the master database",EnvVars:  prefixEnvVars("DB_PORT"),Required: true,}DbUserFlag = &cli.StringFlag{Name:     "master-db-user",Usage:    "The user of the master database",EnvVars:  prefixEnvVars("DB_USER"),Required: true,}DbPasswordFlag = &cli.StringFlag{Name:     "master-db-password",Usage:    "The password of the master database",EnvVars:  prefixEnvVars("DB_PASSWORD"),Required: true,}DbNameFlag = &cli.StringFlag{Name:     "master-db-name",Usage:    "The name of the master database",EnvVars:  prefixEnvVars("DB_NAME"),Required: true,}
)var requireFlags = []cli.Flag{MigrationsFlag,RpcHostFlag,RpcPortFlag,MetricsHostFlag,MetricsPortFlag,DbHostFlag,DbPortFlag,DbUserFlag,DbPasswordFlag,DbNameFlag,
}var optionalFlags = []cli.Flag{}func init() {Flags = append(requireFlags, optionalFlags...)
}var Flags []cli.Flag

这一步的核心功能是从.env 文件中读取数据,然后生成配置信息,供业务层进行使用
以下是.env 中的信息


export SCHOOL_RPC_PORT=8980
export SCHOOL_RPC_HOST="127.0.0.1"
export SCHOOL_METRICS_PORT=8990
export SCHOOL_METRICS_HOST="127.0.0.1"export SCHOOL_DB_HOST="127.0.0.1"
export SCHOOL_DB_PORT=5432
export SCHOOL_DB_USER="school"
export SCHOOL_DB_PASSWORD="1234"
export SCHOOL_DB_NAME="school"

注意我们一定需要source .env 一下,要不然执行脚本读取不到.env 文件里面的信息

第六步 初始化数据库

到这一步的时候,我们的工程结构是这样的

image.png

使用 make clean && make 命令,重新生成执行文件
执行./school-rpc 会有命令选项出现

image.png


我们可以执行 ./school-rpc migrate 执行初始化sql,如果没有报错,我们在数据库里面能查到新建的表结构代表创建已经成功了

第七步 实现相关的接口

第一步 在工程下,我们创建一个services 目录,services下分别建立一个
rpcServer.go 和 studentHandle.go

rpcServer.go 主要是用来定义rpc服务端,监听指定的端口号

核心方法为三个

创建 rpcServer实例

根据上下文传递的配置和db,返回rpcServer实例的指针

start方法

创建一个协程,使用配置项所设置的地址和端口号,来呼起一个grpc服务

stop方法

这里只是单纯修改一下rpcServer的状态(可忽略)

const MaxRecvMessageSize = 1024 * 1024 * 300type RpcServerConfig struct {GrpcHostname stringGrpcPort     int
}type RpcServer struct {*RpcServerConfigdb *database.DBwallet.UnimplementedWalletServiceServerstopped atomic.Bool
}func (s *RpcServer) Stop(ctx context.Context) error {s.stopped.Store(true)return nil
}func (s *RpcServer) Stopped() bool {//TODO implement mepanic("implement me")
}func NewRpcServer(db *database.DB, config *RpcServerConfig) (*RpcServer, error) {return &RpcServer{RpcServerConfig: config,db:              db,}, nil
}func (s *RpcServer) Start(ctx context.Context) error {go func(s *RpcServer) {addr := fmt.Sprintf("%s:%d", s.GrpcHostname, s.GrpcPort)fmt.Println("start rpc server", "addr", addr)listener, err := net.Listen("tcp", addr)if err != nil {fmt.Println("Could not start rpc server", "err", err)}opt := grpc.MaxRecvMsgSize(MaxRecvMessageSize)//创建一个新的 gRPC 服务器实例 gs,并注册反射服务(允许客户端通过反射查询服务信息)。gs := grpc.NewServer(opt, grpc.ChainUnaryInterceptor(nil))reflection.Register(gs)//注册服务wallet.RegisterWalletServiceServer(gs, s)// 启动grpc服务fmt.Println("start rpc server", "port", s.GrpcPort, "address", listener.Addr())if err := gs.Serve(listener); err != nil {fmt.Println("start rpc server", "err", err)}}(s)return nil
}

studentHandle.go

这部分核心主要就是实现对应的在proto文件中的接口方法的声明

func (s *RpcServer) StudentList(ctx context.Context, request *student.StudentListRequest) (*student.StudentListResponse, error) {schoolDB := s.GetRpcSchoolDB()studentList, err := schoolDB.FindStudentList(request.GetPageSize(), request.GetPageNo())studentPointers := make([]*student.Student, len(studentList))for i := range studentList {studentPoint := &student.Student{Name:      studentList[i].Name,Age:       studentList[i].Age,Gender:    studentList[i].Gender,Mobile:    studentList[i].Mobile,ClassName: studentList[i].ClassName,Grade:     studentList[i].Grade,}studentPointers[i] = studentPoint}if err != nil {return nil, err}return &student.StudentListResponse{Code:        strconv.Itoa(200),Msg:         "get Student List SUCCESS",StudentList: studentPointers,}, nil
}func (s *RpcServer) CreateStudent(ctx context.Context, request *student.CreateStudentRequest) (*student.CreateStudentResponse, error) {schoolDB := s.GetRpcSchoolDB()err := schoolDB.CreateStudent(&database.Student{Name:      request.Name,Age:       request.Age,Gender:    request.Gender,Mobile:    request.Mobile,ClassName: request.ClassName,Grade:     request.Grade,})if err != nil {return &student.CreateStudentResponse{Code: strconv.Itoa(500),Msg:  "Create Student Fail",}, err}return &student.CreateStudentResponse{Code: strconv.Itoa(200),Msg:  "Create Student SUCCESS",}, nil
}func (s *RpcServer) UpdateStudent(ctx context.Context, request *student.UpdateStudentRequest) (*student.UpdateStudentResponse, error) {schoolDB := s.GetRpcSchoolDB()err := schoolDB.UpdateStudent(&database.Student{Id:        request.Id,Name:      request.Name,Age:       request.Age,Gender:    request.Gender,Mobile:    request.Mobile,ClassName: request.ClassName,Grade:     request.Grade,})if err != nil {return &student.UpdateStudentResponse{Code: strconv.Itoa(500),Msg:  "Create Student Fail",}, err}return &student.UpdateStudentResponse{Code: strconv.Itoa(200),Msg:  "Create Student SUCCESS",}, nil}

至于DB中的方法,主要就是gorm中的crud方法,限于篇幅,这里不作赘述,有兴趣可以看源码链接

cli.go

命令行 文件中需要增加对 启动服务端程序的实现,以及命令中增加指定的参数选择

func runRpc(ctx *cli.Context, causeFunc context.CancelCauseFunc) (cliapp.Lifecycle, error) {fmt.Println("running grpc server...")cfg := config.NewConfig(ctx)grpcServerCfg := &services.RpcServerConfig{GrpcHost: cfg.RpcServer.Host,GrpcPort: strconv.Itoa(cfg.RpcServer.Port),}db, err := database.NewDB(ctx.Context, cfg.Database)if err != nil {log.Error("failed to connect to database", "err", err)return nil, err}return services.NewRpcServer(grpcServerCfg, db)
}func NewCli(GitCommit string, GitData string) *cli.App {flags := flags2.Flagsreturn &cli.App{Version:              params.VersionWithCommit(GitCommit, GitData), // 将git提交信息,和版本信息组合在一起生产版本信息Description:          "An exchange school services with rpc and rest api server",EnableBashCompletion: true,Commands: []*cli.Command{{Name:        "rpc",Flags:       flags,Description: "Run rpc services",Action:      cliapp.LifecycleCmd(runRpc),},{Name:        "migrate",Flags:       flags,Description: "Run database migrations",Action:      runMigrations,},},}
}

从上面的代码中可以看到,两个命令行,一个是run rpc services 的命令,另外一个是执行初始化sql的命令。至此,所有的代码已经ready,可以看看效果了。

第八步,试试RPC 服务端的具体效果

第一步 重新执行一下 make clean && make 。生成最新的执行文件

image.png


可以看到项目的根目录中包含了 school-rpc 执行文件

第二步 再次执行./school-rpc

image.png


可以看到COMMANDS 中增加了rpc 启动的服务选项(rpc)

第三步 启动服务

image.png


正常情况下,当前的服务启动成功了

第四步 启动grpc-ui

如果之前还没有安装的话,可以参考 “准备” 进行安装
grpcui -plaintext 127.0.0.1:8980 执行这个命令,注意host 和 port和 我们启动的服务保持一致即可
它会弹出一个UI页面,我们可以在这个页面上面进行调试

image.png

image.png

更多相关信息,,https://t.me/gtokentool 

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

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

相关文章

大数据治理:策略、技术与挑战

随着信息技术的飞速发展,大数据已经成为现代企业运营和决策的重要基础。然而,大数据的复杂性、多样性和规模性给数据管理带来了前所未有的挑战。因此,大数据治理应运而生,成为确保数据质量、合规性、安全性和可用性的关键手段。本…

Web应用性能测试工具 - httpstat

在数字化时代,网站的性能直接影响用户体验和业务成功。你是否曾经在浏览网页时,遇到加载缓慢的困扰?在这个快速变化的互联网环境中,如何快速诊断和优化Web应用的性能呢?今天,我们将探讨一个强大的工具——h…

宝藏虚拟化学习资料大全

最近发现了关于虚拟化的宝藏资料,瑞斯拜!原文链接如下: 500篇关于虚拟化的经典资料,含CPU虚拟化,磁盘虚拟化,内存虚拟化,IO虚拟化。 目录 🪐 虚拟化基础 🍃 虚拟化分类&…

【源码+文档】基于SpringBoot+Vue旅游网站系统【提供源码+答辩PPT+参考文档+项目部署】

作者简介:✌CSDN新星计划导师、Java领域优质创作者、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和学生毕业项目实战,高校老师/讲师/同行前辈交流。✌ 主要内容:🌟Java项目、Python项目、前端项目、PHP、ASP.NET、人工智能…

微服务核心——网关路由

目录 前言 一、登录存在的问题归纳 二、*微服务网关整体方案 三、认识微服务网关 四、网关鉴权实现 五、OpenFeign微服务间用户标识信息传递实现 六、微服务网关知识追问巩固 前言 本篇文章具体讲解微服务中网关的实现逻辑、用于解决什么样的问题。其中标题中标注* 涉…

移植 AWTK 到 纯血鸿蒙(HarmonyOS NEXT)系统 (0) - 序

移植 AWTK 到 纯血鸿蒙 (HarmonyOS NEXT) 系统 (0) - 序 前段时间纯血鸿蒙系统 HarmonyOS 5.0(又称 HarmonyOS NEXT)正式推出,这是继苹果 iOS 和安卓系统后,全球第三大移动操作系统。纯正国产操作系统登场,国人无不欢…

docker-compose安装rabbitmq 并开启延迟队列和管理面板插件(rabbitmq_delayed_message_exchange)

问题: 解决rabbitmq-plugins enable rabbitmq_delayed_message_exchange :plugins_not_found 我是在docker-compose环境部署的 services:rabbitmq:image: rabbitmq:4.0-managementrestart: alwayscontainer_name: rabbitmqports:- 5672:5672- 15672:156…

SpringBoot AOP介绍、核心概念、相应实现

文章目录 AOP介绍AOP的核心概念切面(Aspect)切点(Join Point)语法具体解释 增强(Advice)织入(weaving) 相应实现权限校验日志输出 AOP介绍 AOP全称Aspect Oriented Programming意为面向切面编程,通过预编译和运行期间通过动态代理来实现程序功能统一维护的技术。AO…

Python 数据结构对比:列表与数组的选择指南

文章目录 💯前言💯Python中的列表(list)和数组(array)的详细对比1. 数据类型的灵活性2. 性能与效率3. 功能与操作4. 使用场景5. 数据结构选择的考量6. 实际应用案例7. 结论 💯小结 &#x1f4af…

CSS 超出一行省略号...,适用于纯数字、中英文

文本超出显示省略号... 代码: .ellipsis{ overflow: hidden; -webkit-line-clamp:1; text-overflow: ellipsis; display: -webkit-box; -webkit-box-orient: vertical; word-break: break-all; /** 纯数字、中英文都适用 */ }

C/C++中标准的输入输出

一、c语言的标准输入输出 c语言的标准输出函数式printf,它可以将用户设置的变量输出到控制台;标准的输入函数式scanf,接收用户在控制台的输入数据,注意,如果使用的是visual stdio编译器,会提示使用scanf_s…

Elasticsearch中时间字段格式用法详解

Elasticsearch中时间字段格式用法详解 攻城狮Jozz关注IP属地: 北京 2024.03.18 16:27:51字数 758阅读 2,571 Elasticsearch(简称ES)是一个基于Lucene构建的开源、分布式、RESTful搜索引擎。它提供了全文搜索、结构化搜索以及分析等功能,广泛…

Java实战项目-基于SpringBoot+Vue的二手车交易系统的研究与实现

博主介绍:✌程序员徐师兄、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ 🍅文末获取源码联系🍅 👇🏻 精彩专栏推荐订阅👇…

Redis安装与使用 + Springboot整合Redis

Redis安装与使用 Springboot整合Redis 前言Redis简介Redis优势 Redis安装Windows1.相关配置2.启动Redis服务3.连接Redis,进行操作4.测试一些Redis命令 Linux Springboot项目整合使用Redis1.添加Maven依赖2.配置Redis相关属性3.在测试类中进行测试 结语 &#x1f60…

lust变频器维修电梯变频器CDD34.014.W2.1LSPC1

LUST伺服在安装时须注意,不可有任何的铁屑、螺丝、导线等掉人驱动器内。在安装完成后应作基本的检测动作,如对地阻抗,和短路检测等。 所有的安装及使用事项需要符合安全规定,并且也需要符合当地的相关规定和灾害预防措施。DC BUS…

在VSCode中读取Markdown文件

在VSCode安装Markdown All in One或Markdown Preview Enhanced即可 插件Markdown All in One GitHub:https://github.com/yzhang-gh/vscode-markdown v3.6.2下载链接:https://marketplace.visualstudio.com/_apis/public/gallery/publishers/yzhang/vs…

闪存学习_2:Flash-Aware Computing from Jihong Kim

闪存学习_2:Flash-Aware Computing from Jihong Kim【1】 一、三个闪存可靠性问题二、内存的分类三、NAND 闪存和 NOR 闪存四、HDD和SSD比较Reference 一、三个闪存可靠性问题 耐性(即寿命):最多能经受编程和擦除的次数。数据保留…

Java项目实战II基于Spring Boot的文理医院预约挂号系统的设计与实现(开发文档+数据库+源码)

目录 一、前言 二、技术介绍 三、系统实现 四、文档参考 五、核心代码 六、源码获取 全栈码农以及毕业设计实战开发,CSDN平台Java领域新星创作者,专注于大学生项目实战开发、讲解和毕业答疑辅导。 一、前言 在医疗资源日益紧张的背景下&#xff0…

【Linux系列】磁盘空间不足

💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

keepalive+mysql8双主

1.概述 利用keepalived实现Mysql数据库的高可用,KeepalivedMysql双主来实现MYSQL-HA,我们必须保证两台Mysql数据库的数据完全一致,实现方法是两台Mysql互为主从关系,通过keepalived配置VIP,实现当其中的一台Mysql数据库…