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

说明

本文介绍了利用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/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 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简化代码。
  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值