golang rpc

RPC(Remote Procedure Call)远程过程调用,简单的理解是一个节点请求另一个节点提供的服务,对应rpc的是本地过程调用,函数调用是最常用的本地过程调用,将本地过程调用变成远程调用会面临着各种问题。

以两数相加为例

c 复制代码
package main

import "fmt"

func add(a, b int) int {
	return a + b
}
func main() {
	fmt.Println(add(1, 2))
}

函数调用过程:

(1)将1和2压入函数的栈中

(2)进入add函数,从栈中取出1和2分别赋值给a和b

(3)执行a+b将结果压栈

(4)将栈中的结果取出打印

远程过程面临的问题

1.原本的本地函数放到另外一个服务器上去运行,但是引入了很多新问题

2.Call的id映射

我们怎么告诉远程机器我们要调用add,而不是sub或者Foo呢?在本地调用中,函数体是直接通过函数指针来指定的,我们调用add,编译器就自动帮我们调用它相应的函数指针。但是在远程调用中,函数指针是不行的,因为两个进程的地址空间是完全不一样的。所以,在RPC中,所有的函数都必须有自己的一个ID。这个ID在所有进程中都是唯一确定的。客户端在做远程过程调用时,必须附上这个ID。然后我们还需要在客户端和服务端分别维护一个(函数<-->Call ID}的对应表。两者的表不一定需要完全相同,但相同的函数对应的Call ID必须相同。当客户端需要进行远程调用时,它就查一下这个表,找出相应的Call ID,然后把它传给服务端,服务端也通过查表,来确定客户端需要调用的函数,然后执行相应函数的代码。

3.序列化和反序列化

客户端怎么把参数值传给远程的函数呢?在本地调用中,我们只需要把参数压到栈里,然后让函数自己去栈里读就行。但是在远程过程调用时,客户端跟服务端是不同的进程,不能通过内存来传递参数。甚至有时候客户端和服务端使用的都不是同一种语言(比如服务端用C++,客户端用Java或者Python)。这时候就需要客户端把参数先转成一个字节流,传给服务端后,再把字节流转成自己能读取的格式。这个过程叫序列化和反序列化。同理,从服务端返回的值也需要序列化反序列化的过程。

4.网络传输

远程调用往往用在网络上,客户端和服务端是通过网络连接的。所有的数据都需要通过网络传输,因此就需要有一个网络传输层。网络传输层需要把Call ID和序列化后的参数字节流传给服务端,然后再把序列化后的调用结果传给客户端。只要能完成这两者的,都可以作为传输层使用。因此,他所使用的协议其实是不限的,能完成传输就行,尽管大部分RPC框架都使用TCP协议,但其实UDP也可以,而RPC干脆就用了HTTP2。Java的Netty也属于这层的东西。

RPC第一个要点:数据编码协议

一般采用将数据传输到gin,gin传输到服务端,服务端负责解析数据

客户端流程

cpp 复制代码
	1.建立连接tcp/http
	2.将employee对象序列化成json字符串-序列化
	3.发送json字符串-调用成功后实际上你接受到的是一个二进制的数据
	4.等待服务器发送结果
	5将服务返回的数据解析成PrintResult对象-反序列化

服务端流程

c 复制代码
	1.监听网络接口80
	2.读取数据-二进制的json数据
	3.对数据进行反序列化Employee对象
	4.开始处理业务逻辑
	5.将处理的结果PrintReuslt发序列化成json二进制数据-序列化
	6.将数据返回

序列化和反序列化是可以选择的,不一定要采用json、xml、protobuf、msgpack

RPC第二个要点:传输协议

http协议:http1.x http2.0协议

http协议底层使用的也是tcp,http现在主流的是http1.x,这种协议有性能问题(一次性),一旦结果返回,连接就断开。我们可以直接基于tcp/udp协议去封装一层协议myhttp,没有通用型,http2.0既有http的特性也有长连接的特性(grpc就是基于http2.0的)
http协议是文本协议,http底层的传输协议是tcp。grpc基于http2.0,传输协议也是tcp

http协议具有一次性的问题:一旦对方返回了结果,连接断开,http2.0通过长连接解决了这个问题。

基于Http Server实现rpc请求

server端

cpp 复制代码
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"strconv"
)

func add(a, b int) int {
	return a + b
}

