• 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 不能实现真正泛型的原因是什么?

    大家好,我是二哥呀! 今天我来给大家讲一下,Java 不能实现真正泛型的原因是什么? 本文已同步至 GitHub <教妹学 Java>专栏,风趣幽默,通俗易懂,对 Java 初学者亲切友善 ...

  2. windows下python -m pip install --upgrade pip升级后报错的解决方法

    前言: 笔者装某库的时候提示需要升级pip版本,就python -m pip install --upgrade pip默认升级了,结果升级之后只要输入pip就有报错(如下图),网上百度了很多解决方法 ...

  3. 深入刨析tomcat 之---第11篇 how tomcat works( 第15章 ) 如何解析web.xml 文件

    writedby 张艳涛 记得当年是学习jsp的时候,写过web.xml中的标签.在之后的springmvc中也是有关于配置mvc 过滤器 和dispatchServlet的标签,之前是看不懂呢!看到 ...

  4. Blazor 组件之间使用 EventCallback 进行通信

    翻译自 Waqas Anwar 2021年3月28日的文章 <Communication between Blazor Components using EventCallback> [1 ...

  5. anyRTC Web SDK 实现音视频呼叫功能

    前言 大家好,今天小编带给大家一个基于 anyRTC Web SDK 实现音视频呼叫的功能(本项目采用vue开发). 前提条件 在开始写代码之前还需要做一些准备工作,如果你之前没有使用过 anyRTC ...

  6. 『Java』数组

    在学习数组之前先学习java.util.Arrays类中的一个静态方法Arrays.toString(). 该方法可以将传入的数组格式化为一个字符串,便于我们查看数组内容,例如: import jav ...

  7. 案例分享:Qt政务标签设计器,标签排版软件定制与打印

    需求   1.标签设计器:  2.具备文字排版功能:  3.支持六种排版格式:  4.排版后可以输出打印(demo中不包含):  5.排版后可以输出标签的指定协议文本FBD格式:  6.可以调整对应标 ...

  8. 9、改善深度神经网络之正则化、Dropout正则化

    首先我们理解一下,什么叫做正则化? 目的角度:防止过拟合 简单来说,正则化是一种为了减小测试误差的行为(有时候会增加训练误差).我们在构造机器学习模型时,最终目的是让模型在面对新数据的时候,可以有很好 ...

  9. CentOS6与CentOS7的几点区别

    重新安装了一个CentOS7,顺便整理一下与自己之前用的CentOS6的区别 CentOS6以下简称c6  CentOS7以下简称c7 1.关于文件系统: c6 6.x使用EXT4,EXT4单个文件系 ...

  10. 5 秒克隆声音「GitHub 热点速览 v.21.34」

    作者:HelloGitHub-小鱼干 本周特推的 2 个项目都很好用,Realtime-Voice-Clone-Chinese 能让你无需开启变声音,即可获得一个特定声音的语音.这个声音可以是你朋友的 ...