一文带你分析Go语言中的http请求过程

Written by

in

文章目录
  • 在go中开发后端,最基础的就是使用net/http包,本文我将使用一个hello,world程序来进行debug,来探究在代码内部究竟发生了什么。 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)) }
  • 1.http.HandleFunc("/hello", HelloHandler)将处理逻辑注册到DefaultServeMux 2.创建一个http服务器 3.创建一个TCP监听器,并使用http服务器的BaseContext创建初始的context并设置一些其他值 4.当TCP请求到达时,使用http服务器的ConnContext在context基础上创建当此次TCP连接的context,并设置当前的连接状态为StateNew,如果http服务器设置了ConnState钩子函数,在连接状态变动时会触发。最后开启一个新的协程来处理这个TCP连接 5.使用一个循环不断读取和处理http请求 6.处理http请求阶段,如果创建http服务器时传入nil,则使用DefaultServeMux,然后根据RequestURI来寻找对应的Handler接口对象,最终调用它的ServeHTTP进行处理 以上就是一文带你分析Go语言中的http请求过程的详细内容,更多关于Go语言http请求的资料请关注风君子博客其它相关文章! 您可能感兴趣的文章: go中使用curl实现https请求的示例代码 Go实现HTTP请求转发的示例代码 Go语言中最便捷的http请求包resty的使用详解 Go语言封装HTTP请求的Curl工具包详解 go进行http请求偶发EOF问题分析
  • 目录
    • net/http包
    • Debug
      • http.ListenAndServe()
      • server.ListenAndServe()
      • TCP端口监听:Serve(ln)
      • HTTP请求处理serve()
    • 总结

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

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

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

      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服务器,可以这样写:

          // 创建并配置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("服务器错误: %vn", err)
          }
      

      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)

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

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

      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)将监听器注销。

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

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

      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)
      

      最后是持续的监听环节

      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()的源码

      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)

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

      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()

      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()

      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对象 因此调用

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

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

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

      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进行处理

      以上就是一文带你分析Go语言中的http请求过程的详细内容,更多关于Go语言http请求的资料请关注风君子博客其它相关文章!

      您可能感兴趣的文章:

      • go中使用curl实现https请求的示例代码
      • Go实现HTTP请求转发的示例代码
      • Go语言中最便捷的http请求包resty的使用详解
      • Go语言封装HTTP请求的Curl工具包详解
      • go进行http请求偶发EOF问题分析

      站内搜索