func main() {
	//get方法http://127.0.0.1:8000/add?a=1&b=2或http://127.0.0.1:8000?method=add&a=1&b=2
	//返回的格式化:json{"data":3}
	//1、callId的问题:r.URL.Path,2、数据的传输协议:url的参数传输协议,3、网络传输协议:http
	http.HandleFunc("/add", func(w http.ResponseWriter, r *http.Request) {
		err := r.ParseForm() //解析参数
		if err != nil {
			panic("error")
		}
		fmt.Println("path:", r.URL.Path)
		a, err := strconv.Atoi(r.Form["a"][0])
		if err != nil {
			panic("transform error")
		}
		b, err := strconv.Atoi(r.Form["b"][0])
		if err != nil {
			panic("transform error")
		}
		w.Header().Set("Content-Type", "application/json")
		jData, err := json.Marshal(map[string]int{
			"data": a + b,
		})
		w.Write(jData)
	})
	_ = http.ListenAndServe(":8000", nil)
}

缺点:http1.x,麻烦,性能不高

客户端:

cpp 复制代码
package main

import (
	"encoding/json"
	"fmt"
	"time"

	"github.com/kirinlabs/HttpRequest"
)

type ResponseData struct {
	Data int `json:"data"`
}

func Add(a, b int) int {
	req := HttpRequest.NewRequest()
	res, _ := req.Get(fmt.Sprintf("http://127.0.0.1:8000/%s?a=%d&b=%d", "add", a, b))
	body, _ := res.Body()
	rspData := ResponseData{}
	_ = json.Unmarshal(body, &rspData)
	return rspData.Data
}
func main() {
	fmt.Println(Add(2, 2))
}

这里遇到了一个Get "http://127.0.0.1:8000/add?a=1&b=2": context deadline exceeded (Client.Timeout exceeded while awaiting headers)的问题,主要是客户端默认网络请求时间太短

修改之后

cpp 复制代码
package main

import (
	"encoding/json"
	"fmt"
	"time"

	"github.com/kirinlabs/HttpRequest"
)

type ResponseData struct {
	Data int `json:"data"`
}

func Add(a, b int) int {
	req := HttpRequest.NewRequest()
	req.SetTimeout(10 * time.Second)
	res, _ := req.Get(fmt.Sprintf("http://127.0.0.1:8000/%s?a=%d&b=%d", "add", a, b))
	body, _ := res.Body()
	rspData := ResponseData{}
	_ = json.Unmarshal(body, &rspData)
	return rspData.Data
}
func main() {
	fmt.Println(Add(2, 2))
}

rpc开发的要素分析

RPC技术在架构设计上有四部分组成,分别是:客户端、客户端存根、服务端、服务端存根。

客户端(Client):服务调用发起方,也称为服务消费者。

客户端存根(Client Stub):该程序运行在客户端所在的计算机机器上,主要用来存储要调用的服务器的地址,另外,该程序还负责将客户端请求远端服务器程序的数据信息打包成数据包,通过网络发送给服务端Stub程序;其次,还要接收服务端Stub程序发送的调用结果数据包,并解析返回给客户端。

服务端(Server):远端的计算机机器上运行的程序,其中有客户端要调用的方法。

服务端存根(Server Stub):接收客户Stub程序通过网络发送的请求消息数据包,并调用服务端中真正的程序功能方法,完成功能调用;其次,将服务端执行调用的结果进行数据处理打包发送给客户端Stub程序。

rpc需要使用到的术语

1、动态代理技术: 上文中我们提到的Client Stub和Sever Stub程序,在具体的编码和开发实践过程中,都是使用动态代理技术自动生成的一段程序。

序列化和反序列化: 在RPC调用的过程中,我们可以看到数据需要在一台机器上传输到另外一台机器上。在互联网上,所有的数据都是以字节的形式进行传输的。而我们在编程的过程中,往往都是使用数据对象,因此想要在网络上将数据对象和相关变量进行传输,就需要对数据对象做序列化和反序列化的操作。

序列化:把对象转换为字节序列的过程称为对象的序列化,也就是编码的过程。

反序列化:把字节序列恢复为对象的过程称为对象的反序列化,也就是解码的过程。

我们常见的Json,XML等相关框架都可以对数据做序列化和反序列化编解码操作。后面我们要学习的Protobuf协议,这也是一种数据编解码的协议,在RPC框架中使用的更广泛。

简单的rpc实例

服务端

cpp 复制代码
package main

import (
	"net"
	"net/rpc"
)

type HelloService struct {
}

func (s *HelloService) Hello(request string, reply *string) error {
	//返回值是通过修改reply的值
	*reply = "hello, " + request
	return nil
}
func main() {
	//rpc快速开发体验
	//1.实例化一个server
	listener, _ := net.Listen("tcp", ":1234")
	//2.注册处理逻辑
	_ = rpc.RegisterName("HelloService", &HelloService{})
	//3.启动服务
	conn, _ := listener.Accept() //当一个新的连接进来的时候
	rpc.ServeConn(conn)
}

客户端

cpp 复制代码
package main

import (
	"encoding/json"
	"fmt"
	"net/rpc"
	"time"
)

