Golang | HTTP工作流程

1,069 阅读11分钟

默认是长连接的,可以设置server.SetKeepAlivesEnabled(false)

一、基础认识

1、类型和结构体

Multiplexer: 多路复用器,也实现了ServeHTTP接口,是一个特殊的处理器,负责将请求转发给对应的路由处理器 Handle: 根据路径,注册一个处理器(是一个func来的) HandleFunc: 处理器函数,对Handle进行一次封装,方便程序员调用(是一个func来的) Handler: 是一个声明了ServerHTTP函数的接口 HandlerFunc: 一个类型,是func(ResponseWriter, *Request)类型的一个别名

  • Handler接口
//任何实现http.Handler接口的对象都可作为一个处理器。
//(那么我就可以自己写一个实现了Handler接口的结构体)
type Handler interface {
 ServeHTTP(ResponseWriter, *Request)
}
  • Handler处理器
//HandlerFunc实现了Handler接口,可以作为处理器
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request)
 {
 f(w, r)
}

//Handle 和 HandleFunc(是两个函数)
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
 if handler == nil {
  panic("http: nil handler")
 }
 mux.Handle(pattern, HandlerFunc(handler))
}

//使用对比
mux.HandleFunc("/",helloworld)
mux.Handle("/",http.HandlerFunc(helloworld))//强制类型转换

经过HandlerFunc结构包装的handler函数,它实现了ServeHTTP接口方法的函数。
(ServeHTTP方法就是调用handler本身而已,本质上是handler)
调用handler处理器的ServeHTTP方法时,即调用handler函数本身。

//我们自己实现的func(ResponseWriter, *Request)函数没有实现Handler接口,所以要加上这一步转化
  • ServeMux
//它也实现了ServeHTTP接口,也算是一个Handler,不过不是用来处理请求的,而是用来注册路由的。
type ServeMux struct {
    mu    sync.RWMutex
    m     map[string]muxEntry //string就是路由,对应的muxEntry里就存放着一个Handler
    hosts bool 
}

type muxEntry struct {
    explicit bool
    h        Handler
    pattern  string //路由
}
  • Server
type Server struct {
    Addr         string        
    Handler      Handler       //传入Multiplexer
    ReadTimeout  time.Duration 
    WriteTimeout time.Duration 
    TLSConfig    *tls.Config   

    MaxHeaderBytes int

    TLSNextProto map[string]func(*Server, *tls.Conn, Handler)

    ConnState func(net.Conn, ConnState)
    ErrorLog *log.Logger
    disableKeepAlives int32     nextProtoOnce     sync.Once 
    nextProtoErr      error     
}

2、使用http包默认的结构体创建服务

//http.HandleFunc调用的是,http包默认的mux
//由这个默认的mux去调用mux.Handle
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
 DefaultServeMux.HandleFunc(pattern, handler)
}

==========================================================

//使用http.Server 即可创建自定义的server对象:
//自定义的serverMux对象也可以传到server对象中。
mux := http.NewServeMux()
mux.HandleFunc("/",helloworld)
server := &http.Server{
    Addr: ":8000",
    ReadTimeout: 60 * time.Second,
    WriteTimeout: 60 * time.Second,
    Handler: mux,
}
server.ListenAndServe()//底层也是net.Listen

3、自定义的Handler结构体

//自己实现的Handler结构体,即可以作为server也可以作为处理器
type myHandle struct {}

func(this *myHandle) ServeHTTP(w http.ResponseWriter, r *http.Request){
 logs.Debug(r.URL.Path)
}

//作为处理器时
http.Handle("/test",&myHandle{})

//当作为server时,只有你访问8080,都会走你自己实现的ServeHTTP,那么你就可以在里面做一些统一性的东西,比如IP黑名单
//当然,要统一处理也可以使用中间件,中间件可以放在mux之前也可以放在mux之后。
http.ListenAndServe(":8080",&myHandle{})

二、http工作流程

1、基本流程

client -> Request -> Multiplexer(router)->handler ->Response -> client

