Kitex 泛化調(diào)用

2022-04-27 09:49 更新

Kitex 泛化調(diào)用使用指南

目前僅支持 Thrift 泛化調(diào)用,通常用于不需要生成代碼的中臺服務(wù)。

支持場景

  • 二進(jìn)制泛化調(diào)用:用于流量中轉(zhuǎn)場景
  • HTTP映射泛化調(diào)用:用于 API 網(wǎng)關(guān)場景
  • Map映射泛化調(diào)用
  • JSON映射泛化調(diào)用

使用方式示例 

1. 二進(jìn)制泛化調(diào)用

調(diào)用端使用

應(yīng)用場景:比如中臺服務(wù),可以通過二進(jìn)制流轉(zhuǎn)發(fā)將收到的原始 Thrift 協(xié)議包發(fā)給目標(biāo)服務(wù)。

  • 初始化Client
import (
   "github.com/cloudwego/kitex/client/genericclient"
   "github.com/cloudwego/kitex/pkg/generic"
)
func NewGenericClient(destServiceName string) genericclient.Client {
    genericCli := genericclient.NewClient(destServiceName, generic.BinaryThriftGeneric())
    return genericCli
}
  • 泛化調(diào)用

若自行編碼,需要使用 Thrift 編碼格式 thrift/thrift-binary-protocol.md。注意,二進(jìn)制編碼不是對原始的 Thrift 請求參數(shù)編碼,是 method 參數(shù)封裝的 XXXArgs??梢詤⒖?nbsp;github.com/cloudwego/kitex/generic/generic_test.go。

Kitex 提供了 thrift 編解碼包?github.com/cloudwego/kitex/pkg/utils.NewThriftMessageCodec?。

rc := utils.NewThriftMessageCodec()
buf, err := rc.Encode("Test", thrift.CALL, 100, args)
// generic call
resp, err := genericCli.GenericCall(ctx, "actualMethod", buf)

服務(wù)端使用

? 二進(jìn)制泛化 Client 和 Server 并不是配套使用的,Client 傳入正確的 Thrift 編碼二進(jìn)制,可以訪問普通的 Thrift Server。

? 二進(jìn)制泛化 Server 只支持 Framed 或 TTHeader 請求,不支持 Bufferd Binary,需要 Client 通過 Option 指定,如:?client.WithTransportProtocol(transport.Framed)?。

package main

import (
    "github.com/cloudwego/kitex/pkg/generic"
    "github.com/cloudwego/kitex/server/genericserver"
)

func main() {
    g := generic.BinaryThriftGeneric()
    svr := genericserver.NewServer(&GenericServiceImpl{}, g)
    err := svr.Run()
    if err != nil {
            panic(err)
    }
}

type GenericServiceImpl struct {}

// GenericCall ...
func (g *GenericServiceImpl) GenericCall(ctx context.Context, method string, request interface{}) (response interface{}, err error) {
    // request is thrift binary
    reqBuf := request.([]byte)
    // e.g.
    fmt.Printf("Method: %s\n", method))
    result := xxx.NewMockTestResult()
    result.Success = &resp
    respBuf, err = rc.Encode(mth, thrift.REPLY, seqID, result)

    return respBuf, nil
}

2. HTTP 映射泛化調(diào)用

HTTP 映射泛化調(diào)用只針對客戶端,要求 Thrift IDL 遵從接口映射規(guī)范。

IDL 定義示例

namespace go http

struct ReqItem {
    1: optional i64 id(go.tag = "json:\"id\"")
    2: optional string text
}

struct BizRequest {
    1: optional i64 v_int64(api.query = 'v_int64', api.vd = "{#content}gt;0&&{#content}lt;200")
    2: optional string text(api.body = 'text')
    3: optional i32 token(api.header = 'token')
    4: optional map<i64, ReqItem> req_items_map (api.body='req_items_map')
    5: optional ReqItem some(api.body = 'some')
    6: optional list<string> req_items(api.query = 'req_items')
    7: optional i32 api_version(api.path = 'action')
    8: optional i64 uid(api.path = 'biz')
    9: optional list<i64> cids(api.query = 'cids')
    10: optional list<string> vids(api.query = 'vids')
}

struct RspItem {
    1: optional i64 item_id
    2: optional string text
}

struct BizResponse {
    1: optional string T                             (api.header= 'T')
    2: optional map<i64, RspItem> rsp_items           (api.body='rsp_items')
    3: optional i32 v_enum                       (api.none = '')
    4: optional list<RspItem> rsp_item_list            (api.body = 'rsp_item_list')
    5: optional i32 http_code                         (api.http_code = '')
    6: optional list<i64> item_count (api.header = 'item_count')
}

