zoukankan      html  css  js  c++  java
  • 仿照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框架系列代码地址

  • 相关阅读:
    java微信扫码支付(模式二)
    Python学习08
    学习java第12天
    学习java第11天
    学习java第十天
    学习Java第九天
    学习Java第八天
    学习Java第六天
    学习Java第六天
    学习Java第五天
  • 原文地址:https://www.cnblogs.com/CharmCode/p/14359907.html
Copyright © 2011-2022 走看看