2、构成剖析

Serve部分
//该函数作用是创建一个serve,并监听
func ListenAndServe(addr string, handler Handler) error {
 server := &Server{Addr: addr, Handler: handler}
 return server.ListenAndServe()
}

func (srv *Server) ListenAndServe() error {
    ln, err := net.Listen("tcp", addr)//TCP拨号
 ...
    return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})//默认长连接
}

func (srv *Server) Serve(l net.Listener) error {
    for {
  rw, e := l.Accept() //tcp接收连接
        ....
        c := srv.newConn(rw)
     c.setState(c.rwc, StateNew) // before Serve can return
  go c.serve(ctx) //新的连接,开一个协程
        //(如果连接已经存在,是不会走到这里的,长连接啊,复用的啊,连接在另一个协程啊)
    }
}

func (c *conn) serve(ctx context.Context) {
    defer func() {//关闭连接
     if !c.hijacked() {
   c.close()
   c.setState(c.rwc, StateClosed)
  }
    }()
    ...
 for { //循环读取请求,也就是长连接的话,可以复用同一条TCP连接
  w, err := c.readRequest(ctx)
        ...
        serverHandler{c.server}.ServeHTTP(w, w.req) //开始路由解析
    }
}

//type serverHandler struct {
// srv *Server
//}

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
 handler := sh.srv.Handler//这里就是mux啊~
 if handler == nil {
  handler = DefaultServeMux//默认路由器
 }
 if req.RequestURI == "*" && req.Method == "OPTIONS" {
  handler = globalOptionsHandler{}
 }
 handler.ServeHTTP(rw, req)//这里就是下面的mux的ServeHTTP了
}
Mux部分
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
    if r.RequestURI == "*" {
        if r.ProtoAtLeast(11) {
            w.Header().Set("Connection""close")
        }
        w.WriteHeader(StatusBadRequest)
        return
    }
    h, _ := mux.Handler(r) //内部根据url,返回一个Handler
    h.ServeHTTP(w, r) //调用Handler函数,处理请求。直到这里,这个请求就算完了
    //也标志着,上面那个for里的serverHandler{c.server}.ServeHTTP(w, w.req) 运行结束了
}

func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
    mux.mu.RLock()
    defer mux.mu.RUnlock()

    // Host-specific pattern takes precedence over generic ones
    if mux.hosts {
        h, pattern = mux.match(host + path)//匹配路由器
    }
    if h == nil {
        h, pattern = mux.match(path)
    }
    if h == nil {
        h, pattern = NotFoundHandler(), ""
    }
    return
}

func (mux *ServeMux) match(path string) (h Handler, pattern string) {
    var n = 0
    for k, v := range mux.m {//迭代,查找对应路由的处理器
        if !pathMatch(k, path) {
        continue
    }
    if h == nil || len(k) > n {
        n = len(k)
        h = v.h
        pattern = v.pattern
        }
    }
    return
}

归纳
  • server底层一直监听着,当有新的请求来时,mux的ServeHTTP方法通过调用其Handler方法寻找注册到路由上的handler函数(就是自己写的HandleFunc),并调用该函数的ServeHTTP方法,即调用handler函数本身。
  • mux的Handler方法对URL简单的处理,然后调用handler方法,后者会创建一个锁,同时调用match方法返回一个handler和pattern。
  • 在match方法中,mux的m字段是map[string]muxEntry图,后者存储了pattern和handler处理器函数,因此通过迭代m寻找出注册路由的patten模式与实际url匹配的handler函数并返回。
  • 返回的结构一直传递到mux的ServeHTTP方法,接下来调用handler函数的ServeHTTP方法,即helloworld函数,然后把response写到http.ResponseWriter对象返回给客户端。
  • 上述函数运行结束即serverHandler{c.server}.ServeHTTP(w, w.req)运行结束。 接下来就是对请求处理完毕之后希望和连接断开的相关逻辑。
  • 至此,Golang中一个完整的http服务介绍完毕,包括注册路由,开启监听,处理连接,路由处理函数。
