• 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. 《手把手教你》系列技巧篇(十三)-java+ selenium自动化测试-元素定位大法之By partial link text(详细教程)

    1.简介 本文按计划就要开始介绍partial link text,顾名思义是通过链接定位的(官方说法:超链接文本定位).什么是partial link text呢,看到part这个单词我们就可以知道 ...

  2. jquery 获取url地址参数

    1 var url = document.URL; 2 var a = url.split("="); 3 4 if(a[1]){ 5 return options.fn(this ...

  3. 过渡——transition

    ① 设置参与过渡的属性 transition-property:  ;      all / transform / 参与过渡的属性 ② 设置过渡的时长 transition-duration:  s ...

  4. 这个大表走索引字段查询的 SQL 怎么就成全扫描了,我TM人傻了

    今天收到运营同学的一个 SQL,有点复杂,尤其是这个 SQL explain 都很长时间执行不出来,于是我们后台团队帮忙解决这个 SQL 问题,却正好发现了一个隐藏很深的线上问题. select a. ...

  5. Vue学习笔记(二)动态绑定、计算属性和事件监听

    目录 一.为属性绑定变量 1. v-bind的基本使用 2. v-bind动态绑定class(对象语法) 3. v-bind动态绑定class(数组语法) 4. v-bind动态绑定style(对象语 ...

  6. RSA加密算法学习

    一.公钥加密算法 对称加密 非对称加密 二.RSA加密算法

  7. Upload-labs 文件上传靶场通关攻略(下)

    Upload-Labs靶场攻略(下) Pass-11 GET型传参,上传目录可设置,考虑00截断,在/upload/后添加1.php%00,即可上传 Pass-12 POST型传参,上传目录可设置,P ...

  8. [GXYCTF2019]Ping Ping Ping(ping命令执行绕过Waf)

    记一道ping注入的题.过滤了很多字符. 分析 简单的测了一下,很容易就拿到了flag.php和index.php. 但是存在waf无法直接查看.直接?ip=127.0.0.1|cat flag.ph ...

  9. 用 区间判断(if)来猜价格的高低

    1 #include <stdio.h> 2 #include <stdlib.h> 3 int main() 4 { 5 int price = 150; 6 int gue ...

  10. 五、从GitHub浏览Prism示例代码的方式入门WPF下的Prism之MVVM中的EventAggregator

    这一篇我们主要再看完示例12.13后,写了个例子,用于再Modules下执行ApplicationCommands,使用IActiveAware执行当前View的Commands,或者Applicat ...