深入剖析Go Web服务器实现原理

绘你一世倾城 · · 2239 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

1. 前言

对于Go语言来说,只需要短短几行代码,就可以实现一个简单的http server,加上协程的加持,Go实现的http server拥有非常优秀的性能。如下图所示:

通过net/http标准库,我们可以启动一个http服务器,然后让这个服务器接收请求并返回响应。net/http标准库还提供了一个连接多路复用器(multiplexer)的接口以及一个默认的多路复用器。

本文会对Go标准库net/http服务的实现原理进行较为深入的探究,帮助读者学习网络编程的设计范式和常见解决问题的思路。本文主要分为三部分:

  • 第一部分简单介绍一下web服务器的代码简单实现。
  • 第二部分是web服务器的实现原理和使用技巧,是本文的重点,笔者会由浅入深的介绍Go语言实现web服务器各个环节所涉及到的概念和实现
  • 第三部分是源码实现,读者理解了第二部分的内容,会发现这部分内容不是特别的晦涩难懂。

2. Go Web服务器简单实现

Go语言创建一个服务器的步骤非常简单,只要调用ListenAndServe并传入网络地址以及负责处理请求的处理器(handler)作为参数就可以了。如果网络地址为空字符串,那么服务器默认使用80端口进行网络连接;如果处理器参数为nil,那么服务器将使用默认的多路复用器DefaultServeMux。代码实现:

package main

import "net/http"

func main() {
	http.ListenAndServe("", nil)
}
复制代码

除了可以通过ListenAndServe的参数对服务器的网络地址和处理器进行配置之外,还可以通过Server结构对服务器进行更详细的配置,其中包括为请求读取操作设置超时时间,为响应写入操作设置超时时间、为Server结构设置错误日志记录器等。

package main

import "net/http"

func main() {
	server := http.Server{
		Addr:    "127.0.0.1:8080",
		Handler: nil,
	}
	server.ListenAndServe()
}
复制代码

Server结构提供了很多的可选配置项:

// A Server defines parameters for running an HTTP server.
// The zero value for Server is a valid configuration.
type Server struct {
	Addr    string  // TCP address to listen on, ":http" if empty
	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
}
复制代码

3. web服务器的实现原理和使用技巧

前边我们启动了一个web服务器,但是我们访问这个服务器地址会得到:404 page not found 的信息。出现这一问题的原因是我们没有为服务器编写任何处理器,服务器的多路复用器在接收到请求之后找不到任何处理器来处理请求,因此它只能返回一个404响应。为了让服务器能够产生实际的响应结果,我们需要编写处理器。

3.1 处理器和处理器函数

处理器和处理器函数是不同的,本节我们会详细谈谈它们的定义。在Go语言中,一个处理器就是一个拥有ServeHTTP方法的接口,这个ServeHTTP方法需要接收两个参数:第一个参数是一个ResponseWriter接口,而第二个参数则是一个指向Request结构的指针。换句话说,任何接口只要拥有一个ServeHTTP方法,并且该方法带有以下签名,那么它就是一个处理器:

ServeHTTP(http.ResponseWriter, *http.Request)
复制代码

不知道读者有没有想过一个问题: ServeHTTP为什么要接受ResponseWriter接口和一个指向Request结构的指针作为参数呢?接收Request指针的原因很简单:为了让服务器能够觉察到处理器对Request结构的修改,我们必须要以传引用(pass by reference)而不是传值(pass by value)的方式传递Request结构。但是另一方面,为什么ServeHTTP却是以传值的方式接受ResponseWriter呢?难道服务器不需要知道处理器对ResponseWriter所做的修改吗?对于这个问题,笔者深入探究了一下net/http库的源码,在这里给出答案:ResonseWriter实际上就是response这个非导出结构的接口,而ResponseWriter在使用response结构时,传递的也是指向response 结构的指针,也就是说,ResponseWriter 是以传引用而是传值的方式在使用response结构。换句话说:实际上ServeHTTP函数的两个参数传递的都是引用而不是值——虽然ResponseWriter看上去是一个值,但它却是一个带有结构指针的接口

