golang grpc微服务实践

这是一篇关于grpc,etcd,grpc-gateway的实践

目录

  • 这是一篇关于grpc,etcd,grpc-gateway的实践
    • 相关工具安装
      • 1. protocol安装
      • 2. grpc,grpc-gateway安装
      • 3. etcd安装
    • 一个基础微服务demo
      • 1. 程序结构目录
      • 2. proto文件
      • 3. 服务端
      • 4. 客户端代码
    • 使用etcd做服务发现和负载均衡
      • 1.docker安装etcd
      • 2. 关于etcd的代码
      • 3. 服务端代码修改
      • 4. 客户端代码修改
    • grpc-gateway完成http服务接口
      • 1. proto文件修改
      • 2.服务端修改
  • golang和grpc数据传输的流模式

相关工具安装

1. protocol安装

下载地址:

https://github.com/protocolbuffers/protobuf/releases

golang grpc微服务实践_第1张图片
我选的是windows环境的
下载完之后,解压到文件夹内,并添加环境变量。

2. grpc,grpc-gateway安装

代码地址

https://github.com/grpc/grpc-go/tags

golang package

go get -u google.golang.org/grpc

go get google.golang.org/protobuf

// 会在go安装目录 go/bin下生成一个protoc-gen-go.exe
go get -u github.com/golang/protobuf/protoc-gen-go

// go/bin下protoc-gen-grpc-gateway.exe
go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway

3. etcd安装

docker 安装etcd并运行

  1. 拉取镜像 https://hub.docker.com/r/bitnami/etcd
docker pull bitnami/etcd:3.4.20
  1. 创建docker网络
docker network create etcd-bridge --driver bridge
  1. 运行
docker run -d --name etcd-server \
    --network etcd-bridge \
    --publish 2379:2379 \
    --publish 2380:2380 \
    --env ALLOW_NONE_AUTHENTICATION=yes \
    --env ETCD_ADVERTISE_CLIENT_URLS=http://etcd-server:2379 \
    bitnami/etcd:3.4.20

一个基础微服务demo

1. 程序结构目录

/rpc
	/client
		-main.go
	/etcd_service
		-etcd.go
	/proto
		-product.proto
	/proto_service
		-pruduct.pb.go
		-pruduct.pb.gw.go
	/server
		-main.go
		-pruduct.go
 	-main.go

2. proto文件

// 使用的语法版本
syntax = "proto3";

// 生成的go文件包
option go_package = "./proto_service";

//service 服务 接口
service ProdService {
  rpc GetProductStock(ProductRequest) returns (ProductResponse);
}

// 请求参数
message ProductRequest {
  int32 prod_id = 1;
}

// 返回参数
message ProductResponse {
  int32 prod_stock = 1;
}

生成go文件

pwd
/rpc/proto

protoc --go_out=plugins=grpc:../ product.proto

命令会直接生成proto_service目录和product.pb.go文件

3. 服务端

product.go

package main

import (
	"context"
	"log"
	"rpc/proto_service"
)

// 建一个实例
var ProductService = &productService{}

type productService struct {}

// 服务端实现接口业务内容
func (p productService) GetProductStock(ctx context.Context, request *proto_service.ProductRequest) (*proto_service.ProductResponse, error) {
	log.Println("request: ", request)
	return &proto_service.ProductResponse{ProdStock: request.ProdId*1000 + 996}, nil
}

main.go

package main

import (
	"google.golang.org/grpc"
	"log"
	"net"
	"rpc/proto_service"
)

const addr = "127.0.0.1:8001"

func main() {
	rpcServer := grpc.NewServer()
	proto_service.RegisterProdServiceServer(rpcServer, ProductService)
	listener, err := net.Listen("tcp", addr)
	if err != nil {
		log.Fatal("启动监听出错", err)
	}
	err = rpcServer.Serve(listener)
	if err != nil {
		log.Fatal("启动服务出错", err)
	}
}

开启服务

go run main.go

4. 客户端代码

client/main.go

package main

import (
	"context"
	"fmt"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"log"
	"rpc/proto_service"
	"time"
)

const addr = "127.0.0.1:8001"

func main() {
	conn, err := grpc.Dial(addr,grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		log.Fatal("服务端出错,连接不上:", err)
	}
	defer conn.Close()
	
	client := proto_service.NewProdServiceClient(conn)
	for i := 0; i < 100; i++ {
		// 限制5秒自动返回请求失败
		tctx, _ := context.WithTimeout(context.Background(), 5*time.Second) 
		response, err := client.GetProductStock(tctx, &proto_service.ProductRequest{ProdId: int32(i) * 1000})
		if err != nil {
			log.Println("查询出错:", err)
		}
		fmt.Printf("%d response:%v \n", i, response)
		time.Sleep(5 * time.Second)
	}
}

