• Netty 在服务端与客户端的网络通信中,使用的是异步双向通信(双工)的方式,即客户端和服务端可以相互主动发请求给对方,发消息后不会同步等响应。这样就会有一下问题:
  1. 如何识别消息是请求还是响应?
  2. 请求如何正确对应到响应?

1. 如何识别消息是请求还是响应

为了识别消息类型是请求或者响应,我们在消息中加入了 messageType 的属性,在上文我们也提到,这个消息类型在自定义协议的头部,他有几种类型:请求、响应、心跳,我们先来说说请求、响应。

public enum MessageType {
/**
* 普通请求
*/
REQUEST((byte) 1), /**
* 普通响应
*/
RESPONSE((byte) 2), /**
* 心跳
*/
HEARTBEAT((byte) 3),
;
private final byte value;
}

请求(Request)的核心字段如下:

public class RpcRequest {
/**
* 接口名
*/
private String interfaceName;
/**
* 方法名
*/
private String methodName;
/**
* 参数列表
*/
private Object[] params;
/**
* 参数类型列表
*/
private Class<?>[] paramTypes;
/**
* 接口版本
*/
private String version;
}

响应(Response)的核心字段如下:

public class RpcResponse<T> {
/**
* 请求id
*/
private long requestId;
/**
* 响应码
*/
private Integer code;
/**
* 提示消息
*/
private String message;
/**
* 响应数据
*/
private T data;
}

发送消息的时候,按照消息类型和结构体,将数据组装好,写到 channel 即可。接收消息则要先解码,从消息头拿到消息类型,根据消息类型来反序列化到对应的结构体。

2. 请求如何正确对应到响应

流程图如下:



有几个关键点:

  1. 客户端请求之后拿到 Future
  2. 有一个 Map 存放未响应的请求,Key: RequestId,Value: Future
  3. 服务端响应的数据中,包含了客户端的 RequestId,这是对应的关键
  4. 响应的结果会被 NettyClientHandler.channelRead0 监听到,根据响应的 RequestId 取出对应的 Future
  5. 将结果写到对应的 Future 中
  6. 客户端通过 future.get() 获取到数据

1) 客户端发请求

代码如下:

public class NettyInvoker extends AbstractInvoker {

    private final NettyClient nettyClient = NettyClient.getInstance();

    @Override
protected RpcResult doInvoke(RpcRequest request, URL selected) throws RpcException {
// 获取 Channel
Channel channel = nettyClient.getChannel(socketAddress);
// 构造一个空 Future
CompletableFuture<RpcResponse<?>> resultFuture = new CompletableFuture<>();
// 构建 RPC 消息,此处会构建 requestId
RpcMessage rpcMessage = buildRpcMessage(request);
// 将 request 和 Future 对应放到 Map 中
UnprocessedRequests.put(rpcMessage.getRequestId(), resultFuture);
// 发出请求
channel.writeAndFlush(rpcMessage);
// 返回结果
return new AsyncResult(resultFuture);
}
// ...
}

返回的 AsyncResult 只是 future 的包装。

public class AsyncResult implements RpcResult {

    private final CompletableFuture<?> future;

    public AsyncResult(CompletableFuture<?> future) {
this.future = future;
}
}

2) 请求暂存

这个存储未响应的请求在 ccx-rpc 中是 UnprocessedRequests 类在管理:

public class UnprocessedRequests {
private static final Map<Long, CompletableFuture<RpcResponse<?>>> FUTURE_MAP = new ConcurrentHashMap<>(); public static void put(long requestId, CompletableFuture<RpcResponse<?>> future) {
FUTURE_MAP.put(requestId, future);
}
}

3) 服务端响应数据监听

使用 Netty 的 Handler 监听服务端响应的数据,当有数据响应,则调用 UnprocessedRequests.complete 写入。

public class NettyClientHandler extends SimpleChannelInboundHandler<RpcMessage> {
@Override
protected void channelRead0(ChannelHandlerContext context, RpcMessage requestMsg) {
RpcResponse<?> response = (RpcResponse<?>) requestMsg.getData();
UnprocessedRequests.complete(response);
}
}

UnprocessedRequests.complete 实际上就是找出并删除对应的请求,然后将数据写入:future.complete(rpcResponse)

public class UnprocessedRequests {
private static final Map<Long, CompletableFuture<RpcResponse<?>>> FUTURE_MAP = new ConcurrentHashMap<>(); /**
* 完成响应
*
* @param rpcResponse 响应内容
*/
public static void complete(RpcResponse<?> rpcResponse) {
CompletableFuture<RpcResponse<?>> future = FUTURE_MAP.remove(rpcResponse.getRequestId());
if (future != null) {
future.complete(rpcResponse);
} else {
throw new IllegalStateException("future is null. rpcResponse=" + JSONUtil.toJsonStr(rpcResponse));
}
}
}

最后通过 AsyncResult.getData 可以获取到数据。

public class AsyncResult implements RpcResult {

    private final CompletableFuture<?> future;

    public AsyncResult(CompletableFuture<?> future) {
this.future = future;
} @Override
public Object getData() {
try {
return future.get();
} catch (InterruptedException | ExecutionException e) {
log.error("getData error.", e);
}
return null;
}
}

总结

Netty 网络通信是异步双工的,我们需要用正确 Request-Response 模型让客户端和服务端正确交互。

  1. 如何区分请求或响应?

    在消息中,可以加入 messageType 字段用来区分是请求或者响应。
  2. 如何把请求和响应对应?

    发出的请求需要用 RequestId 标记并用 Map 存起来。服务端收到请求之后,将 RequestId 原封不动写到响应结果中。客户端收到响应结果后,拿出 RequestId 找到对应的 Future 并写入结果。

