diff --git a/eBook/19.0.md b/eBook/19.0.md new file mode 100644 index 0000000..37a8cbf --- /dev/null +++ b/eBook/19.0.md @@ -0,0 +1,7 @@ +# 19 构建一个完整的应用程序 + +## 链接 + +- [目录](directory.md) +- 上一章:[出于性能考虑的最佳实践和建议](18.11.md) +- 下一节:[简介](19.1.md) diff --git a/eBook/19.1.md b/eBook/19.1.md new file mode 100644 index 0000000..80f4abf --- /dev/null +++ b/eBook/19.1.md @@ -0,0 +1,21 @@ +# 19.1 简介 + +由于 web 无处不在,本章我们将开发一个完整的程序:`goto`,它是一个 web 缩短网址应用程序。示例来自 Andrew Gerrand 的讲座(见参考资料 22)。我们将把项目分成 3 个阶段,每一个都会比之前阶段包含更多的功能,并逐渐展示更多 Go 语言中的特性。我们会大量使用在 [15章](15.0.md) 所学的网页应用程序的知识。 + +**版本 1:** 利用映射和结构体,与 `sync` 包的 `Mutex` 一起使用,以及一个结构体工厂。 + +**版本 2:** 数据以 `gob` 格式写入文件以实现持久化。 + +**版本 3:** 利用协程和通道重写应用(见 [14章](14.0.md))。 + +**版本 4:** 如果我们要使用 json 格式的文件该如何修改? + +**版本 5:** 用 rpc 协议实现的分布式版本。 + +由于代码变更频繁,不会展示在此处,仅给出访问地址。 + +## 链接 + +- [目录](directory.md) +- 上一节:[构建一个完整的应用程序](19.0.md) +- 下一节:[短网址项目简介](19.2.md) diff --git a/eBook/19.2.md b/eBook/19.2.md new file mode 100644 index 0000000..163d2bf --- /dev/null +++ b/eBook/19.2.md @@ -0,0 +1,22 @@ +# 19.2 短网址项目简介 + +你肯定知道有些浏览器中的地址(称为 URL)非常长且/或复杂,在网上有一些将他们转换成简短 URL 来使用的服务。我们的项目与此类似:它是具有 2 个功能的 *web 服务*(web service): + +## 添加 (Add) + +给定一个较长的 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 +``` +- (A) 转变为:`http://goto/UrcGq` +- (B) 并保存这对数据 + +## 重定向 (Redirect) + +短网址被请求时,会把用户重定向到原始的长 URL。因此如果你在浏览器输入网址 (B),会被重定向到页面 (A)。 + +## 链接 + +- [目录](directory.md) +- 上一节:[简介](19.1.md) +- 下一节:[数据结构](19.3.md) diff --git a/eBook/19.3.md b/eBook/19.3.md new file mode 100644 index 0000000..788ed7c --- /dev/null +++ b/eBook/19.3.md @@ -0,0 +1,67 @@ +# 版本 1 - 数据结构和前端界面 + +第 1 个版本的代码 *goto_v1* 见 [goto_v1](examples/chapter_19/goto_v1)。 + +# 19.3 数据结构 + +(本节代码见 [goto_v1/store.go](examples/chapter_19/goto_v1/store.go)。) + +当程序运行在生产环境时,会收到很多短网址的请求,同时会有一些将长 URL 转换成端 URL 的请求。我们的程序要以什么样的结构存储这些数据呢?[19.2 节](19.2.md)中 (A) 和 (B) 两种 URL 都是字符串,此外,它们相互关联:给定键 (B) 能获取到值 (A),他们互相*映射*(map)。要将数据存储在内存中,我们需要这种结构,它们几乎存在于所有的编程语言中,只是名称有所不同,例如“哈希表”或“字典”等。 + +Go 语言就有这种内建的映射(map):`map[string]string`。 + +键的类型写在 `[` 和 `]` 之间,紧接着是值的类型。有关映射的所有知识详见 [8 章](08.0.md)。为特定类型指定一个别名在严谨的程序中非常实用。Go 语言中通过关键字 `type` 来定义,因此有定义: +```go +type URLStore map[string]string +``` +它从短 URL 映射到长 URL,两者都是字符串。 + +要创建那种类型的变量,并命名为 m,使用: +```go +m := make(URLStore) +``` + +假设 *http://goto/a* 映射到 *http://google.com/* ,我们要把它们存储到 m 中,可以用如下语句: +```go +m["a"] = "http://google.com/" +``` +(键只是 *http://goto/* 的后缀,其前缀总是不变的。) + +要获得给定 "a" 对应的长 URL,可以这么写: +```go +url := m["a"] +``` +此时 `url` 的值等于 `http://google.com/`。 + +注意,使用了 `:=` 就不需要指明 url 的类型为 `string`,编译器会从右侧的值中推断出来。 + +## 使程序线程安全 + +这里,变量 `URLStore` 是中心化的内存存储。当收到网络流量时,会有很多 `Redirect` 服务的请求。这些请求其实只涉及读操作:以给定的短 URL 作为键,返回对应的长 URL 的值。然而,对 `Add` 服务的请求则大不相同,它们会更改 `URLStore`,添加新的键值对。当在瞬间收到大量更新请求时,可能会产生如下问题:添加操作可能被另一个相同请求打断,写入的长 URL 结果可能不是最新的值;另外,读取和更改同时进行,导致可能读到脏数据。代码中的 map 并不保证当开始更新数据时,会彻底阻止另一个更新操作的启动。也就是说,map 不是线程安全的,*goto* 会并发地为很多请求提供服务。因此必须使 `URLStore` 是线程安全的,以便可以从不同的线程访问它。最简单和经典的方法是为其增加一个锁,它是 Go 标准库 `sync` 包中的 `Mutex` 类型,必须导入到我们的代码中(关于锁详见 [9.3 节](09.3.md))。 + +现在,我们把 `URLStore` 类型的定义更改为一个结构体(就是字段的集合,类似 C 或 Java ,[10 章](10.0.md) 介绍了结构体),它含有两个字段:`map` 和 `sync` 包的 `RWMutex`: +```go +import "sync" +type URLStore struct { + urls map[string]string // map from short to long URLs + mu sync.RWMutex +} +``` + +`RWMutex` 有两种锁:分别对应读和写。多个客户端可以同时设置读锁,但只有一个客户端可以设置写锁(以排除所有的读锁),有效地串行化变更,使他们按顺序生效。 + +我们将在 `Get` 函数中实现 `Redirect` 服务的读请求,在 `Set` 函数中实现 `Add` 服务的写请求。`Get` 函数类似下面这样: +```go +func (s *URLStore) Get(key string) string { + s.mu.RLock() + url := s.urls[key] + s.mu.RUnlock() + return url +} +``` + +## 链接 + +- [目录](directory.md) +- 上一节:[短网址项目简介](19.2.md) +- 下一节:[用户界面:web 服务端](19.4.md) diff --git a/eBook/19.4.md b/eBook/19.4.md new file mode 100644 index 0000000..2e09f48 --- /dev/null +++ b/eBook/19.4.md @@ -0,0 +1,123 @@ +# 19.4 用户界面:web 服务端 + +(本节代码见 [goto_v1/main.go](examples/chapter_19/goto_v1/main.go)。) + +我们尚未编写启动程序的必要函数。它们(总是)类似 C,C++ 或 Java 中的 `main()` 函数,我们的 web 服务器由它启动,例如用如下命令在本地 8080 端口启动 web 服务器: +```go +http.ListenAndServe(":8080", nil) +``` + +(web 服务器的功能来自于 `http` 包,[15 章](15.0.md) 做了深入介绍)。web 服务器会在一个无限循环中监听到来的请求,但我们必须定义针对这些请求,服务器该如何响应。可以用被称为 HTTP 处理器的 `HandleFunc` 函数来办到,例如代码: +```go +http.HandleFunc("/add", Add) +``` +如此,每个以 `/add` 结尾的请求都会调用 `Add` 函数(尚未完成)。 + +程序有两个 HTTP 处理器: +- `Redirect`,用于对短 URL 重定向 +- `Add`,用于处理新提交的 URL + +示意图: + +![](images/19.4_fig19.1.jpg?raw=true) + +最简单的 `main()` 函数类似这样: +```go +func main() { + http.HandleFunc("/", Redirect) + http.HandleFunc("/add", Add) + http.ListenAndServe(":8080", nil) +} +``` + +对 `/add` 的请求由 `Add` 处理器处理,所有其他请求会被 `Redirect` 处理器处理。处理函数从到来的请求(一个类型为 `*http.Request` 的变量)中获取信息,然后产生响应并写入 `http.ResponseWriter` 类型变量 `w`。 + +`Add` 函数必须做的事有: +1. 读取长 URL,即:用 `r.FormValue("url")` 从 HTML 表单提交的 HTTP 请求中读取 URL +2. 使用 store 上的 `Put` 方法存储长 URL +3. 将对应的短 URL 发送给用户 + +每个需求都转化为一行代码: +```go +func Add(w http.ResponseWriter, r *http.Request) { + url := r.FormValue("url") + key := store.Put(url) + fmt.Fprintf(w, "http://localhost:8080/%s", key) +} +``` + +这里 `fmt` 包的 `Fprintf` 函数用来替换字符串中的关键字 `%s`,然后将结果作为响应发送回客户端。注意 `Fprintf` 把数据写到了 `ResponseWriter` 中,其实 `Fprintf` 可以将数据写到任何实现了 `io.Writer` 的数据结构,即该结构实现了 `Write` 方法。Go 中 `io.Writer` 称为接口,可见 `Fprintf` 利用接口变得十分通用,可以对很多不同的类型写入数据。Go 中接口的使用十分普遍,它使代码更通用(见 [11 章](11.0.md))。 + +还需要一个表单,仍然可以用 `Fprintf` 来输出,这次将常量写入 `w`。让我们来修改 `Add`,当未指定 URL 时显示 HTML 表单: +```go +func Add(w http.ResponseWriter, r *http.Request) { + url := r.FormValue("url") + if url == "" { + fmt.Fprint(w, AddForm) + return + } + key := store.Put(url) + fmt.Fprintf(w, "http://localhost:8080/%s", key) +} + +const AddForm = ` +
+URL: + +
+` +``` + +在那种情况下,发送字符串常量 `AddForm` 到客户端,它是 html 表单,包含一个 `url` 输入域和一个提交按钮,点击后发送 POST 请求到 `/add`。这样 `Add` 处理函数被再次调用,此时 `url` 的值来自文本域。(` `` ` 用来创建原始字符串,否则按惯例 `""` 将成为字符串边界。) + +`Redirect` 函数在 HTTP 请求路径中找到键(短 URL 的键是请求路径去除首字符,在 Go 中可以写为 `[1:]`。例如请求 "/abc",键就是 "abc"),用 `Get` 函数从 `store` 检索到对应的长 URL,对用户发送 HTTP 重定向。如果没找到 URL,发送 404 "Not Found" 错误取而代之: +```go +func Redirect(w http.ResponseWriter, r *http.Request) { + key := r.URL.Path[1:] + url := store.Get(key) + if url == "" { + http.NotFound(w, r) + return + } + http.Redirect(w, r, url, http.StatusFound) +} +``` + +(`http.NotFound` 和 `http.Redirect` 是发送通用 HTTP 响应的工具函数。) + +我们已经完整地遍历了 [goto_v1](examples/chapter_19/goto_v1) 的代码。 + +## 编译和运行 + +可执行程序已包含在示例代码下,如果你想立即测试可以跳过本节。其中包含 3 个 go 源文件和一个 Makefile 文件,通过它应用可以被编译和链接,只须如下操作: +- **Linux 和 OSX 平台:** 在终端窗口源码目录下启动 `make` 命令,或在 LiteIDE 中构建项目。 +- **Windows 平台:** 启动 MINGW 环境,步骤为:开始菜单,所有程序,MinGW,MinGW Shell(见 [2.5.5 节](02.5.md)),在命令行窗口输入 `make` 并回车,源代码被编译并链接为原生 exe 可执行程序。 + +生成内容为可执行程序,Linux/OS X 下为 `goto`,Windows 下为 `goto.exe`。 + +要启动并运行 web 服务器,那么: +- **Linux 和 OSX 平台:** 输入命令 `./goto`。 +- **Windows 平台:** 从 Go IDE 启动程序(如果 Windows 防火墙阻止程序启动,设置允许该程序) + +## 测试该程序 + +打开浏览器并请求 url:`http://localhost:8080/add` + +这会激活 `Add` 处理函数。请求还未包含 url 变量,所以响应会输出 html 表单询问输入: + +![](images/19.4_fig19.2.png?raw=true) + +添加一个长 URL 以获取等价的缩短版本,例如 `http://golang.org/pkg/bufio/#Writer`,然后单击按钮。应用会为你产生一个短 URL 并打印出来,例如 `http:// +localhost:8080/2`。 + +![](images/19.4_fig19.3.jpg?raw=true) + +复制该 URL 并在浏览器地址栏粘贴以发出请求,现在轮到 `Redirect` 处理函数上场了,对应长 URL 的页面被显示了出来。 + +![](images/19.4_fig19.4.jpg?raw=true) + +## 链接 + +- [目录](directory.md) +- 上一节:[数据结构](19.3.md) +- 下一节:[持久化存储:gob](19.5.md) diff --git a/eBook/images/19.4_fig19.1.jpg b/eBook/images/19.4_fig19.1.jpg new file mode 100644 index 0000000..a4ce283 Binary files /dev/null and b/eBook/images/19.4_fig19.1.jpg differ diff --git a/eBook/images/19.4_fig19.2.png b/eBook/images/19.4_fig19.2.png new file mode 100644 index 0000000..8aa90d9 Binary files /dev/null and b/eBook/images/19.4_fig19.2.png differ diff --git a/eBook/images/19.4_fig19.3.jpg b/eBook/images/19.4_fig19.3.jpg new file mode 100644 index 0000000..500e4c5 Binary files /dev/null and b/eBook/images/19.4_fig19.3.jpg differ diff --git a/eBook/images/19.4_fig19.4.jpg b/eBook/images/19.4_fig19.4.jpg new file mode 100644 index 0000000..c235712 Binary files /dev/null and b/eBook/images/19.4_fig19.4.jpg differ