golang 学习笔记之 etcd protobuffer grpc gorm 服务注册发现 go-micro go-zero

1.etcd使用步骤

1)下载:https://github.com/etcd-io/etcd/releases/

2)配置环境变量

3)编辑local-cluster-profile文件:(利用goreman 启动方式,生产环境参考官方文档)

etcd1: etcd --name infra1 --listen-client-urls http://127.0.0.1:2379 --advertise-client-urls http://127.0.0.1:2379 --listen-peer-urls http://127.0.0.1:2380 --initial-advertise-peer-urls http://127.0.0.1:2380 --initial-cluster-token etcd-cluster-1 --initial-cluster infra1=http://127.0.0.1:2380,infra2=http://127.0.0.1:3380,infra3=http://127.0.0.1:4380 --initial-cluster-state new --enable-pprof --logger=zap --log-outputs=stderr
etcd2: etcd --name infra2 --listen-client-urls http://127.0.0.1:3379 --advertise-client-urls http://127.0.0.1:3379 --listen-peer-urls http://127.0.0.1:3380 --initial-advertise-peer-urls http://127.0.0.1:3380 --initial-cluster-token etcd-cluster-1 --initial-cluster infra1=http://127.0.0.1:2380,infra2=http://127.0.0.1:3380,infra3=http://127.0.0.1:4380 --initial-cluster-state new --enable-pprof --logger=zap --log-outputs=stderr
etcd3: etcd --name infra3 --listen-client-urls http://127.0.0.1:4379 --advertise-client-urls http://127.0.0.1:4379 --listen-peer-urls http://127.0.0.1:4380 --initial-advertise-peer-urls http://127.0.0.1:4380 --initial-cluster-token etcd-cluster-1 --initial-cluster infra1=http://127.0.0.1:2380,infra2=http://127.0.0.1:3380,infra3=http://127.0.0.1:4380 --initial-cluster-state new --enable-pprof --logger=zap --log-outputs=stderr

4)启动etcd集群:goreman -f local-cluster-profile start

2.protobuffer 使用步骤:

1)下载 https://github.com/protocolbuffers/protobuf/releases

2)配置环境变量

3)安装protoc-gen-go: go install github.com/golang/protobuf/protoc-gen-go@v1.3.2

4)新建项目proto,目录结构如下:

	proto/
		proto/
			userinfo.proto
		main.go

5)编辑userinfo.proto(代码如下):

// 注意;号
syntax = "proto3";
option go_package = "./userService;";
message userinfo{
    string username = 1;
    int32 age = 2;
    repeated string hobby = 3;
}

// 编译命令 cd proto && protoc --go_out=./ *.proto

6)回到项目根目录并且执行命令:go mod init proto && go mod tidy //自动安装上一步生成的pb.go中的依赖

7)编辑main.go (代码如下):

package main

import (
	"fmt"
	"proto/proto/userService"

	"google.golang.org/protobuf/proto"
)

func main() {
	u := &userService.Userinfo{
		Username: "lihh",
		Age:      31,
		Hobby:    []string{"a", "b", "c"},
	}
	fmt.Println(u)

	// proto 序列化
	data, _ := proto.Marshal(u)
	fmt.Println(data)

	// proto 反序列化
	user := userService.Userinfo{}
	proto.Unmarshal(data, &user)
	fmt.Println(&user, user.GetUsername())
}

附)生成grpc服务所需proto(示例代码如下,注意编译命令与上面不同):

syntax = "proto3";

option go_package = "./goodsService;";

// 定义rpc服务
service GoodsService {
    rpc AddGoods(AddGoodsReq) returns (AddGoodsRes);
    rpc GetGoods(GetGoodsReq) returns (GetGoodsRes);
}

message GoodsMode {
    string title = 1; 
    double price = 2;
    string content = 3;
}

// AddGoods 相关参数
message AddGoodsReq {
    GoodsMode params = 1;
}

message AddGoodsRes {
    string message = 1; 
    bool success = 2;
}

// GetGoods 相关参数
message GetGoodsReq {
    int32 id = 1;
}
message GetGoodsRes {
    repeated GoodsMode result = 1;
}

// 编译命令 protoc --go_out=plugins=grpc:./ *.proto

3.grpc

1)安装:go get -u -v google.golang.org/grpc

2)创建proto目录,生成proto:

