目前僅支持 Thrift 泛化調(diào)用,通常用于不需要生成代碼的中臺服務(wù)。
應(yīng)用場景:比如中臺服務(wù),可以通過二進(jìn)制流轉(zhuǎn)發(fā)將收到的原始 Thrift 協(xié)議包發(fā)給目標(biāo)服務(wù)。
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
}
若自行編碼,需要使用 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)
? 二進(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
}
HTTP 映射泛化調(diào)用只針對客戶端,要求 Thrift IDL 遵從接口映射規(guī)范。
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')
}
類型:*generic.HTTPRequest
類型:*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)
}
比如增加一個 ?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 ¬BodyStruct{}
}
// 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{}) {
}
Map 映射泛化調(diào)用是指用戶可以直接按照規(guī)范構(gòu)造 Map 請求參數(shù)或返回,Kitex 會對應(yīng)完成 Thrift 編解碼。
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),
},
}
示例 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),
}
類型:map[string]interface{}
類型: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{}
}
類型:map[string]interface{}
類型: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
}
JSON 映射泛化調(diào)用是指用戶可以直接按照規(guī)范構(gòu)造 JSON String 請求參數(shù)或返回,Kitex 會對應(yīng)完成 Thrift 編解碼。
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 |
[]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}"
}
示例 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),
}
類型:JSON string
類型: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
}
類型:JSON string
類型: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
}
HTTP/Map/JSON 映射的泛化調(diào)用雖然不需要生成代碼,但需要使用者提供 IDL。
目前 Kitex 有兩種 IDLProvider 實現(xiàn),使用者可以選擇指定 IDL 路徑,也可以選擇傳入 IDL 內(nèi)容。當(dāng)然也可以根據(jù)需求自行擴(kuò)展 ?generci.DescriptorProvider
?。
p, err := generic.NewThriftFileProvider("./YOUR_IDL_PATH")
if err != nil {
panic(err)
}
所有 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)
若為方便構(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)
更多建議: