Golang启动HTTP服务器

警告
本文最后更新于 2020-05-29,文中内容可能已过时。

本文介绍 Golang 如何实现 HTTP 服务端及客户端。

1. HTTP协议客户端实现

Go语言标准库内置了net/http包,涵盖了HTTP客户端和服务端具体的实现方式。内置的net/http包提供了最简洁的HTTP客户端实现方式,无须借助第三方网络通信库,就可以直接使用HTTP中用得最多的GET和POST方式请求数据。

实现HTTP客户端就是客户端通过网络访问向服务端发送请求,服务端发送响应信息,并将相应信息输出到客户端的过程。实现客户端有多种方式,具体如下所示。

1.1 使用http.NewRequest()方法

首先创建一个client(客户端)对象,其次创建一个request(请求)对象,最后使用client发送request。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package main

import (
	"fmt"
	"net/http"
)

func main() {
	testHttpNewRequest()
}

func testHttpNewRequest() {
	//1.创建一个客户端
	client := http.Client{}
	//2.创建一个请求,请求方式可以是GET或POST
	request, err := http.NewRequest("GET", "http://www.baidu.com", nil)
	checkErr(err)
	//3.客户端发送请求
	cookName := &http.Cookie{Name: "username", Value: "Steven"}

	//添加cookie
	request.AddCookie(cookName)
	response, err := client.Do(request)
	checkErr(err)
	//设置请求头
	request.Header.Set("Accept-Lanauage", "zh-cn")
	defer response.Body.Close()
	//查看请求头的数据
	fmt.Printf("Header:%+v\n", request.Header)
	fmt.Printf("响应状态码: %v\n", response.StatusCode)

	//4.操作数据
	if response.StatusCode == 200 {
		fmt.Println("网络请求成功")
		checkErr(err)
	} else {
		fmt.Println("网络请求失败", response.Status)
	}
}

//检查错误
func checkErr(err error) {
	defer func() {
		if ins, ok := recover().(error); ok {
			fmt.Println("程序出现异常: ", ins.Error())
		}
	}()
	if err != nil {
		panic(err)
	}
}

运行结果如下

1
2
3
4
$ go run main.go
Header:map[Accept-Lanauage:[zh-cn] Cookie:[username=Steven]]
响应状态码: 200
网络请求成功 

1.2 调用client.Get() 方法

这种方法总共两个步骤,先创建一个client(客户端)对象,然后使用client调用Get()方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main

import (
	"fmt"
	"net/http"
)

func main() {
	testClientGet()
}

func testClientGet() {
	//1.创建一个客户端
	client := http.Client{}
	//2.通过client请求
	response, err := client.Get("http://www.baidu.com")
	checkErr(err)

	fmt.Printf("响应状态码: %v\n", response.StatusCode)

	if response.StatusCode == 200 {
		fmt.Println("网络请求成功")
		defer response.Body.Close()
	}
}

//检查错误
func checkErr(err error) {
	defer func() {
		if ins, ok := recover().(error); ok {
			fmt.Println("程序出现异常: ", ins.Error())
		}
	}()
	if err != nil {
		panic(err)
	}
}

运行结果如下

1
2
3
$ go run main.go
响应状态码: 200
网络请求成功

1.3 使用client.Post()或client.PostForm()方法

这种方法也是两个步骤,先创建一个client(客户端)对象,然后使用client调用Post()或PostForm()方法。其实client的Post()或PostForm()方法,就是对http.NewRequest()的封装。

1
2
3
4
resp, err := http.Post("http://example.com/upload", "image/jpeg", &buf)
...
resp, err := http.PostForm("http://example.com/form",
	url.Values{"key": {"Value"}, "id": {"123"}})

1.4 使用http.Get() 方法

这种方式只有一个步骤,http的Get()方法就是对DefaultClient.Get()的封装。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main

import (
	"fmt"
	"net/http"
)

func main() {
	testHttpGet()
}

