Dubbo3.0 Triple协议源码分析
Triple协议基于Http2协议,也就是在使用Triple协议发送数据时,会按HTTP2协议的格式发送,而HTTP2协议相较于HTTP1协议,是HTTP1的升级版,完全兼容HTTP1,而且HTTP2协议从设计层面解决了HTTP1性能低的问题。
Triple的底层原理分析
- UNARY:服务端只有在接收完请求(包括所有HEADERS帧和DATA帧,通过调用onCompleted()发送最后一个DATA帧)之后,才会处理数据,客户端也只有接收完响应包括所有HEADERS帧和DATA帧之后,才会处理响应结果。
- SERVER_STREAM:服务端流,服务端在接收完请求(包括所有DATA帧)之后,才会处理数据。不过在处理数据过程中,可以多次发送响应DATA帧(第一个DATA帧发送之前会发送一个HEADERS帧),客户端每接收到一个响应DATA帧就可以直接处理该响应DATA 帧。这个模式下,客户端只能发一次数据,但能多次处理响应DATA帧。
- BI_STREAM:双端流,或者客户端流,客户端可以控制发送多个请求DATA帧(第一个DATA帧发送之前会发送一个HEADERS帧),服务端会不断的接收请求DATA帧并进行处理,并且及时把处理结果作为响应DATA帧发送给客户端(第一个DATA帧发送之前会发送一个HEADERS帧),而客户端每接收到一个响应结果DATA帧也会直接处理。这种模式下,客户端和服务端都在不断的接收和发送DATA帧并进行处理(注意请求HEADER帧和响应HEADERS帧都只发了一个)。
服务消费端对Triple请求调用和响应处理逻辑
服务消费端请求发送逻辑
服务提供者进行服务导出时,会按照协议以及对应的端口启动Server,比如Triple协议就会启动Netty并绑定指定的端口,等待Socket连接。服务消费端进行服务引入时,会生成TripleInvoker对象,在TripleInvoker对象的构造方法中,会利用ConnectionManager创建一个Connection对象,而Connection对象中包含了一个Bootstrap对象(Netty中用来建立Socket连接的)。不过以上都只是创建对象,并不会真正和服务去建立Socket连接,所以在生成TripleInvoker对象过程中不会真正去创建Socket连接,那什么时候创建的呢?
当我们在服务消费端执行以下代码时
demoService是一个代理对象,在执行方法的过程中,最终会调用TripleInvoker的doInvoke()方法。在doInvoke()方法中,会利用Connection对象来判断Socket连接是否可用,如果不可用并且没有初始化,那就会创建Socket连接。
一个Connection对象表示一个Socket连接,在TripleInvoker对象中也只有一个Connection对象,也就是一个TripleInvoker对象只对应一个Socket连接,这个和DubboInvoker不太一样,一个DubboInvoker中可以有多个ExchangeClient,每个ExchangeClient都会与服务端创建一个Socket连接,所以一个DubboInvoker可以对应多个Socket连接,当然多个Socket连接的目的就是提高并发,不过在TripleInvoker对象中就不需要这么来设计了,因为可以Stream机制来提高并发。
当我们利用服务接口的代理对象执行方法时就会创建一个Socket连接,就算这个代理对象再次执行方法时也不会再次创建Socket连接了,值得注意的是,有可能两个服务接口对应的是一个Socket连接,举个例子:
比如服务提供者A,提供了DemoService和HelloService两个服务,服务消费者B引入了这两个服务,这个两个接口对应的代理对象对应不同的两个TripleInvoker,但是这两个TripleInvoker会共用一个Socket连接,因为ConnectionManager在创建Connection对象时会根据服务URL的address进行缓存,后续这两个代理对象在执行方法时使用的就是同一个Socket连接,但是不同的Stream。
Socket连接创建好后,就需要发送Invocation对象给服务提供者,因为是基于的HTTP2,所以要先创建一个Stream,然后再通过Stream来发送数据。
TripleInvoker用的是Netty,所以最终会利用Netty创建Stream,对应对象为Http2StreamChannel,消费端的TripleInvoker最终会利用Http2StreamChannel发送和接收数据帧,数据帧对应的对象为Http2Frame,它又分为Http2DataFrame、Http2HeadersFrame等具体类型。
正常情况下,会每生成一个数据帧就发送,但是在Triple中有一个批量发送的优化,当发送一个数据帧时,会先把数据帧放入一个WriteQueue中,然后从线程池中拿到一个线程调用WriteQueue的flush方法,该方法的实现为
private void flush() {
try {
QueuedCommand cmd;
int i = 0;
boolean flushedOnce = false;
// 只要队列中有元素就取出来,没有则退出while
while ((cmd = queue.poll()) != null) {
// 把数据帧添加到Http2StreamChannel中,添加完并不会立马发送,只有调用了flush才发送
cmd.run(channel);
i++;
// DEQUE_CHUNK_SIZE=128
// 连续从队列中取到了128个数据帧就flush一次
if (i == DEQUE_CHUNK_SIZE) {
i = 0;
channel.flush();
flushedOnce = true;
}
}
// i != 0 表示从队列中取到了数据但是没满128个
// 如果i=0,flushedOnce=false也flush一次
if (i != 0 || !flushedOnce) {
channel.flush();
}
} finally {
scheduled.set(false);
// 如果队列中又有数据了,则继续会递归调用flush
if (!queue.isEmpty()) {
scheduleFlush();
}
}
}
向WriteQueue添加一个数据帧后,就会尝试开启一个线程,要不要开启线程要看CAS。比如现在有10个线程同时向WriteQueue添加一个数据帧,那么这10个线程中的某一个会CAS成功,其他线程CAS失败。CAS成功的线程会负责从线程池中获取另外一个线程执行上面的flush方法,从而获取WriteQueue中的数据帧然后发送出去。
在TripleInvoker的doInvoke()源码中,在创建完成Socket连接后,就会:
- 基于Socket连接先构造一个ClientCall对象
- 根据当前调用的方法信息构造一个RequestMetadata对象,用来标识当前调用的是哪个接口的哪个方法,并且记录了所配置的序列化方式,压缩方式,超时时间等
- 构造一个ClientCall.Listener,用来处理响应结果,针对不同的流式调用类型,会构造出不同的ClientCall.Listener:
- UNARY:构造一个UnaryClientCallListener,内部包含一个DeadlineFuture,DeadlineFuture用来控制线程等待结果的timeout
- SERVER_STREAM:构造一个ObserverToClientCallListenerAdapter,内部包含了调用方法时传入进来的StreamObserver对象,最终就是由这个StreamObserver对象来处理响应结果
- BI_STREAM:和SERVER_STREAM作用类似,也会构造一个ObserverToClientCallListenerAdapter
- 调用ClientCall对象start方法创建一个Stream,并且返回一个StreamObserver对象
- 得到了StreamObserver对象后,根据不同的流式调用类型使用这个StreamObserver对象
- UNARY:直接调用StreamObserver对象的onNext()方法发送方法参数,然后调用onCompleted方法,返回一个new AsyncRpcResult(CompletableFuture.completedFuture(new AppResponse()), invocation),后续不会同步,并且返回null给业务方法。
- SERVER_STREAM:直接调用StreamObserver对象的onNext()方法来发送方法参数,然后调用onCompleted方法,返回一个new AsyncRpcResult(CompletableFuture.completedFuture(new AppResponse()), invocation),后续不会同步,并且返回null给业务方法。
- BI_STREAM:直接返回new AsyncRpcResult( CompletableFuture.completedFuture(new AppResponse(requestObserver)), invocation),不等待响应结果,直接把requestObserver对象返回给业务方法。
所以我们可以发现,不管是哪种流式调用类型,都会先创建一个Stream,得到对应的StreamObserver对象,然后调用StreamObserver对象的onNext方法来发送数据,比如发送服务接口方法的入参值,比如一个User对象:
- 在发送User对象之前,会先发送请求头,请求头中包含了当前调用的是哪个接口、哪个方法、版本号、序列化方式、压缩方式等信息(注意请求头中会包含一些gRPC相关的key,主要就是为了兼容gRPC)
- 发送请求体
- 对User对象进行序列化,得到字节数组
- 压缩字节数组
- 把压缩之后的字节数组以及是否压缩标记生成一个DataQueueCommand对象,把这个对象添加到writeQueue中,执行scheduleFlush(),该方法会开启一个线程从writeQueue中获取数据发送。发送时触发DataQueueCommand对象的doSend方法进行发送,该方法会构造一个DefaultHttp2DataFrame对象,该对象有两个属性:endStream(表示是不是Stream中的最后一帧),content(表示帧携带的核心数据),该数据格式为:
- 第一个字节记录请求体是否被压缩
- 紧接着的四个字节记录字节数组的长度
- 后面就真正的字节数据
以上是TripleInvoker发送数据的流程,接下来是TripleInvoker接收响应数据的流程。
服务消费端响应处理逻辑
ClientCall.Listener用来监听是否接收到响应数据,不同的流式调用方式会对应不同的ClientCall.Listener:
- UNARY:UnaryClientCallListener,内部包含了一个DeadlineFuture,DeadlineFuture用来控制线程等待结果的timeout
- SERVER_STREAM:ObserverToClientCallListenerAdapter,内部包含了调用方法时传入进来的StreamObserver对象,最终由这个StreamObserver对象处理响应结果
- BI_STREAM:和SERVER_STREAM一样,也会构造出来一个ObserverToClientCallListenerAdapter
要监听某个Stream中是否有响应数据,这个由Netty处理,在之前创建Stream时,会向Http2StreamChannel绑定一个TripleHttp2ClientResponseHandler,这个Handler用来处理接收到的响应数据。
在TripleHttp2ClientResponseHandler的channelRead0方法中,每接收一个响应数据就会判断是Http2HeadersFrame还是Http2DataFrame,然后调用ClientTransportListener中对应的onHeader方法和onData方法:
- onHeader方法通过处理响应头,生成一个TriDecoder,用来解压并处理响应体
- onData方法利用TriDecoder的deframe()方法来处理响应体
TriDecoder的deframe()方法在处理响应体数据时,会分为两个步骤:
3. 先解析前5个字节,第1个字节确定该响应体是否压缩,后续4个字节确定响应体内容的字节长度
4. 再取出该长度的字节作为响应体数据,如果压缩了就解压,把解压后的字节数组传给ClientStreamListenerImpl的onMessage(),该方法会按对应序列化方式进行反序列化得到最终对象,再调用UnaryClientCallListener或者ObserverToClientCallListenerAdapter的onMessage()方法。
UnaryClientCallListener,构造时传入一个DeadlineFuture对象:
- onMessage()接收到响应结果对象后,会把结果对象赋值给appResponse属性
- onClose()会取出appResponse属性记录的结果对象构造出来一个AppResponse对象,然后调用DeadlineFuture的received方法,从而将方法调用线程接阻塞,并得到响应结果对象。
ObserverToClientCallListenerAdapter,构造时传入一个StreamObserver对象:
- onMessage()接收到响应结果对象后,会调用StreamObserver对象的onNext(),并把结果对象传给onNext()方法,触发自定义的onNext()方法逻辑。
- onClose()会调用StreamObserver对象的onCompleted(),或者调用onError()方法
另外如果服务提供者调用onCompleted方法,会向客户端响应一个请求头,endStream为true,表示响应结束,也会触发执行onHeader方法,调用TriDecoder的close()方法。TriDecoder close()最终也会调用UnaryClientCallListener或者ObserverToClientCallListenerAdapter的close()方法。
服务提供端对Triple请求处理和响应结果发送逻辑
这部分内容和服务消费者对发送请求和处理响应逻辑类似,就是把视角从消费端切换到服务端,前面分析的是消费端发送和接收数据,现在分析的是服务端接收和发送数据。
消费端在创建一个Stream后,会生成一个对应的StreamObserver对象用来发送数据和一个ClientCall.Listener用来接收响应数据。对于服务端其实也一样,在接收到消费端创建Stream命令后,也需要生成一个对应的StreamObserver对象用来响应数据以及一个ServerCall.Listener用来接收请求数据。
在服务导出时,TripleProtocol的export方法会开启一个ServerBootstrap,并绑定指定端口,并且最重要的是,Netty会负责接收创建Stream的信息,一旦就收到这个信号,就会生成一个ChannelPipeline,并给ChannelPipeline绑定一个TripleHttp2FrameServerHandler,用来处理Http2HeadersFrame和Http2DataFrame。
比如在接收到请求头后,会构造一个ServerStream对象,该对象有一个ServerTransportObserver对象,ServerTransportObserver对象处理请求头和请求体
- onHeader()方法,处理请求头
- 比如从请求头中得到当前请求调用的是哪个服务接口,哪个方法
- 构造一个TriDecoder对象,TriDecoder对象用来处理请求体
- 构造一个ReflectionServerCall对象并调用doStartCall()方法,生成不同的ServerCall.Listener
- UNARY:UnaryServerCallListener
- SERVER_STREAM:ServerStreamServerCallListener
- BI_STREAM:BiStreamServerCallListener
- 在构造这些ServerCall.Listener时会把ReflectionServerCall对象传入进去,ReflectionServerCall对象向客户端发送数据
- onData()方法,处理请求体,调用TriDecoder对象deframe方法处理请求体,如果是endStream,那还会调用TriDecoder对象close方法
TriDecoder:
- deframe():先解析请求体前5个字节,然后解压请求体,反序列化得到请求参数对象,调用不同的ServerCall.Listener中的onMessage()
- close():当客户端调用onCompleted方法时,表示发送完毕,此时会发送一个DefaultHttp2DataFrame并且endStream为true,触发服务端TriDecoder对象的close()方法,调用不同ServerCall.Listener中的onComplete()
UnaryServerCallListener
- 在接收到请求头时,构造UnaryServerCallListener对象
- 在接收到请求体时,请求体中的数据就是调用接口方法的入参值,比如User对象,就会调用UnaryServerCallListener的onMessage()方法,在这个方法中把User对象设置到invocation对象中
- 当消费端调用onCompleted()方法,表示请求体数据发送完毕,从而触发UnaryServerCallListener的onComplete()方法,在该方法中调用invoke()方法,从而执行服务方法,并得到结果。得到结果后,会调用UnaryServerCallListener的onReturn()方法,把结果通过responseObserver发送给消费端,并调用responseObserver的onCompleted()方法,表示响应数据发送完毕。responseObserver是ReflectionServerCall对象的一个StreamObserver适配对象(ServerCallToObserverAdapter)。
ServerStreamServerCallListener
- 接收到请求头时,会构造ServerStreamServerCallListener对象
- 接收到请求体时,请求体中的数据就是调用接口方法的入参值,比如User对象,就会调用ServerStreamServerCallListener的onMessage()方法,把User对象以及responseObserver对象设置到invocation对象中,这是和UnaryServerCallListener不同的地方,因为服务端流需要这个responseObserver对象,服务方法拿到这个对象后就可以自己控制如何发送响应体,什么时候调用onCompleted()方法。
- 当消费端调用onCompleted()方法,表示请求体数据发送完毕,触发ServerStreamServerCallListener的onComplete()方法,在该方法中调用invoke()方法,执行服务方法,通过responseObserver对象来发送数据
- 方法执行完,仍然会调用ServerStreamServerCallListener的onReturn()方法,但是个空方法
BiStreamServerCallListener
- 接收到请求头时,会构造BiStreamServerCallListener对象,这里比较特殊,会把responseObserver设置给invocation并执行invoke()方法,执行服务方法,并执行onReturn()方法,onReturn()方法中会把服务方法的执行结果(也是一个StreamObserver对象),赋值给BiStreamServerCallListener对象的requestObserver属性。
- 接收到请求头时,服务方法就开始执行,得到了一个requestObserver,它是我们自定义逻辑用来处理请求体,另外responseObserver用来发送响应体。
- 接收到请求体时,触发onMessage()方法,该方法调用requestObserver的onNext()方法。服务端实时接收消费端每次发送数据,并且进行处理。处理过程中,如果需要响应就可以利用responseObserver进行响应
- 一旦消费端调用了onCompleted()方法,触发BiStreamServerCallListener的onComplete方法,该方法调用requestObserver的onCompleted(),主要就触发我们自定义的StreamObserver对象中的onCompleted(),并没有针对底层的Stream做什么事情。
总结
不管是Unary,还是ServerStream,还是BiStream,底层客户端和服务端之前都只有一个Stream,它们三者的区别在于:
- Unary:通过流,将方法入参值作为请求体发送出去,而且只发送一次,服务端这边接收到请求体之后,会执行服务方法,得到结果,把结果返回给客户端,也只响应一次。
- ServerStream:通过流,将方法入参值作为请求体发送出去,而且只发送一次,服务端接收到请求体后,会执行服务方法,并且把当前流对应的StreamObserver对象也传给服务方法,由服务方法自己控制如何响应,响应几次,响应什么数据,什么时候响应结束,都由服务方法自己控制。
- BiStream,通过流,客户端和服务端,都可以发送和响应多次。
Triple协议源码流程图
https://www.processon.com/view/link/64ae24c3504ef15cbb98c4fb