Go 的 web 编程非常简洁。python 写 web app,比如 flask 或者 django,都要使用 uwsgi 或者 gunicorn 这种 web server 来提供生产级服务,web server 可以启动多进程来利用多核 CPU 实现更高的并发处理能力。Go 天生的 goroutine 实现了用户级线程,不仅更快而且可以充分利用多核 CPU,并发性能非常好,编译成二进制文件以后直接启动就可以提供生产可用的服务了,非常简洁。
最简单的 web app 只需要如下几个组成部分:
func test(w http.ResponseWriter, _ *http.Request){
fmt.Fprintf(w, "test\n")
}
http.HandleFunc("/test", test)
log.Fatal(http.ListenAndServe(":5000", nil))
下面我从服务启动函数读起,逐步了解一次 http 请求的处理过程。
func ListenAndServe(addr string, handler Handler) error {
server := &Server{
Addr: addr, Handler: handler}
return server.ListenAndServe()
}
该函数监听 TCP 地址然后调用处理器(handler)处理到来的请求,典型的 handler 为 nil,这时使用默认的路由器来路由请求。ListenAndServe 只会返回非 nil 的错误。
这个函数的主要作用是初始化一个服务器结构体(Server)的实例,为它的Handler属性赋值。
看一下 Server 结构体,它定义了运行一个 http 服务器所需的参数,这个结构体的零值(默认值)就是一个可用的配置。
type Server struct {
Addr string // 监听的 TCP 地址,为空的话默认为 ":http",也就是80端口
Handler Handler // 处理器,如果为 nil 的话使用默认的路由器 http.DefaultServeMux
// TLSConfig 是可选项,配置 TLS
TLSConfig *tls.Config
// 读取完整请求的超时时长,包括请求体
ReadTimeout time.Duration
// 允许读取请求头的时长。
ReadHeaderTimeout time.Duration
// 写回响应的超时时长
WriteTimeout time.Duration
// 启用 keep-alives 时,一个长连接的两个请求之间间隔的最大时长
IdleTimeout time.Duration
// 允许的请求头的最大字节数,不限制请求体
MaxHeaderBytes int
// TLS 相关的配置
TLSNextProto map[string]func(*Server, *tls.Conn, Handler)
// 当客户端连接状态改变时调用的回调函数,可选
ConnState func(net.Conn, ConnState)
// 可选的自定义 logger,不设就是标准日志库的 logger
ErrorLog *log.Logger
// 为来到此服务器的请求指定基 context ,不设就是 context.Background()
BaseContext func(net.Listener) context.Context
// 可选,通常指定一个函数,用于修改新连接的 context
ConnContext func(ctx context.Context, c net.Conn) context.Context
disableKeepAlives int32 // 关闭 keep-alives
inShutdown int32 // 非0表示正在关闭
nextProtoOnce sync.Once // http2相关
nextProtoErr error // http2相关
mu sync.Mutex
listeners map[*net.Listener]struct{
}
activeConn map[*conn]struct{
}
doneChan chan struct{
}
onShutdown []func()
}
接下来是 Server 的 ListenAndServe 方法,它监听 TCP 网络地址 srv.Addr 然后调用 Serve 函数处理到达连接的请求。支持 TCP keep-alives。该函数返回非空的 error。
func (srv *Server) ListenAndServe() error {
// 如果服务关闭了,返回的错误为 ErrServerClosed。
if srv.shuttingDown() {
return ErrServerClosed
}
addr := srv.Addr
// 如果结构体的 Addr 属性为空就设置
// 默认值 ":http"
if addr == "" {
addr = ":http"
}
// 调用 net 的 Listen 方法,得到一个 TCPListener
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
返回 srv 的 Serve 方法,上面得到的 TCPListener作为参数
return srv.Serve(ln)
}
net.Listen 方法监听本地的网络地址,network参数可以是 tcp、tcp4、tcp6、 unix 或者 unixpacket。address 参数可以用 hostname,但是不建议,因为这样创建的 listener(监听器)最多监听主机的一个IP地址。如果 address 参数的 port 为空或者“0”,如“127.0.0.1:” 或者 “[::1]:0”,将自动选择一个端口号。
这个函数的主要作用就是初始化一个net包的 TCPListener 结构体的实例,然后调用服务器的Serve方法为这个连接提供服务。
Server 结构体的 Serve 方法。它为 listerner 上每个到达的连接创建一个新的服务协程(goroutine)。服务协程读取request(请求)然后调用 srv.Handler 方法做出回应。
Serve 方法返回非空的error并关闭listener。
这个方法运行一个死循环,等待底层tcp或者tls连接到来的请求数据,没有请求数据就休眠一段时间,数据到来以后把服务器实例(Server)和底层连接(net.Conn)封装为一个内部的conn结构体的实例,执行它的serve方法。
func (srv *Server) Serve(l net.Listener) error {
// 如果有一个包裹了 srv 和 listener 的钩子函数,就执行它
if fn := testHookServerServe; fn != nil {
fn(srv, l) // call hook with unwrapped listener
}
// 把 listener 赋值给 origListener 变量,然后将原来的 linstner
// 作为 Listener 属性初始化一个onceCloseListener结构体的实例,
// 顾名思义,只关闭一次的监听器,这个包裹是为了防止有多个 Close 调用。
origListener := l
l = &onceCloseListener{
Listener: l}
defer l.Close()
// srv.setupHTTP2_Serve 方法有条件的配置 srv 的 HTTP/2。
if err := srv.setupHTTP2_Serve(); err != nil {
return err
}
// srv.trackListener 向跟踪的监听器集(srv.listeners)中添加一个监听器(l),
// 并保证它最终会删除这个监听器
if !srv.trackListener(&l, true) {
return ErrServerClosed
}
defer srv.trackListener(&l, false)
var tempDelay time.Duration // 如果没有接收到请求的话,休眠多久
// 定义基础context
baseCtx := context.Background()
if srv.BaseContext != nil {
baseCtx = srv.BaseContext(origListener)
if baseCtx == nil {
panic("BaseContext returned a nil context")
}
}
// 为ctx的ServerContextKey赋值srv
ctx := context.WithValue(baseCtx, ServerContextKey, srv)
// 死循环,即伺服
for {
// rw 是net包里的Conn接口,他是一个通用的面向流的网络连接。
rw, e := l.Accept()
// 如果监听器接收报错
if e != nil {
select {
// 如果 srv.getDoneChan 中有内容,返回 ErrServerClosed
case <-srv.getDoneChan():
return ErrServerClosed
default:
}
// 如果 e 是 net.Error, 并且错误是临时性的
if ne, ok := e.(net.Error); ok && ne.Temporary() {
// 第一次没接收到数据,睡眠5毫秒
if tempDelay == 0 {
tempDelay = 5 * time.Millisecond
// 每次随眠结束,唤醒后还是没接收到数据,睡眠时间加倍
} else {
tempDelay *= 2
}
// 单次睡眠时间上限设定为1秒
if max := 1 * time.Second; tempDelay > max {
tempDelay = max
}
// 记录接收数据失败日志,休眠 tempDelay
srv.logf("http: Accept error: %v; retrying in %v", e, tempDelay)
time.Sleep(tempDelay)
continue
}
// 如果e不是net.Error,或者不是临时性错误,就返回错误
return e
}
// 如果接收到数据就做如下处理:
// 如果指定了srv的ConnContext,就用它修改连接的context
connCtx := ctx
if cc := srv.ConnContext; cc != nil {
connCtx = cc(connCtx, rw)
if connCtx == nil {
panic("ConnContext returned nil")
}
}
// 休眠定时器归零
tempDelay = 0
// 使用当前的Conn接口构建新的 conn 实例,它包含了srv服务器和rw连接。
// conn 实例代表一个 HTTP 连接的服务端,rw是底层的网络连接
c := srv.newConn(rw)
// 设置底层连接的状态
// 第一次调用的时候初始化srv.activeConn为一个map[*conn]struct{}
// srv.activeConn是一个集合,保存服务器的活跃连接
// setState方法将当前连接加入集合跟踪状态
c.setState(c.rwc, StateNew) // before Serve can return
// 启动一个goroutine处理请求
go c.serve(connCtx)
}
}
看一下这个server.go 内的conn结构体
// conn 代表 HTTP 连接的服务侧.
type conn struct {
// 连接到达的服务器。
// 不可变,不能为空。
server *Server
// 取消连接层面的context的取消函数
cancelCtx context.CancelFunc
// rwc 是底层网络连接。
// 它不能被其他类型包裹,通常是 *net.TCPConn 或 *tls.Conn 类型。
rwc net.Conn
// remoteAddr 就是 rwc.RemoteAddr().String()。它不在 Listener 接收数据的 goroutine,里同步填充。
// 它在 (*conn).serve goroutine里被立即填充
// 这是 Handler 的 (*Request).RemoteAddr 的值。
remoteAddr string
// tlsState 是使用 TLS 时 TLS 的连接状态。
// nil 表示不用 TLS。
tlsState *tls.ConnectionState
// werr is set to the first write error to rwc.
// 通过 checkConnErrorWriter{w} 设置,bufw 写入。
werr error
// r 是 bufr 的读入源。它是 rwc 的一个包裹器,提供io.LimitedReader
// 式的限制 (在读取请求头的时候)
// 并支持 CloseNotifier 的功能。详阅 *connReader 的文档。
r *connReader
// bufr 从 r 读取。
bufr *bufio.Reader
// bufw 写入 checkConnErrorWriter{c}, 当发生错误时填充 werr。
bufw *bufio.Writer
// lastMethod 是当前连接的最后一个请求的方法。
lastMethod string
curReq atomic.Value // of *response (which has a Request in it)
curState struct{
atomic uint64 } // packed (unixtime<<8|uint8(ConnState))
// mu 保护 hijackedv
mu sync.Mutex
// hijackedv 表示这个连接是否被一个带有Hijacker接口的 Handler hijacked了。
hijackedv bool
}
下面看看goroutine处理http(s) 请求的过程:
调用上面srv.newConn(rw)方法创建的conn实例的serve方法服务一个新的连接
func (c *conn) serve(ctx context.Context) {
// 设置远端地址和本端地址
c.remoteAddr = c.rwc.RemoteAddr().String()
ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
// 错误处理
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 !c.hijacked() {
c.close()
c.setState(c.rwc, StateClosed)
}
}()
// https相关
if tlsConn, ok := c.rwc.(*tls.Conn); ok {
if d := c.server.ReadTimeout; d != 0 {
c.rwc.SetReadDeadline(time.Now().Add(d))
}
if d := c.server.WriteTimeout; d != 0 {
c.rwc.SetWriteDeadline(time.Now().Add(d))
}
if err := tlsConn.Handshake(); err != nil {
// If the handshake failed due to the client not speaking
// TLS, assume they're speaking plaintext HTTP and write a
// 400 response on the TLS conn's underlying 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
}
c.tlsState = new(tls.ConnectionState)
*c.tlsState = tlsConn.ConnectionState()
if proto := c.tlsState.NegotiatedProtocol; validNPN(proto) {
if fn := c.server.TLSNextProto[proto]; fn != nil {
h := initNPNRequest{
ctx, tlsConn, serverHandler{
c.server}}
fn(c.server, tlsConn, h)
}
return
}
}
// HTTP/1.x 相关从这里开始
// 确保执行cancelCtx
ctx, cancelCtx := context.WithCancel(ctx)
c.cancelCtx = cancelCtx
defer cancelCtx()
// connReader 是一个包裹了 *conn的 io.Reader。
c.r = &connReader{
conn: c}
c.bufr = newBufioReader(c.r)
c.bufw = newBufioWriterSize(checkConnErrorWriter{
c}, 4<<10)
for {
// 从连接中读取请求,在读取超时时间内解析验证请求头中的内容。
// w 是返回的response实例
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)
}
// 根据错误码,向底层的net.Conn(实现了读写关闭接口)实例写入错误信息
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:
// err 是 badRequestError 类型
publicErr := "400 Bad Request"
if v, ok := err.(badRequestError); ok {
publicErr = publicErr + ": " + string(v)
}
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}
}
} 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 不能同时有多个活跃的请求。[*]
// 服务器回复了一个请求后才能读取下一个请求,
// 所以我们可以在这个 goroutine 里执行 handler 。
// [*] 并非严格限定: HTTP pipelining. 即便它们需要串行地响应,
// 我们也可以让它们全部并行地处理。
// 但我们不打算实现 HTTP pipelining because it
// was never deployed in the wild and the answer is HTTP/2.
// 将 server 包裹为 serverHandler 的实例,执行它的
// ServeHTTP 方法,处理请求,返回响应。
// serverHandler 委托给 server 的 Handler 或者 DefaultServeMux(默认路由器)
// 来处理 "OPTIONS *" 请求。
serverHandler{
c.server}.ServeHTTP(w, w.req)
w.cancelCtx()
if c.hijacked() {
return
}
w.finishRequest()
if !w.shouldReuseConnection() {
if w.requestBodyLimitHit || w.closedRequestBodyEarly() {
c.closeWriteAndWait()
}
return
}
c.setState(c.rwc, StateIdle)
c.curReq.Store((*response)(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))
if _, err := c.bufr.Peek(4); err != nil {
return
}
}
c.rwc.SetReadDeadline(time.Time{
})
}
}
这一步的主要工作是解析验证请求头,然后把server 包裹为 serverHandler 的实例,执行它的ServeHTTP 方法,处理请求,返回响应。
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
handler := sh.srv.Handler
如果没有定义 server.Handler,就是用默认路由器
if handler == nil {
handler = DefaultServeMux
}
if req.RequestURI == "*" && req.Method == "OPTIONS" {
handler = globalOptionsHandler{
}
}
handler.ServeHTTP(rw, req)
}
Handler 是一个接口,嵌入在Server里。默认的DefaultServeMux是一个ServeMux类型的实例。
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
Handler 响应 HTTP 请求。
ServeHTTP 方法将响应头贺响应体写入 ResponseWriter 并返回。返回标志着请求的结束;与 ServeHTTP 调用同时或者在ServeHTTP后使用 ResponseWriter 或者 读取 Request.Body 都是无效的。
由于 HTTP 客户端、HTTP 协议版本和任何客户端与Go服务器之间的中间人的不同,有可能在写入 ResponseWriter 后就不能读取Request.Body了。注意handlers应该先读取Request.Body然后再响应。
除了读取请求体之外,handlers不应该修改请求。
ServeHTTP panic了,(调用ServeHTTP方法的)服务器假设 panic 的影响仅限于当前活跃的请求。它恢复 panic、将堆栈追踪记入错误日志,然后根据不同的HTTP协议执行关闭网络连接或者发送一个HTTP/2 RST_STREAM,使用 ErrAbortHandler panic可以中断处理请求,客户端可以看到中断的响应而服务器不会记录错误日志。
ServeMux是标准库提供的HTTP请求路由器,它将到来的请求的url与一套注册的模板进行匹配,调用与请求的URL匹配最接近的handler处理请求。
type ServeMux struct {
mu sync.RWMutex
m map[string]muxEntry
es []muxEntry // 路由项切片,从长到短排好序的
hosts bool // 匹配的模板是否包含主机名
}
ServeHTTP 方法将请求分发给与请求URL匹配最接近的handler
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方法根据请求的方法、主机名和URL路径返回给定请求的处理器(handler)。它总是会返回非空的handler。如果路径不符合格式规范,handler将是一个跳转到规范路径的内部生成的handler。如果host有端口,在匹配handler时会忽略端口。
Handler也返回请求匹配的模板,如果是内部生成的跳转,模板就在跳转后匹配。
如果没有为请求注册handler,Handler就返回 ‘page not found’ 和空模板
h.ServeHTTP(w, r)
是具体的处理函数,最后说明。
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
// CONNECT requests are not canonicalized.
if r.Method == "CONNECT" {
// If r.URL.Path is /tree and its handler is not registered,
// the /tree -> /tree/ redirect applies to CONNECT requests
// but the path canonicalization does not.
if u, ok := mux.redirectToPathSlash(r.URL.Host, r.URL.Path, r.URL); ok {
return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
}
return mux.handler(r.Host, r.URL.Path)
}
// 删除port,整理path
host := stripHostPort(r.Host)
path := cleanPath(r.URL.Path)
// 如果给的路径是 /tree 但没注册这个handler,就跳转到 /tree/.
if u, ok := mux.redirectToPathSlash(host, path, r.URL); ok {
return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
}
if path != r.URL.Path {
_, pattern = mux.handler(host, path)
url := *r.URL
url.Path = path
return RedirectHandler(url.String(), StatusMovedPermanently), pattern
}
return mux.handler(host, r.URL.Path)
}
这是Handler的主要实现。除了 CONNECT 方法以外,路径格式必须符合规范。
func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
mux.mu.RLock()
defer mux.mu.RUnlock()
// 指定host的模板优先处理
if mux.hosts {
h, pattern = mux.match(host + path)
}
if h == nil {
h, pattern = mux.match(path)
}
if h == nil {
h, pattern = NotFoundHandler(), ""
}
return
}
这里的match
函数返回HandlerFunc(f).
之前函数func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request)
的最后一句h.ServeHTTP(w, r)
找了半天没找到是什么,其实上面这个func (mux *ServeMux) handler(host, path string) (h Handler, pattern string)
方法返回的就是通过路由匹配到的注册到Server的HandleFunc,它的ServeHTTP方法即是注册的HandleFunc函数,也就是最上面的那个test
==函数。至此,一个http 请求的完整处理过程基本捋出来了。==下面是HandlerFunc类型和它的ServeHTTP方法:
// HandlerFunc 类型是一个允许一个普通函数用于HTTP处理函数的适配器。
// 如果 f 是一个签名适配的函数,HandlerFunc(f) 是一个调用 f 的 Handler。
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP 调用 f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
因为很喜欢 Go 的 Http Server 所以很想了解源码。第一次啃这么大量的源码,用了几天的时间,只是捋出一个梗概的脉络,有一个总体的认知,并没有深入很多细枝末节,但是收获还是很大,后面有时间再细致研究。
代码方面的收获主要有以下几点:
ServeHTTP(ResponseWriter, *Request)
方法就是Handler
接口,http包的server.go里面有大量这个方法调用。有很多类型都实现了这个方法,比如*ServeMux
、HandlerFunc
,而且HandlerFunc
是一个函数类型,也可以实现方法。有点麻烦的地方在于不一定能在IDE里直接跳转到方法定义,可能需要先找到正确的结构体,再找它的方法定义。type badRequestError string
类型有个返回string的Error()
方法,对于自定义类型,fmt.Print 会打印Error方法返回的字符串,如果没有Error方法会打印String方法返回的字符串