通向Golang的捷径【19. 创建一个完整应用】

19.1 介绍

本章将开发一个完整的应用 goto, 它是一个可上线的 web 应用, 来自于 Andrew Gerrand 的讲座, 这里将分三个阶段, 每个阶段都会追加一些功能, 以便展现 Go 语言的更多特性, 它比第 15 章给出的 web 应用更加复杂.

• 第 1 版: 会使用一个 map 和一个结构, 以及 sync 包的 Mutex 和一个结构类型的创建工厂.
• 第 2 版: 可将 gob 格式的数据, 写入文件中.
• 第 3 版: 将使用并发协程和并发通道, 重新编写之前的应用
• 第 4 版: 将给出 json 格式的应用
• 第 5 版: 将使用 rpc 协议, 实现一个分布式应用

19.2 应用项目的介绍

在浏览器中, 可输入一些复杂的 url 地址, 而 web 服务功能可将这些复杂地址, 替换成简单的 url 地址, 我们的项目类似于一个 web 服务, 它包含了两个功能:

• 将超长 url 地址转换成简单地址, 比如:
http://maps.google.com/maps?f=q&source=s_q&hl=en&geocode=&q=tokyo&sll=37.0625,-95.677068
&sspn=68.684234,65.566406&ie=UTF8&hq=&hnear=Tokyo,+Japan&t=h&z=9
可转换成http://goto/UrcGq,并存储 url 地址中包含的数据

• 如果用户请求了上述的简单地址,web 服务可将简单地址, 重定向到复杂的原始 url 地址, 也就是在浏览
器中输入 B 地址,web 服务可重定向到 A 地址.

19.3 数据结构

以下将给出项目的第 1 版本,19.3 和 19.4 节将讨论这一版本, 可在随书代码中找到 goto_v1.

当项目上线时, 将会收到大量简单 url 地址的请求, 而其中一些请求, 是由复杂 url 地址转换而来, 因此需要将简单地址和复杂地址都保存起来, 因为这两类地址都是字符串, 并且相互关联, 可将简单地址视为 key, 而复制地址可视为 value, 所以能将它们保存在一个 map 中, 而几乎所有的编程语言都给出了相似的类型, 比如哈希表, 字典等. 在 Go 语言中, 可使用内建的 map 类型, 即map[string]string,[] 中是 key 类型, 随后是value 类型, 第 8 章已详细介绍了 map, 在实际应用中, 需要为 map 指定一个特殊的类型名, 比如type URLStore map[string]string, 它可将 url 长地址转换为 url 短地址.

之后可创建上述类型的变量 m,m := make(URLStore). 假定在 m 中, 需要将 http://google.com/转换成http://goto/a,可使用以下语句:
在这里插入图片描述
同时可将http://goto/前缀也配置成一个 key, 这可节省一些空间, 因为前缀都是一样的, 无须重复保存, 所以使用 a 可获取 url 长地址,url := m[”a”].

注意, 使用:= 赋值, 则无须将 url 定义为 string 类型, 因为编译器可基于赋值的右侧值, 推断出 url 的类型.

提供线程的安全性

URLStore 变量可视为一个存储区, 当获得一些传输数据, 大量 Redirect 类型的请求, 这时只能提供读取操作,使用 url 短地址作为 key, 获取 url 长地址, 但 Add 类型的请求不同, 它可在 URLStore 变量中, 增加一组键值对, 如果同一时刻下, 出现大量的 Add 更新请求, 将产生问题,add 请求的处理能被同类型的其他请求所中断,因此 url 长地址 (value) 可能没有足够时间来写入, 由于读取操作和修改操作同时进行, 读取结果也可能出现错误,map 类型也无法保证在下一次更新之前, 当前的更新操作可以完成, 所以 map 变量将被多个并发请求所访问, 但它无法为线程提供安全性, 因此必须保证 URLStore(map) 类型, 能被线程安全访问, 而最简单和最经典的方式则是加锁, 在 Go 语言的 sync 标准包中, 提供了 Mutex 类型, 参见 9.3 节.

