Golang语言之gRPC程序设计示例

简介: 这篇文章是关于Golang语言使用gRPC进行程序设计的详细教程,涵盖了RPC协议的介绍、gRPC环境的搭建、Protocol Buffers的使用、gRPC服务的编写和通信示例。

                                              作者:尹正杰
版权声明:原创作品,谢绝转载!否则将追究法律责任。

一.RPC协议介绍

1.什么是RPC

RPC对应的英文名称为"Remote Procedure Call",即远程过程调用。

与HTTP一致,也是应用程协议,该协议的目标是实现: 调用远程过程(方法,函数)就如调用本地方法一致。

RPC协议没有对网络层作出规范,那也就意味着具体的RPC实现可以基于TCP,也可以基于其他协议,例如:HTTP,UDP等。

RPC也没有对数据传输格式做规范,也就是逻辑层面,传输JSON,Text,protobuf都可以。

广泛使用的RPC产品有gRPC,Thrift等。说白了,RPC是一协议,GRPC,Thrift是实现RPC的具体的一个产品。

如上图所示,RPC调用过程说明如下:
    - 1.ServiceA需要调用ServerB的FuncOnB函数,对于ServerA来说FuncOnB就是远程过程;
    - 2.RPC的目的是让ServiceA可以像调用ServiceA本地的函数一样调用远程函数FuncOnB;
            换句话说,也就是ServerA上代码层面上使用"serviceB.FuncOnB()"即可完成调用。
    - 3.RPC是C/S模式,调用方为Client,远程方为Server;
    - 4.RPC把整个的调用过程,数据打包,网络请求等封装完毕,在C,S两端的stub(代码存根,就好像电影院检票员会撕掉票的一部分留存证据是否有这张票)中;
    - 5.RPC典型调用流程如下:
        - 5.1 ServerA将调用需求告知Client Sub;
        - 5.2 Client Sub将调用目标(Call ID),参数数据(params)等调用信息进行打包(序列化),并将打包好的调用信息通过网络传输给Server Sub;
        - 5.3 Server Sub将根据调用信息,调用相应过程,期涉及到数据的拆包(反序列化)等操作;
        - 5.4 远程过程FuncOnB运行,并得到结果,将结果告知Server Sub;
        - 5.5 Server Sub将结果打包,并传输回给Client Sub;
        - 5.6 Client Sub将结果拆包,把最终函数调用的结果告知ServerA;

2.什么是GRPC

gRPC是一个高性能,开源的通用RPC框架。是一个Google开源的高性能远程过程调用RPC框架,可以在任何环境中运行。

gRPC可以通过对负载平衡,跟踪,健康检查和身份验证的可插拔支持有效的连接数据中心内和跨数据中心的服务。

gRPC也是用于分布式计算的最后一步,将设备,移动应用程序和浏览器与后端服务对接。

gRPC支持多种主流编程语言,包括但不限于: C/C++,C#,Dart,Go,Java,Kotlin,Node.js,Objective-C,PHP,Python,Ruby等。

gRPC的核心是帮助咱们通信,帮助我们去标准化,结构化消息,最终的业务逻辑和gRPC无关。

gRPC官网地址:
    https://grpc.io

3.安装gRPC环境

3.1 使用gRPC的前提

使用Google的gRPC的前提条件是:
    - 1.安装Golang环境;
    - 2.安装Protocol Buffer编译器protoc,推荐3版本;
    - 3.安装go plugin,用于protocol buffer编译器转换成Golang环境的代码;


安装protoc的方式:
    可以使用yum或apt包管理器安装,但通常版本回比较滞后,因此更建议使用预编译的二进制方式安装。
    protoc下载地址:
        https://github.com/protocolbuffers/protobuf

3.2 安装protoc

Mac安装protoc:
    1.下载protoc程序包
curl -LO https://github.com/protocolbuffers/protobuf/releases/download/v27.3/protoc-27.3-osx-x86_64.zip

    2.解压protoc程序
sudo unzip protoc-27.3-osx-x86_64.zip -d /usr/local/

    3.查看安装版本,如上图所示
protoc --version


