小徐先生的编程世界

引言#

本篇文章是学习了小徐先生的编程世界中的http底层原理实现公众号,想要记录学习一下

学习的内容更多是为了更加清晰http底层的流程实现,而不是源码解读

过于底层的源码解读我也读不懂,同时他包装地太过于深入让我难以理解

但是,清晰了http的流程让我受益匪浅

CS架构#

http实现的是cs架构,也就是客户端和服务端架构

但是在理解http标准库的时候,我更愿意把cs架构理解为端到端的联系,也就是 端 + conn + 端

的组合,至于客户端和服务端的差别,也就仅仅只是职责的差距,导致的代码上的差距而已

Server#

  • 数据结构

    1. Server

    server是服务端最核心的类,下面的两个字段简洁地表示出了Server的功能

    从 Addr 匹配路径到具体的 Handler 进行实现功能

    如果 Handler 没有显示声明,则会从 DefaultServerMux 中兜底

    1
    2
    3
    4
    5
    6
    7
    type Server struct {
    // server 的地址
    Addr string
    // 路由处理器.
    Handler Handler // handler to invoke, http.DefaultServeMux if nil
    // ...
    }
    1. Handler

    Handler 定义了方法 ServerHTTP,总的来说就是 匹配路径 + 对请求的处理响应

    1
    2
    3
    type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
    }
    1. ServerMux

    ServerMux 就是对 Handler的具体实现,内部有一个 map 和 slice 来维护 path -> handler 的映射

    map 是用来精准映射的 ;如果没命中则使用 slice进行模糊匹配

    1
    2
    3
    4
    5
    6
    type ServeMux struct {
    mu sync.RWMutex // 锁
    m map[string]muxEntry
    es []muxEntry // slice of entries sorted from longest to shortest.
    hosts bool // whether any patterns contain hostnames
    }
    1. muxEntry

    muxEntry 包含了 path + handler 两个部分

    1
    2
    3
    4
    type muxEntry struct {
    h Handler
    pattern string
    }

    看完这些数据结构可能会有点思绪,也有可能会一脸懵逼,但总体就是为了

    path -> handler 这一个功能服务的

  • 流程Debug

    带着刚才的数据结构,进行Debug,以下面的代码为例,在 HandleFunc 和 ListenAndServe 上打两个断点:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    func main() {
    http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
    _, err := w.Write([]byte("Hello, World!"))
    if err != nil {
    fmt.Println(err)
    }
    fmt.Println("Request received")
    })

    _ = http.ListenAndServe("127.0.0.1:8080", nil)
    }
    1. http.HandleFunc
    1
    2
    3
    func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    DefaultServeMux.HandleFunc(pattern, handler)
    }
    1. DefaultServeMux.HandleFunc

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

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      func (mux *ServeMux) Handle(pattern string, handler Handler) {
      mux.mu.Lock()
      defer mux.mu.Unlock()
      // ... 部分代码省略
      e := muxEntry{h: handler, pattern: pattern}
      mux.m[pattern] = e
      if pattern[len(pattern)-1] == '/' {
      mux.es = appendSorted(mux.es, e)
      }
      // ...
      }

    代码很简单,总的来说就是将handle注册到相应Path下,下面是serve的debug

    1. http.ListenAndServe

      1
      _ = http.ListenAndServe("127.0.0.1:8080", nil)
    2. server.ListenAndServe

      装配一下 path-handler

      1
      2
      3
      4
      func ListenAndServe(addr string, handler Handler) error {
      server := &Server{Addr: addr, Handler: handler}
      return server.ListenAndServe()
      }
    3. server.ListenAndServe

      启动监听器

      1
      2
      3
      4
      5
      6
      func (srv *Server) ListenAndServe() error {
      // ...
      ln, err := net.Listen("tcp", addr)
      // ...
      return srv.Serve(ln)
      }
    4. srv.Serve

      通过For循环持续地监听请求,如果有就形成连接并启动

      1
      2
      3
      4
      5
      6
      7
      8
      9
      func (srv *Server) Serve(l net.Listener) error {
      ctx := context.WithValue(baseCtx, ServerContextKey, srv)
      for {
      rw, err := l.Accept()
      // ....
      c := srv.newConn(rw)
      go c.serve(connCtx)
      }
      }

    这里的代码稍微复杂点,不过经过代码的省略流程还是非常清晰的:

    1
    2
    3
    4
    5
    6
    lis := net.listener("tcp",addr)    // start a listener
    srv.serve(lis) // begin to listen

    conn := lis.Accept() // wait for request,if has a request,get the conn

    serve(conn) // begin the conn

    就是会先启动一个监听器监听一个端口,然后通过持续监听端口(一个for循环中持续的lis.Accept()),但有收到请求的时候,就会形成一个连接 conn ,这里的连接是一个接口,有读写各种接口,最后通过serve(conn)来实现与客户端的互动

    至于内部的其他细节内容就不去看了,可以再去看源码

    图片

