golang net/http 服务端 源码分析

参考文档



基于 net/http web 服务端 示例

package main

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

func main() {
	http.HandleFunc("/", func1)
	http.HandleFunc("/hello", func2)
	log.Fatal(http.ListenAndServe(":6665", nil))
}

func func1(w http.ResponseWriter, req *http.Request){
	fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path)
}

func func2(w http.ResponseWriter, req *http.Request) {
	for k, v := range req.Header {
		fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
	}
}

     这段代码调用 http.HandleFunc 进行了路由注册,再调用ListenAndServe进行监听,运行结果如下

[root@localhost ~]# curl  http://127.0.0.1:6665/
URL.Path = "/"
[root@localhost ~]# 


关键过程分析

路由注册

几个关键的数据结构,和变量

//保存服务端路由映射
type ServeMux struct {
	mu    sync.RWMutex
	m     map[string]muxEntry
	hosts bool // whether any patterns contain hostnames
}
//记录一条路由映射
type muxEntry struct {
	h       Handler
	pattern string
}
// http 内置默认路由.
var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux

//函数类型,我们示例代码中, 传入的函数变量,就是该数据类型
type HandlerFunc func(ResponseWriter, *Request)
//注意HandlerFunc实现了ServeHTTP方法
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
	f(w, r)
}

//接口类型,实现ServeHTTP函数, 这是实际处理客户端请求的函数,非常重要
type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

api调用跟踪

http.HandleFunc("/", func1)

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	DefaultServeMux.HandleFunc(pattern, handler)
}
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	mux.Handle(pattern, HandlerFunc(handler)) //将变量,转化成 type HandlerFunc
}
func (mux *ServeMux) Handle(pattern string, handler Handler) {
	mux.mu.Lock()
	defer mux.mu.Unlock()
	//此处省略很多代码
	mux.m[pattern] = muxEntry{h: handler, pattern: pattern} //建立url 与 处理函数的映射
	//此处省略很多代码
}

自此,路由注册完成, 路由信息,应该会全部注册到 DefaultServeMux 变量。
下面我们调试一下,确认我们的分析。

[root@localhost httpserver]# dlv debug main.go  --check-go-version=false
Type 'help' for list of commands.
(dlv) b main.go:12
Breakpoint 1 set at 0x6e6e59 for main.main() ./main.go:12
(dlv) c
> main.main() ./main.go:12 (hits goroutine(1):1 total:1) (PC: 0x6e6e59)
     7:	)
     8:	
     9:	func main() {
    10:		http.HandleFunc("/", func1)
    11:		http.HandleFunc("/hello", func2)
=>  12:		log.Fatal(http.ListenAndServe(":6665", nil))
    13:	}
    14:	
    15:	func func1(w http.ResponseWriter, req *http.Request){
    16:		fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path)
    17:	}
(dlv) p http.DefaultServeMux
*net/http.ServeMux {
	mu: sync.RWMutex {
		w: (*sync.Mutex)(0x8eba60),
		writerSem: 0,
		readerSem: 0,
		readerCount: 0,
		readerWait: 0,},
	m: map[string]net/http.muxEntry [
		"/": (*"net/http.muxEntry")(0xc420012a48), 
		"/hello": (*"net/http.muxEntry")(0xc420012a68), 
	],
	hosts: false,}
(dlv) q
[root@localhost httpserver]# 


疑问点

  传入的函数变量,为什么不是直接和url映射, 而是经过 2次 转换,第一次转换为HandlerFunc, 第二次转换为 handler,即 转换为一个接口类型。
  因为我们的例子中,是传入一个函数变量,单单传一个函数过去, 能做的事情太少了。实际应用会复杂一些。如果有看过相关web框架源码就知道,一般不会使用默认的ServeMux。
  如果传一个结构体过去,可以实现更复杂的功能,当结构体作为一个合法参数。这种方式适用于逻辑较为复杂的场景,如果对数据库的操作需要很多信息,地址、用户名、密码,还有很多中间状态需要保持,比如超时、重连、加锁等等。这种情况下,更适合封装为一个结构体作为参数。
  上述也称为接口函数,既能够将普通的函数类型(需类型转换)作为参数,也可以将结构体作为参数,使用更为灵活,可读性也更好,这就是接口型函数的价值。
