go-micro实战一个http服务调用一个grpc服务

官方文档: https://micro.mu/docs/

1. etcd启动:

单机版etcd,直接下载etcd.exe点击启动就好,默认监听2379端口,环境变量设置set ETCDCTL_API 3, 这样etcdctl命令就使用v3和etcd server交互。

   consul启动 :(go-micro目前版本已经不再支持consul)  

   consul.exe agent -server -bootstrap -ui -client 0.0.0.0 -bind 192.168.1.101 -data-dir=F:/consul_data

   其中,-server 代表以服务端的方式启动;-boostrap 指定自己为leader,而不需要选举;-ui 启动一个内置web界面; -client指定客 户端可以访问的IP,设置为0.0.0.0表示可以任意访问,否则只是默认本机能访问。

   如果用docker启动,则:

   docker pull consul 

   docker run -d --name=cs -p 8500:8500 consul agent -server -boostrap -ui -client 0.0.0.1

  如果启动正常,访问 http://localhost:8500 会跳转到consul管理页面。

2.go-micro启动一个grpc服务

需要提前安装protoc工具用来编译pb文件,同时安装micro go插件:

安装protoc
https://github.com/google/protobuf/releases/tag/v3.4.1 下找到win32包 解压并配置环境变量

安装 gen-proto-go
go get -u github.com/golang/protobuf/protoc-gen-go

安装micro protobuf 插件
go get github.com/micro/protobuf/proto github.com/micro/protobuf/protoc-gen-go

proto生成文件
protoc -I . --go_out=plugins=micro:. greeter.proto

安装protco-go-inject-tag插件,用来作pb注解
go get github.com/favadi/protoc-go-inject-tag

 编写pb文件:

syntax = "proto3";
package Service;
import "models.proto";
message ProdsReq {
    // @inject_tag: json:"size", form:"size"
    int32 size = 1;
    // @inject_tag: uri:"pid"
    int32 prod_id = 2;
}

message ProdListResp {
        repeated ProdModel data = 1;
}

message ProdDetailtResp {
    ProdModel data = 1;
}

service ProdService {
    rpc GetProdsList(ProdsReq)  returns (ProdListResp);
    rpc GetProdsDetail(ProdsReq)  returns (ProdDetailtResp);
}

编译pb:

protoc --micro_out=../ --go_out=../ models.proto
protoc --micro_out=../ --go_out=../ prodService.proto
protoc-go-inject-tag -input=../models.pb.go
protoc-go-inject-tag -input=../prodService.pb.go

编写项目工程:

package main

import (
    "github.com/micro/go-micro"
    "github.com/micro/go-micro/registry"
    "github.com/micro/go-plugins/registry/etcd"
    "go-micro-grpc/ServiceImpl"
    Service "go-micro-grpc/Services"
)
func main()  {
   //consul作为注册中心
    //consulReg := consul.NewRegistry(
    //        registry.Addrs("192.168.1.101:8500"),
    //    )
   //etcd作为注册中心
    etcdReg := etcd.NewRegistry(registry.Addrs("127.0.0.1:2379"))
    svc := micro.NewService(
        micro.Name("prodservice"), //可以通过这个名字调用到此服务
        micro.Address(":8011"),
        micro.Registry(etcdReg),
        )

    svc.Init()
    //protoc --micro_out=../ --go_out=../ prodService.proto编译出的grpc服务接口
    Service.RegisterProdServiceHandler(svc.Server(), new(ServiceImpl.ProdService))
    svc.Run()
}

 实现服务接口:

package ServiceImpl

import (
    "context"
    "fmt"
    Service "go-micro-grpc/Services"
    "strconv"
    "time"
)

type ProdService struct {
}

func (service *ProdService) GetProdsDetail(ctx context.Context, req*Service.ProdsReq, resp*Service.ProdDetailtResp) error {
    time.Sleep(time.Second*3)
    fmt.Printf("req.size:%d, req.prodid:%d\n",req.Size, req.ProdId)
    resp.Data = newProd(req.ProdId, "测试商品详情")
    return nil
}

func newProd(id int32, pname string) *Service.ProdModel {
    return &Service.ProdModel{ProdId:id, ProdName:pname}
}