client#

  • 数据结构

    1. client
    • • Transport:负责 http 通信的核心部分,但是说实话我没怎么看懂,就不介绍了哈哈哈
    • • Jar:cookie 管理
    • • Timeout:超时设置
    1
    2
    3
    4
    5
    6
    7
    8
    type Client struct {
    // ...
    Transport RoundTripper
    // ...
    Jar CookieJar
    // ...
    Timeout time.Duration
    }
    1. request

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      type Request struct {
      // 方法
      Method string
      // 请求路径
      URL *url.URL
      // 请求头
      Header Header
      // 请求参数内容
      Body io.ReadCloser
      // 服务器主机
      Host string
      // query 请求参数
      Form url.Values
      // 响应参数 struct
      Response *Response
      // 请求链路的上下文
      ctx context.Context
      // ...
      }
    2. response

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      type Response struct {
      // 请求状态,200 为 请求成功
      StatusCode int // e.g. 200
      // http 协议,如:HTTP/1.0
      Proto string // e.g. "HTTP/1.0"
      // 请求头
      Header Header
      // 响应参数内容
      Body io.ReadCloser
      // 指向请求参数
      Request *Request
      // ...
      }
    3. RoundTripper

      RoundTripper 是通信模块的 interface,需要实现方法 Roundtrip,即通过传入请求 Request,与服务端交互后获得响应 Response.

      1
      2
      3
      type RoundTripper interface {
      RoundTrip(*Request) (*Response, error)
      }
  • 流程Debug

    以下面的代码为例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    func Test_server(t *testing.T) {
    t.Log("Client test")

    resp, err := http.Post("http://localhost:8080", "text/plain", nil)
    if err != nil {
    t.Error(err)
    }
    defer resp.Body.Close()

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
    t.Error(err)
    }

    t.Log(string(body))
    }
    1. http.Post

      1
      2
      3
      func Post(url, contentType string, body io.Reader) (resp *Response, err error) {
      return DefaultClient.Post(url, contentType, body)
      }
    2. DefaultClient.Post

      1
      2
      3
      4
      5
      6
      7
      8
      func (c *Client) Post(url, contentType string, body io.Reader) (resp *Response, err error) {
      req, err := NewRequest("POST", url, body) // 断点
      if err != nil {
      return nil, err
      }
      req.Header.Set("Content-Type", contentType)
      return c.Do(req) // 断点
      }
    3. NewRequest()

      就是会创建一个新的请求

    4. c.Do(req)

      就是会发送请求并且获取回复

    🆗就是这么简单,好吧其实不是这么简单的事情,由下图可知,client做的事情就是创建一个请求并通过client.do(req)来获取resp

    在client(do)的内部是通过Transport.RoundTrip来处理通信的,Transport.RoundTrip会创建两个goroutine:一个writeLoop,一个readLoop,

    分别通过reqChannel 和 respChannel 进行传递消息,具体的实现细节详见顶部的博客(我没看懂,不好意思抄过来)

    图片

总结#

这次的源码解析更多的感觉不是解析源码而是在熟悉http底层的操作流程,之前总是在接触http相关的事情,但是具体的先后操作不是很清晰,经过这次操作以及之前的实践会清晰不少,感谢小徐先生的博客