RPC和Protobuf(一)
RPC和Protobuf
RPC是远程过程调用(Remote Procedure Call) 的缩写, 通俗地来说就是调用远处的一个函数,远处到底有多远?可能是同一个机器的另一个进程,也可能是远在火星好奇号上的一个秘密东西。因为RPC涉及的函数可能非常远,远到它们之间说着不同的语言,所以我们需要解决沟通语言的障碍。而Protobuf由于支持多种不同的语言(甚至不支持的语言也可以拓展),其本身特性也非常方便描述服务的接口,因此非常适合作为RPC世界的接口交流语言。
RPC版“Hello World”
Go语言的RPC包路径为net/rpc,也就是说Go默认是支持使用rpc的,在目前的行业中RPC是分布式系统中不同节点间流行的通行方式,在互联网时代,RPC已经和IPC(进程通信)一样成为一个不可或缺的基础构件。
server.go代码如下:
package main import ( "log" "net" "net/rpc" ) // 构建一个承载函数的结构体 type HelloServer struct { } // 为承载对象添加实现方法,方法只能有两个可序列化参数:一个请求、一个相应,并且有一个error返回值,同时必须是个公开的方法 func (p *HelloServer) Hello (request string, reply *string) error { *reply = "Hello "+request return nil } func main() { // RegisterName 会将对象类型中所有满足rpc规则的对象方法注册到rpc服务中,所有注册的方法会放到HelloServer服务空间之下 rpc.RegisterName("grpc_Pro.HelloServer",new(HelloServer)) listener, err := net.Listen("tcp",":1234") if err != nil { log.Fatal("ListenTCP error:", err) } // 建立唯一的TCP连接,并通过rpc.ServerConn函数在该TCP连接上为对方提供RPC服务 conn, err := listener.Accept() if err != nil { log.Fatal("Accept error:",err) } rpc.ServeConn(conn) }
client.go代码如下:
package main import ( "fmt" "log" "net/rpc" ) // 自定义命名空间,与服务注册的名字一致 const HelloServiceName = "grpc_Pro.HelloServer" // 使用一个接口来约束注册的结构体 type HelloServiceInterface = interface { Hello (request string, reply *string) error } // 将结构体注册到rpc的命名空间下,注意这里的命名空间可以是一个路径片段,这个函数是为了隐式实现HelloService是否实现接口需求 func RegisterHelloService(svc HelloServiceInterface) error { return rpc.RegisterName(HelloServiceName, svc) } 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服务名字和方法名字,第二个和第三个参数分别是定义rpc方法的两个参数 err = client.Call(HelloServiceName+".Hello","hello",&reply) if err != nil { log.Fatal(err) } fmt.Println(reply) }
更安全的RPC接口
以上的代码中rpc并没有进行封装,下面我们我们将进行封装,使其更加安全。
safe_server.go代码如下:
package main import ( "fmt" "log" "net" "net/rpc" ) // 定义服务结构体 type HelloService struct { } // 定义服务空间名,之后要与客户端一致绑定 const HelloServiceName = "grpc.Server" // 服务端接口要求方法实现 type HelloServiceInterface = interface { Hellos(request string, reply *string) error } // 将结构体注册到rpc的命名空间下,注意这里的命名空间可以是一个路径片段,隐式约束实现接口中的方法后才能注册 func RegisterHelloService(svc HelloServiceInterface) error { // 将注册一这个名字为空间的,可以通过映射到这个结构体下,在客户端通过.调用对应的方法 return rpc.RegisterName(HelloServiceName, svc) } // 服务端实现方法,注意这个函数名要和client端的.名字保持一致 func (p *HelloService) Hellos(request string, reply *string) error { *reply = "hello :" + request return nil } func main() { // 将结构体注册到rpc中,前提式实现了方法 RegisterHelloService(new(HelloService)) listener, err := net.Listen("tcp",":1234") if err != nil { log.Fatal("ListenTCP error: ",err) } for { fmt.Println("listenning...") // 监听服务 conn, err := listener.Accept() if err != nil { log.Fatal("Accept error:", err) } // 将监听到的服务挂在在rpc上 go rpc.ServeConn(conn) } }
safe_client.go代码如下:
package main import ( "fmt" "log" "net/rpc" ) // rpc服务空间 const HelloServiceNames = "grpc.Server" // 客户端接口,要求客户端实现的方法 type HelloServiceInterfaces = interface { Hello(request string, reply *string) error } // 客户端结构体,需要有roc的方法 type HelloServiceClient struct { *rpc.Client } // 约束实现接口,这种方式在执行时判断,如果不是_,其实式放回的接口对象 var _ HelloServiceInterfaces = (*HelloServiceClient)(nil) /* 隐式约束结构体实现接口 //方式一: func RegisterHelloServices(svc HelloServiceInterfaces) error { return rpc.RegisterName(HelloServiceNames, svc) } //方式二: var _ HelloServiceInterfaces = new(HelloServiceClient) / */ // 完成rpc拨号,并返回客户端对象 func DiaHelloServiceClient(network, address string) (*HelloServiceClient, error) { c, err := rpc.Dial(network, address) if err != nil { return nil, err } return &HelloServiceClient{Client: c}, nil } // 客户端方法实现 func (p *HelloServiceClient) Hello(request string, reply *string) error { // 客户端服务调用远端程序 return p.Client.Call(HelloServiceNames+".Hellos", request, reply) } func main() { // 返回客户端对象 client, err := DiaHelloServiceClient("tcp","localhost:1234") if err != nil { log.Fatal("dialing...",err) } var reply string // 客户端调用方法 err = client.Hello("hello Mr.Wang", &reply) if err != nil { log.Fatal(err) } fmt.Println("result: ",reply) } // 总结: 客户端和服务端都有接口,服务端的接口约束要实现的服务,客户端会根据接口定义去调用服务器的方法 // 客户端的接口为了约束调用服务器的函数