# 15.9 用 rpc 实现远程过程调用

Go 程序之间可以使用 `net/rpc` 包实现相互通信，这是另一种客户端-服务器应用场景。它提供了一种方便的途径，通过网络连接调用远程函数。当然，仅当程序运行在不同机器上时，这项技术才实用。`rpc` 包建立在 `gob` 包之上（见 [12.11节](12.11.md)），实现了自动编码/解码传输的跨网络方法调用。

服务器端需要注册一个对象实例，与其类型名一起，使之成为一项可见的服务：它允许远程客户端跨越网络或其他 I/O 连接访问此对象已导出的方法。总之就是在网络上暴露类型的方法。

`rpc` 包使用了 http 和 tcp 协议，以及用于数据传输的 `gob` 包。服务器端可以注册多个不同类型的对象（服务），但同一类型的多个对象会产生错误。

我们讨论一个简单的例子：定义一个类型 `Args` 及其方法 `Multiply`，完美地置于单独的包中。方法必须返回可能的错误。

示例15.21 [rpc_objects.go](examples/chapter_15/rpc/rpc_objects.go)
```go
package rpc_objects

import "net"

type Args struct {
	N, M int
}

func (t *Args) Multiply(args *Args, reply *int) net.Error {
	*reply = args.N * args.M
	return nil
}
```

（**译注：Go 当前版本要求此方法返回类型为 `error`，以上示例中返回 `net.Error` 已无法通过编译，见更新后的[rpc_objects.go](examples/chapter_15/rpc_updated/rpc_objects/rpc_objects.go)。**）

服务器端产生一个 `rpc_objects.Args` 类型的对象 `calc`，并用 `rpc.Register(object)` 注册。调用 `HandleHTTP()`，然后用 `net.Listen` 在指定的地址上启动监听。也可以按名称来注册对象，例如：`rpc.RegisterName("Calculator", calc)`。

以协程启动 `http.Serve(listener, nil)` 后，会为每一个进入 `listener` 的 HTTP 连接创建新的服务线程。我们必须用诸如 `time.Sleep(1000e9)` 来使服务器在一段时间内保持运行状态。

示例 15.22 [rpc_server.go](examples/chapter_15/rpc/rpc_server.go)
```go
package main

import (
	"net/http"
	"log"
	"net"
	"net/rpc"
	"time"
	"./rpc_objects"
)

func main() {
	calc := new(rpc_objects.Args)
	rpc.Register(calc)
	rpc.HandleHTTP()
	listener, e := net.Listen("tcp", "localhost:1234")
	if e != nil {
		log.Fatal("Starting RPC-server -listen error:", e)
	}
	go http.Serve(listener, nil)
	time.Sleep(1000e9)
}
```

输出：

	Starting Process E:/Go/GoBoek/code_examples/chapter_14/rpc_server.exe ...
	** 5 秒后： **
	End Process exit status 0

客户端必须知晓对象类型及其方法的定义。执行 `rpc.DialHTTP()` 连接到服务器后，就可以用 `client.Call("Type.Method", args, &reply)` 调用远程对象的方法。`Type` 是远程对象的类型名，`Method` 是要调用的方法，`args` 是用 Args 类型初始化的对象，`reply` 是一个必须事先声明的变量，方法调用产生的结果将存入其中。

示例 15.23 [rpc_client.go](examples/chapter_15/rpc/rpc_client.go)
```go
package main

import (
	"fmt"
	"log"
	"net/rpc"
	"./rpc_objects"
)

const serverAddress = "localhost"

func main() {
	client, err := rpc.DialHTTP("tcp", serverAddress + ":1234")
	if err != nil {
		log.Fatal("Error dialing:", err)
	}
	// Synchronous call
	args := &rpc_objects.Args{7, 8}
	var reply int
	err = client.Call("Args.Multiply", args, &reply)
	if err != nil {
		log.Fatal("Args error:", err)
	}
	fmt.Printf("Args: %d * %d = %d", args.N, args.M, reply)
}
```

先启动服务器，再运行客户端，然后就能得到如下输出结果：

	Starting Process E:/Go/GoBoek/code_examples/chapter_14/rpc_client.exe ...

	Args: 7 * 8 = 56
	End Process exit status 0

该远程调用以同步方式进行，它会等待服务器返回结果。也可使用如下方式异步地执行调用：
```go
call1 := client.Go("Args.Multiply", args, &reply, nil)
replyCall := <- call1.Done
```

如果最后一个参数值为 `nil` ，调用完成后会创建一个新的通道。

如果你有一个以 root 管理员身份运行的 Go 服务器，想要以不同的用户身份运行某部分代码，Brad Fitz 利用 `rpc` 写的 `go-runas` 包可以完成任务：[https://github.com/bradfitz/go-runas](https://github.com/bradfitz/go-runas)。我们将会在 19 章看到一个完整的项目，它是一个使用了 `rpc` 的应用程序。

## 链接

- [目录](directory.md)
- 上一节：[精巧的多功能网页服务器](15.8.md)
- 下一节：[基于网络的通道 netchan](15.10.md)