func testHttpGet() {
	//获取服务器数据
	response, err := http.Get("http://www.baidu.com")
	checkErr(err)
	fmt.Printf("响应状态码: %v\n", response.StatusCode)

	if response.StatusCode == 200 {
		fmt.Println("网络请求成功")
		defer response.Body.Close()
		checkErr(err)
	} else {
		fmt.Println("请求失败", response.Status)
	}
}

//检查错误
func checkErr(err error) {
	defer func() {
		if ins, ok := recover().(error); ok {
			fmt.Println("程序出现异常: ", ins.Error())
		}
	}()
	if err != nil {
		panic(err)
	}
}

运行结果为

1
2
3
$ go run main.go
响应状态码: 200
网络请求成功

1.5 使用http.Post()或http.PostForm()方法

http的Post()函数或PostForm(),就是对DefaultClient.Post()或DefaultClient.PostForm()的封装。这种方法也只需要一个步骤

2. HTTP协议服务端实现

使用Go语言标准库内置的net/http包,就可以实现一个基本的HTTP服务端。一个基本的HTTP服务器主要应完成如下功能

  1. 处理动态请求:处理浏览网站,登录帐户或发布图片等用户传入的请求。
  2. 提供静态文件:将JavaScript,CSS和图像等静态文件提供给浏览器,服务于用户。
  3. 接受连接请求:HTTP服务器必须监听指定端口从而接收来自网络的连接请求。

2.1 处理动态请求

我们可以使用http.HandleFunc函数注册一个新的 Handler 来处理动态请求。它的第一个参数是请求路径的匹配模式,第二个参数是一个函数类型,表示针对这个请求要执行的功能。下例中针对请求返回一个欢迎访问的提示语。

1
2
3
http.HandleFunc("/", func (w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, "Welcome to my website!")
})

http.ResponseWriter类型包含了服务器端给客户端的响应数据。服务器端往里面写入了什么内容,浏览器的网页源码就是什么内容。*http.Request包含了客户端发送给服务器端的请求信息(路径、浏览器类型等)。

2.2 提供静态文件

使用http.FileServer() 方法提供 Javascript,CSS或图片等静态文件。它的参数是文件系统接口,可以使用http.Dir()来指定文件所在的路径。如果该路径中有index.html文件,则会优先显示html文件,否则会显示文件目录。

1
fs := http.FileServer(http.Dir("static/"))

http.FileServer()的返回值正好是 Handler 类型,也就是可以提供文件访问服务的HTTP处理器。现在,我们只需要将一个URL指向它,期间我们可以使用http.StripPrefix() 去除某些URL前缀,返回值同样是一个 Handler类型

1
http.Handle("/static/", http.StripPrefix("/static/", fs))

2.3 接收连接请求

http.ListenAndServer()函数用来启动HTTP服务器,并且在指定的 IP 地址和端口上监听客户端请求

1
http.ListenAndServe(":80", nil)

函数实现如下,其中第一个参数为监听地址,第二个参数表示一个HTTP处理器 Handler。可以看到,底层调用的是 net/http 包的 ListenAndServe 方法,首先会初始化一个 Server 对象,然后调用该 Server 实例的 ListenAndServe 方法,进而调用 net.Listen("tcp", addr),也就是基于 TCP 协议创建 Listen Socket,并在传入的IP 地址和端口号上监听请求。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
func ListenAndServe(addr string, handler Handler) error {
	server := &Server{Addr: addr, Handler: handler}
	return server.ListenAndServe()
}

func (srv *Server) ListenAndServe() error {
	if srv.shuttingDown() {
		return ErrServerClosed
	}
	addr := srv.Addr
	if addr == "" {
		addr = ":http"
	}
	ln, err := net.Listen("tcp", addr)
	if err != nil {
		return err
	}
	return srv.Serve(ln)
}

最终我们看到调用了 Server 实例的 Serve(net.Listener) 方法,这个方法里面起了一个 for 循环,在循环体中首先通过 net.Listener(即上一步监听端口中创建的 Listen Socket)实例的 Accept 方法接收客户端请求,接收到请求后根据请求信息创建一个 conn 连接实例,最后单独开了一个 goroutine,把这个请求的数据当做参数扔给这个 conn 去服务:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
for {
		rw, err := l.Accept()
		if err != nil {
			...
		}
		connCtx := ctx
		if cc := srv.ConnContext; cc != nil {
			connCtx = cc(connCtx, rw)
			if connCtx == nil {
				panic("ConnContext returned nil")
			}
		}
		tempDelay = 0
		c := srv.newConn(rw)
		c.setState(c.rwc, StateNew) // before Serve can return
		go c.serve(connCtx)
	}