目录结构:
	proto/
		greeter/
			greeter.pb.go
		greeter.proto
greeter.proto代码如下:

syntax = "proto3";
option go_package="./greeter";

service Greeter {
    rpc SayHello(HelloReq) returns (HelloRes);
}

message HelloReq {
    string name = 1;
}

message HelloRes {
    string message = 1;
}
// 编译命令 protoc --go_out=plugins=grpc:./ *.proto

3)server代码:

目录结构:
	greeter/
		proto/ (第2步构建的proto文件夹)
		main.go
main.go代码如下:

package main

import (
	"context"
	"fmt"
	"greeter/proto/greeter"
	"net"

	"google.golang.org/grpc"
)

// 1.定义远程调用的结构体喝方法 实现GreeterServer接口 interface
type Hello struct {
}

func (this *Hello) SayHello(c context.Context, req *greeter.HelloReq) (*greeter.HelloRes, error) {
	fmt.Println(req)

	return &greeter.HelloRes{
		Message: req.Name + ", hello",
	}, nil
}

func main() {
	// 1. 初始化 grpc对象
	grpcServer := grpc.NewServer()

	// 2. 注册服务
	greeter.RegisterGreeterServer(grpcServer, &Hello{})

	// 3. 监听端口
	listenner, err := net.Listen("tcp", "0.0.0.0:8001")
	if err != nil {
		fmt.Println(err)
	}

	// 4. 启动服务
	grpcServer.Serve(listenner)
}

执行命令:go mod init greeter && go mod tidy && go run main.go

4)client代码:

目录结构:
	greeter/
		proto/ (第2步构建的proto文件夹)
		main.go
main.go代码如下:

package main

import (
	"greeter/proto/greeter"
	"context"
	"fmt"

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

func main() {
	// 1. 建立连接
	/*
		credentials.NewClientTLSFromFile:从输入的证书文件中为客户端构造TLS凭证
		grpc.WithTransportCredentials:配置连接级别的安全凭证(例如,TLS/SSL),返回一个DialOption,用于连接服务器
	*/
	grpcClient, err := grpc.Dial("0.0.0.0:8001", grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		fmt.Println(err)
	}

	// 2. 注册客户端
	client := greeter.NewGreeterClient(grpcClient)
	res, err := client.SayHello(context.Background(), &greeter.HelloReq{
		Name: "lihh",
	})
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(res, res.GetMessage())

}

执行命令:go mod init greeter && go mod tidy && go run main.go

4.gorm 参考 https://gorm.io/zh_CN/docs/index.html

1)安装:

	go mod init gorm
	go get -u gorm.io/gorm
	go get -u gorm.io/driver/mysql

2)代码:

目录结构:
	models/
		core.go
		user.go
	main.go

1.core.go 
package models

import (
	"fmt"

	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

func InitGormDB() *gorm.DB {
	dbuser := "xxxx"
	dbpass := "xxxx"
	dbhost := "xxxx"
	dbport := "3306"
	dbname := "xxxx"
	dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=true&loc=Local", dbuser, dbpass, dbhost, dbport, dbname)
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		fmt.Println(err)
	}
	if db.Error != nil {
		fmt.Println(db.Error)
	}
	fmt.Println("数据库连接成功")
	return db
}

2.user.go
package models

/*
CREATE TABLE `test_user` (
 `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户基础信息表',
 `user_id` int(11) DEFAULT NULL COMMENT '用户短ID',
 `user_name` varchar(100) DEFAULT NULL COMMENT '昵称',
 PRIMARY KEY (`id`),
 UNIQUE KEY `user_id` (`user_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
*/
type User struct {
	Id       int64
	UserId   int64
	UserName string
}

func (User) TableName() string {
	return "test_user"
}
执行命令:go mod init gorm && go mod tidy
3.main.go
package main
import (
	"fmt"
	"gorm/models"
	"time"
)

func main() {
	db := models.InitGormDB()

	// 增
	user := models.User{
		UserName: "lihh",
		UserId:   time.Now().Unix(),
	}
	result := db.Create(&user)
	if result.Error != nil {
		fmt.Println(result.Error)
	}

	// 查
	userList := []models.User{}
	result = db.Find(&userList)
	if result.Error != nil {
		fmt.Println(result.Error)
	}
	fmt.Println(userList)
}

