辽宁省建设教育协会网站,怎么给QQ名片做网站,广州 网站开发,简单设计网站文章目录 RPC原理与Go RPC什么是RPC本地调用RPC调用HTTP调用RESTful API net/rpc基础RPC示例基于TCP协议的RPC使用JSON协议的RPCPython调用RPC RPC原理 RPC原理与Go RPC
什么是RPC
RPC#xff08;Remote Procedure Call#xff09;#xff0c;即远程过程调用。它允许像调用… 文章目录 RPC原理与Go RPC什么是RPC本地调用RPC调用HTTP调用RESTful API net/rpc基础RPC示例基于TCP协议的RPC使用JSON协议的RPCPython调用RPC RPC原理 RPC原理与Go RPC
什么是RPC
RPCRemote Procedure Call即远程过程调用。它允许像调用本地服务一样调用远程服务。
RPC是一种服务器-客户端Client/Server模式经典实现是一个通过发送请求-接受回应进行信息交互的系统。
首先与RPC远程过程调用相对应的是本地调用。
本地调用
package mainimport fmtfunc add(x, y int) int {z : x yreturn z
}func main() {// 调用本地函数adda : 10b : 20ret : add(a, b)fmt.Println(ret)
}将上述程序编译成二进制文件——app1后运行会输出结果30。
在app1程序中本地调用add函数的执行流程可以理解为以下四个步骤。
将a和b的值压栈通过函数指针找到add函数进入函数取出栈中的值10和20将其赋予x和y计算x*y并将结果存在z将z的值压栈然后从 add函数返回从栈中取出z返回值并赋值给ret
RPC调用
本地过程调用发生在同一进程中——定义add函数的代码和调用add函数的代码共享同一个内存空间所以调用能够正常执行。
但是我们无法直接在另一个程序——app2中调用add函数因为它们是两个程序——内存空间是相互隔离的。app1和app2可能部署在同一台服务器上也可能部署在互联网的不同服务器上。
RPC就是为了解决类似远程、跨内存空间、的函数/方法调用的。要实现RPC就需要解决以下三个问题。
如何确定要执行的函数 在本地调用中函数主体通过函数指针函数指定然后调用 add 函数编译器通过函数指针函数自动确定 add 函数在内存中的位置。但是在 RPC 中调用不能通过函数指针完成因为它们的内存地址可能完全不同。因此调用方和被调用方都需要维护一个{ function - ID }映射表以确保调用正确的函数。如何表达参数 本地过程调用中传递的参数是通过堆栈内存结构实现的但 RPC 不能直接使用内存传递参数因此参数或返回值需要在传输期间序列化并转换成字节流反之亦然。如何进行网络传输 函数的调用方和被调用方通常是通过网络连接的也就是说function ID 和序列化字节流需要通过网络传输因此只要能够完成传输调用方和被调用方就不受某个网络协议的限制。例如一些 RPC 框架使用 TCP 协议一些使用 HTTP。
以往实现跨服务调用的时候我们会采用RESTful API的方式被调用方会对外提供一个HTTP接口调用方按要求发起HTTP请求并接收API接口返回的响应数据。下面的示例是将add函数包装成一个RESTful API。
HTTP调用RESTful API
首先我们编写一个基于HTTP的server服务它将接收其他程序发来的HTTP请求执行特定的程序并将结果返回。
// server/main.gopackage mainimport (encoding/jsonlognet/http
)type addParam struct {X int json:xY int json:y
}type addResult struct {Code int json:codeData int json:data
}func add(x, y int) int {return x y
}func addHandler(w http.ResponseWriter, r *http.Request) {// Check for the HTTP method to be POSTif r.Method ! http.MethodPost {http.Error(w, Method not allowed, http.StatusMethodNotAllowed)return}// Parse the request bodyvar param addParamerr : json.NewDecoder(r.Body).Decode(param)if err ! nil {http.Error(w, Invalid request body, http.StatusBadRequest)return}// Perform the business logicret : add(param.X, param.Y)// Return the responseresp : addResult{Code: 0, Data: ret}w.Header().Set(Content-Type, application/json)err json.NewEncoder(w).Encode(resp)if err ! nil {log.Println(Error encoding response:, err)}
}func main() {http.HandleFunc(/add, addHandler)log.Fatal(http.ListenAndServe(:9090, nil))
}我们编写一个客户端来请求上述HTTP服务传递x和y两个整数等待返回结果。
// client/main.gopackage mainimport (bytesencoding/jsonfmtionet/http
)type Param struct {X int json:xY int json:y
}type Result struct {Code int json:codeData int json:data
}func main() {// 通过HTTP请求调用其他服务器上的add服务url : http://127.0.0.1:9090/addparam : Param{X: 10,Y: 20,}paramBytes, err : json.Marshal(param)if err ! nil {fmt.Println(Error marshalling request body:, err)return}resp, err : http.Post(url, application/json, bytes.NewReader(paramBytes))if err ! nil {fmt.Println(Error making HTTP POST request:, err)return}defer resp.Body.Close()respBytes, err : io.ReadAll(resp.Body)if err ! nil {fmt.Println(Error reading response body:, err)return}var respData Resulterr json.Unmarshal(respBytes, respData)if err ! nil {fmt.Println(Error unmarshalling response body:, err)return}fmt.Println(respData.Data) // 30
}这种模式是我们目前比较常见的跨服务或跨语言之间基于RESTful API的服务调用模式。 既然使用API调用也能实现类似远程调用的目的为什么还要用RPC呢
使用 RPC 的目的是让我们调用远程方法像调用本地方法一样无差别。并且基于RESTful API通常是基于HTTP协议传输数据采用JSON等文本协议相较于RPC 直接使用TCP协议传输数据多采用二进制协议来说RPC通常相比RESTful API性能会更好。
RESTful API多用于前后端之间的数据传输而目前微服务架构下各个微服务之间多采用RPC调用。
net/rpc
基础RPC示例
Go语言的 rpc 包提供对通过网络或其他 i/o 连接导出的对象方法的访问服务器注册一个对象并把它作为服务对外可见服务名称就是类型名称。注册后对象的导出方法将支持远程访问。服务器可以注册不同类型的多个对象(服务) 但是不支持注册同一类型的多个对象。
在下面的代码中我们定义一个ServiceA类型并为其定义了一个可导出的Add方法。并将ServiceA类型注册为一个服务其Add方法就支持RPC调用了。
// rpc demo/service.gopackage maintype Args struct {X, Y int
}// ServiceA 自定义一个结构体类型
type ServiceA struct{}// Add 为ServiceA类型增加一个可导出的Add方法
func (s *ServiceA) Add(args *Args, reply *int) error {*reply args.X args.Yreturn nil
}func main() {service : new(ServiceA)rpc.Register(service) // 注册RPC服务rpc.HandleHTTP() // 基于HTTP协议l, e : net.Listen(tcp, :9091)if e ! nil {log.Fatal(listen error:, e)}http.Serve(l, nil)
}此时client 端便能看到一个拥有“Add”方法的“ServiceA”服务想要调用这个服务需要使用下面的代码先连接到server端再执行远程调用。
// rpc demo/client.gopackage mainimport (fmtlognet/rpc
)type ClientArgs struct {X, Y int
}func main() {// 建立HTTP连接client, err : rpc.DialHTTP(tcp, 127.0.0.1:9091)if err ! nil {log.Fatal(dialing:, err)}// 同步调用args : ClientArgs{10, 20}var reply interr client.Call(ServiceA.Add, args, reply)if err ! nil {log.Fatal(ServiceA.Add error:, err)}fmt.Printf(ServiceA.Add: %d%d%d\n, args.X, args.Y, reply)// 异步调用var reply2 intdivCall : client.Go(ServiceA.Add, args, reply2, nil)replyCall : -divCall.Done // 接收调用结果fmt.Println(replyCall.Error)fmt.Println(reply2)
}a. 同步调用
client.Call(ServiceA.Add, args, reply): 该行代码表示使用client连接对象对名为ServiceA.Add的远程方法进行同步调用传递了args作为参数并将结果存储在reply中。如果调用出现错误则通过log.Fatal输出错误信息。
b. 异步调用
client.Go(ServiceA.Add, args, reply2, nil): 该行代码表示使用client连接对象对名为ServiceA.Add的远程方法进行异步调用传递了args作为参数并将结果存储在reply2中。此处使用了Go方法该方法会立即返回一个rpc.Call对象它代表了异步调用的状态。-divCall.Done: 通过使用-操作符我们等待异步调用完成这里divCall.Done是一个通道它会在异步调用结束时收到一个通知。replyCall.Error: 获取异步调用结果的错误信息如果有的话。reply2: 获取异步调用的返回值。
执行上述两个程序查看 RPC 调用的结果。
会看到如下输出结果。
ServiceA.Add: 102030
nil
30基于TCP协议的RPC
当然 rpc 包也支持直接使用 TCP 协议而不使用HTTP协议。
server 端代码修改如下。
// rpc demo/service.gopackage mainimport (lognetnet/rpc
)type Args struct {X, Y int
}// ServiceA 自定义一个结构体类型
type ServiceA struct{}// Add 为ServiceA类型增加一个可导出的Add方法
func (s *ServiceA) Add(args *Args, reply *int) error {*reply args.X args.Yreturn nil
}func main() {service : new(ServiceA)rpc.Register(service) // 注册RPC服务l, e : net.Listen(tcp, :9091)if e ! nil {log.Fatal(listen error:, e)}for {conn, _ : l.Accept()rpc.ServeConn(conn)}
}client 端代码修改如下。
// rpc demo/client.gopackage mainimport (fmtlognet/rpc
)type ClientArgs struct {X, Y int
}func main() {// 建立TCP连接client, err : rpc.Dial(tcp, 127.0.0.1:9091)if err ! nil {log.Fatal(dialing:, err)}// 同步调用args : ClientArgs{10, 20}var reply interr client.Call(ServiceA.Add, args, reply)if err ! nil {log.Fatal(ServiceA.Add error:, err)}fmt.Printf(ServiceA.Add: %d%d%d\n, args.X, args.Y, reply)// 异步调用var reply2 intdivCall : client.Go(ServiceA.Add, args, reply2, nil)replyCall : -divCall.Done // 接收调用结果fmt.Println(replyCall.Error)fmt.Println(reply2)
}使用JSON协议的RPC
rpc 包默认使用的是 gob 协议对传输数据进行序列化/反序列化比较有局限性。下面的代码将尝试使用 JSON 协议对传输数据进行序列化与反序列化。
server 端代码修改如下。
// rpc demo/service.gopackage mainimport (lognetnet/rpcnet/rpc/jsonrpc
)type Args struct {X, Y int
}// ServiceA 自定义一个结构体类型
type ServiceA struct{}// Add 为ServiceA类型增加一个可导出的Add方法
func (s *ServiceA) Add(args *Args, reply *int) error {*reply args.X args.Yreturn nil
}func main() {service : new(ServiceA)rpc.Register(service) // 注册RPC服务l, e : net.Listen(tcp, :9091)if e ! nil {log.Fatal(listen error:, e)}for {conn, _ : l.Accept()// 使用JSON协议rpc.ServeCodec(jsonrpc.NewServerCodec(conn))}
}client 端代码修改如下。
// rpc demo/client.gopackage mainimport (fmtlognetnet/rpcnet/rpc/jsonrpc
)type ClientArgs struct {X, Y int
}func main() {// 建立TCP连接conn, err : net.Dial(tcp, 127.0.0.1:9091)if err ! nil {log.Fatal(dialing:, err)}// 使用JSON协议client : rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))// 同步调用args : ClientArgs{10, 20}var reply interr client.Call(ServiceA.Add, args, reply)if err ! nil {log.Fatal(ServiceA.Add error:, err)}fmt.Printf(ServiceA.Add: %d%d%d\n, args.X, args.Y, reply)// 异步调用var reply2 intdivCall : client.Go(ServiceA.Add, args, reply2, nil)replyCall : -divCall.Done // 接收调用结果fmt.Println(replyCall.Error)fmt.Println(reply2)
}Python调用RPC
下面的代码演示了如何使用 python client 远程调用上面 Go server中 serviceA的Add方法。
import socket
import jsonrequest {id: 0,params: [{x:10, y:20}], # 参数要对应上Args结构体method: ServiceA.Add
}client socket.create_connection((127.0.0.1, 9091),5)
client.sendall(json.dumps(request).encode())rsp client.recv(1024)
rsp json.loads(rsp.decode())
print(rsp)输出结果
{id: 0, result: 30, error: None}RPC原理
RPC 让远程调用就像本地调用一样其调用过程可拆解为以下步骤。 ① 服务调用方client以本地调用方式调用服务
② client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体
③ client stub找到服务地址并将消息发送到服务端
④ server 端接收到消息
⑤ server stub收到消息后进行解码
⑥ server stub根据解码结果调用本地的服务
⑦ 本地服务执行并将结果返回给server stub
⑧ server stub将返回结果打包成能够进行网络传输的消息体
⑨ 按地址将消息发送至调用方
⑩ client 端接收到消息
⑪ client stub收到消息并进行解码
⑫ 调用方得到最终结果。
使用RPC框架的目标是只需要关心第1步和最后1步中间的其他步骤统统封装起来让使用者无需关心。例如社区中各式RPC框架grpc、thrift等就是为了让RPC调用更方便。
Referenceshttps://www.liwenzhou.com/posts/Go/golang-menu/