Dubbo3源码(三)Triple协议

1,810 阅读15分钟

前言

本章基于dubbo3.2.0版本,分析Triple协议的实现。

先前的所有分析,都没有分析到通讯层,主要原因是因为dubbo rpc协议是个私有协议,不具备普适性。

先前分析的所有东西,在目前版本都有借鉴意义,比如2.x的暴露、引用、调用,2.7.5的应用级别服务注册发现。

dubbo3.0推出了三代协议Triple,基于http2,兼容grpc,可能是未来的主力协议,详细信息参见官网。

dubbo3.0将部分扩展点实现从主项目中拆分到dubbo-spi-extensions,其中就包含部分rpc协议,所以主力协议都在主项目中。

本文将分析以下内容

  • 基于unary方式调用的原理(简单理解unary为传统一个请求一个响应)
  • 基于stream方式调用的原理
  • triple兼容grpc原理
  • Stub的运行原理(基于dubbo-compiler)
  • 端口协议复用特性

通过这次Triple协议阅读,收获会很多

  • 对于http2不仅停留在八股文层,有一个体感的认知
  • dubbo对于triple的实现,很多地方和grpc-java类似,所以顺带理解grpc-java
  • 巩固理解netty

注:通讯层仅支持netty4,netty基础可能要复习一下。

铺垫

案例

采用官方dubbo-demo-triple,做了一些小改动,方便调试。

注:使用IDL(Interface Description Language)+unary方式调用。

syntax = "proto3";
option java_multiple_files = true;
option java_package = "org.apache.dubbo.demo.hello";
option java_outer_classname = "HelloWorldProto";
option objc_class_prefix = "HLW";
package helloworld;
message HelloRequest {
  string name = 1;
}
message HelloReply {
  string message = 1;
}

服务提供者

这里register-mode=instance,采用应用级别注册。

public static void main(String[] args) {
    ServiceConfig<GreeterService> serviceConfig = new ServiceConfig<>();
    serviceConfig.setInterface(GreeterService.class);
    serviceConfig.setRef(new GreeterServiceImpl());
	// 默认端口50051
    ProtocolConfig protocol = new ProtocolConfig("tri", -1);
    // 应用级别注册
    ApplicationConfig applicationConfig = new ApplicationConfig("dubbo-demo-triple-api-provider");
    applicationConfig.setRegisterMode("instance");
    DubboBootstrap bootstrap = DubboBootstrap.getInstance();
    bootstrap.application(applicationConfig)
        .registry(new RegistryConfig("zookeeper://127.0.0.1:2181"))
        .protocol(protocol)
        .service(serviceConfig)
        .start()
        .await();
}

这里服务实现,我加入了attachment。

每个响应会回复一个responseId,可以观察这个attachment在http2中体现在哪里。

@Override
public HelloReply sayHello(HelloRequest request) {
    // requestId
    Object requestId = RpcContext.getServerAttachment().getObjectAttachment("requestId");
    System.out.println("requestId=" + requestId);
    // responseId
    String responseId = UUID.randomUUID().toString();
    System.out.println("responseId=" + responseId);
    RpcContext.getServerResponseContext().setAttachment("responseId", responseId);
    return HelloReply.newBuilder().setMessage("Hello " + request.getName()).build();
}

服务消费者

服务消费者强制走应用级别发现FORCE_APPLICATION。

当收到控制台输入回车,发送一次rpc请求,每个请求携带requestId。

public static void main(String[] args) throws InterruptedException {
    System.setProperty("dubbo.application.service-discovery.migration", "FORCE_APPLICATION");
    ReferenceConfig<GreeterService> referenceConfig = new ReferenceConfig<>();
    referenceConfig.setInterface(GreeterService.class);
    referenceConfig.setCheck(false);
    referenceConfig.setProtocol(CommonConstants.TRIPLE);
    referenceConfig.setTimeout(100000);

    DubboBootstrap bootstrap = DubboBootstrap.getInstance();
    bootstrap.application(new ApplicationConfig("dubbo-demo-triple-api-consumer"))
        .registry(new RegistryConfig("zookeeper://127.0.0.1:2181"))
        .protocol(new ProtocolConfig(CommonConstants.TRIPLE, -1))
        .reference(referenceConfig)
        .start();
    while (true) {
        try {
            // wait user enter
            System.in.read();

            // requestId
            String requestId = UUID.randomUUID().toString();
            System.out.println("requestId=" + requestId);
            RpcContext.getClientAttachment().setAttachment("requestId", requestId);

            // send request
            final HelloReply reply = referenceConfig.get()
                .sayHello(HelloRequest.newBuilder().setName("triple").build());
            System.out.println("Reply: " + reply.getMessage());

            // responseId
            Object responseId = RpcContext.getClientResponseContext().getAttachment("responseId");
            System.out.println("responseId=" + responseId);
        } catch (Exception e) {
            e.printStackTrace();
            Thread.sleep(3000);
        }
    }
}

