Go语言http请求过程分析

net/http包

在go中开发后端,最基础的就是使用net/http包,本文我将使用一个hello,world程序来进行debug,来探究在代码内部究竟发生了什么。

go 复制代码
package main

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

func HelloHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello World")
}
func main() {
    http.HandleFunc("/hello", HelloHandler)
    log.Fatal(http.ListenAndServe("localhost:8080", nil))
}

Debug

http.ListenAndServe()

整个程序的启动在于http.ListenAndServe("localhost:8080", nil)这行代码: 传入地址localhost:8080以及nil

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

可以看出这个函数的作用就是封装Server对象的创建并调用server.ListenAndServe()Server对象代表着一个http服务器,内部封装了很多http相关的参数。

从这里可以看出,如果是需要一个快速简单的的http服务器,直接使用http.ListenAndServe("localhost:8080", nil),如果需要更加精细化参数的http服务器,可以这样写:

go 复制代码
    // 创建并配置Server
    server := &http.Server{
        Addr:    ":8080",  // 监听所有接口的8080端口
        Handler: mux,      // 使用自定义多路复用器
        
        // 重要的超时设置
        ReadHeaderTimeout: 5 * time.Second,  // 5秒内必须读完请求头
        ReadTimeout:       10 * time.Second, // 10秒内必须读完整个请求
        WriteTimeout:      10 * time.Second, // 10秒内必须写完响应
        IdleTimeout:       120 * time.Second, // 空闲连接2分钟后关闭
        
        MaxHeaderBytes: 1 << 20, // 请求头最大1MB
    }

    fmt.Println("服务器正在 8080 端口监听...")
    if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
        fmt.Printf("服务器错误: %v\n", err)
    }

server.ListenAndServe()

go 复制代码
func (s *Server) ListenAndServe() error {
    if s.shuttingDown() {
       return ErrServerClosed
    }
    addr := s.Addr
    if addr == "" {
       addr = ":http"
    }
    ln, err := net.Listen("tcp", addr)
    if err != nil {
       return err
    }
    return s.Serve(ln)
}

既然方法叫ListenAndServe,自然便有ListenServe的部分,从源码可以看出ln, err := net.Listen("tcp", addr)在对应的主机端口上启动一个TCP监听器,最后调用s.Serve(ln)

TCP端口监听:Serve(ln)

go 复制代码
func (s *Server) Serve(l net.Listener) error {
    if fn := testHookServerServe; fn != nil {
       fn(s, l) // call hook with unwrapped listener
    }

    origListener := l
    l = &onceCloseListener{Listener: l}
    defer l.Close()

    if err := s.setupHTTP2_Serve(); err != nil {
       return err
    }

    if !s.trackListener(&l, true) {
       return ErrServerClosed
    }
    defer s.trackListener(&l, false)

    baseCtx := context.Background()
    if s.BaseContext != nil {
       baseCtx = s.BaseContext(origListener)
       if baseCtx == nil {
          panic("BaseContext returned a nil context")
       }
    }

    var tempDelay time.Duration // how long to sleep on accept failure

    ctx := context.WithValue(baseCtx, ServerContextKey, s)
    for {
       rw, err := l.Accept()
       if err != nil {
          if s.shuttingDown() {
             return ErrServerClosed
          }
          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
             }
             s.logf("http: Accept error: %v; retrying in %v", err, tempDelay)
             time.Sleep(tempDelay)
             continue
          }
          return err
       }
       connCtx := ctx
       if cc := s.ConnContext; cc != nil {
          connCtx = cc(connCtx, rw)
          if connCtx == nil {
             panic("ConnContext returned nil")
          }
       }
       tempDelay = 0
       c := s.newConn(rw)
       c.setState(c.rwc, StateNew, runHooks) // before Serve can return
       go c.serve(connCtx)
    }
}

源码过长,因此下面的分析中省略不重要的代码

go 复制代码
origListener := l
l = &onceCloseListener{Listener: l}
defer l.Close()
...
if !s.trackListener(&l, true) {
    return ErrServerClosed
}
defer s.trackListener(&l, false)

这部分代码先是备份了先前创建的TCP监听器,然后将其封装为oneCloseListener,随后使用s.trackListener(&l, true)将监听器注册到http服务器中,退出方法时调用s.trackListener(&l, false)将监听器注销。

go 复制代码
baseCtx := context.Background()
if s.BaseContext != nil {
    baseCtx = s.BaseContext(origListener)
    if baseCtx == nil {
       panic("BaseContext returned a nil context")
    }
}

http服务器中的BaseContext是一个回调函数,用于为整个http服务器设置服务器级别的基础上下文。 用法示例如下:

go 复制代码
ctx, cancel := context.WithCancel(context.Background())
server := &http.Server{
    BaseContext: func(l net.Listener) context.Context {
        // 可以在这里为每个连接添加上下文值
        return context.WithValue(ctx, "listener_addr", l.Addr().String())
    },
}
// 将cancel函数注册到实际server的关闭钩子
server.RegisterOnShutdown(cancel)

最后是持续的监听环节

