现在,我们已经有了可以用于 RPC 服务的 URLStore , 我们可以构建另一种类型来代表 RPC 客户端,并将发送请求到 RPC 服务器端;我们叫它 ProxyStore :
type ProxyStore struct {
client *rpc.Client
}
一个 RPC 客户端必须使用 DialHTTP()
方法去连接一个 RPC 服务器端,所以我们将它合并到我们的 ProxyStore 对象的 NewProxyStore 函数中:
func NewProxyStore(addr string) *ProxyStore {
client, err := rpc.DialHTTP("tcp", addr)
if err != nil {
log.Println("Error constructing ProxyStore:", err)
}
return &ProxyStore{client: client}
}
这个 ProxyStore 有 Get 和 Put 方法,可以在 RPC 客户端调用这些方法将请求直接传递给 RPC 服务器端:
func (s *ProxyStore) Get(key, url *string) error {
return s.client.Call("Store.Get", key, url)
}
func (s *ProxyStore) Put(url, key *string) error {
return s.client.Call("Store.Put", url, key)
}
缓存 ProxyStore :
但是,如果从服务器只是简单的将工作委托给主服务器,这样做没有任何意义! 我们希望从服务要去处理 Get 请求。为了做到这一点,从服务器必须有一个带有 map 的 URLStore 的副本(一个缓存)。所以我们扩展下 ProxyStore ,在它里面定义一个 URLStore :
type ProxyStore struct {
urls *URLStore
client *rpc.Client
}
并且 NewProxyStore 也必须被更改:
func NewProxyStore(addr string) *ProxyStore {
client, err := rpc.DialHTTP("tcp", addr)
if err != nil {
log.Println("ProxyStore:", err)
}
return &ProxyStore{urls: NewURLStore(""), client: client}
}
我们必须修改 URLStore ,以便在给他一个空的 filename 时不去尝试写入或读取磁盘:
func NewURLStore(filename string) *URLStore {
s := &URLStore{urls: make(map[string]string)}
if filename != "" {
s.save = make(chan record, saveQueueLength)
if err := s.load(filename); err != nil {
log.Println("Error loading URLStore: ", err)
}
go s.saveLoop(filename)
}
return s
}
我们的 Get 方法需要去扩展: 它应该首先检查缓存中是否有 key 。如果有, Get 返回缓存中的结果。如果没有,它应该进行 RPC 调用,并将结果更新到它的本地缓存:
func (s *ProxyStore) Get(key, url *string) error {
if err := s.urls.Get(key, url); err == nil { // 在本地 map 中找到 url
return nil
}
// 本地 map 中没有找到 url ,运行 RPC 调用:
if err := s.client.Call("Store.Get", key, url); err != nil {
return err
}
s.urls.Set(key, url)
return nil
}
同样的, Put 方法在成功执行 RPC 调用 Put 之后,只需要更新本地缓存:
func (s *ProxyStore) Put(url, key *string) error {
if err := s.client.Call("Store.Put", url, key); err != nil {
return err
}
s.urls.Set(key, url)
return nil
}
总结一下: 所有的从服务器都使用 ProxyStore , 只有主服务器使用 URLStore 。但是我们创建它们的方式看起来非常相似: 它们都实现了使用相同签名的 Get 和 Put 方法,所以我们能定义一个接口 Store 来归纳它们的行为:
type Store interface {
Put(url, key *string) error
Get(key, url *string) error
}
现在我们的全局变量 store 的类型可以是 Store 类型: var store Store
最终我们调整我们的 main () 函数,以便启动一个从服务器或者一个主服务器(并且我们只能这样,因为 store 现在是一个 Store 类型的接口!)。
为此,我们添加一个新的命令行标志 masterAddr ,它没有默认值(也就是空字符串)。
var masterAddr = flag.String("master", "", "RPC master address")
如果给出一个主服务器地址,我们启动一个从服务器进程,并且创建一个新的 ProxyStore;否则,我们启动一个主服务器进程并且创建一个新的 URLStore :
func main() {
flag.Parse()
if *masterAddr != "" { // 如果主服务器地址不为空,我们是一个从服务器
store = NewProxyStore(*masterAddr)
} else {
// 我们是主服务器
store = NewURLStore(*dataFile)
}
...
}
通过这种方式,我们启动了 ProxyStore 来代替 URLStore 去使用 web 前端。
其余的前端代码会像以前一样执行,它不需要去了解 Store 接口。将只有主服务器可以向数据文件写入数据。
现在,我们可以启动一个主服务器和多个从服务器,并且对这些从服务器进行压力测试。
编译这个第 5 版的代码或者使用现有的可执行文件(译者注:示例代码中没有,自己编译执行)。
要测试它,首先要在命令行下启动主服务器:
./goto -http=:8081 -rpc=true
(or goto replacing ./goto on Windows)
指定了两个参数: 在端口 8081 上的主服务器的监听地址、启用 RPC 。
启动一个从服务器: ./goto -master=127.0.0.1:8081
它收到了主服务器的地址,并将在 8080 端口上接收客户端请求。
在示例代码中包含了下面这个 shell 脚本: demo.sh ,它可以像 Unix 系统一样自动启动。
#!/bin/sh
gomake
./goto -http=:8081 -rpc=true &
master_pid=$!
sleep 1
./goto -master=127.0.0.1:8081 &
slave_pid=$!
echo "Running master on :8081, slave on :8080."
echo "Visit: http://localhost:8080/add"
echo "Press enter to shut down"
read
kill $master_pid
kill $slave_pid
要在 Windows 下测试, 启动一个 MINGW shell 并启动主服务器,然后每一个从服务器启动一个新的 MINGW shell 并启动从服务器的进程。