func main() {

	//1.建立连接
	client, err := rpc.Dial("tcp", "localhost:1234")
	if err != nil {
		panic("连接失败")
	}
	var reply *string = new(string)
	err = client.Call("HelloService.Hello", "bobby", reply)
	if err != nil {
		panic("调用失败")
	}
	fmt.Println(*reply)
}

替换rpc的序列化协议为json

序列化协议为json,各种语言都可以调用服务端的内容

服务端

cpp 复制代码
package main

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

type HelloService struct {
}

func (s *HelloService) Hello(request string, reply *string) error {
	//返回值是通过修改reply的值
	*reply = "hello, " + request
	return nil
}
func main() {
	//替换rpc的序列化协议为json
	//1.实例化一个server
	listener, _ := net.Listen("tcp", ":1234")
	//2.注册处理逻辑
	_ = rpc.RegisterName("HelloService", &HelloService{})
	//3.启动服务
	conn, _ := listener.Accept() //当一个新的连接进来的时候
	rpc.ServeCodec(jsonrpc.NewServerCodec(conn))

}

客户端

c 复制代码
package main

import (
	"encoding/json"
	"fmt"
	"net"
	"net/rpc"
	"net/rpc/jsonrpc"
	"time"

	"github.com/kirinlabs/HttpRequest"
)

func main() {
	//替换rpc的序列化协议为json
	//1.建立连接
	conn, err := net.Dial("tcp", "localhost:1234")
	if err != nil {
		panic("连接失败")
	}
	var reply *string = new(string)
	client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))
	err = client.Call("HelloService.Hello", "bobby", reply)
	if err != nil {
		panic("调用失败")
	}
	fmt.Println(*reply)
}

python连接rpc,序列化协议为json

服务端

cpp 复制代码
package main

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

type HelloService struct {
}

func (s *HelloService) Hello(request string, reply *string) error {
	//返回值是通过修改reply的值
	*reply = "hello, " + request
	return nil
}
func main() {
	//替换rpc的序列化协议为json
	//1.实例化一个server
	listener, _ := net.Listen("tcp", ":1234")
	//2.注册处理逻辑
	_ = rpc.RegisterName("HelloService", &HelloService{})
	//3.启动服务
	conn, _ := listener.Accept() //当一个新的连接进来的时候
	rpc.ServeCodec(jsonrpc.NewServerCodec(conn))

}

客户端

c 复制代码
import json
import socket
request={
    "id":0,
    "params":["bobby"],
    "method":"HelloService.Hello"
}
client=socket.create_connection(("localhost",1234))
client.sendall(json.dumps(request).encode())
#获取服务器返回的数据
rsp=client.recv(4096)
rsp=json.loads(rsp.decode())
print(rsp)

替换rpc的传输协议为http

服务端

cpp 复制代码
package main

import (
	"io"
	"net/http"
	"net/rpc"
	"net/rpc/jsonrpc"
)
type HelloService struct {
}

func (s *HelloService) Hello(request string, reply *string) error {
	//返回值是通过修改reply的值
	*reply = "hello, " + request
	return nil
}
func main() {
	//替换rpc的序列化协议为http
	//2.注册处理逻辑
	_ = rpc.RegisterName("HelloService", &HelloService{})
	http.HandleFunc("/jsonrpc", func(w http.ResponseWriter, r *http.Request) {
		var conn io.ReadWriteCloser = struct {
			io.Writer
			io.ReadCloser
		}{
			ReadCloser: r.Body,
			Writer:     w,
		}
		rpc.ServeRequest(jsonrpc.NewServerCodec(conn))
	})
	http.ListenAndServe(":1234", nil)
}

客户端

c 复制代码
import requests
request={
    "id":0,
    "params":["bobby"],
    "method":"HelloService.Hello"
}
rsp=requests.post("http://localhost:1234/jsonrpc",json=request)
print(rsp.text)

客户端

cpp 复制代码
package main

import (
	"encoding/json"
	"fmt"
	"net/rpc"
	"time"

	"github.com/kirinlabs/HttpRequest"
)

func main() {
	//1.实例化一个
	client, err := rpc.Dial("tcp", "127.0.0.1:1234")
	if err != nil {
		panic("连接失败")
	}
	var reply string
	err = client.Call("HelloService.Hello", "bobby", &reply)
	if err != nil {
		// panic("调用失败")
		fmt.Println(err)
	}
	fmt.Println(reply)
}

代理封装

代理类

cpp 复制代码
package handler

import "net/rpc"

const HelloServiceName = "HelloServiceName"

type HelloServiceStub struct {
	*rpc.Client
}

