目录
一、什么是RPC
-
RPC概念:RPC(Remote Procedure Call)远程过程调用,简单的理解就是一个节点请求另一个节点提供的服务
-
本地过程调用:对应RPC的是本地过程调用,函数调用是最常见的本地过程调用
-
可能产生的问题:将本地过程调用变成远程过程调用会面临各种问题
- ①.Call的id映射
- ②.序列化和反序列化
- ③.网络传输
-
Call的id映射:我们怎么告诉远程机器我们要调用add,而不是sub或者Foo呢?在本地调用中,函数体是直接通过函数指针来指定的,我们调用add,编译器就自动帮我们调用它相应的函数指针。但是在远程调用中,函数指针是不行的,因为两个进程的地址空间是完全不一样的。所以,在RPC中,所有的函数都必须有自己的一个ID。这个ID在所有进程中都是唯一确定的。客户端在做远程过程调用时,必须附上这个ID。然后我们还需要在客户端和服务端分别维护一个 {函数 <–> Call ID} 的对应表。两者的表不一定需要完全相同,但相同的函数对应的Call ID必须相同。当客户端需要进行远程调用时,它就查一下这个表,找出相应的Call ID,然后把它传给服务端,服务端也通过查表,来确定客户端需要调用的函数,然后执行相应函数的代码。
-
序列化和反序列化:客户端怎么把参数值传给远程的函数呢?在本地调用中,我们只需要把参数压到栈里,然后让函数自己去栈里读就行。但是在远程过程调用时,客户端跟服务端是不同的进程,不能通过内存来传递参数。甚至有时候客户端和服务端使用的都不是同一种语言(比如服务端用C++,客户端用Java或者Python)。这时候就需要客户端把参数先转成一个字节流,传给服务端后,再把字节流转成自己能读取的格式。这个过程叫序列化和反序列化。同理,从服务端返回的值也需要序列化反序列化的过程。
-
网络传输:远程调用往往用在网络上,客户端和服务端是通过网络连接的。所有的数据都需要通过网络传输,因此就需要有一个网络传输层。网络传输层需要把Call ID和序列化后的参数字节流传给服务端,然后再把序列化后的调用结果传回客户端。只要能完成这两者的,都可以作为传输层使用。因此,它所使用的协议其实是不限的,能完成传输就行。尽管大部分RPC框架都使用TCP协议,但其实UDP也可以,而gRPC干脆就用了HTTP2(可以保持常连接,而http一旦返回后连接就断开了,http有性能问题)。Java的Netty也属于这层的东西。
二、http实现server和client的add
1 - http的server端
package main
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
)
func main() {
// http://127.0.0.1:8000/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) {
_ = r.ParseForm() //解析参数
fmt.Println("path: ", r.URL.Path)
a, _ := strconv.Atoi(r.Form["a"][0])
b, _ := strconv.Atoi(r.Form["b"][0])
w.Header().Set("Content-Type", "application/json")
jData, _ := json.Marshal(map[string]int{
"data": a + b,
})
_, _ = w.Write(jData)
})
_ = http.ListenAndServe(":8000", nil)
}
2 - http的client端
package main
import (
"encoding/json"
"fmt"
"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()
//fmt.Println(string(body))
rspData := ResponseData{}
_ = json.Unmarshal(body, &rspData)
return rspData.Data
}
func main() {
fmt.Println(Add(3, 2))
}
三、rpc开发四大要素
-
rpc技术在架构设计上有四分部组成:客户端、客户端存根、服务端、服务端存根
- 客户端(Client):服务调用发起方,也称为服务消费者
- 客户端存根(Client Stub):该程序运行在客户端所在的计算机机器上,主要用来存储要调用的服务器的地址,另外,该程序还负责将客户端请求远端服务器程序的数据信息打包成数据包,通过网络发送给服务端Stub程序;其次,还要接收服务端Stub程序发送的调用结果数据包,并解析返回给客户端
- 服务端(Server):远端的计算机机器上运行的程序,其中有客户端要调用的方法
- 服务端存根(Server Stub):接收客户Stub程序通过网络发送的请求消息数据包,并调用服务端中真正的程序功能方法,完成功能调用;其次,将服务端执行调用的结果进行数据处理打包发送给客户端Stub程序
-
RPC调用过程
- ①.客户端想要发起一个远程过程调用,首先通过调用本地客户端Stub程序的方式调用想要使用的功能方法名;
- ②.客户端Stub程序接收到了客户端的功能调用请求,将客户端请求调用的方法名,携带的参数等信息做序列化操作,并打包成数据包。
- ③.客户端Stub查找到远程服务器程序的IP地址,调用Socket通信协议,通过网络发送给服务端。
- ④.服务端Stub程序接收到客户端发送的数据包信息,并通过约定好的协议将数据进行反序列化,得到请求的方法名和请求参数等信息。
- ⑤.服务端Stub程序准备相关数据,调用本地Server对应的功能方法进行,并传入相应的参数,进行业务处理。
- ⑥.服务端程序根据已有业务逻辑执行调用过程,待业务执行结束,将执行结果返回给服务端Stub程序。
- ⑦.服务端Stub程序将程序调用结果按照约定的协议进行序列化,并通过网络发送回客户端Stub程序。
- ⑧.客户端Stub程序接收到服务端Stub发送的返回数据,对数据进行反序列化操作,并将调用返回的数据传递给客户端请求发起者。
- ⑨.客户端请求发起者得到调用结果,整个RPC调用过程结束。
-
动态代理技术:上面看到的Client Stub和Server Stub程序,在具体的编码和开发实践过程中,都是使用动态代理技术自动生成的一段程序
四、go的rpc实现
1 - 快速体验rpc
- server端
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() {
//1. 实例化一个server
listener, _ := net.Listen("tcp", ":1234")
//2. 注册处理逻辑 handler
_ = rpc.RegisterName("HelloService", &HelloService{})
//3. 启动服务
conn, _ := listener.Accept() //当一个新的连接进来的时候,
rpc.ServeConn(conn)
//一连串的代码大部分都是net的包好像和rpc没有关系
//不行。rpc调用中有几个问题需要解决 1. call id 2. 序列化和反序列化 编码和解码
//可以跨语言调用呢 1. go语言的rpc的序列化协议是什么(Gob) 2. 能否替换成常见的序列化
}
- client
package main
import (
"fmt"
"net/rpc"
)
func main() {
//1. 建立连接
client, err := rpc.Dial("tcp", "localhost:1234")
if err != nil {
return
}
var reply string //string有默认值
err = client.Call("HelloService.Hello", "bobby", &reply)
if err != nil {
return
}
fmt.Println(reply)
}
2 - rpc实现json序列化协议
- server:只要发送json数据给server,server都可以解析
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() {
//1. 实例化一个server
listener, _ := net.Listen("tcp", ":1234")
//2. 注册处理逻辑 handler
_ = rpc.RegisterName("HelloService", &HelloService{})
//3. 启动服务
for {
conn, _ := listener.Accept() //当一个新的连接进来的时候,
go rpc.ServeCodec(jsonrpc.NewServerCodec(conn))
}
}
- client
package main
import (
"fmt"
"net"
"net/rpc"
"net/rpc/jsonrpc"
)
func main() {
//1. 建立连接
conn, err := net.Dial("tcp", "localhost:1234")
if err != nil {
return
}
var reply string //string有默认值
client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))
err = client.Call("HelloService.Hello", "bobby", &reply)
if err != nil {
return
}
fmt.Println(reply)
}
3 - rpc实现http传输协议
- server
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() {
//1. 实例化一个server
_ = 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)
}
五、rpc调用改造(grpc模拟)
1 - serviceName统一和名称冲突的问题
- 问题分析
- server端和client端如何统一serviceName
- 多个server的包中serviceName同名的问题
- ** 解耦方案**:新建handler/handler.go文件内容如下
package handler
// 名称冲突的问题
const HelloServiceName = "handler/HelloService"
- server:
_ = rpc.RegisterName(handler.HelloServiceName, &HelloService{})
- client:
err = client.Call(handler.HelloServiceName+".Hello", "imooc", &reply)
2 - 屏蔽HelloServiceName和Hello函数名称
- 项目结构
- handle.go
package handler
// 名称冲突的问题
const HelloServiceName = "handler/HelloService"
// 我们关心的是NewHelloService这个名字呢 还是这个结构体中的方法
type NewHelloService struct{}
func (s *NewHelloService) Hello(request string, reply *string) error {
//返回值是通过修改reply的值
*reply = "hello, " + request
return nil
}
- server代理:server_proxy.go
package server_proxy
import (
"net/rpc"
"test_project/handler"
)
type HelloServicer interface {
Hello(request string, reply *string) error
}
//如果做到解耦 - 我们关心的是函数 鸭子类型
func RegisterHelloService(srv HelloServicer) error {
return rpc.RegisterName(handler.HelloServiceName, srv)
}
- server.go
package main
import (
"net"
"net/rpc"
"test_project/handler"
"test_project/server_proxy"
)
func main() {
//1. 实例化一个server
listener, _ := net.Listen("tcp", ":1234")
//2. 注册处理逻辑 handler
_ = server_proxy.RegisterHelloService(&handler.NewHelloService{})
//3. 启动服务
for {
conn, _ := listener.Accept() //当一个新的连接进来的时候,
go rpc.ServeConn(conn)
}
}
- client代理:client_proxy.go
package client_proxy
import (
"net/rpc"
"test_project/handler"
)
type HelloServiceStub struct {
*rpc.Client
}
//在go语言中没有类、对象 就意味着没有初始化方法
func NewHelloServiceClient(protocol, address string) HelloServiceStub {
conn, err := rpc.Dial(protocol, address)
if err != nil {
return HelloServiceStub{}
}
return HelloServiceStub{conn}
}
func (c *HelloServiceStub) Hello(request string, reply *string) error {
err := c.Call(handler.HelloServiceName+".Hello", request, reply)
if err != nil {
return err
}
return nil
}
- client.go
package main
import (
"fmt"
"test_project/client_proxy"
)
func main() {
//1. 建立连接
client := client_proxy.NewHelloServiceClient("tcp", "localhost:1234")
//1. 只想写业务逻辑 不想关注每个函数的名称
// 客户端部分
var reply string //string有默认值
err := client.Hello("bobby", &reply)
if err != nil {
return
}
fmt.Println(reply)
//1. 这些概念在grpc中都有对应
//2. 发自灵魂的拷问: server_proxy 和 client_proxy能否自动生成啊 为多种语言生成
//3. 都能满足 这个就是protobuf + grpc
}