从零开始实现一个RPC框架(一)

前言
在上一篇文章中我们先列举了大致的需求,定义了消息协议。这次我们着手搭建基本的RPC框架,首先实现基础的方法调用功能。

功能设计

RPC调用的第一步,就是在服务端定义要对外暴露的方法,在grpc或者是thrift中,这一步我们需要编写语言无关的idl文件,然后通过idl文件生成对应语言的代码。而在我们的框架里,出于简单起见,我们不采用idl的方式,直接在代码里定义接口和方法。这里先规定对外的方法必须遵守以下几个条件:

  1. 对外暴露的方法,其所属的类型和其自身必须是对外可见(Exported)的,也就是首字母必须是大写的
  2. 方法的参数必须为三个,而且第一个必须是context.Context类型
  3. 第三个方法参数必须是指针类型
  4. 方法返回值必须是error类型
  5. 客户端通过"Type.Method"的形式来引用服务方法,其中Type是方法实现类的全类名,Method就是方法名称

为什么要有这几个规定呢,具体的原因是这样的:因为java中的RPC框架场用到的动态代理在go语言中并不支持,所以我们需要显式地定义方法的统一格式,这样在RPC框架中才能统一地处理不同的方法。所以我们规定了方法的格式:

  • 方法的第一个参数固定为Context,用于传递上下文信息
  • 第二个参数是真正的方法参数
  • 第三个参数表示方法的返回值,调用完成后它的值就会被改变为服务端执行的结
  • 方法的返回值固定为error类型,表示方法调用过程中发生的错。

这里我们需要注意的是,服务提供者在对外暴露时并不需要以接口的形式暴露,只要服务提供者有符合规则的方法即可;而客户端在调用方法时指定的是服务提供者的具体类型,不能指定接口的名称,即使服务提供者实现了这个接口。
contet.Context
context是go语言提供的关于请求上下文的抽象,它携带了请求deadline、cancel信号的信息,还可以传递一些上下文信息,非常适合作为RPC请求的上下文,我们可以在context中设置超时时间,还可以将一些参数无关的元数据通过context传递到服务端。
实际上,方法的固定格式以及用Call和Go来表示同步和异步调用都是go自带的rpc里的规则,只是在参数里增加了context.Context。不得不说go自带的rpc设计确实十分优秀,值得好好学习理解。

接口定义

client和server

首先是面向使用者的RPC框架中的客户端和服务端接口:

type RPCServer interface {
        //注册服务实例,rcvr是receiver的意思,它是我们对外暴露的方法的实现者,metaData是注册服务时携带的额外的元数据,它描述了rcvr的其他信息
        Register(rcvr interface{}, metaData map[string]string) error
        //开始对外提供服务
        Serve(network string, addr string) error
}
type RPCClient interface {      
        //Go表示异步调用
        Go(ctx context.Context, serviceMethod string, arg interface{}, reply interface{}, done chan *Call) *Call
        //Call表示异步调用
        Call(ctx context.Context, serviceMethod string, arg interface{}, reply interface{}) error
        Close() error
}
type Call struct {
	ServiceMethod string      // 服务名.方法名
	Args          interface{} // 参数
	Reply         interface{} // 返回值(指针类型)
	Error         error       // 错误信息
	Done          chan *Call  // 在调用结束时激活
}

selector和registery

这次先实现RPC调用部分,这两层暂时忽略,后续再实现。

codec

接下来我们需要选择一个序列化协议,这里就选之前使用过的messagepack。之前设计的通信协议分为两个部分:head和body,这两个部分都需要进行序列化和反序列化。head部分是元数据,可以直接采用messagepack序列化,而body部分是方法的参数或者响应,其序列化由head中的SerializeType决定,这样的好处就是为了后续扩展方便,目前也使用messagepack序列化,后续也可以采用其他的方式序列化。
序列化的逻辑也定义为接口:

type Codec interface {
   Encode(value interface{}) ([]byte, error)
   Decode(data []byte, value interface{}) error
}

protocol

确定好了序列化协议之后,我们就可以定义消息协议相关的接口了。协议的设计参考上一篇文章:从零开始实现一个RPC框架(零)

接下来就是协议的接口定义:

//Messagge表示一个消息体
type Message struct {
	*Header //head部分, Header的定义参考上一篇文章
	Data []byte //body部分
}

//Protocol定义了如何构造和序列化一个完整的消息体
type Protocol interface {
	NewMessage() *Message
	DecodeMessage(r io.Reader) (*Message, error)
	EncodeMessage(message *Message) []byte
}

根据之前的设计,所以交互都通过接口进行,这样方便扩展和替换。

transport

协议的接口定义好了之后,接下来就是网络传输层的定义:

//传输层的定义,用于读取数据
type Transport interface {
	Dial(network, addr string) error
	//这里直接内嵌了ReadWriteCloser接口,包含Read、Write和Close方法
	io.ReadWriteCloser 
	RemoteAddr() net.Addr
	LocalAddr() net.Addr
}
//服务端监听器定义,用于监听端口和建立连接
type Listener interface {
	Listen(network, addr string) error
	Accept() (Transport, error)
	//这里直接内嵌了Closer接口,包含Close方法
	io.Closer
}

