SOFABolt 源码分析6 - Future异步通信方式的设计

 MyRequest request = new MyRequest();
 request.setReq("hello, bolt-server");
 RpcResponseFuture future = client.invokeWithFuture("127.0.0.1:8888", request, 30 * 1000);
 MyResponse response = (MyResponse) future.get()

注意:invokeWithFuture 第三个参数 int timeoutMillis 指的是 io.netty.util.TimerTask 延迟启动时间;而 future 阻塞等待的超时时间 是在future.get( int timeoutMillis) 中进行设置的。

一、线程模型图

image.png

总结:

future 与 sync 只在“发出请求后是否阻塞等待”处不同,其他相同(包括阻塞点都是 InvokeFuture.waitResponse() 阻塞在一个 CountDownLatch 上)

  1. 客户端用户线程 user-thread 发出请求(实际上是将 netty task 压到 netty 处理队列中,netty 客户端 worker 线程进行真正的请求发出),然后直接返回一个 RpcResponseFuture 实例(该实例是 InvokeFuture 的代理类),之后 user-thread 就可以去做其他事了,当调用 RpcResponseFuture.get() 时会阻塞等待响应(底层调用的是 InvokeFuture.waitResponse())
  2. 服务端 worker 线程接收请求,根据是否在 IO 线程执行所有操作来决定是否使用一个 Bolt 线程池(或者自定义的线程池)来处理业务
  3. 服务端返回响应后,客户端 worker 线程接收到响应,将响应转发给 Bolt 线程池(或者自定义的线程池)
  4. Bolt 线程池(或者自定义的线程池)中的线程将响应设置到相应的 InvokeFuture 中,之后唤醒阻塞的 user-thread
  5. user-thread 进行反序列化和响应的抽取,最后返回给调用处

二、代码执行流程梯形图

2.1 客户端发出请求

-->RpcClient.invokeWithFuture(String addr, Object request, int timeoutMillis)
  -->RpcClientRemoting.invokeWithFuture(String addr, Object request, InvokeContext invokeContext, int timeoutMillis) // invokeContext = null
    -->Url url = this.addressParser.parse(addr) // 将 addr 转化为 Url
    -->RpcClientRemoting.invokeWithFuture(Url url, Object request, InvokeContext invokeContext, int timeoutMillis)
      <!-- 一、获取连接 -->
      -->Connection conn = getConnectionAndInitInvokeContext(url, invokeContext)
      <!-- 二、检查连接 -->
      -->this.connectionManager.check(conn) // 校验 connection 不为 null && channel 不为 null && channel 是 active 状态 && channel 可写
      <!-- 三、创建请求对象 -->
      -->RpcCommandFactory.createRequestCommand(Object requestObject)
        -->new RpcRequestCommand(Object request) // 设置唯一id + 消息类型为 Request(还有 Response 和 heartbeat)+ MyRequest request
        -->command.serialize() // 序列化
      <!-- 四、发起请求 -->
      -->BaseRemoting.invokeWithFuture(Connection conn, RemotingCommand request, int timeoutMillis)
        <!-- 4.1 创建InvokeFuture,并将 {invokeId : InvokeFuture实例} 存储到 Connection 的 Map<Integer, InvokeFuture> invokeFutureMap -->
        -->InvokeFuture future = new DefaultInvokeFuture(int invokeId, InvokeCallbackListener callbackListener, InvokeCallback callback, byte protocol, CommandFactory commandFactory, InvokeContext invokeContext);
        -->Connection.addInvokeFuture(InvokeFuture future)
          -->Connection.(Map<Integer, InvokeFuture> invokeFutureMap).putIfAbsent(future.invokeId(), future) // future.invokeId()就是消息的唯一id,后续的响应也会塞入这个id,最后根据响应中的该id来获取对应的InvokeFuture,做相应的操作
        <!-- 4.2 创建Timeout实例并设置到future实例中 -->
        -->TimerHolder.getTimer().newTimeout
        -->InvokeFuture.addTimeout(Timeout timeout)
        <!-- 4.3 使用 netty 发送消息 -->
        -->conn.getChannel().writeAndFlush(request) // netty发送消息 然后返回刚刚创建的InvokeFuture实例
        <!-- 4.4 创建RpcResponseFuture,包装InvokeFuture实例 -->
        -->new RpcResponseFuture(String addr, InvokeFuture future)

关于连接 Connection 相关的,放在《Connection 连接设计》章节分析,此处跳过;
关于超时 TimeOut 相关的,放在《TimeOut 超时设计》章节分析,此处跳过;

总结:

  1. 获取连接
  1. 检查连接
  1. 使用 RpcCommandFactory 创建请求对象 + 序列化
  1. 发起请求
  • 创建 InvokeFuture,并将 {invokeId : InvokeFuture实例} 存储到 Connection 的Map<Integer, InvokeFuture> invokeFutureMap
  • 创建 io.netty.util.Timeout 实例并设置到future实例中
  • 使用 netty 发送消息
  • 创建RpcResponseFuture,包装InvokeFuture实例

2.2 客户端阻塞等待响应

RpcResponseFuture.get()
<!-- 一、当前线程阻塞在这里,等待响应返回并填充到future后,再进行唤醒 -->
-->RemotingCommand response = InvokeFuture.waitResponse(timeoutMillis) //阻塞等待消息
  -->this.countDownLatch.await(timeoutMillis, TimeUnit.MILLISECONDS)

... 服务端处理并返回响应 ...
... 客户端 netty worker 线程接收响应并填充到指定 invokeId 的 InvokeFuture 中,唤醒如下流程 ...

RpcResponseResolver.resolveResponseObject(ResponseCommand responseCommand, String addr)
-->preProcess(ResponseCommand responseCommand, String addr) // 处理错误的响应,如果有,直接封装为响应的 Exception,然后 throw
-->toResponseObject(ResponseCommand responseCommand) // 如果是成功状态
-->response.deserialize() // 反序列化
-->response.getResponseObject() // 抽取响应结果

总结:

  1. 如果服务端已经填充好了相应数据,则直接进行第2步对响应消息进行处理;否则,当前线程阻塞(countDownLatch.await(timeoutMillis)),等待响应返回并填充到 InvokeFuture 后,再进行唤醒(countDownLatch.countDown()

服务端处理请求并返回响应

客户端 netty worker 线程接收响应并填充到指定 invokeId 的 InvokeFuture 中,唤醒当前线程

  1. 处理响应消息:首先处理错误响应的情况,如果响应是错误状态,直接封装为响应的 Exception,然后 throw;如果响应是成功状态,进行 RpcResponseCommand 的反序列化,之后从 RpcResponseCommand 抽取真正的相应数据

2.3 服务端处理请求并返回响应

RpcHandler.channelRead(ChannelHandlerContext ctx, Object msg)
<!-- 一、创建上下文 -->
-->new InvokeContext()
-->new RemotingContext(ChannelHandlerContext ctx, InvokeContext invokeContext, boolean serverSide, ConcurrentHashMap<String, UserProcessor<?>> userProcessors)
<!-- 二、根据 channel 中的附加属性获取相应的 Protocol,之后使用该 Protocol 实例的 CommandHandler 处理消息 -->
-->RpcCommandHandler.handle(RemotingContext ctx, Object msg)
  <!-- 2.1 从 CommandHandler 中获取 CommandCode 为 REQUEST 的 RemotingProcessor 实例 RpcRequestProcessor,之后使用 RpcRequestProcessor 进行请求处理-->
  -->RpcRequestProcessor.process(RemotingContext ctx, RpcRequestCommand cmd, ExecutorService defaultExecutor)
    <!-- 2.1.1 反序列化clazz(感兴趣key),用于获取相应的UserProcessor;如果相应的UserProcessor==null,创建异常响应,发送给调用端,否则,继续执行 -->
    -->反序列化 clazz
    <!-- 
        2.1.2 如果 userProcessor.processInIOThread()==true,直接对请求进行反序列化,然后创建ProcessTask任务,最后直接在当前的netty worker线程中执行ProcessTask.run();
              否则,如果用户自定义了ExecutorSelector,则从众多的自定义线程池选择一个线程池,如果没定义,则使用自定义的线程池userProcessor.getExecutor(),如果最后没有自定义的线程池,则使用ProcessorManager的defaultExecutor,
              来执行ProcessTask.run()
    -->
      <!-- ProcessTask.run() -->
      -->RpcRequestProcessor.doProcess(RemotingContext ctx, RpcRequestCommand cmd)
        -->反序列化header、content
        -->dispatchToUserProcessor(RemotingContext ctx, RpcRequestCommand cmd)
          <!-- 构造用户业务上下文 -->
          -->new DefaultBizContext(remotingCtx)
          <!-- 使用用户自定义处理器处理请求 -->
          -->MyServerUserProcessor.handleRequest(BizContext bizCtx, MyRequest request)
          <!-- 创建响应 -->
          -->RemotingCommand response = RpcCommandFactory.createResponse(Object responseObject, RemotingCommand requestCmd) // 这里将response.id = requestCmd.id
          <!-- 序列化响应并发送 -->
          -->RpcRequestProcessor.sendResponseIfNecessary(RemotingContext ctx, byte type, RemotingCommand response)
            -->response.serialize() // 序列化
            -->ctx.writeAndFlush(serializedResponse) // netty发送响应

总结:

与 Sync 完全相同

  1. 创建 InvokeContext 和 RemotingContext
  1. 根据 channel 中的附加属性获取相应的 Protocol,之后使用该 Protocol 实例的 CommandHandler 处理消息
  • 从 CommandHandler 中获取 CommandCode 为 REQUEST 的 RemotingProcessor 实例 RpcRequestProcessor,之后使用 RpcRequestProcessor 进行请求处理
  • 反序列化clazz(感兴趣key),用于获取相应的UserProcessor;如果相应的 UserProcessor==null,创建异常响应,发送给调用端,否则,继续执行
  • 如果 userProcessor.processInIOThread()==true,直接对请求进行反序列化,然后创建 ProcessTask 任务,最后直接在当前的 netty worker 线程中执行 ProcessTask.run();

否则,如果用户 UserProcessor 自定义了 ExecutorSelector,则从众多的自定义线程池选择一个线程池,如果没定义,则使用 UserProcessor 自定义的线程池 userProcessor.getExecutor(),如果还没有,则使用 RemotingProcessor 自定义的线程池 executor,如果最后没有自定义的线程池,则使用 ProcessorManager 的defaultExecutor,来执行ProcessTask.run()

  • 反序列化 header、content(如果用户自定义了 ExecutorSelector,则header的反序列化需要提前,header 会作为众多自定义线程池的选择参数)
  • 构造用户业务上下文 DefaultBizContext
  • 使用用户自定义处理器处理请求
  • 创建响应,序列化响应并发送

2.4 客户端接收响应

RpcHandler.channelRead(ChannelHandlerContext ctx, Object msg)
<!-- 一、创建上下文 -->
-->new RemotingContext(ctx, new InvokeContext(), serverSide, userProcessors)
<!-- 二、根据 channel 中的附加属性获取相应的 Protocol,之后使用该 Protocol 实例的 CommandHandler 处理消息 -->
-->RpcCommandHandler.handle(RemotingContext ctx, Object msg)
  <!-- 三、获取线程池(如果 RemotingProcessor 自定义了线程池 executor 执行 ProcessTask.run(),否则使用 ProcessorManager 的 defaultExecutor)-->
  -->AbstractRemotingProcessor.process(RemotingContext ctx, T msg, ExecutorService defaultExecutor)
    -->new ProcessTask(RemotingContext ctx, T msg)
    <!-- ProcessTask.run() -->
    -->RpcResponseProcessor.doProcess(RemotingContext ctx, RemotingCommand cmd) // cmd是RpcResponseCommand
      <!-- 3.1 从连接中根据响应id获取请求的 InvokeFuture -->
      -->InvokeFuture future = conn.removeInvokeFuture(cmd.getId()) // 根据响应id获取请求的 InvokeFuture
      <!-- 3.2 填充响应 + 唤醒主线程 + 取消 io.netty.util.TimerTask 超时任务 + 如果有回调,调用回调 -->
      -->InvokeFuture.putResponse(RemotingCommand response)
        -->this.countDownLatch.countDown() // 解锁等待,唤醒主线程
      -->future.cancelTimeout();
      -->DefaultInvokeFuture.executeInvokeCallback() // 执行相应的 callbackListener

总结:

唯一与 sync 不同的是对于 Future 和 Callback 模式,启动了 netty 的时间轮超时任务,此处会取消该 TimerTask 任务

  1. 创建 InvokeContext 和 RemotingContext
  1. 根据 channel 中的附加属性获取相应的 Protocol,之后使用该 Protocol 实例的 CommandHandler 处理消息
  • 从 CommandHandler 中获取 CommandCode 为 RESPONSE 的 RemotingProcessor 实例 RpcResponseProcessor,之后使用 RpcResponseProcessor 进行响应处理
  • 如果RemotingProcessor自定义了线程池executor执行ProcessTask.run(),否则使用ProcessorManager的defaultExecutor
    ProcessTask.run():
  • 从连接中根据响应 id 获取请求的 InvokeFuture
  • 填充响应 + 唤醒阻塞线程 + 取消 io.netty.util.TimerTask 超时任务 + 如果有回调,调用回调
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 161,780评论 4 369
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,424评论 1 305
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,397评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,576评论 0 218
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,997评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,945评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,107评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,850评论 0 208
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,625评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,804评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,285评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,613评论 3 261
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,291评论 3 242
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,164评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,963评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,096评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,886评论 2 278

推荐阅读更多精彩内容