用户的每一次请求都是在一个新的 goroutine 去服务,相互不影响。客户端请求的具体处理逻辑都是在 c.serve 中完成的。 conn 实例的 serve 方法首先会通过 c.readRequest() 解析请求,然后在 serverHandler{c.server}.ServeHTTP(w, w.req)ServeHTTP 方法中获取相应的 handlerhandler := c.server.Handler,也就是我们刚才在调用函数 ListenAndServe 时候的第二个参数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
	handler := sh.srv.Handler
	if handler == nil {
		handler = DefaultServeMux
	}
	if req.RequestURI == "*" && req.Method == "OPTIONS" {
		handler = globalOptionsHandler{}
	}
	handler.ServeHTTP(rw, req)
}

我们发现当 handler 为 nil,也就是 ListenAndServe() 的第二个参数为 nil 时,使用了默认的 http.DefaultServeMux,这是 ServeMux的默认实例

1
2
var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux

ServeMux的数据结构如下

1
2
3
4
5
6
type ServeMux struct {
    mu    sync.RWMutex. // 由于请求涉及到并发处理,因此这里需要一个锁机制
    m     map[string]muxEntry // 路由规则字典,存放 URL 路径与处理器的映射关系
    es    []muxEntry // MuxEntry 切片(按照最长到最短排序)
    hosts bool       // 路由规则中是否包含 host 信息
}

这里,我们需要重点关注的是 muxEntry 结构:

1
2
3
4
type muxEntry struct {
    h   Handler       // 处理器具体实现
    pattern string    // 模式匹配字符串
}

最后我们来看一下 Handler 的定义,这是一个接口:

1
2
3
type Handler interface {
    ServeHTTP(ResponseWriter, *Request) // 路由处理实现方法
}

当请求路径与 pattern 匹配时,就会调用 HandlerServeHTTP 方法来处理请求。

1
http.HandleFunc("/", sayHelloWorld)

当我们使用一个自定义的处理函数时,如上面的sayHelloWorld,并没有实现 Handler 接口,之所以可以成功添加到路由映射规则,是因为在底层通过 HandlerFunc() 函数将其强制转化为了 HandlerFunc 类型,而 HandlerFunc 类型实现了 ServeHTTP 方法,这样,sayHelloWorld 方法也就变相实现了 Handler 接口

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    if handler == nil {
		  panic("http: nil handler")
    }
    mux.Handle(pattern, HandlerFunc(handler))
}

...

type HandlerFunc func(ResponseWriter, *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

对于 sayHelloWorld 方法来说,它已然变成了 HandlerFunc 类型的函数类型,当我们在其实例上调用 ServeHTTP 方法时,调用的是 sayHelloWorld 方法本身。

前面我们提到,DefaultServeMuxServeMux 的默认实例,当我们在 HandleFunc 中调用 mux.Handle 方法时,实际上是将其路由映射规则保存到 DefaultServeMux 路由处理器的数据结构中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func (mux *ServeMux) Handle(pattern string, handler Handler) {
	mux.mu.Lock()
	defer mux.mu.Unlock()

	if pattern == "" {
		panic("http: invalid pattern")
	}
	if handler == nil {
		panic("http: nil handler")
	}
	if _, exist := mux.m[pattern]; exist {
		panic("http: multiple registrations for " + pattern)
	}

	if mux.m == nil {
		mux.m = make(map[string]muxEntry)
	}
	e := muxEntry{h: handler, pattern: pattern}
	mux.m[pattern] = e
	if pattern[len(pattern)-1] == '/' {
		mux.es = appendSorted(mux.es, e)
	}

	if pattern[0] != '/' {
		mux.hosts = true
	}
}

还是以 sayHelloWorld 为例,这里的 pattern 字符串对应的是请求路径 /handler 对应的是 sayHelloWorld 函数。

保存好路由映射规则之后,客户端请求的处理就默认调用ServeMux 实现的 ServeHTTP 方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
    if r.RequestURI == "*" {
        w.Header().Set("Connection", "close")
        w.WriteHeader(StatusBadRequest)
        return
    }
    
    h, _ := mux.Handler(r)
    h.ServeHTTP(w, r)
}

