go-echo實現對gprc的代理

前言

目前公司框架準備開源自研的go微服務框架,而HTTP模塊則是用的業界比較成熟的echo框架,考慮到後期框架的使用者會使用HTTP協議訪問GRPC服務,本文章會詳細對這塊的設計以及實現做詳細說明.

如何實現

  1. 入口:
   // 正常往ECHO中註冊路由
	echo.GET("/ping", func(ctx echo.Context) error {
		return ctx.JSON(200, "pong")
	})
	echo.GET("/hehe", func(context echo.Context) error {
		return context.JSON(200,"hello")
	})
	 // GRPC SERVER
	g := greeter.Greeter{}
	// 通過HTTP請求代理GRPC服務
	// xecho.GRPCProxyWrapper():核心方法 如何代理都是根據該方法實現
	//下文會做詳細講解
	echo.GET("/grpc",xecho.GRPCProxyWrapper(g.SayHello),xecho.AccessLogger())
	echo.POST("/grpc-post",xecho.GRPCProxyWrapper(g.SayHello),xecho.AccessLogger())

2.GRPCProxyWrapper:核心方法

//h:grpc 服務引用
func GRPCProxyWrapper(h interface{}) echo.HandlerFunc {
	// 利用反射判斷傳遞的參數是否爲函數類型
	// 下方的回調函數中會反射調用該方法
	t := reflect.TypeOf(h)
	if t.Kind() != reflect.Func {
		panic("reflect error: handler must be func")
	}
	// 該閉包返回需要滿足echo的要求(具體要求:後面會講解)
	return func(c echo.Context) error {
	    //關鍵步驟一:反射獲取GRPC函數參數,並將參數值進行綁定
		var req = reflect.New(t.In(1).Elem()).Interface()
		if err := c.Bind(req); err != nil {
			return ProtoError(c, StatusBadRequest, errBadRequest)
		}
		var md = metadata.MD{}
		// append Header
		for k, vs := range c.Request().Header {
			for _, v := range vs {
				bs := bytes.TrimFunc([]byte(v), func(r rune) bool {
					return r == '\n' || r == '\r' || r == '\000'
				})
				md.Append(k, string(bs))
			}
		}
		ctx := metadata.NewOutgoingContext(context.TODO(), md)
		var inj = inject.New()
		inj.Map(ctx)
		inj.Map(req)
		// 關鍵步驟二: 執行具體的 GRPC 方法
		vs, err := inj.Invoke(h)
		if err != nil {
			return ProtoError(c, StatusInternalServerError, errMicroInvoke)
		}
		if len(vs) != 2 {
			return ProtoError(c, StatusInternalServerError, errMicroInvokeLen)
		}
		repV, errV := vs[0], vs[1]
		if !errV.IsNil() || repV.IsNil() {
			if e, ok := errV.Interface().(error); ok {
				// error logic
				return ProtoError(c, StatusOK, e)
			}
			return ProtoError(c, StatusInternalServerError, errMicroInvokeInvalid)
		}
		if !repV.IsValid() {
			return ProtoError(c, StatusInternalServerError, errMicroResInvalid)
		}
		// 發送帶有狀態代碼和數據的Protobuf JSON響應
		return ProtoJSON(c, StatusOK, repV.Interface())
	}
}

3.ProtoJSON()

func ProtoJSON(c echo.Context, code int, i interface{}) error {
	var acceptEncoding = c.Request().Header.Get(HeaderAcceptEncoding)
	var ok bool
	var m proto.Message
	if m, ok = i.(proto.Message); !ok {
		c.Response().Header().Set(HeaderHRPCErr, "true")
		m = statusMSDefault
	}
	// protobuf output
	if strings.Contains(acceptEncoding, MIMEApplicationProtobuf) {
		c.Response().Header().Set(HeaderContentType, MIMEApplicationProtobuf)
		c.Response().WriteHeader(code)
		bs, _ := proto.Marshal(m)
		_, err := c.Response().Write(bs)
		return err
	}
	// json output
	c.Response().Header().Set(HeaderContentType, MIMEApplicationJSONCharsetUTF8)
	c.Response().WriteHeader(code)
	return jsonpbMarshaler.Marshal(c.Response().Writer, m)
}

4.以上基本上簡單的實現了上述功能,具體細節由於時間問題描述的不是很完善,各位有問題的話,歡迎下方評論.

echo路由註冊解析

func (e *Echo) GET(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
	return e.Add(http.MethodGet, path, h, m...)
}

截取了一段Echo中GET方法
參數:h HandlerFunc

	// HandlerFunc defines a function to serve HTTP requests.
	HandlerFunc func(Context) error

該參數是一個回調函數,當路由註冊後,相對應的請求會由該回調進行處理,GRPCProxyWrapper 中也是基於該回調進行實現。
參數:m …MiddlewareFunc

	// MiddlewareFunc defines a function to process middleware.
	MiddlewareFunc func(HandlerFunc) HandlerFunc

根據該參數我們可以傳遞相關的中間件,比如在最開始的時候我們在調用方法時,就傳遞了自定義的中間件(xecho.AccessLogger)

echo.POST("/grpc-post",xecho.GRPCProxyWrapper(g.SayHello),xecho.AccessLogger())

xecho.AccessLogger()代碼:

func AccessLogger() echo.MiddlewareFunc {
	return func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(ctx echo.Context) (err error) {
			err = next(ctx)
			if trace, ok := xlog.ExtractTraceMD(ctx); ok {
				trace.Info(zap.String("method", ctx.Request().Method))
				trace.Info(zap.Int("code", ctx.Response().Status))
				trace.Info(zap.String("host", ctx.Request().Host))

				if cost := time.Since(trace.BeginTime).Milliseconds(); cost > 500 {
					trace.Warn(zap.Int64("slow", cost))
				}
			}
			return err
		}
	}
}

各位可以根據自己的需求,比如說進行採樣,打點等傳遞自己定義的中間件,當然要滿足ECHO中間件的要求

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章