引入

要了解Triple,还是得先了解一下HTTP2。

这里不会长篇大论HTTP2,就说一下和dubbo中源码相关的内容。

下图是根据案例抓的包:

流Stream和帧Frame

流:虚拟概念,建立在连接之上,支持双向通讯。每个连接上可以有多个流,每个流有一个StreamId标识。

帧:传输数据的最小单元,每个帧包含StreamId标识,每个流上可以有多个帧。

以37号Stream为例,37号Stream对应一轮unary请求+响应。

567和569对应客户端请求,将http请求头和请求体分为两个帧传输。

575和577对应服务端响应,将http响应头和响应体分为三个帧传输(2个HeaderFrame和1个DataFrame)。

帧的结构

一个Frame由两部分组成:Header(注意不是http请求头)和Payload。

下图是No=567数据包,只发送了一个Frame,Payload是http请求头。

下图是No=569数据包,只发送了一个Frame,Payload是http请求体。

帧的Header固定9个字节长度:

  • Length:3个字节,表示Payload的长度,比如567数据包表示http头长度是49字节。
  • Type:1个字节,表示Frame的类型。我们只需要关注两个类型,HEADER代表这个Frame对应http头,DATA代表http体。
  • Flags:1个字节,一些标志位,最关键的是EndStream标志位。客户端发送EndStream=true,代表客户端请求已经发完了,等待服务端响应,服务端发送EndStream=true,代表服务端响应已经发完了,流可以关闭。

  • Reserved:保留位,1个bit。
  • Stream Identifier:Stream ID,标识当前帧属于哪个流,31个bit。

Payload我们只关注Header和Data类型。

比如No=567,Frame类型是Header,Payload就是http请求头,其中就包括我们通过attachment设置的requestId。

比如No=569,Frame类型是Data,Payload就是http请求体。

服务端(Unary)

暴露阶段

通过前面几章的了解,直接定位到rpc协议层,TripleProtocol#export。

triple协议暴露分为几步:

1)封装InvokerExporter

这里invoker就是后续调用阶段的调用链(Filter->Proxy),最终调用到目标service方法。

2)将serviceKey和invoker的映射关系,保存到PathResolver

这个PathResolver就理解为springmvc中的RequestMappingHandlerMapping,管理接口路径到目标方法的映射关系。

3)初始化服务端线程池;

一个port对应一个线程池,默认200线程数,之前提到过。

4)PortUnificationExchanger#bind开启底层Netty服务端;

这里涉及到多协议端口复用的特性,我们后面再看。

调用阶段

跳过PU多协议端口复用相关逻辑,直接进入Triple协议逻辑。

当客户端Channel注册完毕,触发TripleHttp2Protocol#configServerProtocolHandler配置Channel的pipeline。

所有逻辑都在各个ChannelHandler中,接下来我们重点关注几个核心的ChannelHandler。

  • Http2FrameCodec:创建Stream抽象对象,负责ByteBuf与帧的转换,比如请求头帧、请求体帧,传递读写事件;
  • Http2MultiplexHandler:创建子Channel,将父Channel中的读写事件传播到子Channel中;
  • TripleHttp2FrameServerHandler:接收请求头帧和请求体帧,调用目标Invoker;
  • TripleCommandOutBoundHandler:Invoker返回值解析为响应头帧和响应体帧,写到父Channel;

解码(Http2FrameCodec)

Http2FrameCode继承ByteToMessageDecoder在收到ByteBuf时转换为帧Frame。

DefaultHttp2FrameReader#processPayloadState:解析固定9字节长度Frame头,得到Frame类型,Payload长度等。

在Triple中一次请求最少要有两个Frame,一个是请求头,一个是请求体。

对于请求头来说,有一个比较关键的逻辑。

DefaultHttp2ConnectionDecoder.FrameReadListener#onHeadersRead:收到请求头后,会构造一个Stream,触发Http2FrameStreamEvent用户事件,这个事件会被Http2MultiplexHandler处理,下面再看。

Http2FrameCodec#onHttp2Frame:无论是HeaderFrame还是DataFrame,最终都会传递给下一个ChannelHandler。

父子转换(Http2MultiplexHandler)

An HTTP/2 handler that creates child channels for each stream. This handler must be used in combination with Http2FrameCodec.

这是javadoc里的描述,Http2MultiplexHandler的作用就是针对一个连接Channel,创建基于Stream的子Channel,需要和Http2FrameCodec组合使用

构造子Channel