也就是将 URLStore 类型设定为结构类型, 其中将包含两个数据域, 一是 map, 二是来自于 sync 包的 RWMutex:
在这里插入图片描述
一个 RWMutex 类型可包含两个锁, 一个用于读取, 另一个用于写入, 多个客户端可同时获取读取锁, 但只有一个客户端可获取到写入锁 (其实可忽略读取锁), 这使得上述的更新操作可实现高效串行, 并实现连续处理.

在 Get 函数中, 实现了 Redirect 类型的读取请求, 而在 Set 函数中, 实现 Add 类型的写入请求, 如下:
在这里插入图片描述
Get 函数可使用 url 短地址 (key), 可从 map 中获取到 url 长地址 (value), 同时它从属于 URLStore 类型, 进行读取之前, 可使用语句 s.mu.RLock(), 获取读取锁, 因此更新操作将无法中断读取操作, 当读取完毕后, 可使用语句 s.mu.RUnlock(), 释放读取锁, 同时挂起的更新操作可恢复运行, 如果 key 未包含在 map 中, 将返回字符串类型的默认值 (空串), 注意, 这里的点操作符与 OO 语言很相似, 所以 s.mu.RLock() 意味着, 调用 s 数据域 mu 的方法 RLock().

Set 函数包含了两个形参, 一个是 key, 另一个是 url 地址, 进行更新操作之前, 将获取写入锁 (Lock), 因此其他更新将无法中断当前更新, 该函数还可返回一个布尔值, 用于描述更新是否成功:
在这里插入图片描述
使用语句_, present := s.urls[key], 可测试 map 中是否包含了 key, 如果包含,present 将为 true, 否则将为 false, 以上即为 comma, ok 格式, 如果 key 包含在 map 中,Set 函数将返回 false, 这时 map 并未更新, 因为函数提前返回了 (因为不允许重复使用同一个 url 地址), 如果 key 未包含在 map 中, 将被添加到 map 中,Set 函数将返回 true, 左侧的 _ 是一个占位符, 且表示该数值不会使用, 注意一下更新完成后的 Unlock() 函数.

使用 defer

以上给出的代码相当简单, 但容易忘记使用 Unlock(), 而在更复杂的代码中, 更容易忘记 Unlock(), 或是将其放置在错误的地方, 这将引发难以跟踪的错误, 基于以上原因,Go 语言提供了一个特殊的关键字 defer(参见 6.4节), 以便在锁定操作之后, 自动进行解锁, 也就是在函数退出之前, 自动调用 Unlock(), 如下:
在这里插入图片描述
在 Set 函数中也可使用 defer, 因此无须再考虑解锁的问题:
在这里插入图片描述

创建 URLStore 变量的工厂函数

在 URLStore 结构中, 包含了一个 map 数据域, 但在使用前, 必须进行初始化, 因此可定义一个包含 New 前缀的函数, 以创建 URLStore 结构的实例, 同时该函数还能返回已创建的 URLStore 实例 (在大多数情况下, 该实例会是一个指针),
在这里插入图片描述
在返回的 URLStore 实例中, 将提供已初始化的 map 数据域, 但锁定标志无须初始化, 这也是 Go 语言中创建结构的标准方法, 同时可使用取地址操作符 &, 返回一个结构指针, 即 *URLStore, 使用语句var s = NewURLStore(), 可创建一个 URLStore 变量 s.

URLStore 变量的用法

为了在 map 中添加长短 url 地址, 需调用 s 的 Set 方法, 该方法可返回一个布尔值, 因此它将封闭在一个 if 条件中,
在这里插入图片描述
为了基于 url 短地址, 获取 url 长地址, 可调用 s 的 Get 方法, 并可将 url 长地址返回给 url 变量,
在这里插入图片描述
在 Go 语言的实际编程中, 在 if 条件中, 通常会给出一条初始化语句, 同时还需要对 map 包含的键值对进行计数, 所以需要一个 Count(计数) 方法:
在这里插入图片描述
在这里插入图片描述
如果需要基于 url 长地址, 而获取 url 短地址, 则需要另一个函数 genKey(n int) string {…}, 其中的 n 即为
s.Count() 的当前值.

