http 服务器

客户端主机使用浏览器去访问一个网站,这个网站的 http 服务器接收到客户端的请求后向用户返回所请求的信息,这个工作流程主要包括以下过程:

go rpc 传送文件 go net rpc_go rpc 传送文件

用户访问一个网站的过程就如上图所示,而在上面这个过程中,在 http 服务器上进行了如下工作:

  1. 客户端通过 TCP/IP 协议建立与服务器的 TCP 连接
  2. 客户端向服务器发送 HTTP 协议请求报文,请求获得服务器资源
  3. 服务器解析接收到的 HTTP 协议请求报文,并根据报文内容处理相关的数据,然后把请求的资源通过 HTTP 协议响应报文发送回给客户端
  4. 客户端与服务器断开,由客户端的浏览器解析并渲染返回的 HTML 文档,并把网页显示在屏幕上

URL

用户通过在浏览器上输入“统一资源定位符” URL (Uniform Resource Locator) 来向服务器请求资源和发送数据,URL 的基本格式如下:

scheme://host[:port#]/path/[?query][#anchor]
scheme         指定使用的协议,如 http, https, ftp 等
host           对应服务器或主机的 IP 地址或域名
port#          端口号,HTTP 服务器的默认端口是80,如果指定了其他端口,则需要手动输入,如localhost:8090
path           访问资源在服务器上的路径
query          发送给服务器的数据,以键值对 key=value 的形式发送
anchor         指向网页上特定位置的锚

net/http 库如何实现 http 服务器功能

要使一个 http 服务器能够正常工作,必须至少实现如下的核心功能:

  • 监听主机的某个端口
  • 当在监听的端口上有客户端的请求到来时,接收该客户端的请求
  • 处理客户端的请求

net/http 库对应这三个功能都有相应的实现:

监听端口

监听主机上的某个窗口应该使用函数 http.ListenAndServe ,该函数的源码定义如下:

// ListenAndServe listens on the TCP network address addr
// and then calls Serve with handler to handle requests
// on incoming connections.
// Accepted connections are configured to enable TCP keep-alives.
// Handler is typically nil, in which case the DefaultServeMux is
// used.
//
// ListenAndServe always returns a non-nil error.
func ListenAndServe(addr string, handler Handler) error {
  server := &Server{Addr: addr, Handler: handler}
  return server.ListenAndServe()
}

调用该函数的时候,指定要监听的端口 addr 和相应的处理函数 handler (当 http 服务器的路由为 DefaultServeMux 时,这个 handler 的值为 nil)即可:

err := http.ListenAndServe(":23333", nil)
if err != nil {
  log.Fatal("ListenAndServe: ", err)
}

观察源码可以发现,http.ListenAndServe 是先利用端口号和处理函数初始化一个 Server 结构实例,然后通过调用这个实例的 ListenAndServe() 来实现监听功能的,而 Server 结构的 ListenAndServe() 函数定义如下:

// ListenAndServe listens on the TCP network address srv.Addr and then
// calls Serve to handle requests on incoming connections.
// Accepted connections are configured to enable TCP keep-alives.
// If srv.Addr is blank, ":http" is used.
// ListenAndServe always returns a non-nil error.
func (srv *Server) ListenAndServe() error {
    addr := srv.Addr
    if addr == "" {
        addr = ":http"
    }
    ln, err := net.Listen("tcp", addr)
    if err != nil {
        return err
    }
    return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
}

如上代码所示,Server 结构通过调用 net 包的 net.Listen("tcp", addr) 在底层用 TCP 协议搭建了一个服务,然后监听我们设置的端口,这个函数将会返回一个 Listener 接口,这个接口定义了方法 AcceptCloseAddr ,分别用于建立新的连接、关闭该接口对应的连接和返回接口对应的网络地址。

