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

推荐订阅源

Google DeepMind News
Google DeepMind News
C
Cybersecurity and Infrastructure Security Agency CISA
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
Tailwind CSS Blog
G
GRAHAM CLULEY
博客园 - 叶小钗
T
Threatpost
小众软件
小众软件
The Hacker News
The Hacker News
博客园 - 聂微东
博客园 - 三生石上(FineUI控件)
P
Privacy & Cybersecurity Law Blog
AWS News Blog
AWS News Blog
P
Proofpoint News Feed
Jina AI
Jina AI
S
Schneier on Security
N
News | PayPal Newsroom
Help Net Security
Help Net Security
A
Arctic Wolf
T
The Blog of Author Tim Ferriss
大猫的无限游戏
大猫的无限游戏
T
Troy Hunt's Blog
美团技术团队
L
Lohrmann on Cybersecurity
The Last Watchdog
The Last Watchdog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
P
Privacy International News Feed
D
Darknet – Hacking Tools, Hacker News & Cyber Security
C
Cisco Blogs
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Hugging Face - Blog
Hugging Face - Blog
B
Blog RSS Feed
The Register - Security
The Register - Security
博客园 - Franky
Stack Overflow Blog
Stack Overflow Blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
S
SegmentFault 最新的问题
腾讯CDC
云风的 BLOG
云风的 BLOG
Simon Willison's Weblog
Simon Willison's Weblog
Google DeepMind News
Google DeepMind News
AI
AI
GbyAI
GbyAI
Attack and Defense Labs
Attack and Defense Labs
Cloudbric
Cloudbric
I
Intezer
The GitHub Blog
The GitHub Blog
V2EX - 技术
V2EX - 技术
Scott Helme
Scott Helme
J
Java Code Geeks

ISLAND

轻糖的 KMP 渐进式迁移实践(二):ViewModel、本地存储与平台抽象 SugarLite 的 KMP 渐进式迁移实践 在 CC Switch 中配置 Claude Desktop 使用 cc switch 和 cc desktop switch 快速切换 Claude Code 供应商 用vscode开发 ios/macos App LazyVim 使用 分布式理论 neovim入门指南(四):LSP配置(下) neovim入门指南(三):LSP配置(上) neovim入门指南(二):常用插件 neovim入门指南(一):基础配置 Gin源码分析一:引擎 Engine vim 学习路径
HTTP 源码解析
youngxhui · 2023-04-30 · via ISLAND

警告

本文最后更新于 2023-08-24,文中内容可能已过时。

Go 语言以其出色的并发性能和优雅的编程模型而闻名,对于 http 服务可以做到开箱即用,无需第三方框架,而且使用起来也很简单。即便如此,还会有很多 http 框架的诞生,例如 ginecho 等,说明自带的 http 服务还有不完美的地方,导致了用户选择第三方开发的框架。

这是一个采用标准库开发的 http 服务。

package main

import "net/http"

func main() {
	http.HandleFunc("/ping", pingHandler)
	http.ListenAndServe(":8000", nil)
}

func pingHandler(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("pong"))
}

这是一个简单的HTTP服务。在启动服务后,通过访问 http://localhost:8000/ping,可以访问相关的处理程序并返回响应信息:“pong”。

这段简单的代码中,直接运行了一个高性能的HTTP服务。代码中涉及了两个与HTTP相关的函数,分别是:http.HandleFunchttp.ListenAndServe

接下来,对这两个函数进行解析,让您了解每个步骤的具体操作。

HandleFunc函数的作用是将指定的处理函数handle注册到HTTP框架中,使得特定的URL路径和该处理函数建立映射关系。

以下是HandleFunc()函数的源代码实现

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

在这段代码中,HandleFunc函数接收两个参数:patternhandlepattern是URL路径的模式,用于匹配请求的URL;handle是处理函数,接收ResponseWriterRequest作为参数,用于处理请求并生成相应的响应。

函数内部调用了DefaultServeMux.HandleFunc()函数,将patternhandle注册到默认的 ServeMux(多路复用器)中,建立URL和处理函数之间的映射关系。

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

HandleFunc函数是ServeMux类型的方法。ServeMux是一个HTTP请求多路复用器,用于将请求路由到相应的处理程序。在这个方法中,我们传入了一个pattern参数和一个handler函数参数。