以下将创建一个 Put 方法, 它将包含一个 url 长地址, 并在函数中, 使用 genKey 生成一个 key, 之后可使用Set 方法, 保存已生成的 key 和 url 长地址, 再返回 key,
在这里插入图片描述
在 for 循环中,Set 方法可运行成功, 这意味着生成的 key 并未包含在 map 中, 之后可定义一个存储区和存储函数, 以实现 map 的保存 (参见 store.go 文件), 当然目前并不需要这类功能, 只需定义一个 web 服务器, 已提供 Add 和 Redirect 服务.

19.4 用户接口: 前端 web 服务器

以下代码可参见goto_v1\main.go 文件, 所有 Go 项目都会包含一个主函数 main(), 这与 C,C++ 和 Java 语言相同, 使用语句 http.ListenAndServe(”:8080”, nil), 可在 8080 端口上, 启动一个本地 web 服务器. 在第 15章中已经详细介绍了 http 包, 它可为 web 服务器提供所需的功能, 这时 web 服务器将在一个死循环中, 监听来自外部的请求, 但是在 web 服务器中, 必须定义外部请求的响应, 也就是调用 http 处理器 (HandleFunc 函数),
在这里插入图片描述
因此基于/add 路径的所有请求, 将调用 Add 函数, 在当前项目中, 将给出两个 http 处理器:
• Redirect, 可实现 url 短地址请求的重定向
• Add, 可实现新 url 地址的保存
在这里插入图片描述
以下给出了最小化的主函数 main(),
在这里插入图片描述
基于/add 路径的请求, 将交给 Add 处理器, 而其他请求将交给 Redirect 处理器, 处理器函数可从外部请求(*http.Request 类型的变量) 中获取信息, 并会创建一个 http.ResponseWriter 类型的变量 w, 同时会将外部请求, 写入该变量.

在 Add 函数中, 可实现以下操作:
• 读取 url 长地址, 也就是从 r.FormValue(”url”) 包含的 http 请求中, 读取一个 html 格式的 url 地址
• 使用 Put 方法, 保存已读取的 url 长地址
• 将基于 url 长地址所转换的 url 短地址, 发送给用户

每个请求都将实现长短 url 地址的转换, 如下:
在这里插入图片描述
使用 Fprintf(来自 fmt 包) 函数, 可打印出与 url 长地址对应的 key(map), 而 key 也将回传给客户端, 注意
Fprintf 还可实现对 ResponseWriter 类型变量的写入, 事实上 Fprintf 可基于 io.Writer(将在 Write 方法中调用该函数), 实现对结构变量的写入, 而 io.Writer() 将调用一个接口, 因此基于接口的使用,Fprintf 可变成一个通用函数, 可处理不同类型的数据, 在 Go 语言中广泛使用了接口, 它可使代码更加通用, 参见第 11 章.

同时 Fprintf 还可显示一个 html 表单, 并能将其写入到 w 中, 因此如果请求中未包含 url 地址, Add 函数还需显示出 html 表单:
在这里插入图片描述
在上述功能中, 需要将常量字符串 AddForm 发送给客户端, 其实 AddForm 就是一个 html 表单, 其中包含了一个 url 数据域, 以及一个 submit 按钮, 并能在客户端的浏览器中显示, 如果在浏览器中, 点击 submit, 可发送一个基于/add 路径的请求, 这时 Add 处理器将再次被调用, 同时 url 数据域的 text 子域 (包含地址) 将传递给 web 服务器 (’’ 可封闭一个字符串流, 而其他字符串通常会封闭在”” 中).

Redirect 函数可获取 http 请求中包含的 key(而请求路径中包含的 url 短地址, 可移除首字符, 因此在 Go 语言中会使用 [1:], 比如请求路径为/abc, 那么 key 则为 abc), 那么使用 Get 函数, 可获取 url 长地址, 同时会将重定向的 http 地址, 返回给用户, 如果 url 无法找到, 则会发送一个 404(url 无法找到) 错误.
在这里插入图片描述
http.NotFound 和 http.Redirect 可用于发送通用的 http 响应.

编译和运行

由于在随书源码包中, 给出了已编译的可执行文件, 因此可以跳过编译, 直接测试可执行文件, 而上述的三个Go 文件将包含在一个 Makefile 中, 之后可使用 Makefle, 实现可执行文件的编译和链接.