也可以参考下这篇博文 https://geektutu.com/post/7days-golang-q1.html




服务端响应分析

使用很广泛的apache使用的是多进程模型, apache会维护一个进程池, 新的请求到来,就取一个进程去处理,或者根据策略创建一个新的子进程。
golang 使用的当然就是 go 协程了, 分析一下它是怎么响应请求到。

几个关键数据结构

// A Server defines parameters for running an HTTP server.
// The zero value for Server is a valid configuration.
type Server struct {
	Addr    string  // TCP address to listen on, ":http" if empty
	Handler Handler // handler to invoke, http.DefaultServeMux if nil
	TLSConfig *tls.Config
	ReadTimeout time.Duration
	ReadHeaderTimeout time.Duration
	WriteTimeout time.Duration
	IdleTimeout time.Duration
	MaxHeaderBytes int
	TLSNextProto map[string]func(*Server, *tls.Conn, Handler)
	ConnState func(net.Conn, ConnState)
	ErrorLog *log.Logger
	disableKeepAlives int32     // accessed atomically.
	inShutdown        int32     // accessed atomically (non-zero means we're in Shutdown)
	nextProtoOnce     sync.Once // guards setupHTTP2_* init
	nextProtoErr      error     // result of http2.ConfigureServer if used
	mu         sync.Mutex
	listeners  map[net.Listener]struct{}
	activeConn map[*conn]struct{}
	doneChan   chan struct{}
	onShutdown []func()
}
// A conn represents the server side of an HTTP connection.
type conn struct {
	server *Server
	cancelCtx context.CancelFunc
	rwc net.Conn
	remoteAddr string
	tlsState *tls.ConnectionState
	werr error
	r *connReader
	bufr *bufio.Reader
	bufw *bufio.Writer
	lastMethod string
	curReq atomic.Value
	curState atomic.Value
	mu sync.Mutex
	hijackedv bool
}
//就是封装的一个server 指针
type serverHandler struct {
	srv *Server
}

先从示例代码的 http.ListenAndServe(":6665", nil) 开始分析“

记住我们传入的Handler 是nil

   func ListenAndServe(addr string, handler Handler) error {
	server := &Server{Addr: addr, Handler: handler}
	return server.ListenAndServe()
}
func (srv *Server) ListenAndServe() error {
	addr := srv.Addr
	if addr == "" {
		addr = ":http"
	}
	ln, err := net.Listen("tcp", addr)
	if err != nil {
		return err
	}
	return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
}
//监听到请求, 通过go 协程处理: go c.serve(ctx)
func (srv *Server) Serve(l net.Listener) error {
	//此处省略很多代码
	for {
		rw, e := l.Accept()
		//此处省略很多代码
		c := srv.newConn(rw)
		c.setState(c.rwc, StateNew) // before Serve can return
		go c.serve(ctx)
	}
}

这就是章节开头说的, apache是过进程模型, 而net/http 就是靠go 协程去并发处理请求。
这里又有一个疑问,我们用apache时, 内部代码异常了,即使没有catch, web服务也不会退出,因为多进程模型,子进程异常退出,并不会导致父进程退出,
那用net/http时, 假如在我们的示例代码中出现panic, web服务也是不会退出的(这里就不贴测试代码了) 那它时怎么是实现的, 我猜肯定时用到了 defer + recover
我们继续分析web服务处理请求的过程。

处理过程最终

go c.serve(ctx)

调用newConn返回的是什么呢

func (srv *Server) newConn(rwc net.Conn) *conn {
	c := &conn{
		server: srv,
		rwc:    rwc,
	}
	if debugServerConnections {
		c.rwc = newLoggingConn("server", c.rwc)
	}
	return c
}

