[golang] web 基础

397 阅读5分钟

web 工作方式

待补充

http 包建立 web 服务器

func main() {
    http.HandleFunc("/", sayhelloName)       // 设置访问的路由和执行的函数
    err := http.ListenAndServe(":9090", nil) // 设置监听的端口
    if err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}

func sayhelloName(w http.ResponseWriter, r *http.Request) {
    r.ParseForm() // 解析参数,默认是不会解析
    fmt.Println(r.Form)
    fmt.Println("path", r.URL.Path)
    fmt.Println("scheme", r.URL.Scheme)
    fmt.Println(r.Form["url_long"])
    for k, v := range r.Form {
        fmt.Println("key:", k)
        fmt.Println("val:", strings.Join(v, ""))
    }
    fmt.Fprintf(w, "Hello astaxie!")
}

http 包运行机制

创建 Listen Socket,监听指定的端口,等待客户端请求。Listen Socket 接受客户端的请求,得到 Client Socket,首先从 Client Socket 读取 HTTP 请求的协议头。如果是 POST 方法,还可能要读取客户端提交的数据,然后交给相应的 handler 处理请求,handler 处理完毕准备客户端需要的数据,通过 Client Socket 写给客户端。 image.png

如何监听端口?

首先 http.ListenAndServe 会初始化一个 Server,然后调用 (http.Server).ListenAndServe

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

然后在 (http.Server).ListenAndServe 中调用 net.Listen("tcp", addr),也就是底层用 TCP 协议搭建了一个服务,最后调用 (http.Server).Serve 监控设置的端口。

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

如何接收客户端的请求?

for{} 中,首先通过 net.Listener 接收请求:l.Accept(),其次创建一个 http.connc := srv.newConn(rw),最后单独开了一个 goroutine,把这个请求的数据当做参数扔给这个 http.conn 去服务:go c.serve(connCtx)

func (srv *Server) Serve(l net.Listener) error {
    ...
    ctx := context.WithValue(baseCtx, ServerContextKey, srv)
    for {
        rw, err := l.Accept()
        ...
        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)
    }
}

如何分配到相应的方法来处理请求?

http.conn 首先会解析 request:w, err := c.readRequest(ctx),然后获取相应的 handler 去处理请求:serverHandler{c.server}.ServeHTTP(w, w.req)

func (c *conn) serve(ctx context.Context) {
    ...
    ctx, cancelCtx := context.WithCancel(ctx)
    c.cancelCtx = cancelCtx
    defer cancelCtx()

    c.r = &connReader{conn: c}
    c.bufr = newBufioReader(c.r)
    c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10)

    for {
        w, err := c.readRequest(ctx)
        ...
        // HTTP cannot have multiple simultaneous active requests.[*]
        // Until the server replies to this request, it can't read another,
        // so we might as well run the handler in this goroutine.
        // [*] Not strictly true: HTTP pipelining. We could let them all process
        // in parallel even if their responses need to be serialized.
        // But we're not going to implement HTTP pipelining because it
        // was never deployed in the wild and the answer is HTTP/2.
        serverHandler{c.server}.ServeHTTP(w, w.req)
        w.cancelCtx()
        ...
    }
}

sh.srv.Handler 就是调用 http.ListenAndServe 时候的第二个参数,如果传递的是 nil,那么默认获取 handler = DefaultServeMux。这个变量就是一个路由器,用来匹配 url 跳转到对应的 handler。然后 http.HandleFunc("/", sayhelloName) 的作用就是注册了请求 / 的路由规则。http.DefaultServeMux 会调用 ServeHTTP,这个方法内部其实就是调用 sayhelloName 本身,最后通过写入 response 的信息反馈到客户端。

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

image.png

http 包详解

conn 和 goroutine

为了实现高并发和高性能,使用 goroutine 来处理 http.conn 的读写事件,这样每个请求都能保持独立,相互不会阻塞。
客户端的每次请求都会创建一个 http.conn,保存该次请求的信息,然后再传递到对应的 handler,该 handler 中可以读取到相应的 header 信息,这样保证每个请求的独立性。