service BizService {
    BizResponse BizMethod1(1: BizRequest req)(api.get = '/life/client/:action/:biz', api.baseurl = 'ib.snssdk.com', api.param = 'true')
    BizResponse BizMethod2(1: BizRequest req)(api.post = '/life/client/:action/:biz', api.baseurl = 'ib.snssdk.com', api.param = 'true', api.serializer = 'form')
    BizResponse BizMethod3(1: BizRequest req)(api.post = '/life/client/:action/:biz/other', api.baseurl = 'ib.snssdk.com', api.param = 'true', api.serializer = 'json')
}

泛化調(diào)用示例

  • Request

類型:*generic.HTTPRequest

  • Response

類型:*generic.HTTPResponse

package main

import (
    "github.com/cloudwego/kitex/client/genericclient"
    "github.com/cloudwego/kitex/pkg/generic"
)

func main() {
    // 本地文件idl解析
    // YOUR_IDL_PATH thrift文件路徑: 舉例 ./idl/example.thrift
    // includeDirs: 指定include路徑,默認(rèn)用當(dāng)前文件的相對路徑尋找include
    p, err := generic.NewThriftFileProvider("./YOUR_IDL_PATH")
    if err != nil {
        panic(err)
    }
    // 構(gòu)造http類型的泛化調(diào)用
    g, err := generic.HTTPThriftGeneric(p)
    if err != nil {
        panic(err)
    }
    cli, err := genericclient.NewClient("destServiceName", g, opts...)
    if err != nil {
        panic(err)
    }
    // 構(gòu)造request,或者從ginex獲取
    body := map[string]interface{}{
        "text": "text",
        "some": map[string]interface{}{
            "id":   1,
            "text": "text",
        },
        "req_items_map": map[string]interface{}{
            "1": map[string]interface{}{
                "id":   1,
                "text": "text",
            },
        },
    }
    data, err := json.Marshal(body)
    if err != nil {
        panic(err)
    }
    url := "http://example.com/life/client/1/1?v_int64=1&req_items=item1,item2,itme3&cids=1,2,3&vids=1,2,3"
    req, err := http.NewRequest(http.MethodGet, url, bytes.NewBuffer(data))
    if err != nil {
        panic(err)
    }
    req.Header.Set("token", "1")
    customReq, err := generic.FromHTTPRequest(req) // 考慮到業(yè)務(wù)有可能使用第三方http request,可以自行構(gòu)造轉(zhuǎn)換函數(shù)
    // customReq *generic.HttpRequest
    // 由于http泛化的method是通過bam規(guī)則從http request中獲取的,所以填空就行
    resp, err := cli.GenericCall(ctx, "", customReq)
    realResp := resp.(*generic.HttpResponse)
    realResp.Write(w) // 寫回ResponseWriter,用于http網(wǎng)關(guān)
}

注解擴(kuò)展

比如增加一個 ?xxx.source='not_body_struct'? 注解,表示某個字段本身沒有對 HTTP 請求字段的映射,需要遍歷其子字段從 HTTP 請求中獲取對應(yīng)的值。使用方式如下:

struct Request {
    1: optional i64 v_int64(api.query = 'v_int64')
    2: optional CommonParam common_param (xxx.source='not_body_struct')
}

struct CommonParam {
    1: optional i64 api_version (api.query = 'api_version')
    2: optional i32 token(api.header = 'token')
}

擴(kuò)展方式如下:

func init() {
        descriptor.RegisterAnnotation(new(notBodyStruct))
}

// 實現(xiàn)descriptor.Annotation
type notBodyStruct struct {
}

func (a * notBodyStruct) Equal(key, value string) bool {
        return key == "xxx.source" && value == "not_body_struct"
}

// Handle 目前支持四種類型:HttpMapping, FieldMapping, ValueMapping, Router
func (a * notBodyStruct) Handle() interface{} {
        return newNotBodyStruct
}

type notBodyStruct struct{}

var newNotBodyStruct descriptor.NewHttpMapping = func(value string) descriptor.HttpMapping {
        return &notBodyStruct{}
}

// get value from request
func (m *notBodyStruct) Request(req *descriptor.HttpRequest, field *descriptor.FieldDescriptor) (interface{}, bool) {
        // not_body_struct 注解的作用相當(dāng)于 step into,所以直接返回req本身,讓當(dāng)前filed繼續(xù)從Request中查詢所需要的值
        return req, true
}

// set value to response
func (m *notBodyStruct) Response(resp *descriptor.HttpResponse, field *descriptor.FieldDescriptor, val interface{}) {
}