使用etcd做服务发现和负载均衡

1.docker安装etcd

2. 关于etcd的代码

etcd_service/etcd.go

package etcd_service

import (
	"context"
	"fmt"
	clientv3 "go.etcd.io/etcd/client/v3"
	"go.etcd.io/etcd/client/v3/naming/endpoints"
	"log"
	"time"
)

var (
	config clientv3.Config
	client *clientv3.Client
	err    error
)

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

func Register(serviceName, addr string) error {
	log.Printf("etcd etcd_service proto_service:%s, addr:%s \n", serviceName, addr)

	leaseGrantResp, err := client.Grant(context.TODO(), 10)
	if err != nil {
		log.Println(err)
		return err
	}
	leaseId := leaseGrantResp.ID
	kv := clientv3.NewKV(client)

	{
		em, err := endpoints.NewManager(client, serviceName)
		err = em.AddEndpoint(context.TODO(), fmt.Sprintf("%s/%s", serviceName, addr),
			endpoints.Endpoint{Addr: addr}, clientv3.WithLease(leaseId))
		if err != nil {
			log.Println(err)
			return err
		}
	}
	// 查看一下
	getResp, err := kv.Get(context.TODO(), fmt.Sprintf("%s/%s", serviceName, addr))
	if err != nil {
		log.Println(err)
		return err
	}
	// 输出本次的Revision
	for i, v := range getResp.Kvs {
		log.Printf("%d Key is s %s , Value is %s \n", i, v.Key, v.Value)
	}
	// 自动续租
	keepRespChan, err := client.KeepAlive(context.TODO(), leaseId)
	if err != nil {
		log.Println(err)
		return err
	}
	go func() {
		for {
			select {
			case keepResp, ok := <-keepRespChan:
				if !ok || keepRespChan == nil {
					log.Println("租约已经失效")
					goto END
				} else if ok {
					//log.Println("收到自动续租", keepResp.ID)
					_ = keepResp.ID
				}
			}
		}
	END:
	}()
	return nil
}

func UnRegister(serviceName, addr string) error {
	log.Printf("etcd unregister proto_service:%s, addr:%s \n", serviceName, addr)
	kv := clientv3.NewKV(client)
	deleteResp, err := kv.Delete(context.TODO(), fmt.Sprintf("%s/%s", serviceName, addr), clientv3.WithPrevKV())
	if err != nil {
		log.Println(err)
		return err
	}
	log.Println(deleteResp.Header.Revision)
	if deleteResp.PrevKvs != nil {
		for _, kvpair := range deleteResp.PrevKvs {
			fmt.Printf("delete key is: %s , Value: %s \n", string(kvpair.Key), string(kvpair.Value))
		}
	}
	return err
}

这里可以不用引用endpoints的方法,可以使用etcd.client的put键值对的方法把服务地址存进去,但是value要做成 {"Op":0,"Addr":"127.0.0.1:8001","Metadata":null} 这样的字符串,因为客户端自动解析的时候会做json解析

	putResp, err := kv.Put(context.TODO(), fmt.Sprintf("%s/%s", serviceName, addr), fmt.Sprintf(`{"Op":0,"Addr":"%s","Metadata":null} `, addr), clientv3.WithPrevKV(), clientv3.WithLease(leaseId))
	if err != nil {
		log.Println(err)
		return err
	}

3. 服务端代码修改

server/main.go

package main

import (
	"google.golang.org/grpc"
	"log"
	"net"
	"os"
	"os/signal"
	"syscall"
	"rpc/etcd_service"
	"rpc/proto_service"
)

const addr = "127.0.0.1:8001"
const serviceName = "aaa/"

func main() {
	//关闭信号处理
	ch := make(chan os.Signal, 1)
	signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT, syscall.SIGKILL, syscall.SIGHUP, syscall.SIGQUIT)
	go func() {
		s := <-ch
		// 监听关闭信号,关闭了就从etcd中删除
		etcd_service.UnRegister(serviceName, addr)
		if i, ok := s.(syscall.Signal); ok {
			os.Exit(int(i))
		} else {
			os.Exit(0)
		}
	}()
	// etcd 注册
	etcd_service.Register(serviceName, addr)

	rpcServer := grpc.NewServer()
	proto_service.RegisterProdServiceServer(rpcServer, ProductService)
	listener, err := net.Listen("tcp", addr)
	if err != nil {
		log.Fatal("启动监听出错", err)
	}
	err = rpcServer.Serve(listener)
	if err != nil {
		log.Fatal("启动服务出错", err)
	}

}

修改addr开启三个服务

4. 客户端代码修改

