基于Netty的高性能RPC框架Nifty(二)- 数据读取和响应

1,145 阅读12分钟

在《基于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);
    }
}
  1. 首先获取消息ThriftMessage,设置开始处理的时间为当前时间;

  2. 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)获取到了方法调用的结果,现在问题是如何将执行结果响应给客户端。 主要分两个步骤:

  1. 将响应结果写到ChannelBuffer, 即TNiftyTransport中的outbuffer;
  2. 将已经写入数据的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指定的格式来写出。