提笔忘字

Gin 框架的 Web 服务流程

Go 标准库 net/http 提供了基础的 Web 功能,即监听端口,映射静态路由,解析 HTTP 报文。一些 Web 开发中简单的需求并不支持,需要更强大的框架实现:

gin 是一个应用广泛的 Go 语言 Web 框架框架,它基于 htttprouter 实现最重要的路由模块,采用类似字典树一样的数据结构来存储路由与 handle 方法的映射。

Go 语言 Web 服务流程

使用 go 语言内置的 net 包启动的一个 web 服务:

func main() {
   // 注册一个服务
   http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
      fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
   })

   // 监听 8080 端口
   log.Fatal(http.ListenAndServe(":8080", nil))
}

当执行上面的服务 go run main.go 时,此时在 ListenAndServe 方法会先创建一个 Server{Addr: addr, Handler: handler} 结构:

// net/http/server.go

// ListenAndServe listens on the TCP network address addr and then calls
// Serve with handler to handle requests on incoming connections.
// Accepted connections are configured to enable TCP keep-alives.
//
// The handler is typically nil, in which case the DefaultServeMux is used.
//
// ListenAndServe always returns a non-nil error.
func ListenAndServe(addr string, handler Handler) error {
	server := &Server{Addr: addr, Handler: handler}
	return server.ListenAndServe()
}

server 对象的 ListenAndServe 方法会开启 net.Listen 进行监听,然后调用 srv.Serve(ln) 服务:

// net/http/server.go

// ListenAndServe listens on the TCP network address srv.Addr and then
// calls Serve to handle requests on incoming connections.
// Accepted connections are configured to enable TCP keep-alives.
//
// If srv.Addr is blank, ":http" is used.
//
// ListenAndServe always returns a non-nil error. After Shutdown or Close,
// the returned error is ErrServerClosed.
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)
}

Serve 函数中,用了一个 for 循环,通过 l.Accept 不断接收从客户端传进来的请求连接。当接收到了一个新的请求连接的时候,通过 srv.NewConn 创建一个连接结构 http.conn,并创建一个 Goroutine 为这个请求连接对应服务 c.serve。也就是说只要服务端监听到了服务,就会开启一个 goroutine

// net/http/server.go

// Serve accepts incoming connections on the Listener l, creating a
// new service goroutine for each. The service goroutines read requests and
// then call srv.Handler to reply to them.
//
// HTTP/2 support is only enabled if the Listener returns *tls.Conn
// connections and they were configured with "h2" in the TLS
// Config.NextProtos.
//
// Serve always returns a non-nil error and closes l.
// After Shutdown or Close, the returned error is ErrServerClosed.
func (srv *Server) Serve(l net.Listener) error {
	...

	ctx := context.WithValue(baseCtx, ServerContextKey, srv)
	for {
		rw, err := l.Accept()
		if err != nil {
			select {
			case <-srv.getDoneChan():
				return ErrServerClosed
			default:
			}
			if ne, ok := err.(net.Error); ok && ne.Temporary() {
				if tempDelay == 0 {
					tempDelay = 5 * time.Millisecond
				} else {
					tempDelay *= 2
				}
				if max := 1 * time.Second; tempDelay > max {
					tempDelay = max
				}
				srv.logf("http: Accept error: %v; retrying in %v", err, tempDelay)
				time.Sleep(tempDelay)
				continue
			}
			return err
		}
		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, runHooks) // before Serve can return
		go c.serve(connCtx)
	}
}

c.serve 代码量很大,但是只要知道它的功能是判断本次 HTTP 请求是否需要升级为 HTTPs,接着创建读文本的 reader 和写文本的 buffer,再进一步读取本次请求数据。最重要的就是 serverHandler{c.server}.ServeHTTP(w, w.req)

// net/http/server.go

// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
	...

	for {
		...

		// 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.
		inFlightResponse = w
		serverHandler{c.server}.ServeHTTP(w, w.req)
		inFlightResponse = nil
		w.cancelCtx()
		if c.hijacked() {
			return
		}
		w.finishRequest()
		if !w.shouldReuseConnection() {
			if w.requestBodyLimitHit || w.closedRequestBodyEarly() {
				c.closeWriteAndWait()
			}
			return
		}
		c.setState(c.rwc, StateIdle, runHooks)
		c.curReq.Store((*response)(nil))

		...
	}
}

serverHandler{c.server}.ServeHTTP(w, w.req) 这个是最重要的函数,也就是说如果你在服务开始的时候自定义了 handler,那么就使用你自定义的,如果没有,就使用 go 默认的。也就是说,只要传入任何实现了 ServerHTTP 接口的实例,所有的 HTTP 请求,就都交给了该实例处理了。

// net/http/server.go

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{}
	}

	if req.URL != nil && strings.Contains(req.URL.RawQuery, ";") {
		var allowQuerySemicolonsInUse int32
		req = req.WithContext(context.WithValue(req.Context(), silenceSemWarnContextKey, func() {
			atomic.StoreInt32(&allowQuerySemicolonsInUse, 1)
		}))
		defer func() {
			if atomic.LoadInt32(&allowQuerySemicolonsInUse) == 0 {
				sh.srv.logf("http: URL query contains semicolon, which is no longer a supported separator; parts of the query may be stripped when parsed; see golang.org/issue/25192")
			}
		}()
	}

	handler.ServeHTTP(rw, req)
}

经过上面的分析我们也就知道了 go 语言 web 服务的大致流程,如果想要修改 web 服务,只需要自定义 handler 就可以完成了。go 默认的 DefaultServeMux 只是简单的使用 map 结构来存放路由,key 是路径,比如 /hello,value 是具体处理逻辑。这也是一般开发不使用原生 web 的原因。

最后来一个 demo:

package main

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

// Engine is the uni handler for all requests
type Engine struct{}

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
   switch req.URL.Path {
   case "/":
      fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path)
   case "/hello":
      for k, v := range req.Header {
         fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
      }
   default:
      fmt.Fprintf(w, "404 NOT FOUND: %s\n", req.URL)
   }
}

func main() {
   engine := new(Engine)
   log.Fatal(http.ListenAndServe(":9999", engine))
}

为什么 Gin 这么快

gin 框架使用的是定制版本的 httprouter,我们来分析一下 htttprouter 的 demo:

package main

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

    "github.com/julienschmidt/httprouter"
)

func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
    fmt.Fprint(w, "Welcome!\n")
}

func Hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
    fmt.Fprintf(w, "hello, %s!\n", ps.ByName("name"))
}

func main() {
    router := httprouter.New()
    router.GET("/", Index)
    router.GET("/hello/:name", Hello)

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

下面是 httprouter 代码里路由匹配的逻辑:

// router.go

// ServeHTTP makes the router implement the http.Handler interface.
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
   if r.PanicHandler != nil {
      defer r.recv(w, req)
   }

   path := req.URL.Path

   if root := r.trees[req.Method]; root != nil {
      if handle, ps, tsr := root.getValue(path); handle != nil {
         handle(w, req, ps)
         return
      } else if req.Method != http.MethodConnect && path != "/" {
         ...
      }
   }
   
   ...
}

其路由的原理是大量使用公共前缀的树结构,它基本上是一个紧凑的前缀树(字典树),具有公共前缀的节点也共享一个公共父节点。

#Golang #Gin