为了搞清楚另外一个问题,我们不得不再离题一下。既然ListenAndServe第二个参数是一个处理器,那么为何它的默认值却是多路复用器DefaultServeMux呢?原来DefaultServeMux是ServeMux结构的一个实例,ServeMux结构也拥有ServeHTTP方法,并且这个方法的签名与成为处理器所需要的签名完全一致。所以有了结论:DefaultServeMux不仅是一个多路复用器,它还是一个处理器。不过DefaultServeMux处理器与其他的处理器不同,DefaultServeMux是一个特殊的处理器,它唯一要做的就是根据请求的URL将请求重定向到不同的处理器。

有了这个结论以后,我们就可以自行编写一个处理器并使用它去代替默认的多路复用器,使服务器能够对客户端正常响应了,代码实现:

package main

import (
	"fmt"
	"net/http"
)

type MyHandler struct {}

func (h *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "hello world!")
}

func main() {
	handler := MyHandler{}
	server := http.Server{
		Addr:    "127.0.0.1:8080",
		Handler: &handler,
	}
	server.ListenAndServe()
}
复制代码

现在,我们再次通过浏览器访问http://localhost:8080/得到的结果将是:hello world!

但是无论我们访问http://localhost:8080/的任何路由得到的结果始终是hello world!,比如http://localhost:8080/get/list。造成这个问题的原因很明显:我们自己创建了一个处理器并与服务器绑定,替换了原来使用的默认多路复用器(DefaultServeMux),也就是服务器不会再通过URL匹配将请求导向到不同的处理器,而是直接使用自己的处理程序处理所有的请求,这并不是我们想要的!我们继续使用DefaultServeMux作为处理器好了,然后通过http.Handle函数将处理器绑定至DefaultServeMux。Handle函数是为了操作便利而创建的函数,调用它等同于调用了DefaultServeMux的Handle方法。代码如下:

package main

import (
	"fmt"
	"net/http"
)

type HelloHandler struct {}

func (h *HelloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "hello")
}

type WorldHandler struct {}

func (h *WorldHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "world!")
}


func main() {
	server := http.Server{
		Addr:    "127.0.0.1:8080",
	}
	hello := HelloHandler{}
	http.Handle("/hello", &hello)
	world := WorldHandler{}
	http.Handle("/world", &world)

	server.ListenAndServe()
}
复制代码

知道了什么是处理器,我们再来看一下什么是处理器函数:处理器函数实际上就是与处理器拥有相同行为的函数,这些函数与ServeHTTP方法拥有相同的签名。我们将上边的处理器方法简化一下,代码如下:

package main

import (
	"fmt"
	"net/http"
)

func hello(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "hello")
}

func world(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "world!")
}


func main() {
	server := http.Server{
		Addr:              "127.0.0.1:8080",
	}
	http.HandleFunc("/hello", hello)
	http.HandleFunc("/world", world)

	server.ListenAndServe()
}
复制代码

处理器函数的实现原理也很简单:Go语言拥有一种HandlerFunc函数类型,它可以把一个带有正确签名的函数f转换成一个带有方法f的Handler。比如说hello函数,程序只需要执行以下代码:

helloHandler := HandlerFunc(hello)
复制代码

就可以把helloHandler设置成为一个Handler。后边我们会通过源码再来回顾以下HandleFunc的实现原理,我们现在只需要记住结论就好了:处理器函数只不过是创建处理器的一种便利的方法而已

3.2 ServeMux 和 DefaultServeMux

我们之前对ServeMux和DefaultServeMux做过介绍,ServeMux是一个HTTP请求多路复用器,它负责接收HTTP请求并根据请求中的URL将请求重定向到正确的处理器,ServeMux也实现了ServeHTTP方法,它也是一个处理器。当ServeMux的ServeHTTP方法接收到一个请求的时候,它会在结构的映射里面找出与被请求URL最为匹配的URL,然后调用与之相对应的处理器的ServeHTTP方法,如图所示:

因为ServeMux是一个结构而不是一个接口,所以DefaultServeMux并不是ServeMux的实现。DefaultServeMux实际上是ServeMux的一个实例,并且所有引入了net/http标准库的程序都可以使用这个实例。当用户没有为Server结构指定处理器时,服务器就会使用DefaultServeMux作为ServeMux的默认实例。

