Golang 第一章 7.web服务器
Go 的库使得编写一个响应客户端请求的网络服务器变得很容易,比如由 fetch 发起的请求。在本节中,我们将展示一个最小化的服务器,它返回用于访问服务器的 URL 的路径部分。也就是说,如果请求的 URL 是 http://localhost:8000/hello,响应将是 URL.Path = "/hello"。
gopl.io/ch1/server1
这个程序只有几行长,因为库函数完成了大部分工作。main 函数将处理函数连接到以 / 开头的传入 URL,即所有 URL,并启动一个服务器,监听端口 8000 上的传入请求。请求表示为 http.Request 类型的结构体,该结构体包含许多相关字段,其中一个字段是传入请求的 URL。当请求到达时,它会被传递给处理函数,处理函数从请求 URL 中提取路径部分(/hello),并使用 fmt.Fprintf 将其作为响应发送回去。网络服务器将在第七章.7 节中详细解释。
让我们在后台启动服务器。在 Mac OS X 或 Linux 上,在命令后添加一个和号 (&);在 Microsoft Windows 上,您需要在单独的命令窗口中运行没有和号的命令。
$ go run src/gopl.io/ch1/server1/main.go &
然后我们可以从命令行发出客户端请求:
$ go build gopl.io/ch1/fetch
$ ./fetch http://localhost:8000
URL.Path = "/"
$ ./fetch http://localhost:8000/help
URL.Path = "/help"
或者,我们可以从网页浏览器访问服务器,如图 1.2 所示。
图 1.2
很容易向服务器添加功能。一个有用的补充是添加一个特定的 URL,返回某种状态。例如,这个版本除了回显请求外,还计算请求的数量;访问 URL /count 将返回到目前为止的请求计数,不包括 /count 请求本身:
// Server2 is a minimal "echo" and counter server.
package main
import (
"fmt"
"log"
"net/http"
"sync"
)
var mu sync.Mutex
var count int
func main() {
http.HandleFunc("/", handler)
http.HandleFunc("/count", counter)
log.Fatal(http.ListenAndServe("localhost:8000", nil))
}
// handler echoes the Path component of the requested URL.
func handler(w http.ResponseWriter, r *http.Request) {
mu.Lock()
count++
mu.Unlock()
fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
}
// counter echoes the number of calls so far.
func counter(w http.ResponseWriter, r *http.Request) {
mu.Lock()
fmt.Fprintf(w, "Count %d\n", count)
mu.Unlock()
}
服务器有两个处理程序,请求的 URL 决定调用哪一个:对 /count
的请求调用 counter
,而所有其他请求调用 handler
。处理程序模式以斜杠结尾,匹配任何以该模式作为前缀的 URL。在幕后,服务器为每个传入的请求在单独的 goroutine 中运行处理程序,以便可以同时处理多个请求。然而,如果两个并发请求尝试同时更新计数,可能会导致计数不一致;这种情况称为竞态条件(§9.1),是一个严重的 bug。为了避免这个问题,我们必须确保最多只有一个 goroutine 在同一时间访问该变量,这就是在每次访问 count
前后调用 mu.Lock()
和 mu.Unlock()
的目的。我们将在第 9 章更详细地讨论并发性和共享变量。
作为一个更丰富的示例,处理函数可以报告它接收到的头部和表单数据,使得服务器可以用于检查和调试请求:
// handler echoes the HTTP request.
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "%s %s %s\n", r.Method, r.URL, r.Proto)
for k, v := range r.Header {
fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
}
fmt.Fprintf(w, "Host = %q\n", r.Host)
fmt.Fprintf(w, "RemoteAddr = %q\n", r.RemoteAddr)
if err := r.ParseForm(); err != nil {
log.Print(err)
}
for k, v := range r.Form {
fmt.Fprintf(w, "Form[%q] = %q\n", k, v)
}
}
这个例子使用 http.Request 结构体的字段来生成如下输出:
GET /?q=query HTTP/1.1
Header["Accept-Encoding"] = ["gzip, deflate, sdch"]
Header["Accept-Language"] = ["en-US,en;q=0.8"]
Header["Connection"] = ["keep-alive"]
Header["Accept"] = ["text/html,application/xhtml+xml,application/xml;..."]
Header["User-Agent"] = ["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5)..."]
Host = "localhost:8000"
RemoteAddr = "127.0.0.1:59911"
Form["q"] = ["query"]
注意到 ParseForm 的调用嵌套在一个 if 语句中。在 Go 中,允许在 if 条件之前放置一个简单的语句,比如局部变量声明,在处理错误时特别有用,就像这个例子中一样。我们本可以这样写:
err := r.ParseForm()
if err != nil {
log.Print(err)
}
但将语句合并起来更短,并减少了变量 err
的作用域,这是良好的习惯。我们将在第二章.7 节定义作用域。
在这些程序中,我们看到了三种非常不同的输出流类型。fetch 程序将 HTTP 响应数据复制到 os.Stdout,一个文件,就像 lissajous 程序一样。fetchall 程序通过将响应复制到无用的目标 ioutil.Discard(同时计算其长度)来丢弃响应。而上面的 web 服务器则使用 fmt.Fprintf 写入表示网页浏览器的 http.ResponseWriter。
尽管这三种类型在其具体功能细节上有所不同,但它们都满足一个共同的接口,允许在需要输出流的任何地方使用它们。这个接口叫做 io.Writer,在第七章.1 节中讨论。
Go 的接口机制是第七章的主题,但为了给出一个它的能力的概念,让我们看看如何将 web 服务器与 lissajous 函数结合起来,以便将动画 GIF 写入不是标准输出,而是 HTTP 客户端。只需将以下几行添加到 web 服务器中:
handler := func(w http.ResponseWriter, r *http.Request) {
lissajous(w)
}
http.HandleFunc("/", handler)
或者:
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
lissajous(w)
})
上面 HandleFunc 函数调用的第二个参数是一个函数字面量,也就是在使用它的地方定义的匿名函数。我们将在第五章.6节进一步解释它。
做出这些更改,访问 http://localhost:8000 在您的浏览器中。每次加载页面时,您会看到一个类似于图1.3中的新动画。
// Server2 is a minimal "echo" and counter server.
package main
import (
"fmt"
"image"
"image/color"
"image/gif"
"io"
"log"
"math"
"math/rand"
"net/http"
"sync"
)
var mu sync.Mutex
var count int
func main() {
http.HandleFunc("/", handler)
http.HandleFunc("/count", counter)
log.Fatal(http.ListenAndServe("localhost:8000", nil))
}
var palette = []color.Color{color.White, color.Black}
const (
whiteIndex = 0 // 调色板中的第一个颜色
blackIndex = 1 // 调色板中的下一种颜色
)
func lissajous(out io.Writer) {
const (
cycles = 5 // 完整的 x 振荡器旋转次数
res = 0.001 // 角分辨率
size = 100 // 图片画布封面 [-size..+size]
nframes = 64 // 动画帧数
delay = 8 // 帧间延迟以 10ms 为单位
)
freq := rand.Float64() * 3.0 // y 振荡器的相对频率
anim := gif.GIF{LoopCount: nframes}
phase := 0.0 // 相位差
for i := 0; i < nframes; i++ {
rect := image.Rect(0, 0, 2*size+1, 2*size+1)
img := image.NewPaletted(rect, palette)
for t := 0.0; t < cycles*2*math.Pi; t += res {
x := math.Sin(t)
y := math.Sin(t*freq + phase)
img.SetColorIndex(size+int(x*size+0.5), size+int(y*size+0.5),
blackIndex)
}
phase += 0.1
anim.Delay = append(anim.Delay, delay)
anim.Image = append(anim.Image, img)
}
gif.EncodeAll(out, &anim) // 注意:忽略编码错误
}
// handler echoes the Path component of the requested URL.
func handler(w http.ResponseWriter, r *http.Request) {
mu.Lock()
count++
mu.Unlock()
lissajous(w)
// fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
}
// counter echoes the number of calls so far.
func counter(w http.ResponseWriter, r *http.Request) {
mu.Lock()
fmt.Fprintf(w, "Count %d\n", count)
mu.Unlock()
}
图1.3
练习 1.12:修改 Lissajous 服务器以从 URL 中读取参数值。例如,您可以安排使 URL http://localhost:8000/?cycles=20 设置cycles数为20,而不是默认的5。使用 strconv.Atoi 函数将字符串参数转换为整数。您可以通过 go doc strconv.Atoi 查看它的文档。
v2ray节点购买
评论