前言
本章基于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)封装Invoker为Exporter;
这里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协议。
欢迎大家评论或私信讨论问题。
本文原创,未经许可不得转载。
欢迎关注公众号【程序猿阿越】。