type HandlerFunc func(ResponseWriter, *Request)

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

HandlerFunc 的定义很简单,并且实现了 ServeHTTP 方法。这个方法主要是调用本身。

mux.Handler实现同样比较简单,可以看到将路由(pattern)和具体实现(handler)注册到 DefaultServeMux 这个对象上。通过不断的往下看源码,可以找到 ServeMux.Handler 这个方法上。这个方法主要是将服务中的路由进行注册。将服务注册到 ServerMux 对象上,也就是上文所提到的 DefaultServeMux

对于 ServeMux 来说,是一个比较简单的结构。其中可以看到 mes 保存了路由

var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux

type ServeMux struct {
	mu    sync.RWMutex
	m     map[string]muxEntry
	es    []muxEntry
	hosts bool
}

type muxEntry struct {
	h       Handler
	pattern string
}

func (mux *ServeMux) Handle(pattern string, handler Handler) {
	// 省略代码 ...
	if mux.m == nil {
		mux.m = make(map[string]muxEntry)
	}
	e := muxEntry{h: handler, pattern: pattern}
	mux.m[pattern] = e
	if pattern[len(pattern)-1] == '/' {
		mux.es = appendSorted(mux.es, e)
	}

	if pattern[0] != '/' {
		mux.hosts = true
	}
}

如果 ServerMux.m 为空,会进行初始化,之后将 handlerpattern 存放到 muxEntry 中,最后将 muxEntry 存放到 m 中,m 的 key 是 pattern。这里的 pattern 就是 url 路径。

在 ServeMux 中,mux.es 切片是用来保存以斜杠结尾的路由模式对应的 muxEntry 对象的。它的作用是在请求的 URL 中去掉末尾的斜杠后进行匹配,从而避免重复处理类似 /path 和 /path/ 这样的 URL。

例如,如果有两个路由模式分别为 /path 和 /path/,请求的 URL 为 /path/,如果没有 mux.es 切片,将会尝试匹配 /path 和 /path/ 两个路由模式,最终会选择匹配 /path/ 的路由模式进行处理。这会导致处理器被重复调用。而使用 mux.es 切片,请求的 URL 会被处理为 /path,只会匹配到 /path 这一个路由模式,避免了处理器被重复调用的问题。

因此,mux.es 切片的作用是为了提高 ServeMux 的匹配效率,避免重复处理请求。

所有的 url 和 Handler 的映射关系都是通过 map[string]muxEntry 进行保存。这样就会出现问题,稍微复杂一些的 url 就无法很好的匹配。这也就是为什么会有大量的 go web 框架,而这些框架都是改写路由的匹配算法。

关于 DefaultServeMux 是一个实现了 HandlerServe 接口的结构体。

上面就是一个主要的注册过程。

ListenAndServe

这个方法主要对端口进行监听。

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

从源码可以看到,这里需要一个 handler 参数,并且新生成一个 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)
}

在 ListenAndServe 中,首先对服务的状态进行了判断,如果是 shuttingDown 就提示 http: Server closed。这里的 shuttingDown 主要是通过一个叫 atomicBool 进行判断的。咋一看以为是原子操作,仔细看其实是定义了一个 int32 类型,通过 int32 的原子操作保证了并发安全。

type atomicBool int32

func (b *atomicBool) isSet() bool { return atomic.LoadInt32((*int32)(b)) != 0 }
func (b *atomicBool) setTrue()    { atomic.StoreInt32((*int32)(b), 1) }
func (b *atomicBool) setFalse()   { atomic.StoreInt32((*int32)(b), 0) }

之后通过 net.Listen() 方法进行监听,这里对这个方法不做过多的赘述,之后通过 Serve 方法。 下面是主要的核心方法

ctx := context.WithValue(baseCtx, ServerContextKey, srv)
for {
	rw, err := l.Accept()
	if err != nil {
		select {
		case <-srv.getDoneChan():
			return ErrServerClosed
		default:
		}
		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) // before Serve can return
	go c.serve(connCtx)
}

l 为 net.Listener 对象,当每次接收到信息的时候,首先会进行一个错误判断。 如果是 down 的信号,就会直接返回相关错误,否则先对错误进行断言,检查是否为 net.Error,这个是一个接口,其中 Temporary 方法官方已经标记为启用,这个方法更多的表示为超时。如果有超时,你们就会对延时 tempDelay,进行增加,起初是 5 毫秒,之后每次增加 2 倍,最大为 1 秒钟,之后会进行重试。

