微服务 — gRPC+Consul项目实践

微服务 — gRPC+Consul项目实践

一、项目功能(项目运行需要启动consul服务端)
  1. 基于golang工程,开发一个中心-边缘类系统,实现例如数据采集、任务下发等场景的功能需要
  2. 集成gRPC,实现中心服务和边缘服务的远程交互
  3. 引入Consul,集成服务发现,改造中心边缘通过服务发现进行远程链接
二、技术简介
2-1、Go 协程技术- goroutine
  • goroutine的概念类似于线程,但 goroutine是由Go的运行时(runtime)调度和管理的。Go程序会智能地将 goroutine 中的任务合理地分配给每个CPU。

    • Go语言中使用goroutine非常简单,只需要在调用函数的时候在前面加上go关键字,就可以为一个函数创建一个goroutine。

      • 一个goroutine必定对应一个函数,可以创建多个goroutine去执行相同的函数。
  • 启动goroutine的方式非常简单,只需要在调用的函数(普通函数和匿名函数)前面加上一个go关键字,例如:

    func hello() {
        fmt.Println("Hello Goroutine!")
    }
    func main() {
        go hello()
        fmt.Println("main goroutine done!")
    }
    
  • 在Go语言中,可以使用sync.WaitGroup来管理goroutine,实现goroutine的同步和退出等,例如:

    var wg sync.WaitGroup
    
    func hello(i int) {
        defer wg.Done() // goroutine结束就登记-1
        fmt.Println("Hello Goroutine!", i)
    }
    func main() {
    
        for i := 0; i < 10; i++ {
            wg.Add(1) // 启动一个goroutine就登记+1
            go hello(i)
        }
        wg.Wait() // 等待所有登记的goroutine都结束
    }
    
2-2、gRPC
  • gRPC 基于如下思想:定义一个服务, 指定其可以被远程调用的方法及其参数和返回类型。gRPC 默认使用 protocol buffers 作为接口定义语言,来描述服务接口和有效载荷消息结构。

    • gRPC 是一个高性能、开源和通用的 RPC 框架,面向移动和 HTTP/2 设计。

    • gRPC支持多语言,目前提供 C、Java 和 Go 语言版本。

    • gRPC 基于 HTTP/2 标准设计,带来诸如双向流、流控、头部压缩、单 TCP 连接上的多复用请求等特点。

      在这里插入图片描述

  • gRPC 允许你定义四类服务方法:

    • 单项 RPC,即客户端发送一个请求给服务端,从服务端获取一个应答,就像一次普通的函数调用。
    • 服务端流式 RPC,即客户端发送一个请求给服务端,可获取一个数据流用来读取一系列消息。客户端从返回的数据流里一直读取直到没有更多消息为止。
    • 客户端流式 RPC,即客户端用提供的一个数据流写入并发送一系列消息给服务端。一旦客户端完成消息写入,就等待服务端读取这些消息并返回应答。
    • 双向流式 RPC,即两边都可以分别通过一个读写数据流来发送一系列消息。这两个数据流操作是相互独立的,所以客户端和服务端能按其希望的任意顺序读写。
  • gRPC 提供 protocol buffer 编译插件,能够从一个服务定义的 .proto 文件生成客户端和服务端代码。通常 gRPC 用户可以在服务端实现这些API,并从客户端调用它们。

    • 在服务侧,服务端实现服务接口,运行一个 gRPC 服务器来处理客户端调用。gRPC 底层架构会解码传入的请求,执行服务方法,编码服务应答。
    • 在客户侧,客户端有一个存根实现了服务端同样的方法。客户端可以在本地存根调用这些方法,用合适的 protocol buffer 消息类型封装这些参数— gRPC 来负责发送请求给服务端并返回服务端 protocol buffer 响应。
