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/protoc-gen-go@v1.3.3 将.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
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 {
    log.Println("Try register to etcd ...")
    // 创建一个租约
    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(ctx, 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
    }
    log.Println("Register etcd success")

    del := func() {
        log.Println("Register close")

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

        lease.Close()
    }
    // 保持注册状态(连接断开重连)
    keepRegister(ctx, leaseChannel, del, serviceName, addr)

    return nil
}

func keepRegister(ctx context.Context, leaseChannel <-chan *clientv3.LeaseKeepAliveResponse, cleanFunc func(), serviceName, addr string) {
    go func() {
        failedCount := 0
        for {
            select {
            case resp := <-leaseChannel:
                if resp != nil {
                    //log.Println("keep alive success.")
                } else {
                    log.Println("keep alive failed.")
                    failedCount++
                    for failedCount > 3 {
                        cleanFunc()
                        if err := Register(ctx, serviceName, addr); err != nil {
                            time.Sleep(time.Second)
                            continue
                        }
                        return
                    }
                    continue
                }
            case <-ctx.Done():
                cleanFunc()
                client.Close()
                return
            }
        }
    }()
}

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
posted @ 2021-06-12 19:04  Me1onRind  阅读(685)  评论(0编辑  收藏  举报