3. Map 映射泛化調(diào)用

Map 映射泛化調(diào)用是指用戶可以直接按照規(guī)范構(gòu)造 Map 請求參數(shù)或返回,Kitex 會對應(yīng)完成 Thrift 編解碼。

Map 構(gòu)造

Kitex 會根據(jù)給出的 IDL 嚴(yán)格校驗用戶構(gòu)造的字段名和類型,字段名只支持字符串類型對應(yīng) Map Key,字段 Value 的類型映射見類型映射表。

對于Response會校驗 Field ID 和類型,并根據(jù) IDL 的 Field Name 生成相應(yīng)的 Map Key。

類型映射

Golang 與 Thrift IDL 類型映射如下:

Golang 類型 Thrift IDL 類型
bool bool
int8 i8
int16 i16
int32 i32
int64 i64
float64 double
string string
[]byte binary
[]interface{} list/set
map[interface{}]interface{} map
map[string]interface{} struct
int32 enum

示例

以下面的 IDL 為例:

enum ErrorCode {
    SUCCESS = 0
    FAILURE = 1
}

struct Info {
    1: map<string,string> Map
    2: i64 ID
}

struct EchoRequest {
    1: string Msg
    2: i8 I8
    3: i16 I16
    4: i32 I32
    5: i64 I64
    6: binary Binary
    7: map<string,string> Map
    8: set<string> Set
    9: list<string> List
    10: ErrorCode ErrorCode
    11: Info Info

    255: optional Base Base
}

構(gòu)造請求如下:

req := map[string]interface{}{
                "Msg":    "hello",
                "I8":     int8(1),
                "I16":    int16(1),
                "I32":    int32(1),
                "I64":    int64(1),
                "Binary": []byte("hello"),
                "Map": map[interface{}]interface{}{
                        "hello": "world",
                },
                "Set":       []interface{}{"hello", "world"},
                "List":      []interface{}{"hello", "world"},
                "ErrorCode": int32(1),
                "Info": map[string]interface{}{
                        "Map": map[interface{}]interface{}{
                                "hello": "world",
                        },
                        "ID": int64(232324),
                },
        }

泛化調(diào)用示例 

示例 IDL :

?base.thrift?

namespace py base
namespace go base
namespace java com.xxx.thrift.base

struct TrafficEnv {
    1: bool Open = false,
    2: string Env = "",
}

struct Base {
    1: string LogID = "",
    2: string Caller = "",
    3: string Addr = "",
    4: string Client = "",
    5: optional TrafficEnv TrafficEnv,
    6: optional map<string, string> Extra,
}

struct BaseResp {
    1: string StatusMessage = "",
    2: i32 StatusCode = 0,
    3: optional map<string, string> Extra,
}

?example_service.thrift ?

include "base.thrift"
namespace go kitex.test.server

struct ExampleReq {
    1: required string Msg,
    255: base.Base Base,
}
struct ExampleResp {
    1: required string Msg,
    255: base.BaseResp BaseResp,
}
service ExampleService {
    ExampleResp ExampleMethod(1: ExampleReq req),
}

客戶端使用

  • Request

類型:map[string]interface{}

  • Response

類型:map[string]interface{}

package main

import (
    "github.com/cloudwego/kitex/pkg/generic"
    "github.com/cloudwego/kitex/client/genericclient"
)

func main() {
    // 本地文件idl解析
    // YOUR_IDL_PATH thrift文件路徑: 舉例 ./idl/example.thrift
    // includeDirs: 指定include路徑,默認(rèn)用當(dāng)前文件的相對路徑尋找include
    p, err := generic.NewThriftFileProvider("./YOUR_IDL_PATH")
    if err != nil {
        panic(err)
    }
    // 構(gòu)造map 請求和返回類型的泛化調(diào)用
    g, err := generic.MapThriftGeneric(p)
    if err != nil {
        panic(err)
    }
    cli, err := genericclient.NewClient("destServiceName", g, opts...)
    if err != nil {
        panic(err)
    }
    // 'ExampleMethod' 方法名必須包含在idl定義中
    resp, err := cli.GenericCall(ctx, "ExampleMethod", map[string]interface{}{
        "Msg": "hello",
    })
    // resp is a map[string]interface{}
}

服務(wù)端使用

  • Request

類型:map[string]interface{}

  • Response

類型:map[string]interface{}

package main

import (
    "github.com/cloudwego/kitex/pkg/generic"
    "github.com/cloudwego/kitex/server/genericserver"
)

