手写RPC-简单思路与通信协议

手写RPC-思路与通信协议

前面文章也简单介绍了一下RPC是什么,接下来就是实现简单的RPC框架;

Simple-RPC具备的功能

  1. netty双端通信;
  2. 负载均衡;
  3. SPI机制;
  4. Agent全链路追踪;
  5. 优雅上下线;
  6. 全链路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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值