go语言之扒一扒HTTP原理

miloyang
0 评论
/ /
665 阅读
/
12534 字
29 2023-10

go语言调用http协议,相信都用过,那它底层是如何实现的呢?今天我们来扒一扒。

gocsjiagoushiyitus

如上,交互框架有客户端和服务端组成,Client发送request,Server处理后回复response,那底层是什么呢?

使用方式

按照惯例,都是先来一个简单的例子,然后由浅入深进行分析,我们来看看go语言启动一个http服务,来实现上图中的例子

  • server端代码
func main() {
    http.HandleFunc("/ping", func(writer http.ResponseWriter, request *http.Request) {
        writer.Write([]byte("pong"))
    })

    http.ListenAndServe(":8081", nil)
}
  • Client代码
func main() {
    resp, err := http.Get("http://127.0.0.1:8081/ping")
    if err != nil {
        log.Fatal(err)
    }
    defer resp.Body.Close()
    body, _ := io.ReadAll(resp.Body)
    fmt.Println(string(body))
}

上述例子,就是Client请求一个ping,server回复一个pone。很简单的一个请求,服务端主要完成了两件事:

  • 调用HandleFun的方法,注册了请求路径/ping,然后通过ping后进入到方法,再通过write写回到客户端
  • 调用ListenAndServe,启动一个8081端口的http服务。这是一个常驻的进程。
    客户端也主要是完成了两件事
  • 通过get方法去调用具体的路径,发起http请求
  • 读取response回来的数据流信息,然后解析

golang语言其实比较适合web服务的开发,比对于处理并发逻辑有很大优势,天然支持goroutine,而且你看,搭建一个http只需要寥寥几行代码即可。

我们通过进行服务端和客户端两个源码进行分析。

服务端

数据结构

type Server struct {
// Addr optionally specifies the TCP address for the server to listen on,
    // in the form "host:port". If empty, ":http" (port 80) is used.
    // The service names are defined in RFC 6335 and assigned by IANA.
    // See net.Dial for details of the address format.
    Addr string

    Handler Handler // handler to invoke, http.DefaultServeMux if nil

    // TLSConfig optionally provides a TLS configuration for use
    // by ServeTLS and ListenAndServeTLS. Note that this value is
    // cloned by ServeTLS and ListenAndServeTLS, so it's not
    // possible to modify the configuration with methods like
    // tls.Config.SetSessionTicketKeys. To use
    // SetSessionTicketKeys, use Server.Serve with a TLS Listener
    // instead.
    TLSConfig *tls.Config

    // ReadTimeout is the maximum duration for reading the entire
    // request, including the body. A zero or negative value means
    // there will be no timeout.
    //
    // Because ReadTimeout does not let Handlers make per-request
    // decisions on each request body's acceptable deadline or
    // upload rate, most users will prefer to use
    // ReadHeaderTimeout. It is valid to use them both.
    ReadTimeout time.Duration

    // ReadHeaderTimeout is the amount of time allowed to read
    // request headers. The connection's read deadline is reset
    // after reading the headers and the Handler can decide what
    // is considered too slow for the body. If ReadHeaderTimeout
    // is zero, the value of ReadTimeout is used. If both are
    // zero, there is no timeout.
    ReadHeaderTimeout time.Duration

    // WriteTimeout is the maximum duration before timing out
    // writes of the response. It is reset whenever a new
    // request's header is read. Like ReadTimeout, it does not
    // let Handlers make decisions on a per-request basis.
    // A zero or negative value means there will be no timeout.
    WriteTimeout time.Duration

    // IdleTimeout is the maximum amount of time to wait for the
    // next request when keep-alives are enabled. If IdleTimeout
    // is zero, the value of ReadTimeout is used. If both are
    // zero, there is no timeout.
    IdleTimeout time.Duration

    // MaxHeaderBytes controls the maximum number of bytes the
    // server will read parsing the request header's keys and
    // values, including the request line. It does not limit the
    // size of the request body.
    // If zero, DefaultMaxHeaderBytes is used.
    MaxHeaderBytes int

    // TLSNextProto optionally specifies a function to take over
    // ownership of the provided TLS connection when an ALPN
    // protocol upgrade has occurred. The map key is the protocol
    // name negotiated. The Handler argument should be used to
    // handle HTTP requests and will initialize the Request's TLS
    // and RemoteAddr if not already set. The connection is
    // automatically closed when the function returns.
    // If TLSNextProto is not nil, HTTP/2 support is not enabled
    // automatically.
    TLSNextProto map[string]func(*Server, *tls.Conn, Handler)

    // ConnState specifies an optional callback function that is
    // called when a client connection changes state. See the
    // ConnState type and associated constants for details.
    ConnState func(net.Conn, ConnState)

    // ErrorLog specifies an optional logger for errors accepting
    // connections, unexpected behavior from handlers, and
    // underlying FileSystem errors.
    // If nil, logging is done via the log package's standard logger.
    ErrorLog *log.Logger

    // BaseContext optionally specifies a function that returns
    // the base context for incoming requests on this server.
    // The provided Listener is the specific Listener that's
    // about to start accepting requests.
    // If BaseContext is nil, the default is context.Background().
    // If non-nil, it must return a non-nil context.
    BaseContext func(net.Listener) context.Context

    // ConnContext optionally specifies a function that modifies
    // the context used for a new connection c. The provided ctx
    // is derived from the base context and has a ServerContextKey
    // value.
    ConnContext func(ctx context.Context, c net.Conn) context.Context

    inShutdown atomicBool // true when server is in shutdown

    disableKeepAlives int32     // accessed atomically.
    nextProtoOnce     sync.Once // guards setupHTTP2_* init
    nextProtoErr      error     // result of http2.ConfigureServer if used

    mu         sync.Mutex
    listeners  map[*net.Listener]struct{}
    activeConn map[*conn]struct{}
    doneChan   chan struct{}
    onShutdown []func()
}

