热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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

在上一篇文章中我们先列举了大致的需求,定义了消息协议。这次我们着手搭建基本的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

代码实现

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

发送请求的核心在 Gosend 方法, Go 的功能是组装参数, send 方法是将参数序列化并通过传输层的接口发送出去,同时将请求缓存到 pendingCalls 中。而 Call 方法则是直接调用 Go 方法并阻塞等待知道返回或者超时。 接收响应的核心在 input 方法, input 方法在client初始化完成时通过 go input() 执行。 input 方法包含一个无限循环,在无限循环中读取传输层的数据并将其反序列化,并将反序列化得到的响应与缓存的请求进行匹配。

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

Server

代码实现

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

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

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

codec和protocol

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

线程模型

在执行过程中,除了客户端的用户线程和服务端用来执行方法的服务线程,还分别增加了客户端轮询线程和服务端监听线程,大致的示意图如下:

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

这里的线程模型比较简单,服务端针对每个建立的连接都会创建一个线程(goroutine),虽说goroutine很轻量,但是也不是完全没有消耗的,后续可以再进一步进行优化,比如把读取数据反序列化和执行方法拆分到不同的线程执行,或者把goroutine池化等等。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 我们


推荐阅读
  • 本文介绍了如何使用C#制作Java+Mysql+Tomcat环境安装程序,实现一键式安装。通过将JDK、Mysql、Tomcat三者制作成一个安装包,解决了客户在安装软件时的复杂配置和繁琐问题,便于管理软件版本和系统集成。具体步骤包括配置JDK环境变量和安装Mysql服务,其中使用了MySQL Server 5.5社区版和my.ini文件。安装方法为通过命令行将目录转到mysql的bin目录下,执行mysqld --install MySQL5命令。 ... [详细]
  • 本文介绍了解决Netty拆包粘包问题的一种方法——使用特殊结束符。在通讯过程中,客户端和服务器协商定义一个特殊的分隔符号,只要没有发送分隔符号,就代表一条数据没有结束。文章还提供了服务端的示例代码。 ... [详细]
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • Java在运行已编译完成的类时,是通过java虚拟机来装载和执行的,java虚拟机通过操作系统命令JAVA_HOMEbinjava–option来启 ... [详细]
  • 怎么在PHP项目中实现一个HTTP断点续传功能发布时间:2021-01-1916:26:06来源:亿速云阅读:96作者:Le ... [详细]
  • Java中包装类的设计原因以及操作方法
    本文主要介绍了Java中设计包装类的原因以及操作方法。在Java中,除了对象类型,还有八大基本类型,为了将基本类型转换成对象,Java引入了包装类。文章通过介绍包装类的定义和实现,解答了为什么需要包装类的问题,并提供了简单易用的操作方法。通过本文的学习,读者可以更好地理解和应用Java中的包装类。 ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • 本文介绍了Java工具类库Hutool,该工具包封装了对文件、流、加密解密、转码、正则、线程、XML等JDK方法的封装,并提供了各种Util工具类。同时,还介绍了Hutool的组件,包括动态代理、布隆过滤、缓存、定时任务等功能。该工具包可以简化Java代码,提高开发效率。 ... [详细]
  • 如何使用Java获取服务器硬件信息和磁盘负载率
    本文介绍了使用Java编程语言获取服务器硬件信息和磁盘负载率的方法。首先在远程服务器上搭建一个支持服务端语言的HTTP服务,并获取服务器的磁盘信息,并将结果输出。然后在本地使用JS编写一个AJAX脚本,远程请求服务端的程序,得到结果并展示给用户。其中还介绍了如何提取硬盘序列号的方法。 ... [详细]
  • 原文地址:https:www.cnblogs.combaoyipSpringBoot_YML.html1.在springboot中,有两种配置文件,一种 ... [详细]
  • Mac OS 升级到11.2.2 Eclipse打不开了,报错Failed to create the Java Virtual Machine
    本文介绍了在Mac OS升级到11.2.2版本后,使用Eclipse打开时出现报错Failed to create the Java Virtual Machine的问题,并提供了解决方法。 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • 本文介绍了RPC框架Thrift的安装环境变量配置与第一个实例,讲解了RPC的概念以及如何解决跨语言、c++客户端、web服务端、远程调用等需求。Thrift开发方便上手快,性能和稳定性也不错,适合初学者学习和使用。 ... [详细]
  • 本文介绍了Web学习历程记录中关于Tomcat的基本概念和配置。首先解释了Web静态Web资源和动态Web资源的概念,以及C/S架构和B/S架构的区别。然后介绍了常见的Web服务器,包括Weblogic、WebSphere和Tomcat。接着详细讲解了Tomcat的虚拟主机、web应用和虚拟路径映射的概念和配置过程。最后简要介绍了http协议的作用。本文内容详实,适合初学者了解Tomcat的基础知识。 ... [详细]
  • 本文介绍了通过ABAP开发往外网发邮件的需求,并提供了配置和代码整理的资料。其中包括了配置SAP邮件服务器的步骤和ABAP写发送邮件代码的过程。通过RZ10配置参数和icm/server_port_1的设定,可以实现向Sap User和外部邮件发送邮件的功能。希望对需要的开发人员有帮助。摘要长度:184字。 ... [详细]
author-avatar
天之苍
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有