在 Linux 和 OS X 系统中, 可在一个控制台窗口中输入 make, 或是在 LiteIDE 中创建一个项目, 生成可执行文件, 在 Windows 系统中, 可启动 MINGW 环境, 并使用 MINGW Shell 生成可执行文件, 因此可选择控制台窗口, 输入 make 并回车, 可得以下消息:
在这里插入图片描述
完成编译和链接后, 在 Linux/OS X 系统中, 将得到一个 goto 程序, 在 Windows 系统中, 将得到一个 goto.exe程序.

使用以下方式, 可运行 web 服务器,
• 在 Linux/OS X 系统中, 可输入命令./goto
• 在 Windows 系统中, 可从 GoIDE 中, 启动 goto.exe(如果防火墙阻碍了程序的运行, 请关闭防火墙)

程序测试

打开浏览器, 并请求 url 地址http://localhost:8080/add, 这调用 web 服务器的 Add 处理器函数, 由于在 html表单中未提供 url 变量, 因此 web 服务器将发送另一个 html 表单给浏览器, 以询问所需的信息.
在这里插入图片描述
在表单的文本框中, 可输入一个 url 长地址 (需转换成 url 短地址), 比如 http://golang.org/pkg/bufio/#Writer,并点击 Add 按钮, 之后 web 服务器将回传一个 url 短地址给浏览器, 比如http://localhost:8080/2.
在这里插入图片描述
将显示的 url 短地址, 复制到浏览器的地址栏中, 并请求该地址, 这时 web 服务器的 Redirect 处理器将被调用, 并回传 url 长地址的页面信息.
在这里插入图片描述

19.5 god 的连续存储

以下将给出本项目的第 2 个版本, 参见随书源码包的 goto_v2 目录.

当 goto 程序退出, 内存中保存的长短 url 地址的转换, 将被丢弃, 为了防止 map 中存储数据的丢失, 必须将这些数据, 保存在一个磁盘文件中, 当完成 URLStore 变量的修改后, 可将对应数据, 写入文件, 同时在 goto 程序启动后, 可将文件中保存的数据, 重新写入到 map 中, 这些操作都需要使用 encoding/gob 包, 它可实现结构与数组 (更准确地说, 应该是 slice) 之间的串行和并行的数据交换, 参见 12.11 节.

gob 包的 NewEncoder 和 NewDecoder 函数, 可实现数据的读写, 而 Encode 和 Decode 方法则提供了 Encoder和 Decoder 对象, 可完成结构与文件之间的读写, 即 Encoder 实现了 Writer(写入器) 接口, Decoder 实现了Reader(读取器) 接口, 所以需要在 URLStore 结构中, 增加一个新的数据域 (*os.File 类型), 它可获取一个文件句柄, 以实现文件的读写.
在这里插入图片描述
之后在创建 URLStore 实例时, 可传入一个文件名 store.gob,
在这里插入图片描述
因此还需要对 NewURLStore 函数进行修改,
在这里插入图片描述
函数包含了一个文件名形参, 在函数中, 可打开该文件, 并关联到 URLStore 变量的 file 数据域, 如果 OpenFile调用出错 (比如磁盘文件被删除), 将返回一个错误 err.

如果 err 并非 nil 值, 则表明出现了一个错误, 这时需终止程序, 并发送一条警告消息, 在一般情况下, 都需要对函数返回的错误码进行检查, 但可以使用之前给出的错误检查模式, 同时上述函数可打开一个文件.

