RPC(Remote Procedure Call Protocol),是远程过程调用的缩写,通俗的说就是调用远处的一个函数。与之相对应的是本地函数调用,咱们先来看一下本地函数调用。当咱们写下以下代码的时候:
规则golang
result := Add(1,2)
咱们知道,咱们传入了1,2两个参数,调用了本地代码中的一个Add函数,获得result这个返回值。这时参数,返回值,代码段都在一个进程空间内,这是本地函数调用。shell
那有没有办法,咱们可以调用一个跨进程(因此叫"远程",典型的事例,这个进程部署在另外一台服务器上)的函数呢?json
这也是RPC主要实现的功能。服务器
咱们使用微服务化的一个好处就是,不限定服务的提供方使用什么技术选型,可以实现公司跨团队的技术解耦。网络
这样的话,若是没有统一的服务框架,RPC框架,各个团队的服务提供方就须要各自实现一套序列化、反序列化、网络框架、链接池、收发线程、超时处理、状态机等“业务以外”的重复技术劳动,形成总体的低效。因此,统一RPC框架把上述“业务以外”的技术劳动统一处理,是服务化首要解决的问题。框架
Go语言的RPC包的路径为net/rpc,也就是放在了net包目录下面。所以咱们能够猜想该RPC包是创建在net包基础之上的。接着咱们尝试基于rpc实现一个相似的例子。咱们先构造一个HelloService类型,其中的Hello方法用于实现打印功能:异步
type HelloService struct{} func(p *HelloService)Hello(request string,reply *string)error{ *reply = "hello:" + request return nil }
Hello方法方法必须知足Go语言的RPC规则:方法只能有两个可序列化的参数,其中第二个参数是指针类型,而且返回一个error类型,同时必须是公开的方法。tcp
golang 中的类型好比:channel(通道)、complex(复数类型)、func(函数)均不能进行 序列化ide
而后就能够将HelloService类型的对象注册为一个RPC服务:函数
func main(){ //rpc注册服务 //注册rpc服务,维护一个hash表,key值是服务名称,value值是服务的地址 rpc.RegisterName("HelloService",new(HelloService)) //设置服务监听 listener,err := net.Listen("tcp",":1234") if err != nil { panic(err) } //接受传输的数据 conn,err := listener.Accept() if err != nil { panic(err) } //rpc调用,并返回执行后的数据 //1.read,获取服务名称和方法名,获取请求数据 //2.调用对应服务里面的方法,获取传出数据 //3.write,把数据返回给client rpc.ServeConn(conn) }
其中rpc.Register函数调用会将对象类型中全部知足RPC规则的对象方法注册为RPC函数,全部注册的方法会放在“HelloService”服务空间之下。而后咱们创建一个惟一的TCP连接,而且经过rpc.ServeConn函数在该TCP连接上为对方提供RPC服务。
下面是客户端请求HelloService服务的代码:
func main(){ //用rpc链接 client,err := rpc.Dial("tcp","localhost:1234") if err != nil { panic(err) } var reply string //调用服务中的函数 err = client.Call("HelloService.Hello","world",&reply) if err != nil { panic(err) } fmt.Println("收到的数据为,",reply) }
首选是经过rpc.Dial拨号RPC服务,而后经过client.Call调用具体的RPC方法。在调用client.Call时,第一个参数是用点号连接的RPC服务名字和方法名字,第二和第三个参数分别咱们定义RPC方法的两个参数。
标准库的RPC默认采用Go语言特有的gob编码。所以,其它语言调用Go语言实现的RPC服务将比较困难。跨语言是互联网时代RPC的一个首要条件,这里咱们再来实现一个跨语言的RPC。得益于RPC的框架设计,Go语言的RPC其实也是很容易实现跨语言支持的。
这里咱们将尝试经过官方自带的net/rpc/jsonrpc扩展实现一个跨语言RPC。
首先是基于json编码从新实现RPC服务:
func main(){ //注册rpc服务 rpc.RegisterName("HelloService",new(HelloService)) //设置监听 listener,err := net.Listen("tcp",":1234") if err != nil { panic(err) } for{ //接收链接 conn,err := listener.Accept() if err != nil { panic(err) } //给当前链接提供针对json格式的rpc服务 go rpc.ServeCodec(jsonrpc.NewServerCodec(conn)) } }
代码中最大的变化是用rpc.ServeCodec函数替代了rpc.ServeConn函数,传入的参数是针对服务端的json编解码器。
而后是实现json版本的客户端:
func main(){ //简历tcp链接 conn,err := net.Dial("tcp","localhost:1234") if err !=nil{ panic(err) } //简历基于json编解码的rpc服务 client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn)) var reply string //调用rpc服务方法 err = client.Call("HelloService.Hello"," world",&reply) if err != nil { panic(err) } fmt.Println("收到的数据为:",reply) }
先手工调用net.Dial函数创建TCP连接,而后基于该连接创建针对客户端的json编解码器。
在确保客户端能够正常调用RPC服务的方法以后,咱们能够用命令来查看一下客户端发给服务端的究竟是什么数据。这里咱们使用 ==nc -l 1234== 这条命令 模拟服务器监听1234端口接收的数据,而后从新运行客户端,将会发现nc输出了如下的信息:
{"method":"HelloService.Hello","params":["hello"],"id":0}
nc经常使用有两种一种是链接到指定ip和端口
nc hostname port
另一种是监听端口,等待链接
nc -l port
这是一个json编码的数据,其中method部分对应要调用的rpc服务和方法组合成的名字,params部分的第一个元素为参数,id是由调用端维护的一个惟一的调用编号。
请求的json数据对象在内部对应两个结构体:客户端是clientRequest,服务端是serverRequest。clientRequest和serverRequest结构体的内容基本是一致的:
type clientRequest struct { Method string `json:"method"` Params []interface{} `json:"params"` Id uint64 `json:"id"` } type serverRequest struct { Method string `json:"method"` Params *json.RawMessage `json:"params"` Id *json.RawMessage `json:"id"` }
了解了客户端须要发送哪些数据以后,咱们能够再来看看服务器接收到客户端传输的数据以后会返回哪些数据,仍是用咱们的nc命令。操做以下:
echo -e '{"method":"HelloService.Hello","params":["hello"],"id":1}'| nc localhost 1234
返回的数据以下:
其中id对应输入的id参数,result为返回的结果,error部分在出问题时表示错误信息。对于顺序调用来讲,id不是必须的。可是Go语言的RPC框架支持异步调用,当返回结果的顺序和调用的顺序不一致时,能够经过id来识别对应的调用。
返回的json数据也是对应内部的两个结构体:客户端是clientResponse,服务端是serverResponse。两个结构体的内容一样也是相似的:
type clientResponse struct { Id uint64 `json:"id"` Result *json.RawMessage `json:"result"` Error interface{} `json:"error"` } type serverResponse struct { Id *json.RawMessage `json:"id"` Result interface{} `json:"result"` Error interface{} `json:"error"` }
所以不管采用何种语言,只要遵循一样的json结构,以一样的流程就能够和Go语言编写的RPC服务进行通讯。这样咱们就解用json简单实现了跨语言的RPC。
可是通常在开发的时候除了用json作跨语言的RPC服务以外,如今不少公司还会选用protobuf作跨语言的RPC服务。那什么是ProtoBuf呢?接下来咱们详细了解一下。
上面的代码服务名都是写死的,不够灵活(容易写错),这里咱们对RPC的服务端和客户端再次进行一次封装,来屏蔽掉服务名,具体代码以下
//抽离服务名称 var serverName = "LoginService" //定义一个父类 type RPCDesign interface { Hello(string,*string)error } //实现工厂函数 func RegisterRPCServer(srv RPCDesign)error{ return rpc.RegisterName(serverName,srv) }
封装以后的服务端实现以下:
type RpcServer struct{} //5 + 3i chan func complex func (this *RpcServer) Hello(req string, resp *string) error { *resp += req + "你好" return nil } func main() { //设置监听 listener, err := net.Listen("tcp", ":8899") if err != nil { fmt.Println("设置监听错误") return } defer listener.Close() fmt.Println("开始监听....") for { //接收连接 conn, err := listener.Accept() if err != nil { fmt.Println("获取链接失败") return } defer conn.Close() fmt.Println(conn.RemoteAddr().String() + "链接成功") //rpc表 注册rpc服务 if err = RegisterRPCServer(new(RpcServer)); err != nil { fmt.Println("注册rpc服务失败") return } //把rpc服务和套接字绑定 //rpc.ServeConn(conn) rpc.ServeCodec(jsonrpc.NewServerCodec(conn)) } }
type RPCClient struct { rpcClient *rpc.Client } func NewRpcClient(addr string)(RPCClient){ conn,err := net.Dial("tcp",addr) if err != nil { fmt.Println("连接服务器失败") return RPCClient{} } defer conn.Close() //套接字和rpc服务绑定 client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn)) return RPCClient{rpcClient:client} } func (this*RPCClient)CallFunc(req string,resp*string)error{ return this.rpcClient.Call(serverName+".Hello",req,resp) }
封装以后客户端实现
func main() { //初始化对象 与服务名有关的内容彻底封装起来了 client := NewRpcClient("127.0.0.1:8899") //调用成员函数 var temp string client.CallFunc("xiaoming",&temp) fmt.Println(temp) }