func (*ProdService)GetProdsList(ctx context.Context,  req *Service.ProdsReq,resp *Service.ProdListResp) error  {
    time.Sleep(time.Second*3)
    models := make([]*Service.ProdModel, 0)
    var i int32
    for i = 0; i < req.Size; i++ {
        models = append(models, newProd(i+100, "prodName"+strconv.Itoa(int(i)+100)))
    }
    resp.Data = models

    return nil
}

启动上面的服务就行了。下面来讲如何通过micro.exe工具检查我们启动的服务

安装micro.exe工具:

https://github.com/micro/micro

micro工具中带有一个etcd管理页面,启动etcd管理页面:

set MICRO_REGISTRY=etcd
set MICRO_REGISTRY_ADDRESS=127.0.0.1:2379
set MICRO_API_NAMESPACE=api.jtthink.com
micro web

默认监听8082端口

http://localhost:8082/registry

访问就能看到我们注册在里面的服务了。点击我们的服务还能看到各个方法的调用方法

我们通过micro工具调用:

 

列出服务
$ micro.exe --registry etcd --registry_address 127.0.0.1:2379 list services
go.micro.http.broker
go.micro.registry
go.micro.web
httpprodservice
prodservice

获取服务信息
$ micro.exe --registry etcd --registry_address 127.0.0.1:2379 get service prodservice
service  prodservice

version 2020.03.21.14.29

ID      Address Metadata
prodservice-8f83af4e-43d2-4c16-843b-0c61ef4decf5        192.168.56.1:8011       protocol=mucp,registry=etcd,server=mucp,transport=http,broker=http

Endpoint: ProdService.GetProdsDetail

Request: {
        size int32
        prod_id int32
}

Response: {
        data ProdModel {
                prod_id int32
                prod_name string
        }
}


Endpoint: ProdService.GetProdsList

Request: {
        size int32
        prod_id int32
}

Response: {
        data []ProdModel
}

调用服务:
$ micro.exe --registry etcd --registry_address 127.0.0.1:2379 call prodservice ProdService.GetProdsList "{\"size\":3}"
{
        "data": [
                {
                        "prod_id": 100,
                        "prod_name": "prodName100"
                },
                {
                        "prod_id": 101,
                        "prod_name": "prodName101"
                },
                {
                        "prod_id": 102,
                        "prod_name": "prodName102"
                }
        ]
}

 

3. go-micro启动一个http服务,此服务调用后端的grpc服务:

      gin整合到go-micro中,此服务可以不注册到服务发现中心

     此项目的pb要和后台的grpc服务pb一致,这样才能通过grpc去调用,生成pb之后编写项目:

package main

import (
    "context"
    "fmt"
    "github.com/micro/go-micro"
    "github.com/micro/go-micro/client"
    "github.com/micro/go-micro/metadata"
    "github.com/micro/go-micro/registry"
    "github.com/micro/go-micro/web"
    "github.com/micro/go-plugins/registry/consul"
    "github.com/micro/go-plugins/registry/etcd"
    grpcSvc "go-micro-study/Services"
    "go-micro-study/webLib"
)

type logWrapper struct {
    client.Client
}

