仿照Go web框架gin手写自己的web框架 【上】

主要目的是学习Go web服务器的构成原理,方便工作开发。

本文内容主要是参考了

学习目标,构建一个类似gin的框架 gee,当然学习的话,只用包含最简单的几个核心功能就可以了,比如路由分组,中间件,异常恢复等。

最终构成的代码结构如下:

gee/            // 自定义gee框架
  |-- gee.go    // 核心文件 封装net/http
  |-- xxx.go    // 其他扩展文件
  |-- ...
  
main.go         // 用户web服务代码 引入gee框架
go.mod

Go标准库 net/http

package main

import (
	"fmt"
	"net/http"
)

func main() {

	// 设置路由 以及 请求处理函数
	http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
		_, _ = writer.Write([]byte("hello world"))
	})
        // 设置服务地址
	server := http.Server{
		Addr: "127.0.0.1:7050",
	}
	// 启动监听服务
	_ = server.ListenAndServe()
}

以上是一个最简单的示例,性能也绝对不差,为什么我会肯定性能不差了?因为Go标准库 net/http 库已经把主要的部分封装的非常强悍了。现有的大部分框架(如gin)都是基于标准库,然后自己封装一层wrapper,常见的功能有
路由分组,中间件,异常机制等核心功能。

可以从server.ListenAndServe()看到核心服务函数
https://github.com/golang/go/blob/e491c6eea9ad599a0ae766a3217bd9a16ca3a25a/src/net/http/server.go#L2951

func (srv *Server) Serve(l net.Listener) error {
	if fn := testHookServerServe; fn != nil {
		fn(srv, l) // call hook with unwrapped listener
	}

	origListener := l
	l = &onceCloseListener{Listener: l}
	defer l.Close()

	if err := srv.setupHTTP2_Serve(); err != nil {
		return err
	}

	if !srv.trackListener(&l, true) {
		return ErrServerClosed
	}
	defer srv.trackListener(&l, false)

	baseCtx := context.Background()
	if srv.BaseContext != nil {
		baseCtx = srv.BaseContext(origListener)
		if baseCtx == nil {
			panic("BaseContext returned a nil context")
		}
	}

	var tempDelay time.Duration // how long to sleep on accept failure

	ctx := context.WithValue(baseCtx, ServerContextKey, srv)
	// 最外层死循环
	for {
		// 监听listener的请求
		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) // before Serve can return
		go c.serve(connCtx)  // 每新进来一个连接,就开一个 goroutine 处理
	}
}

每个 goroutine 最小只需要2K的内存,这也是go并发性能的保证。
https://github.com/golang/go/blob/bbd25d26c0a86660fb3968137f16e74837b7a9c6/src/runtime/stack.go#L72

如果当前 HTTP 服务接收到了海量的请求,会在内部创建大量的 Goroutine,这可能会使整个服务质量明显降低无法处理请求。

但是有一个第三方库宣称比 net/http快10倍。fasthttp

关于`fasthttp`的简单介绍以及常见问题

对比测试设备以及结果
https://github.com/valyala/fasthttp/issues/4

为什么fasthttp 比 net/http 快10倍?
https://stackoverflow.com/questions/41627931/why-is-fasthttp-faster-than-net-http

基于 fasthttp,就诞生了 如 https://github.com/gofiber/fiber 的框架。

关于为什么 gin框架 不使用 fasthttp 替换标准库 net/http的问题?
https://github.com/gin-gonic/gin/issues/498

使用实例化Handler接口的方式构建web服务

上面一种方式,扩展方式不好,路由控制什么不好再次封装,于是可以采用以下方式扩展。

https://github.com/golang/go/blob/e491c6eea9ad599a0ae766a3217bd9a16ca3a25a/src/net/http/server.go#L86

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

func ListenAndServe(address string, h Handler) error

这种是实现 Handler接口 ServeHTTP(ResponseWriter, *Request) 的方式启动服务。

该接口只定义了一个方法,只要一个类型实现了这个 ServeHTTP 方法,
就可以把该类型的变量直接赋值给 Handler接口,以下demo就是基于这种方式实现。

Go中的接口,是隐式的实现,只要实现了接口的所有方法,就是实现了接口,不需要显示的实现,也就是常说的鸭子类型。

此片段GitHub地址

package main

import (
	"fmt"
	"net/http"
)

type Engine struct {
	router router
}

// 定义请求处理函数类型 
type handler func(w http.ResponseWriter, r *http.Request)

// 定义路由 - 对应 处理请求函数
type router map[string]handler

func (engine *Engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {

	// 只通过路径 匹配处理请求的方法 不区分 GET or POST
	// r.URL.Path
	if handler, ok := engine.router[r.URL.Path]; ok {
		handler(w, r)
	} else {
		_, _ = fmt.Fprintf(w, "404")
	}
}

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

func main() {

	r := router{}

	r["/"] = func(w http.ResponseWriter, r *http.Request) {
		_, _ = fmt.Fprintf(w, "首页")
	}

	r["/hello"] = hello

	//engine := new(Engine)
	engine := &Engine{
		router: r,
	}

	addr := "127.0.0.1:7051"
	fmt.Println("服务启动:", addr)

	_ = http.ListenAndServe(addr, engine)
}

学习总结

总而言之呢,就是Go net/http标准库已经很难强大了,自己只用封装一些wrapper就足够了,第二个实例化Handler接口方式例子 是后面模仿gin框架的基础,需要明白Go基础关于接口的知识,才好理解这个demo。

如果对接口不熟可以参考 https://draveness.me/golang/docs/part2-foundation/ch04-basic/golang-interface/

模仿ginweb框架系列代码地址

posted @ 2021-02-04 11:12  王小右  阅读(715)  评论(0编辑  收藏  举报