Linux安装protoc:
    1.下载protoc程序包
curl -LO https://github.com/protocolbuffers/protobuf/releases/download/v27.3/protoc-27.3-linux-x86_64.zip


    2.解压protoc程序
sudo unzip protoc-27.3-linux-x86_64.zip -d /usr/local

    3.查看安装版本
protoc --version




windows安装protoc:
    1.下载protoc程序包
curl -LO https://github.com/protocolbuffers/protobuf/releases/download/v27.3/protoc-27.3-win64.zip

    2.解压protoc程序
sudo unzip protoc-27.3-win64.zip -d <解压到你自定义的安装目录即可,并将该目录添加到PATH环境变量>

    3.查看安装版本
protoc --version

3.3 安装go plugin

    1.安装protoc生成go代码的插件包,程序会默认回直接安装到"$GOPATH/bin"路径下
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest

    2.安装protoc生成grpc的插件包,程序会默认回直接安装到"$GOPATH/bin"路径下
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

    3.查看grpc版本,如上图所示
protoc-gen-go-grpc -version
protoc-gen-go --version

二.Protocol Buffer的使用指南

1.使用Protocol Buffer的基本流程概述

默认情况下,gRPC使用Protocol Buffers,这是Google用于序列化数据的成熟开源机制(尽管它可以与JSON等其他数据格式一起使用)。

使用Protocol Buffer的基本流程:
    1.使用protocol buffers语法定义消息,消息是用于传递的数据;
    2.使用protocol buffers与法定义服务,服务是RPC方法的集合,来使用消息;
    3.使用protocol buffer编译工具protoc来编译,生成对应语言的代码,例如Go的代码;

使用Protocol Buffers的第一步是在".proto"文件中定义序列化数据的结构,".proto"文件是普通的文本文件。

Protocol Buffer数据被结构化为消息,其中每条消息都是一个小的信息逻辑记录,包含一系列称为字段的key-value对。

除了核心内容外,".proto"文件还需要指定与法版本,目前主流的也是最新的proto3版本,在".proto"文件的开头指定。

Protol Buffers官方文档:
    https://developers.google.com/protocol-buffers/docs/overview

2.编写产品服务等proto文件示例

syntax = "proto3";

// go_package 定义"go_package"属性选项,protoc会基于包构建目录,用来说明生成go代码所在的包。
// 名称可以自定义,但该包的文件尽量不要去修改。
option go_package = "./proto-compiles-readonly";



// ProductResponse 定义用于在服务间传递消息,响应的产品信息结构。
message ProductResponse {
    // 消息的字段,将来响应时会返回id,name和is_sale这三个字段。
    int64 id = 1;
    string name = 2;
    bool is_sale = 3;
}


// ProductRequest 定义用于在服务间传递消息,请求产品信息时的参数消息
message ProductRequest {
    // 将来基于id字段来查询数据。
    int64 id = 1;
}


// Product 定义服务可以完成的操作,这些方法需要在服务端实现,以便于客户端调用时能够响应。
service Product {
    // ProductInfo 是一个rpc方法,请求参数类型是ProductRequest,响应参数类型为ProductResponse
    rpc ProductInfo (ProductRequest) returns (ProductResponse) {}

    // 可以继续定义其他的服务操作,gRPC的核心是帮助咱们通信,帮助我们去标准化,结构化消息,最终的业务逻辑和gRPC无关。
}

3.使用protoc工具编译生成go代码

localhost:grpc yinzhengjie$ pwd
/Users/yinzhengjie/golang/gosubjects/src/gocode/grpc
localhost:grpc yinzhengjie$ 
localhost:grpc yinzhengjie$ ls -R
01-product

./01-product:
product.proto
localhost:grpc yinzhengjie$ 
localhost:grpc yinzhengjie$ protoc --go_out=./01-product/ --go-grpc_out=./01-product/ ./01-product/product.proto 
localhost:grpc yinzhengjie$ 
localhost:grpc yinzhengjie$ ls -R
01-product

./01-product:
product.proto           proto-compiles-readonly

./01-product/proto-compiles-readonly:
product.pb.go           product_grpc.pb.go
localhost:grpc yinzhengjie$ 


