rpc是什么

远程过程调用协议。通过网络从远程计算机程序请求服务,不关心底层网络技术的协议。简而言之,RPC从客户端通过参数传递的方式调用另服务器的方法服务并得到返回结果

net/rpc简介

Go标准包中已经提供了对RPC的支持,支持三个级别的RPC:TCP、HTTP、JSONRPC
Go的RPC包与传统的RPC系统不同,他只支持Go开发的服务器与客户端之间的交互,因为在内部,它们采用了Gob来编码

Rpc调用流程

  • 客户端建立通信
  • 服务寻址(向注册中心请求ip)
  • 序列化(gob或json)
  • 反序列化
  • 服务调用,返回响应

HTTP和RPC对比及使用场景

传输协议

RPC:可以基于TCP协议,也可以基于HTTP协议
HTTP:基于HTTP协议

传输效率

RPC:使用自定义的TCP协议,请求报文体积更小,或者使用HTTP2协议,也可以减少报文体积,提高传输效率
HTTP:HTTP1.1的协议,请求中会包含很多无用的内容,基于HTTP2.0,简单的封装一下可以作为一个RPC来使用的,这时标准RPC框架更多的是服务治理

性能消耗

主要在于序列化和反序列化的耗时,http大部分基于json实现,字节大小和序列化耗时消耗大

服务治理和负载均衡

RPC:基本都自带了负载均衡策略,服务治理能做到自动通知,不影响上游
HTTP:需要配置Nginx,HAProxy来实现负载均衡,服务治理也需要需要事先通知

应用场景

RPC主要用于公司内部的服务调用,性能消耗低,传输效率高,服务治理方便。HTTP主要用于对外的异构环境,浏览器接口调用,APP接口调用,第三方接口调用等

协议是如何设计的

HTTP 报文,分为 header 和 body 2 部分,body 的格式和长度通过 header 中的 Content-Type 和 Content-Length 指定,服务端通过解析 header 就能够知道如何从 body 中读取需要的信息。

rpc可以在请求报文前固定的json编码,标注该请求为rpc请求和序列化方式

rpc请求头
  • 服务名和方法名,用于确认请求的方法
  • 序号,用来区分不同的请求(request ID)
  • 错误码,返回服务端的错误信息
rpc请求体
  • 序号
  • 服务名(类名)和方法名
  • 参数
  • 响应
  • error
长连接如何退出

在一次连接中,允许接收多个请求,使用了 for 无限制地等待请求的到来,直到发生错误(例如连接被关闭,接收到的报文有问题等)
sync.WaitGroup会等待正在运行的goroutine,当WaitGroup为0时,也会退出

报文会不会混在一起

在发送请求报文时,要加锁,确保每次发送的是一个完整的请求
对于发送的报文序号也要加锁,确保序号正确

会有粘包拆包的问题吗

json 字符串是有数据的边界的即 “{” 和 “}”,理论上不会出现粘包

客户端和服务端

  1. 实现了一个简易的客户端和服务端,支持消息的编码解码。对于rpc来说,双方在确定传输协议后(tcp),需要确定报文的编码格式(项目支持gob和json)。
  2. 编码格式的确认:以客户端举例,采用gob进行编码,而服务端并不知道客户端的编码方式,因此可以在请求报文前添加一个固定的json编码,编码内容:1. 标记该请求是否为rpc请求。2. CodeType结构体(编码方式,header长度,body长度)

客户端优化

  1. 实现一个支持异步和并发的高性能客户端,首先封装了结构体 Call 来承载一次 RPC 调用所需要的信息(序号,方法名,参数等)。*为了支持异步调用,Call 结构体中添加了一个字段 Done,Done 的类型是 chan Call,当调用结束时,会调用 call.done() 通知调用方。该调用不会阻塞当前线程执行
  2. 重点在于接受服务端响应,响应存在三种状态:
  • 响应不存在,可能是请求没有发送完整,或者因为其他原因被取消,但是服务端仍旧处理了。
  • 响应存在,但服务端处理出错,即返回了一个Error
  • 响应存在,服务端处理正常,那么需要从 body 中读取 Reply 的值。

服务注册(就是向客户端通知服务器提供了哪些服务)

RPC 框架的一个基础能力是:像调用本地程序一样调用远程服务,结构体的方法映射为服务。可以通过if else硬编码的方式来实现结构体与服务的映射,那么每暴露一个方法,就需要编写等量的代码,借助反射支持自动创建服务,避免了服务端提供了很多服务的情况下,需要手动创建服务对象,并且手动进行注册
如果没有服务注册,一个服务器只能注册一个服务,因此将服务的注册和服务器启动分离,使得服务端可以提供多个服务,进而需要一个容器(类),用来保存服务端提供的所有服务,方便查询使用,即通过服务名字就能返回这个服务的具体信息(服务端只负责启动,容器管理服务)。
java可以通过泛型完成,go不支持泛型,只能通过反射实现
实现服务注册功能,对 rpc 而言,一个函数需要能够被远程调用,需要满足如下条件:

  1. 方法本身是可导出的(能被其他package访问)
  2. 方法有且仅有两个参数,都是可导出类型或者内置类型
  3. 方法的第二个参数必须是一个指针(作为响应值)
  4. 方法有且仅有一个error类型的返回值

实现方式
1.借助反射支持自动创建服务,避免了服务端提供了很多服务的情况下,需要手动创建服务对象,并且手动进行注册。
2.通过大量的if else枚举每个方法

超时处理

在 3 个地方添加了超时处理机制。分别是:

  1. 客户端创建连接时
  2. 客户端 Client.Call() 整个过程导致的超时(包含发送报文,等待处理,接收报文所有阶段),即发送请求,接受响应阶段
  3. 服务端处理报文,即 Server.handleRequest 超时

第一点和第三点实现较为容易,使用 time.After() 结合 select+chan 完成。第二点则需要context 包实现(用户可以使用 context.WithTimeout 创建具备超时检测能力的 context 对象来控制)

面试:实现了创建连接时的超时处理机制,在类中添加time.Duration字段(放在那个固定的json编码中,这个json编码表明了是否为rpc请求),在建立连接时多套一层,就是select配合time.After实现超时控制,time.After就相当于实现了定时器,且是无阻塞的,select根据结果选择执行
select:用于等待一个或者多个channel的输出。()
time.After():在等待传入参数的时间后,向返回的chan里面写入当前时间

keeplive

实现一个keeplive,采用http协议,信息存放在http包头中。在注册中心的结构体中添加time.Duration字段,注册中心用map记录服务的ip和start time。基于time.NewTicker实现周期性发送心跳包的功能。然后在实现一个update功能,添加服务实例时,如果服务已经存在,则更新 start time,存在超时的服务,则删除

time.Duration:可以实现用毫秒表示的连续时间段与预先定义的时间段进行比较

json数据解码的两种方法NewDecoder与Unmarshal

json.NewDecoder用于http连接与socket连接的读取与写入,或者文件读取
json.Unmarshal在内存中读取json解码,用于直接byte的输入

go语言net/rpc处理超时丢包的方法。

就是用一个chan的ticker去等十秒,如果无返回结果直接放弃这个rpc调用 必需客户端重新发一个新的rpc调用。