Golang 原生Http Server实现


引言

本文我们来看看golang原生http库的实现 , 首先来看一下golang http库的demo案例:

package main

import (
	"fmt"
	"net/http"
)

func HelloHandle(w http.ResponseWriter, req *http.Request) {
	fmt.Println(w, "hello world")
}

func main() {
	http.HandleFunc("/hello", HelloHandle)
	err := http.ListenAndServe(":8080", nil)
	if err != nil {
		return
	}
}

首先,我们调用http.HandleFunc(“/”, index)注册路径处理函数,这里将路径/的处理函数设置为index。处理函数的类型必须是:

func (http.ResponseWriter, *http.Request)

其中*http.Request表示 HTTP 请求对象,该对象包含请求的所有信息,如 URL、首部、表单内容、请求的其他内容等。

http.ResponseWriter是一个接口类型:

// net/http/server.go
type ResponseWriter interface {
  Header() Header
  Write([]byte) (int, error)
  WriteHeader(statusCode int)
}

用于向客户端发送响应,实现了ResponseWriter接口的类型显然也实现了io.Writer接口。所以在处理函数HelloHandle中,可以调用fmt.Fprintln()向ResponseWriter写入响应信息。


源码解析

整体流程

Golang 默认提供的http server实现,核心流程还是比较简单的,如下图所示:
在这里插入图片描述

本文只抓核心逻辑,细节大家后续可以慢慢研究,很多框架核心流程其实都不难理解,难的还是各种细节处理。


路由注册

下面我们深入源码,一起来看看具体实现。

首先是用来注册路由映射的HandleFunc函数:

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

我们发现它直接调用了一个名为DefaultServeMux对象的HandleFunc()方法。DefaultServeMux是ServeMux类型的实例:

type ServeMux struct {
  mu    sync.RWMutex // 互斥锁
  m     map[string]muxEntry  // 请求映射集合
  es    []muxEntry // 有序数组,用于实现最长前缀匹配
  hosts bool       // whether any patterns contain hostnames
}

// 请求映射条目
type muxEntry struct {
	h       Handler // 处理器 
	pattern string // 请求路径
}

// http请求处理器
type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

// 默认情况下提供的多路复用处理器
var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux

像这种提供默认类型实例的用法在 Go 语言的各个库中非常常见,在默认参数就已经足够的场景中使用默认实现很方便

ServeMux保存了注册的所有路径和处理函数的对应关系。ServeMux.HandleFunc()方法如下:

func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
  mux.Handle(pattern, HandlerFunc(handler))
}

这里将处理函数handler转为HandlerFunc类型,然后调用ServeMux.Handle()方法注册。注意这里的HandlerFunc(handler)是类型转换,而非函数调用,类型HandlerFunc的定义如下

// 此处的作用是适配器,用于将用户传入的函数适配为实现了Handler接口的实现类 
type HandlerFunc func(ResponseWriter, *Request)

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

HandlerFunc实际上是以函数类型func(ResponseWriter, *Request)为底层类型,为HandlerFunc类型定义了方法ServeHTTP。是的,Go 语言允许为(基于)函数的类型定义方法。

下面会调用请求多路复用器的Handle方法,完成路由注册,也就是建立请求路径及其处理器的映射关系:

func (mux *ServeMux) Handle(pattern string, handler Handler) {
	mux.mu.Lock()
	defer mux.mu.Unlock()
	...
	// 判断是否存在重复映射问题
	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
	}
}

服务启动

通过调用ListenAndServe函数,完成端口绑定与监听,以及开启循环接收客户端连接,并查询映射集合,获取对应的处理器来处理请求:

func ListenAndServe(addr string, handler Handler) error {
    // handler不传则为nil,后续会采用默认的请求多路复用器
	server := &Server{Addr: addr, Handler: handler}
	return server.ListenAndServe()
}

下面调用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的serve方法中会不断处理客户端连接及后续读写事件:

func (srv *Server) Serve(l net.Listener) error {
    ...
	ctx := context.WithValue(baseCtx, ServerContextKey, srv)
	for {
	    // 同步阻塞接收客户端链接
		rw, err := l.Accept() 
		...
		// 此处返回的是net/http包下的conn结构体实现
		c := srv.newConn(rw)
		c.setState(c.rwc, StateNew, runHooks) // before Serve can return
		// 每个连接交给单独的协程进行处理
		go c.serve(connCtx)
	}
}

请求处理

上文讲到,go http库会为每个客户端连接单独分配一个协程,用于处理该连接上后续的数据读写请求,具体实现在net/http包下conn结构体的serve函数中:

func (c *conn) serve(ctx context.Context) {
	...
	// 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 {
		// 按照http协议格式解析tcp报文为http request对象,这里的conn是net/http包下提供的
		w, err := c.readRequest(ctx)
		...
		// 调用serverHandler的ServeHttp方法处理请求
		serverHandler{c.server}.ServeHTTP(w, w.req)
		inFlightResponse = nil
		w.cancelCtx()
		// 如果http请求被劫持了,此处直接返回
		if c.hijacked() {
			return
		}
	    ...
	}
}

serverHandler对server对象进行了一层包装,主要是处理handler为nil时,为handler设置默认的请求多路复用器:

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
	handler := sh.srv.Handler
	if handler == nil {
		handler = DefaultServeMux
	}
	...
	// 将请求处理转发给handler来处理
	handler.ServeHTTP(rw, req)
}

此处我们看一下默认的请求多路复用器的ServeHttp函数实现逻辑:

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

// ServeMux实现了ServeHttp接口,因此也实现了Handler接口
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
	...
	// 查询请求映射集合,寻找当前请求对应的处理器
	h, _ := mux.Handler(r)
	// 调用HandlerFunc的ServerHttp方法处理请求
	h.ServeHTTP(w, r)
}

路由匹配

ServeMux的Handler方法中,包含了路由匹配到核心逻辑,如下所示:

func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
	// CONNECT requests are not canonicalized.
	// 单独处理CONNECT请求
	if r.Method == "CONNECT" {
		// redirectToPathSlash函数处理逻辑就是: 
		// 0. 假设当前请求路径URI为/tree
		// 1. 如果ServeMux的请求映射集合m中包含/tree -- 精确匹配 , 那么不需要重定向
		// 2. 如果不包含/tree,但是包含/tree/ , 则创建一个新的URI , 路径为/tree/
		if u, ok := mux.redirectToPathSlash(r.URL.Host, r.URL.Path, r.URL); ok {
		    // 如果需要重定向,则将/tree请求永久重定向到/tree/
			return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
		}
        // 如果不需要重定向,则调用请求复用器的handler方法匹配具体的Handler实现
		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路径
		_, pattern = mux.handler(host, path)
		// 将当前请求r.URL.RawQuery重定向到到此路径path
		u := &url.URL{Path: path, RawQuery: r.URL.RawQuery}
		return RedirectHandler(u.String(), StatusMovedPermanently), pattern
	}
    // 如果不需要重定向,则调用请求复用器的handler方法匹配具体的Handler实现
	return mux.handler(host, r.URL.Path)
}

ServeMux的handler方法中先执行请求路径的精确匹配,再执行请求路径的最长前缀匹配:

func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
	mux.mu.RLock()
	defer mux.mu.RUnlock()
	// host+path去匹配
	if mux.hosts {
		h, pattern = mux.match(host + path)
	}
	// 只带path去匹配
	if h == nil {
		h, pattern = mux.match(path)
	}
	// 没有找到handler
	if h == nil {
		h, pattern = NotFoundHandler(), ""
	}
	return
}


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
		}
	}
	
	// 如果都没匹配上,则返回nil
	return nil, ""
}

服务隔离

调用http.HandleFunc()/http.Handle()都是将处理器/函数注册到ServeMux的默认对象DefaultServeMux上。使用默认对象有一个问题:不可控

一来Server参数都使用了默认值,二来第三方库也可能使用这个默认对象注册一些处理,容易冲突。更严重的是,我们在不知情中调用http.ListenAndServe()开启 Web 服务,那么第三方库注册的处理逻辑就可以通过网络访问到,有极大的安全隐患。所以,除非在示例程序中,否则建议不要使用默认对象。

