RPC是远程过程调用协议的缩写,简单说就是他可以在本地网络下,调用远方的一个进程中的方法。比如我在广州自己的电脑上运行自己写的一个程序,该程序使用了RPC协议,然后就可以调用远在北京的一个程序,该程序当然也使用了RPC协议。这样的话就可以屏蔽了语言的差异,比如本地可以用C或Python语言写,远端可以使用JAVA或GO语言写。
他们之间使用的是网络通信,为了保证数据传输的稳定性和安全性,使用TCP进行连接,所以RPC算是位于传输层和应用层之间。
我们进行网络编程一般使用Socket编程,相比TCP编程,Socket是全双工的长连接,也就是说服务端和客户端之间可以同时收发,并且相比TCP编程,客户端和服务端建立连接之后就一直连着,除非超时或主动断开,这样的话就可以节约每次通信的都要建立连接而浪费资源。RPC编程时使用了Socket。
Socket服务端编程时一般先创建监听端口,然后建立连接,最后进行读写操作来收发信息,而客户端直接连接服务端,然后就可以进行读写操作来收发信息。RPC编程和Socket编程类似。
- RPC的服务端首先注册rpc服务,然后创建socket监听端口,然后建立连接,最后绑定rpc服务
- RPC的客户端直接连接服务端,然后就可以直接调用远程的方法了
Golang实现RPC编程
当然只有服务端注册了的方法,客户端才能使用注册了的方法。同时,对于服务端要注册的方法也有硬性要求,这里使用Golang语言实现:
- 1)注册的方法必须是导出的,也就是包外可见。在Golang中首字母大写,java中使用public关键字
- 2)注册的方法必须有两个参数,都是导出类型或内建类型。第一个参数是客户端传来的值,第二个参数是返回客户端的值
- 3)注册的方法的第二个参数必须是指针
- 4)注册的方法只有一个error接口类型的返回值
// 服务端
package main
import (
"log"
"net"
"net/rpc"
)
type World struct { // 使用结构体来模拟类的概念
}
func (this *World) HelloWorld(name string, respon *string) error { // 关联了结构体,它就是一个方法了,等下要注册这个方法,注意这个方法的参数要符合上面说的要求
*respon = "要是能重来,我要选" + name
return nil // 返回空
}
func main() {
// func RegisterName(name string, rcvr interface{}) error {} // 注册方法原型
// 第一个参数随意指定服务名,客户端要使用到,第二个参数传递对象,结构体只是定义,没有实例化,所以要使用new()实例化它
if err := rpc.RegisterName("Sayhello", new(World)); err != nil { // 注册rpc服务
log.Println("注册失败", err)
}
listen, err := net.Listen("tcp", "127.0.0.1:20000") // 创建socket监听端口
if err != nil {
log.Println("监听失败", err)
return
}
log.Println("监听中……")
conn, err := listen.Accept() // 建立连接
if err != nil {
log.Println("连接失败", err)
return
}
log.Println("已有一个连接过来了")
defer log.Println("关闭连接成功")
defer listen.Close()
defer conn.Close()
rpc.ServeConn(conn) // 绑定rpc服务
}
// 客户端
package main
import (
"fmt"
"log"
"net/rpc"
)
func main() {
rpc_cli, err := rpc.Dial("tcp", "127.0.0.1:20000") // 使用rpc包的Dial方法连接
if err != nil {
log.Println("连接失败", err)
}
defer rpc_cli.Close()
var reply string
// 连接之后要使用Call方法调用
// 第一个参数是(服务名.方法名),所以是Sayhello.HelloWorld
// 第二个参数是要传递过去的参数,为字符型
// 第三个参数是接受返回值的,注意是指针类型
if err := rpc_cli.Call("Sayhello.HelloWorld", "李白", &reply); err != nil {
log.Println(err)
}
fmt.Println(reply)
}
先运行服务端,然后运行客户端,就会看到如下面:
它的通信过程中,里面传的是什么?
使用netcat工具查看,在Linux中该工具叫做nc,在Windows中要自己寻找下载。在Windows中使用该工具进行监听,启动命令netcat.exe -l 127.0.0.1 -p 20000
它就开始监听,-l
指定地址,也可以不写具体IP默认本地,但要写参数,-p
指定端口,必须写,然后运行刚刚写的客户端代码,就可以看到如下,乱码了:
在不同系统中,由于系统的设计不同,有些系统认为00010101 00101010是2142,但有些系统认为这是4221,所以为了屏蔽这种差异,使用序列化的方法解决。将一个对象序列化成字符串,在传输过程中转为比特流,到另一端后,将比特流转为字符串,然后将字符串反序列化成对象,该对象和本地创建的对象就没有区别了。在上面的代码中Golang使用了自己特有的序列化方法(又称gob),对对象进行了序列化和反序列化,由于是特有的,所以其他语言不能解析,而netcat工具不是Golang语言写的,所以你就会看到上面乱码了
使用其他通用的序列化、反序列化方法
上面写RPC时使用的是net/rpc
这个包,现在使用net/rpc/jsonrpc
这个包编程,用法一致
客户端的修改
现在我只修改客户端的代码
// 客户端
package main
import (
"fmt"
"log"
"net/rpc/jsonrpc"
)
func main() {
rpc_cli, err := jsonrpc.Dial("tcp", "127.0.0.1:20000")
if err != nil {
log.Println("连接失败", err)
}
defer rpc_cli.Close()
var reply string
if err := rpc_cli.Call("Sayhello.HelloWorld", "李白", &reply); err != nil {
log.Println(err)
}
fmt.Println(reply)
}
同样先运行netcat工具,再运行客户端,结果如下,英文正常了,但中文乱码:
为什么中文乱码? 因为我的Windows的编码是GBK(中文2字节),而程序使用的是UTF-8(中文3字节),对于英文和一些半角符号,他们在两种编码中都是在最低8位上,所以他们没有乱码,但中文乱码。我使用了我的远程阿里云主机Ubuntu系统,使用nc监听,该工具Linux自带,命令nc -l -p 20000
开启监听(记得阿里云的防火墙策略开放该端口),然后修改代码中的地址指向远程主机,最后开启上面的客户端,在远程主机中显示结果为:
因此可以确定客户端就是给服务端发了这么一个东西。
服务端的修改
客户端修改了序列化的方法,不再是Golang自带的序列化方法了,那么服务端就不能正常和客户端通信了,所以要修改服务端的代码。只需在最后一行的rpc.ServeConn(conn)
改为jsonrpc.ServeConn(conn)
,然后导包即可。
骚方法测试。。。。。
已经知道了它发送的就是=={“method”:“Sayhello.HelloWorld”,“params”:[“李白”],“id”:0}==这么一段字符串,那么使用netcat再次模拟客户端,使用命令echo '{"method":"Sayhello.HelloWorld","params":["李白"],"id":0}' |.\netcat.exe 127.0.0.1 20000
,echo向终端显示信息,但使用了管道符就会发送向后面的命令,后面的命令就是模拟客户端的方式,向这个地址发送信息并持续监听。由于在Windows中操作,可以看到中文有乱码了,但那串乱码就是返回的字符要是能重来我要选李白
返回异常
在上面的图片中,也可以知道服务器返回的数据是这样的:{“id”:0,“result”:“要是能重来我要选李白”,“error”:null}
error参数也是注册的方法返回的值,同时返回的是nil
func (this *World) HelloWorld(name string, respon *string) error {
*respon = "要是能重来,我要选" + name
return nil
}
如果我们返回的不是nil呢
func (this *World) HelloWorld(name string, respon *string) error {
*respon = "要是能重来,我要选" + name
return errors.New("sorry I love you")
}
可以看到result参数为空了,而error参数为异常值了