func main() {
    // 本地文件idl解析
    // YOUR_IDL_PATH thrift文件路徑: e.g. ./idl/example.thrift
    p, err := generic.NewThriftFileProvider("./YOUR_IDL_PATH")
    if err != nil {
        panic(err)
    }
    // 構(gòu)造map請求和返回類型的泛化調(diào)用
    g, err := generic.MapThriftGeneric(p)
    if err != nil {
        panic(err)
    }
    svc := genericserver.NewServer(new(GenericServiceImpl), g, opts...)
    if err != nil {
        panic(err)
    }
    err := svr.Run()
    if err != nil {
        panic(err)
    }
    // resp is a map[string]interface{}
}

type GenericServiceImpl struct {
}

func (g *GenericServiceImpl) GenericCall(ctx context.Context, method string, request interface{}) (response interface{}, err error) {
        m := request.(map[string]interface{})
        fmt.Printf("Recv: %v\n", m)
        return  map[string]interface{}{
            "Msg": "world",
        }, nil
}

4. JSON 映射泛化調(diào)用 

JSON 映射泛化調(diào)用是指用戶可以直接按照規(guī)范構(gòu)造 JSON String 請求參數(shù)或返回,Kitex 會對應(yīng)完成 Thrift 編解碼。

JSON 構(gòu)造

Kitex 與 MAP 泛化調(diào)用嚴(yán)格校驗用戶構(gòu)造的字段名和類型不同,JSON 泛化調(diào)用會根據(jù)給出的 IDL 對用戶的請求參數(shù)進(jìn)行轉(zhuǎn)化,無需用戶指定明確的類型,如 int32 或 int64。

對于 Response 會校驗 Field ID 和類型,并根據(jù) IDL 的 Field Name 生成相應(yīng)的 JSON Field。

類型映射

Golang 與 Thrift IDL 類型映射如下:

Golang 類型 Thrift IDL 類型
bool bool
int8 i8
int16 i16
int32 i32
int64 i64
float64 double
string string
[]byte binary
[]interface{} list/set
map[interface{}]interface{} map
map[string]interface{} struct
int32 enum

示例

以下面的 IDL 為例:

enum ErrorCode {
    SUCCESS = 0
    FAILURE = 1
}

struct Info {
    1: map<string,string> Map
    2: i64 ID
}

struct EchoRequest {
    1: string Msg
    2: i8 I8
    3: i16 I16
    4: i32 I32
    5: i64 I64
    6: map<string,string> Map
    7: set<string> Set
    8: list<string> List
    9: ErrorCode ErrorCode
   10: Info Info

    255: optional Base Base
}

構(gòu)造請求如下:

req := {
  "Msg": "hello",
  "I8": 1,
  "I16": 1,
  "I32": 1,
  "I64": 1,
  "Map": "{\"hello\":\"world\"}",
  "Set": ["hello", "world"],
  "List": ["hello", "world"],
  "ErrorCode": 1,
  "Info": "{\"Map\":\"{\"hello\":\"world\"}\", \"ID\":232324}"

}

泛化調(diào)用示例

示例 IDL :

?base.thrift?

namespace py base
namespace go base
namespace java com.xxx.thrift.base

struct TrafficEnv {
    1: bool Open = false,
    2: string Env = "",
}

struct Base {
    1: string LogID = "",
    2: string Caller = "",
    3: string Addr = "",
    4: string Client = "",
    5: optional TrafficEnv TrafficEnv,
    6: optional map<string, string> Extra,
}

struct BaseResp {
    1: string StatusMessage = "",
    2: i32 StatusCode = 0,
    3: optional map<string, string> Extra,
}

?example_service.thrift ?

include "base.thrift"
namespace go kitex.test.server

struct ExampleReq {
    1: required string Msg,
    255: base.Base Base,
}
struct ExampleResp {
    1: required string Msg,
    255: base.BaseResp BaseResp,
}
service ExampleService {
    ExampleResp ExampleMethod(1: ExampleReq req),
}

客戶端使用

  • Request

類型:JSON string

  • Response

類型:JSON string

package main

import (
    "github.com/cloudwego/kitex/pkg/generic"
    "github.com/cloudwego/kitex/client/genericclient"
)

func main() {
    // 本地文件idl解析
    // YOUR_IDL_PATH thrift文件路徑: 舉例 ./idl/example.thrift
    // includeDirs: 指定include路徑,默認(rèn)用當(dāng)前文件的相對路徑尋找include
    p, err := generic.NewThriftFileProvider("./YOUR_IDL_PATH")
    if err != nil {
        panic(err)
    }
    // 構(gòu)造JSON 請求和返回類型的泛化調(diào)用
    g, err := generic.JSONThriftGeneric(p)
    if err != nil {
        panic(err)
    }
    cli, err := genericclient.NewClient("destServiceName", g, opts...)
    if err != nil {
        panic(err)
    }
    // 'ExampleMethod' 方法名必須包含在idl定義中
    resp, err := cli.GenericCall(ctx, "ExampleMethod", "{\"Msg\": \"hello\"}")
    // resp is a JSON string
}

