• Welcome to Journal web site.

我是 PHP 程序员

- 开发无止境 -

没有了
Prev

Go语言标准库之net/http(四) —— Server

Data: 2020-01-27 02:08:34Form: JournalClick: 4

基于 HTTP 构建的网络应用包括两个端,即客户端 ( Client ) 和服务端 ( Server )。

两个端的交互行为包括从客户端发出 request、服务端接受 request 进行处理并返回 response 以及客户端处理 response。所以 http 服务器的工作就在于如何接受来自客户端的 request,并向客户端返回 response, 如下图所示:

上一章节内容,主要对net/http包的Client部分进行了介绍,这章节就对 Server内容进行介绍。

HTTP Server简单实现

对于 golang 来说,利用 net/http 包实现一个Http Server非常简单,只需要简简单单几句代码就可以实现,先看看 Golang 的其中一种 http server简单的实现:

例1:

package main

import (
    "fmt"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "hello World!")
}

func main() {
    //注册路由
    http.HandleFunc("/", handler)
    //创建服务且监听
    http.ListenAndServe(":8080", nil)
}

再来看看另外一种http server实现,代码如下:

例2:

package main

import (
    "fmt"
    "net/http"
)

type routeIndex struct {
    content string
}

func (route *routeIndex) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, route.content)
}

func main() {
    //注册路由
    http.Handle("/", &routeIndex{content: "Hello, World"})
    //创建服务且监听
    http.ListenAndServe(":8080", nil)
}

上述两种方法都实现了简单的 http server实现,写法虽然不同,但底层用到的原理其实都是一样的,我们通过源码进行解析。

在上述两种实现种,分别调用了 http.Handlehttp.HandleFunc 来实现路由的处理,展开源码看看:

func Handle(pattern string, handler Handler) { DefaultServeMux.Handle(pattern, handler) }

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    DefaultServeMux.HandleFunc(pattern, handler)
}

func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    if handler == nil {
        panic("http: nil handler")
    }
    mux.Handle(pattern, HandlerFunc(handler))
}

func (mux *ServeMux) Handle(pattern string, handler Handler) {
  ....
}

总结 http.Handlehttp.HandleFunc 函数代码,可以得出以下几个重点内容: DefaultServeMuxServeMuxHandler 以及 ServeMux.Handle函数,下面逐一展开说明。

路由注册

ServeMux

ServerMuxnet/http 包中的一个路由器(router),是一个 HTTP 请求多路复用器,用于将收到的 HTTP 请求路由到相应的处理程序(handlers)。

ServerMux 中,我们可以通过调用 HandleFunc 或者 Handle 方法来注册一个路由和对应的处理函数。当一个请求到达 ServerMux 时,路由器会根据请求的 URL 路径找到对应的处理函数,并将请求转发给该函数进行处理。

ServerMux 结构体定义如下:

type ServeMux struct {
    mu    sync.RWMutex        //用于保证并发安全性的互斥锁
    m     map[string]muxEntry //一个映射表,将URL模式映射到对应的处理程序。在处理HTTP请求时,ServeMux将使用此映射表来查找与请求URL路径匹配的处理程序
    es    []muxEntry          //一个按长度排序的URL模式条目的切片。这个切片是用来加速ServeMux的URL匹配操作的。在处理HTTP请求时,ServeMux会按照长度递减的顺序迭代这个切片,以便找到最长的匹配URL模式
    hosts bool                //标志位,表示ServeMux是否具有任何带有主机名的URL模式。如果是,则在处理HTTP请求时,ServeMux还需要匹配主机名。如果不是,则可以忽略主机名匹配
}

ServeMux 是一个非常重要的组件,用于将HTTP请求路由到正确的处理程序,并且在Go标准库中被广泛使用。在ServeMux 中,还有一个 muxEntry 结构,muxEntryServeMux 内部维护的数据结构,用于将 URL 路径模式与处理程序相关联。定义代码如下:

type muxEntry struct {
    h       Handler //一个处理程序,它是用于处理与该URL模式匹配的HTTP请求的函数
    pattern string  //与该处理程序相关联的URL模式。在ServeMux中,pattern是映射到处理程序的关键字之一。在匹配请求路径时,ServeMux将使用pattern来判断请求是否与该条目匹配。
}

muxEntry结构体用于将URL模式与处理程序相关联,以便在处理HTTP请求时能够正确地路由请求。

再来看看 DefaultServeMux ,可以看到 http.Handlehttp.HandleFunc 这两个函数最终都由 DefaultServeMux 调用 Handle 方法来完成路由的注册的,该变量定义如下:

var defaultServeMux ServeMux
var DefaultServeMux = &defaultServeMux

这里的 DefaultServeMux 表示一个默认的 ServeMux,当我们没有创建自定义的 ServeMux,则会自动使用一个默认的 ServeMux

自定义 ServeMux

我们可以创建自定义的 ServeMux 取代默认的 DefaultServeMux,示例代码如下:

package main

import (
    "fmt"
    "net/http"
)

type routeIndex struct {
    content string
}

func (route *routeIndex) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, route.content)
}

func htmlHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "text/html")
    html := `<!doctype  html>  
    <META  http-equiv="Content-Type"  content="text/html"  charset="utf-8">    
    <html  lang="zhCN">  
            <head>   
                    <title>Golang</title>  
                    <meta  name="viewport"  content="width=device-width,  initial-scale=1.0,  maximum-scale=1.0,  user-scalable=0;"  />   
            </head>     
            <body>        
            <div  id="app">Hello, HandleFunc World!</div>       
            </body>   
    </html>`
    fmt.Fprintf(w, html)
}

func main() {
    //自定义serveMux
  mux := http.NewServeMux()
    mux.Handle("/Handle", &routeIndex{content: "Hello, Handle World"})
    mux.HandleFunc("/HandleFunc", htmlHandler)
    //创建服务且监听
    http.ListenAndServe(":8080", mux)
}

http.NewServeMux 方法用于创建一个新的 ServeMux 实例,如果调用的是自定义 ServeMux 实例 mux,那么 Server 实例接收到的路由对象将不再是 DefaultServeMux 而是 mux

Handler

了解完 ServeMux,再来看看 Handler对象:

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

Handler对象是个接口,Handler 接口中声明了名为 ServeHTTP 的函数,也就是说任何结构只要实现了这个 ServeHTTP 方法,那么这个结构体就是一个 Handler 对象。http.Handler.ServeHTTP 方法是用来是用以处理 request 并构建 response 的核心逻辑所在。

总结起来一句话,要完成完整的http server 服务,必须完成对Handler接口的实现,即对 http.Handler.ServeHTTP 方法的实现。

Handler对象有了大概了解后,回到 http.Handlehttp.HandleFunc 函数,看看他们是怎么实现 Handler 接口的。

先看func Handle(pattern string, handler Handler)函数, 其第二个参数必须为Handler接口的实现,所以调用该函数则必须先自行完成对Handler接口的实现,方式具体参考示例2。

再看看func HandleFunc(pattern string, handler func(ResponseWriter, *Request))函数,发现第二个参数只需传入满足它参数的一个函数即可,并不需要用户自行去实现 Handler接口的,究其原因,我们一看源码就明白了:

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    DefaultServeMux.HandleFunc(pattern, handler)
}

func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    if handler == nil {
        panic("http: nil handler")
    }
    mux.Handle(pattern, HandlerFunc(handler))
}

注意一下这行代码:mux.Handle(pattern, HandlerFunc(handler)),这里 HandlerFunc 实际上是将 handler 函数做了一个类型转换,看一下 HandlerFunc 的定义:

type HandlerFunc func(ResponseWriter, *Request)

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

看到这里,应该最终明白了把,原来这里 HandlerFunc 实际上已经默认实现了Handler接口,实现了它的ServeHTTP函数,用户只需要传入 handler 函数即可被HandlerFunc 强行转为一个Handler对象,这是一种巧妙的转换技巧,不需要定义一个结构体,再让这个结构实现 ServeHTTP 方法。

路由绑定

ServeMux内部维护一个map[string]muxEntry,该map作用将URL模式映射到对应的muxEntry结构体,而muxEntry结构体则将处理程序与URL模式相关联。

ServeMux是如何将将URL模式映射到对应的muxEntry结构体的呢?

通过调用 http.Handlehttp.HandleFunc 函数完成映射的。而这两个函数,最终调用的方法的是: ServeMux.Handle, 其代码如下:

func (mux *ServeMux) Handle(pattern string, handler Handler) {
    //互斥锁,解决 多个goroutine 并发访问时的线程安全
    mux.mu.Lock()
    defer mux.mu.Unlock()

    //检查参数的合法性,如果有不合法的参数,则会抛出 panic 异常
    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)
    }
    //初始化ServeMux内部保存URL路径模式和处理程序之间映射关系的map(ServeMux.m),如果该 map 还未被初始化,则会在此处进行初始化
    if mux.m == nil {
        mux.m = make(map[string]muxEntry)
    }
    //将URL路径模式和处理程序建立映射关系
    //首先,创建一个 muxEntry 结构体,保存处理程序和 URL 路径模式
    //然后,将该 muxEntry 对象加入到 ServeMux 内部维护的 map 中
    e := muxEntry{h: handler, pattern: pattern}
    mux.m[pattern] = e
    //如果URL路径模式的最后一个字符是斜杠(即该 URL 路径模式对应的处理程序是一个目录),则将该 muxEntry 对象插入到 ServeMux.es中
    if pattern[len(pattern)-1] == '/' {
        mux.es = appendSorted(mux.es, e)
    }
    //如果URL路径模式的第一个字符不是斜杠(即该 URL 路径模式对应的处理程序是一个主机名),则将 ServeMux 的 hosts 字段设置为 true
    if pattern[0] != '/' {
        mux.hosts = true
    }
}

上述函数主要作用是:

  • URL 路径模式和处理程序建立映射关系,并将映射关系保存到 ServeMux 的内部数据结构中
  • 同时,该函数还会对传入的参数进行一些合法性检查,如 URL 路径模式和处理程序不能为空,URL 路径模式不能重复等
  • 最后,该函数还会将 URL 路径模式按照长度从长到短排序,并标记 ServeMux 是否支持主机名路由。

最后用一张图来总结整个注册路由流程:

 

ServerMux

请求处理

处理完路由相关信息注册,就要进行TCP监听服务启动以及TCP 连接并处理请求了。标准库提供的 net/http.ListenAndServe可以用来监听 TCP 连接并处理请求,该函数会使用传入的监听地址和处理器初始化一个 HTTP 服务器 net/http.Server,调用该服务器的 net/http.Server.ListenAndServe方法:

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

上述代码先创建了一个 Server 对象,传入了地址和 handler 参数后调用 Server 对象 ListenAndServe() 方法。

特别需要注意的一点是:该函数的第二个参数是 Handler 类型,不管是一个新的 ServeMux 对象mux,还是默认的 DefaultServeMuxServeMux其本身自己也实现了 Handler 接口,也实现了ServeHTTP方法,也是一个 Handler 对象,但 ServeMuxServeHTTP() 方法主要作用是匹配当前路由对应的 handler 方法,与自定义的http.Handler.ServeHTTP 方法用以处理 request 并构建 response 功能区别不同

下面逐一分析。

Server

server结构体

net/http.ServerHTTP 服务器的主要结构体,用于控制 HTTP 服务器的行为。

其结构体定义为:

type Server struct {
  //服务器监听的地址和端口号,格式为 "host:port",例如 "127.0.0.1:8080"
    Addr                         string
  //HTTP 请求的处理器。对于每个收到的请求,服务器会将其路由到对应的处理器进行处理。通常使用 http.NewServeMux() 方法创建一个默认的多路复用器,并将其作为处理器。如果没有设置该字段,则使用 DefaultServeMux
    Handler                      Handler
  //一个布尔值,用于指示是否禁用 OPTIONS 方法的默认实现。如果该值为 true,则在收到 OPTIONS 请求时,服务器不会自动返回 Allow 头部,而是交给用户自行处理。默认为 false,即启用 OPTIONS 方法的默认实现
    DisableGeneralOptionsHandler bool
  //HTTPS 服务器的 TLS 配置,用于控制 HTTPS 服务器的加密方式、证书、密钥等安全相关的参数
    TLSConfig                    *tls.Config
  //HTTP 请求的读取超时时间。如果服务器在该时间内没有读取到完整的请求,就会关闭连接。该字段为 time.Duration 类型,默认为 0,表示没有超时限制
    ReadTimeout                  time.Duration
  //HTTP 请求头部读取超时时间。如果服务器在该时间内没有完成头部读取,就会关闭连接。该字段为 time.Duration 类型,默认为 0,表示没有超时限制
    ReadHeaderTimeout            time.Duration
  //HTTP 响应的写入超时时间。如果服务器在该时间内没有完成对响应的写入操作,就会关闭连接。该字段为 time.Duration 类型,默认为 0,表示没有超时限制
    WriteTimeout                 time.Duration
  //HTTP 连接的空闲超时时间。如果服务器在该时间内没有收到客户端的请求,就会关闭连接。该字段为 time.Duration 类型,默认为 0,表示没有超时限制
    IdleTimeout                  time.Duration
  //HTTP 请求头部的最大大小。如果请求头部的大小超过该值,服务器就会关闭连接。该字段为 int 类型,默认为 1 << 20(1MB)
    MaxHeaderBytes               int
    TLSNextProto                 map[string]func(*Server, *tls.Conn, Handler)
  //连接状态变化的回调函数,用于处理连接的打开、关闭等事件
    ConnState                    func(net.Conn, ConnState) 
   //错误日志的输出目标。如果该字段为 nil,则使用 log.New(os.Stderr, "", log.LstdFlags) 创建一个默认的日志输出目标
    ErrorLog                     *log.Logger
  //所有 HTTP 请求的基础上下文。当处理器函数被调用时,会将请求的上下文从基础上下文派生出来。默认为 context.Background()。
    BaseContext                  func(net.Listener) context.Context 
  //连接上下文的回调函数,用于创建连接上下文。每个连接上下文都与一个客户端连接相关联。如果未设置该字段,则每个连接的上下文都是 BaseContext 的副本
    ConnContext                  func(ctx context.Context, c net.Conn) context.Context
  //标志变量,用于表示服务器是否正在关闭。该变量在执行 Shutdown 方法时被设置为 true,用于避免新的连接被接受
    inShutdown                   atomic.Bool 
  //标志变量,用于控制服务器是否支持 HTTP keep-alive。如果该变量为 true,则服务器在每次响应完成后都会关闭连接,即不支持 keep-alive。如果该变量为 false,则服务器会根据请求头部中的 Connection 字段来决定是否支持 keep-alive。该变量在执行 Shutdown 方法时被设置为 true,用于关闭正在进行的
    disableKeepAlives            atomic.Bool
  // 一个 sync.Once 类型的值,用于确保在多线程环境下,NextProtoOnce 方法只被调用一次。NextProtoOnce 方法用于设置 Server.NextProto 字段
    nextProtoOnce                sync.Once 
  // error 类型的值,用于记录 NextProto 方法的调用结果。该值在多个 goroutine 之间共享,用于检测 NextProto 方法是否成功
    nextProtoErr                 error 
  //互斥锁,用于保护 Server 结构体的字段。因为 Server 结构体可能被多个 goroutine 并发访问,所以需要使用互斥锁来确保它们的安全性
    mu                           sync.Mutex    
   //存储 HTTP 或 HTTPS 监听器的列表。每个监听器都是一个 net.Listener 接口类型的实例,用于接收客户端请求。当调用 Server.ListenAndServe() 或 Server.ListenAndServeTLS() 方法时,会为每个监听地址创建一个对应的监听器,并将其添加到该列表中
    listeners                    map[*net.Listener]struct{}
  //表示当前处于活动状态的客户端连接的数量。该字段只是一个计数器,并不保证一定准确。该字段用于判断服务器是否处于繁忙状态,以及是否需要动态调整服务器的工作负载等
    activeConn                   map[*conn]struct{}                                    
  //在服务器关闭时执行的回调函数列表。当服务器调用 Server.Shutdown() 方法时,会依次执行该列表中的每个回调函数,并等待它们全部执行完毕。该字段可以用于在服务器关闭时释放资源、保存数据等操作
    onShutdown                   []func()                                              
  //表示所有监听器的组。该字段包含一个读写互斥锁 sync.RWMutex 和一个映射表 map[interface{}]struct{}。在监听器启动时,会将监听器地址作为键添加到映射表中。该字段主要用于实现优雅地关闭服务器。在服务器关闭时,会遍历所有监听器,逐个关闭它们,并等待所有连接关闭。如果在等待连接关闭时,有新的连接进来,服务器会先将新连接添加到 activeConn 字段中,并等待所有连接关闭后再退出。这样可以保证服务器在关闭过程中,不会丢失任何连接
    listenerGroup                sync.WaitGroup                                        
}

*Server.ListenAndServer

当创建完 一个 Server 对象后,调用 Server 对象 ListenAndServe() 方法会使用网络库提供的 net.Listen监听对应地址上的 TCP 连接并通过 net/http.Server.Serve处理客户端的请求:

func (srv *Server) ListenAndServe() error {
    //判断服务器是否正在关闭,如果是,则返回
    if srv.shuttingDown() {
        return ErrServerClosed
    }
    //获取服务器监听的地址
    addr := srv.Addr
    //如果服务器监听的地址为空,将其设置为 ":http"
    if addr == "" {
        addr = ":http"
    }
    //创建一个 TCP 监听器,监听指定的地址,如果创建监听器时出现错误,返回错误
    ln, err := net.Listen("tcp", addr)
    if err != nil {
        return err
    }
    //使用 Serve() 方法开始监听并处理连接,返回处理连接时可能出现的错误
    return srv.Serve(ln)

                
                
                
                
                
                
              
Name:
<提交>