Http2MultiplexHandler#userEventTriggered:下一个Handler收到事件,创建子Channel,即Http2MultiplexHandlerStreamChannel。所以一个tcp连接对应一个父Channel,一个tcp连接上可以有多个Stream,每个Stream对应一个子Channel

子Channel会和父Channel注册到同一个EventLoop上(Selector、同一个线程)。

注册会触发子Channel的初始化,子Channel的pipeline会有两个核心ChannelHandler。

向子Channel传播read

根据Http2FrameCodec传来的StreamFrame,这里可以拿到Stream对应的子Channel,在子Channel中继续传播read。

请求头处理(TripleHttp2FrameServerHandler)

一个Netty的Stream对应一个TripleHttp2FrameServerHandler,

一个TripleHttp2FrameServerHandler会对应一个TripleServerStream,

之后很多逻辑都在TripleServerStream之中。

TripleHttp2FrameServerHandler#channelRead:

终于来到了业务逻辑部分,此时我们在一个StreamChannel中。

TripleHttp2FrameServerHandler#onHeadersRead:

为当前Stream分配执行线程池,默认还是每个port对应200线程。

TripleServerStream.ServerTransportObserver#onHeader:

线程切换,netty的worker线程提交头处理任务到dubbo线程池。

如何保证请求头和请求体丢到线程池后顺序处理,见SerializingExecutor,思路和Netty的EventLoop差不多。

TripleServerStream.ServerTransportObserver#processHeader:

收到请求头服务端主要是做一些数据准备,我们只梳理比较核心的部分。

TripleServerStream#getInvoker:根据请求头里的数据,从PathResovler里定位Invoker

  • 请求头path:如/org.apache.dubbo.demo.GreeterService/sayHello,可以拿到接口名;
  • 请求头tri-service-version:服务版本;
  • 请求头tri-service-group:服务分组;

ReflectionAbstractServerCall #buildInvocation:根据invoker中的url,构造 RpcInvocation

注意,请求头被放到attachment中了,融入了dubbo的模型。

此时RpcInvocation并不能直接执行,因为还没有请求体,没入参。

其实就是将一次rpc调用,分成了多阶段处理。

请求体处理(TripleHttp2FrameServerHandler)

TripleHttp2FrameServerHandler#onDataRead:

收到请求体,还是交给TripleServerStream。

TripleServerStream.ServerTransportObserver#onData:同上线程切换。

TripleServerStream.ServerTransportObserver#doOnData

1)反序列化请求体;2)如果客户端说流结束,执行rpc方法调用。

反序列化请求

TriDecoder#processHeader:在http头之下,triple和grpc一样,还有个头,固定5字节。

1个字节标记是否压缩,4个字节标识业务报文长度。

ReflectionAbstractServerCall#parseSingleMessage:这里反序列化有两个分支。

PbUnpack:如果用IDL生成请求定义,直接走protobuf反序列化。

ReflectionPackableMethod.WrapRequestUnpack#unpack:用普通pojo请求,走包装逻辑。

本质上是proto定义了TripleRequestWrapper,包含序列化方式、参数列表、参数类型列表。

UnaryServerCallListener#onMessage:将参数列表注入RpcInvocation,至此所有数据准备完成。

执行目标方法

UnaryServerCallListener#onComplete:普通的unary方式调用,一个请求一个响应。

AbstractServerCallListener#invoke:最终执行Invoker调用,和原有逻辑一致。

响应阶段

业务线程

UnaryServerCallListener#onReturn:

如果是异步方法,这里是用户定义线程池,如果是同步方法,这里还是DubboServerHandler线程。

在这一步会再次发生线程切换,响应会提交到netty的worker线程

onNext

ServerCallToObserverAdapter#onNext->AbstractServerCall#doSendMessage:

1)发送标准http响应头;2)发送序列化响应体

AbstractServerCall#sendHeader:发送http响应头,比如status、content-type等。

序列化同反序列化,如果通过IDL定义,走PbArrayPacker#pack,调用Message#toByteArray。

否则走pojo包装WrapResponsePack#pack。

onCompleted

ServerCallToObserverAdapter#onCompleted->TripleServerStream#complete:

这里会再发送一部分响应头,称为trailer

trailer中包含服务端给客户端的attachment,即RpcContext.getServerResponseContext。

并且这个Frame会被标记上endStream,告知客户端当前流已经结束。

提交

BatchExecutorQueue#enqueue:头和体都会封装为一个Command,提交到TripleWriteQueue。