如上所示,路由处理器接收到请求之后,如果 URL 路径是 *,则关闭连接,否则调用 mux.Handler(r) 返回对应请求路径匹配的处理器,然后执行 h.ServeHTTP(w, r),也就是调用对应路由 handlerServerHTTP 方法,以 / 路由为例,调用的就是 sayHelloWorld 函数本身。

总结

现在我们来捋一下,当我们调用 http.ListenAndServe,首先建立了一个 Server 实例,然后把两个参数都赋给了该实例,之后我们在该实例的基础上调用底层 net 包监听端口、创建socket并开启连接,最后把这个连接交给了 Server实例的 handler处理,这个handler 正是我们在 ListenAndServe 中传入的第二个参数。

当第二个参数为 nil 时调用了 ServeMux 的默认实例 DefaultServeMux ,该实例实现了一个 ServeMux 结构体,而这个结构体中最重要的一个字段就是muxEntry 结构体,包含 pattern 和 handler 两部分。所以我们实现 Handle 和 HandleFunc 都是在将路由映射规则保存到 DefaultServeMux 路由处理器的 muxEntry 结构体的这两个字段。

客户端请求的处理就默认调用ServeMux 实现的 ServeHTTP 方法,把对应的请求交给对应的处理器。

2.4 获取客户端提交的数据

前面已经提到,客户端提交的数据全部位于 *http.Request 中,下面的例子虽然做了声明,但没有使用,本节介绍一下如何从 *http.request 中提取想要的数据

1
2
3
http.HandleFunc("/", func (w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, "Welcome to my website!")
})

Request的部分结构如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
type Request struct {
    ...
    // Method指定HTTP方法(GET、POST、PUT等)。对客户端,""代表GET。
    Method string
    // Form是解析好的表单数据,包括URL字段的query参数和POST或PUT的表单数据。
    Form url.Values
    // PostForm是解析好的POST或PUT的表单数据。
    PostForm url.Values
    ...
}

使用ParseForm解析URL中的查询字符串,并将解析结果更新到r.Form字段。对于POST或PUT请求,ParseForm还会将body当作表单解析,并将结果既更新到r.PostForm也更新到r.Form。解析结果中,POST或PUT请求主体要优先于URL查询字符串(同名变量,主体的值在查询字符串的值前面)。

1
func (r *Request) ParseForm() error

然后使用 FormValue 返回以 key 为健查询 r.Form 得到的第一个值

1
func (r *Request) FormValue(key string) string

PostFormValue则返回key为键查询r.PostForm字段得到的第一个值,用于POST和PUT

1
func (r *Request) PostFormValue(key string) string

当提交的请求数据中有文件时,使用FormFile,可以返回以key为键查询r.MultipartForm字段得到结果中的第一个文件和它的信息。

1
func (r *Request) FormFile(key string) (multipart.File, *multipart.FileHeader, error)

一个简单的实现如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
func loginActionHandler(w http.ResponseWriter, r *http.Request) {
	r.ParseForm()
	if r.Method == "GET" && r.ParseForm() == nil {
		username := r.FormValue("username")
		pwd := r.FormValue("password")
		if len(username) < 4 || len(username) > 10 {
			w.Write([]byte("用户名不符合规范"))
		}
		if len(pwd) < 6 || len(pwd) > 16 {
			w.Write([]byte("密码不符合规范"))
		}
		http.Redirect(w, r, "/list", http.StatusFound)
		return
	} else {
		w.Write([]byte("请求方式不对"))
		return
	}
	w.Write([]byte("登录失败"))
}
支付宝
微信
0%