// A Listener is a generic network listener for stream-oriented protocols.
//
// Multiple goroutines may invoke methods on a Listener simultaneously.
type Listener interface {
    // Accept waits for and returns the next connection to the listener.
    Accept() (Conn, error)

    // Close closes the listener.
    // Any blocked Accept operations will be unblocked and return errors.
    Close() error

    // Addr returns the listener's network address.
    Addr() Addr
}

ListenAndServe() 函数的最后一行 return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)}) ,先把调用 net.Listen("tcp", addr) 得到的接口利用类型断言转化为实现了相应函数的 *net.TCPListener 的一个结构实例,然后用这个结构实例创建用于在限定时间内保持 TCP 连接的 tcpKeepAliveListener 结构实例,最后,调用 srv.Serve 函数接收客户端传来的数据。

由此可见,Go 的 net/http 包是通过调用下层的 tcp 协议来设置端口进行监听的。

接收客户端请求

设置好监听端口后,Go 通过调用 *Server 结构实例的 Serve(net.Listener) 来接收客户端的请求。Serve 函数定义如下:

// Serve accepts incoming connections on the Listener l, creating a
// new service goroutine for each. The service goroutines read requests and
// then call srv.Handler to reply to them.
//
// For HTTP/2 support, srv.TLSConfig should be initialized to the
// provided listener's TLS Config before calling Serve. If
// srv.TLSConfig is non-nil and doesn't include the string "h2" in
// Config.NextProtos, HTTP/2 support is not enabled.
//
// Serve always returns a non-nil error. After Shutdown or Close, the
// returned error is ErrServerClosed.
func (srv *Server) Serve(l net.Listener) error {
    defer l.Close()
    if fn := testHookServerServe; fn != nil {
        fn(srv, l)
    }
    var tempDelay time.Duration // how long to sleep on accept failure

    if err := srv.setupHTTP2_Serve(); err != nil {
        return err
    }

    srv.trackListener(l, true)
    defer srv.trackListener(l, false)

    baseCtx := context.Background() // base is always background, per Issue 16220
    ctx := context.WithValue(baseCtx, ServerContextKey, srv)
    for {
        rw, e := l.Accept()
        if e != nil {
            select {
            case <-srv.getDoneChan():
                return ErrServerClosed
            default:
            }
            if ne, ok := e.(net.Error); ok && ne.Temporary() {
                if tempDelay == 0 {
                    tempDelay = 5 * time.Millisecond
                } else {
                    tempDelay *= 2
                }
                if max := 1 * time.Second; tempDelay > max {
                    tempDelay = max
                }
                srv.logf("http: Accept error: %v; retrying in %v", e, tempDelay)
                time.Sleep(tempDelay)
                continue
            }
            return e
        }
        tempDelay = 0
        c := srv.newConn(rw)
        c.setState(c.rwc, StateNew) // before Serve can return
        go c.serve(ctx)
    }
}

在这个函数的循环体 for{} 内,首先通过调用接口的 Accept() 函数接收客户端请求,请求信息保存在变量 rw 中,然后调用 newConn(rw) 创建一个新的连接 c ,最后单独创建一个 goroutine ,把相应的上下文信息作为参数去调用新连接的 c.serve(ctx) 函数,处理客户端的请求。

处理客户端请求

在处理客户端请求的 c.serve(ctx) 函数内,关键的操作有两个:

  1. 调用 w, err := c.readRequest(ctx) ,取出分析相应的请求信息。
  2. 调用 serverHandler{c.server}.ServeHTTP(w, w.req) ,获取相应的处理函数对请求信息进行处理。具体做法是先创建一个 serverHandlerhandler 相当于路由器,它会根据客户端传来的 URL 跳转到相应的处理函数,然后在处理函数中对请求信息进行处理。这个 serverHandler 调用了 ServeHTTP 函数,而 ServeHTTP 函数内部则会调用我们自定义的处理函数对客户端的请求信息进行处理。

整个 http 服务器的流程图如下所示:

go rpc 传送文件 go net rpc_net-http_02

参考文献

  1. build-web-application-with-golang astaxie .