本文相关代码:gitee


前言

在之前的部分,我们分别用gRpc和消息实现了微服务间的相互调用。

这一章,我们重点研究如何运用网关向前端提供统一的http接口调用服务。


具体步骤

一、启动服务

启动task-srv服务:

> go run main.go
2020-09-11 09:55:16  file=v2@v2.9.1/service.go:200 level=info Starting [service] go.micro.service.task
2020-09-11 09:55:16  file=grpc/grpc.go:864 level=info Server [grpc] Listening on [::]:56487
2020-09-11 09:55:16  file=grpc/grpc.go:697 level=info Registry [mdns] Registering node: go.micro.service.task-8d0a083e-b117-4498-bc2f-7b5cb72cc707

二、micro api 网关

之前已经说过micro工具包为我们微服务开发提供了很多强大的辅助功能,现在我们来试试它提供的API网关服务。

2.1 启动网关

在另一个命令行窗口,运行micro网关命令:

> micro api --namespace=go.micro --type=service
2020-09-11 11:45:42  file=api/api.go:285 level=info service=api Registering API Default Handler at /
2020-09-11 11:45:42  file=http/http.go:90 level=info service=api HTTP API Listening on [::]:8080
2020-09-11 11:45:42  file=v2@v2.9.1/service.go:200 level=info service=api Starting [service] go.micro.api
2020-09-11 11:45:42  file=grpc/grpc.go:864 level=info service=api Server [grpc] Listening on [::]:58653
2020-09-11 11:45:42  file=grpc/grpc.go:697 level=info service=api Registry [mdns] Registering node: go.micro.api-227b83a3-686c-4766-a44f-b15544cda1b6

可以看到网关自身以go.micro.api-227b83a3-686c-4766-a44f-b15544cda1b6注册为微服务,同时监听8080端口对外提供web服务。
micro api命令默认namespace=go.micro, type=api

需要注意的是,一个正常的三层服务架构是这样的:

服务路径说明
micro apilocalhost:8080作为http入口点
api服务go.micro.api.XXX为面向公众提供服务
后端服务go.micro.srv.XXX内部范围服务

这里我们强行设置--type=service主要是为了展示网关功能,正常情况不建议这么做。

2.2 service not found 异常处理

这里我必须要强烈吐槽,本来根据micro new命令的经验,我第一时间就想到了上面的命令写法,但是当时在学习,很多博主东拼西凑的教程里,经常出现这样一种写法micro api --namespace=go.micro.service,你要是跟着这么写,web调用时就会看到:

{
    "id": "go.micro.service.api",
    "code": 500,
    "detail": "service not found",
    "status": "Internal Server Error"
}

实际上翻阅官方早期文档会发现,这种写法早就已经被namespace + type替代了。

2.3 接口调用

网关默认会以服务名/api名/方法名的方式将服务接口映射为http路径,接受各种类型(POST/GET/…)的请求:

http://{host}:{post}/{serverName}/{apiName}/{methodName}

接口调用
至此,一个可用的http服务就完成了。但从后端来看他很简陋,同时也并不是标准的restful接口,下面我们自己实现一个restful标准的API服务。


三、编写api服务

正如前面所说,一个正常的三层服务架构是这样的:

服务路径说明
micro apilocalhost:8080作为http入口点
api服务go.micro.api.XXX为面向公众提供服务
后端服务go.micro.srv.XXX内部范围服务

正常开发不应直接暴露内网的gRPC接口,而是编写专门的api服务,由api服务定义向外暴露那些接口。

3.1 安装gin

不明白网上为什么普遍教程都使用go-restful来编写API服务,这里我还是推荐我平时使用比较多的gin框架:

go get github.com/gin-gonic/gin

3.2 修改task.proto

gin框架支持自动绑定请求参数到struct,并可以根据用户配置对参数进行校验,主要要在struct的tag增加form参数。
原则上,我们应该为api接口参数设置专用的struct,不过这只是个演示项目,业务也很简单,我们就直接复用task.ptoto文件里的消息结构,仍然使用之前的protoc-go-inject-tag工具为生成文件增加form tag:

//声明proto本版
syntax = "proto3";
//服务名
package go.micro.service.task;
//生成go文件的包路径
option go_package = "proto/task";

//定义task服务的接口,主要是增删改查
//结构非常类似于go语言的interface定义,只是返回值必须用括号包裹,且不能使用基本类型作为参数或返回值
service TaskService {
  rpc Create(Task)returns (EditResponse){}
  rpc Delete(Task)returns (EditResponse){}
  rpc Modify(Task)returns (EditResponse){}
  rpc Finished(Task)returns (EditResponse){}
  rpc Search(SearchRequest)returns (SearchResponse){}
}

//下面是消息体message的定义,可以暂时理解为go中的struct,其中的1,2,3...是每个变量唯一的编码
message Task {
  //每条任务的ID,本项目中对应mongodb记录的"_id"字段
  //@inject_tag: bson:"_id" form:"id"
  string id = 1;
  //任务主体文字
  //@inject_tag: bson:"body" form:"body"
  string body = 2;
  //用户设定的任务开始时间戳
  //@inject_tag: bson:"startTime" form:"startTime"
  int64 startTime = 3;
  //用户设定的任务截止时间戳
  //@inject_tag: bson:"endTime" form:"endTime"
  int64 endTime = 4;
  //任务是否已完成
  //@inject_tag: bson:"isFinished" form:"isFinished"
  int32 isFinished = 5;
  //用户实际完成时间戳
  //@inject_tag: bson:"finishTime" form:"finishTime"
  int64 finishTime = 6;
  //任务创建时间
  //@inject_tag: bson:"createTime" form:"createTime"
  int64 createTime = 7;
  //任务修改时间
  //@inject_tag: bson:"updateTime" form:"updateTime"
  int64 updateTime = 8;
  //用户ID
  //@inject_tag: bson:"userId" form:"userId"
  string userId=9;
}

//增删改接口返回参数
message EditResponse {
  //操作返回的消息
  string msg = 1;
}
//查询接口的参数
message SearchRequest{
  //分页查询页码,从第一页开始
  //@inject_tag: form:"pageSize"
  int64 pageSize = 1;
  //分页查询每页数量,默认20
  //@inject_tag: form:"pageCode"
  int64 pageCode = 2;
  // 排序字段
  //@inject_tag: form:"sortBy"
  string sortBy = 3;
  // 顺序 -1降序 1升序
  //@inject_tag: form:"order"
  int32 order=4;
  //关键字模糊查询任务body字段
  //@inject_tag: form:"keyword"
  string keyword = 5;
}

message SearchResponse{
  //分页查询页码,从第一页开始
  //@inject_tag: form:"pageSize"
  int64 pageSize = 1;
  //分页查询每页数量,默认20
  //@inject_tag: form:"pageCode"
  int64 pageCode = 2;
  // 排序字段
  //@inject_tag: form:"sortBy"
  string sortBy = 3;
  // 顺序 -1降序 1升序
  //@inject_tag: form:"order"
  int32 order=4;
  //数据总数
  //@inject_tag: form:"total"
  int64 total = 5;
  //具体数据,这里repeated表示可以出现多条,类似于go中的slice
  //@inject_tag: form:"rows"
  repeated Task rows = 6;
}

3.3 创建新项目

我们再创建一个新项目task-api

mkdir task-api

此时整个项目目录如下:
在这里插入图片描述
新建并编辑task-api/main.go

package main

import (
	"github.com/gin-gonic/gin"
	"github.com/micro/go-micro/v2/client"
	"github.com/micro/go-micro/v2/web"
	pb "go-todolist/task-srv/proto/task"
	"log"
)