TripleWriteQueue是个性能优化,支持批量flush,减少用户线程和io线程切换次数。(见issue#10915)

Command提交到channel对应EventLoop上。

具体run方法细节不看了,就是优先channel.write(command),如果queue中没数据了,再channel.flush。

netty线程

TripleCommandOutBoundHandler

上面channel对应子channel,来到子channel的TripleCommandOutBoundHandler。

执行不同Command的send方法。

HeaderQueueCommand:封装DefaultHttp2HeadersFrame头帧。

DataQueueCommand:封装DefaultHttp2DataFrame数据帧,和grpc一致。

子父转换(Http2ChannelUnsafe)

最终无论write还是flush,都会从Stream子Channel传播到父Channel。

AbstractHttp2StreamChannel.Http2ChannelUnsafe#writeHttp2StreamFrame:

AbstractHttp2StreamChannel.Http2ChannelUnsafe#flush:

编码(Http2FrameCodec)

Http2FrameCodec#write:回到父channel后,netty提供的http2编码器将DataFrame和HeadersFrame编码。

客户端(Unary)

引用阶段

TripleProtocol#refer:

1)初始化consumer共享线程池;

2)创建netty客户端,与对端建立连接;

3)封装TripleInvoker返回;

当客户端与服务端建立连接完成,TripleHttp2Protocol#configClientPipeline配置客户端channel的pipeline。

其中包括了两个和服务端一样的netty http2 handler组件:

  • Http2FrameCodec:在read和write过程中,ByteBuf与Frame帧转换;
  • Http2MultiplexHandler:在子channel的pipeline中传播帧;

调用阶段

TripleInvoker#doInvoke:对于unary调用方式,走TripleInvoker#invokeUnary。

TripleInvoker#invokeUnary:核心方法在最后三行。

子channel注册

TripleClientCall#start:核心方法是构造TripleClientStream抽象,和server端的TripleServerStream可以类比。

在TripleClientStream构造阶段,TripleClientStream#initHttp2StreamChannel做了很重要的操作。

TripleClientStream#initHttp2StreamChannel:利用netty的Http2StreamChannelBootstrap创建子channel。

1)bootstrap#handler:为子channel定义pipeline,当子channel注册到EventLoop上之后触发;

2)CreateStreamQueueCommand:提交到io线程,执行子channel创建和注册;

Http2StreamChannelBootstrap#open0:创建Stream并创建子StreamChannel,注册到父channel的EventLoop上。

发送请求

StreamObserver#onNext->TripleClientCall#sendMessage:将请求头和请求体封装为Command提交到io线程,和server端类似,不再赘述。

StreamObserver#onCompleted->TripleClientStream#halfClose:发送一个frame,包含endStream标识,告知服务端本次请求已经发送完成。

本质上EndStreamQueueCommand会转换为只包含endStream=true的空DataFrame。

为什么抓包客户端只有2个frame给服务端,而这里看到有3个frame?

因为在流控里有merge操作,支持将同类型frame合并。

比如DataFrame合并,DefaultHttp2ConnectionEncoder.FlowControlledData#merge。

如果赶的巧业务线程提交Command在一批里,那么就能执行合并。

TripleClientCall#halfClose:比如这里业务线程故意睡一秒,netty的io线程就会先发送2个frame过去,随后业务线程提交EndStreamQueueCommand,会发送一个无payload帧到对端。

响应阶段

和server端请求阶段类似。

Http2FrameCodec将ByteBuf转换为Frame;

Http2MultiplexHandler找到streamId对应子Channel,传播Frame。

TripleHttp2ClientResponseHandler#channelRead0:收到server端返回Frame。

根据收到的frame类型不同,走不同逻辑,最终都会来到UnaryClientCallListener

当收到数据,设置反序列化后的appResponse;

当收到trailer(endStream),完成future。

org.apache.dubbo.rpc.protocol.AbstractInvoker#waitForResultIfSync,2.x的AsyncToSyncInvoker被优化调了,异步适配同步的逻辑内聚到AsyncRpcResult。

流调用

案例

ServerStream

服务端流,简单理解:一轮rpc调用,一个客户端请求,对应多个服务端响应(Stream)。

提供者实现:针对每个HelloRequest返回5个HelloReply。