5.ETCD服务注册发现 https://godoc.org/github.com/coreos/etcd/clientv3

1)安装 go get github.com/coreos/etcd/clientv3

2) server 代码(服务注册):

结合第3点 grpc代码修改如下
/*
package main

import (
	"context"
	"fmt"
	"greeter/proto/greeter"
	"net"

	"google.golang.org/grpc"
)

// 1.定义远程调用的结构体喝方法 实现GreeterServer接口 interface
type Hello struct {
}

func (this *Hello) SayHello(c context.Context, req *greeter.HelloReq) (*greeter.HelloRes, error) {
	fmt.Println(req)

	return &greeter.HelloRes{
		Message: req.Name + ", hello",
	}, nil
}

func main() {
	// 1. 初始化 grpc对象
	grpcServer := grpc.NewServer()

	// 2. 注册服务
	greeter.RegisterGreeterServer(grpcServer, &Hello{})

	// 3. 监听端口
	listenner, err := net.Listen("tcp", "0.0.0.0:8001")
	if err != nil {
		fmt.Println(err)
	}

	// 4. 启动服务
	grpcServer.Serve(listenner)
}
*/

package main

import (
	"flag"
	"fmt"
	"greeter/proto/greeter"
	"net"
	"os"
	"os/signal"
	"strings"
	"syscall"
	"time"

	"github.com/coreos/etcd/clientv3"
	"golang.org/x/net/context"
	"google.golang.org/grpc"
)

/**
etcd是用go语言编写的key-value存储中间件组件,能保证多个节点数据的强一致性,适合存储重要数据,但不适合存储大量数据,比较适合做微服务注册中心及分布式锁等。

etcd做服务发现原理简单分析:监听服务注册的key,当对应的值发生变化时,通知grpc更新服务列表地址

etcd做服务注册原理简单分析:向etcd组件注册服务名称及地址,通过租约机制不断续约注册的key以保持服务的存活状态

参考地址:
etcd/clientv3 兼容问题解决办法 :
	etcd undefined: resolver.BuildOption ----- https://huangzhongde.cn/post/2020-03-02-etcd_undefined_resolver_buildoption/
	undefined: balancer.PickOptions ---- https://www.cnblogs.com/wanghaostec/p/15311365.html
代码实现:
	https://blog.csdn.net/kankan231/article/details/126212654


*/
var host = "0.0.0.0" //服务器主机
var (
	Port        = flag.Int("Port", 8001, "listening port")                                                   //服务器监听端口
	ServiceName = flag.String("ServiceName", "greet_service", "service name")                                //服务名称
	EtcdAddr    = flag.String("EtcdAddr", "0.0.0.0:2379;0.0.0.0:3379;0.0.0.0:4379", "register etcd address") //etcd的地址
)
var cli *clientv3.Client

//rpc服务接口
type Hello struct {
}

func (this *Hello) SayHello(c context.Context, req *greeter.HelloReq) (*greeter.HelloRes, error) {
	fmt.Println(req)

	return &greeter.HelloRes{
		Message: req.Name + ", hello",
	}, nil
}

//将服务地址注册到etcd中
func register(etcdAddr, serviceName, serverAddr string, ttl int64) error {
	var err error

	if cli == nil {
		//构建etcd client
		cli, err = clientv3.New(clientv3.Config{
			Endpoints:   strings.Split(etcdAddr, ";"),
			DialTimeout: 15 * time.Second,
		})
		if err != nil {
			fmt.Printf("连接etcd失败:%s\n", err)
			return err
		}
	}

	//与etcd建立长连接,并保证连接不断(心跳检测)
	ticker := time.NewTicker(time.Second * time.Duration(ttl))
	go func() {
		key := getKey(serviceName, serverAddr)
		for {
			resp, err := cli.Get(context.Background(), key)
			//fmt.Printf("resp:%+v\n", resp)
			if err != nil {
				fmt.Printf("获取服务地址失败:%s", err)
			} else if resp.Count == 0 { //尚未注册
				err = keepAlive(serviceName, serverAddr, ttl)
				if err != nil {
					fmt.Printf("保持连接失败:%s", err)
				}
			}
			<-ticker.C
		}
	}()

	return nil
}

//组装etcd key
func getKey(serviceName, serverAddr string) string {
	return fmt.Sprintf("/%s/%s/%s", "etcd", serviceName, serverAddr)
}