c.server的具体实现中就使用了 defer + revocer, 如下:

// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
	..........................................
	defer func() {
		if err := recover(); err != nil && err != ErrAbortHandler {
			const size = 64 << 10
			buf := make([]byte, size)
			buf = buf[:runtime.Stack(buf, false)]
			c.server.logf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)
		}
		if !c.hijacked() {
			c.close()
			c.setState(c.rwc, StateClosed)
		}
	}()

  //此处省略了很多代码

  //留意注意这里的for循环, 长连接时, 这个go协程就不会退出
  //当时最终调用的处理函数是: serverHandler{c.server}.ServeHTTP(w, w.req)
	for {
		w, err := c.readRequest(ctx)
		
        //此处省略了很多代码
    
		// HTTP cannot have multiple simultaneous active requests.[*]
		// Until the server replies to this request, it can't read another,
		// so we might as well run the handler in this goroutine.
		// [*] Not strictly true: HTTP pipelining. We could let them all process
		// in parallel even if their responses need to be serialized.
		// But we're not going to implement HTTP pipelining because it
		// was never deployed in the wild and the answer is HTTP/2.

		serverHandler{c.server}.ServeHTTP(w, w.req)
        
        //此处省略了很多代码
        
		if !w.conn.server.doKeepAlives() {
			// We're in shutdown mode. We might've replied
			// to the user without "Connection: close" and
			// they might think they can send another
			// request, but such is life with HTTP/1.1.
			return
		}
    //此处省略了很多代码

	}
}

继续跟踪 serverHandler{c.server}.ServeHTTP(w, w.req)

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
	handler := sh.srv.Handler
	if handler == nil {  //示例代码。我们传入的就是nil
		handler = DefaultServeMux
	}
	if req.RequestURI == "*" && req.Method == "OPTIONS" {
		handler = globalOptionsHandler{}
	}
	handler.ServeHTTP(rw, req)  
}

DefaultServeMux的类型是 ServeMux, 继续跟踪

func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
	if r.RequestURI == "*" {
		if r.ProtoAtLeast(1, 1) {
			w.Header().Set("Connection", "close")
		}
		w.WriteHeader(StatusBadRequest)
		return
	}
	h, _ := mux.Handler(r) //  找到最终 Handler
	h.ServeHTTP(w, r)   // 调用ServeHTTP方法
}

怎么找到Handler的呢

func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
     //此处省略了很多代码
	return mux.handler(host, r.URL.Path)
}


func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
	mux.mu.RLock()
	defer mux.mu.RUnlock()
 //此处省略了很多代码
		h, pattern = mux.match(path)
   //此处省略了很多代码
}

// Find a handler on a handler map given a path string.
// Most-specific (longest) pattern wins.
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
	// Check for exact match first.
	v, ok := mux.m[path]
	if ok {
		return v.h, v.pattern
	}
 //此处省略了很多代码
}

还记得最开始的那个数据结构吗

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

我们注册是,传入的函数变量经过2次转换,转换成Handler 类型,其ServeHTTP方法就是其函数变量 自身。

其实, 一些web框架, 就是传入的 handler 是我们自己定义的而,不是传nil(最终会使用DefaultServeMux)

http.ListenAndServe(":6665", nil)
|
|
——————
func ListenAndServe(addr string, handler Handler) error {
	server := &Server{Addr: addr, Handler: handler}
	return server.ListenAndServe()
}
|
|
——————
	handler := sh.srv.Handler
	if handler == nil {
		handler = DefaultServeMux
	}

至此,路由注册, 根据url找到对应api, 并执行, 这一些列过程, 已经分析完成。
整个分析过程,我们使用的是net/http的默认路由,api追踪过程,也是做了很多理想情况的假设
我们分析了 net/http 默认路由中注册过程, 服务端如何实现并发处理请求,以及如何进行异常恢复,以及长连接保持
后续还会对httpclient, 长连接, context的使用做分析