推薦使用go module, 我選擇go module的最主要緣由是足夠簡單,能夠脫離gopath,就跟寫nodejs同樣,隨便在一個地方新建一個文件夾就能夠擼代碼了,clone下來的源碼也能夠直接跑,不須要設置各類gopath之類的。go-micro本來也是傳統管理依賴來寫的,而後有一個issue裏,做者說他不會把micro項目的依賴管理改爲go module,直到go module成爲標準。後來,在一晚上之間,做者把所有的micro項目都改爲了go module。node
一個模塊使用一個大文件夾,其中又分api、cli、srv三個文件夾。srv文件夾用來寫後端微服務,供其餘微服務內部訪問;api文件夾用來寫http接口,供用戶訪問;cli文件夾用來寫客戶端, 生成command line程序,接口測試等,各類語言均可以python
在以前的博客裏牌類遊戲使用微服務重構筆記(二): micro框架簡介:micro toolkit提過,搭配使用micro api網關時,推薦使用三層架構組織服務。
git
這裏就拿商城爲例分享個人方案,筆者沒有作過電商,僅僅是用來舉例,無須在乎數據結構的合理性。github
提供最小粒度的服務,通常來講儘量不考慮業務,如某一類數據的crud。在個人項目中沒有在這一層使用驗證,由於暫時用不到,任何服務均可以直接訪問。golang
商城裏有商品, 因此有一個提供商品的服務 go.micro.srv.goodweb
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;
}
複製代碼
這個服務提供了兩個接口,建立商品、查找商品。
json
商品有各類各樣的標籤,再寫一個標籤服務 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; // 標籤
}
複製代碼
假如要寫一個客戶端程序,獲取並打印商品列表api
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接口,若是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;
}
複製代碼
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,api中的proto定義貌似就沒什麼意義了,由於獲取http請求參數的方法都是gin提供的。若是要使用上這個proto, 能夠將micro網關的處理器設置爲api micro api --handler=api
,請求將會自動解析成本身寫的proto結構,詳情可見以前的博客 牌類遊戲使用微服務重構筆記(二): micro框架簡介:micro toolkit 處理器章節
不過也可使用gin提供的c.BindJSON
c.BindQuery
來手動解析成proto結構
上文中的獲取商品列表的http請求是沒有任何認證的, 誰均可以進行訪問, 實際項目中可能會有驗證。http驗證的方式很是多,這裏以jsonWebToken舉例實現一個簡單的驗證方法。
實現一個用戶微服務, 提供簽名token和驗證token的rpc方法
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
複製代碼
以前的博客中建立一個後端服務,咱們使用了
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('<span style="color: green;">Connection Open</span>');
}
ws.onclose = function(evt) {
print('<span style="color: red;">Connection Closed</span>');
ws = null;
}
ws.onmessage = function(evt) {
print('<span style="color: blue;">Onmessage: </span>' + parseCount(evt));
}
ws.onerror = function(evt) {
print('<span style="color: red;">Error: </span>' + parseCount(evt));
}
};
複製代碼
能夠正常鏈接到websocket(我在項目中是使用micro web來代理websocket 這裏僅僅是舉例)
正常啓動,正常退出狀況下,服務註冊與服務發現不會有什麼問題。但有些時候服務可能異常退出、或者網絡出現問題,在這種狀況下若是不能及時移除服務,可能會形成訪問異常,解決辦法是增長TTL和Interval,ttl是服務的過時時間,interval是服務的從新註冊時間,這樣的組合相似於心跳
service := micro.NewService(
micro.Name("srv.foo"),
micro.RegisterTTL(time.Second*30),
micro.RegisterInterval(time.Second*15),
)
複製代碼
若是該服務的網絡出現問題,並無退出,那麼30秒後此服務將會失效,網絡恢復正常後,從新加入到服務發現中
一下想不完使用經驗,後續想到哪裏會再補充
本人學習golang、micro、k8s、grpc、protobuf等知識的時間較短,若是有理解錯誤的地方,歡迎批評指正,能夠加我微信一塊兒探討學習