Go语言的RPC包的路径为net/rpc
,也就是放在了net包目录下面。因此我们可以猜测该RPC包是建立在net包基础之上的。我们基于http实现了一个打印例子。下面我们尝试基于rpc实现一个类似的例子。
服务端
1packagemain
2
3import(
4 "log"
5 "net"
6 "net/rpc"
7)
8
9type HelloServicestruct{
10}
11
12func(s*HelloService)Hello(request string, reply*string)error{
13 //返回值是通过修改request的值
14 *reply = "Hello" + request
15 return nil
16}
17
18func main(){
19
20 //1.实例化一个server
21 listener,err:=net.Listen("tcp",":1234")
22 if err!=nil{
23 log.Fatalln(err)
24 }
25 //2.注册处理逻辑
26 err=rpc.RegisterName("HelloService",&HelloService{})
27 if err!=nil{
28 return
29 }
30 //3.启动服务
31 conn,err:=listener.Accept()//当一个新的链接进来的时候,
32 rpc.ServeConn(conn)
33
34//一连串的代码大部分都是net的包好像和rpc没有什么关系
35//1.go语言的rpc序列化的反序列协议是Gob
36}
其中Hello方法必须满足Go语言的RPC规则:
- 方法只能有两个可序列化的参数,其中第二个参数是指针类型,并且返回一个error类型,同时必须是公开的方法。
然后就可以将HelloService
类型的对象注册为一个RPC服务:(TCP RPC服务)。
其中rpc.Register()
函数调用会将对象类型中所有满足RPC规则的对象方法注册为RPC函数,所有注册的方法会放在“HelloService”
服务空间之下。
然后我们建立一个唯一的TCP链接,并且通过rpc.ServeConn()
函数在该TCP链接上为对方提供RPC服务。
客户端
1package main
2
3import(
4 "log"
5 "net/rpc"
6)
7
8func main(){
9
10 //1.建立连接
11 client,err:=rpc.Dial("tcp","localhost:1234")
12 if err!=nil{
13 log.Fatalln(err)
14 }
15 var reply=new(string)//在内存中分配变量,并把指针赋值给变量
16 //varreplystring//此时的string已经有地址了,而且还有零值使用&reply传递参数
17 //这里调用的服务的方法是服务名.方法名
18 err=client.Call("HelloService.Hello","jimyag",reply)
19 if err!=nil{
20 log.Fatalln(err)
21 }
22 log.Printf(*reply)
23}
首先是通过rpc.Dial拨号RPC服务,然后通过client.Call调用具体的RPC方法。在调用client.Call时,第一个参数是用点号链接的RPC服务名字和方法名字,第二和第三个参数分别我们定义RPC方法的两个参数。
改进rpc的调用过程
改进1
前面的rpc调用虽然简单,但是和普通的http的调用差异不大,这次我们解决下面的问题
- serviceName统一和名称冲突的问题
- 多个server的包中serviceName同名的问题
- server端和client端如何统一serviceName
上述实现中服务名称是在客户端和服务端写死的,如果有一方改动,那么双方都要改动
目录结构
1.
2├── client
3│ └── main.go
4├── handle
5│ └── handle.go
6└── server
7 └── main.go
新建handler/handler.go
文件内容如下:
1package handle
2
3const (
4 // 解决命名冲突
5 HelloServiceName = "handle/HelloService"
6)
为什么要新建这个文件?
是为了解耦。
服务端
1package main
2
3import (
4 "net"
5 "net/rpc"
6
7 "test-rpc/handle" // 自己的包名
8)
9
10type HelloService struct {
11}
12
13func (S *HelloService) Hello(request string, reply *string) error {
14 *reply = "hello " + request
15 return nil
16}
17func main() {
18 _ = rpc.RegisterName(handle.HelloServiceName, &HelloService{})
19 lisener, err := net.Listen("tcp", ":1234")
20 if err != nil {
21 panic("监听端口失败")
22 }
23 conn, err := lisener.Accept()
24 if err != nil {
25 panic("建立连接失败")
26 }
27
28 rpc.ServeConn(conn)
29}
客户端
1package main
2
3import (
4 "fmt"
5 "net/rpc"
6
7 "test-rpc/handle" // 自己的包名
8)
9
10func main() {
11 client, err := rpc.Dial("tcp", "localhost:1234")
12 if err != nil {
13 panic("连接到服务器失败")
14 }
15
16 var reply string
17 // 只要加上调用的方法名即可
18 err = client.Call(handle.HelloServiceName+".Hello", "jimyag", &reply)
19 if err != nil {
20 panic("服务调用失败")
21 }
22
23 fmt.Println(reply)
24}
改进2
以上,我们解耦了服务名。但是,对于服务端和客户端来说,他们只要管调用相关的方法就行,不要管相关的实现。
那么我们可以封装一个client和server端的代理,让client和server端就像调用本地方法一样。
继续屏蔽HelloserviceName
和Hello
函数名称
目录结构
1.
2├── client
3│ └── main.go
4├── client_proxy
5│ └── client_proxy.go
6├── handle
7│ └── handle.go
8├── server
9│ └── main.go
10└── server_porxy
11 └── server_proxy.go
handle.go
1package handle
2
3type HelloService struct{}
4
5func (s *HelloService) Hello(request string, reply *string) error {
6 *reply = "hello " + request
7 return nil
8}
server_proxy.go
在提供的服务中通过interface进行封装,在这里我们关心的调用的函数,而不是某个结构体。所以封装的时候传入的参数为interface
1package server_porxy
2
3import "net/rpc"
4
5const HelloServiceName = "handler/HelloService"
6
7type HelloServiceInterface interface {
8 Hello(request string, reply *string) error
9}
10
11// 封装服务的注册
12func RegisterHelloService(srv HelloServiceInterface) error {
13 return rpc.RegisterName(HelloServiceName, srv)
14}
server.go
服务端调用的时候,就可以直接注册一个hello的服务。
1package main
2
3import (
4 "net"
5 "net/rpc"
6
7 "test-rpc3/handle" // 项目包名
8 "test-rpc3/server_porxy" // 项目包名
9)
10
11func main() {
12 helloHandler := &handle.HelloService{}
13 _ = server_porxy.RegisterHelloService(helloHandler)
14 listener, err := net.Listen("tcp", ":1234")
15 if err != nil {
16 panic("监听端口失败")
17 }
18 conn, err := listener.Accept()
19 if err != nil {
20 panic("建立链接失败")
21 }
22 rpc.ServeConn(conn)
23}
client_proxy.go
客户端调用远程的方法时候,要像调用本地方法一样进行调用。封装一个hello的client,只需要调用client里面的方法就行。
1package client_proxy
2
3import "net/rpc"
4
5const HelloServiceName = "handler/HelloService"
6
7// 将hello client暴露出去
8type HelloServiceClient struct {
9 *rpc.Client
10}
11
12func NewClient(address string) HelloServiceClient {
13 conn, err := rpc.Dial("tcp", address)
14 if err != nil {
15 panic("连接服务器错误")
16 }
17 return HelloServiceClient{conn}
18}
19
20func (c *HelloServiceClient) Hello(request string, reply *string) error {
21 err := c.Call(HelloServiceName+".Hello", request, reply)
22 if err != nil {
23 return err
24 }
25 return nil
26}
client.go
1package main
2
3import (
4 "fmt"
5
6 "test-rpc3/client_proxy"
7)
8
9func main() {
10 client := client_proxy.NewClient("localhost:1234")
11 var reply string
12 err := client.Hello("jimyag", &reply)
13 if err != nil {
14 panic("调用失败")
15 }
16 fmt.Println(reply)
17}