• 初衷:想用go开发一个类Ansible运维工具

    • Ansible性能较差并且太老派了不想用

    • 调研了一下agent端基本要从json-rpc和grpc中选型

      • 学都学了,就学个主流新潮的

八股文

什么是gRPC?

  • 想象一下,你和基友在不同的密室里,你们想要聊天并交换小秘密。这时候,你们需要一个高效、可靠的通信方式摩斯密码。gRPC就是这样一种让服务之间像朋友一样轻松交流的工具。

  • gRPC(Google Remote Procedure Call)是谷歌推出的高性能通信框架。它使用HTTP/2作为传输协议,Protobuf作为数据序列化方式。换句话说,gRPC让你的服务能够快速、可靠地互相打电话,还能彼此理解复杂的“悄悄话”。

gRPC的QWER技能

  • 极速传输:gRPC搭载HTTP/2这辆跑车,数据传输快如闪电,让你几乎感觉不到延迟。

  • 多语言支持:无论你是用Java、Python、Go,还是其他语言编写的服务,gRPC都能让它们无障碍交流。

  • 代码自动生成:只需定义一次接口,gRPC就能帮你自动生成客户端和服务端代码,减少重复劳动。

  • 流式传输:像抖音直播一样,支持双向数据流,让通信变得更加实时、顺畅。

什么是gRPC-Go?

gRPC-Go是gRPC的Go语言实现版本。它是Go开发者的秘密武器,让你在使用Go语言时,也能享受到gRPC带来的各种超能力。通过gRPC-Go,你可以快速构建高性能、可扩展的服务,让你的分布式系统像精密的机器一样高效运转。

gRPC-Go的魅力

  • 简单上手:通过Protobuf定义接口,gRPC-Go帮你生成所有需要的代码,让你省去很多麻烦。

  • 高效可靠:基于HTTP/2和Protobuf,gRPC-Go让你的服务通信又快又稳。

  • 生态系统强大:与Go的标准库和社区库完美融合,让你可以轻松扩展和集成各种功能。

上手

参考一下官方示例
https://github.com/grpc/grpc-go/blob/master/examples/helloworld/helloworld/helloworld.proto

关键字

  • syntax:是必须写的,而且要定义在第一行;目前proto3是主流(引入了许多改进和简化),不写默认使用proto2

  • option go_package:定义生成的pb.go的包名,我们通常在proto文件中定义。如果不在proto文件中定义,也可以在使用protoc生成代码时指定pb.go文件的包名

  • package:定义我们proto文件的包名

  • service: 服务是一组方法的集合。定义了客户端可以调用的远程过程。

  • message:非常重要,用于定义消息结构体

分配标识号

string name =1; 这个1是什么?

这些数字是“分配表示号”:在消息定义中,每个字段后面都有一个唯一的数字,序号用于标识字段在序列化和反序列化中的顺序和唯一性。这个就是标识号。

小技巧:[1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留 [1,15]之内的标识号。

所以这里也可以保留一些标识号,留给以后用。举个🌰

message HelloRequest {
  reserved 2, 5, 7 to 10; // 保留2,5,7到10这些标识号
}

如果使用了这些保留的标识号,protocol buffer编译器无法编译通过,将会输出警告信息。

创建grpc demo项目

配置环境

# Mac的话这里直接用homebrew安装
brew install protobuf

# 初始化项目
go mod init grpc

# protoc-gen-go 主要用来生成 go代码
# protoc-gen-go-grpc 生成grpc代码
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

# 配置一下环境变量到zshrc或bashrc,看你终端用的啥
echo 'export PATH=$PATH:$(go env GOPATH)/bin' >> ~/.zshrc
source ~/.zshrc

编写hello.proto

syntax = "proto3";

option go_package = "./;hello";

package hello;

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

生成go代码

protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative *.proto


tree
.
├── go.mod
├── go.sum
├── hello.pb.go
├── hello.proto
└── hello_grpc.pb.go

  • hello.pb.go: 处理消息类型的定义和序列化/反序列化,主要用于消息的创建、操作和传输。

    • 这个文件主要负责 Protocol Buffers(protobuf)的消息类型定义和序列化/反序列化逻辑。

      • 当你需要创建、读取、更新、或删除消息类型的实例时,你会使用到这个文件。

      • 任何涉及到消息类型的序列化(发送数据)或反序列化(接收数据)时,也会使用到这个文件。

  • hello_grpc.pb.go: 处理 gRPC 服务和客户端的接口和基础实现,主要用于定义服务端和客户端的行为以及服务注册。

    • 实现gRPC服务端喝客户端远程调用,以及服务注册到gRPC时使用

编写一下服务端与客户端

服务端

项目内创建server/main.go

package main

import (
	"context"
	"flag"
	"fmt"
	"log"
	"net"

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

var (
	port = flag.Int("port", 50051, "The server port")
)

// server is used to implement helloworld.GreeterServer.
type server struct {
	pb.UnimplementedGreeterServer
}

// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
	log.Printf("Received: %v", in.GetName())
	return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}

func main() {
	flag.Parse()
	lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}
	s := grpc.NewServer()
	pb.RegisterGreeterServer(s, &server{})
	log.Printf("server listening at %v", lis.Addr())
	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

客户端

项目内创建client/main.go

/*
 *
 * Copyright 2015 gRPC authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

// Package main implements a client for Greeter service.
package main

import (
    "context"
    "flag"
    "log"
    "time"

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

const (
    defaultName = "world"
)

var (
    addr = flag.String("addr", "localhost:50051", "the address to connect to")
    name = flag.String("name", defaultName, "Name to greet")
)

func main() {
    flag.Parse()
    // Set up a connection to the server.
    conn, err := grpc.NewClient(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil {
       log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()
    c := pb.NewGreeterClient(conn)

    // Contact the server and print out its response.
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()
    r, err := c.SayHello(ctx, &pb.HelloRequest{Name: *name})
    if err != nil {
       log.Fatalf("could not greet: %v", err)
    }
    log.Printf("Greeting: %s", r.GetMessage())
}

通信一下

go run server/main.go
2024/06/13 15:47:08 server listening at [::]:50051
2024/06/13 15:48:34 Received: world


go run client/main.go 
2024/06/13 15:48:34 Greeting: Hello world