• 仿照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框架系列代码地址

  • 相关阅读:
    组装query,query汇总,query字段
    POJ 1276, Cash Machine
    POJ 1129, Channel Allocation
    POJ 2531, Network Saboteur
    POJ 1837, Balance
    POJ 3278, Catch That Cow
    POJ 2676, Sudoku
    POJ 3126, Prime Path
    POJ 3414, Pots
    POJ 1426, Find The Multiple
  • 原文地址:https://www.cnblogs.com/CharmCode/p/14359907.html
Copyright © 2020-2023  润新知