惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

宝玉的分享
宝玉的分享
T
Threat Research - Cisco Blogs
H
Hacker News: Front Page
N
News and Events Feed by Topic
Know Your Adversary
Know Your Adversary
Cisco Talos Blog
Cisco Talos Blog
SecWiki News
SecWiki News
C
Cisco Blogs
D
Darknet – Hacking Tools, Hacker News & Cyber Security
T
Tor Project blog
K
Kaspersky official blog
Forbes - Security
Forbes - Security
Webroot Blog
Webroot Blog
Schneier on Security
Schneier on Security
P
Privacy & Cybersecurity Law Blog
H
Heimdal Security Blog
Y
Y Combinator Blog
The GitHub Blog
The GitHub Blog
S
SegmentFault 最新的问题
V
Vulnerabilities – Threatpost
T
Tenable Blog
T
Tailwind CSS Blog
P
Privacy International News Feed
WordPress大学
WordPress大学
大猫的无限游戏
大猫的无限游戏
小众软件
小众软件
博客园 - Franky
Hacker News: Ask HN
Hacker News: Ask HN
Jina AI
Jina AI
C
Cybersecurity and Infrastructure Security Agency CISA
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
雷峰网
雷峰网
Vercel News
Vercel News
A
About on SuperTechFans
爱范儿
爱范儿
Simon Willison's Weblog
Simon Willison's Weblog
AWS News Blog
AWS News Blog
The Last Watchdog
The Last Watchdog
Engineering at Meta
Engineering at Meta
Spread Privacy
Spread Privacy
Security Archives - TechRepublic
Security Archives - TechRepublic
博客园 - 司徒正美
量子位
博客园 - 三生石上(FineUI控件)
J
Java Code Geeks
Hacker News - Newest:
Hacker News - Newest: "LLM"
Recorded Future
Recorded Future
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Martin Fowler
Martin Fowler
Project Zero
Project Zero

ike‘s blog

mac软件推荐 用Siri来控制Windows电脑开机关机吧 clickhouse的一些优化建议和工具 2023sumppary 记录一下我的第一个网页游戏Mazeball 如何使用Clickhouse的索引 如何从zookeeper切换为clickhouse—keeper cloki分布式查询和clickhouse副本存储 使用ilogtail+cloki+clickhouse来做日志系统吧 clickhouse集群部署指南 快速安装部署clickhouse和cloki 快速部署thanos架构 如何使用go的jwt 如何修改git commit记录 业务容器化需要注意的一些地方 游戏企划 Prometheus Metrics精简优化2 来在线听音乐吧 Chatgpt的API入门 简单搭建国内也可以使用的chatgpt Prometheus Metrics精简优化 Kubernetes的探针机制 本地部署chatAI【chatglm】 AI绘画工具webui简单入门 之 高清化 写在地下城邂逅Ⅳ·灾厄篇·完结之后 如何部署gitalk作为评论系统 AI绘画工具webui简单入门 之 工具安装 grafanadb迁移到mysql kubectl常用命令 开始写博客啦 隐私政策URL Markdown Example Include Video in the Posts Markdown Extended Features Simple Guides for Fuwari
Golang net/http & HTTP Serve 源码分析
ike · 2022-03-09 · via ike‘s blog

这篇是转载的!不过还是很有用的,go的http可以推荐用gin,下次梳理下gin的教程好了 Go HTTP Server的大致处理流程|wide

服务器在收到请求时,首先进入路由 Router,接着路由会根据 request 请求的路径,找到对应的处理器(Handler),处理器再根据 request 进行处理并构造 response 进行返回。

利用标准库实现一个简单HTTP Server#

main.go文件写入如下内容:

package main

import (
    "fmt"
    "net/http"
)

// 方法一
type HelloContext struct {
    content string
}

func(h *HelloContext) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, h.content)
}

// 方法二
func helloHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, net/http! v2\n")
}

func main() {
    http.Handle("/v1", &HelloContext{content: "Hello, net/http! v1\n"})
    http.HandleFunc("/v2", helloHandler)
    http.ListenAndServe(":8080", nil)
}

