Go利用net/http包搭建Web服务器

来源:互联网 发布:知乎66条神回复集锦 编辑:程序博客网 时间:2024/05/18 18:46

说明

本文介绍了利用net/http包搭建Web服务器的一般流程。并不涉及Web开发的细节问题。

简介

利用Go的标准包net/http可以很方便的搭建服务器。实际上只需要一个函数和一个接口:

net/http

package http// 建立服务器,address为服务器地址,比如:"localhost:8000"// h 是处理请求的接口,类型为 Handler// 该函数将一直运行,除非有错误发生,则返回error,返回的error永远不为nilfunc ListenAndServe(address string, h Handler) error// Handler 为处理请求的接口type Handler interface {    // 该函数处理所有请求    // r为*Request类型,表示请求对象    // w为ResponseWriter类型,表示响应对象,我们将应内容写入到w    ServeHTTP(w ResponseWriter, r *Request)}

有了以上知识,我们就可以实现一个简单的服务器了:

http1.go

package mainimport (    "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 mainimport (    "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 mainimport (    "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 mainimport (    "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/greethttp://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 mainimport (    "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 httptype 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简化代码。
原创粉丝点击