篇幅可能较长,可以先收藏,方便后续观看。
RPC⼊⻔
RPC代指 远程过程调用(Remote Procedure Call)
RPC是远程过程调⽤的简称,是分布式系统中不同节点间流⾏的通信⽅式。在互联⽹时代,RPC已经 和 IPC⼀样成为⼀个不可或缺的基础构件。因此Go语⾔的标准库也提供了⼀个简单的RPC实现,我们 将以此为⼊⼝学习RPC的各种⽤法。
1. RPC版"Hello, World"
Go语⾔的RPC包的路径为net/rpc,也就是放在了net包⽬录下⾯。因此我们可以猜测该RPC包是建⽴ 在net包基础之上的。在第⼀章“Hello, World”⾰命⼀节最后,我们基于http实现了⼀个打印例⼦。下⾯ 我们尝试基于rpc实现⼀个类似的例⼦。
server/main.go
type HelloService struct {} // Hello的逻辑就是将对方发送的消息前面添加一个Hello然后返还给对方 // 由于我们是一个rpc服务,因此参数上面还是有约束: // 第一个参数是请求 // 第二个参数是响应 // 可以类比Http handler func (p *HelloService) Hello(request string, reply *string) error { *reply = "hello:" + request return nil }
其中Hello⽅法必须满⾜Go语⾔的RPC规则:
⽅法只能有两个可序列化的参数,其中第⼆个参数是指针 类型,并且返回⼀个error类型,同时必须是公开的⽅法。
然后就可以将HelloService类型的对象注册为⼀个RPC服务:
func main() { // 把我们的对象注册成一个 rpc 的 receiver // 其中rpo Register函数调用会将对象类型中所有满足RPC规则的对象方法注册为RPC函数, // 所有注册的方法会放在"HelloService"服务空间之下 rpc.RegisterName("HelloService", new(service.HelloService)) // 然后我们建立一个唯一的TCP链接,监听1234端口 listener, err := net.Listen("tcp", ":1234") if err != nil { log.Fatal("ListenTCP error:", err) } // 通过 rpc.ServeConn函数在该TCP链接上为对⽅提供RPC服务。 // 每Accept一个请求,就创建一个 goroutie 进行处理 for{ // 从监听里获取一个连接 conn, err := listener.Accept() if err != nil { log.Fatal("Accept error:", err) } // 将获取的连接交给RPC // 前面都是tcp的知识,到这个RPC就接管了 // 因此,你可以认为 rpc 帮我们封装消息到函数调用的这个逻辑, // 提升了工作效率,逻辑比较简洁,可以看看他代码 go rpc.ServeConn(conn) } }
client/main.go
下⾯是客户端请求HelloService服务的代码:
func main() { // ⾸先是通过rpc.Dial拨号RPC服务,建立连接 client, err := rpc.Dial("tcp", "localhost:1234") if err != nil { log.Fatal("dialing:", err) } var reply string // 然后通过client.Call调⽤具体的RPC⽅法 // 在调⽤client.Call时: // 第⼀个参数是⽤点号链接的RPC服务名字和⽅法名字, // 第⼆和第三个参数分别我们定义RPC⽅法的两个参数: // 第二个参数:请求参数 // 第三个参数:请求响应,必须是一个指针,由底层的rpc服务帮你赋值 err = client.Call("HelloService.Hello", "hello", &reply) if err != nil { log.Fatal(err) } fmt.Println(reply) }
由这个例⼦可以看出RPC的使⽤其实⾮常简单。
RPC的优点:
可以像使用本地函数一样使用远程服务
- 简单
- 高效
2. 更安全的RPC接⼝
在涉及RPC的应⽤中,作为开发⼈员⼀般⾄少有三种⻆⾊:⾸选是服务端实现RPC⽅法的开发⼈员, 其次是客户端调⽤RPC⽅法的⼈员,最后也是最重要的是制定服务端和客户端RPC接⼝规范的设计⼈ 员。
在前⾯的例⼦中我们为了简化将以上⼏种⻆⾊的⼯作全部放到了⼀起,虽然看似实现简单,但是 不利于后期的维护和⼯作的切割。
上面的RPC有一个显著的缺陷,就是我们可以看到Call的方法是这样一个结构:
// Call 调用指定函数,等待其完成,并返回其错误状态。 func (client *Client) Call(serviceMethod string, args interface{}, reply interface{}) error { call := <-client.Go(serviceMethod, args, reply, make(chan *Call, 1)).Done return call.Error }
至于这个serviceMethod ,你作为一个RPC-Service服务的提供端,你可能要告诉调用者,这个服务的名字叫SeriviceA,方法叫Hello,如果他不知道这个信息,他就完全不知道怎么调用,你可能还需要根据这个信息写一个文档,这是不是又回到了之前的RESTful的矛盾点上了,又要写一大堆说明,又要写一大堆代码。
然后是这个请求的参数args它是一个interface,意味着什么都可以传,调用者也不知道你要他传的到底是什么,是一个string,还是一个int,还是一个你自定义的struct,调用者无从知晓。
因此我们如果直接使用底层的这一套RPC的方式,那么对调用者是非常不友好的,那么怎么办呢?
我们可以选择去包装一下我们的RPC,让他看起来更加友好。有没有什么方式或者方法,让它变得更加规范?
我们的解决办法是,为它定义一套接口:
定义接口
service/interface.go
定义Hello Service 的接口:
package service const HelloServiceName = "HelloService" type HelloService interface { Hello(request string,reply *string) error }
相当于一种约束。
约束服务端和客户端
server/main.go
约束服务端:
type HelloServer struct{} func (p *HelloServer) Hello(request string, reply *string) error { *reply = "hello:" + request return nil } // 通过接口约束HelloService服务 var _ service.HelloService = (*HelloServer)(nil) func main() { rpc.RegisterName(service.HelloServiceName, new(HelloServer)) listener, err := net.Listen("tcp", ":1234") if err != nil { log.Fatal("ListenTCP error:", err) } for { conn, err := listener.Accept() if err != nil { log.Fatal("Accept error:", err) } go rpc.ServeConn(conn) } }
这行代码的意思是,声明一个service.HelloService类型的变量,声明后这个变量的实体,抛弃,不用内存来存,只是做一下静态检查,让编译器来帮我们把一些错误屏蔽掉,那么为什么我们要采用静态检查呢,他的核心点在(*HelloService)(nil),
如此的话,如果你在编写方法的时候,如果不满足实现接口的要求就会报错,从而必须按照规范实现接口。
client/main.go
约束客户端:
封装客户端,让其满足HelloService接口约束
type HelloServiceClient struct { *rpc.Client } func (p *HelloServiceClient) Hello(request string, reply *string) error { return p.Client.Call(service.HelloServiceName+".Hello", request, reply) } // 静态检查,同上面一样 var _ service.HelloService = (*HelloServiceClient)(nil) // 通过rpc.Dial拨号RPC服务,建立连接,并将获取连接后的客户端返回 func DialHelloService(network, address string) (*HelloServiceClient, error) { client, err := rpc.Dial(network, address) if err != nil { return nil, err } return &HelloServiceClient{client}, nil }
基于约束 后的客户端,使用起来就容易多了:
func main() { client, err := DialHelloService("tcp", "localhost:1234") if err != nil { log.Fatalln("dialing:", err) } var reply string // 在使用goland的时候就会提示 err = client.Hello("hello", &reply) fmt.Println(reply) }
现在客户端⽤户不⽤再担⼼RPC⽅法名字或参数类型不匹配等低级错误的发⽣。
3. 跨语⾔的RPC
标准库的RPC默认采⽤Go语⾔特有的gob编码,因此从其它语⾔调⽤Go语⾔实现的RPC服务将⽐较困难。
相比较与通用的JSON编码,每个语言都认识它,他就是一个跨语言的编码,但是gob不是跨语言的编码,所以我们要选择一个合适的编码。
常见的编解码:
MessagePack:高效的二进制序列化格式
JSON:文本编码(即肉眼可以看懂的)
XML:文本编码
ProtoBuf:二进制编码(即肉眼看不懂,需要按照他的规范去解码,才可以看懂)
在互联⽹的微服务时代,每个RPC以及服务的使⽤者都可能采⽤不同的编程语⾔,因此跨语⾔是 互联⽹时代RPC的⼀个⾸要条件。得益于RPC的框架设计,Go语⾔的RPC其实也是很容易实现跨语⾔⽀持的。
Go语⾔的RPC框架有两个⽐较有特⾊的设计:⼀个是RPC数据打包时可以通过插件实现⾃定义的编码 和解码;另⼀个是RPC建⽴在抽象的io.ReadWriteCloser接⼝之上的,我们可以将RPC架设在不同的通讯协议之上。
这⾥我们将尝试通过官⽅⾃带的net/rpc/jsonrpc扩展(JSON)实现⼀个跨语⾔的PPC。
JSON ON TCP
server/main.go
服务端:
... func main() { rpc.RegisterName("HelloService", new(HelloService)) listener, err := net.Listen("tcp", ":1234") if err != nil { log.Fatal("ListenTCP error:", err) } for { conn, err := listener.Accept() if err != nil { log.Fatal("Accept error:", err) } // NewServerCodec: 在 conn 上使用 JSON-RPC 返回一个新的 rpc.ServerCodec // ServeCodec: ServeCodec 类似于 ServeConn,但使用指定的编解码器解码请求和编码响应。 // Codec: 编解码器 go rpc.ServeCodec(jsonrpc.NewServerCodec(conn)) } }
代码中最⼤的变化是⽤rpc.ServeCodec函数替代了rpc.ServeConn函数,传⼊的参数是针对服务端的 json编解码器。
client/main.go
客户端:
func main() { // 建立链接 conn, err := net.Dial("tcp", "localhost:1234") if err != nil { log.Fatal("net.Dial:", err) } // 基于该链接建立针对客户端的JSON编解码器 client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn)) var reply string err = client.Call("HelloService.Hello", "world", &reply) if err != nil { log.Fatal(err) } fmt.Println(reply) }
先⼿⼯调⽤net.Dial函数建⽴TCP链接,然后基于该链接建⽴针对客户端的json编解码器。
在确保客户端可以正常调⽤RPC服务的⽅法之后,我们⽤⼀个普通的TCP服务代替Go语⾔版本的RPC 服务,这样可以查看客户端调⽤时发送的数据格式。
因此⽆论采⽤何种语⾔,只要遵循同样的json结构,以同样的流程就可以和Go语⾔编写的RPC服务进 ⾏通信。这样我们就实现了跨语⾔的RPC。
JSON ON HTTP
Go语⾔内在的RPC框架已经⽀持在Http协议上提供RPC服务。但是框架的http服务同样采⽤了内置的 gob协议,并且没有提供采⽤其它协议的接⼝,因此从其它语⾔依然⽆法访问的。
在前⾯的例⼦中,我 们已经实现了在TCP协议之上运⾏jsonrpc服务,并且通过nc命令⾏⼯具成功实现了RPC⽅法调⽤。现在我们尝试在http协议上提供jsonrpc服务。
新的RPC服务其实是⼀个类似REST规范的接⼝,接收请求并采⽤相应处理流程:
server/main.go
服务端
type HelloService struct{} func (p *HelloService) Hello(request string, reply *string) error {..} // 内嵌了io.Writer,io.ReaderCloser // 即实现了这俩接口的所有方法,也即实现了io.ReadWriterCloser接口 type RPCReadWriterCloser struct { io.Writer // Writer 是现成的w io.ReadCloser // ReadCloser就是r的Body } func NewRPCReadWriterCloserFromHTTP(w http.ResponseWriter, r *http.Request) *RPCReadWriterCloser { return &RPCReadWriterCloser{w, r.Body} } func main() { rpc.RegisterName("HelloService", new(HelloService)) http.HandleFunc("/jsonrpc", func(w http.ResponseWriter, r *http.Request) { var conn io.ReadWriteCloser = NewRPCReadWriterCloserFromHTTP(w, r) rpc.ServeRequest(jsonrpc.NewServerCodec(conn)) }) http.ListenAndServe(":1234", nil) }
RPC的服务架设在“/jsonrpc”路径,在处理函数中基于http.ResponseWriter和http.Request类型的参数 构造⼀个io.ReadWriteCloser类型的conn通道。然后基于conn构建针对服务端的json编码解码器。最后通过rpc.ServeRequest函数为每次请求处理⼀次RPC⽅法调⽤。
运行之后,我们使用postman进行测试向该链接发送⼀个json字符串:
GET: http://localhost:1234/jsonrpc { "method":"HelloService.Hello", "params":[ "hyy" ], "id":0 }
返回的结果依然是json字符串:
{ "id": 0, "result": "hello:hyy", "error": null }
这样就可以很⽅便地从不同语⾔中访问RPC服务了。
当你后端写了很多接口,但又不想写RESTful风格那样的API,去不断重复的写很多接口的时候,你可以将你所有的功能都封装到一个
案例:
通过jsonrpc 实现简单的go-web http业务
model/user.go
type User struct { UserName string `json:"user_name"` Password string `json:"password"` } func (p *User)Post(user User,reply *bool) error { fmt.Println(user) *reply = true return nil } func (p *User)Delete(UID int,reply *bool) error { fmt.Println(UID) *reply = true return nil } func (p *User)Get(UID int,reply *User) error { fmt.Println(UID) *reply = User{UserName: "111",Password: "222"} return nil } func (p *User)Update(user User,reply *bool) error { fmt.Println(user) *reply = true return nil }
server/main.go
type RPCReadWriterCloser struct { io.Writer io.ReadCloser } func NewRPCReadWriterCloserFromHTTP(w http.ResponseWriter, r *http.Request) *RPCReadWriterCloser { return &RPCReadWriterCloser{Writer: w, ReadCloser: r.Body} } func main() { rpc.RegisterName("user", new(models.User)) http.HandleFunc("/user", func(w http.ResponseWriter, r *http.Request) { var conn io.ReadWriteCloser = NewRPCReadWriterCloserFromHTTP(w, r) rpc.ServeRequest(jsonrpc.NewServerCodec(conn)) }) http.ListenAndServe(":8000", nil) }
postman测试:
GET: http://localhost:8000/user { "method":"user.Get", "params":[ 1 ], "id":0 } { "method":"user.Post", "params":[ {"user_name":"111","password":"222"} ], "id":0 } { "method":"user.Delete", "params":[ 1 ], "id":0 } { "method":"user.Update", "params":[ {"user_name":"111","password":"222"} ], "id":0 }