通过 srv.ConnContext 会生成一个新的 ctx,否则就使用之前的 ctx,也就是 context.Backgroud()。然后开启一个协程进行服务。

在协程中,通过 readRequest 方法进行获取,返回 response。通过 response 对象获取 request。通过 request 判断请求是否要继续。

req := w.req
if req.expectsContinue() {
	if req.ProtoAtLeast(1, 1) && req.ContentLength != 0 {
		req.Body = &expectContinueReader{readCloser: req.Body, resp: w}
		w.canWriteContinue.Store(true)
	}
} else if req.Header.get("Expect") != "" {
	w.sendExpectationFailed()
	return
}

这里判断首先通过 expectsContinue 方法,这个方法中获取请求头中的 Expect 字段是否等于 100-continue。当等于的时候要继续进行判断,其中请求的协议为 HTTP 1.1 和 ContentLength 不为 0。这样就可以获取到请求体。

当请求头中的 Expect 和上述条件不相同的时候,直接返回 417 错误。

之后创建了一个 serverHandler 并且调用了 ServeHTTP 并且传入了 response 和 request。

ServeHTTP 再一次出现,其中第一步就是获取 Handler。

handler := sh.srv.Handler
if handler == nil {
	handler = DefaultServeMux
}

那么,这里获取的 handler 应该是什么呢?经过多个方法或函数,可以已经对 sh.srv.Handler 一步一步的向上推到。这里我将这个过程画了一张图,图上箭头表示关系之间的依赖,红色表示持有 handler 数据。

https://island-hexo.oss-cn-beijing.aliyuncs.com/http_handler.png

通过这个依赖图可以看到,handler 是由最开始的 ListenAndServe 方法进行传入的,而我们的示例代码中这部分传入的是 nil,也就是从开始到现在 handler 一直为 nil。这也就是为什么会一个判断,当 handler 为空的时候使用 DefaultServeMux。其实关于默认的 handler 为 DefaultServeMux 这个事情,在 ListenAndServe 这个代码的注释中就已经说明。

之后就是调用 handler.ServeHTTP 方法,ServeHTTP 方法主要是一个请求分发的作用。源码中调用了 Handler 方法。这个方法主要的作用就是查找请求对应的 Handler 是那个。

// ServeHTTP dispatches the request to the handler whose
// pattern most closely matches the request 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)
}

先对方法为 CONNECT 的请求做了处理。通过 redirecToPathSlash 方法,这个方法主要是要判断给定的路径是否要加 /,而这个方法中加锁后调用了 shouldRedirectRLocked 这个方法。通过查找 ServeMux 的 m 这个属性中是否存在相关的路径。这个 m 在上文中介绍过,是一个 map 结构。这也就是为什么会加锁,这里的 map 是并发不安全的。查询的方式也很简单,就是在之前的 map 中查询是否存在,如果存在就返回 false,如果没有找到会进行一些特殊的处理,在路径上加上后缀 / 进行查找,同时为了防止路径为 //,所以在返回的时候又进行了一次判断。

在上述的方法结束后,会返回一个 bool 值,来确定是否需要添加末尾的 /。从而返回相关的 url。通过新的 url 生成一个 RedirectHandler 的结构体。如果没有找到通过 通过 handler 这个方法。

handler 这个方法通过 match 这个方法进行查找。

func (mux *ServeMux) match(path string) (h Handler, pattern string) {
	v, ok := mux.m[path]
	if ok {
		return v.h, v.pattern
	}

	for _, e := range mux.es {
		if strings.HasPrefix(path, e.pattern) {
			return e.h, e.pattern
		}
	}
	return nil, ""
}

这个方法比较简单,现在 m 中进行查找,如果没有找到,从 es 中查找。之前我们说过 es 是保存了后缀有 / 的 handler。如果都无法查找到就返回 nil。

这里的 handler 就是在一开始注册的 HandleFunc(pingHandler)

最后在 ServeMux.ServeHTTP 这个方法中调用 handler 相关的 ServeHTTP。这样就调用成功了。

通过两个方法基本可以做到路由的注册方式和路由的查询方式,并且对请求来临的时候相关的处理过程。这些方法对之后研究其他框架源码或者工作方式更加清晰。