golang学习3:标准库中的rpc

RPC

RPC是Remote Procedure Call Protocol三个单词首字母的缩写,翻译过来叫远程过程调用协议。
故明思议,也就是在本地调用远程的函数,这里的远程是相对于本地函数调用来讲的。
既然是远程了,一般就需要使用网络通信,客户端把要调用的方法和参数传过去,传过去之前这些参数要进行序列化从而转化为适合网络传输的格式,而服务端接收后需要进行相反的解码动作,也就是反序列化,从而得到程序使用的格式。处理后,返回的结果同样先序列化再反序列化传回给客户端,从而完成一次交互。

逻辑上讲于本地调用无异,但是因为跨网络的,增强了程序的复用性,降低了客户端和服务端的耦合程度。特别对于分布式系统,更有利于不同组件进行通信。

RPC框架

有了RPC的基本功能和概念,那么我们大概就知道怎么实现一个RPC框架了。起码要分成服务端和客户端吧,序列化和反序列化协议总要有吧(可能有多种),传输协议得确定吧(TCP、UDP还是http)。此外怎么实现高并发低延时也是一个问题, 要是想完善的话服务治理总得有吧,多开发语言支不支持等等。

比如谷歌有自己的RPC协议gRPC, 序列化协议使用protobuf,传输层使用http2, 支持多种开发语言的客户和服务端。facebook有Thrift, 阿里巴巴有dubbo, 百度也有子的brpc框架, 搜狗有自己的srpc,腾讯有tars。

这些RPC框架有的支持多开发语言,有的支持单开发语言,有的仅仅支持一对一的调用,有的支持服务发现治理等复杂功能,不一而足。

golang的RPC

golang的自带库里有一个rpc包和一个jsonrpc包,这都是golang自带的RPC调用方式,使用起来非常方便。
后面我再介绍下gRPC,毕竟这是云原生的标准调用方式。

其实还有很多支持多语言的RPC框架支持go语言的,我也没时间一一介绍了。

package net/rpc

服务端注册一个对象,使它作为一个服务被暴露,服务的名字是该对象的类型名。注册之后,对象的导出方法就可以被远程访问。服务端可以注册多个不同类型的对象(服务),但注册具有相同类型的多个对象是错误的。

只有满足如下标准的方法才能用于远程访问,其余方法会被忽略:

方法是导出的
方法有两个参数,都是导出类型或内建类型
方法的第二个参数是指针
方法只有一个error接口类型的返回值

事实上,方法必须看起来像这样:

func (t *T) MethodName(argType T1, replyType *T2) error

其中T、T1和T2都能被encoding/gob包序列化。这些限制即使使用不同的编解码器也适用。

方法的第一个参数代表调用者提供的参数;第二个参数代表返回给调用者的参数。方法的返回值,如果非nil,将被作为字符串回传,在客户端看来就和errors.New创建的一样。如果返回了错误,回复的参数将不会被发送给客户端。

服务端(基于http)

这里实现一个简单的服务端,提供的服务就是做乘法和除法。这里通过http对外提供服务。可以看到这里监听的是tcp:1234端口。

package main

import (
	"log"
	"net"
	"errors"
	"net/rpc"
	"net/http"
	"time"
)

type Args struct {
	A, B int
}
type Quotient struct {
	Quo, Rem int
}
type Arith int
func (t *Arith) Multiply(args *Args, reply *int) error {
	*reply = args.A * args.B
	time.Sleep(3 * time.Second)
	return nil
}
func (t *Arith) Divide(args *Args, quo *Quotient) error {
	if args.B == 0 {
		return errors.New("divide by zero")
	}
	quo.Quo = args.A / args.B
	quo.Rem = args.A % args.B
	time.Sleep(3 * time.Second)
	return nil
}

func main() {
	arith := new(Arith)
	rpc.Register(arith)
	rpc.HandleHTTP()
	l, e := net.Listen("tcp", ":1234") //监听tcp:1234端口, 如果有多个网卡,冒号前也可以写IP
	if e != nil {
		log.Fatal("listen error:", e)
	}
	http.Serve(l, nil)
}

客户端(基于http)

服务端基于http实现,客户端自然也要基于http实现。访问端口也要对应。