2-3、Consul
  • Consul是一个服务网格解决方案,提供了一个功能齐全的控制平面,具有服务发现、配置和分段功能。这些功能中的每一项都可以根据需要单独使用,也可以一起使用来构建一个完整的服务网格。Consul需要一个数据平面,并支持代理和原生集成模型。

    在这里插入图片描述

  • Consul的主要功能有:

    • 服务发现 : Consul的客户端可以注册一个服务,比如api或mysql,其他客户端可以使用Consul来发现特定服务的提供者。使用DNS或HTTP,应用程序可以很容易地找到他们所依赖的服务。
    • 健康检查 : Consul客户端可以提供任何数量的健康检查,要么与给定的服务相关联,要么与本地节点相关联。这些信息可以运维人员用来监控集群的健康状况,并被服务发现组件来路由流量(比如: 仅路由到健康节点)
    • KV存储 : 应用程序可以利用Consul的层级K/V存储来实现任何目的,包括动态配置、功能标记、协调、领导者选举等。Consul提供了HTTP API,使其非常简单以用。
    • 安全服务通信: Consul可以为服务生成和分发TLS证书,以建立相互的TLS连接。可以使用Intention来定义哪些服务被允许进行通信。服务隔离可以通过可以实时更改Intention策略轻松管理,而不是使用复杂的网络拓扑结构和静态防火墙规则。
    • 多数据中心: Consul支持开箱即用的多数据中心。这意味着Consul的用户不必担心建立额外的抽象层来发展到多个区域。
三、项目结构
├─.idea
├─client
│  └─client.go  //边缘组件
├─common
│  ├─lib  //工具包
│  ├─message // 协议文件-proto及其生成的代码
│  │  ├─transceiver
│  │  │ ├─transceiver.pb.gp
│  │  │ └─transceiver_grpc.pb.go
│  │  └─transceiver.proto
│  └─repo // 公共仓库-工具服务
│      └─consul // consul 服务集成
│     	 ├─uconsul.go
│      	 └─uconsul_test.go
└─server
	└─server.go // 服务端组件