上边的例子中,请求URL /hello完美的匹配了多路复用器绑定的URL,但是如果我们使用浏览器访问/hello/china呢?那么服务器会返回什么响应呢?

这个问题的答案跟我们绑定的URL方法相关:匹配不成功的URL会根据URL的层级逐层下降,直到最终落到根URL上。当浏览器访问/hello/china时,会返回什么结果呢?很多人会认为因为我们的程序已经绑定了/hello,所以响应结果应该是hello。相信试验过后的读者可能会有所失望,服务器并没有这样去做,因为程序在绑定url使用的是/hello而不是/hello/。如果被绑定的url不是以/结尾,那么它只能会与完全相同的url匹配;但是如果被绑定的url以/结尾,那么即使请求的url只有前缀部分与被绑定的url相同,ServeMux也会认为这两个url是匹配的。(留心的读者可能会发现,这和Nginx的策略是相同的)

ServeMux的一个缺陷是无法使用变量实现URL模式匹配,比如/get/post/123,我们的本意是获得id为123的帖子内容,但是我们使用ServeMux并不能实现这样的功能。所以才有了业界著名的HttpRouter包的出现。

3.3 优雅的实现Web框架中间件

我们上边了解了如何Go语言如何实现一个服务器,知道了很多概念和原理,这很枯燥,本节我们来实战一个例子:优雅的实现Web框架中间件。

中间件是什么,在这里无需多言,利用中间件技术可以将业务代码和非业务代码解耦。我们来看一下我们要实现中间件的用法:

r := router.NewRouter()
r.Use(timeMiddleware)
r.Add("/", http.HandlerFunc(hello))
r.Run(":8080")
复制代码

相信没有比这更简洁、更灵活的中间件了,实现它却非常简单:

package router

import "net/http"

type middleware func(http.Handler) http.Handler

type Router struct {
	middlewareChain [] middleware
	mux map[string] http.Handler
}

func NewRouter() *Router{
	return &Router{
		[]middleware{},
		make(map[string] http.Handler),
	}
}

func (r *Router) Use(m middleware) {
	r.middlewareChain = append(r.middlewareChain)
}

func (r *Router) Add(route string, h http.Handler) {
	var mergeHandler = h
	for i := len(r.middlewareChain) - 1; i >= 0; i-- {
		mergeHandler = r.middlewareChain[i](mergeHandler)
	}
	r.mux[route] = mergeHandler
}

func (r *Router) Run(addr string) {
	for route, handler := range r.mux {
		http.Handle(route, handler)
	}
	http.ListenAndServe(addr, nil)
}
复制代码

可以看到我们代码实现的核心逻辑也只有40行的代码,首先定义一个路由结构体,其内容一个是中间件的调用链条,我们用slice存放,另外一个是实现了路由及其handler的map:

type Router struct {
	middlewareChain [] middleware
	mux map[string] http.Handler
}
复制代码

我们为路由结构体定义了三个方法,Use、Add、Run。Use的实现只是简单的向中间件链条中添加了一个元素而已,使用append方法,middlerware并不是随便写的,我们也定义了一个结构体,接收一个http.Handler,返回值也是一个http.Handler,这样我们才能在最后使用handle将路由url成功注册到处理器上:

type middleware func(http.Handler) http.Handler
...
func (r *Router) Use(m middleware) {
	r.middlewareChain = append(r.middlewareChain)
}
复制代码

Add方法就是使用middleware对处理器函数进行包装,需要注意的是代码中middleware数组遍历顺序与用户希望的调用顺序是“相反”的,这个应该不难理解。

func (r *Router) Add(route string, h http.Handler) {
	var mergeHandler = h
	for i := len(r.middlewareChain) - 1; i >= 0; i-- {
		mergeHandler = r.middlewareChain[i](mergeHandler)
	}
	r.mux[route] = mergeHandler
}
复制代码

最后是Run函数,所做的工作就是将我们定义的路由解析,也就是将url绑定到指定的处理器上,我们默认只开发了addr可配置,使用DefaultServeMux:

func (r *Router) Run(addr string) {
	for route, handler := range r.mux {
		http.Handle(route, handler)
	}
	http.ListenAndServe(addr, nil)
}
复制代码