...
c := srv.newConn(rw)
...
go c.serve(connCtx)

ServeMux

执行 (*http.conn).serve(ctx context.Context) 的时候,其实内部是调用了 http 默认的路由器 ServeMux,把本次请求的信息传递到了后端的处理函数。

type ServeMux struct {
    mu    sync.RWMutex        // 锁,由于请求涉及到并发处理,因此这里需要一个锁机制
    m     map[string]muxEntry // 路由规则,一个 string 对应一个 mux 实体,这里的 string 就是注册的路由表达式
    hosts bool                // 是否在任意的规则中带有 host 信息
}

type muxEntry struct {
    explicit bool    // 是否精确匹配
    h        Handler // 这个路由表达式对应哪个 handler
    pattern  string  // 匹配字符串
}

type Handler interface {
    ServeHTTP(ResponseWriter, *Request) // 路由实现器
}

http 里面还定义了一个类型 HandlerFuncsayhelloName 就是 HandlerFunc 调用之后的结果,这个类型默认就实现了 Handler 接口,即调用 HandlerFunc(f),强制类型转换 f 成为 HandlerFunc 类型,这样 f 就拥有了 ServeHTTP 方法。

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

type HandlerFunc func(ResponseWriter, *Request)

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

路由器接收到请求之后,如果是 * 则关闭链接,不然调用 (*http.ServeMux).Handler(r *Request) (h Handler, pattern string) 返回对应设置路由的 handler,然后执行 func (http.Handler).ServeHTTP(ResponseWriter, *Request)

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
    }
    h, _ := mux.Handler(r)
    h.ServeHTTP(w, r)
}

根据用户请求的 URL 和路由器里面存储的 map 去匹配的,当匹配到之后返回存储的 handler,就可以调用这个 handler 的 ServeHTTP 方法。

func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
    if r.Method != "CONNECT" {
        if p := cleanPath(r.URL.Path); p != r.URL.Path {
            _, pattern = mux.handler(r.Host, p)
            return RedirectHandler(p, StatusMovedPermanently), pattern
        }
    }
    return mux.handler(r.Host, r.URL.Path)
}

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
}

自行实现简单的路由器

go 其实支持外部实现的路由器,ListenAndServe 的第二个参数就是用来配置外部路由器,它是一个 Handler 接口,即外部路由器只要实现了 Handler 接口就可以。

type MyMux struct {
}

func (p *MyMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if r.URL.Path == "/" {
        sayhelloName(w, r)
        return
    }
    http.NotFound(w, r)
    return
}

func sayhelloName(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello myroute!")
}

func main() {
    mux := &MyMux{}
    http.ListenAndServe(":9090", mux)
}

http 执行顺序

  • 首先调用 http.HandleFunc("/", sayhelloName)
    • 调用 DefaultServeMux.HandleFunc(pattern, handler)
    • 调用 mux.Handle(pattern, HandlerFunc(handler))
    • DefaultServeMuxmap[string]muxEntry 中增加对应的 handler 和路由规则;
  • 然后调用 http.ListenAndServe(":9090", nil)
    • 实例化 server := &Server{Addr: addr, Handler: handler}
    • 调用 server.ListenAndServe()
    • 调用 ln, err := net.Listen("tcp", addr) 监听端口;
    • 调用 srv.Serve(ln),启动一个 for{},在循环体中调用 rw, err := l.Accept(),接收请求;
    • 对每个请求实例化 c := srv.newConn(rw),并且开启一个 goroutine 为这个请求进行服务 go c.serve(connCtx)
    • 调用 w, err := c.readRequest(ctx) 读取每个请求的内容;
    • 调用 serverHandler{c.server}.ServeHTTP(w, w.req),如果没有设置 handler,handler 就设置为 handler = DefaultServeMux
    • 调用 handler.ServeHTTP(rw, req)
    • 选择 handler;
      • 判断是否有路由能满足这个 request(循环遍历 ServeMuxmuxEntry);
      • 如果有路由满足,调用这个路由 handler 的 ServeHTTP,否则调用 NotFoundHandlerServeHTTP

参阅