四、项目实现
  • transceiver.proto【产物生成详见:Grpc Golang项目实践从小白到入门_玉言心的博客-优快云博客

    syntax = "proto3";
    
    option go_package="/transceiver";
    
    package client;
    
    
    service Transceiver {
      rpc registerClient(TransceiverMsg) returns (TransceiverID);
      rpc getClient(TransceiverID) returns (TransceiverMsg);
      rpc taskHandel(TransceiverID) returns (TaskMsg);
      rpc taskCall(TaskMsg) returns (response);
    }
    
    message response {
      string code = 1;
    }
    
    message TaskMsg {
      machine monitor = 1;
      repeated targetMsg  target = 2; // 目标数组
    }
    
    message targetMsg {
      machine target = 1;
      taskInfo task = 2;
    }
    
    message taskInfo {
      controlMsg control = 1;
      tasks tasks = 2;
    }
    
    message tasks {
      string cmd = 1;
      string result = 2;
    }
    
    message controlMsg {
      string params = 1;
      string task_name = 2;
      string time = 3;
      string taskType = 4;
      string protocol = 5;
    }
    
    message machine {
      string name = 1;
      string prov = 2;
      string city = 3;
      string status = 4;
      string ip = 5;
    }
    
    message TransceiverMsg {
      string id = 1; // id
      float uuid = 2; // uid
      string name = 3;   // 客户端名称:hostname
      string version = 4; // 版本号
      string time = 5;   // 上报时间,用于心跳检测
      string status = 6; // 是否空闲等
      string description = 7; // 描述
    }
    
    message TransceiverID {
      string value = 1;
    }
    
    message TransceiverName {
      string value = 1;
    }
    
    
  • uconsul.go

    package consul
    
    import (
    	"errors"
    	"fmt"
    	capi "github.com/hashicorp/consul/api"
    	"log"
    	"net"
    )
    
    type CSApi struct {
    	opt *capi.AgentServiceRegistration
    	// log logger2.Logger
    	cli *capi.Client
    }
    
    var instance *CSApi
    
    func CSInstance(opt *capi.AgentServiceRegistration) *CSApi {
    	if instance == nil {
    		instance = &CSApi{
    			opt: opt,
    			// log: logger2.NewSugar("consul api", false),
    			cli: nil,
    		}
    		//初始化consul配置
    		consulConfig := capi.DefaultConfig()
    		//获取consul客户端
    		consulClient, err := capi.NewClient(consulConfig)
    		if err != nil {
    			fmt.Println("consul api NewClient err :", err)
    			// retry once
    			return CSInstance(opt)
    		}
    		instance.cli = consulClient
    	}
    	return instance
    }
    
    func (cs *CSApi) Register() error {
    	if cs.opt == nil {
    		log.Fatalf("Warn : info register config is nil")
    		return errors.New("register config is nil")
    	}
    	//注册到consul
    	err := cs.cli.Agent().ServiceRegister(cs.opt)
    	if err != nil {
    		log.Fatalf("register Error info : %v", err)
    		return err
    	}
    	log.Println("register success ", "server id : ", cs.opt.ID)
    	return nil
    }
    
    func (cs *CSApi) Deregister(serverID string) {
    	err := cs.cli.Agent().ServiceDeregister(serverID)
    	if err != nil {
    		log.Fatalf("register server error : %v", err)
    	}
    	log.Println("server deregister success, server id :", serverID)
    }
    
    func (cs *CSApi) HealthCheck() error {
    	if cs.cli.Agent().DisableServiceMaintenance(cs.opt.ID) != nil {
    		err := cs.Register()
    		if err != nil {
    			return err
    		}
    	}
    	log.Println("", "server_health_check", true)
    	return nil
    }
    
    
  • client.go

    package main
    
    import (
    	"context"
    	"fmt"
    	"github.com/hashicorp/consul/api"
    	"google.golang.org/grpc/credentials/insecure"
    	"log"
    	"s-grpc/common/lib"
    	"s-grpc/common/message/transceiver"
    	"strconv"
    	"sync"
    	"time"
    
    	"google.golang.org/grpc"
    )
    
    const (
    	serverName = "Detect Server"
    	addr       = "localhost:50051"
    )
    
    type Client struct {
    	lock     sync.RWMutex
    	clientID string
    	stopChan chan int
    	cli      transceiver.TransceiverClient
    }
    
    var ins *Client
    
    func Instance() *Client {
    	if ins == nil {
    		ins = &Client{
    			clientID: "",
    			lock:     sync.RWMutex{},
    			stopChan: make(chan int),
    		}
    	}
    	return ins
    }
    
    func (c *Client) Start(clientName string) {
    	consulConfig := api.DefaultConfig()
    	consulClient, err := api.NewClient(consulConfig)
    	if err != nil {
    		fmt.Println("api.NewClient:", err)
    	}
    	services, _, err := consulClient.Health().Service(serverName, "Detect", true, nil)
    	//获取链接
    	address := services[0].Service.Address + ":" + strconv.Itoa(services[0].Service.Port)
    
    	// Set up a connection to the server.
    	conn, err := grpc.Dial(address, grpc.WithTransportCredentials(insecure.NewCredentials()))
    	if err != nil {
    		log.Fatalf("did not connect: %v", err)
    	}
    	defer conn.Close()
    	c.cli = transceiver.NewTransceiverClient(conn)
    
    	ctx := context.WithValue(context.Background(), "grpc", "agent-test") //指定测试人员
    	/*ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
    	defer cancel()*/
    	c.RegisterServer(ctx, clientName)
    	lib.Loop(time.Second*time.Duration(5), ctx, c.GetTask, c.stopChan)
    }
    
    func (c *Client) RegisterServer(ctx context.Context, clientName string) {
    	// Contact the server and print out its response.
    	// name := "proto agent"
    	description := "test grpc conn."
    	uid := float32(1001.00)
    	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    	defer cancel()
    	r, err := c.cli.RegisterClient(ctx, &transceiver.TransceiverMsg{
    		Id:          strconv.Itoa(int(uid)),
    		Uuid:        uid,
    		Name:        clientName,
    		Version:     "1.0.0",
    		Time:        "2022-11-25 16:25:00",
    		Status:      "0",
    		Description: description,
    	})
    	if err != nil {
    		log.Fatalf("Client: Could not register Client: %v", err)
    	}
    	log.Printf("Client: ID %s register successfully", r.Value)
    	c.clientID = r.Value
    }
    
    func (c *Client) GetTask(ctx context.Context) {
    	cli, err := c.cli.GetClient(ctx, &transceiver.TransceiverID{Value: c.clientID})
    	if err != nil {
    		log.Printf("Client: Could not get client: %v", err)
    	}
    	log.Printf("Client: %v", cli.String())
    	c.RunTask(ctx)
    }
    
    func (c *Client) RunTask(ctx context.Context) {
    	task, err := c.cli.TaskHandel(ctx, &transceiver.TransceiverID{Value: c.clientID})
    	if err != nil {
    		log.Fatalf("Client: Could not get client: %v", err)
    	}
    	log.Println("Client: receive task, len : ", len(task.Target))
    	for i := range task.Target {
    		log.Println("Client: run task : ", task.Target[i].Task.Control.TaskName)
    		task.Target[i].Task.Tasks.Result = "0.1::0.13::0.25::0.45"
    	}
    	call, err := c.cli.TaskCall(ctx, task)
    	if err != nil {
    		return
    	}
    	log.Printf("Client: run task end: { %v }", call)
    }
    
    func main() {
    	// Get client instance
    	cli := Instance()
    	cli.Start("client-01")
    }
    
    
  • server.go

    package main
    
    import (
    	"context"
    	"fmt"
    	"github.com/hashicorp/consul/api"
    	"log"
    	"math/rand"
    	"net"
    	"s-grpc/common/lib"
    	"s-grpc/common/message/transceiver"
    	"s-grpc/common/repo/consul"
    	"strconv"
    	"strings"
    	"time"
    
    	"google.golang.org/grpc"
    	"google.golang.org/grpc/codes"
    	"google.golang.org/grpc/status"
    )
    
    const (
    	port    = "50051"
    	address = "detect.center_api.com"
    )
    
    // server is used to implement ecommerce/product_info.
    type server struct {
    	clientMap map[string]*transceiver.TransceiverMsg // 连接池维护
    	tasks     map[string]*transceiver.TaskMsg        // 任务存储
    }
    
    func (s *server) TaskCall(ctx context.Context, msg *transceiver.TaskMsg) (*transceiver.Response, error) {
    	log.Println("server: receive task response : ", msg.Monitor.Name)
    	if msg == nil || len(msg.Target) == 0 {
    		log.Println("server: receive task result len : ", 0)
    	}
    	log.Println("server: -------- task result : ", msg.Target)
    	return &transceiver.Response{Code: "200"}, nil
    }
    
    func (s *server) TaskHandel(ctx context.Context, id *transceiver.TransceiverID) (*transceiver.TaskMsg, error) {
    	var tasks = s.tasks
    	if len(tasks) == 0 {
    		log.Printf("server: task is null.")
    	}
    	if _, ok := tasks[id.Value]; !ok {
    		return &transceiver.TaskMsg{}, status.Errorf(codes.NotFound, "Client does not exist.")
    	}
    	return tasks[id.Value], nil
    }
    
    // RegisterClient implements ecommerce.AddProduct
    func (s *server) RegisterClient(ctx context.Context,
    	in *transceiver.TransceiverMsg) (*transceiver.TransceiverID, error) {
    	in.Id = lib.Md5(in.Name)
    	if s.clientMap == nil {
    		s.clientMap = make(map[string]*transceiver.TransceiverMsg)
    	}
    	s.clientMap[in.Id] = in
    	if _, ok := s.tasks[in.Id]; !ok {
    		s.tasks[in.Id] = &transceiver.TaskMsg{
    			Monitor: s.getMachine(in.Id),
    			Target:  nil,
    		}
    	}
    	log.Printf("server: client [%v-%v] - registered.", in.Name, in.Id)
    	return &transceiver.TransceiverID{Value: in.Id}, status.New(codes.OK, "").Err()
    }
    
    // GetClient implements ecommerce.GetProduct
    func (s *server) GetClient(ctx context.Context, in *transceiver.TransceiverID) (*transceiver.TransceiverMsg, error) {
    	product, exists := s.clientMap[in.Value]
    	if exists && product != nil {
    		log.Printf("Server: client [%v-%v] - Retrieved.", product.Name, product.Id)
    		return product, status.New(codes.OK, "").Err()
    	}
    	return nil, status.Errorf(codes.NotFound, "Client does not exist.", in.Value)
    }
    
    func (s *server) DivTask(ctx context.Context) {
    	s.tasks = map[string]*transceiver.TaskMsg{}
    	log.Printf("server: task div run. cleint len %d", len(s.clientMap))
    	for uid, _ := range s.clientMap {
    		if _, ok := s.tasks[uid]; !ok {
    			s.tasks[uid] = &transceiver.TaskMsg{
    				Monitor: s.getMachine(uid),
    				Target:  nil,
    			}
    		}
    		targetMsg := &transceiver.TargetMsg{
    			Target: s.getMachine("target00" + uid),
    			Task:   s.getTask(uid),
    		}
    		s.tasks[uid].Target = append(s.tasks[uid].Target, targetMsg)
    	}
    }
    
    func (s server) getTask(taskID string) *transceiver.TaskInfo {
    	timeStr := time.Now().Format("2022-10-10 12:00:00")
    	uis := fmt.Sprintf("%06v", rand.New(rand.NewSource(time.Now().UnixNano())).Int31n(1000000))
    	ctl := &transceiver.ControlMsg{
    		Params:   "{}",
    		TaskName: fmt.Sprintf("task-%s@%s ", taskID, uis),
    		Time:     timeStr,
    		TaskType: "test",
    		Protocol: "ping",
    	}
    	task := &transceiver.Tasks{
    		Cmd:    "ping www.baidu.com",
    		Result: "0::0::0::0",
    	}
    	return &transceiver.TaskInfo{
    		Control: ctl,
    		Tasks:   task,
    	}
    }
    
    func (s *server) getMachine(uid string) *transceiver.Machine {
    	return &transceiver.Machine{
    		Name:   "test-" + uid,
    		Prov:   "guizhou",
    		City:   "guiyang",
    		Status: "2201",
    		Ip:     "192.168.137.100",
    	}
    }
    
    func main() {
    	// 创建consul服务发现
    	serverName := "Detect Server"
    	p, _ := strconv.Atoi(port)
    	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    	defer cancel()
    	csConfig := api.AgentServiceRegistration{
    		ID:      "001",
    		Tags:    strings.Split(serverName, " "),
    		Name:    serverName,
    		Address: address,
    		Port:    p,
    		//注册check服务。
    		Check: &api.AgentServiceCheck{
    			CheckID: "grpc detect",
    			//HTTP:    fmt.Sprintf("https://%s:%d%s", address, p, "/check"),
    			TCP: fmt.Sprintf("%s:%d", address, p),
    			//设置超时 5s。
    			Timeout: "5s",
    			//设置间隔 5s。
    			Interval: "5s",
    		},
    	}
    	consulInstance := consul.CSInstance(&csConfig)
    	err := consulInstance.Register()
    	if err != nil {
    		return
    	}
    	// 服务注册检测,注册中心重启,需要重新注册
    	go lib.Loop(time.Second*time.Duration(30), ctx, func(ctx context.Context) {
    		err = consulInstance.HealthCheck()
    		if err != nil {
    			log.Fatalf("err: %v", err)
    		}
    	}, nil)
    	// 创建服务端
    	ser := &server{
    		clientMap: make(map[string]*transceiver.TransceiverMsg),
    		tasks:     make(map[string]*transceiver.TaskMsg),
    		// taskQueue: queue.NewQueue(50),
    	}
    	// 任务分配
    	go lib.Loop(time.Second*time.Duration(10), ctx, ser.DivTask, nil)
    	s := grpc.NewServer()
    	// 注册服务端到grpc
    	transceiver.RegisterTransceiverServer(s, ser)
    
    	// 开启服务监听
    	lis, err := net.Listen("tcp", ":"+port)
    	if err != nil {
    		log.Fatalf("failed to listen: %v", err)
    	}
    	if err := s.Serve(lis); err != nil {
    		log.Fatalf("failed to serve: %v", err)
    	}
    }
    
    
  • 运行效果

    • 服务端

      在这里插入图片描述

    • 客户端

      在这里插入图片描述

    • 注册中心

      在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

玉言心

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值