接下来我们就可以在main函数中使用它了:

......
func hello(wr http.ResponseWriter, r *http.Request) {
	wr.Write([]byte("hello"))
}

func timeMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(wr http.ResponseWriter, r *http.Request) {
		timeStart := time.Now()
		//next handler
		next.ServeHTTP(wr, r)

		timeElapsed := time.Since(timeStart)

		log.Println(timeElapsed)
	})
}


func main() {
	r := router.NewRouter()
	r.Use(timeMiddleware)
	r.Add("/", http.HandlerFunc(hello))
	r.Run(":8080")
}
复制代码

我们定义了一个timeMiddleware,用来统计接口的耗时,timeMiddleware符合middleware结构体的定义,注意在返回的匿名函数内调用了next.ServeHTTP(wr, r)方法。路由的Add方法中使用HandlerFunc对hello处理器函数做了封装,使其转化为了处理器。

这样我们一个优雅的web中间件就实现了,我们可以拓展这个中间件的功能:比如在Run函数中添加更多的控制参数。给指定的处理器函数添加指定的中间件等等,这些有趣而实用的功能就交给感兴趣的读者完成吧。

4. Go Web服务器实现源码解析

学习软件设计的思想和深入理解架构理论最好的途径是阅读源码,本节将带领大家一步步阅读Go Web服务器的实现逻辑,相信大家能收获很多。

4.1 路由注册:http.HandleFunc 和 http.Handle源码

我们前边看到,http.HandleFunc和http.Handle方法都是用于注册路由,两者的区别在于第二个参数,前者是一个具有 func(w http.ResponseWriter, r *http.Requests) 签名的函数,而后者是一个结构体,该结构体实现了 func(w http.ResponseWriter, r *http.Requests) 签名的方法。

http.HandleFunc 和 http.Handle源码实现如下:

// HandleFunc registers the handler function for the given pattern.
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	if handler == nil {
		panic("http: nil handler")
	}
	mux.Handle(pattern, HandlerFunc(handler))
}
...
// HandleFunc registers the handler function for the given pattern
// in the DefaultServeMux.
// The documentation for ServeMux explains how patterns are matched.
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	DefaultServeMux.HandleFunc(pattern, handler)
}
复制代码
// Handle registers the handler for the given pattern
// in the DefaultServeMux.
// The documentation for ServeMux explains how patterns are matched.
func Handle(pattern string, handler Handler) { DefaultServeMux.Handle(pattern, handler) }
复制代码

可以看到最终这两个函数都是由多路复用器调用Handle方法完成路由的注册。这里我们遇到了两种类型的对象:ServeMux 和 Handler,我们先来说Handler:

Handler是一个接口:

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}
复制代码

Handler 接声明了名为 ServeHTTP 的函数签名,也就是说任何结构只要实现了这个 ServeHTTP 方法,那么这个结构体就是一个 Handler 对象。其实go的 http 服务都是基于 Handler 进行处理,而 Handler 对象的 ServeHTTP 方法也正是用以处理 request 并构建 response 的核心逻辑所在。

在上边的代码中,我们看到有这么一段逻辑:

mux.Handle(pattern, HandlerFunc(handler))
复制代码

这里的HandlerFunc是个什么呢?函数、结构体......,答案还要从源码中寻找:

// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler that calls f.
type HandlerFunc func(ResponseWriter, *Request)

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

读到这里,读者应该明白了,HandlerFunc其实是一个类型,只不过是具有func(ResponseWriter, *Request)函数签名的类型。HandlerFunc(handler)只不过是将handler方法做了类型转化。并且这种类型自己实现了ServeHTTP函数,也就是说这个类型的函数其实就是一个 Handler 类型的对象。利用这种类型转换,我们可以将一个 handler 函数转换为一个 Handler 对象,而不需要定义一个结构体,再让这个结构实现 ServeHTTP 方法。读者可以认真体会一下这种设计思想。

4.2 路由解析: ServeMux和 DefaultServeMux源码实现

前边我们说到,Go语言中的路由基于多路复用器实现,多路复用器基本结构是: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
}

type muxEntry struct {
	h       Handler
	pattern string
}
复制代码

