客户端和服务器通过约定的协议来传输消息(数据),通过特定的格式来编解码字节流,并转化成业务消息,提供给上层框架调用。
Thrift的协议比较简单,它把协议和编解码整合在了一起。抽象类TProtocol定义了协议和编解码的顶层接口。个人感觉采用抽象类而不是接口的方式来定义顶层接口并不好,TProtocol关联了一个TTransport传输对象,而不是提供一个类似getTransport()的接口,导致抽象类的扩展性比接口差。
TProtocol主要做了两个事情:
2.定义一系列读写消息的编解码接口,包括两类,一类是复杂数据结构比如readMessageBegin, readMessageEnd, writeMessageBegin, writMessageEnd.还有一类是基本数据结构,比如readI32, writeI32, readString, writeString
所谓协议就是客户端和服务器端约定传输什么数据,如何解析传输的数据。对于一个RPC调用的协议来说,要传输的数据主要有:
调用方
方法的名称,包括类的名称和方法的名称
方法的参数,包括类型和参数值
3.一些附加的数据,比如附件,超时事件,自定义的控制信息等等
返回方
调用的返回码
返回值
3.异常信息
从TProtocol的定义我们可以看出Thrift的协议约定如下事情:
先writeMessageBegin表示开始传输消息了,写消息头。Message里面定义了方法名,调用的类型,版本号,消息seqId
接下来是写方法的参数,实际就是写消息体。如果参数是一个类,就writeStructBegin
接下来写字段,writeFieldBegin, 这个方法会写接下来的字段的数据类型和顺序号。这个顺序号是Thrfit对要传输的字段的一个编码,从1开始
如果是一个集合就writeListBegin/writeMapBegin,如果是一个基本数据类型,比如int, 就直接writeI32
每个复杂数据类型写完都调用writeXXXEnd,直到writeMessageEnd结束
读消息时根据数据类型读取相应的长度
每个writeXXX都是采用消息头+消息体的方式。我们来看TBinaryProtocol的实现。
writeMessgeBegin方法写了消息头,包括4字节的版本号和类型信息,字符串类型的方法名,4字节的序列号seqId
writeFieldBegin,写了1个字节的字段数据类型,和2个字节字段的顺序号
writeI32,写了4个字节的字节数组
writeString,先写4字节消息头表示字符串长度,再写字符串字节
writeBinary,先写4字节消息头表示字节数组长度,再写字节数组内容
6.readMessageBegin时,先读4字节版本和类型信息,再读字符串,再读4字节序列号
7.readFieldBegin,先读1个字节的字段数据类型,再读2个字节的字段顺序号
还存在一个问题,就是服务器端如何知道客户端发送过来的数据是怎么组合的,比如第一个字段是字符串类型,第二个字段是int。这个信息是在IDL生成客户端时生成的代码时提供了。Thrift生成的客户端代码提供了读写参数的方法,这两个方式是一一对应的,包括字段的序号,类型等等。客户端使用写参数的方法,服务器端使用读参数的方法。
thrift可分为以下几个组件,其中传输层被细分为低级传输层和复写传输层。 代码生成器:根据thrift idl文件生成各个语言代码,位于compiler目录内。 低级传输层:靠近网络层、作为rpc框架接收报文的入口,提供各种底层实现如socket创建、读写、接收连接等。 复写传输层:基于低级传输层,实现各种复写传输层包括http、framed、buffered、压缩传输层等,复写传输层可以被协议层直接使用,用户也可以通过重写低级传输层和复写传输层实现自己的传输层。 协议层:协议层主要负责解析请求、应答报文为具体的结构体、类实例,供处理层直接使用,目前的协议包括Binary(最为常用)、json、多路混合协议等。 处理层:由代码生成器生成,根据获取到的具体信息如method name,进行具体的接口处理,处理层构造函数的入口包含一个handler,handler由业务方进行具体的实现,然后在处理层内被调用,并应答处理结果。 服务层:融合低级传输层、复写传输层、协议层、处理层,自身包含各种不同类型的服务模型,如非阻塞单进程服务、one request per fork、one request per thread、thread pool等模型。