// task-srv服务的restful api映射
func main() {
	g := gin.Default()
	service := web.NewService(
		web.Name("go.micro.api.task"),
		web.Address(":8888"),
		web.Handler(g),
	)
	cli := pb.NewTaskService("go.micro.service.task", client.DefaultClient)

	v1 := g.Group("/task")
	{
		v1.GET("/search", func(c *gin.Context) {
			req := new(pb.SearchRequest)
			if err := c.BindQuery(req); err != nil {
				c.JSON(200, gin.H{
					"code": "500",
					"msg":  "bad param",
				})
				return
			}
			if resp, err := cli.Search(c, req); err != nil {
				c.JSON(200, gin.H{
					"code": "500",
					"msg":  err.Error(),
				})
			} else {
				c.JSON(200, gin.H{
					"code": "200",
					"data": resp,
				})
			}
		})
	}
	service.Init()
	if err := service.Run(); err != nil {
		log.Fatal(err)
	}
}

3.4 REST 映射

因为只是演示,整个调用没有额外的业务逻辑,所有代码就简单写到main.go中了。
新建并编辑go-todolist/task-api/main.go

package main

import (
	"github.com/gin-gonic/gin"
	"github.com/micro/go-micro/v2/client"
	"github.com/micro/go-micro/v2/web"
	pb "go-todolist/task-srv/proto/task"
	"log"
)

// task-srv服务的restful api映射
func main() {
	g := gin.Default()
	service := web.NewService(
		web.Name("go.micro.api.task"),
		web.Address(":8888"),
		web.Handler(g),
	)
	cli := pb.NewTaskService("go.micro.service.task", client.DefaultClient)

	v1 := g.Group("/task")
	{
		v1.GET("/search", func(c *gin.Context) {
			req := new(pb.SearchRequest)
			if err := c.ShouldBind(req); err != nil {
				c.JSON(200, gin.H{
					"code": "500",
					"msg":  "bad param",
				})
				return
			}
			if resp, err := cli.Search(c, req); err != nil {
				c.JSON(200, gin.H{
					"code": "500",
					"msg":  err.Error(),
				})
			} else {
				c.JSON(200, gin.H{
					"code": "200",
					"data": resp,
				})
			}
		})
	}
	service.Init()
	if err := service.Run(); err != nil {
		log.Fatal(err)
	}
}

运行main.go,使用postman访问8888端口,熟悉的restful调用就完成了:
在这里插入图片描述


四、micro api反向代理

完成api服务后,我们当然可以使用传统的nginx反向代理,通过配置文件对外暴露api,但是micro工具包为我们提供了更方便的方式。
再次启动micro 网关,这次我们加一个参数--header=http他会将网关变为一个基于服务发现的http代理:

> micro api --handler=http

现在我们访问micro网关的8080端口,不需要任何额外配置,http接口被自动发现并代理了:
在这里插入图片描述


五、grpc-gateway(选读)

grpc-gateway是一个protoc的插件,能够从proto文件读取gRPC服务定义,并生成将RESTful JSON API转换为gRPC的反向代理服务器。

这是我在网上看到转发最多的一个go-micro集成grpc-gateway的例子,推荐大家可以跟着教程试一试,感受一下他的用法:

go-micro 微服务开发中文手册 - GRPC网关

不论你是否跟着上面链接的教程尝试过grpc-gateway,这里说结论:

  • 优点:实现接口代理非常方便,只需要在proto文件定义路径和请求方式,main.go文件虽然有一点点代码,但几乎可以完全复用不需要大的修改。
  • 缺点:不支持像micro api一样的服务发现能力,需要用户手动指定被代理方的地址,同时多个服务的负载均衡也需要在代码中自己实现。

最后,我选择了功能更加强大的且与整个系统集成度更高的micro api方案

总结

这一章是本系列文章第一部分的最后一章。

现在,我们已经简单体验了go-micro框架的基础功能,能够实现服务的自动注册和发现,基于gRpc和消息实现服务调用,使用默认网关对外提供http服务接口,完成了从零开始的第一步——从无到有。

下一阶段,我们将围绕go-micro的插件设计,学习如何快速且灵活的为项目集成服务治理(etcd)消息队列(nats)熔断器(hystrix)鉴权(JWT)以及链路追踪(jaeger)

支持一下

原创不易,支持一下买杯咖啡,谢谢:p
支持一下

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