温馨提示:
    - 1.protoc相关参数说明:
        --go_out
            会自动生成一个名为"*pb.go"文件,包含消息类型的定义和操作相关代码。

        --go-grpc_out
            会自动生成一个名为"*_grpc.pb.go"文件,包含客户端和服务端的相关代码。

    - 2.生成的代码主要是结构上的封装,在继续使用时,还需要继续充实业务逻辑;
    - 3.protoc生成的代码一般情况下不需要修改,如果的确有修改的需求,则需要重新结构体,重写对应的方法即可;
    - 4.如果修改了proto文件,则需要重新编译生成对应心得go代码;

三.基于gRPC的服务间通信示例

1.gRPC程序架构设计说明

定义两个服务,订单服务和产品服务,要求如下:
    - 1.订单服务提供HTTP接口,用于完成订单查询,订单中包含产品信息,要利用grpc从产品服务获取产品信息;
    - 2.产品服务提供grpc接口,用于响应微服务内部产品信息查询;

如上图所示,对于grpc来说,产品服务为服务端,订单服务为客户端,而订单服务需要给用户提供http接口。

同时不考虑其他业务逻辑,例如产品服务也需要对外提供http接口等,仅在乎grpc等通信示例,同时也不考虑服务发现和网关等功能。

2.编写产品服务

2.1 编写proto文件并编译go代码

略,此过程直接参考上面的案例即可。我的目录组织结构如上图所示。

说白了,就是基于之前的案例继续操作即可。

2.2 生成go.mod文件并导入依赖包

    1.生成go.mod文件
localhost:01-product yinzhengjie$ pwd
/Users/yinzhengjie/golang/gosubjects/src/gocode/grpc/01-product
localhost:01-product yinzhengjie$
localhost:01-product yinzhengjie$ go mod init yinzhengjie-grpc
go: creating new go.mod: module yinzhengjie-grpc
go: to add module requirements and sums:
        go mod tidy
localhost:01-product yinzhengjie$ 
localhost:01-product yinzhengjie$ ls -R
go.mod                  product.proto           proto-compiles-readonly

./proto-compiles-readonly:
product.pb.go           product_grpc.pb.go
localhost:01-product yinzhengjie$  

    2.导入依赖包
localhost:01-product yinzhengjie$ go mod tidy

2.3 编写gRPC服务端代码

package main

import (
    "context"
    "flag"
    "fmt"
    "log"
    "net"
    proto_compiles_readonly "yinzhengjie-grpc/proto-compiles-readonly"

    "google.golang.org/grpc"
)

var (
    port = flag.Int("port", 9150, "The gRPC Server Port")
)

// ProductServer 对rpc生成Go代码的方法进行重写,实现完整的业务逻辑。
type ProductServer struct {
    proto_compiles_readonly.UnimplementedProductServer
}

// ProductInfo 对rpc生成Go代码的方法进行重写,实现完整的业务逻辑。
func (ProductServer) ProductInfo(context.Context, *proto_compiles_readonly.ProductRequest) (*proto_compiles_readonly.ProductResponse, error) {
    // 假设data是我们查询到数据
    data := proto_compiles_readonly.ProductResponse{
        Id:     18,
        Name:   "尹正杰 Go K8S 云原生,博客地址: https://www.cnblogs.com/yinzhengjie",
        IsSale: true,
    }

    // 返回查询到的数据
    return &data, nil
}

func main() {
    flag.Parse()

    // 设置TCP的监听地址
    listener, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", *port))
    if err != nil {
        log.Fatalln(err)
    }

    // 新建(实例化)grpc的服务器
    gRpcServer := grpc.NewServer()

    // 将咱们重写后的产品服务(ProductServer)注册到grpc服务端。
    proto_compiles_readonly.RegisterProductServer(gRpcServer, &ProductServer{})

    // 启动监听服务
    log.Printf("gRPC Server is listening on %s.\n", listener.Addr())
    if err := gRpcServer.Serve(listener); err != nil {
        log.Fatalf("err = %v\n", err)
    }

}