客户端访问有两种,一种是同步,一种是异步。异步的时候返回一个channel, 当返回结果的时候里面才有值。

package main

import (
	"log"
	"fmt"
	"net/rpc"
)

type Args struct {
	A, B int
}

type Quotient struct {
	Quo, Rem int
}

func main() {
	client, err := rpc.DialHTTP("tcp", ":1234")
	if err != nil {
		log.Fatal("dialing:", err)
	}

	// 同步调用, 乘法
	args := &Args{6,3}
	var reply int
	err = client.Call("Arith.Multiply", args, &reply)
	if err != nil {
		log.Fatal("arith error:", err)
	}
	fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)

	// 异步调用, 除法
	quotient := new(Quotient)
	divCall := client.Go("Arith.Divide", args, quotient, nil)
	replyCall, OK := <-divCall.Done	// divCall.Done是一个channel,返回值是一个Call指针,并与divCall相等,结束前一直阻塞
	if divCall == replyCall && OK {
		fmt.Printf("Divide: %d/%d=%d, %d%%%d=%d\n", args.A, args.B, quotient.Quo, args.A, args.B, quotient.Rem)
	} else {
		log.Fatal("Divide error")
	}
}

调用时输出为:

$ go run rpc_clinet.go
Arith: 6*3=18
Divide: 6/3=2, 6%3=0

服务端(基于tcp)

除了用http作为传输协议,还可以用tcp。相对上面的代码,改动可以说是相当少,我就只贴main的函数部分了。

func main() {
	arith := new(Arith)
	rpc.Register(arith)

	l, e := net.Listen("tcp", ":1234")
	if e != nil {
		log.Fatal("listen error:", e)
	}
	//这个accpt会一直阻塞,每个新的连接都会建立一个新的协程去处理
	rpc.Accept(l)
}

客户端(基于tcp)

改为tcp,客户端的修改更简单,改一行就行:DialHTTP=>Dial

func main() {
    //从http改为tcp,只要改这一行就行:DialHTTP=>Dial
	client, err := rpc.Dial("tcp", ":1234")
	if err != nil {
		log.Fatal("dialing:", err)
	}

	// 同步调用, 乘法
	args := &Args{6,3}
	var reply int
	err = client.Call("Arith.Multiply", args, &reply)
	if err != nil {
		log.Fatal("arith error:", err)
	}
	fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)

	// 异步调用, 除法
	quotient := new(Quotient)
	divCall := client.Go("Arith.Divide", args, quotient, nil)
	replyCall, OK := <-divCall.Done	// divCall.Done是一个channel,返回值是一个Call指针,并与divCall相等,结束前一直阻塞
	if divCall == replyCall && OK {
		fmt.Printf("Divide: %d/%d=%d, %d%%%d=%d\n", args.A, args.B, quotient.Quo, args.A, args.B, quotient.Rem)
	} else {
		log.Fatal("Divide error")
	}
}

package net/rpc/jsonrpc

其实上面的net/rpc也支持自定义序列化、反序列化方法,不过默认的方式是golang语言库自带的GOB, 那么也很容易添加对json的支持。其实golang已经自带了相关实现,就在net/rpc/jsonrpc里,当然是基于net/rpc实现的。

JSON-RPC是一个无状态且轻量级的远程过程调用(RPC)协议。它很简单,只有请求和通知两种。而且JSON-RPC也有两个版本, V1和V2, V2主要的变动就是增加了版本号字段,并且通知的时候不再传“id”字段,出错的时候可以用“result”或者“error”字段。

一次典型的JSON-RPC调用的来回报文如下:

--> {"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": 1}
<-- {"jsonrpc": "2.0", "result": 19, "id": 1}

RPC批量调用:

--> [
    {"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"},
    {"jsonrpc": "2.0", "method": "notify_hello", "params": [7]},
    {"jsonrpc": "2.0", "method": "subtract", "params": [42,23], "id": "2"},
    {"foo": "boo"},
    {"jsonrpc": "2.0", "method": "foo.get", "params": {"name": "myself"}, "id": "5"},
    {"jsonrpc": "2.0", "method": "get_data", "id": "9"}
    ]