具体实现

各个层次的接口定义好了之后,就可以开始搭建基础的框架了,这里不附上具体的代码了,具体代码可以参考github链接
,这里大致描述一下各个部分的实现思路。

Client

代码实现

客户端的功能比较简单,就是将参数序列化之后,组装成一个完整的消息体发送出去。请求发送出去的同时,将未完成的请求都缓存起来,每收到一个响应就和未完成的请求进行匹配。

发送请求的核心在Go和send方法,Go的功能是组装参数,send方法是将参数序列化并通过传输层的接口发送出去,同时将请求缓存到pendingCalls中。而Call方法则是直接调用Go方法并阻塞等待知道返回或者超时。

接收响应的核心在input方法,input方法在client初始化完成时通过go input() 执行。input方法包含一个无限循环,在无限循环中读取传输层的数据并将其反序列化,并将反序列化得到的响应与缓存的请求进行匹配。

注:send和input方法的命名也是从go自带的rpc里学来的。

Server

代码实现

服务端在接受注册时,会过滤服务提供者的各个方法,将合法的方法缓存起来。

服务端的核心逻辑是serveTransport方法,它接收一个Transport对象,然后在一个无限循环中从Transport读取数据并反序列化成请求,根据请求指定的方法查找自身缓存的方法,找到对应 的方法后通过反射执行对应的实现并返。执行完成后再根据返回结果或者是执行发生的异常组装成一个完整的消息,通过Transport发送出去。

服务端在反射执行方法时,需要将实现者作为执行的第一个参数,所以参数比方法定义中的参数多一个。

codec和protocol

这两个部分就比较简单了,codec基本上就是使用messagepack实现了对应的接口;protocol的实现就是根据我们定义的协议进行解析。

线程模型

在执行过程中,除了客户端的用户线程和服务端用来执行方法的服务线程,还分别增加了客户端轮询线程和服务端监听线程,大致的示意图如下:
在这里插入图片描述
这里的线程模型比较简单,服务端针对每个建立的连接都会创建一个线程(goroutine),虽说goroutine很轻量,但是也不是完全没有消耗的,后续可以再进一步进行优化,比如把读取数据反序列化和执行方法拆分到不同的线程执行,或者把goroutine池化等等。

结语

到此我们的RPC框架已经具备了雏形,能够支持基础的RPC调用了。实际上整个框架就是参考go自带的rpc的结构,客户端和服务端的线程模型和go自带的rpc一样,只是自己定义了序列化和消息协议,而且实现的过程中保留了扩展的接口,方便后续进行完善和扩展。下一步的规划是实现过滤器链,以便后续实现服务治理相关的功能。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/517670.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

如何删除 iPhone 上的 iCloud 激活锁

Apple 在 iPhone 上通过不同的安全屏障来保护您的数据。 iCloud 激活锁可阻止外部人员访问您的手机。您可以通过打开“查找我的 iPhone”功能来激活此锁。 使用安全协议似乎是无害的,直到你到达门的另一边。如果您购买了带有激活锁的二手 iPhone 或忘记了 iCloud 凭…

面试经典-Spring篇

1、解释Spring框架中bean的生命周期 2、单例Bean的优势

CEF的了解

(14 封私信 / 80 条消息) CEF和Electron的区别是什么? - 知乎 (zhihu.com) Electron面向的开发者:会用JavaScript,HTML,CSS,不会C CEF面向的开发者:会用JavaScript,HTML,CSS,会C (14 封私信 / 80 条消息) liulun - …

【文献分享】ALKEMIE:加速材料发现和设计的智能计算平台

题目:ALKEMIE: An intelligent computational platform for accelerating materials discovery and design 链接:DOI: 10.1016/j.commatsci.2020.110064 ALKEMIE:加速材料发现和设计的智能计算平台 摘要 通过传统的试错方式开发具有目标特性…

如何使用PL/SQL Developer工具导出clob字段的表?

