完成了RPC的实现

This commit is contained in:
xiemengjun
2012-10-12 23:16:56 +08:00
parent cea88e25b0
commit dc5d6cb4c4
4 changed files with 386 additions and 0 deletions

385
8.4.md
View File

@@ -1,4 +1,389 @@
#8.4 RPC
前面几个小节我们介绍了基于Socket和HTTP来编写应用通过介绍我们了解了Socket和HTTP使用类似消息传递的模式客户端发送一个消息到服务端然后一般服务器端都会有一定的返回信息。客户端和服务端都需要约定交互的消息格式双方都需要对这种消息能够解析。但是很多独立的应用不会使用这种消息传递的模式而是采用类似函数调用的方式这种方式应用通过调用带有一串参数的函数然后返回一组值。
RPC就是想实现函数调用模式的网络化。客户端就像调用本地函数一样然后客户端把这些参数打包之后通过网络传递到服务端服务端解包到处理过程中执行然后执行的结果反馈给客户端。
RPCRemote Procedure Call Protocol——远程过程调用协议它是一种通过网络从远程计算机程序上请求服务而不需要了解底层网络技术的协议。RPC协议假定某些传输协议的存在如TCP或UDP为通信程序之间携带信息数据。在OSI网络通信模型中RPC跨越了传输层和应用层。RPC使得开发包括网络分布式多程序在内的应用程序更加容易。
##RPC工作原理
![](images/8.4.rpc.png?raw=true)
运行时,一次客户机对服务器的RPC调用,其内部操作大致有如下十步:
- 1.调用客户端句柄;执行传送参数
- 2.调用本地系统内核发送网络消息
- 3.消息传送到远程主机
- 4.服务器句柄得到消息并取得参数
- 5.执行远程过程
- 6.执行的过程将结果返回服务器句柄
- 7.服务器句柄返回结果,调用远程系统内核
- 8.消息传回本地主机
- 9.客户句柄由内核接收消息
- 10.客户接收句柄返回的数据
##Go RPC
Go标准包里面已经支持了RPC而且支持三个级别的RPCTCP、HTTP、JSONRPC。但Go的RPC包是独一无二的RPC他和传统的RPC系统不一样Go的客户端只能和Go的服务端交互因为他们的交互采用了Go的Gob编码。
Go RPC的函数只有符合下面的条件才能被远程访问不然会被忽略详细的要求如下
- 函数必须是导出的(首字母大写)
- 必须有两个导出类型的参数,
- 第一个参数是接收的参数,第二个参数是返回给客户端的参数,第二个参数必须是指针类型的
- 函数还要有一个返回值error
举个例子正确的RPC函数格式如下
func (t *T) MethodName(argType T1, replyType *T2) error
T、T1和T2类型必须能被`encoding/gob`包编解码。
任何的RPC都需要通过网络来传递数据Go RPC可以利用HTTP和TCP来传递数据利用HTTP的好处是可以直接复用`net/http`里面的一些函数。详细的例子请看下面的实现
###HTTP RPC
http的服务端代码实现如下
package main
import (
"errors"
"fmt"
"net/http"
"net/rpc"
)
type Args struct {
A, B int
}
type Quotient struct {
Quo, Rem int
}
type Arith int
func (t *Arith) Multiply(args *Args, reply *int) error {
*reply = args.A * args.B
return nil
}
func (t *Arith) Divide(args *Args, quo *Quotient) error {
if args.B == 0 {
return errors.New("divide by zero")
}
quo.Quo = args.A / args.B
quo.Rem = args.A % args.B
return nil
}
func main() {
arith := new(Arith)
rpc.Register(arith)
rpc.HandleHTTP()
err := http.ListenAndServe(":1234", nil)
if err != nil {
fmt.Println(err.Error())
}
}
通过上面的例子我们看到我们注册了一个Arith的RPC服务然后通过`rpc.HandleHTTP`函数把该服务注册到了HTTP协议上然后我们就可以利用http的方式来传递数据了。
请看下面的客户端代码:
package main
import (
"fmt"
"log"
"net/rpc"
"os"
)
type Args struct {
A, B int
}
type Quotient struct {
Quo, Rem int
}
func main() {
if len(os.Args) != 2 {
fmt.Println("Usage: ", os.Args[0], "server")
os.Exit(1)
}
serverAddress := os.Args[1]
client, err := rpc.DialHTTP("tcp", serverAddress+":1234")
if err != nil {
log.Fatal("dialing:", err)
}
// Synchronous call
args := Args{17, 8}
var reply int
err = client.Call("Arith.Multiply", args, &reply)
if err != nil {
log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)
var quot Quotient
err = client.Call("Arith.Divide", args, &quot)
if err != nil {
log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)
}
我们把上面的服务端和客户端的代码分别编译,然后先把服务端开启,然后开启客户端,输入代码,就会输出如下信息:
$ ./http_c localhost
Arith: 17*8=136
Arith: 17/8=2 remainder 1
通过上面的调用我们看到参数和返回值是我们定义的结构体,在服务端我们把他们当做调用函数的参数类型,在客户端作为`client.Call`的参数。客户端最重要的就是这个`Call`函数,他三个参数,第一个参数是调用对象的函数,第二个参数是传递的参数,第三个参数是返回的参数(注意是指针类型)通过上面服务端和客户端的代码例子我们发现写Go的RPC相当的简单调用也很方便。
###TCP RPC
上面我们实现了基于HTTP协议的RPC接下来我们要实现基于TCP协议的RPC服务端的实现代码如下所示
package main
import (
"errors"
"fmt"
"net"
"net/rpc"
"os"
)
type Args struct {
A, B int
}
type Quotient struct {
Quo, Rem int
}
type Arith int
func (t *Arith) Multiply(args *Args, reply *int) error {
*reply = args.A * args.B
return nil
}
func (t *Arith) Divide(args *Args, quo *Quotient) error {
if args.B == 0 {
return errors.New("divide by zero")
}
quo.Quo = args.A / args.B
quo.Rem = args.A % args.B
return nil
}
func main() {
arith := new(Arith)
rpc.Register(arith)
tcpAddr, err := net.ResolveTCPAddr("tcp", ":1234")
checkError(err)
listener, err := net.ListenTCP("tcp", tcpAddr)
checkError(err)
for {
conn, err := listener.Accept()
if err != nil {
continue
}
rpc.ServeConn(conn)
}
}
func checkError(err error) {
if err != nil {
fmt.Println("Fatal error ", err.Error())
os.Exit(1)
}
}
上面这个代码和http的服务器对比唯一不同的就是这边是采用了TCP然后需要自己控制连接这个连接通知给rpc来处理。
而且我们可以发现这是一个阻塞型的单用户的程序如果想要实现多并发那么可以使用goroutine来实现我们前面在socket小节的时候已经介绍过如何处理goroutine。
下面展现了TCP实现的RPC客户端
package main
import (
"fmt"
"log"
"net/rpc"
"os"
)
type Args struct {
A, B int
}
type Quotient struct {
Quo, Rem int
}
func main() {
if len(os.Args) != 2 {
fmt.Println("Usage: ", os.Args[0], "server:port")
os.Exit(1)
}
service := os.Args[1]
client, err := rpc.Dial("tcp", service)
if err != nil {
log.Fatal("dialing:", err)
}
// Synchronous call
args := Args{17, 8}
var reply int
err = client.Call("Arith.Multiply", args, &reply)
if err != nil {
log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)
var quot Quotient
err = client.Call("Arith.Divide", args, &quot)
if err != nil {
log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)
}
这个客户端代码和http的客户端代码对比唯一的区别一个是DialHTTP一个是Dial(tcp),其他处理一模一样。
###JSON RPC
JSON RPC是数据编码采用了JSON而不是gob编码其他和上面介绍的RPC概念一模一样下面我们来看Go标准包里面如何实现JSON RPC请看服务端代码的实现
package main
import (
"errors"
"fmt"
"net"
"net/rpc"
"net/rpc/jsonrpc"
"os"
)
type Args struct {
A, B int
}
type Quotient struct {
Quo, Rem int
}
type Arith int
func (t *Arith) Multiply(args *Args, reply *int) error {
*reply = args.A * args.B
return nil
}
func (t *Arith) Divide(args *Args, quo *Quotient) error {
if args.B == 0 {
return errors.New("divide by zero")
}
quo.Quo = args.A / args.B
quo.Rem = args.A % args.B
return nil
}
func main() {
arith := new(Arith)
rpc.Register(arith)
tcpAddr, err := net.ResolveTCPAddr("tcp", ":1234")
checkError(err)
listener, err := net.ListenTCP("tcp", tcpAddr)
checkError(err)
for {
conn, err := listener.Accept()
if err != nil {
continue
}
jsonrpc.ServeConn(conn)
}
}
func checkError(err error) {
if err != nil {
fmt.Println("Fatal error ", err.Error())
os.Exit(1)
}
}
我们可以发现上面的代码是基于TCP协议的目前只支持基于TCP协议的方式不支持HTTP方式。
请看客户端的实现代码:
package main
import (
"fmt"
"log"
"net/rpc/jsonrpc"
"os"
)
type Args struct {
A, B int
}
type Quotient struct {
Quo, Rem int
}
func main() {
if len(os.Args) != 2 {
fmt.Println("Usage: ", os.Args[0], "server:port")
log.Fatal(1)
}
service := os.Args[1]
client, err := jsonrpc.Dial("tcp", service)
if err != nil {
log.Fatal("dialing:", err)
}
// Synchronous call
args := Args{17, 8}
var reply int
err = client.Call("Arith.Multiply", args, &reply)
if err != nil {
log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)
var quot Quotient
err = client.Call("Arith.Divide", args, &quot)
if err != nil {
log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)
}
##总结
Go的标准包已经对RPC支持的非常好这样就可以开发很多分布式的Web应用通过上面HTTP、TCP、JSON RPC的实现我们可以领会到这一点。但唯一遗憾的是目前Go标准包里面不支持SOAP RPC协议当然第三方开源包有在实现类似的东西我们也可以找到相应的开源代码。
## links
* [目录](<preface.md>)
* 上一节: [REST](<8.3.md>)