@Override
public void sayMultiHello1(HelloRequest request/*客户端请求*/, StreamObserver<HelloReply> responseStream/*响应流*/) {
    System.out.println("sayMultiHello1[" + Thread.currentThread().getName() + "] request = " + request);
    for (int i = 0; i < 5; i++) {
        responseStream.onNext(HelloReply.newBuilder().setMessage("Hello " + request.getName() + i).build());
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
    System.out.println("sayMultiHello1 complete...");
    responseStream.onCompleted();
}

消费者:

private static void serverStreamCall(ReferenceConfig<GreeterService> referenceConfig) {
    GreeterService greeterService = referenceConfig.get();
    while (true) {
        try {
            System.in.read();
            CountDownLatch cdl = new CountDownLatch(1);
            greeterService.sayMultiHello1(
                // 请求参数
                HelloRequest.newBuilder().setName("ServerStream").build(),
                // 响应流仅打印server返回
                new StdoutObserver<>(cdl));
            // 等待server complete
            cdl.await();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

消费者接收服务端响应流输出:

static class StdoutObserver<T> implements StreamObserver<T> {

    private final CountDownLatch countDownLatch;

    public StdoutObserver() {
        this(new CountDownLatch(0));
    }

    public StdoutObserver(CountDownLatch countDownLatch) {
        this.countDownLatch = countDownLatch;
    }

    @Override
    public void onNext(T data) {
        System.out.println(Thread.currentThread().getName() + " Server Reply: " + data);
    }

    @Override
    public void onError(Throwable throwable) {
        System.out.println(Thread.currentThread().getName() + " Server Error: " + throwable.getMessage());
        countDownLatch.countDown();
    }

    @Override
    public void onCompleted() {
        System.out.println(Thread.currentThread().getName() + " Server complete");
        countDownLatch.countDown();
    }
}

客户端输出:注意处理服务端响应的线程是consumer公用线程池。

DubboServerHandler-xxx:50051-thread-32 Server Reply: message: "Hello ServerStream0"
DubboServerHandler-xxx:50051-thread-33 Server Reply: message: "Hello ServerStream1"
DubboServerHandler-xxx:50051-thread-34 Server Reply: message: "Hello ServerStream2"
DubboServerHandler-xxx:50051-thread-35 Server Reply: message: "Hello ServerStream3"
DubboServerHandler-xxx:50051-thread-36 Server Reply: message: "Hello ServerStream4"
DubboServerHandler-xxx:50051-thread-37 Server complete

服务端输出:

sayMultiHello1[DubboServerHandler-xxx:50051-thread-1] request = name: "ServerStream"
sayMultiHello1 complete...

ClientStream/BiStream

客户端流(双向流),简单理解:一轮rpc调用,包含多个客户端请求,对应多个服务端响应。

提供者实现:入参是响应流,出参是请求流。每个HelloRequest对应一个HelloReply,在客户端complete后,回复一个Bye,服务端执行complete。

public StreamObserver<HelloRequest>/*请求流*/ sayMultiHello2(StreamObserver<HelloReply> responseStream/*响应流*/) {
    System.out.println("sayMultiHello2当前线程:" + Thread.currentThread().getName());
    return new StreamObserver<HelloRequest>() {
        @Override
        public void onNext(HelloRequest request) {
            System.out.println("sayMultiHello2.onNext[" + Thread.currentThread().getName() + "] request: " + request);
            responseStream.onNext(HelloReply.newBuilder().setMessage("Hello " + request.getName()).build());
        }

        @Override
        public void onError(Throwable throwable) {
            throwable.printStackTrace();
        }

        @Override
        public void onCompleted() {
            System.out.println("sayMultiHello2.onCompleted[" + Thread.currentThread().getName() + "]");
            responseStream.onNext(HelloReply.newBuilder().setMessage("Bye!!!").build());
            responseStream.onCompleted();
        }
    };
}

客户端:对于服务端响应仅输出打印,发送5个HelloRequest后complete。

private static void clientStreamCall(ReferenceConfig<GreeterService> referenceConfig) {
    while (true) {
        try {
            System.in.read();
            GreeterService greeterService = referenceConfig.get();
            StreamObserver<HelloRequest> requestStream = greeterService.sayMultiHello2(new StdoutObserver<>());
            for (int i = 0; i < 5; i++) {
                requestStream.onNext(HelloRequest.newBuilder().setName("ClientStream" + i).build());
                Thread.sleep(200);
            }
            requestStream.onCompleted();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

客户端输出:

DubboServerHandler-xxx:50051-thread-1 Server Reply: message: "Hello ClientStream0"
DubboServerHandler-xxx:50051-thread-2 Server Reply: message: "Hello ClientStream1"
DubboServerHandler-xxx:50051-thread-3 Server Reply: message: "Hello ClientStream2"
DubboServerHandler-xxx:50051-thread-4 Server Reply: message: "Hello ClientStream3"
DubboServerHandler-xxx:50051-thread-5 Server Reply: message: "Hello ClientStream4"
DubboServerHandler-xxx:50051-thread-6 Server Reply: message: "Bye!!!"
DubboServerHandler-xxx:50051-thread-7 Server complete

服务端输出:

sayMultiHello2当前线程:DubboServerHandler-xxx:50051-thread-22
sayMultiHello2.onNext[DubboServerHandler-xxx:50051-thread-23] request: name: "ClientStream0"
sayMultiHello2.onNext[DubboServerHandler-xxx:50051-thread-24] request: name: "ClientStream1"
sayMultiHello2.onNext[DubboServerHandler-xxx:50051-thread-25] request: name: "ClientStream2"
sayMultiHello2.onNext[DubboServerHandler-xxx:50051-thread-26] request: name: "ClientStream3"
sayMultiHello2.onNext[DubboServerHandler-xxx:50051-thread-27] request: name: "ClientStream4"
sayMultiHello2.onCompleted[DubboServerHandler-xxx:50051-thread-28]

小总结

无论是哪种Stream,在http2层面上,只要不发送endStream=true的帧,当前端(client/server)就可以持续发送数据

所以unary可以认为是stream的特例,双向流StreamObserver调用被dubbo框架托管,由框架主动调用StreamObserver的onNext和onCompleted

此外,无论哪种Stream,无论在服务端还是在客户端,在方法层面上:

入参StreamObserver是响应流处理,出参StreamObserver是请求流处理,StreamObserver是dubbo框架提供给用户的操作流的api

客户端

对于客户端来说,unary/serverStream/clientStream/biStream的区别在于TripleInvoker#doInvoke。

第一点,对于stream调用,所有服务端流StreamObserver默认都用consumer公共线程池处理

ServerStream

对于serverStream:

1)和unary相同,由dubbo框架发送请求,执行客户端Stream的onNext和onCompleted调用,发送请求和endStream;

2)与unary不同,返回AsyncRpcResult中的CompletableFuture是已完成状态,不会阻塞业务线程(unary同步调用需要阻塞等待响应);

ClientStream/BiStream

对于clientStream/biStream:

1)与unary不同,由用户代码通过出参StreamObserver发送onNext/onCompleted,以此实现客户端流;

2)与unary不同,返回AsyncRpcResult中的CompletableFuture是已完成状态,包含客户端流StreamObserver,不会阻塞业务线程;

出参客户端流StreamObserver和unary一致,都是ClientCallToObserverAdapter,onNext发送请求,onCompleted发送endStream ,都是流操作api

此外还要强调一点,同一个Stream,客户端只会在第一个onNext中发送请求头,针对ClientStream和BiStream就很重要。

TripleClientCall#sendMessage:

Stream共同点

stream的共同点在于TripleInvoker#streamCall

stream传入TripleClientCall#start的是ObserverToClientCallListenerAdapter,主要作用是回调入参StreamObserver响应流处理器,让客户端接收服务端响应。

unary传入TripleClientCall#start的是UnaryClientCallListener,主要作用是收集server端响应,设置future结果。

ObserverToClientCallListenerAdapter收到对端响应,回调方法入参StreamObserver。

服务端

对于服务端来说,区别在于收到http请求头后,执行AbstractServerCall#startInternalCall,做一些准备工作。

注意ClientStream和BiStream情况下,只会收到一个http请求头。

无论是那种rpcType,都会构造响应流responseObserver,只不过unary是托管框架调用流api。

ServerStream

ServerStream和Unary类似的点在于,需要收到完整的客户端请求后,才执行目标方法。

区别在于ServerStream方法入参需要传入给用户代码使用的ObserverStream流api,用于响应客户端。

ServerStreamServerCallListener#onMessage:

当收到http请求body,ServerStream和Unary一致,设置方法入参。

区别在于方法入参除了客户端的请求message之外,还注入了框架提供的流api,即ObserverStream响应流。

ServerStreamServerCallListener#onComplete:当收到endStream帧后,执行目标方法。

ServerStreamServerCallListener#onReturn:目标方法执行完毕,不需要处理响应结果。

ClientStream/BiStream

双向流比较特殊的一点是,在收到http请求头时,就执行了业务方法,拿到业务返回的请求流ObserverStream。

和ServerStream一样,入参设置为响应流ObserverStream。

当收到http请求体时,回调请求流的onNext方法。

当收到客户端endStream时,回调请求流的onCompleted方法。

grpc打通

案例

官网说triple与grpc直接打通,可以来验证一下。

书写一个proto定义,与官方dubbo-demo-triple的区别在于:

1)package与javapackage一致;

2)新增GreeterService定义;

syntax = "proto3";

package org.apache.dubbo.demo;

message HelloRequest {
  string name = 1;
}
message HelloReply {
  string message = 1;
}
service GreeterService {
  rpc sayHello(HelloRequest) returns (HelloReply);
}

这里直接用Postman做客户端来验证,导入proto。

注:如果要用dubbo客户端来验证,需要服务提供者主动开启triple+grpc多协议,这样不能确定triple与grpc完全互通。

看到triple协议能够正常响应grpc请求。

原理

本质上grpc协议完全继承自triple协议,且按照工程结构划分,grpc被分在dubbo-rpc-triple模块之中。

所以grpc可以认为是triple的一种特例。

Stub

这里说的Stub不是本地存根特性,而是基于dubbo-compiler生成像grpc一样的Stub。

案例

这里引用dubbo-samples项目下的dubbo-samples-triple模块。

书写IDL。

message GreeterRequest {
  string name = 1;
}
message GreeterReply {
  string message = 1;
}
service Greeter{
  rpc greet(GreeterRequest) returns (GreeterReply);
}

使用dubbo-compiler生成Stub。

生成服务端GreeterImplBase

服务端,GreeterImpl继承compiler生成的GreeterImplBase,实现业务逻辑。

客户端,直接引用生成的Greeter。

服务端

启动阶段

ServiceConfig#buildAttributes会解析服务端实现类实现ServerService,会设置使用nativeStub对应ProxyFactory,在创建Invoker时,不走javasist实现,走StubProxyFactory

StubProxyFactory会返回通过Stub构造的Invoker。

GreeterImplBase#getInvoker:生成StubInvoker,里面包含path到目标方法的映射关系。

private static final StubMethodDescriptor greetMethod = new StubMethodDescriptor("greet",
    org.apache.dubbo.sample.tri.GreeterRequest.class, org.apache.dubbo.sample.tri.GreeterReply.class, serviceDescriptor, MethodDescriptor.RpcType.UNARY,
    obj -> ((Message) obj).toByteArray(), obj -> ((Message) obj).toByteArray(), org.apache.dubbo.sample.tri.GreeterRequest::parseFrom,
    org.apache.dubbo.sample.tri.GreeterReply::parseFrom);
@Override
public final Invoker<Greeter> getInvoker(URL url) {
    PathResolver pathResolver = url.getOrDefaultFrameworkModel()
    .getExtensionLoader(PathResolver.class)
    .getDefaultExtension();
    // 标记path对应nativeStub
    Map<String,StubMethodHandler<?, ?>> handlers = new HashMap<>();
    pathResolver.addNativeStub( "/" + SERVICE_NAME + "/greet" );
    // 将业务实现的greet方法,存储到StubInvoker
    BiConsumer<org.apache.dubbo.sample.tri.GreeterRequest, StreamObserver<org.apache.dubbo.sample.tri.GreeterReply>> greetFunc = this::greet;
    handlers.put(greetMethod.getMethodName(), new UnaryStubMethodHandler<>(greetFunc));
    return new StubInvoker<>(this, url, Greeter.class, handlers);
}

同样的TripleProtocol会将层层包装后的StubInvoker加入到PathResolver中。

运行阶段

运行阶段通过PathResolver拿到StubInvoker,最终进入目标方法。

客户端

启动阶段

客户端需要设置Reference的proxy为nativeStub。

ReferenceConfig#init:这个阶段识别proxy=nativeStub,做了特殊处理。

StubSuppliers#getServiceDescriptor:主动加载Dubbo+接口名+Triple这个类。这个类就是由dubbo-complier生成的。

比如Greeter接口对应DubboGreeterTriple这个类,在静态代码块中将构造Stub方法和service描述都注册到全局map中。

newStub方法如下,将invoker用GreeterStub包装。

在引用的最后阶段,通过StubProxyFactory,用GreeterStub包装invoker作为客户端代理。

运行阶段

GreeterStub运行阶段,走StubInvocationUtil。

StubInvocationUtil#call:组装RpcInvocation,最终走和非stub一样的InvocationUtil.invoke。

端口协议复用(PU)

这个端口协议复用主要是针对服务端,目前主要针对dubbo和triple(grpc)协议。

因为单独用triple,也会走这段逻辑,所以提一下端口协议复用的实现方式。

NettyPortUnificationServer开启时,定义了客户端建立连接后的pipeline中只有NettyPortUnificationServerHandler

NettyPortUnificationServerHandler#decode

1)在首次收到请求时,会循环所有WireProtocol,找到一个可以处理这个Bytebuf的WireProtocol;