//保持服务器与etcd的长连接
func keepAlive(serviceName, serverAddr string, ttl int64) error {
	//创建租约
	leaseResp, err := cli.Grant(context.Background(), ttl)
	if err != nil {
		fmt.Printf("创建租期失败:%s\n", err)
		return err
	}

	//将服务地址注册到etcd中
	key := getKey(serviceName, serverAddr)
	_, err = cli.Put(context.Background(), key, serverAddr, clientv3.WithLease(leaseResp.ID))

	if err != nil {
		fmt.Printf("注册服务失败:%s", err)
		return err
	}
	fmt.Printf("etcd服务注册成功,key:%s,value:%s", key, serverAddr)
	//建立长连接
	ch, err := cli.KeepAlive(context.Background(), leaseResp.ID)
	if err != nil {
		fmt.Printf("建立长连接失败:%s\n", err)
		return err
	}

	//清空keepAlive返回的channel
	go func() {
		for {
			<-ch
		}
	}()
	return nil
}

//取消注册
func unRegister(serviceName, serverAddr string) {
	if cli != nil {
		key := getKey(serviceName, serverAddr)
		cli.Delete(context.Background(), key)
	}
}

func main() {
	flag.Parse()

	//监听网络
	listener, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", *Port))
	if err != nil {
		fmt.Println("监听网络失败:", err)
		return
	}
	defer listener.Close()

	//创建grpc句柄
	// 1. 初始化 grpc对象
	grpcServer := grpc.NewServer()
	defer grpcServer.GracefulStop()

	// 2. 注册服务
	greeter.RegisterGreeterServer(grpcServer, &Hello{})

	//将服务地址注册到etcd中
	serverAddr := fmt.Sprintf("%s:%d", host, *Port)
	fmt.Printf("greeting server address: %s\n", serverAddr)
	register(*EtcdAddr, *ServiceName, serverAddr, 5)

	//关闭信号处理
	ch := make(chan os.Signal, 1)
	signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT, syscall.SIGKILL, syscall.SIGHUP, syscall.SIGQUIT)
	go func() {
		s := <-ch
		unRegister(*ServiceName, serverAddr)
		if i, ok := s.(syscall.Signal); ok {
			os.Exit(int(i))
		} else {
			os.Exit(0)
		}
	}()

	//监听服务
	err = grpcServer.Serve(listener)
	if err != nil {
		fmt.Println("监听异常:", err)
		return
	}
}

3) client 代码(服务发现):

// package main

// import (
// 	"client/proto/greeter"
// 	"context"
// 	"fmt"

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

// func main() {
// 	// 1. 建立连接
// 	/*
// 		credentials.NewClientTLSFromFile:从输入的证书文件中为客户端构造TLS凭证
// 		grpc.WithTransportCredentials:配置连接级别的安全凭证(例如,TLS/SSL),返回一个DialOption,用于连接服务器
// 	*/
// 	grpcClient, err := grpc.Dial("0.0.0.0:8001", grpc.WithTransportCredentials(insecure.NewCredentials()))
// 	if err != nil {
// 		fmt.Println(err)
// 	}

// 	// 2. 注册客户端
// 	client := greeter.NewGreeterClient(grpcClient)
// 	res, err := client.SayHello(context.Background(), &greeter.HelloReq{
// 		Name: "lihh",
// 	})
// 	if err != nil {
// 		fmt.Println(err)
// 	}
// 	fmt.Println(res, res.GetMessage())

// }
package main

import (
	"client/proto/greeter"
	"flag"
	"fmt"
	"log"
	"strings"
	"time"

	"github.com/coreos/etcd/clientv3"
	"golang.org/x/net/context"
	"google.golang.org/grpc"
	"google.golang.org/grpc/resolver"
)

/**
参考地址:
将grpc版本替换成v1.26.0版本:
go mod edit -require=google.golang.org/grpc@v1.26.0
go get -u -x google.golang.org/grpc@v1.26.0
*/

var (
	ServiceName = flag.String("ServiceName", "greet_service", "service name")                                //服务名称
	EtcdAddr    = flag.String("EtcdAddr", "0.0.0.0:2379;0.0.0.0:3379;0.0.0.0:4379", "register etcd address") //etcd的地址
)

var cli *clientv3.Client