当然,我们不会看那么多,不会太过于细节,支持查看我们可以自定义服务器的监听地址、处理程序、超时设置和其他等等行为,满足我们日常开发行为,如:

  • Addr string: 服务端的地址,表示服务器要监听的网络地址和端口号,如 host:port
  • Handler Handler: 处理HTTP请求的接口,通常是一个http.handler接口的实现,用于处理请求,决定服务器如何处理传入的http请求
  • ReadTimeout:表示服务器在读取请求头和请求体数据时的超时时间,如果在指定时间内没有完成请求的话,服务器将关闭连接。防止阻塞。
  • WriteTimeOut:服务器在写入响应数据时的超时时间,如果在指定时间内没有完成响应的写入,关闭连接。
  • MaxHeaderBytes:字段定义了请求头的最大字节数。
  • ErrorLog:字段是一个可选的日志记录器,用于记录服务器内部错误。
  • TLSConfig:字段允许您指定用于启用TLS(Transport Layer Security)的配置。如果不使用TLS,可以将其设置为nil。

由上述例子可看,我们用到的方法,其实就两个,一个是HandleFunc,一个是ListenAndServe,大道至简,我们从分析这两个,应该可以把整个server的框架分析出来。话不多说,开干。

HandleFunc

老规矩,先上源码片段,该源码片段,采取了截取、组装到同一个文件,方便查看,关键地方都给了注释:

var DefaultServeMux = &defaultServeMux // 这个是单例ServeMux,如果直接通过HandleFunc注入handle的话,会直接注册到DefaultSerMux中来
var defaultServeMux ServeMux

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    DefaultServeMux.HandleFunc(pattern, handler)
}

type muxEntry struct {
    h       Handler
    pattern string
}

type ServeMux struct {
    mu    sync.RWMutex // 并发下的读写锁,主要是针对map的
    m     map[string]muxEntry // 这里就定义了key:路径和value:处理当前路径的Handle,这里的Handle是一个接口,主要是ServeHTTP方法,下面介绍
    es    []muxEntry  // 这个是用户路径匹配的,如果你路径的最后一个字符位/的话,后续在map中找不到就跑到es中来找。
    hosts bool       // whether any patterns contain hostnames
}

func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    if handler == nil {
        panic("http: nil handler")
    }
    mux.Handle(pattern, HandlerFunc(handler))
}

type Handler interface {
    ServeHTTP(ResponseWriter, *Request) // 根据http的请求路径对应到处理函数,对请求进行处理和响应的
}

// 这里,才是重点,就是把所有的请求和请求处理的Handle,都放到map中,方便匹配
func (mux *ServeMux) Handle(pattern string, handler Handler) {
    mux.mu.Lock() // 并发下,给了读写锁
    defer mux.mu.Unlock()

    ...

    if mux.m == nil {
        mux.m = make(map[string]muxEntry)
    }
    e := muxEntry{h: handler, pattern: pattern} // 这里把handler和路径参数,都封装到了一个结构体中,但是map的key又是参数,这里有点看不懂,总感觉有点冗余
    mux.m[pattern] = e // 路径、Handle在这里对应上了
    if pattern[len(pattern)-1] == '/' {
        mux.es = appendSorted(mux.es, e) // 这里就是处理了短路匹配逻辑
    }
    ...
}