func NewHelloServiceClient(protcol, address string) HelloServiceStub {
	conn, err := rpc.Dial(protcol, address)
	if err != nil {
		panic("connect error!")
	}
	return HelloServiceStub{conn}
}
func (c *HelloServiceStub) Hello(request string, reply *string) error {
	err := c.Call(HelloServiceName+".Hello", request, reply)
	if err != nil {
		return err
	}
	return nil
}

type HelloService struct {
}

func (s *HelloService) Hello(request string, reply *string) error {
	//返回值是通过修改reply的值
	*reply = "hello, " + request
	return nil
}

func RegisterHelloService() error {
	return rpc.RegisterName(HelloServiceName, &HelloService{})
}

服务端

cpp 复制代码
package main

import (
	"TEMP/handler"
	"net"
	"net/rpc"
)

func main() {
	//进一步改造rpc调用的代码
	//1.实例化一个server
	listener, _ := net.Listen("tcp", ":1234")
	//2.注册处理逻辑handler
	_ = handler.RegisterHelloService()
	//3.启动服务
	conn, _ := listener.Accept() //当一个新的连接进来的时候
	rpc.ServeConn(conn)

}

客户端

cpp 复制代码
package main

import (
	"TEMP/handler"
	"fmt"
)

func main() {
	//进一步改造rpc调用的代码
	//1.实例化一个
	client := handler.NewHelloServiceClient("tcp", "127.0.0.1:1234")
	var reply string
	err := client.Hello("bobby", &reply)
	if err != nil {
		// panic("调用失败")
		fmt.Println(err)
	}
	fmt.Println(reply)
}

解耦合

将服务端的Hello函数传递变换成接口类型

代理类

cpp 复制代码
package handler

import "net/rpc"

const HelloServiceName = "HelloServiceName"

type HelloServiceStub struct {
	*rpc.Client
}

func NewHelloServiceClient(protcol, address string) HelloServiceStub {
	conn, err := rpc.Dial(protcol, address)
	if err != nil {
		panic("connect error!")
	}
	return HelloServiceStub{conn}
}
func (c *HelloServiceStub) Hello(request string, reply *string) error {
	err := c.Call(HelloServiceName+".Hello", request, reply)
	if err != nil {
		return err
	}
	return nil
}

type NewHelloService struct {
}
type HelloServicer interface {
	Hello(request string, reply *string) error
}

func (s *NewHelloService) Hello(request string, reply *string) error {
	//返回值是通过修改reply的值
	*reply = "hello, " + request
	return nil
}

func RegisterHelloService() error {
	return rpc.RegisterName(HelloServiceName, &NewHelloService{})
}

服务端

cpp 复制代码
package main

import (
	"TEMP/handler"
	"net"
	"net/rpc"
)

func main() {
	//进一步改造rpc调用的代码
	//1.实例化一个server
	listener, _ := net.Listen("tcp", ":1234")
	//2.注册处理逻辑handler
	_ = handler.RegisterHelloService()
	//3.启动服务
	conn, _ := listener.Accept() //当一个新的连接进来的时候
	rpc.ServeConn(conn)

}

客户端

cpp 复制代码
package main

import (
	"TEMP/handler"
	"fmt"
)

func main() {
	//进一步改造rpc调用的代码
	//1.实例化一个
	client := handler.NewHelloServiceClient("tcp", "127.0.0.1:1234")
	var reply string
	err := client.Hello("bobby", &reply)
	if err != nil {
		// panic("调用失败")
		fmt.Println(err)
	}
	fmt.Println(reply)
}
相关推荐
尘浮生27 分钟前
Java项目实战II基于微信小程序的南宁周边乡村游平台(开发文档+数据库+源码)
java·开发语言·数据库·spring boot·微信小程序·小程序·maven
wangjing_05223 小时前
C语言练习.if.else语句.strstr
c语言·开发语言
Tony_long74833 小时前
Python学习——字符串操作方法
开发语言·c#
SoraLuna3 小时前
「Mac玩转仓颉内测版26」基础篇6 - 字符类型详解
开发语言·算法·macos·cangjie
出逃日志3 小时前
JS的DOM操作和事件监听综合练习 (具备三种功能的轮播图案例)
开发语言·前端·javascript
前端青山4 小时前
React事件处理机制详解
开发语言·前端·javascript·react.js
sky_feiyu4 小时前
HTTP超文本协议
网络·网络协议·web安全·http
black0moonlight5 小时前
ISAAC Gym 7. 使用箭头进行数据可视化
开发语言·python
时光の尘5 小时前
C语言菜鸟入门·关键字·int的用法
c语言·开发语言·数据结构·c++·单片机·链表·c
C++忠实粉丝5 小时前
计算机网络socket编程(6)_TCP实网络编程现 Command_server
网络·c++·网络协议·tcp/ip·计算机网络·算法