2)执行这个WireProtocol的configServerProtocolHandler方法定义pipeline;

3)最后将自己NettyPortUnificationServerHandler从pipeline中移除;

对于Triple协议来说。

Http2ProtocolDetector需要读到客户端发送PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n这个数据包,才能识别为http2协议。

那么客户端什么时候会发送这个数据包呢?

当客户端连接成功,pipeline中加入Http2FrameCodec就会触发。

Http2ConnectionHandler.PrefaceDecoder#sendPreface:

从注释来看,这段称为preface的数据包必须作为第一波数据发送到服务端。

总结

Triple协议在暴露阶段,将path(serviceKey)和invoker的映射关系保存到PathResolver用,供运行时反查。

特殊的,如果采用Stub,底层invoker是StubInvoker,StubInvoker中包含path到具体方法的映射关系。

Triple协议在引用阶段,与服务端建立连接,封装为TripleInvoker,供运行时调用。

无论是客户端还是服务端,netty的pipeline都类似。

主channel(连接纬度)

1)Http2FrameCodec:netty提供,在read和write过程中,ByteBuf与Frame帧转换;

2)Http2MultiplexHandler:netty提供,和Http2FrameCodec配合使用,在子channel的pipeline中传播帧;

子channel(流纬度)

1)TripleHttp2FrameServerHandler:server端,重点在于read,接收请求头和请求体,根据unary/stream调用方式不同,走不同逻辑,调用业务方法;