<-- [
    {"jsonrpc": "2.0", "result": 7, "id": "1"},
    {"jsonrpc": "2.0", "result": 19, "id": "2"},
    {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null},
    {"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": "5"},
    {"jsonrpc": "2.0", "result": ["hello", 5], "id": "9"}
    ]

通知:

--> {"jsonrpc": "2.0", "method": "update", "params": [1,2,3,4,5]}
--> {"jsonrpc": "2.0", "method": "foobar"}

实际上net/rpc/jsonrpc实现的是JSON-RPC V1。

jsonrpc服务端(基于TCP)

注意net/rpc/jsonrpc只支持tcp的调用的。相对上面的两个版本,这次的改动都在main函数里。

package main

import (
	"log"
	"net"
	"errors"
	"net/rpc"
	"net/rpc/jsonrpc"
	"time"
)

type Args struct {
	A, B int
}
type Quotient struct {
	Quo, Rem int
}
type Arith int
func (t *Arith) Multiply(args *Args, reply *int) error {
	*reply = args.A * args.B
	time.Sleep(3 * time.Second)
	return nil
}
func (t *Arith) Divide(args *Args, quo *Quotient) error {
	if args.B == 0 {
		return errors.New("divide by zero")
	}
	quo.Quo = args.A / args.B
	quo.Rem = args.A % args.B
	time.Sleep(3 * time.Second)
	return nil
}

func main() {
	arith := new(Arith)
	rpc.Register(arith)

	l, e := net.Listen("tcp", ":1234")
	if e != nil {
		log.Fatal("listen error:", e)
	}

	for {
		//监听连接
		conn, err := l.Accept()
		if err != nil {
			log.Fatal("Accept error:", err)
			continue
		}
		//另起一个协程处理新连接,主循环继续监听
		go jsonrpc.ServeConn(conn)
	}
}

jsonrpc客户端(基于TCP)

相对上一个client的版本,只有一行需要变动:rcp=>jsonrpc

package main

import (
	"log"
	"fmt"
	"net/rpc/jsonrpc"
)

type Args struct {
	A, B int
}

type Quotient struct {
	Quo, Rem int
}

func main() {
    //相对上一个client的版本,仍然只有这一行需要变动:rcp=>jsonrpc
	client, err := jsonrpc.Dial("tcp", ":1234")
	if err != nil {
		log.Fatal("dialing:", err)
	}

	// 同步调用, 乘法
	args := &Args{6,3}
	var reply int
	err = client.Call("Arith.Multiply", args, &reply)
	if err != nil {
		log.Fatal("arith error:", err)
	}
	fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)

	// 异步调用, 除法
	quotient := new(Quotient)
	divCall := client.Go("Arith.Divide", args, quotient, nil)
	replyCall, OK := <-divCall.Done	// divCall.Done是一个channel,返回值是一个Call指针,并与divCall相等,结束前一直阻塞
	if divCall == replyCall && OK {
		fmt.Printf("Divide: %d/%d=%d, %d%%%d=%d\n", args.A, args.B, quotient.Quo, args.A, args.B, quotient.Rem)
	} else {
		log.Fatal("Divide error")
	}
}

总结

相对其它一些语言(比如c/c++), golang针对网络环境下的编程有较好的支持,除了具有特色的协程,较为完善的官方库也是一方面。golang自带的rpc使用的GOB编码虽然其它语言没有官方支持,但是自己实现起开并非不可能,目前网上也有各种开源方法,从而达到跨语言调用。golang自带的jsonrpc使用tcp传输,跨语言调用起来也更简单。

从网上别人的反馈来看,golang自带的rpc效率还是比较高的,在点对点的简单场景下,也么有必要去用复杂的框架。

国内还有一个golang的RCP框架rpcx也是很不错的,支持各种服务治理,功能类似dubbo-go, 但是效率很高,据说两倍于gRPC,有兴趣的可以去学习。dubbo-go主要是想打通jsva和go,如果没有和java通信的需求,学习这个rpcx也蛮好的。

参考资料

golang标准库文档

Go语言微服务框架实战:1.RPC简介及原理介绍

JSON-RPC官网

protobuf初识

gRPC快速入门

JSON-RPC官网

(译) JSON-RPC 2.0 规范(中文版)

RPCX:最好的Go语言的RPC服务治理框架,快、易用却功能强大。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值