01|net/http:使用标准库搭建Server并不是那么简单

思考并回答以下问题:

框架地址
说明文档

https://www.jianshu.com/p/e5df57bd6740

标准库

对Web Server来说,Golang提供了net库和net/http库,分别对应OSI的TCP层和HTTP层,它们两个负责的就是HTTP的接收和解析。

用net/http来创建一个HTTP服务很简单。

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
package main

import (
"fmt"
"html"
"log"
"net/http"
)

type Hello struct {
}

func (hello Hello) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello World!"))
}

func main() {
http.Handle("/", Hello{})

http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})

log.Fatal(http.ListenAndServe(":8080", nil))
}


这几行代码做了什么,为什么就能启动一个HTTP服务,具体的逻辑是什么样的?

创建框架的Server结构

geekbang/01

代码结构图

第一层的关键结论就是:net/http标准库创建服务,实质上就是通过创建Server数据结构来完成的。所以接下来,我们就来创建一个Server数据结构。

通过go doc net/http.Server我们可以看到Server的结构:

1
2
3
4
5
6
7
type Server struct {
// 请求监听地址
Addr string
// 请求核心处理函数
Handler Handler
...
}

其中最核心的是Handler这个字段,从主流程中我们知道(第六层关键结论),当Handler这个字段设置为空的时候,它会默认使用DefaultServerMux这个路由器来填充这个值,但是我们一般都会使用自己定义的路由来替换这个默认路由。

所以在框架代码中,我们要创建一个自己的核心路由结构,实现Handler。

所有的框架代码都存放在framework文件夹中,而所有的示例业务代码都存放在framework文件夹之外。framework文件夹是框架文件夹,而把外层称为业务文件夹。

在一个新的业务中,如果要使用到我们自己写好的框架,可以直接通过引用“import 项目地址/framework”来引入。

创建一个framework文件夹,新建core.go。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package framework

import "net/http"

// 框架核心结构
type Core struct {
}

// 初始化框架核心结构
func NewCore() *Core {
return &Core{}
}

// 框架核心结构实现Handler接口
func (c *Core) ServeHTTP(response http.ResponseWriter, request *http.Request) {
// TODO
}


而在业务文件夹中创建main.go:
1
2
3
4
5
6
7
8
9
func main() {
server := &http.Server{
// 自定义的请求核心处理函数
Handler: framework.NewCore(),
// 请求监听地址
Addr: ":8080",
}
server.ListenAndServe()
}

我们通过自己创建了Server数据结构,并且在数据结构中创建了自定义的Handler(Core数据结构)和监听地址,实现了一个HTTP服务。这个服务的具体业务逻辑都集中在我们自定义的Core结构中,后续我们要做的事情就是不断丰富这个Core数据结构的功能逻辑。

net/http标准库怎么学

1
2
go doc net/http | grep "^func"
go doc net/http | grep "^type"|grep struct

代码分析图

先顺着http.ListenAndServe的脉络读。

第一层http.ListenAndServe本质是通过创建一个Server数据结构,调用server.ListenAndServe对外提供服务,这一层完全是比较简单的封装,目的是,将Server结构创建服务的方法ListenAndServe,直接作为库函数对外提供,增加库的易用性。

1
2
3
4
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}

进入到第二层,创建服务的方法ListenAndServe先定义了监听信息net.Listen,然后调用Serve函数。

1
2
3
4
5
6
7
func (srv *Server) ListenAndServe() error {
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return srv.Serve(ln)
}

而在第三层Serve函数中,用了一个for循环,通过l.Accept不断接收从客户端传进来的请求连接。当接收到了一个新的请求连接的时候,通过srv.newConn创建了一个连接结构(http.conn),并创建一个Goroutine为这个请求连接对应服务(c.serve)。
1
2
3
4
5
6
7
8
9
10
func (srv *Server) Serve(l net.Listener) error {
// ...
for {
rw, err := l.Accept()
// ...
c := srv.newConn(rw)

go c.serve(connCtx)
}
}

