在《基于Netty的高性能RPC框架Nifty(一)》介绍netty处理器的时候说了其中一个处理器是编解码处理器,通过该处理器将netty收到的ChannelBuffer封装为thrift特定的消息类型,在这里会是ThriftMessage,内部包含了ChannelBuffer。然后会交给NiftyDispatcher来进行处理。所以本篇会分两部分,编解码器和NiftyDispatcher来介绍。
首先说明一点,我们提供的服务实现类EchoServiceImpl提供了一个方法echo,假设客户端是调用了该方法的。
1. ThriftFrameCodec
目前ThriftFrameCodec只有一个实现类DefaultThriftFrameCodec,内部包含了编码器和解码器
public class DefaultThriftFrameCodec implements ThriftFrameCodec {
private final ThriftFrameDecoder decoder;
private final ThriftFrameEncoder encoder;
public DefaultThriftFrameCodec(int maxFrameSize, TProtocolFactory inputProtocolFactory){
this.decoder = new DefaultThriftFrameDecoder(maxFrameSize, inputProtocolFactory);
this.encoder = new DefaultThriftFrameEncoder(maxFrameSize);
}
@Override
public void handleDownstream(ChannelHandlerContext ctx, ChannelEvent e) throws Exception{
encoder.handleDownstream(ctx, e);
}
@Override
public void handleUpstream(ChannelHandlerContext ctx, ChannelEvent e) throws Exception{
decoder.handleUpstream(ctx, e);
}
}
在收到消息的时候调用handleUpstream使用解码器进行解码,在调用handleDownstream的时候使用编码器进行编码。我们这里暂时只关注解码器。
在调用decoder.handleUpstream的时候,后续的调用链如下
SimpleChannelUpstreamHandler.handleUpstream -> FrameDecoder.messageReceived -> FrameDecoder.callDecode -> DefaultThriftFrameDecoder.decode
decode方法如下所示:
protected ThriftMessage decode(ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer) throws Exception {
// ...
return new ThriftMessage(messageBuffer, ThriftTransportType.FRAMED);
}
ThriftMessage内部就是持有个类型为ChannelBuffer的buffer和传输类型TTransportType。
如果我们再好奇一下从FrameDecoder.callDecode继续往下看,其实在调用decode后得到这个ThriftMessage,最后会调用netty的Channels.fireMessageReceived方法,将结果封装成MessageEvent往下个handler传递
public static void fireMessageReceived(ChannelHandlerContext ctx, Object message, SocketAddress remoteAddress) {
ctx.sendUpstream(new UpstreamMessageEvent(ctx.getChannel(), message, remoteAddress));
}
// MessageEvent
public class UpstreamMessageEvent implements MessageEvent {
private final Channel channel;
private final Object message;
private final SocketAddress remoteAddress;
}
到这里就分析的差不多了,在下一个处理器handler中,收到这个event,获取message进行处理即可。接下来看处理器NiftyDispatcher。
2. NiftyDispatcher
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e)
throws Exception
{
if (e.getMessage() instanceof ThriftMessage) {
ThriftMessage message = (ThriftMessage) e.getMessage();
if (taskTimeoutMillis > 0) {
message.setProcessStartTimeMillis(System.currentTimeMillis());
}
TNiftyTransport messageTransport = new TNiftyTransport(ctx.getChannel(), message);
TTransportPair transportPair = TTransportPair.fromSingleTransport(messageTransport);
TProtocolPair protocolPair = duplexProtocolFactory.getProtocolPair(transportPair);
TProtocol inProtocol = protocolPair.getInputProtocol();
TProtocol outProtocol = protocolPair.getOutputProtocol();
outProtocol.setServerSide(true);
processRequest(ctx, message, messageTransport, inProtocol, outProtocol);
}
else {
ctx.sendUpstream(e);
}
}
-
首先获取消息ThriftMessage,设置开始处理的时间为当前时间;
-
TNiftyTransport,传输组件,这个类非常非常重要,内部会存下来客户端传来的ChannelBuffer,同时会构建一个动态扩容的ChannelBuffer。后续涉及数据的读取写入都会依赖这个类。
public TNiftyTransport(Channel channel, ThriftMessage message){
this(channel, message.getBuffer(), message.getTransportType());
}
public TNiftyTransport(Channel channel, ChannelBuffer in, ThriftTransportType thriftTransportType) {
this.channel = channel;
this.in = in;
this.thriftTransportType = thriftTransportType;
this.out = ChannelBuffers.dynamicBuffer(DEFAULT_OUTPUT_BUFFER_SIZE);
this.initialReaderIndex = in.readerIndex();
buffer = in.array();
initialBufferPosition = bufferPosition = in.arrayOffset() + in.readerIndex();
bufferEnd = bufferPosition + in.readableBytes();
in.readerIndex(in.readerIndex() + in.readableBytes());
}
主要是一些初始化工作,比如in,out两个ChannelBuffer;获取buffer数组元素;设置buffer的起始位置和结束位置;
TTransportPair内部持有一个inputTransport和一个outputTransport; 而TProtocolPair内部持有一个input和一个output协议,协议内部则是持有TTransport;
TTransportPair:
public class TTransportPair {
private final TTransport inputTransport;
private final TTransport outputTransport;
public static TTransportPair fromSingleTransport(final TTransport transport) {
return new TTransportPair(transport, transport);
}
}
TProtocolPair
public class TProtocolPair {
private final TProtocol inputProtocol;
private final TProtocol outputProtocol;
}
TProtocal:这里的协议是TBinaryProtocal,具体的就不展开说了
public abstract class TProtocol {
/**
* Transport
*/
protected TTransport trans_;
}
继续往下看,在方法processRequest中看到
processFuture = processorFactory.getProcessor(messageTransport).process(inProtocol, outProtocol, requestContext);
processorFactory.getProcessor(messageTransport)返回的就是ThriftServiceProcessor, 该服务处理器对象会进行数据的一系列处理,这些在《基于Netty的高性能RPC框架Nifty(一)》是有介绍过的。所以这里我们直接来看它的process方法
public ListenableFuture<Boolean> process(final TProtocol in, TProtocol out, RequestContext requestContext) throws TException{
TMessage message = in.readMessageBegin();
String methodName = message.name;
int sequenceId = message.seqid;
}
TMessage有三个属性,表示方法名,类型, 序号。比如这里我们得到的name="echo", 类型Void, 序号1
public final class TMessage {
public final String name;
public final byte type;
public final int seqid;
}
关于协议和传输层简单介绍
前面说了in的类型是TBinaryProtocal,继承自TProtocal,内部持有TTransport(可以理解为传输层),最后真正涉及到数据读写的是TTransport,其实关于RPC框架十有八九都有相似的协议接口和传输接口。 看看定义了哪些协议接口
/**
* Protocol interface definition.
*
*/
public abstract class TProtocol {
/**
* Transport
*/
protected TTransport trans_;
/**
* Constructor
*/
protected TProtocol(TTransport trans) {
trans_ = trans;
}
/**
* Transport accessor
*/
public TTransport getTransport() {
return trans_;
}
private boolean serverSide;
private String serviceName;
// getter, setter
/**
* Reading methods.
*/
public abstract TMessage readMessageBegin() throws TException;
public abstract void readMessageEnd() throws TException;
public abstract TStruct readStructBegin() throws TException;
public abstract void readStructEnd() throws TException;
public abstract TField readFieldBegin() throws TException;
public abstract void readFieldEnd() throws TException;
public abstract TMap readMapBegin() throws TException;
public abstract void readMapEnd() throws TException;
public abstract TList readListBegin() throws TException;
public abstract void readListEnd() throws TException;
public abstract TSet readSetBegin() throws TException;
public abstract void readSetEnd() throws TException;
public abstract boolean readBool() throws TException;
public abstract byte readByte() throws TException;
public abstract short readI16() throws TException;
public abstract int readI32() throws TException;
public abstract long readI64() throws TException;
public abstract double readDouble() throws TException;
public abstract String readString() throws TException;
public abstract ByteBuffer readBinary() throws TException;
/**
* Writing methods.
*/
// ...
}
里面主要是数据的读和写方法,写和读方法是对应的就不贴了。
- 开始和结束消息的读取;
- 开始和结束结构体的读取;
- 开始和结束参数的读取;
- 开始和结束集合的读取;
我们先看readMessageBegin的实现
public TMessage readMessageBegin() throws TException {
int size = readI32();
return new TMessage(readString(), (byte) (size & 0x000000ff), readI32());
}
为了易读,这里对方法稍微简化了一下。首先读取4个byte,标识类型,然后读取string,再读取4个字节标识序号。其实这就是Thrift自己定义的协议,我们自己定义也可以定义读前2个字节标识类型呢。
另外readString方法也是先读取4个字节作为size,然后从协议中持有的transport的ChannelBuffer中读取size个字节来构建string。
关于readI32和readString更多细节还是在这里说完好了,如果没有兴趣可以跳过。
private byte[] i32rd = new byte[4];
public int readI32() throws TException {
byte[] buf = i32rd;
int off = 0;
if (trans_.getBytesRemainingInBuffer() >= 4) {
buf = trans_.getBuffer();
off = trans_.getBufferPosition();
trans_.consumeBuffer(4);
} else {
readAll(i32rd, 0, 4);
}
return
((buf[off] & 0xff) << 24) |
((buf[off + 1] & 0xff) << 16) |
((buf[off + 2] & 0xff) << 8) |
((buf[off + 3] & 0xff));
}
这里的trans_在前面说过,就是TNiftyTransport,由nifty包提供的。
trans_.getBytesRemainingInBuffer()表示的是内部持有的channelBuffer剩余字节数,即bufferEnd - bufferPosition; 如果有四个字节就读取4个字节,否则读取所有;buf = trans_.getBuffer()是获取transport内部的的buffer数组;off = trans_.getBufferPosition();是获取transport内部buffer当前读取到的位置,即bufferPosition;trans_.consumeBuffer(4);则是transport内部buffer消费4个字节,即bufferPosition += 4;- 关于返回值,注意到16进制的0xff就是二进制的11111111,最终结果就是将四个字节拼接在一起构成一个int值
关于readByte,readShort,readLong都是类似的。
再来看readString:
public String readString() throws TException {
int size = readI32();
checkStringReadLength(size);
if (trans_.getBytesRemainingInBuffer() >= size) {
String s = new String(trans_.getBuffer(), trans_.getBufferPosition(), size, "UTF-8");
trans_.consumeBuffer(size);
return s;
}
return readStringBody(size);
}
首先读取四个字节构成的size,表示需要读取多少byte从而来构造string。在获取buffer和position,从而从buffer的position位置读取size个字节,构造出string;最后需要移动position再返回string结果。
再回到
TMessage message = in.readMessageBegin();
String methodName = message.name;
int sequenceId = message.seqid;
到这里就知道了方法名和序号了,比如这里分别是"echo",1。 继续往下看,
ThriftMethodProcessor method = methods.get(methodName);
ListenableFuture<Boolean> processResult = method.process(in, out, sequenceId, context);
根据方法名从methods映射表中获取方法处理器ThriftMethodProcessor。注意当前是在服务处理器(即ThriftServiceProcessor),内部持有有多个方法处理器ThriftMethodProcessors,每个方法处理器对应
Thrift对象的一个方法。
在初始化ThriftServiceProcessor的时候会进行解析,读取类中的所有方法来构造方法处理器,设置到methods=Map[String, ThriftMethodProcessor]中,其中key为方法名。
获取到方法处理器ThriftMethodProcessor后,调用process方法进行处理。所以,接下来看如果进行数据处理的。到这里范围是不是缩小了呢?从类级别到了方法级别。
public ListenableFuture<Boolean> process(TProtocol in, final TProtocol out, final int sequenceId, final ContextChain contextChain) throws Exception {
// read args
Object[] args = readArguments(in);
in.readMessageEnd();
// invoke method
final ListenableFuture<?> invokeFuture = invokeMethod(args);
}
- 从协议中读取参数,这个类似于前面读取方法名和序号等;
- 标识消息读取完毕; 这里是个空实现,啥也没做。
- 传入获取的参数,调用方法获取结果。
我们着重看获取参数和方法调用。
获取方法形参值 readArguments
private final Method method;
private final Map<Short, ThriftCodec<?>> parameterCodecs;
private final Map<Short, Short> thriftParameterIdToJavaArgumentListPositionMap;
private Object[] readArguments(TProtocol in) throws Exception {
int numArgs = method.getParameterTypes().length;
Object[] args = new Object[numArgs];
TProtocolReader reader = new TProtocolReader(in);
reader.readStructBegin();
while (reader.nextField()) {
short fieldId = reader.getFieldId();
ThriftCodec<?> codec = parameterCodecs.get(fieldId);
args[thriftParameterIdToJavaArgumentListPositionMap.get(fieldId)] = reader.readField(codec);
}
reader.readStructEnd();
return args;
}
ThriftMethodProcessor内部含有Method一点也不奇怪,持有参数相关的元数据也是理所当然,这里没贴出来,都是在初始化ThriftServiceProcessor的时候一次性全部初始化好了。
- 获取方法中形参的个数numArgs,创建numArgs个元素的Object数组;
- 构建协议读取器TProtocolReader,循环判断是否还有参数
- 获取参数id,从缓存的编解码器中获取编解码器
- 使用获取的编解码器和reader读取参数值,设置到数组中
下面涉及到的东西同样比较细节,如果没兴趣可以跳过。
构建TProtocalReader如下,该类内部会持有传入的协议对象,currentField表示当前读取的方法形参Field,内部有name和fieldId
public class TProtocolReader{
private final TProtocol protocol;
private TField currentField;
public TProtocolReader(TProtocol protocol){
this.protocol = protocol;
}
public void readStructBegin() throws TException {
protocol.readStructBegin();
currentField = null;
}
public boolean nextField() throws TException {
currentField = protocol.readFieldBegin();
return currentField.type != TType.STOP;
}
}
nextFiled返回false的条件是协议中读取到的field.type==TType.STOP,协议的readFieldBegin则是先读取一个字节,判断类型是否为STOP从而来设置id,从而来TField
public TField readFieldBegin() throws TException {
byte type = readByte();
short id = type == TType.STOP ? 0 : readI16();
return new TField("", type, id);
}
关于何时结束读取参数这个是在客户端设置的,这里我们就不去计较这些。
如果还有参数可读,则获取fieldId,再从编解码器中获取对应该参数的编解码器。
public short getFieldId(){
return currentField.id;
}
再回到process方法,关于编解码器map(parameterCodecs),是在构建方法处理器的时候,使用ThriftCodecManager来构建的,key为参数id,value为参数的编解码器; 同样thriftParameterIdToJavaArgumentListPositionMap的key是参数id,value为参数位置(0 - (n-1),n为该方法形参个数)。
最后来看如何读取参数值的
ThriftMethodProcessor:
args[thriftParameterIdToJavaArgumentListPositionMap.get(fieldId)] = reader.readField(codec);
TProtocalReader:
public Object readField(ThriftCodec<?> codec) throws Exception{
currentField = null;
Object fieldValue = codec.read(protocol);
protocol.readFieldEnd(); // 空实现
return fieldValue;
}
假设参数是String类型,得到的编解码器则是StringThriftCodec 重复一遍这里的protocal=TNiftyProtocal,基于此来看codec.read(protocol)
public class StringThriftCodec implements ThriftCodec<String> {
@Override
public String read(TProtocol protocol) throws Exception{
return protocol.readString();
}
@Override
public void write(String value, TProtocol protocol) throws Exception {
protocol.writeString(value);
}
}
其实调用的是protocal.readString(),自然其它的编解码器的read和write方法的实现也容易理解了,编解码器相当于一层转发器了。
补充:其实在readArguments方法后续还有一项操作,遍历参数列表args,如果该位置值为null,那么会设置默认值,这里又发现了用到了Guava的Defaults.defaultValue(argumentClass)来设置默认值
方法调用和响应结果
到这里关于读取方法参数值的流程就介绍完了,接下来看方法的调用。
private ListenableFuture<?> invokeMethod(Object[] args) {
Object response = method.invoke(service, args);
return Futures.immediateFuture(response);
}
首先就是通过反射调用获取结果,因为这个时候服务实现类service和参数args都有了。还是使用guava包,这里用来构建ListenableFuture。
回过头来继续看ThriftMethodProcessor.process方法
final ListenableFuture<?> invokeFuture = invokeMethod(args);
final SettableFuture<Boolean> resultFuture = SettableFuture.create();
Futures.addCallback(invokeFuture, new FutureCallback<Object>() {
@Override
public void onSuccess(Object result) {
writeResponse(out,
sequenceId,
TMessageType.REPLY,
"success",
(short) 0,
successCodec,
result);
re sultFuture.set(true);
}
@Override
public void onFailure(Throwable t){
}
}, Runnable::run);
return resultFuture;
从invokeMethod(args)获取到了方法调用的结果,现在问题是如何将执行结果响应给客户端。 主要分两个步骤:
- 将响应结果写到ChannelBuffer, 即TNiftyTransport中的outbuffer;
- 将已经写入数据的ChannelBuffer通过netty api发送给客户端;
我们先看第一步,即writeResponse,方法内容不多,如下:
private <T> void writeResponse(TProtocol out,
int sequenceId,
byte responseType,
String responseFieldName,
short responseFieldId,
ThriftCodec<T> responseCodec,
T result) throws Exception {
out.writeMessageBegin(new TMessage(name, responseType, sequenceId));
TProtocolWriter writer = new TProtocolWriter(out);
writer.writeStructBegin(resultStructName);
writer.writeField(responseFieldName, (short) responseFieldId, responseCodec, result);
writer.writeStructEnd();
out.writeMessageEnd();
out.getTransport().flush();
}
这和前面的各种read方法是相对应的
- 在前面readMessageBegin方法是先读取个TMessage,所以这里就是先写一个TMessage, name为方法名。这里就是"echo";
public void writeMessageBegin(TMessage message) throws TException {
int version = VERSION_1 | message.type;
writeI32(version);
writeString(message.name);
writeI32(message.seqid);
}
依次写name,type和序号,是不是发现和readMessageBegin读取的时候顺序一致呢。这里是将数据写到一个空的protocal.ChannelBuffer中。
- 在前面是readStructBegin, 这里就是writeStructBegin,不过这里是空实现;
- 由于读取参数的时候肯定是多个参数,所以需要用while循环,但是返回结果就一个,所以这里只需要使用writeField一次即可;
public <T> void writeField(String name, short id, ThriftCodec<T> codec, T value) throws Exception{
protocol.writeFieldBegin(new TField(name, codec.getType().getProtocolType().getType(), id));
codec.write(value, protocol);
protocol.writeFieldEnd();
}
先打标记writeFieldBegin,该方法会将type和id依次写到buffer中;关于codec.write就不用说了吧,比如返回类型为String,这里的codec就是StringThriftCodec,在前面已经介绍过了;writeFieldEnd是空实现。
再来看writer.writeStructEnd(),最后调用的是协议的writeFieldStop方法,写个STOP标志即可,再while循环读取的时候读到了就标识结束了。如下:
public void writeFieldStop() throws TException {
writeByte(TType.STOP);
}
关于out.writeMessageEnd();是个空实现;
最后关于out.getTransport().flush();在TNiftyTransport中是个空实现,因为刷出数据这件事是在NiftyDispatcher中做,其实是为了保证顺序, 这也就是我接下来会说的响应数据的步骤2.
回到NiftyDispather.processRequest,有
ThriftMessage response = message.getMessageFactory().create(messageTransport.getOutputBuffer());
writeResponse(ctx, response, requestSequenceId, DispatcherContext.isResponseOrderingRequired(ctx));
response中包含了TNiftyTransport中的outbuffer,在之前已经写进去了响应的数据。
public Factory getMessageFactory(){
return new Factory() {
@Override
public ThriftMessage create(ChannelBuffer messageBuffer)
{
return new ThriftMessage(messageBuffer, getTransportType());
}
};
}
接着看writeResponse方法,一直跟到下面的代码
private void writeResponseInOrder(ChannelHandlerContext ctx, ThriftMessage response, int responseSequenceId) {
// Ensure responses to requests are written in the same order the requests were received.
synchronized (responseMap) {
int currentResponseId = lastResponseWrittenId.get() + 1;
if (responseSequenceId != currentResponseId) {
responseMap.put(responseSequenceId, response);
}
else {
do {
Channels.write(ctx.getChannel(), response);
lastResponseWrittenId.incrementAndGet();
++currentResponseId;
response = responseMap.remove(currentResponseId);
} while (null != response);
}
}
}
如果不需要保证响应顺序和请求顺序一致,其实直接使用下面的代码就好了
Channels.write(ctx.getChannel(), response);
如果需要保证顺序的话,那么受到序号大(currentResponseId!=responseSequenceId)的响应的时候先将其保存下来,当收到currentResponseId!=responseSequenceId的请求的时候再一次性写出。 举个例子:比如lastResponseWrittenId=1, 当收到4,5序号的响应的时候,将其保存到map中。当收到了序号2的响应的时候,将其写出,再从map中查找序号=3的响应,这里没有3,所以结束循环;当后续又收到序号为3的响应的时候,再讲序号3,4,5的响应依次写出。
Channels.write(channel,message)是netty提供的用来将消息写出的方法,最终该结果会传递到编解码处理器DefaultThriftFrameCodec,进行编码,转换为thrift指定的格式来写出。