gRPC 的使用

2023-12-18 15:24:57 浏览数 (3)

前言

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

安装

  1. 首先要安装Go
  2. HOME/.local/bin) export PATH="
  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
    • 当数据集中的消息对象可能非常大时,最好是使用流式处理请求或响应传输这些对象。

例子

  • 安装完成之后可以跟着官网的例子学习一下
    • https://grpc.io/docs/languages/go/quickstart/
    • https://grpc.io/docs/what-is-grpc/core-concepts/

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

  • 初始化项目
代码语言:javascript复制
mkdir grpc-demo && cd grpc-demo
go mod init github.com/seth-shi/grpc-demo
  • go.mod
代码语言:javascript复制
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
代码语言:javascript复制
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
代码语言:javascript复制
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
代码语言:javascript复制
package enums

const (
	HttpHost = ":8080"
	GoodsHost = ":8888"
	OrderHost = ":9999"
)
  • goods/main.go
代码语言:javascript复制
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
代码语言:javascript复制
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
代码语言:javascript复制
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服务查询
  • 启动三个服务
代码语言:javascript复制
go run goods/main.go
go run order/main.go
go run api/main.go
  • 运行结果
代码语言:javascript复制
# 获取用户为 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的话, 网络问题很多, 直接把所有服务都到宿主机运行

1 人点赞