一个rpc框架,最核心的就是如何网络传输数据以及如何处理请求。对于框架使用者来说,比较直接相关的就是序列化(idl)和实现接口(业务逻辑)。本文将会从具体的细节角度来讲述KiteX中请求如何被序列化,以及请求到来时是怎样一步步走到用户定义的接口实现。
首先,来了解一下rpc服务的基本工作流程,大多数有关rpc的文章都会提到这个。基本都是像下图一样,先是根据配置初始化服务,然后服务启动,启动后监听端口,当有请求进来时,将字节码反序列化,进入业务处理,处理完之后再编码成字节码在网络中传回给rpc客户端。
KiteX中各层之间是通过接口衔接的,每个接口在框架内都有相应的实现。虽然增加了可扩展性,但是从一定程度上降低了代码的可读性。在介绍执行流程之前,先给出KiteX各层级的关系,让读者有个初步的印象,在阅读后文中,对某个层级的定位不清楚时,可以翻回来查阅。
有人说,了解组件的第一步,先让它跑起来。所以先写个小demo,后文都是围绕此示例展开。
IDL:
include "base.thrift"
namespace go example.hello.world
struct HelloWorldRequest {
1: required string name,
255: optional base.Base Base,
}
struct HelloWorldResponse {
1: required string msg,
255: optional base.BaseResp BaseResp,
}
service HellowordService {
HelloWorldResponse Greet (1: HelloWorldRequest req)
}
生成的handler:
// HellowordServiceImpl implements the last service interface defined in the IDL.
type HellowordServiceImpl struct{}
// Greet implements the HellowordServiceImpl interface.
func (s *HellowordServiceImpl) Greet(ctx context.Context, req *world.HelloWorldRequest) (resp *world.HelloWorldResponse, err error) {
// TODO: Your code here...
return
}
生成的代码中,main方法如下:
func main() {
svr := hellowordservice.NewServer(new(HellowordServiceImpl))
err := svr.Run()
if err != nil {
log.Println(err.Error())
}
}
所以就从NewServer()方法作为入口开始探索整个流程。
先看一下server的几个重要属性:
type server struct {
opt *internal_server.Options
svcInfo *serviceinfo.ServiceInfo
// actual rpc service implement of biz
handler interface{}
eps endpoint.Endpoint
mws []endpoint.Middleware
svr remotesvr.Server
stopped sync.Once
sync.Mutex
}
// Endpoint represent one method for calling from remote.
type Endpoint func(ctx context.Context, req, resp interface{}) (err error)
// Middleware deal with input Endpoint and output Endpoint.
type Middleware func(Endpoint) Endpoint
Handler: IDL中定义的service实现,即示例中的HellowordServiceImpl
eps: 将业务处理函数以及middlewares串成的一个Endpoint链。(Endpoint是执行rpc method的函数,Middleware是给Endpoint添加一些额外的操作,把一个Endpoint包装成另一个Endpoint)
mws:执行中间件,框架使用者可以在执行完业务逻辑后,返回给客户端之前,增加一个额外的逻辑,比如修改response,打日志等
svr:接口remote.Server的实现,server将handler,eps打包后丢给remote.Server处理。
进入NewServer后,主要是以下两步操作:
1.创建server:
svr := server.NewServer(options...)
创建服务主要做了以下几件比较重要的事情:
a) 初始化remoteServer的option,这几个分别影响了具体的server实现以及编解码方式:
&remote.ServerOption{
TransServerFactory: netpoll.NewTransServerFactory(),
SvrHandlerFactory: detection.NewSvrTransHandlerFactory(),
Codec: codec.NewDefaultCodec()
}
b) 在s.init()中初始化了mws,以及将mws与业务调用组装成一个Endpoint,赋给eps属性。
func (s *server) buildInvokeChain() {
innerHandlerEp := s.invokeHandleEndpoint()
s.eps = endpoint.Chain(s.mws...)(innerHandlerEp)
}
// Chain connect middlewares into one middleware.
func Chain(mws ...Middleware) Middleware {
return func(next Endpoint) Endpoint {
for i := len(mws) - 1; i >= 0; i-- {
next = mws[i](next)
}
return next
}
}
用类似于数学中函数表示,那么eps形如嵌套函数:
mws[0](mws[1](mws[2](...(mws[N](innerHandlerEp)...)))
最内层的先执行,所以执行顺序为:
innerHandlerEp -> mws[N] -> ... ->mws[1] ->mws[0]
即使业务中可能定义了多个rpc接口,这里也都封装在innerHandlerEp这一个Endpoint里面,通过解析Ctx来获取调用的method name,再在svcInfo中根据method name找到相应的method并执行:
func (s *server) invokeHandleEndpoint() endpoint.Endpoint {
return func(ctx context.Context, args, resp interface{}) (err error) {
ri := rpcinfo.GetRPCInfo(ctx)
methodName := ri.Invocation().MethodName()
implHandlerFunc := s.svcInfo.MethodInfo(methodName).Handler()
internal_stats.Record(ctx, ri, stats.ServerHandleStart, nil)
err = implHandlerFunc(ctx, s.handler, args, resp)
if err != nil {
err = kerrors.ErrBiz.WithCause(err)
}
return err
}
}
2.注册服务
上一步中提到从svcInfo中根据method name拿到对应的method,那svcInfo就是通过RegisterService获得的。
if err := svr.RegisterService(serviceInfo(), handler); err != nil {
panic(err)
}
func (s *server) RegisterService(svcInfo *serviceinfo.ServiceInfo, handler interface{}) error {
s.svcInfo = svcInfo // 填充svcInfo
s.handler = handler // 将 HellowordServiceImpl实例绑定到handler
return nil
}
serviceInfo()中最关键的就是一个map,key为method name,value为对应的method:
serviceName := "HellowordService"
handlerType := (*world.HellowordService)(nil)
methods := map[string]kitex.MethodInfo{
"Greet": kitex.NewMethodInfo(greetHandler, newHellowordServiceGreetArgs, newHellowordServiceGreetResult, false),
}
extra := map[string]interface{}{
"PackageName": "world",
}
svcInfo := &kitex.ServiceInfo{
ServiceName: serviceName,
HandlerType: handlerType,
Methods: methods,
PayloadCodec: kitex.Thrift,
KiteXGenVersion: "v1.9.1",
Extra: extra,
}
以上就是初始化过程,其中最重要的几点是:
eps:将mws与接口method组装成的Endpoint
remote.ServerOption中分别给TransServerFactory和SvrHandlerFactory赋了相应的工厂
以上两点在服务启动过程中还会用到。
服务启动主要有几下几个步骤:
以上五步均在代码中予以注释说明:
// Run runs the server.
func (s *server) Run() (err error) {
if s.svcInfo == nil {
return errors.New("no service, use RegisterService to set one")
}
if err = s.check(); err != nil { // 1. 校验有没有注册服务
return err
}
...
s.richRemoteOption() // 2.初始化remote server option
transHdlr, err := s.newSvrTransHandler() // 3. 创建svrTransHandler
if err != nil {
return err
}
s.Lock()
s.svr, err = remotesvr.NewServer(s.opt.RemoteOpt, s.eps, transHdlr) // 4. 创建remoteServer
s.Unlock()
if err != nil {
return err
}
errCh := s.svr.Start() // 5. 启动remoteServer
...
}
接下来基于代码依次解释上述步骤。
先是校验有没有注册服务,也就是svcInfo和handler是否为空:
func (s *server) check() error {
if s.svcInfo == nil {
return errors.New("Run: no service. Use RegisterService to set one")
}
if s.handler == nil || reflect.ValueOf(s.handler).IsNil() {
return errors.New("Run: handler is nil")
}
return nil
}
初始化remote server option,s.richRemoteOption调用两个函数:
initBasicRemoteOption和addBoundHandlers。initBasicRemoteOption是初始化option,其中一个重要的option就是initRPCInfoFunc,它是负责请求进来后,初始化RPCInfo,并把初始化的RPCInfo塞进ctx。这里有两点需要注意:
func (s *server) initBasicRemoteOption() {
remoteOpt := s.opt.RemoteOpt
remoteOpt.SvcInfo = s.svcInfo
remoteOpt.InitRPCInfoFunc = s.initRPCInfoFunc()
remoteOpt.TracerCtl = s.opt.TracerCtl
remoteOpt.Logger = s.opt.Logger
remoteOpt.ReadWriteTimeout = s.opt.Configs.ReadWriteTimeout()
}
func (s *server) initRPCInfoFunc() func(context.Context, net.Addr) (rpcinfo.RPCInfo, context.Context) {
return func(ctx context.Context, rAddr net.Addr) (rpcinfo.RPCInfo, context.Context) {
if ctx == nil {
ctx = context.Background()
}
rpcStats := rpcinfo.AsMutableRPCStats(rpcinfo.NewRPCStats())
if s.opt.StatsLevel != nil {
rpcStats.SetLevel(*s.opt.StatsLevel)
}
// 初始化只有RPCInfo
ri := rpcinfo.NewRPCInfo(
rpcinfo.EmptyEndpointInfo(),
rpcinfo.FromBasicInfo(s.opt.Svr),
rpcinfo.NewServerInvocation(),
rpcinfo.AsMutableRPCConfig(s.opt.Configs).Clone().ImmutableView(),
rpcStats.ImmutableView(),
)
rpcinfo.AsMutableEndpointInfo(ri.From()).SetAddress(rAddr)
ctx = rpcinfo.NewCtxWithRPCInfo(ctx, ri)
return ri, ctx
}
}
addBoundHandlers主要是增加一些拦截器,比如meta信息的拷贝,限流操作等。
还记得在第4大节初始化的时候提过SvrHandlerFactory吗?在初始化remote.ServerOption时指定为detection.NewSvrTransHandlerFactory(),现在派上用场了。
func (s *server) newSvrTransHandler() (handler remote.ServerTransHandler, err error) {
transHdlrFactory := s.opt.RemoteOpt.SvrHandlerFactory
transHdlr, err := transHdlrFactory.NewTransHandler(s.opt.RemoteOpt)
if err != nil {
return nil, err
}
if setter, ok := transHdlr.(remote.InvokeHandleFuncSetter); ok {
setter.SetInvokeHandleFunc(s.eps)
}
transPl := remote.NewTransPipeline(transHdlr)
for _, ib := range s.opt.RemoteOpt.Inbounds {
transPl.AddInboundHandler(ib)
}
for _, ob := range s.opt.RemoteOpt.Outbounds {
transPl.AddOutboundHandler(ob)
}
return transPl, nil
}
transHdlrFactory为组合了http和netpoll两者对应的工厂,所以会初始化http和netpoll对应的handler,后面会根据transServer类型选择对应的handler:
&svrTransHandlerFactory{
http2: nphttp2.NewSvrTransHandlerFactory(),
netpoll: transNetpoll.NewSvrTransHandlerFactory(),
}
http2(kitex/pkg/remote/trans/nphttp2):
--> nphttp2.svrTransHandlerFactory.NewTransHandler()--> nphttp2.svrTransHandler()
netpooll(kitex/pkg/remote/trans/netpoll):
--> netpoll.svrTransHandlerFactory.NewTransHandler()--> netpoll.newSvrTransHandler()
--> trans.svrTransHandler(kitex/pkg/remote/trans/default_server_handler.go)
最终创建的transHdlr为:
// kitex/pkg/remote/trans/detection/server_handler.go
type svrTransHandler struct {
http2 remote.ServerTransHandler --> nethttp2.svrTransHandler
netpoll remote.ServerTransHandler --> trans.svrTransHandler
}
创建完transHdlr之后,绑定前面build好的的eps,也就是真正的请求处理逻辑。这时再结合inbounds和outbounds组成TransPipeline,也就是第2节图中的TransPipeline。
再一次回想第4大节提到的另一个工厂:TransServerFactory,它用来选择创建http型的transServer还是netpoll型的transServer。默认指定为netpoll.NewTransServerFactory()。
// NewServer creates a remote server.
func NewServer(opt *remote.ServerOption, inkHdlFunc endpoint.Endpoint, transHdlr remote.ServerTransHandler) (Server, error) {
transSvr := opt.TransServerFactory.NewTransServer(opt, transHdlr)
s := &server{
opt: opt,
inkHdlFunc: inkHdlFunc, // s.eps
transSvr: transSvr,
}
return s, nil
}
所以transSvr为netpoll.transServer,其成员transHdlr为5.3节创建的TransPipeline:
netpoll.NewTransServerFactory() --> netpollTransServerFactory
opt.TransServerFactory.NewTransServer
--> netpollTransServerFactory.NewTransServer
--> netpoll.transServer{transHdlr:TransPipeline}
故最终创建的remote.Server为:
server{
inkHdlFunc: s.eps
transSvr: netpoll.transServer{ransHdlr:TransPipeline{netHdlr:detection.svrTransHandler}}
}
server已经创建好了,启动就简单多了。先创建一个listener,然后启动transServer:
// Start starts the server and return chan, the chan receive means server shutdown or err happen
func (s *server) Start() chan error {
errCh := make(chan error, 1)
ln, err := s.buildListener()
if err != nil {
errCh <- err
return errCh
}
s.Lock()
s.listener = ln
s.Unlock()
go func() { errCh <- s.transSvr.BootstrapServer() }()
return errCh
}
由于s.transSvr为netpoll.transServer,故BootstrapServer为:
// BootstrapServer implements the remote.TransServer interface.
func (ts *transServer) BootstrapServer() (err error) {
if ts.ln == nil {
return errors.New("listener is nil in netpoll transport server")
}
opts := []netpoll.Option{
netpoll.WithIdleTimeout(ts.opt.MaxConnectionIdleTime),
netpoll.WithReadTimeout(ts.opt.ReadWriteTimeout),
}
ts.Lock()
opts = append(opts, netpoll.WithOnPrepare(ts.onConnActive))
ts.evl, err = netpoll.NewEventLoop(ts.onConnRead, opts...)
ts.Unlock()
if err != nil {
return err
}
return ts.evl.Serve(ts.ln)
}
BootstrapServer基于netpoll库创建了一个eventLoop,并注册了两个方法,onConnActive和onConnRead,并启动了事件监听。
onConnActive在初始化连接的时候执行,主要作用是初始化RPCInfo,并塞到ctx中(注:执行onConnActive时还没解析请求,所以只是初始化RPCInfo,并没有填充值)。其调用链如下:
netpool.transServer.onConnActive()
--> TransPipeline.OnActive()
--> detection.svrTransHandler.onActive()
--> trans.svrTransHandler.onActive()
// OnActive implements the remote.ServerTransHandler interface.
func (t *svrTransHandler) OnActive(ctx context.Context, conn net.Conn) (context.Context, error) {
// init rpcinfo
_, ctx = t.opt.InitRPCInfoFunc(ctx, conn.RemoteAddr())
return ctx, nil
}
onConnRead是处理连接的函数,其调用链和onConnActive类似,最终走到trans.svrTransHandler.OnRead()中。OnRead解析请求,调用inkHdlFunc(s.eps)处理请求。
netpoll不是本文的重点,所以就不做详细介绍了,其核心就是epoll。
至此,KiteX的RPC server启动了,万事俱备,就等待请求到来处理了。
叮叮叮,rpc请求来了。按照前文的介绍,请求到来的处理顺序为:
端口可读 --> onConnActive() --> onConnRead()
onConnActive前文已经介绍了,就是初始化一个待填值的RPCInfo,并置于ctx中。
重头戏自然落到onConnRead中。onConnRead褪去层层包装,最终执行的函数是trans.svrTransHandler.OnRead()。
// OnRead implements the remote.ServerTransHandler interface.
func (t *svrTransHandler) OnRead(ctx context.Context, conn net.Conn) error {
// 1. 解析请求
err = t.Read(ctx, conn, recvMsg)
// 2. 处理请求
ctx, err = t.transPipe.OnMessage(ctx, recvMsg, sendMsg)
// 3. 发送回复
err = t.transPipe.Write(ctx, conn, sendMsg)
}
t.Read()调用t.codec.Decode()进行解析,在初始化的时候,codec选择defaut_codec。经过如下调用链:
t.Read()
--> t.codec.Decode()
--> defaultCodec.Decode()
协议的解析格式如下:
0 1 2 3 4 5 6 7 8 9 a b c d e f 0 1 2 3 4 5 6 7 8 9 a b c d e f
+----------------------------------------------------------------+
| 0| LENGTH |
+----------------------------------------------------------------+
| 0| HEADER MAGIC | FLAGS |
+----------------------------------------------------------------+
| SEQUENCE NUMBER |
+----------------------------------------------------------------+
| 0| Header Size(/32) | ...
+---------------------------------
Header is of variable size:
(and starts at offset 14)
+----------------------------------------------------------------+
| PROTOCOL ID (varint) | NUM TRANSFORMS (varint) |
+----------------------------------------------------------------+
| TRANSFORM 0 ID (varint) | TRANSFORM 0 DATA ...
+----------------------------------------------------------------+
| ... ... |
+----------------------------------------------------------------+
| INFO 0 ID (varint) | INFO 0 DATA ...
+----------------------------------------------------------------+
| ... ... |
+----------------------------------------------------------------+
| |
| PAYLOAD |
| |
+----------------------------------------------------------------+
下面模拟请求本文的示例服务:
func main(){
...
req := &world.HelloWorldRequest{
Name: "aaa",
Base: base.NewBase(),
}
resp, err := helloClient.Greet(context.Background(), req, callopt.WithHostPort("127.0.0.1:8888"))
}
抓包如下:
0 1 2 3 4 5 6 7 8 9 a b c d e f
0000 00 00 01 79 10 00 00 00 00 00 00 01 00 27 00 00 ...y.........'..
-----------|-----|-----|-----------|-----|-----
length |magic|flags| seq ID |header|header
| | | |size/4|
------------------------------------------------
0010 10 00 0c 00 01 00 06 66 72 61 6d 65 64 00 02 00 .......framed...
------------------------------------------------
header ...
------------------------------------------------
0020 35 30 32 31 36 35 33 38 31 30 32 34 32 33 39 38 5021653810242398
0030 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 0000000000000000
0040 30 30 30 30 66 66 66 66 63 30 61 38 30 31 30 35 0000ffffc0a80105
0050 34 34 61 65 63 64 00 0a 00 04 70 72 6f 64 00 04 44aecd....prod..
0060 00 07 64 65 66 61 75 6c 74 00 05 00 03 62 6f 65 ..default....boe
0070 00 09 00 05 47 72 65 65 74 00 16 00 01 31 00 10 ....Greet....1..
0080 00 01 33 00 03 00 01 2d 00 14 00 00 00 06 00 13 ..3....-........
0090 65 78 61 6d 70 6c 65 2e 68 65 6c 6c 6f 2e 77 6f example.hello.wo
00a0 72 6c 64 00 08 00 03 62 6f 65 00 00 00 cf|80 01 rld....boe......
-----------------------------------------|-----
header |payload
-----------------------------------------|------
00b0 00 01 00 00 00 05 47 72 65 65 74 00 00 00 01 0c ......Greet.....
-----------------------------------------------
payload...
-----------------------------------------------
00c0 00 01 0b 00 01 00 00 00 03 61 61 61 0c 00 ff 0b .........aaa....
00d0 00 01 00 00 00 35 30 32 31 36 35 33 38 31 30 32 .....50216538102
00e0 34 32 33 39 38 30 30 30 30 30 30 30 30 30 30 30 4239800000000000
00f0 30 30 30 30 30 30 30 30 30 66 66 66 66 63 30 61 000000000ffffc0a
0100 38 30 31 30 35 34 34 61 65 63 64 0b 00 02 00 00 8010544aecd.....
0110 00 01 2d 0b 00 03 00 00 00 0b 31 39 32 2e 31 36 ..-.......192.16
0120 38 2e 31 2e 35 0b 00 04 00 00 00 00 0d 00 06 0b 8.1.5...........
0130 0b 00 00 00 04 00 00 00 03 65 6e 76 00 00 00 04 .........env....
0140 70 72 6f 64 00 00 00 0a 73 74 72 65 73 73 5f 74 prod....stress_t
0150 61 67 00 00 00 00 00 00 00 03 69 64 63 00 00 00 ag........idc...
0160 03 62 6f 65 00 00 00 07 63 6c 75 73 74 65 72 00 .boe....cluster.
0170 00 00 07 64 65 66 61 75 6c 74 00 00 00 ...default...
-----------------------------------------------
payload...
-----------------------------------------------
在decode过程中,先解析header,再解析payload。payload中先解析出方法名(Greet),消息类型(CALL),请求序列号(01),然后再解析执行method所需的参数(HelloWorldRequest)。
还记得之前说过请求到来时初始化RPCInfo,但是并没有真正赋值吗?是因为那时候还没有解析请求。所以在解析payload的时候,获取method name,并将相应信息填充到RPCInfo里面,因为RPCInfo已经塞到ctx里面了,所以ctx里面也能获取rpcInfo:
// Unmarshal implements the remote.PayloadCodec interface.
func (c thriftCodec) Unmarshal(ctx context.Context, message remote.Message, in remote.ByteBuffer) error {
tProt := NewBinaryProtocol(in)
methodName, msgType, seqID, err := tProt.ReadMessageBegin()
// Must check after Exception handle.
// For server side, the following error can be sent back and 'SetSeqID' should be executed first to ensure the seqID
// is right when return Exception back.
// 设置请求序列号
if err = codec.SetOrCheckSeqID(seqID, message); err != nil {
return err
}
// 设置请求方法名
if err = codec.SetOrCheckMethodName(methodName, message); err != nil {
return err
}
// 初始化method请求体
if err = codec.NewDataIfNeeded(methodName, message); err != nil {
return err
}
// decode thrift
data := message.Data()
...
具体解析data,主要是按照thrift的数据编码格式解析,本文就不详细介绍了。后面可以考虑专门写一篇关于thrift协议解码的。
解析请求之后,开始调用OnMessage()处理了。实际上就是执行inkHdlFunc,在5.4节的时候,已经将构建好的eps赋给了inkHdlFunc,所以处理请求主要就是调用eps。当然TransPipeline中还有inbound和outbound,这里不做过多涉及。
func (t *svrTransHandler) OnMessage(ctx context.Context, args, result remote.Message) (context.Context, error) {
err := t.inkHdlFunc(ctx, args.Data(), result.Data())
return ctx, err
}
method执行后,需要把数据按照协议格式encode后再发送给客户端。encode和前面的decode格式类似。先编码14字节的header(length, flags, seqID, header size),然后打包header信息,最后打包rpc相关的信息以及response。
经过一番呕心沥血地整理,KiteX终于梳理完了。其实RPC框架大同小异,走一遍KiteX流程后,基本对RPC框架都会有比较深的认识。
当然,不同RPC框架的实现不尽相同,netpoll库(IO多路复用)为KiteX带来了优越的性能。还有KiteX中各层之间的面向接口设计,使得框架具有很高的扩展性,这一点在我们的日常工作中也是值得学习的。