Gin 框架的 Web 服务流程
Go 标准库 net/http
提供了基础的 Web 功能,即监听端口,映射静态路由,解析 HTTP 报文。一些 Web 开发中简单的需求并不支持,需要更强大的框架实现:
- 动态路由:例如
hello/:name
,hello/*
这类的规则; - 鉴权:没有分组/统一鉴权的能力,需要在每个路由映射的handler中实现;
- 模板:没有统一简化的HTML机制;
- …
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 != "/" {
...
}
}
...
}
其路由的原理是大量使用公共前缀的树结构,它基本上是一个紧凑的前缀树(字典树),具有公共前缀的节点也共享一个公共父节点。