在开启文件时, 给出了可写模式, 同时也给出了附加 (append) 模式, 当程序生成了一对新的 URL(长短) 地址时, 就可通过 gob 包的功能, 将 URL 地址写入文件, 因此需要定义一个新结构, 用于 URL 地址的存储.
在这里插入图片描述
另外还需要提供一个 save 方法, 已将 URL 地址保存到文件, 这时可使用 gob 的编码功能, 进行保存:
在这里插入图片描述
在 goto 程序启动时, 还需要从文件中, 将保存的 URL 地址, 读取到 URLStore 变量的 map 中, 因此还需要提供一个载入方法 load:
在这里插入图片描述
load 方法可跳转到文件开头, 并使用 Decode 函数, 读取每对 URL 地址, 之后使用 Set 方法, 将 URL 地址保存到 map 中, 从上可见 load 方法中遍布了多个错误处理, 如果未出现错误, load 方法将在一个死循环中, 遍历整个文件,
在这里插入图片描述
如果所有的 URL 地址都读取完毕, 并出现文件末尾时, 将产生一个 io.EOF 错误, 如果错误并未 io.EOF 错误, 则会停止文件读取, 并返回该错误, 同时 load 方法必须加入到 NewURLStore 方法中,
在这里插入图片描述
Put 函数可在一对新的 URL 地址写入 map 时, 并将这对 URL 地址也保存到文件中,
在这里插入图片描述
在这里插入图片描述
编译和测试本项目的第 2 个版本, 或是直接使用已编译的可执行文件, 都能验证, 即使 web 服务器终止, 也能获取到 url 短地址 (在控制台窗口中, 可使用 Ctrl+C, 终止一个进程), 如果数据文件 store.gob 不存在, 当 goto第一次启动时, 将给出一个错误消息,
在这里插入图片描述
这时停止 goto 进程, 并再次启动时, 将发现一个空的 store.gob, 因为在第一次启动时, 创建了该文件, 在 goto第 2 次启动时, 也可能会出现以下错误,
在这里插入图片描述
这是因为 gob 是一个基于数据流的协议, 并不支持重新启动, 在本项目的第 4 个版本中, 可使用 json 存储格式, 来替代 gob, 以避免上述问题的发生.

19.6 使用并发协程

以下将实现本项目的第 3 个版本, 可在随书源码包中找到 goto_3 目录, 以下的源码都能在该目录下找到.

当大量客户端同步添加 URL(长短) 地址 (即请求 add 页面) 时, 本项目的第 2 个版本将出现性能问题, 基于加锁机制,map 可在并发访问中, 实现安全更新, 而将每对新地址写入磁盘文件的操作将成为处理瓶颈, 因为磁盘的同步写入, 则依赖于 OS 的特性, 这会引发一些性能损失, 即使写入操作之间不会发生冲突, 每个客户端也必须等待数据写入磁盘文件, 以使 Put 函数可返回, 因此这是一个任务繁重的 IO 负载系统, 客户端发出 Add请求后, 必须处于长时间的等待状态.

为了解决上述问题, 必须分离 Put 操作和 Save 操作, 以利用 Go 语言的并发机制, 为了替代磁盘文件的直接写入, 可将数据写入到 channel 中, 因为并发通道可包含缓冲, 所以发送函数无须等待.

在 Save 函数中, 可将并发通道中读取数据, 写入到磁盘文件中, 这类操作可放置一个独立的线程中 (即并发协程 saveLoop),main 程序和 saveLoop 可并发执行, 同时不会出现相互阻塞, 因此在 URLStore 结构中, 将使用channel 类型来替换 file 类型, 如下:
在这里插入图片描述
channel 类型与 map 类型很相似, 也必须使用 make 进行创建, 所以在 NewURLStore 方法中, 必须创建一个缓冲长度为 1000 的并发通道, 即 save := make(chan record,saveQueueLength), 同时还需要创建一个函数, 以将每对 URL 地址保存到磁盘文件, 同时 Put 函数中, 只需将每对 URL 地址, 发送给并发通道 save,
在这里插入图片描述
在并发通道 save 的另一端, 必须提供一个接收器, 因此将定义一个新方法 saveLoop(它可运行在一个独立的并发协程中), 它可从并发通道 save 中读取数据, 并将数据写入文件, 在 NewURLStore 方法中, 将启动saveLoop, 并给出了关键字 go, 这表明它将在一个并发协程中运行, 同时可移除文件开启代码, 以下将给出已修改的 NewURLStore 方法,
在这里插入图片描述
以下是 saveLoop 方法的代码,
在这里插入图片描述
上述方法将在一个死循环中, 从并发通道 save 中读取数据, 并将读取数据写入到文件中.

在第 14 章中, 已经深入学习了并发协程和并发通道, 但在本节中, 将提供一个更好的程序管理的示例, 这里所使用的加密 (Encoder), 是为了节约内存和进程的资源.

增加 goto 程序灵活性的另一种方式, 是放弃文件名, 监听器地址和主机名的硬编码, 使其变成一个常量, 也就是将其定义成 flag, 如果需要在程序中改变这些数值时, 可在命令行中输入, 当未给出对应的输入时,flag 还可提供一个默认值, 只是在编码过程中, 需要导入 flag 包, 即import ”flag”, 参见 12.4 节.

