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
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