前言
目前公司框架準備開源自研的go微服務框架,而HTTP模塊則是用的業界比較成熟的echo框架,考慮到後期框架的使用者會使用HTTP協議訪問GRPC服務,本文章會詳細對這塊的設計以及實現做詳細說明.
如何實現
- 入口:
// 正常往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中間件的要求