2)TripleHttp2ClientResponseHandler:client端,重点在于read,接收响应,根据unary/stream调用方式不同,走不同逻辑;

3)TripleCommandOutBoundHandler:通用,将业务Command(http头/体/endStream)write/flush到子channel;(子channel具备write到父channel的能力,见AbstractHttp2StreamChannel)

何时创建Stream/子Channel

1)客户端:发起调用时会创建Stream并创建子Channel,注册子Channel到父Channel(TripleClientStream#initHttp2StreamChannel);

2)服务端:Http2FrameCodec收到请求头,根据头帧(注意不是请求头)中的客户端提供的streamId,创建一个Stream,封装为一个Http2FrameStreamEvent事件发送。Http2MultiplexHandler收到Http2FrameStreamEvent事件,创建子channel,并注册到父channel;

unary/serverStream/client(bi)Stream

本质上unary和stream调用的区别在于,unary由dubbo框架主动操作StreamObserver流api,而stream由用户操作StreamObserver流api。

  • onNext:发送请求/接收请求,注意,http请求头在一个stream中只能发送一次,这是由客户端实现的。
  • onCompleted:发送endStream/接收endStream,只要在当前端不发送endStream帧之前,当前端都可以持续onNext发送数据

StreamObserver在使用上

方法入参StreamObserver是响应流,对于客户端来说接收响应,对于服务端来说发送响应。

