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

使用golang实现JSONRPC2.0

本文同时发布于我的博客yeqown.github.io什么是RPC?远程过程调用(英语:RemoteProcedureCall,缩写为RPC)是一个计算机通信协议。该协议允许运行于

本文同时发布于我的博客
yeqown.github.io

什么是RPC?

远程过程调用(英语:Remote Procedure Call,缩写为 RPC)是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互作用编程。如果涉及的软件采用面向对象编程,那么远程过程调用亦可称作远程调用或远程方法调用。

远程过程调用是一个分布式计算的客户端-服务器(Client/Server)的例子,它简单而又广受欢迎。远程过程调用总是由客户端对服务器发出一个执行若干过程请求,并用客户端提供的参数。执行结果将返回给客户端。由于存在各式各样的变体和细节差异,对应地派生了各式远程过程调用协议,而且它们并不互相兼容。

什么又是JSON-RPC?

JSON-RPC,是一个无状态且轻量级的远程过程调用(RPC)传送协议,其传递内容通过 JSON 为主。相较于一般的 REST 通过网址(如 GET /user)调用远程服务器,JSON-RPC 直接在内容中定义了欲调用的函数名称(如 {“method”: “getUser”}),这也令开发者不会陷于该使用 PUT 或者 PATCH 的问题之中。
更多JSON-RPC约定参见:https://zh.wikipedia.org/wiki/JSON-RPC

问题

服务端注册及调用

约定如net/rpc

  • the method’s type is exported.
  • the method is exported.
  • the method has two arguments, both exported (or builtin) types.
  • the method’s second argument is a pointer.
  • the method has return type error.

// 这就是约定
func (t *T) MethodName(argType T1, replyType *T2) error

那么问题来了:

问题1: Server怎么来注册`t.Methods`?
问题2: JSON-RPC请求参数里面的Params给到args?

server端类型定义:

type methodType struct {
method reflect.Method // 用于调用
ArgType reflect.Type
ReplyType reflect.Type
}
type service struct {
name string // 服务的名字, 一般为`T`
rcvr reflect.Value // 方法的接受者, 即约定中的 `t`
typ reflect.Type // 注册的类型, 即约定中的`T`
method map[string]*methodType // 注册的方法, 即约定中的`MethodName`的集合
}
// Server represents an RPC Server.
type Server struct {
serviceMap sync.Map // map[string]*service
}

解决问题1,参考了net/rpc中的注册调用。主要使用reflect这个包。代码如下:

// 解析传入的类型及相应的可导出方法,将rcvr的type,Methods的相关信息存放到Server.m中。
// 如果type是不可导出的,则会报错
func (s *Server) Register(rcvr interface{}) error {
_service := new(service)
_service.typ = reflect.TypeOf(rcvr)
_service.rcvr = reflect.ValueOf(rcvr)
sname := reflect.Indirect(_service.rcvr).Type().Name()
if sname == "" {
err_s := "rpc.Register: no service name for type " + _service.typ.String()
log.Print(err_s)
return errors.New(err_s)
}
if !isExported(sname) {
err_s := "rpc.Register: type " + sname + " is not exported"
log.Print(err_s)
return errors.New(err_s)
}
_service.name = sname
_service.method = suitableMethods(_service.typ, true)
// sync.Map.LoadOrStore
if _, dup := s.m.LoadOrStore(sname, _service); dup {
return errors.New("rpc: service already defined: " + sname)
}
return nil
}
// 关于suitableMethods,也是使用reflect,
// 来获取_service.typ中的所有可导出方法及方法的相关参数,保存成*methodType

suitableMethods代码由此去:https: //github.com/yeqown/rpc/blob/master/server.go#L105

解决问题2,要解决问题2,且先看如何调用Method,代码如下:

// 约定: func (t *T) MethodName(argType T1, replyType *T2) error
// s.rcvr: 即约定中的 t
// argv: 即约定中的 argType
// replyv: 即约定中的 replyType
func (s *service) call(mtype *methodType, req *Request, argv, replyv reflect.Value) *Response {
function := mtype.method.Func
returnValues := function.Call([]reflect.Value{s.rcvr, argv, replyv})
errIter := returnValues[0].Interface()
errmsg := ""
if errIter != nil {
errmsg = errIter.(error).Error()
return NewResponse(req.ID, nil, NewJsonrpcErr(InternalErr, errmsg, nil))
}
return NewResponse(req.ID, replyv.Interface(), nil)
}

看了如何调用,再加上JSON-RPC的约定,知道了传给服务端的是一个JSON,而且其中的Params是一个json格式的数据。那就变成了:interface{} – req.Params 到reflect.Value – argv。那么怎么转换呢?看代码:

func (s *Server) call(req *Request) *Response {
// ....
// 根据req.Method来查询method
// req.Method 格式如:"ServiceName.MethodName"
// mtype *methodType
mtype := svc.method[methodName]
// 根据注册时候的mtype.ArgType来生成一个reflect.Value
argIsValue := false // if true, need to indirect before calling.
var argv reflect.Value
if mtype.ArgType.Kind() == reflect.Ptr {
argv = reflect.New(mtype.ArgType.Elem())
} else {
argv = reflect.New(mtype.ArgType)
argIsValue = true
}
if argIsValue {
argv = argv.Elem()
}
// 为argv参数生成了一个reflect.Value,但是argv目前为止都还是是0值。
// 那么怎么把req.Params 复制给argv ?
// 我尝试过,argv = reflect.Value(req.Params),但是在调用的时候 会提示说:“map[string]interface{} as main.*Args”,
// 这也就是说,并没有将参数的值正确的赋值给argv。
// 后面才又了这个convert函数:
// bs, _ := json.Marshal(req.Params)
// json.Unmarshal(bs, argv.Interface())
// 因此有一些限制~,就不多说了
convert(req.Params, argv.Interface())
// Note: 约定中ReplyType是一个指针类型,方便赋值。
// 根据注册时候的mtype.ReplyType来生成一个reflect.Value
replyv := reflect.New(mtype.ReplyType.Elem())
switch mtype.ReplyType.Elem().Kind() {
case reflect.Map:
replyv.Elem().Set(reflect.MakeMap(mtype.ReplyType.Elem()))
case reflect.Slice:
replyv.Elem().Set(reflect.MakeSlice(mtype.ReplyType.Elem(), 0, 0))
}
return svc.call(mtype, req, argv, replyv)
}

支持HTTP调用

已经完成了上述的部分,再来谈支持HTTP就非常简单了。实现http.Handler接口就行啦~。如下:

// 支持之POST & json
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var resp *Response
w.Header().Set("Content-Type", "application/json")
if r.Method != http.MethodPost {
resp = NewResponse("", nil, NewJsonrpcErr(
http.StatusMethodNotAllowed, "HTTP request method must be POST", nil),
)
response(w, resp)
return
}
// 解析请求参数到[]*rpc.Request
reqs, err := getRequestFromBody(r)
if err != nil {
resp = NewResponse("", nil, NewJsonrpcErr(InternalErr, err.Error(), nil))
response(w, resp)
return
}
// 处理请求,包括批量请求
resps := s.handleWithRequests(reqs)
if len(resps) > 1 {
response(w, resps)
} else {
response(w, resps[0])
}
return
}

使用示例

服务端使用

// test_server.go
package main
import (
// "fmt"
"net/http"
"github.com/yeqown/rpc"
)
type Int int
type Args struct {
A int `json:"a"`
B int `json:"b"`
}
func (i *Int) Sum(args *Args, reply *int) error {
*reply = args.A + args.B
return nil
}
type MultyArgs struct {
A *Args `json:"aa"`
B *Args `json:"bb"`
}
type MultyReply struct {
A int `json:"aa"`
B int `json:"bb"`
}
func (i *Int) Multy(args *MultyArgs, reply *MultyReply) error {
reply.A = (args.A.A * args.A.B)
reply.B = (args.B.A * args.B.B)
return nil
}
func main() {
s := rpc.NewServer()
mine_int := new(Int)
s.Register(mine_int)
go s.HandleTCP("127.0.0.1:9999")
// 开启http
http.ListenAndServe(":9998", s)
}

客户端使用

// test_client.go
package main
import (
"github.com/yeqown/rpc"
)
type Args struct {
A int `json:"a"`
B int `json:"b"`
}
type MultyArgs struct {
A *Args `json:"aa"`
B *Args `json:"bb"`
}
type MultyReply struct {
A int `json:"aa"`
B int `json:"bb"`
}
func main() {
c := rpc.NewClient()
c.DialTCP("127.0.0.1:9999")
var sum int
c.Call("1", "Int.Sum", &Args{A: 1, B: 2}, &sum)
println(sum)
c.DialTCP("127.0.0.1:9999")
var reply MultyReply
c.Call("2", "Int.Multy", &MultyArgs{A: &Args{1, 2}, B: &Args{3, 4}}, &reply)
println(reply.A, reply.B)
}

运行截图

《使用golang 实现JSON-RPC2.0》
《使用golang 实现JSON-RPC2.0》
《使用golang 实现JSON-RPC2.0》

实现

上面只挑了我觉得比较重要的部分,讲了实现,更多如客户端的支持,JSON-RPC的请求响应定义,可以在项目中里查阅。目前基于TCP和HTTP实现了JSON-RPC,项目地址:github.com/yeqown/rpc

缺陷

只支持JSON-RPC, 且还没有完全实现JSON-RPC的约定。譬如批量调用中:

若批量调用的 RPC 操作本身非一个有效 JSON 或一个至少包含一个值的数组,则服务端返回的将单单是一个响应对象而非数组。若批量调用没有需要返回的响应对象,则服务端不需要返回任何结果且必须不能返回一个空数组给客户端。

阅读参考中的两个RPC,发现两者都是使用的codec的方式来提供扩展。因此以后可以考虑使用这种方式来扩展。

参考

  • net/rpc
  • grollia/rpc

