现在,我们已经有了可以用于 RPC 服务的 URLStore , 我们可以构建另一种类型来代表 RPC 客户端,并将发送请求到 RPC 服务器端;我们叫它 ProxyStore :

    1. type ProxyStore struct {
    2. client *rpc.Client
    3. }

    一个 RPC 客户端必须使用 DialHTTP() 方法去连接一个 RPC 服务器端,所以我们将它合并到我们的 ProxyStore 对象的 NewProxyStore 函数中:

    1. func NewProxyStore(addr string) *ProxyStore {
    2. client, err := rpc.DialHTTP("tcp", addr)
    3. if err != nil {
    4. log.Println("Error constructing ProxyStore:", err)
    5. }
    6. return &ProxyStore{client: client}
    7. }

    这个 ProxyStore 有 Get 和 Put 方法,可以在 RPC 客户端调用这些方法将请求直接传递给 RPC 服务器端:

    1. func (s *ProxyStore) Get(key, url *string) error {
    2. return s.client.Call("Store.Get", key, url)
    3. }
    4. func (s *ProxyStore) Put(url, key *string) error {
    5. return s.client.Call("Store.Put", url, key)
    6. }

    缓存 ProxyStore :

    但是,如果从服务器只是简单的将工作委托给主服务器,这样做没有任何意义! 我们希望从服务要去处理 Get 请求。为了做到这一点,从服务器必须有一个带有 map 的 URLStore 的副本(一个缓存)。所以我们扩展下 ProxyStore ,在它里面定义一个 URLStore :

    1. type ProxyStore struct {
    2. urls *URLStore
    3. client *rpc.Client
    4. }

    并且 NewProxyStore 也必须被更改:

    1. func NewProxyStore(addr string) *ProxyStore {
    2. client, err := rpc.DialHTTP("tcp", addr)
    3. if err != nil {
    4. log.Println("ProxyStore:", err)
    5. }
    6. return &ProxyStore{urls: NewURLStore(""), client: client}
    7. }

    我们必须修改 URLStore ,以便在给他一个空的 filename 时不去尝试写入或读取磁盘:

    1. func NewURLStore(filename string) *URLStore {
    2. s := &URLStore{urls: make(map[string]string)}
    3. if filename != "" {
    4. s.save = make(chan record, saveQueueLength)
    5. if err := s.load(filename); err != nil {
    6. log.Println("Error loading URLStore: ", err)
    7. }
    8. go s.saveLoop(filename)
    9. }
    10. return s
    11. }

    我们的 Get 方法需要去扩展: 它应该首先检查缓存中是否有 key 。如果有, Get 返回缓存中的结果。如果没有,它应该进行 RPC 调用,并将结果更新到它的本地缓存:

    1. func (s *ProxyStore) Get(key, url *string) error {
    2. if err := s.urls.Get(key, url); err == nil { // 在本地 map 中找到 url
    3. return nil
    4. }
    5. // 本地 map 中没有找到 url ,运行 RPC 调用:
    6. if err := s.client.Call("Store.Get", key, url); err != nil {
    7. return err
    8. }
    9. s.urls.Set(key, url)
    10. return nil
    11. }

    同样的, Put 方法在成功执行 RPC 调用 Put 之后,只需要更新本地缓存:

    1. func (s *ProxyStore) Put(url, key *string) error {
    2. if err := s.client.Call("Store.Put", url, key); err != nil {
    3. return err
    4. }
    5. s.urls.Set(key, url)
    6. return nil
    7. }

    总结一下: 所有的从服务器都使用 ProxyStore , 只有主服务器使用 URLStore 。但是我们创建它们的方式看起来非常相似: 它们都实现了使用相同签名的 Get 和 Put 方法,所以我们能定义一个接口 Store 来归纳它们的行为:

    1. type Store interface {
    2. Put(url, key *string) error
    3. Get(key, url *string) error
    4. }

    现在我们的全局变量 store 的类型可以是 Store 类型: var store Store

    最终我们调整我们的 main () 函数,以便启动一个从服务器或者一个主服务器(并且我们只能这样,因为 store 现在是一个 Store 类型的接口!)。

    为此,我们添加一个新的命令行标志 masterAddr ,它没有默认值(也就是空字符串)。

    1. var masterAddr = flag.String("master", "", "RPC master address")

    如果给出一个主服务器地址,我们启动一个从服务器进程,并且创建一个新的 ProxyStore;否则,我们启动一个主服务器进程并且创建一个新的 URLStore :

    1. func main() {
    2. flag.Parse()
    3. if *masterAddr != "" { // 如果主服务器地址不为空,我们是一个从服务器
    4. store = NewProxyStore(*masterAddr)
    5. } else {
    6. // 我们是主服务器
    7. store = NewURLStore(*dataFile)
    8. }
    9. ...
    10. }

    通过这种方式,我们启动了 ProxyStore 来代替 URLStore 去使用 web 前端。

    其余的前端代码会像以前一样执行,它不需要去了解 Store 接口。将只有主服务器可以向数据文件写入数据。

    现在,我们可以启动一个主服务器和多个从服务器,并且对这些从服务器进行压力测试。

    编译这个第 5 版的代码或者使用现有的可执行文件(译者注:示例代码中没有,自己编译执行)。

    要测试它,首先要在命令行下启动主服务器:

    1. ./goto -http=:8081 -rpc=true
    2. (or goto replacing ./goto on Windows)

    指定了两个参数: 在端口 8081 上的主服务器的监听地址、启用 RPC 。

    启动一个从服务器: ./goto -master=127.0.0.1:8081

    它收到了主服务器的地址,并将在 8080 端口上接收客户端请求。

    在示例代码中包含了下面这个 shell 脚本: demo.sh ,它可以像 Unix 系统一样自动启动。

    1. #!/bin/sh
    2. gomake
    3. ./goto -http=:8081 -rpc=true &
    4. master_pid=$!
    5. sleep 1
    6. ./goto -master=127.0.0.1:8081 &
    7. slave_pid=$!
    8. echo "Running master on :8081, slave on :8080."
    9. echo "Visit: http://localhost:8080/add"
    10. echo "Press enter to shut down"
    11. read
    12. kill $master_pid
    13. kill $slave_pid

    要在 Windows 下测试, 启动一个 MINGW shell 并启动主服务器,然后每一个从服务器启动一个新的 MINGW shell 并启动从服务器的进程。