• 8.4 RPC
    • RPC工作原理
    • Go RPC
      • HTTP RPC
      • TCP RPC
      • JSON RPC
    • 总结
    • links

    8.4 RPC

    前面几个小节我们介绍了如何基于Socket和HTTP来编写网络应用,通过学习我们了解了Socket和HTTP采用的是类似”信息交换”模式,即客户端发送一条信息到服务端,然后(一般来说)服务器端都会返回一定的信息以表示响应。客户端和服务端之间约定了交互信息的格式,以便双方都能够解析交互所产生的信息。但是很多独立的应用并没有采用这种模式,而是采用类似常规的函数调用的方式来完成想要的功能。

    RPC就是想实现函数调用模式的网络化。客户端就像调用本地函数一样,然后客户端把这些参数打包之后通过网络传递到服务端,服务端解包到处理过程中执行,然后执行的结果反馈给客户端。

    RPC(Remote Procedure Call Protocol)——远程过程调用协议,是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。它假定某些传输协议的存在,如TCP或UDP,以便为通信程序之间携带信息数据。通过它可以使函数调用模式网络化。在OSI网络通信模型中,RPC跨越了传输层和应用层。RPC使得开发包括网络分布式多程序在内的应用程序更加容易。

    RPC工作原理

    RPC - 图1

    图8.8 RPC工作流程图

    运行时,一次客户机对服务器的RPC调用,其内部操作大致有如下十步:

    • 1.调用客户端句柄;执行传送参数
    • 2.调用本地系统内核发送网络消息
    • 3.消息传送到远程主机
    • 4.服务器句柄得到消息并取得参数
    • 5.执行远程过程
    • 6.执行的过程将结果返回服务器句柄
    • 7.服务器句柄返回结果,调用远程系统内核
    • 8.消息传回本地主机
    • 9.客户句柄由内核接收消息
    • 10.客户接收句柄返回的数据

    Go RPC

    Go标准包中已经提供了对RPC的支持,而且支持三个级别的RPC:TCP、HTTP、JSONRPC。但Go的RPC包是独一无二的RPC,它和传统的RPC系统不同,它只支持Go开发的服务器与客户端之间的交互,因为在内部,它们采用了Gob来编码。

    Go RPC的函数只有符合下面的条件才能被远程访问,不然会被忽略,详细的要求如下:

    • 函数必须是导出的(首字母大写)
    • 必须有两个导出类型的参数,
    • 第一个参数是接收的参数,第二个参数是返回给客户端的参数,第二个参数必须是指针类型的
    • 函数还要有一个返回值error

    举个例子,正确的RPC函数格式如下:

    1. 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的服务端代码实现如下:

    1. package main
    2. import (
    3. "errors"
    4. "fmt"
    5. "net/http"
    6. "net/rpc"
    7. )
    8. type Args struct {
    9. A, B int
    10. }
    11. type Quotient struct {
    12. Quo, Rem int
    13. }
    14. type Arith int
    15. func (t *Arith) Multiply(args *Args, reply *int) error {
    16. *reply = args.A * args.B
    17. return nil
    18. }
    19. func (t *Arith) Divide(args *Args, quo *Quotient) error {
    20. if args.B == 0 {
    21. return errors.New("divide by zero")
    22. }
    23. quo.Quo = args.A / args.B
    24. quo.Rem = args.A % args.B
    25. return nil
    26. }
    27. func main() {
    28. arith := new(Arith)
    29. rpc.Register(arith)
    30. rpc.HandleHTTP()
    31. err := http.ListenAndServe(":1234", nil)
    32. if err != nil {
    33. fmt.Println(err.Error())
    34. }
    35. }

    通过上面的例子可以看到,我们注册了一个Arith的RPC服务,然后通过rpc.HandleHTTP函数把该服务注册到了HTTP协议上,然后我们就可以利用http的方式来传递数据了。

    请看下面的客户端代码:

    1. package main
    2. import (
    3. "fmt"
    4. "log"
    5. "net/rpc"
    6. "os"
    7. )
    8. type Args struct {
    9. A, B int
    10. }
    11. type Quotient struct {
    12. Quo, Rem int
    13. }
    14. func main() {
    15. if len(os.Args) != 2 {
    16. fmt.Println("Usage: ", os.Args[0], "server")
    17. os.Exit(1)
    18. }
    19. serverAddress := os.Args[1]
    20. client, err := rpc.DialHTTP("tcp", serverAddress+":1234")
    21. if err != nil {
    22. log.Fatal("dialing:", err)
    23. }
    24. // Synchronous call
    25. args := Args{17, 8}
    26. var reply int
    27. err = client.Call("Arith.Multiply", args, &reply)
    28. if err != nil {
    29. log.Fatal("arith error:", err)
    30. }
    31. fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)
    32. var quot Quotient
    33. err = client.Call("Arith.Divide", args, &quot)
    34. if err != nil {
    35. log.Fatal("arith error:", err)
    36. }
    37. fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)
    38. }

    我们把上面的服务端和客户端的代码分别编译,然后先把服务端开启,然后开启客户端,输入代码,就会输出如下信息:

    1. $ ./http_c localhost
    2. Arith: 17*8=136
    3. Arith: 17/8=2 remainder 1

    通过上面的调用可以看到参数和返回值是我们定义的struct类型,在服务端我们把它们当做调用函数的参数的类型,在客户端作为client.Call的第2,3两个参数的类型。客户端最重要的就是这个Call函数,它有3个参数,第1个要调用的函数的名字,第2个是要传递的参数,第3个要返回的参数(注意是指针类型),通过上面的代码例子我们可以发现,使用Go的RPC实现相当的简单,方便。

    TCP RPC

    上面我们实现了基于HTTP协议的RPC,接下来我们要实现基于TCP协议的RPC,服务端的实现代码如下所示:

    1. package main
    2. import (
    3. "errors"
    4. "fmt"
    5. "net"
    6. "net/rpc"
    7. "os"
    8. )
    9. type Args struct {
    10. A, B int
    11. }
    12. type Quotient struct {
    13. Quo, Rem int
    14. }
    15. type Arith int
    16. func (t *Arith) Multiply(args *Args, reply *int) error {
    17. *reply = args.A * args.B
    18. return nil
    19. }
    20. func (t *Arith) Divide(args *Args, quo *Quotient) error {
    21. if args.B == 0 {
    22. return errors.New("divide by zero")
    23. }
    24. quo.Quo = args.A / args.B
    25. quo.Rem = args.A % args.B
    26. return nil
    27. }
    28. func main() {
    29. arith := new(Arith)
    30. rpc.Register(arith)
    31. tcpAddr, err := net.ResolveTCPAddr("tcp", ":1234")
    32. checkError(err)
    33. listener, err := net.ListenTCP("tcp", tcpAddr)
    34. checkError(err)
    35. for {
    36. conn, err := listener.Accept()
    37. if err != nil {
    38. continue
    39. }
    40. rpc.ServeConn(conn)
    41. }
    42. }
    43. func checkError(err error) {
    44. if err != nil {
    45. fmt.Println("Fatal error ", err.Error())
    46. os.Exit(1)
    47. }
    48. }

    上面这个代码和http的服务器相比,不同在于:在此处我们采用了TCP协议,然后需要自己控制连接,当有客户端连接上来后,我们需要把这个连接交给rpc来处理。

    如果你留心了,你会发现这它是一个阻塞型的单用户的程序,如果想要实现多并发,那么可以使用goroutine来实现,我们前面在socket小节的时候已经介绍过如何处理goroutine。
    下面展现了TCP实现的RPC客户端:

    1. package main
    2. import (
    3. "fmt"
    4. "log"
    5. "net/rpc"
    6. "os"
    7. )
    8. type Args struct {
    9. A, B int
    10. }
    11. type Quotient struct {
    12. Quo, Rem int
    13. }
    14. func main() {
    15. if len(os.Args) != 2 {
    16. fmt.Println("Usage: ", os.Args[0], "server:port")
    17. os.Exit(1)
    18. }
    19. service := os.Args[1]
    20. client, err := rpc.Dial("tcp", service)
    21. if err != nil {
    22. log.Fatal("dialing:", err)
    23. }
    24. // Synchronous call
    25. args := Args{17, 8}
    26. var reply int
    27. err = client.Call("Arith.Multiply", args, &reply)
    28. if err != nil {
    29. log.Fatal("arith error:", err)
    30. }
    31. fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)
    32. var quot Quotient
    33. err = client.Call("Arith.Divide", args, &quot)
    34. if err != nil {
    35. log.Fatal("arith error:", err)
    36. }
    37. fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)
    38. }

    这个客户端代码和http的客户端代码对比,唯一的区别一个是DialHTTP,一个是Dial(tcp),其他处理一模一样。

    JSON RPC

    JSON RPC是数据编码采用了JSON,而不是gob编码,其他和上面介绍的RPC概念一模一样,下面我们来演示一下,如何使用Go提供的json-rpc标准包,请看服务端代码的实现:

    1. package main
    2. import (
    3. "errors"
    4. "fmt"
    5. "net"
    6. "net/rpc"
    7. "net/rpc/jsonrpc"
    8. "os"
    9. )
    10. type Args struct {
    11. A, B int
    12. }
    13. type Quotient struct {
    14. Quo, Rem int
    15. }
    16. type Arith int
    17. func (t *Arith) Multiply(args *Args, reply *int) error {
    18. *reply = args.A * args.B
    19. return nil
    20. }
    21. func (t *Arith) Divide(args *Args, quo *Quotient) error {
    22. if args.B == 0 {
    23. return errors.New("divide by zero")
    24. }
    25. quo.Quo = args.A / args.B
    26. quo.Rem = args.A % args.B
    27. return nil
    28. }
    29. func main() {
    30. arith := new(Arith)
    31. rpc.Register(arith)
    32. tcpAddr, err := net.ResolveTCPAddr("tcp", ":1234")
    33. checkError(err)
    34. listener, err := net.ListenTCP("tcp", tcpAddr)
    35. checkError(err)
    36. for {
    37. conn, err := listener.Accept()
    38. if err != nil {
    39. continue
    40. }
    41. jsonrpc.ServeConn(conn)
    42. }
    43. }
    44. func checkError(err error) {
    45. if err != nil {
    46. fmt.Println("Fatal error ", err.Error())
    47. os.Exit(1)
    48. }
    49. }

    通过示例我们可以看出 json-rpc是基于TCP协议实现的,目前它还不支持HTTP方式。

    请看客户端的实现代码:

    1. package main
    2. import (
    3. "fmt"
    4. "log"
    5. "net/rpc/jsonrpc"
    6. "os"
    7. )
    8. type Args struct {
    9. A, B int
    10. }
    11. type Quotient struct {
    12. Quo, Rem int
    13. }
    14. func main() {
    15. if len(os.Args) != 2 {
    16. fmt.Println("Usage: ", os.Args[0], "server:port")
    17. log.Fatal(1)
    18. }
    19. service := os.Args[1]
    20. client, err := jsonrpc.Dial("tcp", service)
    21. if err != nil {
    22. log.Fatal("dialing:", err)
    23. }
    24. // Synchronous call
    25. args := Args{17, 8}
    26. var reply int
    27. err = client.Call("Arith.Multiply", args, &reply)
    28. if err != nil {
    29. log.Fatal("arith error:", err)
    30. }
    31. fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)
    32. var quot Quotient
    33. err = client.Call("Arith.Divide", args, &quot)
    34. if err != nil {
    35. log.Fatal("arith error:", err)
    36. }
    37. fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)
    38. }

    总结

    Go已经提供了对RPC的良好支持,通过上面HTTP、TCP、JSON RPC的实现,我们就可以很方便的开发很多分布式的Web应用,我想作为读者的你已经领会到这一点。但遗憾的是目前Go尚未提供对SOAP RPC的支持,欣慰的是现在已经有第三方的开源实现了。

    • 目录
    • 上一节: REST
    • 下一节: 小结