ServeMux中的m是一个map,key是路由表达式,value是muxEntry结构体,muxEntry结构体的内容是路由表达式和handler(处理器)。

我们前边说过ServeMux也实现了ServeHTTP方法:

// 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)
}
复制代码

代码很简单,不做过多解释,我们直到了什么是handler和ServeMux,再回到原来的代码逻辑:

func Handle(pattern string, handler Handler) { DefaultServeMux.Handle(pattern, handler)
}
复制代码

我们知道:DefaultServeMux是Go语言默认提供的多路复用器(Multiplexer)。当我们没有创建自定义的 Multiplexer ,则会自动使用一个默认的 Multiplexer 。它是ServeMux的一个实例。我们就来看一下ServeMux的Handle方法都做了些什么工作吧,我们前边其实也说过,它将url映射到了相关的处理器。源码如下:

// 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) {
	mux.mu.Lock()
	defer mux.mu.Unlock()

	if pattern == "" {
		panic("http: invalid pattern")
	}
	if handler == nil {
		panic("http: nil handler")
	}
	if _, exist := mux.m[pattern]; exist {
		panic("http: multiple registrations for " + pattern)
	}

	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
	}
}
复制代码

我们发现:ServeMux 的 map[string]muxEntry 增加给定的路由匹配规则;还将路由表达式以 '/' 结尾对应的 muxEntry 对象加入到 []muxEntry 中,按照路由表达式长度排序。这就是我们上边举例子中为什么/hello/注册和/hello是不一样的了,降级的匹配规则就是这样实现的

4.3 开启监听服务ListenAndServe源码实现

我们前边代码中多次使用到ListenAndServe,我们来看一下它的实现:

func ListenAndServe(addr string, handler Handler) error {
	server := &Server{Addr: addr, Handler: handler}
	return 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(tcpKeepAliveListener{ln.(*net.TCPListener)})
}
复制代码

这里先创建了一个 Server 对象,传入了地址和 handler 参数,然后调用 Server 对象 ListenAndServe() 方法。这是http调用ListenAndServe的方式:

http.ListenAndServe(":8080", nil)
复制代码

如果是nil,就使用默认的DefaultServeMux,它也是一个ServeMux实例。Server结构体我们前边提到过,再来看一下:

type Server struct {
	Addr    string  // TCP address to listen on, ":http" if empty
	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)
	ErrorLog *log.Logger

	disableKeepAlives int32     // accessed atomically.
	inShutdown        int32     // accessed atomically (non-zero means we're in Shutdown)
	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 方法中,会初始化监听地址 Addr ,同时调用 Listen 方法设置监听。最后将监听的TCP对象传入 Serve 方法:

func (srv *Server) Serve(l net.Listener) error {
 ...

 baseCtx := context.Background() // base is always background, per Issue 16220
 ctx := context.WithValue(baseCtx, ServerContextKey, srv)
 for {
  rw, e := l.Accept() // 等待新的连接建立

  ...

  c := srv.newConn(rw)
  c.setState(c.rwc, StateNew) // before Serve can return
  go c.serve(ctx) // 创建新的协程处理请求
 }
}
复制代码

这里隐去了一些细节,以便了解 Serve 方法的主要逻辑。首先创建一个上下文对象,然后调用 Listener 的 Accept() 等待新的连接建立;一旦有新的连接建立,则调用 Server 的 newConn() 创建新的连接对象,并将连接的状态标志为 StateNew ,然后开启一个新的 goroutine 处理连接请求。

conn的Serve方法依然很长,不过我们从中能学习到很多东西:

// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
	c.remoteAddr = c.rwc.RemoteAddr().String()
	ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())

    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)
		}
		......

		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))
		......
		c.rwc.SetReadDeadline(time.Time{})
	}
}
复制代码

当一个连接建立之后,该连接中所有的请求都将在这个协程中进行处理,直到连接被关闭。在 serve() 方法中会循环调用 readRequest() 方法读取下一个请求进行处理。

c.setState(c.rwc, StateIdle)  //将连接状态置为空闲
c.curReq.Store((*response)(nil)) //将当前请求置为空
复制代码

其中最关键的逻辑就是一行代码:

serverHandler{c.server}.ServeHTTP(w, w.req)
复制代码

我们继续在源码中查看serverHandler:

// serverHandler delegates to either the server's Handler or
// DefaultServeMux and also handles "OPTIONS *" requests.
type serverHandler struct {
	srv *Server
}

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
	handler := sh.srv.Handler
	if handler == nil {
		handler = DefaultServeMux
	}
	if req.RequestURI == "*" && req.Method == "OPTIONS" {
		handler = globalOptionsHandler{}
	}
	handler.ServeHTTP(rw, req)
}
复制代码

在 serverHandler 的 ServeHTTP() 方法里的 sh.srv.Handler 其实就是我们最初在 http.ListenAndServe() 中传入的 Handler 对象,也就是我们自定义的 ServeMux 对象。如果该 Handler 对象为 nil ,则会使用默认的 DefaultServeMux 。最后调用 ServeMux 的 ServeHTTP() 方法匹配当前路由对应的 handler 方法。

接下来就是ServeMux的handler方法:

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)
	}

	// All other requests have any port stripped and path cleaned
	// before passing to mux.handler.
	host := stripHostPort(r.Host)
	path := cleanPath(r.URL.Path)

	// If the given path is /tree and its handler is not registered,
	// redirect for /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 is the main implementation of Handler.
// The path is known to be in canonical form, except for CONNECT methods.
func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
	mux.mu.RLock()
	defer mux.mu.RUnlock()

	// Host-specific pattern takes precedence over generic ones
	if mux.hosts {
		h, pattern = mux.match(host + path)
	}
	if h == nil {
		h, pattern = mux.match(path)
	}
	if h == nil {
		h, pattern = NotFoundHandler(), ""
	}
	return
}
......
// Find a handler on a handler map given a path string.
// Most-specific (longest) pattern wins.
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
	// Check for exact match first.
	v, ok := mux.m[path]
	if ok {
		return v.h, v.pattern
	}

	// Check for longest valid match.  mux.es contains all patterns
	// that end in / sorted from longest to shortest.
	for _, e := range mux.es {
		if strings.HasPrefix(path, e.pattern) {
			return e.h, e.pattern
		}
	}
	return nil, ""
}
复制代码

ServeMux的Handler方法就是根据url调用指定handler方法,handler方法的作用是调用match匹配路由。在 match 方法里我们看到之前提到的 map[string]muxEntry 和 []muxEntry 。这个方法里首先会利用进行精确匹配,在 map[string]muxEntry 中查找是否有对应的路由规则存在;如果没有匹配的路由规则,则会进行近似匹配。

对于类似 /path1/path2/path3 这样的路由,如果不能找到精确匹配的路由规则,那么则会去匹配和当前路由最接近的已注册的父路由,所以如果路由 /path1/path2/ 已注册,那么该路由会被匹配,否则继续匹配父路由,直到根路由 / 。

ServeMux的Handler方法中找到要执行的handler之后,就调用handler的serveHTTP方法,至此,大家应该熟悉Go Web服务器的整个实现过程了。

5.小结

Go语言实现Web服务器的设计还是相当精巧的,简单总结如下:

  • 一个方法只要实现了ServeHTTP的接口,它就是一个处理器。
ServeHTTP(http.ResponseWriter, *http.Request)
复制代码
  • 处理器是处理用户逻辑的核心所在,Go语言提供了handleFunc方法,将带有func(http.ResponseWriter, *http.Request)函数签名(处理器函数)的方法都能转化为处理器,从而方便读者实现业务逻辑。

  • Go提供了一个默认的多路复用器DefaultServeMux,它是ServeMux的一个实例,同样实现了ServeHTTP方法,作用是将url绑定到具体的处理器。

我们还简单实现了一个web中间件,帮助读者了解了Go设计服务器接口的灵活性和可拓展性。

文章的最后我们还简单分析了一下Go Web服务器实现的源码,学习了一下网络编程的设计思想。希望本文能给读者带来一些收获,为今后学习Go语言在Web方面的编程打下基础。


有疑问加站长微信联系(非本文作者)

本文来自:掘金

感谢作者:绘你一世倾城

查看原文:深入剖析Go Web服务器实现原理

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

2239 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传