因此可在 flag 中, 创建一些全局变量, 如下:
在这里插入图片描述
为了实现命令行参数的解析, 必须在 main 函数中, 加入 flag.Parse(), 当命令行参数完成解析后, 可进行 URLStore 类型的初始化, 经过解析后, 可知 dataFile 的数值 (以下代码中使用了 *dataFile, 由于 flag 是一个指针, 因此它可反向获取到指针指向的数值, 参见 4.9 节).
在这里插入图片描述在这里插入图片描述
在 Add 处理器中, 可使用 *hostname 替换localhost:8080,
在这里插入图片描述
编译和测试本项目的第 3 个版本, 或是直接使用已生成的可执行文件.

19.7 json 格式

以下将给出本项目的第 4 个版本, 在随书源码包的 goto_v4 目录中, 可找到以下代码.

如果你是一个敏锐的测试者, 可能会注意到 goto 第 2 次启动时所出现问题, 如果 goto 第 2 次启动时, 给
出了一个 url 短地址, 则可正常执行, 而在第 3 次启动时, 将出现一个错误: Error loading URLStore: ex-
tra data in buffer, 这是因为 gob 包采用了一种基于数据流的协议, 所以并不支持重新启动, 为了解决这个问题, 应选择 json 格式的存储协议 (参见 12.9 节), 它可将数据保存在一个文本中, 同时该格式也能与其他编程语言所共享, 由于存储操作被分离到两个方法 (load 和 saveLoop) 中, 所以变更存储协议也很简单.

首先需要创建一个空文件 store.json, 并在 main 函数中, 修改 dataFile 变量的声明:
在这里插入图片描述
在 store.go 文件中, 需导入 json 包, 之后在 saveLoop 方法中, 修改一行代码, 如下:
在这里插入图片描述
同样在 load 方法, 也需要修改一行代码,
在这里插入图片描述
其他语句无须修改, 之后可编译和测试可执行文件 goto, 之前的错误不会再出现.

19.8 多机通讯

以下将给出本项目的第 5 个版本, 在随书源码包的 goto_5 目录中, 可找到以下代码, 在当前版本中, 仍会使用gob 存储.

goto 程序可在单个进程中运行 (该进程将在一台主机中运行), 该进程可生成多个并发协程, 以服务 (处理) 多个并发的 web 请求, 同时 URL 短地址可获取到重定向服务 (Redirects, 可使用 Get), 之前的添加服务 (Add,可使用 Put) 将记录该 URL 短地址, 因此我们可创建任意数量的客户端 slave(可发送 Get 请求), 同时也可发送 Put 请求给 web 服务器 master, 如下图:
在这里插入图片描述
在网络中, 只会运行一个服务器应用, 但能运行多个客户端应用, 因此服务器和客户端之间, 可实现多机通讯,rpc 包提供一种机制, 可通过网络连接, 实现函数的调用, 因此可使 URLStore 类型给出一个 rpc 服务 (参见 15.9 节), 那么在客户端进程中, 处理 Get 请求时, 也可获取到 URL 长地址 (可调用服务器的函数, 并得到返回结果), 当一个新的 URL 长地址需要转换成短地址时, 可通过 rpc 连接, 将 URL 长短地址的转换任务, 委托给服务器, 因为只有服务器才可写入数据文件.

URLStore 类型中, 原有的 Get 和 Put 方法的原型如下:
在这里插入图片描述
在这里插入图片描述
同时还需要修改 Get 方法,
在这里插入图片描述
由于 key 和 url 变量都是指针, 因此必须给出前缀 *, 比如*key, u 是一个数值, 如果将其分配给一个指针变量, 应写为*url = u. 同时也需要修改 Put 方法,
在这里插入图片描述
由于 Put 需调用 Set 方法, 因此 key 和 url 包含的数值应当匹配, 否则将返回一个错误码, 而不是一个布尔值.
在这里插入图片描述
在这里插入图片描述
同时还需要修改 http 处理器, 以适应 URLStore 类型的变化, 比如 Redirect 处理器应返回一个错误消息 (字符串类型).
在这里插入图片描述
Add 处理器也需要修改,
在这里插入图片描述
在这里插入图片描述
为了让应用程序更加灵活, 还可增加一个命令行参数 (使能 rpc 功能),
在这里插入图片描述
为了实现 rpc 的正常工作, 还需要在 rpc 包中注册 URLStore 类型, 并配置一个基于 http 连接的 rpc 处理器, 即 HandleHTTP, 如下:
在这里插入图片描述

