Featured image of post gRPC 的使用

gRPC 的使用

在 gRPC 中,客户端应用程序可以直接调用不同机器上的服务器应用程序上的方法,就像它是本地对象一样,使您更容易创建分布式应用程序和服务。

前言

  • 网上有很多的安装使用教程, 由于gRPC的更新, 很多命令都是使用不了, 现在写的这篇文章也只是针对当前
  • 如果发现用不了, 最好的办法还是参考官方文档

安装

  1. 首先要安装Go
  2. 安装protoc编译器 https://grpc.io/docs/protoc-installation/
    • protoc编译器用于编译.proto包含服务和消息定义的文件
    • 下载和你的操作系统和计算机体系结构(protoc-<version>-<os><arch>.zip)对应的 zip 文件(比如我的是WSL系统, 下载了protoc-3.19.4-linux-x86_64.zip)
    • 解压下载好的压缩包(注意不同版本名字)
      • unzip protoc-3.19.4-linux-x86_64.zip -d $HOME/.local
    • 加入环境变量
      • vim ~/.bashrc 然后在最后加($HOME/.local/bin)
      • export PATH="$PATH:$HOME/.local/bin"
      • source ~/.bashrc 刷新环境变量
    • 执行protoc --version如果出现版本就代表安装成功
  3. 安装Go的插件
    • protoc编译器需本身不能生成Go代码, 需要安装此插件来生成Go代码
      • go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.26
    • gRPC代码生成器插件(注: 之前包含在protoc-gen-go)
      • go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.1

介绍

  • gRPC允许您定义四种服务方法:
    • 一元RPC:客户端向服务器发送单个请求并获得单个响应,就像正常的函数调用一样。
    • 服务端流式:客户端发送请求到服务器,拿到一个流去读取返回的消息序列。 客户端读取返回的流,直到里面没有任何消息。
    • 客户端流式:与服务端数据流模式相反,客户端源源不断的向服务端发送数据流,而在发送结束后,由服务端返回一个响应。
    • 双向流式:双方使用读写流去发送一个消息序列,两个流独立操作,双方可以同时发送和同时接收。
  • 流式其实很像数组, 只不过流只能被动的读取(等待对端推送数据过来), 像在Dart中流一般是通过订阅一个回调去拿到里面的所有数据
  • gRPC 中有一个关键字repeated用来声明数组, 所以纠结用stream还是repeated作为集合的返回
    • 可以参考微软的回答: gRPC 流式处理服务与重复字段
    • 对于任何大小受限且能在短时间内(例如在一秒钟之内)全部生成的数据集就用repeated
    • 当数据集中的消息对象可能非常大时,最好是使用流式处理请求或响应传输这些对象。

例子

一个用户订单的RPC服务例子

  • 初始化项目
mkdir grpc-demo && cd grpc-demo
go mod init github.com/seth-shi/grpc-demo
  • go.mod
module github.com/seth-shi/grpc-demo

go 1.17

require (
	github.com/gin-gonic/gin v1.7.7
	google.golang.org/grpc v1.44.0
	google.golang.org/protobuf v1.27.1
)

require (
	github.com/gin-contrib/sse v0.1.0 // indirect
	github.com/go-playground/locales v0.14.0 // indirect
	github.com/go-playground/universal-translator v0.18.0 // indirect
	github.com/go-playground/validator/v10 v10.10.0 // indirect
	github.com/golang/protobuf v1.5.2 // indirect
	github.com/json-iterator/go v1.1.12 // indirect
	github.com/leodido/go-urn v1.2.1 // indirect
	github.com/mattn/go-isatty v0.0.14 // indirect
	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
	github.com/modern-go/reflect2 v1.0.2 // indirect
	github.com/ugorji/go/codec v1.2.6 // indirect
	golang.org/x/crypto v0.0.0-20220214200702-86341886e292 // indirect
	golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect
	golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7 // indirect
	golang.org/x/text v0.3.7 // indirect
	google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect
	gopkg.in/yaml.v2 v2.4.0 // indirect
)

  • pb/goods.proto
syntax = "proto3";

package pb;

option go_package="github.com/seth-shi/grpc-demo/pb";

service Goods {
    rpc Show(GoodsShowRequest) returns (GoodsData);
}

message GoodsShowRequest {
    int64 id = 1;
}

message GoodsData {
    int64 id = 1;
    string name = 2;
    double amount = 3;
}
  • pb/order.proto
syntax = "proto3";

package pb;

option go_package="github.com/seth-shi/grpc-demo/pb";

