1. 程式人生 > 其它 >GO-GRPC實踐(一) 完成第一個GRPC介面並使用etcd作為服務註冊和發現

GO-GRPC實踐(一) 完成第一個GRPC介面並使用etcd作為服務註冊和發現

https://me1onrind.github.io/2021/06/06/grpc_one/

demo程式碼地址

https://github.com/Me1onRind/go-demo

環境搭建

go

go 1.13 以上

需安裝的二進位制檔案

可執行檔名 安裝方式 作用
protoc https://github.com/protocolbuffers/protobuf/releases 下載安裝 將.proto檔案編譯為具體程式語言的檔案
protoc-gen-go go get github.com/golang/protobuf/[email protected] 將.proto檔案編譯為go檔案時需要的外掛

使用etcd作為服務註冊中心

https://github.com/etcd-io/etcd

非生產環境在本地單機部署或者使用docker執行即可

docker-compose.yml

https://github.com/Me1onRind/my_docker/blob/master/etcd/docker-compose.yml

編寫go服務端程式碼

專案目錄

./
├── cmd
│ └── grpc
│     ├── client_test.go # 測試檔案
│     └── main.go # main檔案
├── go.mod
├── go.sum
├── internal
│ ├── controller # grpc介面實現
│ │ └── foo_controller.go
│ ├── core
│ │ └── register # 服務註冊實現
│ │     └── etcd.go
├── protobuf # proto原型檔案和編譯後的檔案
│ ├── build.sh
│ ├── foo.proto
│ └── pb
│     └── foo.pb.go
└── README.md

程式碼

greet.proto

syntax = "proto3";

package pb;

service Foo {  
    rpc Greet(GreetReq) returns (GreetResp);
}

message GreetReq {
    string my_name = 1;
    string msg = 2;
}

message GreetResp {
    string msg = 1;
}
編譯proto檔案生成go程式碼
[root@debian go-demo]# protoc --proto_path=./:  --go_out=plugins=grpc:./pb/. ./*.proto

為了避免每次都要輸一串命令(還有其他用處), 將編譯命令寫在shell腳本里

build.sh
#!/bin/bash
set -ue
cd `dirname $0`
protoc --proto_path=./:  --go_out=plugins=grpc:./pb/. ./*.proto

之後更新proto檔案後只需執行

[root@debian go-demo]# sh protobuf/build.sh 

foo_controller.go

實現定義的Foo介面

package controller

import (
    "context"
    "fmt"

    "github.com/Me1onRind/go-demo/protobuf/pb"
)

type FooController struct {
}

func NewFooController() *FooController {
    f := &FooController{}
    return f
}

func (f *FooController) Greet(ctx context.Context, in *pb.GreetReq) (*pb.GreetResp, error) {
    reply := fmt.Sprintf("Hello %s, I got your msg:%s", in.GetMyName(), in.GetMsg())
    out := &pb.GreetResp{}
    out.Msg = reply
    return out, nil
}

etcd.go

服務註冊功能

package register

import (
    "context"
    "fmt"
    "log"
    "time"

    uuid "github.com/satori/go.uuid"
    clientv3 "go.etcd.io/etcd/client/v3"
    "go.etcd.io/etcd/client/v3/naming/endpoints"
)

var client *clientv3.Client

const (
    prefix = "service"
)

func init() {
    var err error 
    client, err = clientv3.New(clientv3.Config{
        Endpoints:   []string{"localhost:2379"},
        DialTimeout: 5 * time.Second,
    }) 
    if err != nil {
        panic(err)
    }
}

func Register(ctx context.Context, serviceName, addr string) error {
    lease := clientv3.NewLease(client)
    cancelCtx, cancel := context.WithTimeout(ctx, time.Second*3	)
    defer cancel()
    leaseResp, err := lease.Grant(cancelCtx, 3)
    if err != nil {
        return err
    }   


    leaseChannel, err := lease.KeepAlive(context.Background(), leaseResp.ID) // 長連結, 不用設定超時時間
    if err != nil {
        return err 
    }   

    em, err := endpoints.NewManager(client, prefix)
    if err != nil {
        return err
    }

    cancelCtx, cancel = context.WithTimeout(ctx, time.Second*3)
    defer cancel()
    if err := em.AddEndpoint(cancelCtx, fmt.Sprintf("%s/%s/%s", prefix, serviceName, uuid.NewV4().String()), endpoints.Endpoint{
        Addr: addr,
    }, clientv3.WithLease(leaseResp.ID)); err != nil {
        return err
    }

    go func() {
        for {
            select {
            case resp := <-leaseChannel:
                if resp != nil {
                    //log.Println("keep alive success.")
                } else {
                    time.Sleep(time.Second)
                    log.Println("keep alive failed.")
                }
            case <-ctx.Done():
                log.Println("close service register")

                cancelCtx, cancel = context.WithTimeout(ctx, time.Second*3)
                defer cancel()
                em.DeleteEndpoint(cancelCtx, serviceName)

                lease.Close()
                client.Close()
                return
            }
        }
    }()

    return nil
}

main.go

package main
    
import (
    "context"
    "fmt"
    "log"
    "net"

    "github.com/Me1onRind/go-demo/internal/controller"
    "github.com/Me1onRind/go-demo/internal/core/register"
    "github.com/Me1onRind/go-demo/protobuf/pb"

    "google.golang.org/grpc"
)

func registerService(s *grpc.Server) {
    pb.RegisterFooServer(s, controller.NewFooController())
}   
    
func main() {
    addr := "127.0.0.1:8080"
    ctx := context.Background()

    lis, err := net.Listen("tcp", addr)
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    s := grpc.NewServer()
    registerService(s)

    if err := register.Register(ctx, "go-demo", addr); err != nil { // 服務註冊名: go-demo
        log.Fatalf("register %s failed:%v", "go-demo", err)
    }

    fmt.Printf("start grpc server:%s", addr)
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

通過服務發現呼叫Foo.Greet方法

client_test.go

package main
                               
import (
    "context"
    "testing"                  
    "time"
    
    "github.com/Me1onRind/go-demo/protobuf/pb"
    "go.etcd.io/etcd/client/v3"
    "go.etcd.io/etcd/client/v3/naming/resolver"
    "google.golang.org/grpc"   
) 

    
func Test_Greet(t *testing.T) {
  
    cli, err := clientv3.NewFromURL("http://localhost:2379")
    if err != nil {            
        t.Fatal(err)           
    }
    builder, err := resolver.NewBuilder(cli) 
    if err != nil {
        t.Fatal(err)
    }
    conn, err := grpc.Dial("etcd:///service/go-demo",
        grpc.WithResolvers(builder),    
        grpc.WithBalancerName("round_robin"),
        grpc.WithInsecure(), grpc.WithTimeout(time.Second))
    if err != nil {
        t.Fatal(err)
    }

    fooClient := pb.NewFooClient(conn)
    ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
    defer cancel()
    resp, err := fooClient.Greet(ctx, &pb.GreetReq{
        MyName: "Bar",
        Msg:    "Hello, World",
    })
    if err != nil {
        t.Fatal(err)
    }

    t.Log(resp.Msg)
}

驗證

啟動server

[root@debian go-demo]# go run cmd/grpc/main.go 
start grpc server:127.0.0.1:8080	

可以使用etcd命令列客戶端/UI客戶端看到, 服務已經註冊上去

客戶端呼叫

=== RUN   Test_Greet
    client_test.go:43: Hello Bar, I got your msg:Hello, World
-- PASS: Test_Greet (0.00s)
PASS
ok      github.com/Me1onRind/go-demo/cmd/grpc   0.010s