以上代码就是关键性的,总而言之,就是通过路由,把参数pattern和处理的函数handler绑定起来。如下图:(来源于:小徐先生的编程世界)

handlerzhucedeguocheng

ListenAndServe

这个是启动监听端口,一般来说我们不会自定义handler。

上关键源码,还是从上到下把一些关键函数摘出来了


// 一般来说我们都不会自定义handler,所以都会置为nil
func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler} // 声明一个新的server对象
    return server.ListenAndServe() // 嵌套再次执行ListenAndServe
}

func (srv *Server) ListenAndServe() error {
    ...
    addr := srv.Addr
    if addr == "" {
        addr = ":http" // 处理端口、参数情况
    }
    ln, err := net.Listen("tcp", addr) // 申请到一个监听器,net下面的监听器,本文不涉及如何申请监听器,后续有时间整理
    if err != nil {
        return err
    }
    return srv.Serve(ln) //把监听的信息,丢到当前server下面Serve方法,核心。 
}

//  这个方法就很核心了,提现了服务器端运行架构,for+listener.accept模式,监听Accept,使用了epoll多路复用机制。
func (srv *Server) Serve(l net.Listener) error {
    ...
    ctx := context.WithValue(baseCtx, ServerContextKey, srv) // 创建了一个新的Context,是一个valueCtx,并且把当前的请求server封装进去。
    for {
        rw, err := l.Accept() // 使用管理epoll多路复用机制,也就是如果请求到值了,就处理,请求不到不会阻塞,等到有值了会自动返回调用对应的逻辑处理。
        ...
        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)  // 如果有一个连接到达,创建一个 goroutine 异步执行 conn.serve 方法负责处理
    }
}

// 响应客户端连接的核心方法:
func (c *conn) serve(ctx context.Context) {
    c.remoteAddr = c.rwc.RemoteAddr().String() // 读取到请求对应的地址
    ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
    ...

    for {
        w, err := c.readRequest(ctx)
        if c.r.remain != c.server.initialReadLimitSize() {
            // If we read any bytes off the wire, we're active.
            c.setState(c.rwc, StateActive, runHooks)
        }
        ...
        serverHandler{c.server}.ServeHTTP(w, w.req) // 请求的 path 为其分配 handler
        ...
    }
}


type serverHandler struct {
    srv *Server
}

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
    handler := sh.srv.Handler
    if handler == nil {
        handler = DefaultServeMux // 会对 Handler 作判断,倘若其未声明,则取全局单例 DefaultServeMux 进行路由匹配,呼应了 http.HandleFunc 中的处理细节.
    }
        ...

    handler.ServeHTTP(rw, req) // 进行当前请求地址的一个请求,然后处理Handle
}

// 转了一圈,终于转到了serveMux中来了,这个ServeHTTP我们上面也说过,就是根据http的请求路径对应到处理函数,对请求进行处理和响应的
// 但是这个方法是一个实现,主要是做两件事情,第一,找到对应的hander,第二,处理和响应
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
        ...
    
        h, _ := mux.Handler(r) // 这里就是找到对应的h,根据传入的请求结构体中的路径,见如下:
    h.ServeHTTP(w, r) // 找到了对应的hander,调用处理对应的事件,返回给客户端。
}

func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
    ...

    return mux.handler(host, r.URL.Path) // 这里再封装了一层前面...是一些判断过程。
}

func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
    mux.mu.RLock() // 这里很巧妙,使用了读锁,因为一般服务启动了,就会再注册handler,所以只是用到了读锁。
    defer mux.mu.RUnlock()

    if mux.hosts { 
        h, pattern = mux.match(host + path)
    }
    if h == nil {
        h, pattern = mux.match(path)
    }
    if h == nil {
        h, pattern = NotFoundHandler(), ""
    }
    return
}

// 这里,就是根据传入的地址,找到匹配的hander,如果没有,则去判断路径前缀中的es里面有没有
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
    v, ok := mux.m[path]
    if ok {
        return v.h, v.pattern // 这里就是去map中找,也就是我前面说的,为什么不直接使用key:路径,value:hander,而是要封装一个结构体做路径和hander。
    }

    // ServeMux.es 本身是按照 pattern 的长度由大到小排列的
    for _, e := range mux.es {
        if strings.HasPrefix(path, e.pattern) { 
            return e.h, e.pattern
        }
    }
    return nil, ""
}

上述就是整个服务端监听的核心代码片段过程,以下流程图更为直观的展现了整个过程:

matchhandlerxiangg

人未眠
工作数十年
脚步未曾歇,学习未曾停
乍回首
路程虽丰富,知识未记录
   借此博客,与之共进步