运行后,可以用 curl 工具进行测试:

mac:~ $ curl 127.0.0.1:8080/v1
Hello, net/http! v1
mac:~ $ curl 127.0.0.1:8080/v2
Hello, net/http! v2

这段代码我们用 http.Handle 和 http.HandleFunc 两种方法分别在路径 /v1 和 /v2 上注册了两个 http.Handler。注意:Handle 和 Handler 是两个东西。
这两个 Handler 都对 request 进行了处理,并且通过 fmt.Fprintf 方法写入并返回数据。

处理器#

http.Handler#

先来了解一下 http.Handler (处理器),

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

它被定义为一个拥有 ServeHTTP 方法的接口,也就是说任何类型,只要实现了 ServeHTTP 方法,就实现了 http.Handler 接口。

ServeHTTP 方法会读取 *Request 信息,并且向 ResponseWriter 写入 header 与 body 内容。

路由注册#

http.Handle#

从 main函数出发,来看 http.Handle 函数源码:

func Handle(pattern string, handler Handler) { 
  DefaultServeMux.Handle(pattern, handler) 
}

可以看到,http.Handle 函数调用了 DefaultServeMux.Handle 方法。

http.HandleFunc#

再来看 http.HandleFunc 的源码:

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
 DefaultServeMux.HandleFunc(pattern, handler)
}

它也调用了 DefaultServeMux.HandleFunc 方法,再看此方法源码:

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

不难看出,http.Handle 和 http.HandleFunc 都调用了一个和 ServeMux 对象的 Handle 方法有关。

这两个方法的作用都是将传入的处理器 (Handler) 注册到对应的路由规则 (pattern)上。

比如,倒数第三行将 处理器 helloHandler 注册到了路由规则 (路径) /v2 上。这样,当 HTTP 请求的地址是 /v2的时候,就由处理器 helloHandler 来负责处理请求,并且响应。

mux.Handle 方法中还有一个 http.HandlerFunc ,注意不是 HandleFunc。

适配器与处理器#

http.HandlerFunc#

type HandlerFunc func(ResponseWriter, *Request)

HandlerFunc 可以理解为一个适配器,它允许使用普通的函数成为处理器 Handler 对象,前提是这个普通函数拥有 func(ResponseWriter, *Request) 签名。

上文说到,任何类型只要实现了 ServeHTTP 方法,那它就实现了 Handler接口,它就是一个 Handler 类型。

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

这里呢,非常的巧妙,HandlerFunc 类型实现了 ServeHTTP 方法,并且又将 ServeHTTP方法的参数传给了自身。

也就是说:

  1. 一个普通的函数,只要参数是 ResponseWriter 和 *Request,或者换种标准点的说法,它的函数签名为 func(ResponseWriter,*Request),那么它就是 HandlerFunc 类型。
  2. 由于 HandlerFunc 自身实现了 ServeHTTP方法,所以这个普通函数又实现了 Handler 接口,成了 Handler 类型。

到这里,如何注册、DefaultServeMux 和 ServeMux 是什么,我们暂时还不知道,为了便于理解,这个下文再说。

监听与服务#

http.ListenAndServe#

接着往下走,看一下 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.
//
// The 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()
}

不难看出,http.ListenAndServe 负责监听 TCP 网络地址 addr, 代码中写的是:8080 也即是监听 8080 端口,并且处理相关的请求。

这里传入的第二个参数是 Handler 类型,根据注释可以看出:如果传入值为 nil ,那么将会使用 DefaultServeMux 。

服务复用器#

DefaultServeMux#

// DefaultServeMux is the default ServeMux used by Serve.
var DefaultServeMux = &defaultServeMux

var defaultServeMux ServeMux

说白了,DefaultServeMux 是 ServeMux 类型的一个实例,由标准库创建。

下面看 ServeMux 结构体的源码。

ServeMux#

ServeMux 是一个结构体,它的作用是服务复用器。

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
}

