说明
本文介绍了利用net/http包搭建Web服务器的一般流程。并不涉及Web开发的细节问题。
简介
利用Go的标准包net/http可以很方便的搭建服务器。实际上只需要一个函数和一个接口:
net/http−−−−−−−
package http
// 建立服务器,address为服务器地址,比如:"localhost:8000"
// h 是处理请求的接口,类型为 Handler
// 该函数将一直运行,除非有错误发生,则返回error,返回的error永远不为nil
func ListenAndServe(address string, h Handler) error
// Handler 为处理请求的接口
type Handler interface {
// 该函数处理所有请求
// r为*Request类型,表示请求对象
// w为ResponseWriter类型,表示响应对象,我们将应内容写入到w
ServeHTTP(w ResponseWriter, r *Request)
}
有了以上知识,我们就可以实现一个简单的服务器了:
http1.go−−−−−−−
package main
import (
"log"
"net/http"
)
// 步骤1:声明自定义类型
type MyHttpHandler struct{}
// 步骤2:实现ServeHTTP接口
func (handler MyHttpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("<html><body>Hello, world!</body></html>")) // 将HTML文本写入响应流
}
func main() {
// 步骤3:将自定义类型对象作为参数调用ListenAndServe()以启动服务
log.Fatal(http.ListenAndServe("localhost:8000", MyHttpHandler{}))
}
编译、运行程序,在浏览器输入http://localhost:8000/
,则会在浏览器中显示"Hello, world!"
。
很简单吧!不过这里还有两个问题需要解决:
- 上例中将HTML内容作为硬编码的文本写入响应流,简单的HTML页面固然没问题,但如果是复杂的HTML页面,则很容易产生混乱的代码,而且当需要修改HTML页面内容时,必须重新编译服务器代码,导致灵活性和可维护性低下。如何解决这个问题?
- 上例中所有URL请求的响应都是一样的,比如在浏览器中输入:
http://localhost:8000/index.html
,结果仍显示"Hello, world!"
。显然实际的服务器不会这么做,实际的服务器会根据不同的URL请求作出不同的响应。那么具体该怎么做呢?
下面分别讨论如何解决这两个问题。
逻辑与视图分离
从文件加载HTML
解决第一个问题的思路很简单,就是将HTML文本移到程序之外。比如放在一个文件中,在处理请求时读取文件并写入到响应流:
greet.html−−−−−−−−−
<html>
<head></head>
<body>
<p>Hello, world!</p>
</body>
</html>
http2.go−−−−−−−
package main
import (
"io"
"log"
"net/http"
"os"
)
type MyHttpHandler struct{}
func (handler MyHttpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
f, err := os.Open("greet.html")
if err != nil {
log.Fatalln(err)
}
defer f.Close()
io.Copy(w, f)
}
func main() {
log.Fatal(http.ListenAndServe("localhost:8000", MyHttpHandler{}))
}
但是这种方式只能处理静态的HTML页面,如何处理动态的HTML页面呢?我们可以使用HTML模板。
从模板加载HTML
关于HTML模板的使用,请移步至我的另一篇博文:Go使用Text和HTML模板 。
下例输出当前时间到响应流:
time.html−−−−−−−−−
<html>
<body>
<p>Now is {{.}}</p>
</body>
</html>
http3.go−−−−−−−
package main
import (
"html/template"
"log"
"net/http"
"time"
)
type MyHttpHandler struct{}
func (handler MyHttpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
t := template.Must(template.ParseFiles("time.html"))
t.Execute(w, time.Now().Format("2006-01-02 15:04:05"))
}
func main() {
log.Fatal(http.ListenAndServe("localhost:8000", MyHttpHandler{}))
}
根据不同的请求做出不同的响应
如何根据不同的请求做出不同的响应呢?有以下两种方法。
在Handler中根据URL.Path做出不同的处理
http.Request.URL表示请求的URL,URL.path则表示请求的路径,可以根据这个值来针对不同的请求做出不同的响应:
greet.html−−−−−−−−−
<html>
<head></head>
<body>
<p>Hello, world!</p>
</body>
</html>
time.html−−−−−−−−−
<html>
<body>
<p>Now is {{.}}</p>
</body>
</html>
http4.go−−−−−−−
package main
import (
"html/template"
"io"
"log"
"net/http"
"os"
"time"
)
type MyHttpHandler struct{}
func (handler MyHttpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path { // 根据不同路径做出不同响应
case "/greet":
f, err := os.Open("greet.html")
if err != nil {
log.Fatalln(err)
}
defer f.Close()
io.Copy(w, f)
case "/time":
t := template.Must(template.ParseFiles("time.html"))
t.Execute(w, time.Now().Format("2006-01-02 15:04:05"))
default:
w.WriteHeader(http.StatusNotFound) // 返回404错误
w.Write([]byte("<html><body>no such page</body></html>"))
}
}
func main() {
log.Fatal(http.ListenAndServe("localhost:8000", MyHttpHandler{}))
}
这样一来,在浏览器输入http://localhost:8000/greet
和http://localhost:8000/time
将得到不同的结果。
利用ServeMux模块化代码
上述方法利用switch-case来处理不同的请求,很显然是不好的做法,当合法的URL请求变得很多时会有很多个case,不利于维护。可以将case里的代码写入函数,但是仍然改变不了switch-case结构。也可以实现一个map,将路径和处理该路径的函数作为键-值对,这是一个很好的方法,不过我们仍然需要实现http.Handler接口,在ServeHTTP()方法中维护这个map,而且每次写Web服务器都需要写这些同样的代码。幸运的是,net/http已经想到了这一点,net/http包提供了ServeMux来完成这个任务,它类似于map,可以将路径和处理该路径的函数关联:
greet.html−−−−−−−−−
<html>
<head></head>
<body>
<p>Hello, world!</p>
</body>
</html>
time.html−−−−−−−−−
<html>
<body>
<p>Now is {{.}}</p>
</body>
</html>
http5.go−−−−−−−
package main
import (
"html/template"
"io"
"log"
"net/http"
"os"
"time"
)
func greet(w http.ResponseWriter, r *http.Request) {
f, err := os.Open("greet.html")
if err != nil {
log.Fatalln(err)
}
defer f.Close()
io.Copy(w, f)
}
func datetime(w http.ResponseWriter, r *http.Request) {
t := template.Must(template.ParseFiles("time.html"))
t.Execute(w, time.Now().Format("2006-01-02 15:04:05"))
}
func main() {
mux := http.NewServeMux() // 创建一个 ServeMux
mux.Handle("/greet", http.HandlerFunc(greet))
mux.Handle("/time", http.HandlerFunc(datetime))
log.Fatal(http.ListenAndServe("localhost:8000", mux))
}
可以看到,我们不必再自己实现http.Handler接口,只需要提供请求的路径和相应的处理函数就行了。
需要注意的是http.HandlerFunc()并不是一个函数调用,而是一个类型转换:
net/http−−−−−−−
package http
type HandlerFunc func(w ResponseWriter, r *Request)
func (f HandleFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
可见HandlerFunc是一个适配器,任何能够转换为HandlerFunc的函数在转型后都满足Handler接口。这么设计的目的在于使注册的处理函数必须满足http.Handler接口,这样ServeMux就可以在其内部以ResponseWriter和*Request来调用注册的函数了(听起来有点像C++中的模板,不是吗?)。
考虑到注册函数的通用性,ServeMux提供了一个更便捷的注册方式:
mux.HandleFunc("/greet", greet)
mux.HandleFunc("/time", datetime)
通过ServeMux可以在一个程序中建立多个Web服务器:用不同的ServeMux来注册不同的函数簇,然后分别以这些ServeMux调用ListenAndServe()。不过大多数Web服务器都对应一个程序,net/http考虑到这点,提供了一个全局的ServeMux,http.HandleFunc会将处理函数注册到这个全局的ServeMux,在调用ListenAndServe()时,传递nil作为处理对象,则默认为全局的ServeMux:
func main() {
http.HandleFunc("/greet", greet) // 调用全局的 HandleFunc
http.HandleFunc("/time", datetime)
log.Fatal(http.ListenAndServe("localhost:8000", nil)) // nil 为处理对象,默认为全局的ServeMux
}
总结
- net/http提供了搭建Web服务器的一切。
- 用http.ListenAndServe()创建和启动Web服务器。
- 实现http.Handler以处理请求。
- 利用HTML文件和HTML模板将逻辑与视图分离。
- 利用ServeMux模块化代码。
- 一个程序只实现一个Web服务器时,利用全局ServeMux简化代码。