diff --git a/8.4.md b/8.4.md index 11a8a4ad..31dca74c 100644 --- a/8.4.md +++ b/8.4.md @@ -1,4 +1,389 @@ #8.4 RPC +前面几个小节我们介绍了基于Socket和HTTP来编写应用,通过介绍我们了解了Socket和HTTP使用类似消息传递的模式,客户端发送一个消息到服务端,然后一般服务器端都会有一定的返回信息。客户端和服务端都需要约定交互的消息格式,双方都需要对这种消息能够解析。但是很多独立的应用不会使用这种消息传递的模式,而是采用类似函数调用的方式,这种方式应用通过调用带有一串参数的函数,然后返回一组值。 + +RPC就是想实现函数调用模式的网络化。客户端就像调用本地函数一样,然后客户端把这些参数打包之后通过网络传递到服务端,服务端解包到处理过程中执行,然后执行的结果反馈给客户端。 + +RPC(Remote 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,而且支持三个级别的RPC:TCP、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, ") + 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, ") + 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, ") + 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 * [目录]() * 上一节: [REST](<8.3.md>) diff --git a/8.5.md b/8.5.md index 37266ac7..b13a39cf 100644 --- a/8.5.md +++ b/8.5.md @@ -1,4 +1,5 @@ #8.5 小结 +这一章我们介绍了目前流行的集中Web开发方式,第一小节介绍了网络编程中非常重要的Socket编程,因为现在云计算时代,网络通信是唯一的也是必要的一种方式,那么通过网络来进行数据的交流是多么的重要,Socket就是交流的公路,你只有了解了这条路的方向才能开快车。第二小节介绍了目前流行的HTML5中一个重要的特性WebSocket,通过这个可以实现服务器的主动推,可以简化以前ajax轮询的模式。第三小节介绍了REST编写模式,这也是目前很多大公司开发API的实现方式,这种模式特别适合来开发API,结合目前移动的快速发展,我觉得将来会是一个潮流。第四小节介绍了RPC的编程模式,可以实现分布式的应用过程实现。Go对于上面四种模式都很好的支持了,Go的`net`包下面基本实现了所有网络编程的函数。如果你想更加深入的了解里面是怎么实现的,可以尝试阅读这个包下面的源码。 ## links * [目录]() * 上一节: [RPC](<8.4.md>) diff --git a/images/.DS_Store b/images/.DS_Store new file mode 100644 index 00000000..5008ddfc Binary files /dev/null and b/images/.DS_Store differ diff --git a/images/8.4.rpc.png b/images/8.4.rpc.png index 6d0524d0..bb1ef92e 100644 Binary files a/images/8.4.rpc.png and b/images/8.4.rpc.png differ