Go 程序可以通过 net/rpc 包相互通讯,所以这是另一个客户端 - 服务器端模式的应用。它提供了通过网络连接进行函数调用的便捷方法。只有程序运行在不同的机器上它才有用。rpc 包建立在 gob参见 12.11 章节 ) 上,将其编码 / 解码,自动转换成可以通过网络调用的方法。

    服务器注册一个对象,通过对象的类型名称暴露这个服务:注册后就可以通过网络或者其他远程客户端的 I/O 连接它的导出方法。这是关于通过网络暴露类型上的方法。

    这个包使用了 http 协议、tcp 协议和用于数据传输的 gob 包。服务器可以注册多个不同类型的对象(服务),但是相同的类型注册多个对象的时候会出错。

    这里我们讨论一个简单的示例: 我们定义一个 Args 类型,并且在它上面创建一个 Multiply 方法,最好封装在一个单独的包中;这个方法必须返回一个可能的错误。

    示例 15.21—rpc_objects.go:

    1. // rpc_objects.go
    2. package rpc_objects
    3. import "net"
    4. type Args struct {
    5. N, M int
    6. }
    7. func (t *Args) Multiply(args *Args, reply *int) net.Error {
    8. *reply = args.N * args.M
    9. return nil
    10. }

    服务器创建一个用于计算的对象,并且将它通过 rpc.Register(object) 注册,调用 HandleHTTP() ,并在一个地址上使用 net.Listen 开始监听。你也可以通过名称注册对象,如:rpc.RegisterName("Calculator", calc)

    译者注: rpc.Register 要求 Multiply 方法的返回值要求是一个 error 类型,所以示例的 net.Error 执行会出错,因此要换成 error 类型(有可能是版本更新造成的,测试使用的 Go 版本为: go1.10.1 )。

    对每一个进入到 listener 的请求,都是由协程去启动一个 http.Serve(listener, nil) ,为每一个传入的 HTTP 连接创建一个新的服务线程。我们必须保证在一个特定的时间内服务器是唤醒状态,例如:time.Sleep(1000e9) (1000 秒)

    示例 15.22—rpc_server.go:

    1. // rpc_server.go
    2. // 原文注释已经被删除,因为和此代码没有关系,个人猜测是作者在这个示例修改之前的代码。
    3. package main
    4. import (
    5. "net/http"
    6. "log"
    7. "net"
    8. "net/rpc"
    9. "time"
    10. "./rpc_objects"
    11. )
    12. func main() {
    13. calc := new(rpc_objects.Args)
    14. rpc.Register(calc)
    15. rpc.HandleHTTP()
    16. listener, e := net.Listen("tcp", "localhost:1234")
    17. if e != nil {
    18. log.Fatal("Starting RPC-server -listen error:", e)
    19. }
    20. go http.Serve(listener, nil)
    21. time.Sleep(1000e9)
    22. }
    23. /* 输出:
    24. 启动程序 E:/Go/GoBoek/code_examples/chapter_14/rpc_server.exe ...
    25. ** after 5 s: **
    26. End Process exit status 0
    27. */

    客户端必须知道服务器端定义的对象的类型和它的方法。它调用 rpc.DialHTTP() 去创建连接的客户端,当客户端被创建时,它可以通过 client.Call("Type. Method", args, &reply) 去调用远程的方法,其中 TypeMethod 是调用的远程服务器端被定义的类型和方法, args 是一个类型的初始化对象,reply 是一个变量,使用前必须要先声明它,它用来存储调用方法的返回结果。

    示例 15.23—rpc_client.go:

    1. // rpc_client.go
    2. // 如果服务器端没有启动:
    3. // 不能启动服务, 所以客户端会立刻停止并报错:
    4. // 2011/08/01 16:08:05 Error dialing:dial tcp :1234:
    5. // The requested address is not valid in its context.
    6. // with serverAddress = localhost:
    7. // 2011/08/01 16:09:23 Error dialing:dial tcp 127.0.0.1:1234:
    8. // No connection could be made because the target machine actively refused it.
    9. package main
    10. import (
    11. "fmt"
    12. "log"
    13. "net/rpc"
    14. "./rpc_objects"
    15. )
    16. const serverAddress = "localhost"
    17. func main() {
    18. client, err := rpc.DialHTTP("tcp", serverAddress + ":1234")
    19. if err != nil {
    20. log.Fatal("Error dialing:", err)
    21. }
    22. // Synchronous call
    23. args := &rpc_objects.Args{7, 8}
    24. var reply int
    25. err = client.Call("Args.Multiply", args, &reply)
    26. if err != nil {
    27. log.Fatal("Args error:", err)
    28. }
    29. fmt.Printf("Args: %d * %d = %d", args.N, args.M, reply)
    30. }
    31. /* 输出结果:
    32. Starting Process E:/Go/GoBoek/code_examples/chapter_14/rpc_client.exe ...
    33. Args: 7 * 8 = 56
    34. End Process exit status 0
    35. */

    这个调用是同步的,所以需要等待结果返回。如果想要异步调用可以这样:

    1. call1 := client.Go("Args.Multiply", args, &reply, nil)
    2. replyCall := <- call1.Done

    如果最后一个参数的值为 nil,在调用完成后将分配一个新的通道。

    如果你有一台用 root 运行的 Go 服务器,并且想以不同的用户去运行你的代码,Brad Fitz 的 go-runas 包使用 rpc 包可以实现:github.com/bradfitz/go-runas 。在第 19 章节 我们将看到在一个完整的项目中的 rpc 应用程序。