package main

import (
	"context"
	"fmt"
	clientv3 "go.etcd.io/etcd/client/v3"
	etcdResolver "go.etcd.io/etcd/client/v3/naming/resolver"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"log"
	"test/rpc/proto_service"
	"time"
)

const serviceName = "aaa/"

func main() {
	etcdClient, _ := clientv3.New(clientv3.Config{
		Endpoints:   []string{"172.17.219.154:2379"},
		DialTimeout: 5 * time.Second,
	})
	etcd_resolver, err := etcdResolver.NewBuilder(etcdClient)
	if err != nil {
		panic(err)
	}
	fmt.Println(etcd_resolver.Scheme())
	conn, err := grpc.Dial(fmt.Sprintf("etcd:///%s", serviceName),
		grpc.WithResolvers(etcd_resolver),
		grpc.WithTransportCredentials(insecure.NewCredentials()),
		grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`),
	)
	if err != nil {
		log.Fatal("服务端出错,连接不上:", err)
	}
	defer conn.Close()
	client := proto_service.NewProdServiceClient(conn)
	for i := 0; i < 100; i++ {

		tctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
		response, err := client.GetProductStock(tctx, &proto_service.ProductRequest{ProdId: int32(i) * 1000})
		if err != nil {
			log.Println("查询出错:", err)
		}
		time.Sleep(5 * time.Second)
	}

}

grpc.WithResolvers(etcd_resolver) 绑定etcd的解析
grpc.WithDefaultServiceConfig({"loadBalancingPolicy":"round_robin"}), 采用轮询的方式请求服务地址

grpc-gateway完成http服务接口

1. proto文件修改


syntax = "proto3";

// option go_package = "path;name"; path gow文件存放地址,name go包名
option go_package = "./proto_service";

import "google/api/annotations.proto";


//service 服务
service ProdService {
  rpc GetProductStock(ProductRequest) returns (ProductResponse);
  rpc GetProduct(ProductRequest) returns (ProductResponse) {
    option (google.api.http) = {
      post: "/GetProduct"
      body: "*"
    };
  };
}

message ProductRequest {
  int32 prod_id = 1;
}

message ProductResponse {
  int32 prod_stock = 1;
}

这里引用google/api/annotations.proto文件来自于github.com/googleapis/googleapis包,一般go get ~的包会放在$GOPATH
然后运行命令生成文件

protoc -I . -I C:\Users\Administrator\go\pkg\mod\github.com\googleapis\[email protected] --go_out=plugins=grpc:../ product.proto
protoc -I . -I C:\Users\Administrator\go\pkg\mod\github.com\googleapis\[email protected] --grpc-gateway_out=../ product.proto

product.pb.go
product.pb.gw.go

C:\Users\Administrator\go是我电脑上的GOPATH

2.服务端修改

package main

import (
	"context"
	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
	"google.golang.org/grpc"
	"log"
	"net"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"test/rpc/etcd_service"
	"test/rpc/proto_service"
)

const addr = "127.0.0.1:8001"
const httpAddr = "127.0.0.1:9001"
const serviceName = "aaa/"

func main() {
	//关闭信号处理
	ch := make(chan os.Signal, 1)
	signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT, syscall.SIGKILL, syscall.SIGHUP, syscall.SIGQUIT)
	go func() {
		s := <-ch
		etcd_service.UnRegister(serviceName, addr)
		if i, ok := s.(syscall.Signal); ok {
			os.Exit(int(i))
		} else {
			os.Exit(0)
		}
	}()
	// etcd 注册
	etcd_service.Register(serviceName, addr)

	rpcServer := grpc.NewServer()
	//开启http服务
	go func() {
		mux := runtime.NewServeMux()
		err := proto_service.RegisterProdServiceHandlerServer(context.Background(), mux, ProductService)
		//err := proto_service.RegisterProdServiceHandlerFromEndpoint(context.Background(), mux, "127.0.0.1:8001",
		//	[]grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())})

		if err != nil {
			log.Fatal("RegisterProdService  err:", err)
		}
		err = http.ListenAndServe(httpAddr, mux)
		if err != nil {
			log.Fatal("ListenAndServe err:", err)
		}
	}()
	// grpc server
	proto_service.RegisterProdServiceServer(rpcServer, ProductService)
	listener, err := net.Listen("tcp", addr)
	if err != nil {
		log.Fatal("启动监听出错", err)
	}
	err = rpcServer.Serve(listener)
	if err != nil {
		log.Fatal("启动服务出错", err)
	}

}

使用postman测试一下http服务

golang grpc微服务实践_第2张图片

golang和grpc数据传输的流模式

你可能感兴趣的:(rpc,微服务,后端)