我们可以使用http.NewServeMux()创建一个新的ServeMux对象,然后创建http.Server对象定制参数,用ServeMux对象初始化ServerHandler字段,最后调用Server.ListenAndServe()方法开启 Web 服务:

func main() {
  mux := http.NewServeMux()
  mux.HandleFunc("/", index)
  mux.Handle("/greeting", greeting("Welcome to go web frameworks"))

  server := &http.Server{
    Addr:         ":8080",
    Handler:      mux,
    ReadTimeout:  20 * time.Second,
    WriteTimeout: 20 * time.Second,
  }
  server.ListenAndServe()
}

这个程序与上面的Hello World功能基本相同,我们还额外设置了读写超时。

为了便于理解,我画了两幅图,其实整理下来整个流程也不复杂:
在这里插入图片描述


中间件

有时候需要在请求处理代码中增加一些通用的逻辑,如统计处理耗时、记录日志、捕获宕机等等。如果在每个请求处理函数中添加这些逻辑,代码很快就会变得不可维护,添加新的处理函数也会变得非常繁琐。所以就有了中间件的需求。

中间件有点像面向切面的编程思想,但是与 Java 语言不同。在 Java 中,通用的处理逻辑(也可以称为切面)可以通过反射插入到正常逻辑的处理流程中,在 Go 语言中基本不这样做。

在 Go 中,中间件是通过函数闭包来实现的。Go 语言中的函数是第一类值,既可以作为参数传给其他函数,也可以作为返回值从其他函数返回。我们前面介绍了处理器/函数的使用和实现。那么可以利用闭包封装已有的处理函数。

首先,基于函数类型func(http.Handler) http.Handler定义一个中间件类型:

type Middleware func(http.Handler) http.Handler

接下来我们来编写中间件,最简单的中间件就是在请求前后各输出一条日志:

// golang里面典型的装饰器模式实现
func WithLogger(handler http.Handler) http.Handler {
  // 此处是将匿名函数转换为了HandlerFunc类型,而不是函数嵌套
  // HandlerFunc类型本身也实现了Handler接口,不要忘记
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    logger.Printf("path:%s process start...\n", r.URL.Path)
    defer func() {
      logger.Printf("path:%s process end...\n", r.URL.Path)
    }()
    // 调用目标handler的ServeHttp方法处理
    handler.ServeHTTP(w, r)
  })
}

实现很简单,通过中间件封装原来的处理器对象,然后返回一个新的处理函数。在新的处理函数中,先输出开始处理的日志,然后用defer语句在函数结束后输出处理结束的日志。接着调用原处理器对象的ServeHTTP()方法执行原处理逻辑。

类似地,我们再来实现一个统计处理耗时的中间件:

func Metric(handler http.Handler) http.HandlerFunc {
  return func (w http.ResponseWriter, r *http.Request) {
    start := time.Now()
    defer func() {
      logger.Printf("path:%s elapsed:%fs\n", r.URL.Path, time.Since(start).Seconds())
    }()
    time.Sleep(1 * time.Second)
    handler.ServeHTTP(w, r)
  }
}

Metric中间件封装原处理器对象,开始执行前记录时间,执行完成后输出耗时。为了能方便看到结果,我在上面代码中添加了一个time.Sleep()调用。

最后,由于请求的处理逻辑都是由功能开发人员(而非库作者)自己编写的,所以为了 Web 服务器的稳定,我们需要捕获可能出现的 panic。PanicRecover中间件如下:

func PanicRecover(handler http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    defer func() {
      if err := recover(); err != nil {
        logger.Println(string(debug.Stack()))
      }
    }()

    handler.ServeHTTP(w, r)
  })
}

调用recover()函数捕获 panic,输出堆栈信息,为了防止程序异常退出。实际上,在conn.serve()方法中也有recover(),程序一般不会异常退出。但是自定义的中间件可以添加我们自己的定制逻辑。

现在我们可以这样来注册处理函数:

mux.Handle("/", PanicRecover(WithLogger(Metric(http.HandlerFunc(index)))))
mux.Handle("/greeting", PanicRecover(WithLogger(Metric(greeting("welcome, dj")))))

这种方式略显繁琐,我们可以编写一个帮助函数,它接受原始的处理器对象,和可变的多个中间件。对处理器对象应用这些中间件,返回新的处理器对象:

func applyMiddlewares(handler http.Handler, middlewares ...Middleware) http.Handler {
  for i := len(middlewares)-1; i >= 0; i-- {
    handler = middlewares[i](handler)
  }

  return handler
}

注意应用顺序是从右到左的,即右结合,越靠近原处理器的越晚执行。

利用帮助函数,注册可以简化为:

middlewares := []Middleware{
  PanicRecover,
  WithLogger,
  Metric,
}

mux.Handle("/", applyMiddlewares(http.HandlerFunc(index), middlewares...))
mux.Handle("/greeting", applyMiddlewares(greeting("welcome, dj"), middlewares...))

上面每次注册处理逻辑都需要调用一次applyMiddlewares()函数,还是略显繁琐。我们可以这样来优化,封装一个自己的ServeMux结构,然后定义一个方法Use()将中间件保存下来,重写Handle/HandleFunc将传入的http.HandlerFunc/http.Handler处理器包装中间件之后再传给底层的ServeMux.Handle()方法:

type MyMux struct {
  *http.ServeMux
  middlewares []Middleware
}

func NewMyMux() *MyMux {
  return &MyMux{
    ServeMux: http.NewServeMux(),
  }
}

func (m *MyMux) Use(middlewares ...Middleware) {
  m.middlewares = append(m.middlewares, middlewares...)
}

func (m *MyMux) Handle(pattern string, handler http.Handler) {
  handler = applyMiddlewares(handler, m.middlewares...)
  m.ServeMux.Handle(pattern, handler)
}

func (m *MyMux) HandleFunc(pattern string, handler http.HandlerFunc) {
  newHandler := applyMiddlewares(handler, m.middlewares...)
  m.ServeMux.Handle(pattern, newHandler)
}

注册时只需要创建MyMux对象,调用其Use()方法传入要应用的中间件即可:

middlewares := []Middleware{
  PanicRecover,
  WithLogger,
  Metric,
}

mux := NewMyMux()
mux.Use(middlewares...)
mux.HandleFunc("/", index)
mux.Handle("/greeting", greeting("welcome, dj"))

这种方式简单易用,但是也有它的问题,最大的问题是必须先设置好中间件,然后才能调用Handle/HandleFunc注册,后添加的中间件不会对之前注册的处理器/函数生效。

为了解决这个问题,我们可以改写ServeHTTP方法,在确定了处理器之后再应用中间件。这样后续添加的中间件也能生效。很多第三方库都是采用这种方式。http.ServeMux默认的ServeHTTP()方法如下:

func (m *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  if r.RequestURI == "*" {
    if r.ProtoAtLeast(1, 1) {
      w.Header().Set("Connection", "close")
    }
    w.WriteHeader(http.StatusBadRequest)
    return
  }
  h, _ := m.Handler(r)
  h.ServeHTTP(w, r)
}

改造这个方法定义MyMux类型的ServeHTTP()方法也很简单,只需要在m.Handler®获取处理器之后,应用当前的中间件即可:

func (m *MyMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  // ...
  h, _ := m.Handler(r)
  // 只需要加这一行即可
  h = applyMiddlewares(h, m.middlewares...)
  h.ServeHTTP(w, r)
}

后面我们分析其他 Web 框架的源码时会发现,很多都是类似的做法。为了测试宕机恢复,编写一个会触发 panic 的处理函数:

func panics(w http.ResponseWriter, r *http.Request) {
  panic("not implemented")
}

mux.HandleFunc("/panic", panics)

运行,在浏览器中请求localhost:8080/和localhost:8080/greeting,最后请求localhost:8080/panic触发 panic:

在这里插入图片描述

在这里插入图片描述


参考

本文转载至: Go 每日一库之 net/http(基础和中间件) , 并在其基础上进行了优化,方便读者阅读和理解

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值