go 复制代码
for {
    rw, err := l.Accept()
    if err != nil {
       if s.shuttingDown() {
          return ErrServerClosed
       }
       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
          }
          s.logf("http: Accept error: %v; retrying in %v", err, tempDelay)
          time.Sleep(tempDelay)
          continue
       }
       return err
    }
    connCtx := ctx
    if cc := s.ConnContext; cc != nil {
       connCtx = cc(connCtx, rw)
       if connCtx == nil {
          panic("ConnContext returned nil")
       }
    }
    tempDelay = 0
    c := s.newConn(rw)
    c.setState(c.rwc, StateNew, runHooks) // before Serve can return
    go c.serve(connCtx)
}

l.Accept()接收到请求后,通过http服务器的ConnContext在原有的上下文的基础上再次设置上下文,之后封装TCPConnhttp连接,并设置状态为StateNew,并允许http服务器中的ConnState钩子触发。最后启动一个协程开始该连接的服务。

trackListener()

以下是trackListener()的源码

go 复制代码
func (s *Server) trackListener(ln *net.Listener, add bool) bool {
    s.mu.Lock()
    defer s.mu.Unlock()
    if s.listeners == nil {
       s.listeners = make(map[*net.Listener]struct{})
    }
    if add {
       if s.shuttingDown() {
          return false
       }
       s.listeners[ln] = struct{}{}
       s.listenerGroup.Add(1)
    } else {
       delete(s.listeners, ln)
       s.listenerGroup.Done()
    }
    return true
}

先判断http服务器中的监听器容器是否已经初始化了,如果没有则分配一个map对象,利用add来判断是增加监听器还是移除监听器。从这份源码可以看出,一个http服务器是支持监听多个端口的。只需要同时运行多个s.Serve(ln)

HTTP请求处理serve()

因为这部分源码实在过多,因此只展示部分代码

css 复制代码
for{
    w, err := c.readRequest(ctx)
    ...
    serverHandler{c.server}.ServeHTTP(w, w.req)
}

方法中有一段循环,实现在同一个TCP连接上用于处理多个HTTP请求,实现HTTP/1.1Keep-Alive特性。
w, err := c.readRequest(ctx)返回一个responserequest被包含在这个response对象中,后续通过serverHandler{c.server}.ServeHTTP(w, w.req)来处理http请求

serverHandler的ServeHTTP()

ini 复制代码
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
    handler := sh.srv.Handler
    if handler == nil {
       handler = DefaultServeMux
    }
    if !sh.srv.DisableGeneralOptionsHandler && req.RequestURI == "*" && req.Method == "OPTIONS" {
       handler = globalOptionsHandler{}
    }

    handler.ServeHTTP(rw, req)
}

此处会取出最早的时候创建http服务器时传入的Handler,如果这个接口对象是nil,将使用默认的DefaultServeMux,最终使用这个来处理http请求

handler的ServeHTTP()

erlang 复制代码
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
    }
    var h Handler
    if use121 {
       h, _ = mux.mux121.findHandler(r)
    } else {
       h, r.Pattern, r.pat, r.matches = mux.findHandler(r)
    }
    h.ServeHTTP(w, r)
}

核心逻辑是根据use121的值来使用新版的findHandler还是旧版的mux121.findHandler,最终会根据请求的URI来寻找到对应的Handler接口对象,本例中是HelloHandler,它是一个HandlerFunc对象 因此调用

scss 复制代码
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

看起来这段代码似乎很多余,为什么不直接在原本的代码转成HnadlerFunc直接调用?

因为这样我们可以不使用HandlerFunc来处理逻辑,比如以下的代码

go 复制代码
type LogMiddler struct{
    next Handler
}
func (logMiddler *LogMiddler) ServeHTTP(w ResponseWriter, r *Request){
    fmt.Println("处理的日期是....")
    logMiddler.next(w,r)
}

LogMiddler实现了Handler接口,因此上述的h可以是这个LogMiddler对象,将原本的HelloHandler封装在LogMiddler中,就实现了一种链式调用,增强原本的HelloHandler。 到目前为止,自定义的路径处理逻辑已经执行,后续都是一些善后处理。

总结

1.http.HandleFunc("/hello", HelloHandler)将处理逻辑注册到DefaultServeMux

2.创建一个http服务器

3.创建一个TCP监听器,并使用http服务器的BaseContext创建初始的context并设置一些其他值

4.当TCP请求到达时,使用http服务器的ConnContextcontext基础上创建当此次TCP连接的context,并设置当前的连接状态为StateNew,如果http服务器设置了ConnState钩子函数,在连接状态变动时会触发。最后开启一个新的协程来处理这个TCP连接

5.使用一个循环不断读取和处理http请求

6.处理http请求阶段,如果创建http服务器时传入nil,则使用DefaultServeMux,然后根据RequestURI来寻找对应的Handler接口对象,最终调用它的ServeHTTP进行处理

相关推荐
Coding君2 小时前
每日一Go-25、Go语言进阶:深入并发模式1
go
X_PENG10 小时前
【Golang】Retry重试实践
go
怕浪猫12 小时前
第17章:反射与泛型编程——运行时能力与代码复用
后端·go·编程语言
石牌桥网管12 小时前
正则表达式:匹配不包含指定字符串的文本
java·javascript·python·正则表达式·go·php
2301_816997881 天前
Go语言基础语法
go
Nyarlathotep01131 天前
Go结构体字段定义
go
2301_816997881 天前
Go语言开发环境搭建
go
2301_816997881 天前
Go语言简介
golang·go