推荐阅读
  • uniapp开发H5解决跨域问题的两种代理方法
    本文介绍了uniapp开发H5解决跨域问题的两种代理方法,分别是在manifest.json文件和vue.config.js文件中设置代理。通过设置代理根域名和配置路径别名,可以实现H5页面的跨域访问。同时还介绍了如何开启内网穿透,让外网的人可以访问到本地调试的H5页面。 ... [详细]
  • Jquery 跨域问题
    为什么80%的码农都做不了架构师?JQuery1.2后getJSON方法支持跨域读取json数据,原理是利用一个叫做jsonp的概念。当然 ... [详细]
  • 一、路由首先需要配置路由,就是点击good组件进入goodDetail组件配置路由如下{path:goodDetail,component:goodDetail}同时在good组件中写入如下点击事件,路由中加入 ... [详细]
  • 在Docker中,将主机目录挂载到容器中作为volume使用时,常常会遇到文件权限问题。这是因为容器内外的UID不同所导致的。本文介绍了解决这个问题的方法,包括使用gosu和suexec工具以及在Dockerfile中配置volume的权限。通过这些方法,可以避免在使用Docker时出现无写权限的情况。 ... [详细]
  • YOLOv7基于自己的数据集从零构建模型完整训练、推理计算超详细教程
    本文介绍了关于人工智能、神经网络和深度学习的知识点,并提供了YOLOv7基于自己的数据集从零构建模型完整训练、推理计算的详细教程。文章还提到了郑州最低生活保障的话题。对于从事目标检测任务的人来说,YOLO是一个熟悉的模型。文章还提到了yolov4和yolov6的相关内容,以及选择模型的优化思路。 ... [详细]
  • 云原生边缘计算之KubeEdge简介及功能特点
    本文介绍了云原生边缘计算中的KubeEdge系统,该系统是一个开源系统,用于将容器化应用程序编排功能扩展到Edge的主机。它基于Kubernetes构建,并为网络应用程序提供基础架构支持。同时,KubeEdge具有离线模式、基于Kubernetes的节点、群集、应用程序和设备管理、资源优化等特点。此外,KubeEdge还支持跨平台工作,在私有、公共和混合云中都可以运行。同时,KubeEdge还提供数据管理和数据分析管道引擎的支持。最后,本文还介绍了KubeEdge系统生成证书的方法。 ... [详细]
  • 本文介绍了设计师伊振华受邀参与沈阳市智慧城市运行管理中心项目的整体设计,并以数字赋能和创新驱动高质量发展的理念,建设了集成、智慧、高效的一体化城市综合管理平台,促进了城市的数字化转型。该中心被称为当代城市的智能心脏,为沈阳市的智慧城市建设做出了重要贡献。 ... [详细]
  • 目录实现效果:实现环境实现方法一:基本思路主要代码JavaScript代码总结方法二主要代码总结方法三基本思路主要代码JavaScriptHTML总结实 ... [详细]
  • Centos7.6安装Gitlab教程及注意事项
    本文介绍了在Centos7.6系统下安装Gitlab的详细教程,并提供了一些注意事项。教程包括查看系统版本、安装必要的软件包、配置防火墙等步骤。同时,还强调了使用阿里云服务器时的特殊配置需求,以及建议至少4GB的可用RAM来运行GitLab。 ... [详细]
  • 本文介绍了在wepy中运用小顺序页面受权的计划,包含了用户点击作废后的从新受权计划。 ... [详细]
  • 本文介绍了一个React Native新手在尝试将数据发布到服务器时遇到的问题,以及他的React Native代码和服务器端代码。他使用fetch方法将数据发送到服务器,但无法在服务器端读取/获取发布的数据。 ... [详细]
  • Python已成为全球最受欢迎的编程语言之一,然而Python程序的安全运行存在一定的风险。本文介绍了Python程序安全运行需要满足的三个条件,即系统路径上的每个条目都处于安全的位置、"主脚本"所在的目录始终位于系统路径中、若python命令使用-c和-m选项,调用程序的目录也必须是安全的。同时,文章还提出了一些预防措施,如避免将下载文件夹作为当前工作目录、使用pip所在路径而不是直接使用python命令等。对于初学Python的读者来说,这些内容将有所帮助。 ... [详细]
  • 后台自动化测试与持续部署实践
    后台自动化测试与持续部署实践https:mp.weixin.qq.comslqwGUCKZM0AvEw_xh-7BDA后台自动化测试与持续部署实践原创 腾讯程序员 腾讯技术工程 2 ... [详细]
  • java布尔字段用is前缀_POJO类中布尔类型的变量都不要加is前缀详解
    前言对应阿里巴巴开发手册第一章的命名风格的第八条。【强制】POJO类中布尔类型的变量都不要加is前缀,否则部分框架解析会引起序列化错误。反例:定义为基本 ... [详细]
  • Hadoop 源码学习笔记(4)Hdfs 数据读写流程分析
    Hdfs的数据模型在对读写流程进行分析之前,我们需要先对Hdfs的数据模型有一个简单的认知。数据模型如上图所示,在NameNode中有一个唯一的FSDirectory类负责维护文件 ... [详细]
author-avatar
鄙人fisher_779
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有