一、通讯基础协议
首先远程调用需要定义协议,也就是互相约定我们要讲什么样的语言,要保证双方都能听得懂。
常见的三种协议形式
应用层一般有三种类型的协议形式,分别是:固定长度形式、特殊字符隔断形式、header+body 形式。
固定长度形式:指的是协议的长度是固定的,比如100个字节为一个协议单元,那么读取100个字节之后就开始解析。
优点就是效率较高,无脑读一定长度就解析。
缺点就是死板,每次长度只能固定,不能超过限制的长度,并且短了还得填充,在 RPC 场景中不太合适,谁晓得参数啥的要多长,定长了浪费,定短了不够。
特殊字符隔断形式:其实就是定义一个特殊结束符,根据特殊的结束符来判断一个协议单元的结束,比如用换行符等等。
这个协议的优点是长度自由,反正根据特殊字符来截断,缺点就是需要一直读,直到读到一个完整的协议单元之后才能开始解析,然后假如传输的数据里面混入了这个特殊字符就出错了。
header+body 形式:也就是头部是固定长度的,然后头部里面会填写 body 的长度, body 是不固定长度的,这样伸缩性就比较好了,可以先解析头部,然后根据头部得到 body 的 len 然后解析 body。
dubbo 协议就是属于 header+body 形式,而且也有特殊的字符 0xdabb ,这是用来解决 TCP 网络粘包问题的。
Dubbo 协议
Dubbo 支持的协议很多,我们就简单的分析下 Dubbo 协议。
协议分为协议头和协议体,可以看到 16 字节的头部主要携带了魔法数,也就是之前说的 0xdabb,然后一些请求的设置,消息体的长度等等。
16 字节之后就是协议体了,包括协议版本、接口名字、接口版本、方法名字等等。
其实协议很重要,因为从中可以得知很多信息,而且只有懂了协议的内容,才能看得懂编码器和解码器在干嘛,我再截取一张官网对协议的解释图。
需要约定序列化器
网络是以字节流的形式传输的,相对于我们的对象来说,我们对象是多维的,而字节流是一维的,我们需要把我们的对象压缩成一维的字节流传输到对端。
然后对端再反序列化这些字节流变成对象。
序列化协议
其实从上图的协议中可以得知 Dubbo 支持很多种序列化,我不具体分析每一种协议,就大致分析序列化的种类,万变不离其宗。
序列化大致分为两大类,一种是字符型,一种是二进制流。
字符型的代表就是 XML、JSON,字符型的优点就是调试方便,它是对人友好的,我们一看就能知道那个字段对应的哪个参数。
缺点就是传输的效率低,有很多冗余的东西,比如 JSON 的括号,对于网络传输来说传输的时间变长,占用的带宽变大。
还有一大类就是二进制流型,这种类型是对机器友好的,它的数据更加的紧凑,所以占用的字节数更小,传输更快。
缺点就是调试很难,肉眼是无法识别的,必须借用特殊的工具转换。
Dubbo 默认用的是 hessian2 序列化协议。
所以实际落地还需要先约定好协议,然后再选择好序列化方式构造完请求之后发送。
二、调用过程
粗略的调用流程图
我们来看一下官网的图。
简述一下就是客户端发起调用,实际调用的是代理类,代理类最终调用的是 Client (默认Netty),需要构造好协议头,然后将 Java 的对象序列化生成协议体,然后网络调用传输。
服务端的 NettyServer
接到这个请求之后,分发给业务线程池,由业务线程调用具体的实现方法。
但是这还不够,因为 Dubbo 是一个生产级别的 RPC 框架,它需要更加的安全、稳重。
详细的调用流程
前面已经分析过了客户端也是要序列化构造请求的,为了让图更加突出重点,所以就省略了这一步,当然还有响应回来的步骤,暂时就理解为原路返回,下文会再做分析。
可以看到生产级别就得稳,因此服务端往往会有多个,多个服务端的服务就会有多个 Invoker,最终需要通过路由过滤,然后再通过负载均衡机制来选出一个 Invoker 进行调用。
当然 Cluster 还有容错机制,包括重试等等。
请求会先到达 Netty 的 I/O 线程池进行读写和可选的序列化和反序列化,可以通过 decode.in.io
控制,然后通过业务线程池处理反序列化之后的对象,找到对应 Invoker 进行调用。
调用流程-客户端源码分析
handler的调用链路为: InvokerInvocationHandler(MockClusterWrapper(FailoverCluster(directory)))
图解调用链
1、InvokerInvocationHandler.invoke
这个方法主要判断当前调用的远程方法,如果是tostring、hashcode、equals,就直接返回 否则,调用invoker.invoke,进入到 MockClusterWrapper.invok
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//method 调用的目标方法
//args 目标方法的参数
String methodName = method.getName();
Class<?>[] parameterTypes = method.getParameterTypes();
if (method.getDeclaringClass() == Object.class) {
return method.invoke(invoker, args);
}
if ("toString".equals(methodName) && parameterTypes.length == 0) {
return invoker.toString();
}
if ("hashCode".equals(methodName) && parameterTypes.length == 0) {
return invoker.hashCode();
}
if ("equals".equals(methodName) && parameterTypes.length == 1) {
return invoker.equals(args[0]);
}
return invoker.invoke(new RpcInvocation(method, args)).recreate();
}
2、MockClusterInvoker.invoke
Mock,在这里面有两个逻辑
- 1. 是否客户端强制配置了mock调用,那么在这种场景中主要可以用来解决服务端还没开发好的时候 直接使用本地数据进行测试
- 2. 是否出现了异常,如果出现异常则使用配置好的Mock类来实现服务的降级
@Override
public Result invoke(Invocation invocation) throws RpcException {
Result result = null;
//mock ->
String value = directory.getUrl().getMethodParameter(invocation.getMethodName(), MOCK_KEY, Boolean.FALSE.toString()).trim();
if (value.length() == 0 || value.equalsIgnoreCase("false")) {
// 不走mock操作
result = this.invoker.invoke(invocation);
} else if (value.startsWith("force")) { // 强制性的走本地的返回
if (logger.isWarnEnabled()) {
logger.warn("force-mock: " + invocation.getMethodName() + " force-mock enabled , url : " + directory.getUrl());
}
//force:direct mock
result = doMockInvoke(invocation, null);
} else { //调用服务失败.
//fail-mock
try {
result = this.invoker.invoke(invocation); //远程调用
} catch (RpcException e) {
if (e.isBiz()) { //业务异常,直接抛出
throw e;
}
if (logger.isWarnEnabled()) {
logger.warn("fail-mock: " + invocation.getMethodName() + " fail-mock enabled , url : " + directory.getUrl(), e);
}
result = doMockInvoke(invocation, e); //调用Mock类进行返回
}
}
return result;
}
3、AbstractClusterInvoker.invoke
下一个invoke,应该进入FailoverClusterInvoke,但是在这里它又用到了模版方法,所以直接进入到父类的invoke方法中
1. 绑定attachments,Dubbo中,可以通过 RpcContext 上的 setAttachment 和 getAttachment 在 服务消费方和提供方之间进行参数的隐式传递,所以这段代码中会去绑定attachments
RpcContext.getContext().setAttachment("index", "1")
2. 通过list获得invoker列表,这个列表基本可以猜测到是从directory里面获得的、但是这里面还实现 了服务路由的逻辑,简单来说就是先拿到invoker列表,然后通过router进行服务路由,筛选出符 合路由规则的服务提供者(暂时不细讲,属于另外一个逻辑)
3. initLoadBalance 初始化负载均衡机制
4. 执行doInvoke
@Override
public Result invoke(final Invocation invocation) throws RpcException {
checkWhetherDestroyed();//检查 连接是否被销毁
// attachments -> 隐式传参
//RpcContext.getContext().setAttachment("key","value");
Map<String, String> contextAttachments = RpcContext.getContext().getAttachments();
if (contextAttachments != null && contextAttachments.size() != 0) {
((RpcInvocation) invocation).addAttachments(contextAttachments);
}
/**
* 去哪里拿到所有的目标服务呢?(RegistryDirectory)
* route 路由
* invokers.size = 2 ->
* 通过路由过滤从 RegistryDirectory对象中拿到符合条件的 invokers
*/
List<Invoker<T>> invokers = list(invocation);
//invokers -> route决定了invokers返回多少的问题(tag->a(2), tag->b)
/**
* 通过在server端和client端都可以配置 loadbalance="random" "roundrobin"
* 获得url里面配置的负载均衡策略,如果没有,默认为random
* spi -> 通过自适应扩展点进行适配->得到真正意义上的实现
* 容错 -> failover重试 -> 已经调用过失败的节点,如果下次重试,肯定不会再次调用。
* ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(DEFAULT_LOADBALANCE);
*/
// 初始化负载均衡的机制 通过spi机制获取实例 默认 RandomLoadBalance
//loadbalace ->RandomLoadBalance
LoadBalance loadbalance = initLoadBalance(invokers, invocation);
RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);
// 抽象方法,由子类实现
return doInvoke(invocation, invokers, loadbalance);
}
稍微总结一下就是 FailoverClusterInvoker 拿到 Directory 返回的 Invoker 列表,并且经过路由之后,它会让 LoadBalance 从 Invoker 列表中选择一个 Invoker。
initLoadBalance
不用看这个代码,基本也能猜测到,会从url中获得当前的负载均衡算法,然后使用spi机制来获得负载 均衡的扩展点。然后返回一个具体的实现
protected LoadBalance initLoadBalance(List<Invoker<T>> invokers, Invocation invocation) {
if (CollectionUtils.isNotEmpty(invokers)) {
return ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(invokers.get(0).getUrl()
.getMethodParameter(RpcUtils.getMethodName(invocation), LOADBALANCE_KEY, DEFAULT_LOADBALANCE));
} else {
return ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(DEFAULT_LOADBALANCE);
}
}
4、FailoverClusterInvoker.doInvoke
这段代码逻辑也很好理解,因为我们之前在讲Dubbo的时候说过容错机制,而failover是失败重试,所 以这里面应该会实现容错的逻辑
- 获得重试的次数,并且进行循环
- 获得目标服务,并且记录当前已经调用过的目标服务防止下次继续将请求发送过去
- 如果执行成功,则返回结果
- 如果出现异常,判断是否为业务异常,如果是则抛出,否则,进行下一次重试
public Result doInvoke(Invocation invocation, final List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
List<Invoker<T>> copyInvokers = invokers;
checkInvokers(copyInvokers, invocation);
String methodName = RpcUtils.getMethodName(invocation);
//重试次数 默认2
int len = getUrl().getMethodParameter(methodName, RETRIES_KEY, DEFAULT_RETRIES) + 1;
if (len <= 0) {
len = 1;
}
// retry loop.
RpcException le = null; // 异常信息
//invoked ->表示调用过的服务(记录调用过的服务)
List<Invoker<T>> invoked = new ArrayList<Invoker<T>>(copyInvokers.size()); // invoked invokers.
Set<String> providers = new HashSet<String>(len);
for (int i = 0; i < len; i++) {
//Reselect before retry to avoid a change of candidate `invokers`.
//NOTE: if `invokers` changed, then `invoked` also lose accuracy.
if (i > 0) {
checkWhetherDestroyed();
copyInvokers = list(invocation);
// check again
checkInvokers(copyInvokers, invocation);
}
//select -> 通过负载均衡算法之后,得到一个真正的目标invoker
Invoker<T> invoker = select(loadbalance, invocation, copyInvokers, invoked);
invoked.add(invoker);
RpcContext.getContext().setInvokers((List) invoked);
try {
//invoker -> InvokerDelegate(ProtocolFilterWrapper(ListenerInvokerWrapper(DubboInvoker)
Result result = invoker.invoke(invocation); //发起一个远程调用
if (le != null && logger.isWarnEnabled()) {
logger.warn("Although retry the method " + methodName
+ " in the service " + getInterface().getName()
+ " was successful by the provider " + invoker.getUrl().getAddress()
+ ", but there have been failed providers " + providers
+ " (" + providers.size() + "/" + copyInvokers.size()
+ ") from the registry " + directory.getUrl().getAddress()
+ " on the consumer " + NetUtils.getLocalHost()
+ " using the dubbo version " + Version.getVersion() + ". Last error is: "
+ le.getMessage(), le);
}
return result;
} catch (RpcException e) {
if (e.isBiz()) { // 业务异常,不进行重试
throw e;
}
le = e; //保存异常信息
} catch (Throwable e) {
le = new RpcException(e.getMessage(), e);
} finally {
providers.add(invoker.getUrl().getAddress());
}
}
}
5、负载均衡 select() RandomLoadBalance
在调用invoker.invoke之前,会需要通过select选择一个合适的服务进行调用,而这个选择的过程其实 就是负载均衡的实现 所有负载均衡实现类均继承自 AbstractLoadBalance,该类实现了 LoadBalance 接口,并封装了一些 公共的逻辑。所以在分析负载均衡实现之前,先来看一下 AbstractLoadBalance 的逻辑。首先来看一 下负载均衡的入口方法 select,如下:
@Override
public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) {
if (CollectionUtils.isEmpty(invokers)) {
return null;
}
if (invokers.size() == 1) { //目标服务只有一个1, 直接返回这一个
return invokers.get(0);
}
//模板方法在子类中实现
return doSelect(invokers, url, invocation);
}
负载均衡的子类实现有四个,默认情况下是RandomLoadBalance。这里就不进行分析了。。
然后带着这些 Invoker 再进行一波 loadbalance 的挑选,得到一个 Invoker,我们默认使用的是 FailoverClusterInvoker
,也就是失败自动切换的容错方式。
发起一个远程调用 invoker -> InvokerDelegate(ProtocolFilterWrapper(ListenerInvokerWrapper(DubboInvoker)))
Result result = invoker.invoke(invocation); //发起一个远程调用
发起调用的这个 invoke 又是调用抽象类中的 invoke 然后再调用子类的 doInvoker,抽象类中的方法很简单我就不展示了,影响不大,直接看子类 DubboInvoker 的 doInvoke 方法。
6、DubboInvoker.doInvoker
这里面看到一个很熟悉的东西,就是ExchangeClient,这个是客户端和服务端之间的连接 然后如果当前方法有返回值,也就是isOneway=false,则执行else逻辑,然后通过异步的形式进行通信
@Override
protected Result doInvoke(final Invocation invocation) throws Throwable {
RpcInvocation inv = (RpcInvocation) invocation;
final String methodName = RpcUtils.getMethodName(invocation);
inv.setAttachment(PATH_KEY, getUrl().getPath());
inv.setAttachment(VERSION_KEY, version);
ExchangeClient currentClient; //初始化invoker的时候,构建的一个远程通信连接
if (clients.length == 1) { //默认
currentClient = clients[0];
} else {
//通过取模获得其中一个连接
currentClient = clients[index.getAndIncrement() % clients.length];
}
try {
//表示当前的方法是否存在返回值
boolean isOneway = RpcUtils.isOneway(getUrl(), invocation);
int timeout = getUrl().getMethodParameter(methodName, TIMEOUT_KEY, DEFAULT_TIMEOUT);
if (isOneway) {//不存在返回值
boolean isSent = getUrl().getMethodParameter(methodName, Constants.SENT_KEY, false);
currentClient.send(inv, isSent);
RpcContext.getContext().setFuture(null);
return AsyncRpcResult.newDefaultAsyncResult(invocation);
} else { //存在返回值
//是否采用异步
AsyncRpcResult asyncRpcResult = new AsyncRpcResult(inv);
//timeout ->超时时间
//currentClient -> ReferenceCountExhcangeClient(HeaderExchangeClient(HeaderExchangeChannel( ->request)
CompletableFuture<Object> responseFuture = currentClient.request(inv, timeout);
responseFuture.whenComplete((obj, t) -> {
if (t != null) {
asyncRpcResult.completeExceptionally(t);
} else {
asyncRpcResult.complete((AppResponse) obj);
}
});
RpcContext.getContext().setFuture(new FutureAdapter(asyncRpcResult));
return asyncRpcResult;
}
} catch (TimeoutException e) {
throw new RpcException(RpcException.TIMEOUT_EXCEPTION, "Invoke remote method timeout. method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e);
} catch (RemotingException e) {
throw new RpcException(RpcException.NETWORK_EXCEPTION, "Failed to invoke remote method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e);
}
}
调用的三种方式 分别是 oneway、异步、同步。
7、currentClient.request
currentClient还记得是一个什么对象吗? 它实际是一个ReferenceCountExchangeClient(HeaderExchangeClient())
所以它的调用链路是 ReferenceCountExchangeClient->HeaderExchangeClient->HeaderExchangeChannel->(request方 法)
最终,把构建好的RpcInvocation,组装到一个Request对象中进行传递。
@Override
public CompletableFuture<Object> request(Object request, int timeout) throws RemotingException {
if (closed) {
throw new RemotingException(this.getLocalAddress(), null, "Failed to send request " + request + ", cause: The channel " + this + " is closed!");
}
// 组装一个request
Request req = new Request();
req.setVersion(Version.getProtocolVersion()); //
req.setTwoWay(true);
req.setData(request); //Invocation ->
DefaultFuture future = DefaultFuture.newFuture(channel, req, timeout);
try {
channel.send(req);
} catch (RemotingException e) {
future.cancel();
throw e;
}
return future;
}
channel.send的调用链路 AbstractPeer.send ->AbstractClient.send->NettyChannel.send
通过NioSocketChannel把消息发送出去
ChannelFuture future = channel.writeAndFlush(message);
8、 DefaultFuture
我们再来看一下 DefaultFuture
的内部,你有没有想过一个问题,因为是异步,那么这个 future 保存了之后,等响应回来了如何找到对应的 future 呢?
这里就揭秘了!就是利用一个唯一 ID。
private DefaultFuture(Channel channel, Request request, int timeout) {
this.channel = channel;
this.request = request;
this.id = request.getId();
this.timeout = timeout > 0 ? timeout : channel.getUrl().getPositiveParameter(TIMEOUT_KEY, DEFAULT_TIMEOUT);
// put into waiting map.
FUTURES.put(id, this);// 这里就是唯一id 和 DefaultFuture的关系
CHANNELS.put(id, channel);
}
可以看到 Request 会生成一个全局唯一 ID,然后 future 内部会将自己和 ID 存储到一个 ConcurrentHashMap。这个 ID 发送到服务端之后,服务端也会把这个 ID 返回来,这样通过这个 ID 再去ConcurrentHashMap 里面就可以找到对应的 future ,这样整个连接就正确且完整了!
我们再来看看最终接受到响应的代码,应该就很清晰了。
先看下一个响应的 message 的样子:
Response [id=14, version=null, status=20, event=false, error=null, result=RpcResult [result=Hello world, response from provider: 192.168.1.17:20881, exception=null]]
看到这个 ID 了吧,最终会调用 DefaultFuture#received
的方法。
为了能让大家更加的清晰,我再画个图:
发起请求的调用链如下图所示:
处理请求响应的调用链如下图所示:
三、调用流程-服务端端源码分析
客户端把消息发送出去之后,服务端会收到消息,然后把执行的结果返回到客户端。
服务端这边接收消息的处理链路,也比较复杂,我们回到NettServer中创建io的过程
handler配置的是nettyServerHandler
Handler与Servlet中的filter很像,通过Handler可以完成通讯报文的解码编码、拦截指定的报文、统一 对日志错误进行处理、统一对请求进行计数、控制Handler执行与否
1、NettyServerHandler.channelRead()
服务端收到读的请求是,会进入这个方法。 接着通过handler.received来处理msg,这个handle的链路很长,比较复杂,我们需要逐步剖析
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
NettyChannel channel = NettyChannel.getOrAddChannel(ctx.channel(), url, handler);
try {
handler.received(channel, msg); //接收到客户端的请求时候
} finally {
NettyChannel.removeChannelIfDisconnected(ctx.channel());
}
}
handler->MultiMessageHandler->HeartbeatHandler->AllChannelHandler->DecodeHandler- >HeaderExchangeHandler->最后进入这个方法->DubboProtocol$requestHandler(receive)
- MultiMessageHandler: 复合消息处理
- HeartbeatHandler:心跳消息处理,接收心跳并发送心跳响应
- AllChannelHandler:业务线程转化处理器,把接收到的消息封装成ChannelEventRunnable可执行任 务给线程池处理
- DecodeHandler:业务解码处理器
服务端处理链路
HeaderExchangeHandler.received
交互层请求响应处理,有三种处理方式
- handlerRequest,双向请求
- handler.received 单向请求
- handleResponse 响应消息
ExchangeHandler.reply
接着进入到ExchangeHandler.reply这个方法中
- 把message转化为Invocation
- 调用getInvoker获得一个Invoker对象
- 然后通过 Result result = invoker.invoke(inv); 进行调用
@Override
public CompletableFuture<Object> reply(ExchangeChannel channel, Object message) throws RemotingException {
Invocation inv = (Invocation) message;
Invoker<?> invoker = getInvoker(channel, inv);
RpcContext rpcContext = RpcContext.getContext();
rpcContext.setRemoteAddress(channel.getRemoteAddress());
Result result = invoker.invoke(inv);
if (result instanceof AsyncRpcResult) {
return ((AsyncRpcResult) result).getResultFuture().thenApply(r -> (Object) r);
} else {
return CompletableFuture.completedFuture(result);
}
}
getInvoker
Invoker invoker = getInvoker(channel, inv);
这里面是获得一个invoker的实现
DubboExporter exporter = (DubboExporter) exporterMap.get(serviceKey);
关键就是那个 serviceKey, 还记得之前服务暴露将invoker 封装成 exporter 之后再构建了一个 serviceKey将其和 exporter 存入了 exporterMap 中吧,这 map 这个时候就起作用了!
得到结果invoker.invoke(inv);
invoker=ProtocolFilterWrapper(InvokerDelegate(DelegateProviderMetaDataInvoker(AbstractProxy Invoker)))
最后一定会进入到这个代码里面 AbstractProxyInvoker
在AbstractProxyInvoker里面,doInvoker本质上调用的是wrapper.invokeMethod()。
@Override
public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
// TODO Wrapper cannot handle this scenario correctly: the classname contains '$'
//会创建一个动态代理,核心的方法 会生成 三个方法 setPropertyValue getPropertyValue invokeMethod 三个方法
final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type);
return new AbstractProxyInvoker<T>(proxy, type, url) {
@Override
protected Object doInvoke(T proxy, String methodName,
Class<?>[] parameterTypes,
Object[] arguments) throws Throwable {
return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);
}
};
}
而Wrapper是一个动态代理类,它的定义是这样的, 最终调用w.sayHello()方法进行处理。
找到 invoker 最终调用实现类具体的方法再返回响应整个流程就完结了,我再补充一下之前的图。
总结
今天的调用过程我再总结一遍应该差不多了。
首先客户端调用接口的某个方法,实际调用的是代理类,代理类会通过 cluster 从 directory 中获取一堆 invokers(如果有一堆的话),然后进行 router 的过滤(其中看配置也会添加 mockInvoker 用于服务降级),然后再通过 SPI 得到 loadBalance 进行一波负载均衡。
这里要强调一下默认的 cluster 是 FailoverCluster ,会进行容错重试处理,这个日后再详细分析。
现在我们已经得到要调用的远程服务对应的 invoker 了,此时根据具体的协议构造请求头,然后将参数根据具体的序列化协议序列化之后构造塞入请求体中,再通过 NettyClient 发起远程调用。
服务端 NettyServer 收到请求之后,根据协议得到信息并且反序列化成对象,再按照派发策略派发消息,默认是 All,也就是所有消息都派发到业务线程池。
业务线程会根据消息类型判断然后得到 serviceKey 从之前服务暴露生成的 exporterMap 中得到对应的 Invoker ,然后调用真实的实现类。
最终将结果返回,因为请求和响应都有一个统一的 ID, 客户端根据响应的 ID 找到存储起来的 Future, 然后塞入响应再唤醒等待 future 的线程,完成一次远程调用全过程。
当然其实隐藏了很多设计模式在其中,比如责任链、装饰器等等,没有特意挑开来说,源码中太常见了,基本上无处不在。