什么是RPC
rpc是远程过程调用(Remote Procedure Call)的缩写,简单的来讲就是调用远处的一个节点所提供的服务,至于这个远处有多远呢?可能是一个文件内,不同的函数,也可能是同一个机器上的另一个进程的函数,也可能是远在另外省份的机器所提供的函数。
rpc 入门
rpc是分布式微服务系统中不同节点之间流行的通信方式,在现今互联网时代,rpc已经是一个不可缺少的基础组件,因此GO语言的标准库也提供了简单的RPC实现,我们将以此为入口学习RPC的各种用法。
rpc包提供了通过网络或其他I/O连接对一个对象的导出方法的访问。服务端注册一个对象,使它作为一个服务被暴露,服务的名字是该对象的类型名。注册之后,对象的导出方法就可以被远程访问。服务端可以注册多个不同类型的对象(服务),但注册具有相同类型的多个对象是错误的。
只有满足如下标准的方法才能用于远程访问,其余方法会被忽略:
- 方法是导出的
- 方法有两个参数,都是导出类型或内建类型
- 方法的第二个参数是指针
- 方法只有一个error接口类型的返回值
事实上,方法必须看起来像这样:
func (t *T) MethodName(argType T1, replyType *T2) error
项目实操
我们在服务端提供了一个Hello的服务,有着一个Print的方法。
服务端
package main
import (
"log"
"net"
"net/rpc"
)
type Hello struct {
}
//方法需要满足前面所描述的约束条件
func (h *Hello) Print(from string, reply *string) error {
*reply = from + ":Hello World"
return nil
}
func main() {
rpc.Register(new(Hello)) //注册服务
//Register 会将独享中满足约束条件的方法全部注册为RPC函数
//所有注册的方法会放到服务名为对象名的服务之下(RegisterName 可以自定义服务名称)
listen, err := net.Listen("tcp", ":1234") //监听服务
if err != nil {
log.Fatal("listen errpr", err)
}
for {
conn, _ := listen.Accept() //同意连接
rpc.ServeConn(conn) //rpc服务连接,为对方提供rpc服务
}
}
客户端
package main
import (
"log"
"net/rpc"
"fmt"
)
func main() {
client, err := rpc.Dial("tcp", "localhost:1234") //rpc连接
if err!=nil{
log.Fatal("dial err",err)
}
var reply string
//client.Call的第一个参数为 服务名.方法,后两个参数以此为方法所对应的参数
//此函数将返回一个error,这个error也是调用方法中所返回的。
client.Call("Hello.Print","piter",&reply) //调用远程RPC注册对象所提供的服务
fmt.Println(reply) //打印返回的值
}
运行服务端之后,在运行客户端,得到如下结果
piter:Hello World
通过服务端与客户端的案例,我们可以看出go的rpc实现是非常简单的。
更安全的rpc接口
在我们正常的RPC开发中,我们会涉及到至少3中的角色,客户端调用人员、服务端开发人员、rpc接口设计人员。在之前的代码中,我们将3中角色融入y到了一起,虽然代码看着很简单,但是实际开发中维护是非常困难的,也不利于工作分割。 我们将规范划分为3部门
- 要明确服务空间名称,防止在服务过多导致服务名称重复
- 服务端接口规定此服务提供的具体的方法
- 服务端的统一注册以及客户端的统一调用
服务端
我们在项目中新建一个文件 servers/common.go 用来作为公共文件
//此处是用过项目所在包的路径所进行命名可以有效区分服务
package servers
const ServerName = "server/servers/hello"
type HelloInterface interface {
Print(from string, reply *string) error
}
再新建一个文件 servers/server.go 用来作为服务的方法的打实现。
package servers
import "net/rpc"
//统一注册RPC服务,通过接口方法的传参注册,可有效检测服务方法是否实现
func RegisterHelloServer(hs HelloInterface) error {
return rpc.RegisterName(ServerName, hs)
}
//rpc 服务方法的具体实现
type Hello struct {
}
func (h *Hello) Print(from string, reply *string) error {
*reply = from + ":Hello World"
return nil
}
main.go 中修改如下
package main
import (
"log"
"net"
"net/rpc"
"server/servers"
)
func main() {
servers.RegisterHelloServer(new(servers.Hello))
listen, err := net.Listen("tcp", ":1234")
if err != nil {
log.Fatal("listen errpr", err)
}
for {
conn, _ := listen.Accept()
rpc.ServeConn(conn)
}
}
这时候我们将客户端的服务空间修改一下就可以继续调用的,但是这不符合我们的规范约束。
客户端
按照服务的规范进行开发,我们将服务端的 servers/common.go 直接复制到客户端的对应的文件中,作为公共文件。
创建一个client.go ,代码如下:
package servers
import "net/rpc"
// 统一连接rpc服务
func RegisterHelloClient(net, prot string) (*Hello, error) {
client, err := rpc.Dial(net, "localhost:1234")
if err == nil {
return &Hello{client}, nil
} else {
return nil,err
}
}
//客户端调用方法的实现,符合connmon.go 中接口的约定。
type Hello struct {
*rpc.Client
}
func (h *Hello) Print(from string, reply *string) error {
return h.Client.Call(ServerName+".Print", from, reply)
}
新建客服端main.go,对服务端的服务进行调用
package main
import (
"client/servers"
"fmt"
"log"
)
func main() {
//浸信会统一注册
client,err := servers.RegisterHelloClient("tcp", "localhost:1234")
if err != nil {
log.Fatal("dial err", err)
}
var reply string
//调用服务方法
client.Print("piter", &reply)
fmt.Println(reply)
}
在运行服务端后,在运行客户端,得到如下结果
piter:Hello World
这样的一个架构好处是客户端调用再也不用担心方法名称以及参数名称不匹配导致的低级错误。
跨语言的rpc
GO语言标准的rpc采用的是gob编码,因此其他语言调用go语言的服务是比较困难的,在众多的语言中,不同的语言都可能采用不同的编码格式,因此跨语言是RPC的一个必备条件,json格式目前已经是主流的一个语言间的交互规范,当然GO官方也实现了跨语言的RPC(net/rpc/jsonrpc)。其实就是从默认的加解码规则改变成为json的加解码即可。 在服务端main.go,我们用rpc.serveCodec() 代替 rpc.ServeConn() 更改后的代码如下:
package main
import (
"log"
"net"
"net/rpc"
"net/rpc/jsonrpc"
"server/servers"
)
func main() {
servers.RegisterHelloServer(new(servers.Hello))
// rpc.Register(new(servers.Hello))
listen, err := net.Listen("tcp", ":1234")
if err != nil {
log.Fatal("listen errpr", err)
}
for {
conn, _ := listen.Accept()
rpc.ServeCodec(jsonrpc.NewServerCodec(conn)) //此处更改
}
}
在客户端我们将servers/client.go中 rpc.Dial 更换为 net.Dial 在声明rpc的数据加解码格式,具体代码如下:
package servers
import (
"net"
"net/rpc"
"net/rpc/jsonrpc"
)
func RegisterHelloClient(nets, prot string) (*Hello, error) {
conn, err := net.Dial(nets, "localhost:1234")
client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))
if err == nil {
return &Hello{client}, nil
} else {
return nil,err
}
}
type Hello struct {
*rpc.Client
}
func (h *Hello) Print(from string, reply *string) error {
return h.Client.Call(ServerName+".Print", from, reply)
}
运行服务端之后,在运行客户端,得到如下结果
piter:Hello World
在HTTP上提供RPC服务
在http上提供rpc服务,这样的话我们既可以通过rpc的客户端进行调用,也可以通过http服务进行调用,方便业务的横向扩展,一套代码实现多个需求
改造server/main.go 如下
package main
import (
"io"
"net/http"
"net/rpc"
"net/rpc/jsonrpc"
"server/servers"
)
func main() {
servers.RegisterHelloServer(new(servers.Hello)) //注册rpc服务
http.HandleFunc("/jsonrpc", func(w http.ResponseWriter, r *http.Request) {
var conn io.ReadWriteCloser = struct {
io.Writer
io.ReadCloser
}{
w,
r.Body,
}
//rpc.ServeRequest() 给http请求提供rpc服务
rpc.ServeRequest(jsonrpc.NewServerCodec(conn))
})
http.ListenAndServe(":1234", nil) //监听并启动服务
}
对服务进行请求 当然此代码只是实现了http请求提供rpc服务,并没有rpc和http 双端的实现,我们继续改进server/main.go代码,得到如下:
package main
import (
"io"
"log"
"net"
"net/http"
"net/rpc"
"net/rpc/jsonrpc"
"server/servers"
)
func main() {
servers.RegisterHelloServer(new(servers.Hello))
http.HandleFunc("/jsonrpc", func(w http.ResponseWriter, r *http.Request) {
var conn io.ReadWriteCloser = struct {
io.Writer
io.ReadCloser
}{
w,
r.Body,
}
rpc.ServeRequest(jsonrpc.NewServerCodec(conn))
})
go http.ListenAndServe(":1235", nil)
listen, err := net.Listen("tcp", ":1234")
if err != nil {
log.Fatal("listen errpr", err)
}
for {
conn, _ := listen.Accept()
rpc.ServeCodec(jsonrpc.NewServerCodec(conn))
}
}
这样就完成了双端的实现
|