func (l *logWrapper)Call(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error  {
    fmt.Println("调用接口")
    md, _ := metadata.FromContext(ctx)
    fmt.Printf("[Log Wrapper] ctx: %v service: %s method: %s\n", md, req.Service(), req.Endpoint())
    return l.Client.Call(ctx, req, rsp, opts...)
}

func NewLogWrapper(c client.Client) client.Client  {
    return &logWrapper{c}
}

func main()  {
    //consul已经不再使用
    //需要导入 "github.com/micro/go-plugins/registry/consul" 这个路径才行,
    //而不是使用go-micro内置的
   // consulReg := consul.NewRegistry(
    //    registry.Addrs("192.168.1.101:8500"), //服务发现地址,也就是前
        // 面启动的consul
   // )
   //使用etcd作为注册中心
   etcdReg := etcd.NewRegistry(registry.Addrs("127.0.0.1:2379"))
  
    mySvc := micro.NewService(
        micro.Name("prodservice.client"),
        micro.WrapClient(NewLogWrapper), //包装log函数
        )
    //后端grpc服务的client
    prodSvc := grpcSvc.NewProdService("prodservice", mySvc.Client())
    httpSvc := web.NewService(
        web.Name("httpprodservice"), //服务名称
        web.Address(":8001"), //监听端口
        //将gin引入, 里面包含gin注册的http路由,参数prodSvc用于调用后端grpc服务
        web.Handler(webLib.NewGinRouter(prodSvc)), 
        web.Registry(etcdReg), //将服务注册中心引入
     )


    httpSvc.Init()
    httpSvc.Run() //启动
}

gin路由(即包"go-micro-study/webLib"的内容):

func NewGinRouter(prodSvc grpcSvc.ProdService) *gin.Engine {
    ginRouter := gin.Default() //gin web框架
    ginRouter.Use(InitMiddleware(prodSvc))
    v1Group := ginRouter.Group("/v1")
    {
        v1Group.Handle("POST", "/prods", GetProdList) //组成 POST /v1/prods 路由
    }

    return ginRouter
}

//middleware
//为了将grpccli封装到context中
func InitMiddleware(prodSvc grpcSvc.ProdService) gin.HandlerFunc  {
    return func(context *gin.Context) {
        context.Keys = make(map[string]interface{})
        context.Keys["prodservice"] = prodSvc

        context.Next()
    }
}
//统一错误处理
func ErrorMiddleware() gin.HandlerFunc  {
    return func(context *gin.Context) {
        defer func() {
            if r := recover(); r != nil {
                context.JSON(500, gin.H{"status":fmt.Sprintf("%s", r)})
                context.Abort()
            }
            context.Next()
        }()
    }
}

//handler
func defaultProds() (resp *grpcSvc.ProdListResp, err error) {
    data := make([]*grpcSvc.ProdModel, 0)
    for i := 0; i < 5; i++ {
        data = append(data, &grpcSvc.ProdModel{ProdId:int32(20+i), ProdName:"prodname"+strconv.Itoa(i)})
    }

    resp = &grpcSvc.ProdListResp{Data:data} //调用grpc server
    return
}

func GetProdList(ginCtx *gin.Context)  {
        var req grpcSvc.ProdsReq
        var resp *grpcSvc.ProdListResp

    prodSvc := ginCtx.Keys["prodservice"].(grpcSvc.ProdService)
        err := ginCtx.Bind(&req)
        if err != nil {
            ginCtx.JSON(500, gin.H{"status":err.Error()})
        } else {
            resp, err =  prodSvc.GetProdsList(context.Background(), &req)
            ginCtx.JSON(200, gin.H{"data": resp.Data})
            return
            //熔断代码改造,这是在每一个grpc调用钱包装熔断器,后面讲怎么封装
            //1.配置configuration
            configA := hystrix.CommandConfig{
                Timeout: 1000,
            }
            //2.配置command
            hystrix.ConfigureCommand("getprods", configA)
            //3. 执行,使用Do方法
           err = hystrix.Do("getprods", func() error {
                resp, err =  prodSvc.GetProdsList(context.Background(), &req)
                return err
            }, func(e error) error {
               resp, err = defaultProds()
                return e
           })
            resp, err =  prodSvc.GetProdsList(context.Background(), &req)
            if err != nil {
                ginCtx.JSON(500, gin.H{"status":err.Error()})
            }
            ginCtx.JSON(200, gin.H{"data": resp.Data})
        }
}

func PanicError(err error)  {
    if err != nil {
        panic(err)
    }
}

func GetProdDetail(ginCtx *gin.Context)  {
    var prodReq grpcSvc.ProdsReq //此pb结构体添加注解,表示要支持uri可变参数
    PanicError(ginCtx.BindUri(&prodReq))  //这里支持uri带可变参数
    prodSvc := ginCtx.Keys["prodservice"].(grpcSvc.ProdService)
    resp, _ := prodSvc.GetProdsDetail(context.Background(), &prodReq)
    ginCtx.JSON(200, gin.H{"data": resp.Data})

}

3. protocol中使用http uri占位符:

——即protocol定义的结构体支持类似 /v1/prods/:pid 占位符。其中pid表示商品ID,根据实际请求变动

syntax = "proto3";
package Service;

import "models.proto";

message ProdsReq {
    // @inject_tag: json:"size", form:"size"  //注意这里的form表示支持uri带可变参数
    int32 size = 1;
    // @inject_tag: uri:"pid"
    int32 prod_id = 2;
}

message ProdListResp {
    repeated ProdModel data = 1;
}

message ProdDetailtResp {
    ProdModel data = 1;
}

service ProdService {
    rpc GetProdsList(ProdsReq)  returns (ProdListResp);
    rpc GetProdsDetail(ProdsReq)  returns (ProdDetailtResp);
}

message ProdModel {
    // @inject_tag: json:"pid"  //protoc-go-inject-tag 这个工具会把有 @inject_tag 标志的pb tag 改成我们想要的
    int32 prod_id = 1;
    // @inject_tag: json:"prod_name", form:"size" //多种类型:json 和 form
    string prod_name = 2;
}

 使用protoc-go-inject-tag工具生成tag:

protoc --micro_out=../ --go_out=../ models.proto
protoc --micro_out=../ --go_out=../ prodService.proto
protoc-go-inject-tag -input=../models.pb.go
protoc-go-inject-tag -input=../prodService.pb.go  //这个对注解进行处理

 binduri

func GetProdDetail(ginCtx *gin.Context)  {
    var prodReq grpcSvc.ProdsReq
    PanicError(ginCtx.BindUri(&prodReq)) //gin上下文中使用BindUri绑定带有uri注解的pb
    prodSvc := ginCtx.Keys["prodservice"].(grpcSvc.ProdService)
    resp, _ := prodSvc.GetProdsDetail(context.Background(), &prodReq)
    ginCtx.JSON(200, gin.H{"data": resp.Data})

}

这样就可以让pb支持http的uri占位符了

4. 熔断插件

在http调用后端的go-micro微服务时,我们在newMicro的时候包装熔断插件

熔断类及其方法,尤其是Call(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error  方法:

package main

...

func main()  {

    etcdReg := etcd.NewRegistry(registry.Addrs("127.0.0.1:2379"))
    mySvc := micro.NewService(
        micro.Name("prodservice.client"),
        micro.WrapClient(NewLogWrapper),
        micro.WrapClient(wrappers.NewProdsWrapper), //这里注册了熔断插件
        )

...
}

熔断插件:
package wrappers

import (
    "context"
    "github.com/afex/hystrix-go/hystrix"
    "github.com/micro/go-micro/client"
    grpcSvc "go-micro-study/Services"
    "strconv"
)

type ProdsWrapper struct {
    client.Client
}

func NewProdsWrapper(c client.Client) client.Client {
    return &ProdsWrapper{c}
}

func (l *ProdsWrapper)Call(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error  {
    cmdName := req.Service()+"."+req.Endpoint()
    //1.配置configuration
    configA := hystrix.CommandConfig{
        Timeout: 1000,
        RequestVolumeThreshold: 2, //阈值:意思是有20个请求才进行错误百分比计算
        ErrorPercentThreshold:50, //错误百分比 20% 的错误发生之后,就直接执行降级方法
        SleepWindow: 5000, //过多少毫秒之后重新尝试后端被降级的服务
    }
    //2.配置command
    hystrix.ConfigureCommand(cmdName, configA)

    return hystrix.Do(cmdName,
        func() error {
            return l.Client.Call(ctx,req, rsp, opts...)
        },
        func(e error) error {
            //defaultProds(rsp)
            defaultData(rsp)
            return nil
        })
}

//通用降级方法
func defaultData(rsp interface{})  {
    switch t := rsp.(type) {
    case *grpcSvc.ProdListResp:
        defaultProds(rsp)
    case *grpcSvc.ProdDetailtResp:
        t.Data = &grpcSvc.ProdModel{ProdId:int32(10), ProdName:"降级商品详情"+strconv.Itoa(10)}
    default:

    }
}

//商品列表详情
func defaultProds(rsp interface{})  {
    data := make([]*grpcSvc.ProdModel, 0)
    for i := 0; i < 5; i++ {
        data = append(data, &grpcSvc.ProdModel{ProdId:int32(20+i), ProdName:"prodname"+strconv.Itoa(i)})
    }
    resp := rsp.(*grpcSvc.ProdListResp)
    resp.Data = data
    return
}

一般Call方法中使用hystrix的步骤是:

 1)定义一个配置 configA

 2) 调用hystrix.ConfigureCommand(cmdName, configA)将配置和一个名字绑定到hystrix包中

 3)然后调用hystrixDo(cmdName),就可以调用我们定义的配置了,后边的两个回调函数分别是正常业务函数和降级函数

启动服务之后,就可以通过postMan调用http,http再去调用grpc服务了。

 

你可能感兴趣的:(Golang)