2.4 启动服务端测试

如上图所示,启动产品服务的服务端运行测试,如果能够监听地址成功说明是没问题的哟~

localhost:01-product yinzhengjie$ go run grpcService.go 
2024/08/09 00:01:05 gRPC Server is listening on 127.0.0.1:9150.

3.编写订单服务

3.1 编写proto文件并编译go代码

略,此步骤和编写产品的服务的步骤一致,因为gRPC调用时服务端和客户端都需要有proto的代码。

localhost:grpc yinzhengjie$ protoc --go_out=./02-order/ --go-grpc_out=./02-order/ ./02-order/product.proto 
localhost:grpc yinzhengjie$ 
localhost:grpc yinzhengjie$ pwd
/Users/yinzhengjie/golang/gosubjects/src/gocode/grpc
localhost:grpc yinzhengjie$ 
localhost:grpc yinzhengjie$ ls -R
01-product      02-order

./01-product:
go.mod                  go.sum                  grpcService.go          product.proto           proto-compiles-readonly

./01-product/proto-compiles-readonly:
product.pb.go           product_grpc.pb.go

./02-order:
product.proto           proto-compiles-readonly

./02-order/proto-compiles-readonly:
product.pb.go           product_grpc.pb.go
localhost:grpc yinzhengjie$

3.2 生成go.mod文件并导入依赖包

    1.生成go.mod文件
localhost:02-order yinzhengjie$ pwd
/Users/yinzhengjie/golang/gosubjects/src/gocode/grpc/02-order
localhost:02-order yinzhengjie$ 
localhost:02-order yinzhengjie$ go mod init yinzhengjie-order
go: creating new go.mod: module yinzhengjie-order
go: to add module requirements and sums:
        go mod tidy
localhost:02-order yinzhengjie$ 
localhost:02-order yinzhengjie$ ls -R
go.mod                  product.proto           proto-compiles-readonly

./proto-compiles-readonly:
product.pb.go           product_grpc.pb.go
localhost:02-order yinzhengjie$ 

    2.导入依赖包
localhost:02-order yinzhengjie$ go mod tidy

3.3 编写http服务端代码

package main

import (
    "context"
    "encoding/json"
    "flag"
    "fmt"
    "log"
    "net/http"
    "time"
    proto_compiles_readonly "yinzhengjie-order/proto-compiles-readonly"

    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
)

var (
    gRPCAddr = flag.String("grpc", "127.0.0.1:9150", "gRPC Server Address")
    addr     = flag.String("addr", "127.0.0.1", "web server's listen Address. Default: 127.0.0.1")
    port     = flag.Int("port", 8080, "web server's listen Port. Default: 8080 ")
)

