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规则:

  1. 方法只能有两个可序列化的参数,其中第二个参数是指针类型,并且返回一个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的调用差异不大,这次我们解决下面的问题

  1. 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端就像调用本地方法一样。

继续屏蔽HelloserviceNameHello函数名称

目录结构
 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}