方法出参StreamObserver是请求流,对于客户端来说发送请求,对于服务端来说接收请求。

unary

1)客户端:调用直接触发请求流onNext和onCompleted,发送所有数据,同步调用等待future完成

2)服务端:接收到请求头,构造部分RpcInvocation

3)服务端:接收到请求体,反序列化,执行目标方法

4)服务端:目标方法执行完毕,执行响应流onNext和onCompleted(trailer头包含attachment)

5)客户端:收到响应体,反序列化,存储在UnaryClientCallListener

6)客户端:收到trailers(endStream),UnaryClientCallListener完成future

serverStream

1)客户端:调用直接触发请求流onNext和onCompleted,发送所有数据,业务线程直接返回

2)服务端:接收到请求头,构造部分RpcInvocation

3)服务端:接收到请求体,反序列化,执行目标方法,目标方法由用户代码调用响应流onNext和onCompleted

4)客户端:收到响应体,反序列化,执行入参响应流StreamObserver#onNext回调

5)客户端:收到endStream,执行入参响应流StreamObserver#onCompleted回调

client(bi)Stream

1)客户端:调用立即返回请求流ObserverStream,客户端按需通过请求流onNext和onCompleted发送请求

2)服务端:收到请求头,立即执行目标方法,传入响应流ObserverStream,返回请求流ObserverStream

3)客户端/服务端:都可以通过ObserverStream#onNext向对端发送数据,对端通过ObserverStream#onNext收到数据

4)客户端/服务端:都可以通过ObserverStream#onCompleted向对端发送endStream结束流;对端通过ObserverStream#onCompleted收到endStream

普通pojo模型进行triple调用

简单点来说,如果模型没有实现com.google.protobuf.Message接口,

则使用dubbo框架内部通用TripleRequestWrapper包装请求参数,也就实现了Message接口,

TripleRequestWrapper,包含序列化方式、参数列表、参数类型列表。

关于grpc

从代码层面,GrpcProtocol完全继承TripleProtocol,所以Triple的实现完全兼容了grpc。

关于stub

通过dubbo-compiler可以生成stub。

服务端StubProxyFactory创建Invoker,走生成的serviceBase#getInvoker生成StubInvoker,运行时走StubInvoker执行目标方法。

客户端StubProxyFactory创建客户端代理,会生成接口Stub,运行时走接口Stub执行远程调用。

关于PU端口协议复用特性

用Triple协议一定会走端口复用逻辑。

当服务端收到客户端首次数据包时,NettyPortUnificationServerHandler循环所有WireProtocol识别ByteBuf,如果某个WireProtocol能够识别,则交由WireProtocol重新配置pipeline,并将NettyPortUnificationServerHandler从pipeline中移除。

针对http2协议,客户端在建立连接时,Http2FrameCodec会发送PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n数据包,服务端就能识别到当前channel是Triple协议。

欢迎大家评论或私信讨论问题。

本文原创,未经许可不得转载。

欢迎关注公众号【程序猿阿越】。