手写RPC-思路与通信协议
前面文章也简单介绍了一下RPC是什么,接下来就是实现简单的RPC框架;
Simple-RPC具备的功能
- netty双端通信;
- 负载均衡;
- SPI机制;
- Agent全链路追踪;
- 优雅上下线;
- 全链路filter过滤拦截;
网络传输
我们这里使用的是netty来做网络传输层的框架;下面就不废话,直接开讲;
首先看看工程目录:
其实主要的逻辑就在network
里面,核心就是这里;
先看netty服务端和客户端的处理:
RpcServerSocket
这里的话是服务端的一些初始化逻辑,具体的代码这里就不复制了(这里还有很多操作,详细的代码都在我的gitee上面
),简单讲讲:
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
ch.pipeline().addLast(
new RpcMessageDecoder(),
new RpcMessageEncoder(),
new ServerSocketHandler());
}
});
这里粘贴部分代码,RpcMessageDecoder、RpcMessageEncoder
这里是用于处理消息协议的,后面会详细讲讲;然后ServerSocketHandler
就是对应的服务端对请求的处理器;
// 默认启动初始端口
int port = 41200;
while (NetUtil.isPortUsing(port)) {
port++;
}
LocalAddressInfo.LOCAL_HOST = NetUtil.getHost();
LocalAddressInfo.PORT = port;
这里我是采用偷懒的做法,采用的是缓存请求路径和端口,一般请求路径可以直接获取本机的地址,然后端口可以我们自己传进来;后面关于配置中心也会详细说;
编解码这里先不看,直接看下handler做了什么操作:
ServerSocketHandler
服务端的处理器:
// 不理心跳消息
if (rpcMessage.getMessageType() != MessageType.REQUEST.getValue()) {
return;
}
// 拿到请求参数
Request msg = (Request) rpcMessage.getData();
// 每次请求都进行缓存
SyncWriteMap.SERVER_REQUEST.put(msg.getRequestId(), Thread.currentThread());
//调用
Class<?> classType = ClassLoaderUtils.forName(msg.getInterfaceName());
List<String> paramTypes = msg.getParamTypes();
Class[] transfer = new Class[paramTypes.size()];
if (!CollectionUtil.isEmpty(paramTypes)) {
for (int i = 0; i < paramTypes.size(); i++) {
transfer[i] = Class.forName(paramTypes.get(i));
}
}
Method addMethod = classType.getMethod(msg.getMethodName(), transfer);
// 从缓存中里面获取bean信息,并构建key
String registerKey = CommonConstant.RPC_SERVICE_PREFIX +
SymbolConstant.UNDERLINE + msg.getInterfaceName() +
SymbolConstant.UNDERLINE + msg.getAlias();
Object objectBean = SimpleRpcServiceCache.getService(registerKey);
// 远程方法调用之前的初始化,加载各种filter;
SpiLoadFilter.loadFilters();
msg.setSimpleRpcContext(FilterInvoke.loadRemoteInvokeBeforeFilters(msg.getSimpleRpcContext()));
// 进行反射调用
SimpleRpcLog.warn("开始异步真实调用!!");
realInvokeThreadPool.submit(() -> asyncRealInvoke(ctx, rpcMessage, msg, addMethod, objectBean));
- 首先是判断消息类型,如果是心跳消息,则不做处理;
- 然后拿到对应的请求参数,通过反射拿到对应的一些类信息;
SimpleRpcServiceCache
:这是个缓存设计,在服务提供者将接口元信息注册到注册中心的时候,会把对应的实现类放到这个缓存里面,然后在接口请求进来的时候,从这里去获取,然后调用对应的方法;- 然后加载一系列的filter,主要用于传递rpc调用的上下文;
- 然后异步调用;
- 构建返回信息,然后写出;
这里就是整个处理器的处理逻辑,也比较简单;接下里看看客户端;
RpcClientSocket
客户端更简单,就是通过构造器的方式传入连接信息,然后进行连接:
public RpcClientSocket(String host, Integer port) {
this.host = host;
this.port = port;
}
ClientSocketHandler
// 拿到响应值
Response msg = (Response) rpcMessage.getData();
long requestId = rpcMessage.getRequestId();
// 拿到此次请求的id,对应的缓存信息
SyncWriteFuture future = (SyncWriteFuture) SyncWriteMap.syncKey.get(requestId);
SimpleRpcLog.info("客户端拿到了响应值:{}", JSON.toJSONString(msg));
// 这里拿到了结果,就设置响应值
if (future != null) {
future.setResponse(msg);
}
- 拿到请求id,从同步写缓存里面拿出对应的
Future
,然后将结果设置到响应中,这里的setResponse
是使用了阻塞锁的;
@Override
public void setResponse(Response response) {
this.response = response;
// 设置好了响应值之后,这里释放锁
latch.countDown();
}
客户端就这些东西,也比较简单;那么一个简单的基于netty网络编程就完成了;接下来看看编解码协议;
编解码协议
参考的是这个作者写的,这里简单的讲讲;
* <pre>
* 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
* +-----+-----+-------+----+----+----+----+-----------+---------+--------+----+----+----+----+----+----+----+---+
* | magic |version| full length |messageType|serialize|compress| RequestId |
* +-----+-----+-------+----+----+----+----+-----------+----- ---+--------+----+----+----+----+----+----+----+---+
* | |
* | body |
* | |
* | ... ... |
* +-------------------------------------------------------------------------------------------------------------+
* 2B magic(魔数)
* 1B version(版本)
* 4B full length(消息长度)
* 1B messageType(消息类型)
* 1B serialize(序列化类型)
* 1B compress(压缩类型)
* 8B requestId(请求的Id)
* body(object类型数据)
上面是消息协议各个部分代表的意思和对应的字节数;继续看看代码:
@Override
protected void encode(ChannelHandlerContext ctx, RpcMessage rpcMessage, ByteBuf out) {
// 2B magic code(魔数)
out.writeBytes(MessageFormatConstant.MAGIC);
// 1B version(版本)
out.writeByte(MessageFormatConstant.VERSION);
// 4B full length(消息长度). 总长度先空着,后面填。
out.writerIndex(out.writerIndex() + MessageFormatConstant.FULL_LENGTH_LENGTH);
// 1B messageType(消息类型)
out.writeByte(rpcMessage.getMessageType());
// 1B codec(序列化类型)
out.writeByte(rpcMessage.getSerializeType());
// 1B compress(压缩类型)
out.writeByte(rpcMessage.getCompressTye());
// 8B requestId(请求的Id)
out.writeLong(rpcMessage.getRequestId());
// 写 body,返回 body 长度
int bodyLength = writeBody(rpcMessage, out);
// 当前写指针
int writerIndex = out.writerIndex();
out.writerIndex(MessageFormatConstant.MAGIC_LENGTH + MessageFormatConstant.VERSION_LENGTH);
// 4B full length(消息长度)
out.writeInt(MessageFormatConstant.HEADER_LENGTH + bodyLength);
// 写指针复原
out.writerIndex(writerIndex);
}
上面代码就是整个编码的过程;这里比较核心的是writeBody
;我们可以看看:
byte messageType = rpcMessage.getMessageType();
// 如果是 ping、pong 心跳类型的,没有 body,直接返回头部长度
if (messageType == MessageType.HEARTBEAT.getValue()) {
return 0;
}
// 序列化器
SerializeType serializeType = SerializeType.fromValue(rpcMessage.getSerializeType());
if (serializeType == null) {
throw new IllegalArgumentException("codec type not found");
}
Serializer serializer = new ProtostuffSerializer();
// 压缩器
CompressType compressType = CompressType.fromValue(rpcMessage.getCompressTye());
Compressor compressor = new DefaultCompressor();
// 序列化
byte[] notCompressBytes = serializer.serialize(rpcMessage.getData());
// 压缩
byte[] compressedBytes = compressor.compress(notCompressBytes);
// 写 body
out.writeBytes(compressedBytes);
return compressedBytes.length;
这里的话主要涉及到压缩和序列化,写这篇文章的时候这里都是直接写死的,之后可以考虑使用SPI
机制加载自己的序列化器;编码过程就是这么点内容;
然后解码的在看看:
public RpcMessageDecoder() {
super(
// 最大的长度,如果超过,会直接丢弃
MAX_FRAME_LENGTH,
// 描述长度的字段[4B full length(消息长度)]在哪个位置:在 [2B magic(魔数)]、[1B version(版本)] 后面
MAGIC_LENGTH + VERSION_LENGTH,
// 描述长度的字段[4B full length(消息长度)]本身的长度,也就是 4B 啦
FULL_LENGTH_LENGTH,
// LengthFieldBasedFrameDecoder 拿到消息长度之后,还会加上 [4B full length(消息长度)] 字段前面的长度
// 因为我们的消息长度包含了这部分了,所以需要减回去
-(MAGIC_LENGTH + VERSION_LENGTH + FULL_LENGTH_LENGTH),
// initialBytesToStrip: 去除哪个位置前面的数据。因为我们还需要检测 魔数 和 版本号,所以不能去除
0);
}
@Override
protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
Object decoded = super.decode(ctx, in);
if (decoded instanceof ByteBuf) {
ByteBuf frame = (ByteBuf) decoded;
if (frame.readableBytes() >= HEADER_LENGTH) {
try {
return decodeFrame(frame);
} catch (Exception ex) {
SimpleRpcLog.error("Decode frame error.", ex);
} finally {
frame.release();
}
}
}
return decoded;
}
解码这里其实也不难,继承netty提供的LengthFieldBasedFrameDecoder
去完成解码的,这里关于这个类的用法不多说了,可以参考下面的文章:
https://zhuanlan.zhihu.com/p/95621344
这里的解码逻辑在:decodeFrame
方法里面,这里就不复制源码了,直接看就行,比较简单;里面有魔数和版本号的校验等;
其实编解码就是这么简单,中间使用的序列化是protostuff
;上面也说了可以考虑使用SPI机制;
注册中心
这里目前只用了redis来做注册中心,之后也可以自己扩展;直接看看,很简单:
/**
* 初始化注册中心
*
* @param url
*/
void init(SimpleRpcUrl url);
/**
* 服务注册
*
* @param request
* @return
*/
String register(RegisterInfo request);
/**
* 获取服务
*
* @param request
* @return
*/
String get(RegisterInfo request);
/**
* 注销服务
*
* @param hookEntity
* @return
*/
Boolean unregister(HookEntity hookEntity);
/**
* 优雅上下线的时候,置为下线状态 health = 0
*
* @return
*/
Boolean offline();
/**
* 应用上线,置为上线状态 health = 1
*
* @return
*/
Boolean online();
/**
* 检查服务状态
*
* @return
*/
Boolean checkHealth();
/**
* 过滤不健康的服务,不做负载,数据格式是:url + request 的数据格式
*
* @param registerInfos
*/
void filterNotHealth(Map<String, String> registerInfos);
就是提供注册中心初始化逻辑,然后就是服务提供者的元数据信息,然后和获取服务的信息;
然后通过注册中心工厂返回对应的注册中心;其实就是用来存储数据的;
配置中心
使用的是loader机制,提供对不同类型文件的获取功能,这里的话是RPC提供的配置文件获取机制,之后整合spring或者springboot都可以使用其原生的配置文件进行获取;
public interface ConfigLoader {
/**
* 加载配置项
*
* @param key 配置的 key
* @return 配置项的值,如果不存在,返回 null
*/
String loadConfigItem(String key);
}
然后看其一个实现类PropertiesConfigLoader
:
private Setting setting = null;
public PropertiesConfigLoader() {
try {
setting = SettingUtil.get("simple-rpc.properties");
} catch (NoResourceException ex) {
SimpleRpcLog.info("Config file 'simple-rpc.properties' not exist!");
}
}
@Override
public String loadConfigItem(String key) {
if (setting == null) {
return null;
}
return setting.getStr(key);
}
simple-rpc.properties
:或默认去加载这个文件里面的配置信息;
然后关于配置文件的解析和调度,都放在了配置管理器里面:ConfigManager
;这里的代码也不难,可以自行阅读;
整个核心逻辑就是这些,也没有其他的了;
当然,我们之后还是会整合spring来进行操作的;之后的文章讲下整合spring;还有SPI等,都放在后面的时候再讲;
谢谢大家阅读!!!
公众号: 搜索关注,爱搞技术的吴同学 ,公众号上会经常写实用性的文章,谢谢关注!!回复:“加好友”,可获取我的微信二维码,欢迎加好友,一起学习!!!
大量源码: 欢迎star,可能会分享微服务实战,分页插件等;gitee