1 准备测试数据 导出测试对象:表test_0102,others字段为clob类型 --创建中间表test_0101 create table test_0101( id number, name varchar2(20), others clob);--插入100条测试数据 beginfor i in 1..100 loopinsert into test_0101 values(i,i||_a,l…

利用免费的开源AI引擎:优化企业合规管理与合同审核

合同作为商业活动中的重要法律文件,其准确性、完整性和合规性对于保障企业利益至关重要。然而,传统的人工合同审核和管理过程耗时耗力,且容易出错。随着人工智能技术的发展,我们现在可以通过智能化的手段来优化合同审核和管理流程…

【MATLAB源码-第30期】基于matlab的内边界边缘检测算法。

操作环境: MATLAB 2022a 1、算法描述 在计算机视觉领域,图像分割(segmentation)指的是将数字图像细分为多个图像子区域(像素的集合)(也被称作超像素)的过程。图像分割的目的是简化…

聚酰亚胺PI材料难于粘接,用什么胶水粘接?那么让我们先一步步的从认识它开始(十八): 聚酰亚胺PI泡沫有哪些应用领域

聚酰亚胺PI泡沫有哪些应用领域 聚酰亚胺(PI)泡沫由于其一系列优异的特性,在许多高性能应用领域中都有广泛的应用,包括但不限于: 航空航天领域:聚酰亚胺PI泡沫由于其出色的耐高温、隔热和阻燃性能&#xff0…

vue2中的局部组件和全局组件

注:vue2中使用组件远没有vue3中简单,具体可以看阿耿老师的lingshi小程序 如图所示:

包装类的理解

为什么需要包装类 Java提供了两个类型系统,基本数据类型与引用数据类型。使用基本数据类型在于效率,然而当要使用只针对对象设计的API或新特性(例如泛型),怎么办呢?例如: //情况1:方…

Codeforces CodeTON Round 8(Div.1 + Div.2) A~E

A. Farmer John’s Challenge (模拟) 题意: 构造一个长度为 n n n的数组,将这些数组围成一个圈(顺时针)从任意一个位置打开,有且仅有 k k k个非降序排列的数组。 分析: k 1 k1 k1时,升序输…

网络原理 - HTTP / HTTPS(4)——构造http请求

目录 一、postman 的下载安装以及简单介绍 1、下载安装 2、postman的介绍 二、通过 Java socket 构造 HTTP 请求 构造http请求的方式有两种:(1)通过代码构造(有一点难度) (2)通过第三…

StarRocks使用Minio备份和还原

1.安装minio Centos7安装minio-CSDN博客 minio api端口&#xff1a;9090 下文用到这个端口 必须提前创建好桶: packfdv5 名称自定义和后面对上就可以 2.创建备份仓库 格式&#xff1a; CREATE REPOSITORY <repository_name> WITH BROKER ON LOCATION "s3a:/…

Java设计模式:外观模式之优雅门面(九)

码到三十五 &#xff1a; 个人主页 心中有诗画&#xff0c;指尖舞代码&#xff0c;目光览世界&#xff0c;步履越千山&#xff0c;人间尽值得 ! 在软件工程中&#xff0c;设计模式是解决常见设计问题的经验总结&#xff0c;它为开发者提供了一种通用的、可复用的解决方案。外…

Jettison 1.8.7直装版 外部磁盘辅助弹出

Jettison 是一款适用于 macOS 的实用工具&#xff0c;旨在简化外部驱动器的管理。它可以自动卸载和重新挂载外部驱动器&#xff0c;帮助您更方便地使用和保护您的存储设备。 软件下载&#xff1a;Jettison 1.8.7直装版下载 自动卸载和重新挂载&#xff1a;Jettison 可以在您离开…

QT----YOLOv5检测平台

目录 1 opencv环境安装1.1 报错Could NOT find CUDNN (missing: CUDNN_LIBRARY CUDNN_INCLUDE_DIR) (Required is at least version "7.5")1.2 使用camkevs编译opencv4.8.01.3 报错operator !":重载函数具有类似的转换(编译源文件 H:\opencv-4.8.0\opencv-4.8.0…

【C++】类和对象(中篇)

目录 1、类中的6个默认成员函数 2、构造函数 2.1 概念 2.2 特性 3、析构函数 3.1 概念 3.2 特性 4、拷贝构造函数 4.1 概念 4.2 特征 5、赋值运算符重载 5.1 运算符重载 5.1.1 全局的operator ​编辑 5.1.2 成员函数的operator 5.2 赋值运算符重载 6、创建Date类…

移动端基础

移动端基础 一.了解二.视口1.视口形式2.视口标签3.viewport设置 三.二倍图1.像素比2.多倍图3.背景缩放及使用&#xff08;background-size&#xff09;4.多倍图切图 四.移动端开发选择1.单独制作2.响应式3.总结 五.移动端技术解决方案1.初始化2.盒子模型3.特殊样式 六.常见布局…

Linux| Awk 中“next”命令奇用

简介 本文[1]介绍了在Linux中使用Awk的next命令来跳过剩余的模式和表达式&#xff0c;读取下一行输入的方法。 next命令 在 Awk 系列教程中&#xff0c;本文要讲解如何使用 next 命令。这个命令能让 Awk 跳过所有你已经设置的其他模式和表达式&#xff0c;直接读取下一行数据。…

从 Redis 开源协议变更到 ES 国产化:一次技术自主的机遇

引言 近日&#xff0c;Redis Labs 宣布其主导的开源项目 Redis 将采用双重源代码可用许可证&#xff08;RSALv2&#xff09;和服务器端公共许可证&#xff08;SSPLv1&#xff09;。这一重大决策标志着 Redis 从传统的 BSD 许可证向更加严格的控制权转变&#xff0c;同时也引发…