从第四层开始,后面就是单个连接的服务逻辑了。

第四层c.serve函数先判断本次HTTP请求是否需要升级为HTTPs,接着创建读文本的reader和写文本的buffer,再进一步读取本次请求数据,然后第五层调用最关键的方法serverHandler{c.server}.ServeHTTP(w,w.req),来处理这次请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func (c *conn) serve(ctx context.Context) {
// ...
if tlsConn, ok := c.rwc.(*tls.Conn); ok {
// ...
}

// HTTP/1.x from here on.
c.r = &connReader{conn: c}
c.bufr = newBufioReader(c.r)
c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10)

for {
w, err := c.readRequest(ctx)
// Expect 100 Continue support
req := w.req

serverHandler{c.server}.ServeHTTP(w, w.req) // 第五层
}
}

这个关键方法是为了实现自定义的路由和业务逻辑,调用写法是比较有意思的:
1
serverHandler{c.server}.ServeHTTP(w, w.req)

serverHandler结构体,是标准库封装的,代表“请求对应的处理逻辑”,它只包含了一个指向总入口服务server的指针。

这个结构将总入口的服务结构Server和每个连接的处理逻辑巧妙联系在一起了,你可以看接着的第六层逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// serverHandler 结构代表请求对应的处理逻辑
type serverHandler struct {
srv *Server
}

// 具体处理逻辑的处理函数
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
handler := sh.srv.Handler
if handler == nil {
handler = DefaultServeMux
}
...
handler.ServeHTTP(rw, req)
}

如果入口服务server结构已经设置了Handler,就调用这个Handler来处理此次请求,反之则使用库自带的DefaultServerMux。

这里的serverHandler设计,能同时保证这个库的扩展性和易用性:你可以很方便使用默认方法处理请求,但是一旦有需求,也能自己扩展出方法处理请求。

那么DefaultServeMux是怎么寻找Handler的呢,这就是思维导图的最后一部分第七层

DefaultServeMux.Handle是一个非常简单的map实现,key是路径(pattern),value是这个pattern对应的处理函数(handler)。它是通过mux.match(path)寻找对应Handler,也就是从DefaultServeMux内部的map中直接根据key寻找到value的。

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
var DefaultServeMux = &defaultServeMux

var defaultServeMux ServeMux

type ServeMux struct {
mu sync.RWMutex
m map[string]muxEntry
es []muxEntry // slice of entries sorted from longest to shortest.
hosts bool // whether any patterns contain hostnames
}

type muxEntry struct {
h Handler
pattern string
}

func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
mux.mu.RLock()
defer mux.mu.RUnlock()

// Host-specific pattern takes precedence over generic ones
if mux.hosts {
h, pattern = mux.match(host + path)
}
if h == nil {
h, pattern = mux.match(path)
}
if h == nil {
h, pattern = NotFoundHandler(), ""
}
return
}

这种根据map直接查找路由的方式是不是可以满足我们的路由需求呢?我们会在第三讲路由中详细解说。

好,HTTP库Server的代码流程我们就梳理完成了,整个逻辑线大致是:

1
创建服务 -> 监听请求 -> 创建连接 -> 处理请求

  • 第一层,标准库创建HTTP服务是通过创建一个Server数据结构完成的;
  • 第二层,Server数据结构在for循环中不断监听每一个连接;
  • 第三层,每个连接默认开启一个Goroutine为其服务;
  • 第四、五层,serverHandler结构代表请求对应的处理逻辑,并且通过这个结构进行具体业务逻辑处理;
  • 第六层,Server数据结构如果没有设置处理函数Handler,默认使用DefaultServerMux处理请求;
  • 第七层,DefaultServerMux是使用map结构来存储和查找路由规则。

思考题

HTTP库提供FileServer来封装对文件读取的HTTP服务。实现代码也非常简单:

1
2
fs := http.FileServer(http.Dir("/home/bob/static"))
http.Handle("/static/", http.StripPrefix("/static", fs))

请问它的主流程逻辑是什么?你认为其中最关键的节点是什么?

0%