service Order {
    rpc Index(OrderIndexRequest) returns (OrderIndexResponse);
    rpc Store(OrderStoreRequest) returns (OrderData);
}

message OrderIndexRequest {
    int64 userId = 1;
}

message OrderIndexResponse {
    repeated OrderData data = 1;
}

message OrderStoreRequest {
    int64 goodsId = 1;
    int64 goodsNumber = 2;
    int64 userId = 3;
    double Amount = 4;
    string goodsName = 5;
}


message OrderData {
    int64 no = 1;
    double amount = 2;
    int64 number = 3;
    int64 goodsId = 4;
    string goodsName = 5;
}
  • 执行protoc命令生成Go文件

    • protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative pb/*.proto
  • 执行命令对依赖进行更新

    • go mod tidy
  • enums/host.go

package enums

const (
	HttpHost = ":8080"
	GoodsHost = ":8888"
	OrderHost = ":9999"
)
  • goods/main.go
package main

import (
	"context"
	"errors"
	"github.com/seth-shi/grpc-demo/enums"
	"github.com/seth-shi/grpc-demo/pb"
	"google.golang.org/grpc"
	"log"
	"net"
)

type server struct {
	pb.UnimplementedGoodsServer

	// 为了简单, 当住数据库
	data map[int64]*pb.GoodsData
}

func newGoodsServer() *server {
	return &server{data: map[int64]*pb.GoodsData{
		1: {
			Id:     1,
			Name:   "橘子",
			Amount: 9.9,
		},
		2: {
			Id:     2,
			Name:   "香蕉",
			Amount: 8.8,
		},
	}}
}

func (s *server) Show(ctx context.Context, in *pb.GoodsShowRequest) (*pb.GoodsData, error) {

	u, e := s.data[in.Id]
	if !e {
		return nil, errors.New("无此商品")
	}

	return u, nil
}

func main() {

	lis, err := net.Listen("tcp", enums.GoodsHost)
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	s := grpc.NewServer()
	pb.RegisterGoodsServer(s, newGoodsServer())
	log.Printf("server listening at %v", lis.Addr())
	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

  • order/main.go
package main

import (
	"context"
	"github.com/seth-shi/grpc-demo/enums"
	"github.com/seth-shi/grpc-demo/pb"
	"google.golang.org/grpc"
	"log"
	"net"
	"sync"
)

type server struct {
	pb.UnimplementedOrderServer

	// 为了简单, 当住数据库
	data map[int64][]*pb.OrderData
	// 订单的自增 ID
	id int64
	sync.Mutex
}

func newOrderServer() *server {
	return &server{data: make(map[int64][]*pb.OrderData)}
}

func (s *server) Index(c context.Context, r *pb.OrderIndexRequest) (*pb.OrderIndexResponse, error) {

	return &pb.OrderIndexResponse{Data: s.data[r.UserId]}, nil
}

func (s *server) Store(c context.Context, r *pb.OrderStoreRequest) (*pb.OrderData, error) {
	s.Lock()
	defer s.Unlock()

	s.id++
	d := &pb.OrderData{
		No:        s.id,
		Amount:    r.Amount,
		Number:    r.GoodsNumber,
		GoodsId:   r.GoodsId,
		GoodsName: r.GoodsName,
	}

	s.data[r.UserId] = append(s.data[r.UserId], d)

	return d, nil
}

func main() {

	lis, err := net.Listen("tcp", enums.OrderHost)
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	s := grpc.NewServer()
	pb.RegisterOrderServer(s, newOrderServer())
	log.Printf("server listening at %v", lis.Addr())
	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}
  • api/main.go
package main

import (
	"github.com/gin-gonic/gin"
	"github.com/seth-shi/grpc-demo/enums"
	"github.com/seth-shi/grpc-demo/pb"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"log"
	"strconv"
)

var goodsClient pb.GoodsClient
var orderClient pb.OrderClient

func main() {

	goodsClient = createGoodsClient()
	orderClient = createOrderClient()

	// 注册 HTTP 路由
	r := gin.Default()
	r.GET("/orders", orderIndex)
	r.POST("/orders", createOrder)
	r.Run(enums.HttpHost)
}

func createGoodsClient() pb.GoodsClient {
	conn, err := grpc.Dial(enums.GoodsHost, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	return pb.NewGoodsClient(conn)
}

func createOrderClient() pb.OrderClient {
	conn, err := grpc.Dial(enums.OrderHost, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	return pb.NewOrderClient(conn)
}

type createOrderRequest struct {
	UserId  int64 `json:"user_id" form:"user_id" binding:"required"`
	GoodsId int64 `json:"goods_id" form:"goods_id" binding:"required"`
	Number  int64 `json:"number" form:"number" binding:"required"`
}

type createOrderResponse struct {
	No      int64   `json:"no"`
	Name    string  `json:"name"`
	Number  int64   `json:"number"`
	GoodsId int64   `json:"goods_id"`
	Amount  float64 `json:"amount"`
}

func createOrder(c *gin.Context) {

	// 获取输入的参数
	var req createOrderRequest
	if err := c.ShouldBind(&req); err != nil {
		c.JSON(200, gin.H{"code": 400, "msg": err.Error()})
		return
	}

	goods, err := goodsClient.Show(c.Request.Context(), &pb.GoodsShowRequest{Id: req.GoodsId})
	if err != nil {
		c.JSON(200, gin.H{"code": 400, "msg": err.Error()})
		return
	}

	res, err := orderClient.Store(c.Request.Context(), &pb.OrderStoreRequest{
		GoodsId:     goods.Id,
		UserId:      req.UserId,
		GoodsNumber: req.Number,
		GoodsName:   goods.Name,
		Amount:      float64(req.Number) * goods.Amount,
	})
	if err != nil {
		c.JSON(200, gin.H{"code": 400, "msg": err.Error()})
		return
	}

	c.JSON(200, gin.H{
		"code": 200,
		"data": createOrderResponse{
			No:      res.No,
			Name:    res.GoodsName,
			Number:  res.Number,
			Amount:  res.Amount,
			GoodsId: res.GoodsId,
		},
	})
}

func orderIndex(c *gin.Context) {

	userId, err := strconv.ParseInt(c.Query("user_id"), 10, 64)
	if err != nil {
		c.JSON(200, gin.H{"code": 400, "error": "无效的用户"})
		return
	}

	res, err := orderClient.Index(c.Request.Context(), &pb.OrderIndexRequest{UserId: userId})
	if err != nil {
		c.JSON(200, gin.H{"code": 400, "error": err.Error()})
		return
	}

	c.JSON(200, gin.H{
		"code": 200,
		"data": res.Data,
	})
}


  • 代码很简单, 有一个order服务, 一个goods服务, 还有一个向外暴露的api服务
  • 可以通过api服务创建订单, api服务实际调用ordergoods服务去生成订单
  • 也可以通过api服务查询已经创建的订单, api实际调用order服务查询
  • 启动三个服务
go run goods/main.go
go run order/main.go
go run api/main.go
  • 运行结果
# 获取用户为 1 的订单列表
curl --location --request GET '127.0.0.1:8080/orders?user_id=1'
## output
{
    "code": 200,
    "data": null
}

# 创建订单1
curl --location --request POST '127.0.0.1:8080/orders' \
--header 'Content-Type: application/json' \
--data-raw '{
    "user_id": 1,
    "goods_id": 1,
    "number": 1
}'
## output
{
    "code": 200,
    "data": {
        "no": 1,
        "name": "橘子",
        "number": 1,
        "goods_id": 1,
        "amount": 9.9
    }
}
# 创建订单2
curl --location --request POST '127.0.0.1:8080/orders' \
--header 'Content-Type: application/json' \
--data-raw '{
    "user_id": 1,
    "goods_id": 2,
    "number": 9
}'
## output
{
    "code": 200,
    "data": {
        "no": 2,
        "name": "香蕉",
        "number": 9,
        "goods_id": 2,
        "amount": 79.2
    }
}
# 创建订单3
curl --location --request POST '127.0.0.1:8080/orders' \
--header 'Content-Type: application/json' \
--data-raw '{
    "user_id": 1,
    "goods_id": 3,
    "number": 9
}'
## output
{
    "code": 400,
    "msg": "rpc error: code = Unknown desc = 无此商品"
}

# 查询订单1
curl --location --request GET '127.0.0.1:8080/orders?user_id=1'
## output
{
    "code": 200,
    "data": [
        {
            "no": 1,
            "amount": 9.9,
            "number": 1,
            "goodsId": 1,
            "goodsName": "橘子"
        },
        {
            "no": 2,
            "amount": 79.2,
            "number": 9,
            "goodsId": 2,
            "goodsName": "香蕉"
        }
    ]
}

错误

  • rpc error: code = Unavailable desc = connection error:
    • 如果出现上面这个错误, 如果是用WSL的话, 网络问题很多, 直接把所有服务都到宿主机运行
本作品采用知识共享署名 4.0 国际许可协议进行许可,转载时请注明原文链接,图片在使用时请保留全部内容,可适当缩放并在引用处附上图片所在的文章链接。