Golang 第一章 7.web服务器

  • By v2ray节点

  • 2024-07-02 10:40:40

  • 评论

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节点购买