概览
KiteX 是 bytedance 开源的高性能 RPC 框架,实现了高吞吐、高负载、高性能等居多特性,具体请看 KiteX 的实践,文章介绍多传输协议、消息协议时,说到 KiteX 支持的协议类型:Thrift、Protobuf 等,今天我们主要来实践如何利用 KiteX 基于对应的 IDL 生成对应协议的代码。
Thrift 简介
Thrift 本身是一软件框架(远程过程调用框架),用来进行可扩展且跨语言的服务的开发。它结合了功能强大的软件堆栈和代码生成引 擎,以构建在 C , Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, and OCaml 这些编程语言间无缝结合的、高效的服务。同时,作为 IDL(接口定义语言 Interface Definition Language),允许你定义一个简单的定义文件中的数据类型和服务接口,以作为输入文件,编译器生成代码用来方便地生成 RPC 客户端和服务器通信的无缝跨编程语言。
Protobuf 简介
Protobuf 全称是 Google Protocol Buffer,是一种高效轻便的结构化数据存储方式,用于数据的通信协议、数据存储等。相对比 XML 来说,其特点:
- 语言无关,平台无关
- 高效
- 扩展性、兼容性更强
基于 IDL 的 KiteX 实践
在 RPC 框架中,我们知道,服务端与客户端通信的前提是远程通信,但这种通信又存在一种关联,那就是通过一套相关的协议(消息、通信、传输等)来规范,但客户端又不用关心底层的技术实现,只要定义好了这种通信方式即可。
在 KiteX 中,也提供了一种生成代码的命令行工具:kitex,目前支持 thrift、protobuf 等 IDL,并且支持生成一个服务端项目的骨架。
安装命令行工具
环境
- 如果您之前未搭建 Golang 开发环境, 可以参考 Golang 安装
- 推荐使用最新版本的 Golang,我们保证最新三个正式版本的兼容性(现在 >= v1.16)。
- 确保打开 go mod 支持 (Golang >= 1.15 时,默认开启)
- kitex 暂时没有针对 Windows 做支持,如果本地开发环境是 Windows 建议使用 WSL2
安装
- 确保 GOPATH 环境变量已经被正确地定义(例如 export GOPATH=~/go)并且将
GOPATH/bin:$PATH);请勿将 GOPATH 设置为当前用户没有读写权限的目录
- 安装 kitex:go install github.com/cloudwego/kitex/tool/cmd/kitex@latest
- 安装 thriftgo:go install github.com/cloudwego/thriftgo@latest
安装成功后,执行 kitex --version
可以看到如下信息:
$ kitex --version
v0.4.2
如果在安装阶段发生问题,可能主要是由于对 Golang 的不当使用造成的,需要逐一排查。
编写一个 IDL
我们先新建一个项目:hz-kitex-examples
,新建完之后,我们在该项目下新建一个目录:idl,然后我们编写一个 thrift IDL:
接下来,我们编写这个 IDL:
代码语言:javascript复制namespace go hello
struct ReqBody {
1: string name
2: i32 type
3: string email
}
struct Request {
1: string data
2: string message
3: ReqBody reqBody
}
struct Msg {
1: i64 status
2: i64 code
3: string msg
}
struct Response {
1: Msg msg
2: string data
}
service HelloService {
Response echo(1: Request req)
Response testHello4Get(1: Request req)
Response testHello4Post(1: Request req)
}
这里我们定义了一个命名空间:hello
,这个是代表生成的代码中有一个目录:hello,然后我们编写一个请求对象:ReqBody
,接着定义一个泛对象,包括了那个请求对象,这块没要求,自己定义好就行,同时我们定义了响应对象Response
,此外,我们还定义了一个类,类中存在三个函数方法。
生成代码
在定义完 IDL 后,我们来看如何生成代码呢?直接执行如下命令:
代码语言:javascript复制kitex -module "hz-kitex-examples" -thrift frugal_tag -service helloserver idl/hello.thrift
这里有几个参数 tag:
-module module_name
- 该参数用于指定生成代码所属的 Go 模块,会影响生成代码里的 import path。
- 如果当前目录是在
GOPATH/src 开始的相对路径作为 import path 前缀。例如,在 $GOPATH/src/example.com/hello/world 下执行 kitex,那么 kitex_gen/example_package/example_package.go 在其他代码代码里的 import path 会是 example.com/hello/world/kitex_gen/example_package。
- 如果当前目录不在 $GOPATH/src 下,那么必须指定该参数。
- 如果指定了 -module 参数,那么 kitex 会从当前目录开始往上层搜索 go.mod 文件
- 如果不存在 go.mod 文件,那么 kitex 会调用 go mod init 生成 go.mod;
- 如果存在 go.mod 文件,那么 kitex 会检查 -module 的参数和 go.mod 里的模块名字是否一致,如果不一致则会报错退出;
- 最后,go.mod 的位置及其模块名字会决定生成代码里的 import path。
-service service_name
- 使用该选项时,kitex 会生成构建一个服务的脚手架代码,参数 service_name 给出启动时服务自身的名字,通常其值取决于使用 Kitex 框架时搭配的服务注册和服务发现功能。
对于当前项目,我们执行如下:
代码语言:javascript复制kitex -module "hz-kitex-examples" -thrift frugal_tag -service helloserver idl/hello.thrift
由于当前项目不在环境路径下,需要指定 go.mod 所在的目录模块的名称,同时,我们指定一个服务名。
这样在执行后,我们会发现生成的目录结构如下图:
在生成的目录中根目录是kitex_gen
,代表是 kitex 工具生成的,其次其目录下有一个 hello 目录,这是代表 IDL 文件中的 ns,在其下面有一个文件:定义了请求对象与响应对象的序列化、传输信息的读写等操作。
在其下面还存在一个 service 目录,用来生成跟客户端与服务端相关的 service 处理逻辑。其中也定义了 service 中处理的方法信息。
同时,我们可以看到生成了服务端的基础框架:
服务端
新建一个server
目录,然后在里面新建一个项目hello
,此时把生成服务端的骨架代码拷贝到里面:
拷贝完之后,我们可以丰满服务端的函数的逻辑,以 Echo 函数为例:
代码语言:javascript复制func (s *HelloApi) Echo(ctx context.Context, req *api.Request) (resp *api.Response, err error) {
klog.Info("hello service enter: " GetIpAddr2())
resp = &api.Response {
Msg: &api.Msg {
Status: 200,
Code: 10000,
Msg: req.Message,
},
Data: req.Message,
}
return resp, nil
}
func GetIpAddr2() string {
conn, err := net.Dial("udp", "8.8.8.8:53")
if err != nil {
klog.Error(err)
return ""
}
localAddr := conn.LocalAddr().(*net.UDPAddr)
// 192.168.1.20:61085
ip := strings.Split(localAddr.String(), ":")[0]
return ip
}
然后再定义启动函数:
代码语言:javascript复制svr := hello.NewServer(new(api.HelloApi),
server.WithServiceAddr(&net.TCPAddr{Port: 2008}),
server.WithServerBasicInfo(&rpcinfo.EndpointBasicInfo{ServiceName: constants.HelloServiceName}),
server.WithPayloadCodec(thrift.NewThriftCodecWithConfig(thrift.FastRead | thrift.FastWrite)),
server.WithErrorHandler(func(err error) error {
error := errno.ConvertErr(err)
return error
}),
//指定默认 Codec 的包大小限制,默认无限制 option: codec.NewDefaultCodecWithSizeLimit
server.WithCodec(codec.NewDefaultCodecWithSizeLimit(1024 * 1024 * 10)),//10M
server.WithLimit(&limit.Option{MaxConnections: 10000, MaxQPS: 5000}),
//连接多路复用(mux)
server.WithMuxTransport(),
server.WithMetaHandler(transmeta.ServerTTHeaderHandler),
server.WithRegistry(registry.NewNacosRegistry(r1)),
)
在这里,我们定义了服务端的端口:2008,同时被注册到 Nacos。启动之后:
可以看到被注册到 Nacos:
这里注册 Nacos 的代码前面已经讲过了,具体可以看:KiteX 的实践。
客户端
关于客户端,也是一样,新建一个 client 目录,里面新建一个项目customer-service
,新建启动 main 函数,这里与服务端类似不再赘述了。主要注意一点:这里由于需要提供 Http 协议接口,需要结合 Hertz 来进行:
至于 Hertz,它是一个高性能的 Http 微服务框架在后面的文章中会进一步讲解,此处不再赘述。
启动函数新建完后,我们需要初始化一个 RPC 连接的客户端:
此处客户端的初始化,也是基于之前生成的代码:
在这个函数初始化客户端时,需要定义请求的服务名、网络库、负载均衡策略、出错误处理机制等。同时,我们还需要复写需要调用的函数,去调用相关的接口:
写完 RPC 的部分,一个简单的 RPC 协议调用就能串联起来了,此时,我们来简单写下客户端的接口调用:
代码语言:javascript复制func HelloDemo(ctx context.Context, c *app.RequestContext) {
req := &api.Request{Message: "my request"}
// TODO
resp, err := rpc.Echo(context.Background(), req)
if err != nil {
log.Fatal(err)
}
klog.Info(resp)
// TODO
c.JSON(consts.StatusOK, (resp))
}
上面定义的是客户端的接口入口,进入后,会调用 rpc 的部分。同时在调用 RPC 前后,可以有自己的逻辑处理以及响应数据的处理,在 TODO 部分。
启动客户端进行服务注册:
测试
在启动完服务端、客户端后,我们访问客户端的 http 接口:
代码语言:javascript复制http://192.168.6.51:3000/v1/hello/test
我们再多几次进行访问:
发现其访问的性能以及速度还是不错的,这得益于 KiteX 框架中使用了自研的 Netpoll 网络库以及实现了高效的吞吐编解码性能提升。