ccx-rpc 代码已经开源

Github:https://github.com/chenchuxin/ccx-rpc

Gitee:https://gitee.com/imccx/ccx-rpc

从零开始实现简单 RPC 框架 8:网络通信之 Request-Response 模型的更多相关文章

  1. 从零开始实现简单 RPC 框架 5:网络通信之序列化

    我们在接下来会开始讲网络通信相关的内容了.既然是网络通信,那必然会涉及到序列化的相关技术. 下面是 ccx-rpc 序列化器的接口定义. /** * 序列化器 */ public interface ...

  2. 从零开始实现简单 RPC 框架 6:网络通信之 Netty

    网络通信的开发,就涉及到一些开发框架:Java NIO.Netty.Mina 等等. 理论上来说,类似于序列化器,可以为其定义一套统一的接口,让不同类型的框架实现,事实上,Dubbo 就是这么干的. ...

  3. 从零开始实现简单 RPC 框架 7:网络通信之自定义协议(粘包拆包、编解码)

    当 RPC 框架使用 Netty 通信时,实际上是将数据转化成 ByteBuf 的方式进行传输. 那如何转化呢?可不可以把 请求参数 或者 响应结果 直接无脑序列化成 byte 数组发出去? 答:直接 ...

  4. 从零开始实现简单 RPC 框架 2:扩展利器 SPI

    RPC 框架有很多可扩展的地方,如:序列化类型.压缩类型.负载均衡类型.注册中心类型等等. 假设框架提供的注册中心只有zookeeper,但是使用者想用Eureka,修改框架以支持使用者的需求显然不是 ...

  5. 从零开始实现简单 RPC 框架 9:网络通信之心跳与重连机制

    一.心跳 什么是心跳 在 TPC 中,客户端和服务端建立连接之后,需要定期发送数据包,来通知对方自己还在线,以确保 TPC 连接的有效性.如果一个连接长时间没有心跳,需要及时断开,否则服务端会维护很多 ...

  6. 从零开始实现简单 RPC 框架 4:注册中心

    RPC 中服务消费端(Consumer) 需要请求服务提供方(Provider)的接口,必须要知道 Provider 的地址才能请求到. 那么,Consumer 要从哪里获取 Provider 的地址 ...

  7. 从零开始实现简单 RPC 框架 3:配置总线 URL

    URL 的定义 URL 对于大部分程序猿来说都是很熟悉的,其全称是 Uniform Resource Locator (统一资源定位器).它是互联网的统一资源定位标志,也就是指网络地址. 一个标准的 ...

  8. Java实现简单RPC框架(转)

    一.RPC简介 RPC,全称Remote Procedure Call, 即远程过程调用,它是一个计算机通信协议.它允许像本地服务一样调用远程服务.它可以有不同的实现方式.如RMI(远程方法调用).H ...

  9. RPC笔记之初探RPC:DIY简单RPC框架

    一.什么是RPC RPC(Remote Procedure Call)即远程过程调用,简单的说就是在A机器上去调用B机器上的某个方法,在分布式系统中极其常用. rpc原理其实很简单,比较容易理解,在r ...

随机推荐

  1. SpringBoot下Schdule的配置与使用

    我们在平常项目开发中,经常会用到周期性定时任务,这个时候使用定时任务就能很方便的实现.在SpringBoot中用得最多的就是Schedule. 一.SpringBoot集成Schedule 1.依赖配 ...

  2. jvm源码解读--10 enum WKID 枚举

    源码中对于枚举类型WKID的使用 static bool initialize_wk_klass(WKID id, int init_opt, TRAPS); static void initiali ...

  3. 浏览器不支持promise的finally

    IE浏览器以及edge浏览器的不支持es6里面promise的finally 解决方法: 1.npm install axios promise.prototype.finally --save 2. ...

  4. 微软内推常见问题 Q&A

    很高兴,已经成功内推 59 人拿到了微软 offer! 两年前,我就已经写过一篇微软面经,帮助到了不少人: 微软面经分享:如何更好地做好面试准备 在这两年的内推过程中,往往会有不少候选人来问我有关微软 ...

  5. shiro反序列化550、721

    shiro550反序列化 获取docker镜像 docker pull medicean/vulapps:s_shiro_1 重启docker systemctl restart docker 启动d ...

  6. 大数据学习(18)—— Flume介绍

    老规矩,学习新东西先上官网瞅瞅Apache Flume Flume是什么 Flume是一个分布式.可靠的大规模高效日志收集.汇聚和传输的这么一个服务.它的架构基于流式数据,配置简单灵活.它具备可调节的 ...

  7. OpenFaaS实战之七:java11模板解析

    欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...

  8. 一文读懂Spring动态配置多数据源---源码详细分析

    Spring动态多数据源源码分析及解读 一.为什么要研究Spring动态多数据源 ​ 期初,最开始的原因是:想将答题服务中发送主观题答题数据给批改中间件这块抽象出来, 但这块主要使用的是mq消息的方式 ...

  9. 从小白角度探索Android事件分发机制

    今早来上班时看到郭神这边文章超赞,剧情很好引人入胜,特此备份! https://mp.weixin.qq.com/s?__biz=MzA5MzI3NjE2MA==&mid=2650244386 ...

  10. RHCSA_DAY03

    cd 切换工作目录命令 cd(英文全拼:change directory)切换目录 命令格式:cd [-选项] [目录名] 提示:目录名称可以是绝对路径或相对路径,如果不指定目录名称,则切换到当前用户 ...