因为涉及并发,所以这里有个读写锁 mu,主要用于保护下面的 map 类型的成员 m。

es 与 hosts 和路由规则匹配有关。

这里重点关注一下 m,它是一个 map ,key 是 string 类型的路由表达式,val 是 muxEntry 类型的结构体。

type muxEntry struct {
 h       Handler
 pattern string
}

muxEntry 结构体,描述了路由规则 pattern 对应的处理器 h。

mux.Handle#

上文中,http.Handle 和 http.HandleFunc 都调用了 mux.Handle 方法。

它是结构体 ServeMux 的方法,也就是说,此方法主要把 Handler 对象注册到给定的 pattern 上,也即路由注册

// Handle registers the handler for the given pattern.
// If a handler already exists for pattern, Handle panics.
func (mux *ServeMux) Handle(pattern string, handler Handler) {
  // 为了保护 ServeMux 成员 map 类型的 m 的读写,分别在方法开始和结束的时候进行加锁和解锁的操作。
 mux.mu.Lock()
 defer mux.mu.Unlock()
 // 如果路由规则 pattern 为空,则直接 panic。
 if pattern == "" {
  panic("http: invalid pattern")
 }
  // 如果 http.Handler 类型的处理器 handler 为空,则panic。
 if handler == nil {
  panic("http: nil handler")
 }
  // 如果路由规则 pattern 已经存在,则直接 panic。
 if _, exist := mux.m[pattern]; exist {
  panic("http: multiple registrations for " + pattern)
 }
 // 如果成员 m 为空,则 make 一个新的 map。
 if mux.m == nil {
  mux.m = make(map[string]muxEntry)
 }
  // 创建一个 muxEntry,并将 pattern 对应的 Handler 放进去。
 e := muxEntry{h: handler, pattern: pattern}
  // 写入 m, key 为 pattern ,value 为新建的 muxEntry 类型的 e ,也即新增一个路由规则。
 mux.m[pattern] = e
  // 如果路由规则以字符 / 结尾,则给将新建的 muxEntry 类型的 e 放到成员 es 中。
  // es 是一个切片,使用 http.appendSorted 方法加入元素,以确保 es 中的元素(路由)是从最长到最短。
 if pattern[len(pattern)-1] == '/' {
  mux.es = appendSorted(mux.es, e)
 }
  // 最后,如果路由规则不是以字符 / 开头,那么给成员 hosts 赋值 true 。
 if pattern[0] != '/' {
  mux.hosts = true
 }
}

mux.ServeHTTP#

我把 ServeMux 的 ServeHTTP 方法简称为 mux.ServeHTTP,下文也是一样。

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 结构体同样实现了 ServeHTTP 方法,也即它也实现了 Handler 接口,是一个 Handler 类型的对象。

但它并不负责处理具体的请求,篇幅有限,这里给出,调用的 mux.Handler 方法签名:

func (mux *ServeMux) handler(host, path string) (h Handler, pattern string)

总的来说,mux.ServeHTTP 调用了 mux.Handler 方法,通过 host 和 path 找到具体的 处理器 Handler 和路由规则 pattern ,然后让对应的 Handler 的 ServeHTTP 方法去处理请求。

连接与请求的处理#

其实搞懂上面方法以及其之间的关系,对于进一步的学习 Go Web 框架 (比如 Gin ) 就已经有很大的帮助了。从监听与服务开始,代码更加底层,这里我主要关心的是,一次请求是如何到达 ServeHTTP 的。

http.ListenAndServe 方法中,使用传入的监听地址 addr 和处理器 handler 初始化一个 HTTP 服务器 http.Server。

Server 结构体,主要定义了需要跑一个 HTTP Server 所需要的参数:

Server#