// OrderHandle 完成gRPC的客户端请求
func OrderHandle(write http.ResponseWriter, request *http.Request) {

    // 1.连接到gRPC服务端
    conn, err := grpc.Dial(*gRPCAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
    // grpc.NewClient()
    if err != nil {
        log.Fatalln(err)
    }
    defer conn.Close()

    // 2.实例化gRPC客户端
    client := proto_compiles_readonly.NewProductClient(conn)

    // 3.定义上下文环境,超时机制,若超过3秒就认为超时,将来取消调用
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    // 4.开始远程过程调用,客户端携带当前上下文环境以及请求到数据类型
    // 注意:ProductInfo的业务逻辑是在product服务端定义的,客户端只是调用而已。
    response, err := client.ProductInfo(ctx, &proto_compiles_readonly.ProductRequest{
        Id: 18,
    })
    if err != nil {
        log.Fatalln(err)
    }

    // 5.构建http的响应
    data := struct {
        ID       int64                                      `json:"id"`
        Auther   string                                     `json:"Auther`
        Products []*proto_compiles_readonly.ProductResponse `json:"products"`
    }{
        1001, "Jason Yin", []*proto_compiles_readonly.ProductResponse{
            response,
        },
    }

    dataJson, err := json.Marshal(data)
    if err != nil {
        log.Fatalln(err)
    }

    // 6.响应http的客户端
    write.Header().Set("Context-Type", "application/json")

    if _, err := fmt.Fprintf(write, string(dataJson)); err != nil {
        log.Fatalln(err)
    }
}

func main() {

    // 定义业务逻辑服务,假设为订单服务
    service := http.NewServeMux()

    // 当用户访问订单服务时,再去触发调用gRPC客户端请求
    service.HandleFunc("/order", OrderHandle)

    // 启动httpServer监听
    address := fmt.Sprintf("%s:%d", *addr, *port)
    fmt.Printf("订单服务(Order Service) 监听地址: %s\n", address)
    log.Fatalln(http.ListenAndServe(address, service))
}

3.4 启动服务端测试

    1.启动服务
localhost:02-order yinzhengjie$ go run httpServer.go 
订单服务(Order Service) 监听地址: 127.0.0.1:8080

    2.访问测试
如上图所示。
目录
相关文章
|
12天前
|
Go
Golang语言之管道channel快速入门篇
这篇文章是关于Go语言中管道(channel)的快速入门教程,涵盖了管道的基本使用、有缓冲和无缓冲管道的区别、管道的关闭、遍历、协程和管道的协同工作、单向通道的使用以及select多路复用的详细案例和解释。
43 4
Golang语言之管道channel快速入门篇
|
12天前
|
Go
Golang语言文件操作快速入门篇
这篇文章是关于Go语言文件操作快速入门的教程,涵盖了文件的读取、写入、复制操作以及使用标准库中的ioutil、bufio、os等包进行文件操作的详细案例。
36 4
Golang语言文件操作快速入门篇
|
12天前
|
安全 Go
Golang语言goroutine协程并发安全及锁机制
这篇文章是关于Go语言中多协程操作同一数据问题、互斥锁Mutex和读写互斥锁RWMutex的详细介绍及使用案例,涵盖了如何使用这些同步原语来解决并发访问共享资源时的数据安全问题。
36 4
|
12天前
|
Go 调度
Golang语言goroutine协程篇
这篇文章是关于Go语言goroutine协程的详细教程,涵盖了并发编程的常见术语、goroutine的创建和调度、使用sync.WaitGroup控制协程退出以及如何通过GOMAXPROCS设置程序并发时占用的CPU逻辑核心数。
23 4
Golang语言goroutine协程篇
|
12天前
|
Prometheus Cloud Native Go
Golang语言之Prometheus的日志模块使用案例
这篇文章是关于如何在Golang语言项目中使用Prometheus的日志模块的案例,包括源代码编写、编译和测试步骤。
20 3
Golang语言之Prometheus的日志模块使用案例
|
12天前
|
Go
Golang语言错误处理机制
这篇文章是关于Golang语言错误处理机制的教程,介绍了使用defer结合recover捕获错误、基于errors.New自定义错误以及使用panic抛出自定义错误的方法。
35 3
|
12天前
|
Go
Golang语言之函数(func)进阶篇
这篇文章是关于Golang语言中函数高级用法的教程,涵盖了初始化函数、匿名函数、闭包函数、高阶函数、defer关键字以及系统函数的使用和案例。
16 3
Golang语言之函数(func)进阶篇
|
12天前
|
Go
Golang语言之函数(func)基础篇
这篇文章深入讲解了Golang语言中函数的定义和使用,包括函数的引入原因、使用细节、定义语法,并通过多个案例展示了如何定义不返回任何参数、返回一个或多个参数、返回值命名、可变参数的函数,同时探讨了函数默认值传递、指针传递、函数作为变量和参数、自定义数据类型以及返回值为切片类型的函数。
17 2
Golang语言之函数(func)基础篇
|
12天前
|
Go
Golang语言之映射(map)快速入门篇
这篇文章是关于Go语言中映射(map)的快速入门教程,涵盖了map的定义、创建方式、基本操作如增删改查、遍历、嵌套map的使用以及相关练习题。
21 5
|
12天前
|
Go
Golang语言之切片(slice)快速入门篇
这篇文章是关于Go语言中切片(slice)的快速入门教程,详细介绍了切片的概念、定义方式、遍历、扩容机制、使用注意事项以及相关练习题。
21 5