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

浙公网安备 33010602011771号