总结
  • Golang通过一个ServeMux实现了的multiplexer路由多路复用器来管理路由。 同时提供一个Handler接口提供ServeHTTP用来实现handler处理其函数,后者可以处理实际request并构造response。
  • ServeMux和handler处理器函数的连接桥梁就是Handler接口。 ServeMux的ServeHTTP方法实现了寻找注册路由的handler的函数,并调用该handler的ServeHTTP方法。 ServeHTTP方法就是真正处理请求和构造响应的地方。
  • 回顾go的http包实现http服务的流程,可见大师们的编码设计之功力。 学习有利提高自身的代码逻辑组织能力。 更好的学习方式除了阅读,就是实践,接下来,我们将着重讨论来构建http服务。尤其是构建http中间件函数。

三、中间件

1、概念

所谓中间件,就是连接上下级不同功能的函数或者软件,通常进行一些包裹函数的行为,为被包裹函数提供添加一些功能或行为。

go的http中间件很简单,只要实现一个函数签名为func(http.Handler) http.Handler的函数即可。http.Handler是一个接口,接口方法我们熟悉的为serveHTTP。返回也是一个handler

2、构造中间件Middleware

//还是返回一个处理器,只不过该处理器被包装进了一些东西
func middlewareHandler(next http.Handler) http.Handler{
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request){
        // 执行handler之前的逻辑
        next.ServeHTTP(w, r)
        // 执行完毕handler后的逻辑
    })
}

//使用时,一层层的包裹进来即可
func main() {
    http.Handle("/", loggingHandler(http.HandlerFunc(index)))
    http.ListenAndServe(":8000"nil)
}

//中间件也可以把mux包裹起来,这样每个请求来时,先走中间件,再跳转路由

四、HTTP长连接

1、httpServer对于keepAlive长连接的处理方式

// net/http/server.go L1892
for {
 rw, e := l.Accept() //tcp接收到新的连接
    
 go c.serve()
}
httpServer启动之后,会为每一个到来的请求去创建一个goroutine,实际并不一定是这样。
确切的说,应该是为每一个新的tcp连接去创建一个goroutine
// net/http/server.go L1320
func (c *conn) serve() {
 defer func() {//关闭连接,退出协程
  if !c.hijacked() {
   c.close()
  }
 }()

 for {//监听请求
  w, err := c.readRequest()
  
  if err != nil {
  }
  
  serverHandler{c.server}.ServeHTTP(w, w.req)//这个就是自己实现的handler
 }
}
这里有个循环,显然,如果是长连接,一个协程可以执行多次响应。如果只执行了一次,那就是短连接;
长连接会在超时或者出错后退出循环,也就是关闭长连接。 defer 函数可以让协程结束之后关闭 TCP 连接。 

2、手动关闭response.Body.Close()

_, err := http.Get("https://www.baidu.com")  //就算忽略了resp,resp也不会自动关闭

三个goroutine协同完成http事务:(具体看源码)

  1. 主goroutine将request同时发给readLoop和writeLoop。
  2. writeLoop发送request,然后将状态(error)发送给主goroutine和readLoop。
  3. readLoop解析头部response,然后将状态(error)和response发送给主goroutine。
  4. 主goroutine返回用户代码,readLoop等待body读取完成。
  5. readLoop回收连接。
  • 如果body既没有被完全读取,也没有被关闭,那么这次http事务就没有完成,除非连接因超时终止了,否则相关资源无法被回收。
  • 如果请求头或响应头指明Connection: close呢? 还是无法回收,因为close表示在http事务完成后断开连接,而事务尚未完成自然不会断开,更不会回收。
  • 从实现上看只要body被读完,连接就能被回收,只有需要抛弃body时才需要close,似乎不关闭也可以。但那些正常情况能读完的body,即第一种情况,在出现错误时就不会被读完,即转为第二种情况。而分情况处理则增加了维护者的心智负担,所以始终close body是最佳选择

本文使用 mdnice 排版