19.9 ProxyStore

在当前项目中, 加入了 rpc 服务, 因此需创建另一种类型 ProxyStore, 用于描述来自于客户端的 rpc 请求,
在这里插入图片描述
在这里插入图片描述

ProxyStore 缓存

如果客户端将所有任务都委托给服务器, 这并不是一种有效的方式, 因此客户端自身可处理 Get 请求, 为了实现该功能, 必须定义一个 URLStore 类型的副本, 也就是在 ProxyStore 结构中包含 URLStore 类型:
在这里插入图片描述
之后还必须修改 NewProxyStore,
在这里插入图片描述
同时还需要修改 NewURLStore, 以便在文件名不存在的情况下, 不会对文件进行读写,
在这里插入图片描述
客户端的 Get 方法也需要扩展, 首先需要在缓存中, 查找 key 是否存在, 如果存在,Get 方法将返回缓冲包含的数据, 如果 key 不存在, 将生成一个 rpc 调用, 从服务器中获取 url 地址.
在这里插入图片描述
同理,Put 方法在向服务器发送 Put 的 rpc 请求时, 也需要更新本地缓存,
在这里插入图片描述
客户端可使用 ProxyStore, 而服务器只能使用 URLStore, 这使得客户端与服务器的操作很相似, 它们包含了相同的 Get 和 Put 方法, 所以可指定一个接口 Store, 实现相似操作的通用性,
在这里插入图片描述
之后可定义 Store 类型的全局变量 store,
在这里插入图片描述
最后可在客户端或服务器进程启动时, 在对应的 main 函数中, 使用上述接口, 以便使用接口, 实现 Get 和 Put操作, 同时还可添加一个新的命令参数 masterAddr, 以标识服务器地址 (无默认值).
在这里插入图片描述
如果命令行给出了服务器地址, 那么就可启动一个客户端进程, 并创建一个新的 ProxyStore, 否则只能启动一个服务器进程, 并创建一个新的 URLStore,
在这里插入图片描述
这可在 web 前端中使用 ProxyStore, 因为它已经包含了 URLStore, 而 web 前端的操作如前所述, 并且无须使用 Store 接口, 只有服务器进程需要将数据写入文件. 当启动了一个服务器和多个客户端之后, 可基于多个客户端, 实现服务器的压力测试, 同时需要先编译本项目的第 5 个版本, 或是直接使用已编译的可执行文件. 在命令行中, 首先启动服务器,
在这里插入图片描述
在Windows系统中, 可输入goto, 其中包含了两个参数, 服务器将监听 8080 端口, 并使能 rpc 功能. 之后可使用以下命令, 启动一个客户端,
在这里插入图片描述
其中包含了服务器地址, 该地址可在 8080 端口上, 接收客户端的请求.

使用以下的 Unix Shell 脚本 demo.sh, 可自动实现服务器和客户端的启动.
在这里插入图片描述
为了实现 Windows 系统中的测试, 可启动一个 MINGW shell, 开启服务器进程, 再启动一个新的 MINGW shell, 再开启客户端进程.

19.10 总结与提高

在 goto 应用的构建过程中, 运用了 Go 语言包含的所有重要特性, 虽然应用程序给出了所需的功能, 但是还能使用以下办法, 实现一些改进:

• 优雅: 用户接口可更加优雅, 即使用 template 包.
• 可靠性: 服务器/客户端的 rpc 连接能够更加可靠, 如果连接中断, 客户端可尝试重新连接, 这类操作可交
给一个 dialer(连接) 并发协程.
• 资源: 随着 URL 地址记录的增加, 内存用量将变成一个问题, 可使客户端和服务器基于 key, 共享同一个URL 地址记录的集合.
• 删除: 支持 URL 短地址的删除, 但这一操作会导致服务器与客户端之间的交互更加复杂.

在这里插入图片描述