前言
对于 Golang
来说,实现一个简单的 http server
非常容易,只需要短短几行代码。同时有了协程的加持,Go 实现的 http server
能够取得非常优秀的性能。这篇文章将会对 go 标准库 net/http
实现 http 服务的原理进行较为深入的探究,以此来学习了解网络编程的常见范式以及设计思路。
HTTP 处理流程
基于 HTTP
构建的网络应用包括两个端,即客户端 ( Client
) 和服务端 ( Server
)。两个端的交互行为包括从客户端发出 request
、接收 request
进行处理并返回 response
以及客户端处理 response
。
所以 http 服务器的工作就在于如何接受来自客户端的 request
,并向客户端返回 response
。

服务器在接收到请求时,首先会进入路由 ( router
),路由的工作在于为这个 request 找到对应的处理器 ( handler
),处理器对 request
进行处理,并构建 response
。Golang 实现的 http server 同样遵循这样的处理流程。
如何实现一个简单的 HTTP Server ?
这里有两种方式:
第一种方式:最常用,最简单
go
package main
import (
"fmt"
"net/http"
)
func helloWord(w http.ResponseWriter, r *http.Request) {
fmt.Println(w, "hello word")
}
func main() {
http.HandleFunc("/", helloWord)
http.ListenAndServe(":9000", nil)
}
第二种方式:利用 go 的特性,实现 http.Handle
接口 中的 ServeHTTP
方法
go
package main
import (
"fmt"
"net/http"
)
type HelloWord struct {
content string
}
func (h *HelloWord) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Println(w, h.content)
}
func main() {
http.Handle("/", &HelloWord{content: "hello word"})
http.ListenAndServe(":9000", nil)
}
Go 实现的 http
服务步骤非常简单,首先注册路由,然后创建服务并开启监听即可。
注册路由
在 http 包中,注册路由有两种方法:HandleFunc
和 Handle
- http.HandleFunc(pattern string, handler func(ResponseWriter, *Request))
- http.Handle(pattern string, handler Handler)
HandleFunc 和 Handle 的区别
HandleFunc
go
// HandleFunc 在 DefaultServeMux 中注册给定模式的处理程序函数。
// ServeMux 的文档解释了模式是如何匹配的。
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}
// HandleFunc 为给定的模式注册处理程序函数。
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if handler == nil {
panic("http: nil handler")
}
mux.Handle(pattern, HandlerFunc(handler))
}
Handle
go
// Handle 在 DefaultServeMux 中 注册给定模式的处理程序。
// ServeMux 的文档解释了模式是如何匹配的。
func Handle(pattern string, handler Handler) {
DefaultServeMux.Handle(pattern, handler)
}
可以发现两者的区别在第二个形参:
HandleFunc
是一个func(ResponseWriter, *Request)
签名的函数Handle
是一个handler
结构体, 该结构体实现了一个ServeHTTP(ResponseWriter, *Request)
签名的方法
最终两者都是由 DefaultServeMux
调用 Handle
来完成注册路由的操作。
Handler
Handler
是一个接口:
go
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
Handler
接口中,定义了 ServeHTTP
的函数签名,只要任何结构体实现了 ServeHTTP
方法,那该结构体就是 Handler
对象。
Go 的 http
服务都是基于 Handler
处理的, Handler
中的 ServeHTTP
方法也是用以处理 request
并构建 response
的核心逻辑。
ServeMux
Go 中的路由(Multiplexer
)都是基于 ServeMux
结构,那么 ServeMux
是如何定义的?
go
type ServeMux struct {
mu sync.RWMutex
m map[string]muxEntry
es []muxEntry // 从最长到最短排序的条目的切片。
hosts bool // 是否有任何模式包含主机名
}
type muxEntry struct {
h Handler
pattern string
}
这里最重要的是 ServeMux
中的 m
字段,它是一个 map
,key
是路由表达式,value
是一个 muxEntry
结构,muxEntry
结构存储了对于的路由表达式和 handler
。
并且通过源码得知:ServeMux
也实现了 ServeHTTP
方法
go
// ServeHTTP 将请求分派给其模式与请求 URL 最匹配的处理程序。
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)
}
从这里就能看出:
ServeMux
结构体也是Handler
对象,只不过ServeMux
的ServeHTTP
方法不是用来处理具体的request
和构建response
,而是用来确定路由注册的handler
。
如何启动 Server 服务?
启动服务使用:http.ListenAndServe()
方法
go
// ListenAndServe 侦听 TCP 网络地址 addr,然后使用处理程序调用 Serve 来处理传入连接上的请求。
// 接受的连接被配置为启用 TCP 保持活动。
//
// 处理程序通常为 nil,在这种情况下使用 DefaultServeMux。
//
// ListenAndServe 总是返回一个非零错误。
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
// ListenAndServe 侦听 TCP 网络地址 srv.Addr,然后调用 Serve 来处理传入连接上的请求。
// 接受的连接被配置为启用 TCP 保持活动。
//
// 如果 srv.Addr 为空,使用":http"。
//
// ListenAndServe 总是返回一个非零错误。
// 在 Shutdown 或 Close 之后,返回的错误为 ErrServerClosed。
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)
}
这里先创建了一个 Server
对象,传入了地址和 handler
参数,然后调用 Server
对象 ListenAndServe()
方法。
这里用到了 Server
结构体, 咱们看一下这个结构体,Server
结构体中字段比较多,可以先大致了解一下:
go
// 服务器定义用于运 行HTTP 服务器的参数。
// 服务器的零值是有效的配置。
type Server struct {
// Addr可以选择指定要侦听的服务器的TCP地址,格式为"host:port"。如果为空,则使用":http"(端口80)。
// 服务名称在 RFC 6335 中定义,并由 IANA 分配。
// 查看 net.Dial 获取地址格式的详细信息。
Addr string
Handler Handler // 要调用的 Handler,http.DefaultServeMux 如果为零
// DisableGeneralOptionsHandler,如果为true,则将"OPTIONS*"请求传递给Handler,
// 否则以 200 OK 和 Content-Length:0 进行响应。
DisableGeneralOptionsHandler bool
// TLSConfig 可选地提供 TLS 配置,供 ServeTLS 和 ListenAndServeTLS 使用。
// 请注意,该值是由 ServeTLS 和 ListenAndServeTLS 克隆的,
// 因此不可能使用 TLS 等方法修改配置:Config.SetSessionTicketKeys。
// 若要使用 SetSessionTicketKey,请使用 Server.Serve 和 TLS 侦听器
TLSConfig *tls.Config
// ReadTimeout 是读取整个请求(包括正文)的最长持续时间。零值或负值表示没有超时。
//
// 由于 ReadTimeout 不允许处理程序根据请求决定每个请求主体的可接受截止日期或上传速率
// 因此大多数用户更喜欢使用 ReadHeaderTimeout。
// 同时使用它们是有效的。
ReadTimeout time.Duration
// ReadHeaderTimeout 是允许读取请求标头的时间量。
// 连接的读取截止日期在读取标头后重置,处理程序可以决定什么对主体来说太慢。
// 如果ReadHeaderTimeout为零,则使用ReadTimeout的值。
// 如果两者都为零,则没有超时。
ReadHeaderTimeout time.Duration
// WriteTimeout 是超时写入响应之前的最长持续时间。每当读取新请求的标头时,它都会重置。
// 与 ReadTimeout 一样,它不允许处理程序在每个请求的基础上做出决定。
// 零值或负值表示没有超时。
WriteTimeout time.Duration
// IdleTimeout 是启用保持活动时等待下一个请求的最长时间。
// 如果 IdleTimeout 为零,则使用 ReadTimeout 的值。如果两者都为零,则没有超时。
IdleTimeout time.Duration
// MaxHeaderBytes 控制服务器解析请求标头的键和值(包括请求行)时读取的最大字节数。
// 它不限制请求正文的大小。如果为零,则使用 DefaultMaxHeaderBytes。
MaxHeaderBytes int
// TLSNextProto 可选择指定一个函数,以便在发生 ALPN 协议升级时接管所提供 TLS 连接的所有权。
// 映射密钥是协商的协议名称。
// Handler 参数应用于处理 HTTP 请求,并将初始化请求的 TLS 和 RemoteAddr(如果尚未设置)。
// 当函数返回时,连接将自动关闭。
// 如果 TLSNextProto 不是 nil ,则不会自动启用 HTTP/2 支持。
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 为接受连接的错误、处理程序的意外行为以及潜在的 FileSystem 错误指定了一个可选的记录器。
// 如果为零,则通过日志包的标准记录器进行日志记录。
ErrorLog *log.Logger
// BaseContext 可选地指定一个函数,该函数返回此服务器上传入请求的基本上下文。
// 提供的侦听器是即将开始接受请求的特定侦听器。
// 如果 BaseContext 为 nil,则默认为 context.Background()
// 如果不是nil,则必须返回一个非 nil 上下文。
BaseContext func(net.Listener) context.Context
// ConnContext 可选地指定一个函数来修改用于新连接 c 的上下文。
// 所提供的 ctx 是从基本上下文派生的,并且具有 ServerContextKey 值。
ConnContext func(ctx context.Context, c net.Conn) context.Context
inShutdown atomic.Bool // 当服务器处于关闭状态时为true
disableKeepAlives atomic.Bool
nextProtoOnce sync.Once // 保护设置 HTTP2_*init
nextProtoErr error // http2.ConfigureServer 的结果(如果使用)
mu sync.Mutex
listeners map[*net.Listener]struct{}
activeConn map[*conn]struct{}
onShutdown []func()
listenerGroup sync.WaitGroup
}
在 Server
的 ListenAndServe
方法中,会初始化监听地址 Addr
,同时调用 Listen
方法设置监听。最后将监听的 TCP 对象传入 Serve
方法:
go
// Serve 接受 Listener l上的传入连接,为每个连接创建一个新的服务 goroutine。
// 服务 goroutines 读取请求,然后调用 srv.Handler 回复他们。
//
//只有当侦听器返回 *tls.Conn 连接并且在 tls Config.NextProtos 中使用"h2"配置时,才启用 HTTP/2 支持。
//
//Serve 总是返回一个非零错误并关闭l。
//在 Shutdown 或 Close 之后,返回的错误为 ErrServerClosed。
func (srv *Server) Serve(l net.Listener) error {
if fn := testHookServerServe; fn != nil {
fn(srv, l) // 带有 listener 的 hook 钩子
}
origListener := l
l = &onceCloseListener{Listener: l}
defer l.Close()
if err := srv.setupHTTP2_Serve(); err != nil {
return err
}
if !srv.trackListener(&l, true) {
return ErrServerClosed
}
defer srv.trackListener(&l, false)
baseCtx := context.Background()
if srv.BaseContext != nil {
baseCtx = srv.BaseContext(origListener)
if baseCtx == nil {
panic("BaseContext returned a nil context")
}
}
var tempDelay time.Duration // 失败时要睡多久
ctx := context.WithValue(baseCtx, ServerContextKey, srv)
for {
rw, err := l.Accept()
if err != nil {
if srv.shuttingDown() {
return ErrServerClosed
}
if ne, ok := err.(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", err, tempDelay)
time.Sleep(tempDelay)
continue
}
return err
}
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) // 在 Serve 返回之前
go c.serve(connCtx)
}
}
go
// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
if ra := c.rwc.RemoteAddr(); ra != nil {
c.remoteAddr = ra.String()
}
ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
var inFlightResponse *response
defer func() {
if err := recover(); err != nil && err != ErrAbortHandler {
const size = 64 << 10
buf := make([]byte, size)
buf = buf[:runtime.Stack(buf, false)]
c.server.logf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)
}
if inFlightResponse != nil {
inFlightResponse.cancelCtx()
}
if !c.hijacked() {
if inFlightResponse != nil {
inFlightResponse.conn.r.abortPendingRead()
inFlightResponse.reqBody.Close()
}
c.close()
c.setState(c.rwc, StateClosed, runHooks)
}
}()
if tlsConn, ok := c.rwc.(*tls.Conn); ok {
tlsTO := c.server.tlsHandshakeTimeout()
if tlsTO > 0 {
dl := time.Now().Add(tlsTO)
c.rwc.SetReadDeadline(dl)
c.rwc.SetWriteDeadline(dl)
}
if err := tlsConn.HandshakeContext(ctx); err != nil {
// 如果握手失败是因为客户端不说 TLS,
// 那么假设他们说的是明文 HTTP,并在 TLS conn 的底层网络上写一个400响应 net.Conn。
if re, ok := err.(tls.RecordHeaderError); ok && re.Conn != nil && tlsRecordHeaderLooksLikeHTTP(re.RecordHeader) {
io.WriteString(re.Conn, "HTTP/1.0 400 Bad Request\r\n\r\nClient sent an HTTP request to an HTTPS server.\n")
re.Conn.Close()
return
}
c.server.logf("http: TLS handshake error from %s: %v", c.rwc.RemoteAddr(), err)
return
}
// 恢复Conn级别的截止日期。
if tlsTO > 0 {
c.rwc.SetReadDeadline(time.Time{})
c.rwc.SetWriteDeadline(time.Time{})
}
c.tlsState = new(tls.ConnectionState)
*c.tlsState = tlsConn.ConnectionState()
if proto := c.tlsState.NegotiatedProtocol; validNextProto(proto) {
if fn := c.server.TLSNextProto[proto]; fn != nil {
h := initALPNRequest{ctx, tlsConn, serverHandler{c.server}}
// 将新创建的 HTTP/2 标记为活动并防止任何服务器状态挂钩
// 阻止在这些连接上运行。这样可以防止 closeIdleConns 关闭此类连接。
// 请参阅问题 https://golang.org/issue/39776.
c.setState(c.rwc, StateActive, skipHooks)
fn(c.server, tlsConn, h)
}
return
}
}
// HTTP/1.x from here on.
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)
if c.r.remain != c.server.initialReadLimitSize() {
// 如果我们从线路上读取任何字节,我们就是活动的。
c.setState(c.rwc, StateActive, runHooks)
}
if err != nil {
const errorHeaders = "\r\nContent-Type: text/plain; charset=utf-8\r\nConnection: close\r\n\r\n"
switch {
case err == errTooLarge:
// Their HTTP client may or may not be
// able to read this if we're
// responding to them and hanging up
// while they're still writing their
// request. Undefined behavior.
const publicErr = "431 Request Header Fields Too Large"
fmt.Fprintf(c.rwc, "HTTP/1.1 "+publicErr+errorHeaders+publicErr)
c.closeWriteAndWait()
return
case isUnsupportedTEError(err):
// Respond as per RFC 7230 Section 3.3.1 which says,
// A server that receives a request message with a
// transfer coding it does not understand SHOULD
// respond with 501 (Unimplemented).
code := StatusNotImplemented
// We purposefully aren't echoing back the transfer-encoding's value,
// so as to mitigate the risk of cross side scripting by an attacker.
fmt.Fprintf(c.rwc, "HTTP/1.1 %d %s%sUnsupported transfer encoding", code, StatusText(code), errorHeaders)
return
case isCommonNetReadError(err):
return // don't reply
default:
if v, ok := err.(statusError); ok {
fmt.Fprintf(c.rwc, "HTTP/1.1 %d %s: %s%s%d %s: %s", v.code, StatusText(v.code), v.text, errorHeaders, v.code, StatusText(v.code), v.text)
return
}
publicErr := "400 Bad Request"
fmt.Fprintf(c.rwc, "HTTP/1.1 "+publicErr+errorHeaders+publicErr)
return
}
}
// Expect 100 Continue support
req := w.req
if req.expectsContinue() {
if req.ProtoAtLeast(1, 1) && req.ContentLength != 0 {
// Wrap the Body reader with one that replies on the connection
req.Body = &expectContinueReader{readCloser: req.Body, resp: w}
w.canWriteContinue.Store(true)
}
} else if req.Header.get("Expect") != "" {
w.sendExpectationFailed()
return
}
c.curReq.Store(w)
if requestBodyRemains(req.Body) {
registerOnHitEOF(req.Body, w.conn.r.startBackgroundRead)
} else {
w.conn.r.startBackgroundRead()
}
// 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.
inFlightResponse = w
serverHandler{c.server}.ServeHTTP(w, w.req)
inFlightResponse = nil
w.cancelCtx()
if c.hijacked() {
return
}
w.finishRequest()
c.rwc.SetWriteDeadline(time.Time{})
if !w.shouldReuseConnection() {
if w.requestBodyLimitHit || w.closedRequestBodyEarly() {
c.closeWriteAndWait()
}
return
}
c.setState(c.rwc, StateIdle, runHooks)
c.curReq.Store(nil)
if !w.conn.server.doKeepAlives() {
// We're in shutdown mode. We might've replied
// to the user without "Connection: close" and
// they might think they can send another
// request, but such is life with HTTP/1.1.
return
}
if d := c.server.idleTimeout(); d != 0 {
c.rwc.SetReadDeadline(time.Now().Add(d))
} else {
c.rwc.SetReadDeadline(time.Time{})
}
// Wait for the connection to become readable again before trying to
// read the next request. This prevents a ReadHeaderTimeout or
// ReadTimeout from starting until the first bytes of the next request
// have been received.
if _, err := c.bufr.Peek(4); err != nil {
return
}
c.rwc.SetReadDeadline(time.Time{})
}
}
首先创建一个上下文对象,然后调用 Listener
的 Accept()
等待新的连接建立;一旦有新的连接建立,则调用 Server
的 newConn()
创建新的连接对象,并将连接的状态标志为 StateNew
,然后开启一个新的 goroutine
处理连接请求。
至此,Go 实现的 http server
的大致原理介绍完毕!
总结语
本节,通过 http
源码剖析,了解了 http Server
服务端的初始化过程:
- 如何注册路由?
http
是如何解析路由的?- 如何开启
Server
服务?
相信通过本节的学习,可以对 http Server
服务的使用有深入的了解!