type Server struct {
 Addr string
 Handler Handler // handler to invoke, http.DefaultServeMux if nil
  
 TLSConfig *tls.Config
 ReadTimeout time.Duration
 ReadHeaderTimeout time.Duration
 WriteTimeout time.Duration
 IdleTimeout time.Duration
 MaxHeaderBytes int
 TLSNextProto map[string]func(*Server, *tls.Conn, Handler)
 ConnState func(net.Conn, ConnState)
 ErrorLog *log.Logger
 BaseContext func(net.Listener) context.Context
 ConnContext func(ctx context.Context, c net.Conn) context.Context
 inShutdown atomicBool // true when server is in shutdown
 disableKeepAlives int32     // accessed atomically.
 nextProtoOnce     sync.Once // guards setupHTTP2_* init
 nextProtoErr      error     // result of http2.ConfigureServer if used
  
 mu         sync.Mutex
 listeners  map[*net.Listener]struct{}
 activeConn map[*conn]struct{}
 doneChan   chan struct{}
 onShutdown []func()
}

这些参数不是重点,接着往下。

server.ListenAndServe#

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 结构体的 ListenAndServe 方法会监听 TCP 网络地址 addr ,然后调用 srv.Serve 处理传入连接的请求。

srv.Serve#

func (srv *Server) Serve(l net.Listener) error {
 // ...省略部分
 for {
    // 循环监听 TCP 连接
  rw, err := l.Accept()
  if err != nil {
    ...省略部分 
  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)
 }
}

Serve 方法在 Listenner l 上接受传入的连接,并且为每一个连接创建 goroutine 。这些 gorutines 会读取请求并且调用 srv.Handler 去响应它们。

c.Serve#

// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
  // ...
 for {
    // 循环接受请求,一个连接可以处理多个请求
  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, runHooks)
  }
  // 这行代码是重点
  serverHandler{c.server}.ServeHTTP(w, w.req)
  inFlightResponse = nil
  w.cancelCtx()
  if c.hijacked() {
   return
  }
 }
}

serverHandler#

serverHandler 结构体是一个代理,它会代理 server 的 Handler 或 DefaultServeMux 。

type serverHandler struct {
 srv *Server
}

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
  // 这个 handler 就是最初 http.ListenAndServe 传入的 Handler 类型的 handler 。
 handler := sh.srv.Handler
  // 如果 http.ListenAndServe 第二个参数是 nil,那么使用 DefaultServeMux 。
 if handler == nil {
  handler = DefaultServeMux
 }
 if req.RequestURI == "*" && req.Method == "OPTIONS" {
  handler = globalOptionsHandler{}
 }
  
 if req.URL != nil && strings.Contains(req.URL.RawQuery, ";") {
  var allowQuerySemicolonsInUse int32
  req = req.WithContext(context.WithValue(req.Context(), silenceSemWarnContextKey, func() {
   atomic.StoreInt32(&allowQuerySemicolonsInUse, 1)
  }))
  defer func() {
   if atomic.LoadInt32(&allowQuerySemicolonsInUse) == 0 {
    sh.srv.logf("http: URL query contains semicolon, which is no longer a supported separator; parts of the query may be stripped when parsed; see golang.org/issue/25192")
   }
  }()
 }
  // 调用 handler 的 ServeHTTP 方法处理请求。
 handler.ServeHTTP(rw, req)
}

到这里,连接中的请求,就交给了 handler.ServeHTTP 也即 mux.ServeHTTP 方法来处理。

然后 mux.ServeHTTP 方法中,mux.Handler 方法,会根据 request 中的 host 和 path 信息,找到对应的 Handler, 这个 Handler 再处理信息。

总结#

Go net/http 标准库,能让我们轻易地写出一个高性能的 HTTP Server,但肯定不能满足实际业务开发,比如动态路由、中间件、鉴权等这是标准库所不具有的。

很多重复性的工作和常用的工具与特性要由框架来封装和实现,go 很多高性能框架 比如 Gin 都是直接封装了 net/http,这一点难能可贵,由此可见 Go 标准库的价值。

所以学习优秀 Go web 框架的前提就是弄清楚 net/http Server 部分的源码,同时,也能方便更好的去使用和优化框架。
本文所使用的源码均来自 go 1.18.3,部分方法说明翻译自官方注释。

如有不当之处,请批评指出。