服務(wù)端使用

  • Request

類型:JSON string

  • Response

類型:JSON string

package main

import (
    "github.com/cloudwego/kitex/pkg/generic"
    "github.com/cloudwego/kitex/server/genericserver"
)

func main() {
    // 本地文件idl解析
    // YOUR_IDL_PATH thrift文件路徑: e.g. ./idl/example.thrift
    p, err := generic.NewThriftFileProvider("./YOUR_IDL_PATH")
    if err != nil {
        panic(err)
    }
    // 構(gòu)造JSON請求和返回類型的泛化調(diào)用
    g, err := generic.JSONThriftGeneric(p)
    if err != nil {
        panic(err)
    }
    svc := genericserver.NewServer(new(GenericServiceImpl), g, opts...)
    if err != nil {
        panic(err)
    }
    err := svr.Run()
    if err != nil {
        panic(err)
    }
    // resp is a JSON string
}

type GenericServiceImpl struct {
}

func (g *GenericServiceImpl) GenericCall(ctx context.Context, method string, request interface{}) (response interface{}, err error) {
        // use jsoniter or other json parse sdk to assert request
        m := request.(string)
        fmt.Printf("Recv: %v\n", m)
        return  "{\"Msg\": \"world\"}", nil
}

IDLProvider

HTTP/Map/JSON 映射的泛化調(diào)用雖然不需要生成代碼,但需要使用者提供 IDL。

目前 Kitex 有兩種 IDLProvider 實現(xiàn),使用者可以選擇指定 IDL 路徑,也可以選擇傳入 IDL 內(nèi)容。當(dāng)然也可以根據(jù)需求自行擴(kuò)展 ?generci.DescriptorProvider?。

基于本地文件解析 IDL

p, err := generic.NewThriftFileProvider("./YOUR_IDL_PATH")
 if err != nil {
     panic(err)
 }

基于內(nèi)存解析 IDL 

所有 IDL 需要構(gòu)造成 Map ,Key 是 Path,Value 是 IDL 定義,使用方式如下:

p, err := generic.NewThriftContentProvider("YOUR_MAIN_IDL_CONTENT", map[string]string{/*YOUR_INCLUDES_IDL_CONTENT*/})
    if err != nil {
        panic(err)
    }

// dynamic update
err = p.UpdateIDL("YOUR_MAIN_IDL_CONTENT", map[string]string{/*YOUR_INCLUDES_IDL_CONTENT*/})
if err != nil {
    // handle err
}

簡單實例(為最小化展示 Path 構(gòu)造,并非真實的 IDL):

path := "a/b/main.thrift"
content := `
namespace go kitex.test.server
include "x.thrift"
include "../y.thrift"

service InboxService {}
`
includes := map[string]string{
   path:           content,
   "x.thrift": "namespace go kitex.test.server",
   "../y.thrift": `
   namespace go kitex.test.server
   include "z.thrift"
   `,
}

p, err := NewThriftContentProvider(path, includes)

支持絕對路徑的 include path 尋址

若為方便構(gòu)造 IDL Map,也可以通過 ?NewThriftContentWithAbsIncludePathProvider? 使用絕對路徑作為 Key。

p, err := generic.NewThriftContentWithAbsIncludePathProvider("YOUR_MAIN_IDL_PATH", "YOUR_MAIN_IDL_CONTENT", map[string]string{"ABS_INCLUDE_PATH": "CONTENT"})
    if err != nil {
        panic(err)
    }

// dynamic update
err = p.UpdateIDL("YOUR_MAIN_IDL_PATH", "YOUR_MAIN_IDL_CONTENT", map[string]string{/*YOUR_INCLUDES_IDL_CONTENT*/})
if err != nil {
    // handle err
}

簡單實例(為最小化展示 Path 構(gòu)造,并非真實的 IDL):

path := "a/b/main.thrift"
content := `
namespace go kitex.test.server
include "x.thrift"
include "../y.thrift"

service InboxService {}
`
includes := map[string]string{
   path:           content,
   "a/b/x.thrift": "namespace go kitex.test.server",
   "a/y.thrift": `
   namespace go kitex.test.server
   include "z.thrift"
   `,
   "a/z.thrift": "namespace go kitex.test.server",
}
p, err := NewThriftContentWithAbsIncludePathProvider(path, includes)


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號