项目依赖
推荐使用go module, 我选择go module的最主要原因是足够简单,可以脱离gopath,就跟写nodejs一样,随便在一个地方新建一个文件夹就可以撸代码了,clone下来的源码也可以直接跑,不需要设置各种gopath之类的。go-micro原本也是传统管理依赖来写的,然后有一个issue里,作者说他不会把micro项目的依赖管理改成go module,直到go module成为标准。后来,在一夜之间,作者把全部的micro项目都改成了go module。
项目结构
一个模块使用一个大文件夹,其中又分api、cli、srv三个文件夹。srv文件夹用来写后端微服务,供其他微服务内部访问;api文件夹用来写http接口,供用户访问;cli文件夹用来写客户端, 生成command line程序,接口测试等,各种语言都可以
三层架构
在之前的博客里牌类游戏使用微服务重构笔记(二): micro框架简介:micro toolkit提过,搭配使用micro api网关时,推荐使用三层架构组织服务。
这里就拿商城为例分享我的方案,笔者没有做过电商,仅仅是用来举例,无须在意数据结构的合理性。
-
srv 后端服务
提供最小粒度的服务,一般来说尽可能不考虑业务,如某一类数据的crud。在我的项目中没有在这一层使用验证,因为暂时用不到,任何服务都可以直接访问。
商城里有商品, 所以有一个提供商品的服务 go.micro.srv.good
syntax = "proto3";
package good;
service GoodSrv {
// 创建商品
rpc CreateGood(CreateGoodRequest) returns (CreateGoodResponse) {}
// 查找商品
rpc FindGoods(FindGoodsRequest) returns(FindGoodsResponse) {}
}
// 创建商品请求
message CreateGoodRequest {
string name = 1; // 名称
repeated Image images = 2; // 图片
float price = 3; // 价格
repeated string tagIds = 4; // 标签
}
// 创建商品响应
message CreateGoodResponse {
Good good = 1;
}
// 查找商品请求
message FindGoodsRequest {
repeated string goodIds = 1;
}
// 查找商品响应
message FindGoodsResponse {
repeated Good goods = 1;
}
// 商品数据结构
message Good {
string id = 1; // id
string name = 2; // 名称
repeated Image images = 3; // 图片
float price = 4; // 价格
repeated string tagIds = 5; // 标签
}
// 图片数据结构
message Image {
string url = 1;
bool default = 2;
}
复制代码
这个服务提供了两个接口,创建商品、查找商品。
商品有各种各样的标签,再写一个标签服务 go.micro.srv.tag
syntax = "proto3";
package tag;
service TagSrv {
// 获取标签
rpc FindTags (FindTagsRequest) returns (FindTagsResponse) {}
}
// 获取标签请求
message FindTagsRequest {
repeated string tagIds = 1;
}
// 获取标签响应
message FindTagsResponse {
repeated Tag tags = 1;
}
// 标签数据结构
message Tag {
string id = 1; // id
string tag = 2; // 标签
}
复制代码
-
cli
假如要写一个客户端程序,获取并打印商品列表
python
import requests
import json
def main():
url = "http://localhost:8080/rpc"
headers = {'content-type': 'application/json'}
# Example echo method
payload = {
"endpoint": "GoodSrv.FindGoods",
"service": "go.micro.srv.good",
"request": {}
}
response = requests.post(
url, data=json.dumps(payload), headers=headers).json()
print response
if __name__ == "__main__":
main()
复制代码
运行输出
{u'goods': []}
复制代码
golang
package main
import (
"context"
"github.com/micro/go-micro"
"log"
pb "micro-blog/micro-shop/srv/good/proto"
)
func main() {
s := micro.NewService()
cli := pb.NewGoodSrvService("go.micro.srv.good", s.Client())
response ,err := cli.FindGoods(context.TODO(), &pb.FindGoodsRequest{GoodIds: []string{"1", "2"}})
if err != nil {
panic(err)
}
log.Println("response:", response)
}
复制代码
-
api http接口服务
api层也是微服务,是组装其他各种微服务,完成业务逻辑的地方。主要提供http接口,如果micro网关设置--handler=web 还可以支持websock。现完成一个获取商品列表的http接口。
proto
syntax = "proto3";
import "micro-blog/micro-shop/srv/good/proto/good.proto";
import "micro-blog/micro-shop/srv/tag/proto/tag.proto";
package shop;
service Shop {
// 获取商品
rpc GetGood(GetGoodRequest) returns(GetGoodResponse) {}
}
// 商城物品
message ShopItem {
good.Good good = 1;
repeated tag.Tag tags = 2;
}
// 获取商品请求
message GetGoodRequest {
string goodId = 1;
}
// 获取商品响应
message GetGoodResponse {
ShopItem item = 1;
}
复制代码
使用gin完成api
package main
import (
"context"
"github.com/gin-gonic/gin"
"github.com/micro/go-micro/client"
"github.com/micro/go-micro/errors"
"github.com/micro/go-web"
"log"
"micro-blog/micro-shop/api/proto"
pbg "micro-blog/micro-shop/srv/good/proto"
pbt "micro-blog/micro-shop/srv/tag/proto"
)
// 商城Api
type Shop struct{}
// 获取商品
func (s *Shop) GetGood(c *gin.Context) {
id := c.Query("id")
cli := client.DefaultClient
ctx := context.TODO()
rsp := &shop.GetGoodResponse{}
// 获取商品
goodsChan := getGoods(cli, ctx, []string{id})
goodsReply := <-goodsChan
if goodsReply.err != nil {
c.Error(goodsReply.err)
return
}
if len(goodsReply.goods) == 0 {
c.Error(errors.BadRequest("go.micro.api.shop", "good not found"))
return
}
// 获取标签
tagsChan := getTags(cli, ctx, goodsReply.goods[0].TagIds)
tagsReply := <-tagsChan
if tagsReply.err != nil {
c.Error(tagsReply.err)
return
}
rsp.Item = &shop.ShopItem{
Good: goodsReply.goods[0],
Tags: tagsReply.tags,
}
c.JSON(200, rsp)
}
// 商品获取结果
type goodsResult struct {
err error
goods []*pbg.Good
}
// 获取商品
func getGoods(c client.Client, ctx context.Context, goodIds []string) chan goodsResult {
cli := pbg.NewGoodSrvService("go.micro.srv.good", c)
ch := make(chan goodsResult, 1)
go func() {
res, err := cli.FindGoods(ctx, &pbg.FindGoodsRequest{
GoodIds: goodIds,
})
ch <- goodsResult{goods: res.Goods, err: err}
}()
return ch
}
// 标签获取结果
type tagsResult struct {
err error
tags []*pbt.Tag
}
// 获取标签
func getTags(c client.Client, ctx context.Context, tagIds []string) chan tagsResult {
cli := pbt.NewTagSrvService("go.micro.srv.tag", client.DefaultClient)
ch := make(chan tagsResult, 1)
go func() {
res, err := cli.FindTags(ctx, &pbt.FindTagsRequest{TagIds: tagIds})
ch <- tagsResult{tags: res.Tags, err: err}
}()
return ch
}
func main() {
// Create service
service := web.NewService(
web.Name("go.micro.api.shop"),
)
service.Init()
// Create RESTful handler (using Gin)
router := gin.Default()
// Register Handler
shop := &Shop{}
router.GET("/shop/goods", shop.GetGood)
// 这里的http根路径要与服务名一致
service.Handle("/shop", router)
// Run server
if err := service.Run(); err != nil {
log.Fatal(err)
}
}
复制代码
执行
curl -H 'Content-Type: application/json' \
-s "http://localhost:8080/shop/goods"
复制代码
分析
- 首先使用gin提供的方法从http请求中获取商品id
- 向go.micro.srv.good服务发起rpc 获取商品信息
- 向go.micro.srv.tag服务发起rpc 获取标签信息
- 返回结果
注意
可以发现,如果使用gin,api中的proto定义貌似就没什么意义了,因为获取http请求参数的方法都是gin提供的。如果要使用上这个proto, 可以将micro网关的处理器设置为api micro api --handler=api
,请求将会自动解析成自己写的proto结构,详情可见之前的博客 牌类游戏使用微服务重构笔记(二): micro框架简介:micro toolkit 处理器章节
不过也可以使用gin提供的c.BindJSON
c.BindQuery
来手动解析成proto结构
Token认证
上文中的获取商品列表的http请求是没有任何认证的, 谁都可以进行访问, 实际项目中可能会有验证。http验证的方式非常多,这里以jsonWebToken举例实现一个简单的验证方法。
实现一个用户微服务, 提供签名token和验证token的rpc方法
proto
syntax = "proto3";
package user;
// 用户后端微服务
service UserSrv {
// 签名token
rpc SignToken(SignTokenRequest) returns(PayloadToken) {}
// 验证token
rpc VerifyToken(VerifyTokenRequest) returns(PayloadToken) {}
}
// token信息
message PayloadToken {
int32 id = 1;
string token = 2;
int32 expireAt = 3;
}
// 签名token请求
message SignTokenRequest {
int32 id = 1;
}
// 验证token请求
message VerifyTokenRequest {
string token = 1;
}
复制代码
代码完成后,在api里就可以进行token验证了
// token 验证
payload, err := s.UserSrvClient.VerifyToken(context.Background(), &pbu.VerifyTokenRequest{Token: c.GetHeader("Authorization")})
if err != nil {
c.Error(err)
return
}
复制代码
非常的方便,完全不需要了解认证的代码,更没有响应依赖。如果不想写的到处都是可以放在中间件里完成
错误处理
protoc micro插件生成的代码里把原生pb文件包了一层,每个rpc接口都有一个错误返回值,如果要抛出错误只需要return错误即可
func (g *Greeter) Hello(ctx context.Context, req *proto.HelloRequest, rsp *proto.HelloResponse) error {
return errors.BadRequest("go.micro.srv.greeter", "test error")
}
复制代码
错误的定义使用micro包提供的errors包
错误结构体
// Error implements the error interface.
type Error struct {
Id string `json:"id"` // 错误的id 可根据需求自定义
Code int32 `json:"code"` // 错误码
Detail string `json:"detail"` // 详细信息
Status string `json:"status"` // http状态码
}
// 实现了error接口
func (e *Error) Error() string {
b, _ := json.Marshal(e)
return string(b)
}
复制代码
同时也提供了经常用到的各种错误类型,如
// BadRequest generates a 400 error.
func BadRequest(id, format string, a ...interface{}) error {
return &Error{
Id: id,
Code: 400,
Detail: fmt.Sprintf(format, a...),
Status: http.StatusText(400),
}
}
// Unauthorized generates a 401 error.
func Unauthorized(id, format string, a ...interface{}) error {
return &Error{
Id: id,
Code: 401,
Detail: fmt.Sprintf(format, a...),
Status: http.StatusText(401),
}
}
// Forbidden generates a 403 error.
func Forbidden(id, format string, a ...interface{}) error {
return &Error{
Id: id,
Code: 403,
Detail: fmt.Sprintf(format, a...),
Status: http.StatusText(403),
}
}
// NotFound generates a 404 error.
func NotFound(id, format string, a ...interface{}) error {
return &Error{
Id: id,
Code: 404,
Detail: fmt.Sprintf(format, a...),
Status: http.StatusText(404),
}
}
复制代码
跨域支持
本地开发的时候,使用micro toolkit会遇到跨域问题。在早期的micro toolkit版本中可以通过micro api --cors=true
或micro web --cors=true
来允许跨域,后来因为作者说这个支持并不成熟移除了,见issue。
目前可以通过go-plugins自己编译micro得到支持或者其他方式,自定义header也是一样。micro plugin提供了一些接口,一些特定需求都可以通过插件来解决
package cors
import (
"net/http"
"strings"
"github.com/micro/cli"
"github.com/micro/micro/plugin"
"github.com/rs/cors"
)
type allowedCors struct {
allowedHeaders []string
allowedOrigins []string
allowedMethods []string
}
func (ac *allowedCors) Flags() []cli.Flag {
return []cli.Flag{
cli.StringFlag{
Name: "cors-allowed-headers",
Usage: "Comma-seperated list of allowed headers",
EnvVar: "CORS_ALLOWED_HEADERS",
},
cli.StringFlag{
Name: "cors-allowed-origins",
Usage: "Comma-seperated list of allowed origins",
EnvVar: "CORS_ALLOWED_ORIGINS",
},
cli.StringFlag{
Name: "cors-allowed-methods",
Usage: "Comma-seperated list of allowed methods",
EnvVar: "CORS_ALLOWED_METHODS",
},
}
}
func (ac *allowedCors) Commands() []cli.Command {
return nil
}
func (ac *allowedCors) Handler() plugin.Handler {
return func(ha http.Handler) http.Handler {
hf := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ha.ServeHTTP(w, r)
})
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
cors.New(cors.Options{
AllowedOrigins: ac.allowedOrigins,
AllowedMethods: ac.allowedMethods,
AllowedHeaders: ac.allowedHeaders,
AllowCredentials: true,
}).ServeHTTP(w, r, hf)
})
}
}
func (ac *allowedCors) Init(ctx *cli.Context) error {
ac.allowedHeaders = ac.parseAllowed(ctx, "cors-allowed-headers")
ac.allowedMethods = ac.parseAllowed(ctx, "cors-allowed-methods")
ac.allowedOrigins = ac.parseAllowed(ctx, "cors-allowed-origins")
return nil
}
func (ac *allowedCors) parseAllowed(ctx *cli.Context, flagName string) []string {
fv := ctx.String(flagName)
// no op
if len(fv) == 0 {
return nil
}
return strings.Split(fv, ",")
}
func (ac *allowedCors) String() string {
return "cors-allowed-(headers|origins|methods)"
}
// NewPlugin Creates the CORS Plugin
func NewPlugin() plugin.Plugin {
return &allowedCors{
allowedHeaders: []string{},
allowedOrigins: []string{},
allowedMethods: []string{},
}
}
复制代码
修改micro源码 添加插件
package main
import (
"github.com/micro/micro/plugin"
"github.com/micro/go-plugins/micro/cors"
)
func init() {
plugin.Register(cors.NewPlugin())
}
复制代码
使用
micro api \
--cors-allowed-headers=X-Custom-Header \
--cors-allowed-origins=someotherdomain.com \
--cors-allowed-methods=POST
复制代码
令人疑惑的 NewService
之前的博客中创建一个后端服务,我们使用了
micro.NewService(
micro.Name("go.micro.srv.greeter"),
micro.Version("latest"),
)
复制代码
而在api层的微服务,我们使用了
service := web.NewService(
web.Name("go.micro.api.greeter"),
)
复制代码
api层如果使用api处理器
service := micro.NewService(
micro.Name("go.micro.api.greeter"),
)
复制代码
而使用使用grpc(后文会讲到,我们又要使用
service := grpc.NewService(
micro.Name("go.micro.srv.greeter"),
)
复制代码
~hat the *uck?
其实这都是micro特意这样设计的,目的是为了即使从http传输改变到grpc, 只需要改变一行代码,其他的什么都不用变(感觉很爽...),后面的博客源码分析会详细讲。
之前讲过,micro中微服务的名字定义为[命名空间].[资源类型].[服务名]
的,而micro api代理访问api类型的资源,比如go.micro.api.greeter
,micro web代理访问web类型的资源,比如go.micro.web.greeter
web类型的资源与web.NewService
是没什么关系的,还是要看资源类型的定义,上文中我们使用到了gin框架或者websocket不使用service提供的server而使用web提供的server因此使用web.NewService
来创建服务,后面分析源码之后就更清楚了
下面以web.NewService
创建一个websocket服务并使用micro api代理来举例
package main
import (
"github.com/micro/go-web"
"gopkg.in/olahol/melody.v1"
"log"
"net/http"
)
func main() {
// New web service
service := web.NewService(
web.Name("go.micro.api.gateway"),
)
// parse command line
service.Init()
m := melody.New()
m.HandleDisconnect(HandleConnect)
// Handle websocket connection
service.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
m.HandleRequest(w, r)
})
// run service
if err := service.Run(); err != nil {
log.Fatal("Run: ", err)
}
}
// 处理用户连接
func HandleConnect(session *melody.Session) {
log.Println("new connection ======>>>")
}
复制代码
浏览器代码
wsUri = "ws://" + "localhost:8080/gateway"
var print = function(message) {
var d = document.createElement("div");
d.innerHTML = message;
output.appendChild(d);
};
var newSocket = function() {
ws = new WebSocket(wsUri);
ws.onopen = function(evt) {
print('Connection Open');
}
ws.onclose = function(evt) {
print('Connection Closed');
ws = null;
}
ws.onmessage = function(evt) {
print('Onmessage: ' + parseCount(evt));
}
ws.onerror = function(evt) {
print('Error: ' + parseCount(evt));
}
};
复制代码
可以正常连接到websocket(我在项目中是使用micro web来代理websocket 这里仅仅是举例)
本章未完待续
一下想不完使用经验,后续想到哪里会再补充
本人学习golang、micro、k8s、grpc、protobuf等知识的时间较短,如果有理解错误的地方,欢迎批评指正,可以加我微信一起探讨学习