//etcd解析器
type etcdResolver struct {
	etcdAddr   string
	clientConn resolver.ClientConn
}

//初始化一个etcd解析器
func newResolver(etcdAddr string) resolver.Builder {
	return &etcdResolver{etcdAddr: etcdAddr}
}

func (r *etcdResolver) Scheme() string {
	return "etcd"
}

//watch有变化以后会调用
func (r *etcdResolver) ResolveNow(rn resolver.ResolveNowOptions) {
	log.Println("ResolveNow")
	fmt.Println(rn)
}

//解析器关闭时调用
func (r *etcdResolver) Close() {
	log.Println("Close")
}

//构建解析器 grpc.Dial()同步调用
func (r *etcdResolver) Build(target resolver.Target, clientConn resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
	var err error
	fmt.Println("call build...")
	//构建etcd client
	if cli == nil {
		cli, err = clientv3.New(clientv3.Config{
			Endpoints:   strings.Split(r.etcdAddr, ";"),
			DialTimeout: 15 * time.Second,
		})
		if err != nil {
			fmt.Printf("连接etcd失败:%s\n", err)
			return nil, err
		}
	}

	r.clientConn = clientConn

	go r.watch("/" + target.Scheme + "/" + target.Endpoint + "/")

	return r, nil
}

//监听etcd中某个key前缀的服务地址列表的变化
func (r *etcdResolver) watch(keyPrefix string) {
	//初始化服务地址列表
	var addrList []resolver.Address

	resp, err := cli.Get(context.Background(), keyPrefix, clientv3.WithPrefix())
	if err != nil {
		fmt.Println("获取服务地址列表失败:", err)
	} else {
		for i := range resp.Kvs {
			addrList = append(addrList, resolver.Address{Addr: strings.TrimPrefix(string(resp.Kvs[i].Key), keyPrefix)})
		}
	}

	r.clientConn.NewAddress(addrList)

	//监听服务地址列表的变化
	rch := cli.Watch(context.Background(), keyPrefix, clientv3.WithPrefix())
	for n := range rch {
		for _, ev := range n.Events {
			addr := strings.TrimPrefix(string(ev.Kv.Key), keyPrefix)
			switch ev.Type {
			case 0: //mvccpb.PUT
				if !exists(addrList, addr) {
					addrList = append(addrList, resolver.Address{Addr: addr})
					r.clientConn.NewAddress(addrList)
				}
				fmt.Println("有新的服务注册:", addr)
			case 1: //mvccpb.DELETE
				if s, ok := remove(addrList, addr); ok {
					addrList = s
					r.clientConn.NewAddress(addrList)
				}
				fmt.Println("服务注销:", addr)
			}
		}
	}
}

func exists(l []resolver.Address, addr string) bool {
	for i := range l {
		if l[i].Addr == addr {
			return true
		}
	}
	return false
}

func remove(s []resolver.Address, addr string) ([]resolver.Address, bool) {
	for i := range s {
		if s[i].Addr == addr {
			s[i] = s[len(s)-1]
			return s[:len(s)-1], true
		}
	}
	return nil, false
}

func main() {
	flag.Parse()

	//注册etcd解析器
	r := newResolver(*EtcdAddr)
	resolver.Register(r)

	//客户端连接服务器(负载均衡:轮询) 会同步调用r.Build()
	grpcClient, err := grpc.Dial(r.Scheme()+"://author/"+*ServiceName, grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy": "round_robin"}`), grpc.WithInsecure())
	if err != nil {
		fmt.Println("连接服务器失败:", err)
	}
	defer grpcClient.Close()

	//获得grpc句柄
	c := greeter.NewGreeterClient(grpcClient)
	ticker := time.NewTicker(2 * time.Second)
	i := 1
	for range ticker.C {

		resp1, err := c.SayHello(
			context.Background(),
			&greeter.HelloReq{Name: fmt.Sprintf("lihh%d", i)},
		)
		if err != nil {
			fmt.Println("Hello调用失败:", err)
			return
		}
		fmt.Printf("Hello 响应:%s\n", resp1.Message)

		i++
	}
}

6.go-micro 参考:http://www.17bigdata.com/study/programming/micro/micro-index.html

7.go-zero (做项目推荐) 参考:https://go-zero.dev

posted @ 2023-06-03 12:13  李皇皓  阅读(76)  评论(0)    收藏  举报