# 版本 5 - 分布式程序

第 5 个版本的代码 *goto_v5*（19.8 节和 19.9 节讨论）见 [goto_v5](examples/chapter_19/goto_v5)。该版本仍然基于 `gob` 存储，但很容易调整为使用 json，正如版本 4 演示的那样。

# 19.8 多服务器处理架构

目前为止 goto 以单线程运行，但即使用协程，在一台机器上运行的单一进程，也只能为一定数量的并发请求提供服务。一个缩短网址服务，相对于 `Add`（用 `Put()` 写入），通常 `Redirect` 服务（用 `Get()` 读取）要多得多。因此我们应该可以创建任意数量的只读的从（slave）服务器，提供服务并缓存 `Get` 方法调用的结果，将 `Put` 请求转发给主（master）服务器，类似如下架构：

![图 19.5 跨越主从计算机的分布式负载](images/19.8_fig19.5.jpg?raw=true)

对于 slave 进程，要在网络上运行 goto 应用的一个 master 节点实例，它们必须能相互通信。Go 的 `rpc` 包为跨越网络发起函数调用提供了便捷的途径。这里将把 `URLStore` 变为 RPC 服务（[15.9 节](15.9.md) 详细讨论了 rpc 包）。slave 进程将应对 `Get` 请求以交付长 URL。当一个长 URL 要被转换为缩短版本（使用 `Put` 方法）时，它们通过 rpc 连接把任务委托给 master 进程，因此只有 master 节点会写入数据文件。

截至目前 `URLStore` 上基本的 `Get()` 和 `Put()` 方法具有如下签名：
```go
func (s *URLStore) Get(key string) string
func (s *URLStore) Put(url string) string
```

而 RPC 调用仅能使用如下形式的方法（t 是 T 类型的值）：
```go
func (t T) Name(args *ArgType, reply *ReplyType) error
```

要使 `URLStore` 成为 RPC 服务，需要修改 `Put` 和 `Get` 方法使它们符合上述函数签名。以下是修改后的签名：
```go
func (s *URLStore) Get(key, url *string) error
func (s *URLStore) Put(url, key *string) error
```

`Get()` 代码变更为：
```go
func (s *URLStore) Get(key, url *string) error {
	s.mu.RLock()
	defer s.mu.RUnlock()
	if u, ok := s.urls[*key]; ok {
		*url = u
		return nil
	}
	return errors.New("key not found")
}
```

现在，键和长 URL 都变成了指针，必须加上前缀 `*` 来取得它们的值，例如 `*key` 这种形式。`u` 是一个值，可以用 `*url = u` 来将其赋值给指针。

接着对 `Put()` 代码做同样的改动：
```go
func (s *URLStore) Put(url, key *string) error {
	for {
		*key = genKey(s.Count())
			if err := s.Set(key, url); err == nil {
			break
		}
	}
	if s.save != nil {
		s.save <- record{*key, *url}
	}
	return nil
}
```

`Put()` 调用 `Set()`，由于后者也要做调整，`key` 和 `url` 参数现在是指针类型，还必须返回 `error` 取代 `boolean`：
```go
func (s *URLStore) Set(key, url *string) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	if _, present := s.urls[*key]; present {
		return errors.New("key already exists")
	}
	s.urls[*key] = *url
	return nil
}
```

同样，当从 `load()` 调用 `Set()` 时，也必须做调整：
```go
s.Set(&r.Key, &r.URL)
```

还必须修改 HTTP 处理函数以适应 `URLStore` 上的更改。`Redirect` 处理函数现在返回 `URLStore` 给出错误的字符串形式：
```go
func Redirect(w http.ResponseWriter, r *http.Request) {
	key := r.URL.Path[1:]
	var url string
	if err := store.Get(&key, &url); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	http.Redirect(w, r, url, http.StatusFound)
}
```

`Add` 处理函数也以基本相同的方式修改：
```go
func Add(w http.ResponseWriter, r *http.Request) {
	url := r.FormValue("url")
	if url == "" {
		fmt.Fprint(w, AddForm)
		return
	}
	var key string
	if err := store.Put(&url, &key); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	fmt.Fprintf(w, "http://%s/%s", *hostname, key)
}
```

要使应用程序更灵活，正如之前章节所为，可以添加一个命令行标志（flag）来决定是否在 `main()` 函数中启用 RPC 服务器：
```go
var rpcEnabled = flag.Bool("rpc", false, "enable RPC server")
```

要使 RPC 工作，还要用 `rpc` 包来注册 `URLStore`，并用 `HandleHTTP` 创建基于 HTTP 的 RPC 处理器：
```go
func main() {
	flag.Parse()
	store = NewURLStore(*dataFile)
	if *rpcEnabled { // flag has been set
		rpc.RegisterName("Store", store)
		rpc.HandleHTTP()
	}
	... (set up http like before)
}
```

## 链接

- [目录](directory.md)
- 上一节：[以 json 格式存储](19.7.md)
- 下一节：[使用代理缓存](19.9.md)
