zoukankan      html  css  js  c++  java
  • 01 . Go之从零实现Web框架(框架雏形, 上下文Context,路由)

    设计一个框架

    大部分时候,我们需要实现一个 Web 应用,第一反应是应该使用哪个框架。不同的框架设计理念和提供的功能有很大的差别。比如 Python 语言的 djangoflask,前者大而全,后者小而美。Go语言/golang 也是如此,新框架层出不穷,比如BeegoGinIris等。那为什么不直接使用标准库,而必须使用框架呢?在设计一个框架之前,我们需要回答框架核心为我们解决了什么问题。只有理解了这一点,才能想明白我们需要在框架中实现什么功能。

    我们先看看标准库 net/http 如何处理一个请求

    package main
    
    import (
    	"fmt"
    	"log"
    	"net/http"
    )
    
    func main()  {
    	http.HandleFunc("/",handler)
    	//http.HandlerFunc("/count",counter)
    	log.Fatal(http.ListenAndServe("localhost:8000",nil))
    }
    
    func handler(w http.ResponseWriter, r *http.Request)  {
    	fmt.Fprintf(w,"URL.Path = %q
    ",r.URL.Path)
    	fmt.Println("1234")
    }
    

    net/http提供了基础的Web功能,即监听端口,映射静态路由,解析HTTP报文。一些Web开发中简单的需求并不支持,需要手工实现。

    • 动态路由:例如hello/:namehello/*这类的规则。
    • 鉴权:没有分组/统一鉴权的能力,需要在每个路由映射的handler中实现。
    • 模板:没有统一简化的HTML机制。

    当我们离开框架,使用基础库时,需要频繁手工处理的地方,就是框架的价值所在。但并不是每一个频繁处理的地方都适合在框架中完成。Python有一个很著名的Web框架,名叫bottle,整个框架由bottle.py一个文件构成,共4400行,可以说是一个微框架。那么理解这个微框架提供的特性,可以帮助我们理解框架的核心能力。

    • 路由(Routing):将请求映射到函数,支持动态路由。例如'/hello/:name
    • 模板(Templates):使用内置模板引擎提供模板渲染机制。
    • 工具集(Utilites):提供对 cookies,headers 等处理机制。
    • 插件(Plugin):Bottle本身功能有限,但提供了插件机制。可以选择安装到全局,也可以只针对某几个路由生效。

    Gee框架

    这个教程将使用 Go 语言实现一个简单的 Web 框架,起名叫做Geegeektutu.com的前三个字母。我第一次接触的 Go 语言的 Web 框架是GinGin的代码总共是14K,其中测试代码9K,也就是说实际代码量只有5KGin也是我非常喜欢的一个框架,与Python中的Flask很像,小而美。

    7天实现Gee框架这个教程的很多设计,包括源码,参考了Gin,大家可以看到很多Gin的影子。
    时间关系,同时为了尽可能地简洁明了,这个框架中的很多部分实现的功能都很简单,但是尽可能地体现一个框架核心的设计原则。例如Router的设计,虽然支持的动态路由规则有限,但为了性能考虑匹配算法是用Trie树实现的,Router最重要的指标之一便是性能。

    标准库启动Web服务

    Go语言内置了 net/http库,封装了HTTP网络编程的基础的接口,我们实现的Gee Web 框架便是基于net/http的。我们接下来通过一个例子,简单介绍下这个库的使用。

    package main
    
    import (
    	"fmt"
    	"log"
    	"net/http"
    )
    
    func main()  {
    	http.HandleFunc("/",indexHandler)
    	http.HandleFunc("/hello",helloHandler)
    	log.Fatal(http.ListenAndServe(":8000",nil))
    }
    
    // handler echoes r.URL.Path
    func indexHandler(w http.ResponseWriter, req *http.Request)  {
    	fmt.Fprintf(w,"URL.Path = %q
    ",req.URL.Path)
    }
    
    func helloHandler(w http.ResponseWriter, req *http.Request)  {
    	for k,v := range req.Header {
    		fmt.Fprintf(w, "Header[%q] = %q
    ",k,v)
    	}
    }
    

    我们设置了2个路由,//hello,分别绑定 indexHandlerhelloHandler , 根据不同的HTTP请求会调用不同的处理函数。访问/,响应是URL.Path = /,而/hello的响应则是请求头(header)中的键值对信息。

    用 curl 这个工具测试一下,将会得到如下的结果

    $ curl localhost:8000/
    URL.Path = "/"
    
    $ curl localhost:8000/hello
    Header["User-Agent"] = ["curl/7.64.1"]
    Header["Accept"] = ["*/*"]
    

    main 函数的最后一行,是用来启动 Web 服务的,第一个参数是地址,:8000表示在 8000 端口监听。而第二个参数则代表处理所有的HTTP请求的实例,nil 代表使用标准库中的实例处理。第二个参数,则是我们基于net/http标准库实现Web框架的入口。

    实现http.Handler接口

    package http
    
    type Handler interface {
    	ServeHTPP(w ResponseWriter, r *Request)
    }
    
    func ListenAndServe(address string, h Handler) error  {
    	
    }
    
    

    第二个参数的类型是什么呢?通过查看net/http的源码可以发现,Handler是一个接口,需要实现方法 ServeHTTP ,也就是说,只要传入任何实现了 ServerHTTP 接口的实例,所有的HTTP请求,就都交给了该实例处理了。

    main.go
    **

    package main
    
    import (
    	"fmt"
    	"log"
    	"net/http"
    )
    
    type Engine struct{}
    
    func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    	switch req.URL.Path {
    	case "/":
    		fmt.Fprintf(w, "URL.Path = %q
    ", req.URL.Path)
    	case "/hello":
    		for k, v := range req.Header {
    			fmt.Fprintf(w, "Header[%q] = %q
    ", k, v)
    		}
    	default:
    		fmt.Fprintf(w, "404 NOT FOUND: %s
    ", req.URL)
    	}
    }
    
    func main() {
    	engine := new(Engine)
    	log.Fatal(http.ListenAndServe(":8000", engine))
    }
    
    • 我们定义了一个空的结构体Engine,实现了方法ServeHTTP。这个方法有2个参数,第二个参数是 Request ,该对象包含了该HTTP请求的所有的信息,比如请求地址、Header和Body等信息;第一个参数是 ResponseWriter ,利用 ResponseWriter 可以构造针对该请求的响应。

    • main 函数中,我们给 ListenAndServe 方法的第二个参数传入了刚才创建的engine实例。至此,我们走出了实现Web框架的第一步,即,将所有的HTTP请求转向了我们自己的处理逻辑。还记得吗,在实现Engine之前,我们调用 http.HandleFunc 实现了路由和Handler的映射,也就是只能针对具体的路由写处理逻辑。比如/hello。但是在实现Engine之后,我们拦截了所有的HTTP请求,拥有了统一的控制入口。在这里我们可以自由定义路由映射的规则,也可以统一添加一些处理逻辑,例如日志、异常处理等。

    • 代码的运行结果与之前的是一致的。

    Gee框架的雏形

    代码结构

    tree gee_demo1 
    gee_demo1
    ├── gee
    │   ├── gee.go
    │   └── go.mod
    ├── go.mod
    └── main.go
    
    1 directory, 4 files
    

    go.mod

    module gee_demo1
    
    go 1.13
    
    require gee v0.0.0
    
    replace gee => ./gee
    
    • go.mod 中使用 replace 将 gee 指向 ./gee

    从 go 1.11 版本开始,引用相对路径的 package 需要使用上述方式。

    main.go

    package main
    
    import (
    	"fmt"
    	"gee"
    	"net/http"
    )
    
    func main()  {
    	r := gee.New()
    	r.GET("/", func(w http.ResponseWriter, req *http.Request) {
    		fmt.Fprintf(w, "URL.Path = %q
    ", req.URL.Path)
    	})
    
    	r.GET("/hello", func(w http.ResponseWriter, req *http.Request) {
    		for k, v := range req.Header {
    			fmt.Fprintf(w, "Header[%q] = %q
    ", k, v)
    		}
    	})
    
    	r.Run(":8000")
    }
    

    看到这里,如果你使用过gin框架的话,肯定会觉得无比的亲切。gee框架的设计以及API均参考了gin。使用New()创建 gee 的实例,使用 GET()方法添加路由,最后使用Run()启动Web服务。这里的路由,只是静态路由,不支持/hello/:name这样的动态路由,动态路由我们将在下一次实现。

    gee.go

    package gee
    
    import (
    	"fmt"
    	"log"
    	"net/http"
    )
    
    // HandlerFunc defines the request handler used by gee
    type HandlerFunc func(http.ResponseWriter, *http.Request)
    
    // Engine implement the interface of ServeHTTP
    type Engine struct {
    	router map[string]HandlerFunc
    }
    
    // New is the constructor of gee.Engine
    func New() *Engine {
    	return &Engine{router: make(map[string]HandlerFunc)}
    }
    
    func (engine *Engine) addRoute(method string, pattern string, handler HandlerFunc) {
    	key := method + "-" + pattern
    	log.Printf("Route %4s - %s", method, pattern)
    	engine.router[key] = handler
    }
    
    // GET defines the method to add GET request
    func (engine *Engine) GET(pattern string, handler HandlerFunc) {
    	engine.addRoute("GET", pattern, handler)
    }
    
    // POST defines the method to add POST request
    func (engine *Engine) POST(pattern string, handler HandlerFunc) {
    	engine.addRoute("POST", pattern, handler)
    }
    
    // Run defines the method to start a http server
    func (engine *Engine) Run(addr string) (err error) {
    	return http.ListenAndServe(addr, engine)
    }
    
    func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    	key := req.Method + "-" + req.URL.Path
    	if handler, ok := engine.router[key]; ok {
    		handler(w, req)
    	} else {
    		fmt.Fprintf(w, "404 NOT FOUND: %s
    ", req.URL)
    	}
    }
    

    那么gee.go就是重头戏了。我们重点介绍一下这部分的实现。

    • 首先定义了类型HandlerFunc,这是提供给框架用户的,用来定义路由映射的处理方法。我们在Engine中,添加了一张路由映射表router,key 由请求方法和静态路由地址构成,例如GET-/GET-/helloPOST-/hello,这样针对相同的路由,如果请求方法不同,可以映射不同的处理方法(Handler),value 是用户映射的处理方法。

    • 当用户调用(*Engine).GET()方法时,会将路由和处理方法注册到映射表 router 中,(*Engine).Run()方法,是 ListenAndServe 的包装。

    • Engine实现的 ServeHTTP 方法的作用就是,解析请求的路径,查找路由映射表,如果查到,就执行注册的处理方法。如果查不到,就返回 404 NOT FOUND

    执行go run main.go,再用 curl 工具访问,结果与最开始的一致

    测试

    $ curl localhost:8000
    URL.Path = "/"
    
    $ curl localhost:8000/hello
    Header["Accept"] = ["*/*"]
    Header["User-Agent"] = ["curl/7.64.1"]
    

    至此,整个Gee框架的原型已经出来了。实现了路由映射表,提供了用户注册静态路由的方法,包装了启动服务的函数。当然,到目前为止,我们还没有实现比net/http标准库更强大的能力,不用担心,很快就可以将动态路由、中间件等功能添加上去了。

    上下文Context

    • 路由(router)独立出来,方便之后增强。
    • 设计上下文(Context),封装 Request 和 Response ,提供对 JSON、HTML 等返回类型的支持.

    使用效果

    package main
    
    import (
    	"fmt"
    	"gee"
    	"net/http"
    )
    
    func main() {
    	r := gee.New()
    	r.GET("/", func(c *gee.Context) {
    		c.HTML(http.StatusOK, "<h1>Hello Gee</h1>")
    	})
    	r.GET("/hello", func(c *gee.Context) {
    		// expect /hello?name=geektutu
    		c.String(http.StatusOK, "hello %s, you're at %s
    ", c.Query("name"), c.Path)
    	})
    
    	r.POST("/login", func(c *gee.Context) {
    		c.JSON(http.StatusOK, gee.H{
    			"username": c.PostForm("username"),
    			"password": c.PostForm("password"),
    		})
    	})
    
    	r.Run(":9999")
    }
    
    • Handler的参数变成成了gee.Context,提供了查询Query/PostForm参数的功能。
    • gee.Context封装了HTML/String/JSON函数,能够快速构造HTTP响应。

    设计Context

    必要性

    1. 对Web服务来说,无非是根据请求*http.Request,构造响应http.ResponseWriter。但是这两个对象提供的接口粒度太细,比如我们要构造一个完整的响应,需要考虑消息头(Header)和消息体(Body),而 Header 包含了状态码(StatusCode),消息类型(ContentType)等几乎每次请求都需要设置的信息。因此,如果不进行有效的封装,那么框架的用户将需要写大量重复,繁杂的代码,而且容易出错。针对常用场景,能够高效地构造出 HTTP 响应是一个好的框架必须考虑的点。

    用返回JSON数据作比较, 感受下封装前后的差距

    封装前

    obj = map[string]interface{}{
        "name": "geektutu",
        "password": "1234",
    }
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    encoder := json.NewEncoder(w)
    if err := encoder.Encode(obj); err != nil {
        http.Error(w, err.Error(), 500)
    }
    

    封装后

    c.JSON(http.StatusOK, gee.H{
        "username": c.PostForm("username"),
        "password": c.PostForm("password"),
    })
    
    1. 针对使用场景,封装*http.Requesthttp.ResponseWriter的方法,简化相关接口的调用,只是设计 Context 的原因之一。对于框架来说,还需要支撑额外的功能。例如,将来解析动态路由/hello/:name,参数:name的值放在哪呢?再比如,框架需要支持中间件,那中间件产生的信息放在哪呢?Context 随着每一个请求的出现而产生,请求的结束而销毁,和当前请求强相关的信息都应由 Context 承载。因此,设计 Context 结构,扩展性和复杂性留在了内部,而对外简化了接口。路由的处理函数,以及将要实现的中间件,参数都统一使用 Context 实例, Context 就像一次会话的百宝箱,可以找到任何东西。

    Context具体实现

    gee/context.go

    type H map[string]interface{}
    
    type Context struct {
    	// origin objects
    	Writer http.ResponseWriter
    	Req    *http.Request
    	// request info
    	Path   string
    	Method string
    	// response info
    	StatusCode int
    }
    
    func newContext(w http.ResponseWriter, req *http.Request) *Context {
    	return &Context{
    		Writer: w,
    		Req:    req,
    		Path:   req.URL.Path,
    		Method: req.Method,
    	}
    }
    
    func (c *Context) PostForm(key string) string {
    	return c.Req.FormValue(key)
    }
    
    func (c *Context) Query(key string) string {
    	return c.Req.URL.Query().Get(key)
    }
    
    func (c *Context) Status(code int) {
    	c.StatusCode = code
    	c.Writer.WriteHeader(code)
    }
    
    func (c *Context) SetHeader(key string, value string) {
    	c.Writer.Header().Set(key, value)
    }
    
    func (c *Context) String(code int, format string, values ...interface{}) {
    	c.SetHeader("Content-Type", "text/plain")
    	c.Status(code)
    	c.Writer.Write([]byte(fmt.Sprintf(format, values...)))
    }
    
    func (c *Context) JSON(code int, obj interface{}) {
    	c.SetHeader("Content-Type", "application/json")
    	c.Status(code)
    	encoder := json.NewEncoder(c.Writer)
    	if err := encoder.Encode(obj); err != nil {
    		http.Error(c.Writer, err.Error(), 500)
    	}
    }
    
    func (c *Context) Data(code int, data []byte) {
    	c.Status(code)
    	c.Writer.Write(data)
    }
    
    func (c *Context) HTML(code int, html string) {
    	c.SetHeader("Content-Type", "text/html")
    	c.Status(code)
    	c.Writer.Write([]byte(html))
    }
    
    • 代码最开头,给map[string]interface{}起了一个别名gee.H,构建JSON数据时,显得更简洁。
    • Context目前只包含了http.ResponseWriter*http.Request,另外提供了对 Method 和 Path 这两个常用属性的直接访问。
    • 提供了访问Query和PostForm参数的方法。
    • 提供了快速构造String/Data/JSON/HTML响应的方法。

    路由 (Router)

    我们将和路由相关的方法和结构提取了出来,放到了一个新的文件中router.go,方便我们下一次对 router 的功能进行增强,例如提供动态路由的支持。 router 的 handle 方法作了一个细微的调整,即 handler 的参数,变成了 Context。

    gee/router.go

    type router struct {
    	handlers map[string]HandlerFunc
    }
    
    func newRouter() *router {
    	return &router{handlers: make(map[string]HandlerFunc)}
    }
    
    func (r *router) addRoute(method string, pattern string, handler HandlerFunc) {
    	log.Printf("Route %4s - %s", method, pattern)
    	key := method + "-" + pattern
    	r.handlers[key] = handler
    }
    
    func (r *router) handle(c *Context) {
    	key := c.Method + "-" + c.Path
    	if handler, ok := r.handlers[key]; ok {
    		handler(c)
    	} else {
    		c.String(http.StatusNotFound, "404 NOT FOUND: %s
    ", c.Path)
    	}
    }
    

    框架入口

    gee/gee.go

    // HandlerFunc defines the request handler used by gee
    type HandlerFunc func(*Context)
    
    // Engine implement the interface of ServeHTTP
    type Engine struct {
    	router *router
    }
    
    // New is the constructor of gee.Engine
    func New() *Engine {
    	return &Engine{router: newRouter()}
    }
    
    func (engine *Engine) addRoute(method string, pattern string, handler HandlerFunc) {
    	engine.router.addRoute(method, pattern, handler)
    }
    
    // GET defines the method to add GET request
    func (engine *Engine) GET(pattern string, handler HandlerFunc) {
    	engine.addRoute("GET", pattern, handler)
    }
    
    // POST defines the method to add POST request
    func (engine *Engine) POST(pattern string, handler HandlerFunc) {
    	engine.addRoute("POST", pattern, handler)
    }
    
    // Run defines the method to start a http server
    func (engine *Engine) Run(addr string) (err error) {
    	return http.ListenAndServe(addr, engine)
    }
    
    func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    	c := newContext(w, req)
    	engine.router.handle(c)
    }
    

    router相关的代码独立后,gee.go简单了不少。最重要的还是通过实现了 ServeHTTP 接口,接管了所有的 HTTP 请求。相比第一天的代码,这个方法也有细微的调整,在调用 router.handle 之前,构造了一个 Context 对象。这个对象目前还非常简单,仅仅是包装了原来的两个参数,之后我们会慢慢地给Context插上翅膀。
    如何使用,main.go一开始就已经亮相了。运行go run main.go,借助 curl ,一起看一看今天的成果吧。

    curl -i http://localhost:9999/
    HTTP/1.1 200 OK
    Date: Mon, 12 Aug 2019 16:52:52 GMT
    Content-Length: 18
    Content-Type: text/html; charset=utf-8
    <h1>Hello Gee</h1>
    
    $ curl "http://localhost:9999/hello?name=geektutu"
    hello geektutu, you're at /hello
    
    $ curl "http://localhost:9999/login" -X POST -d 'username=geektutu&password=1234'
    {"password":"1234","username":"geektutu"}
    
    $ curl "http://localhost:9999/xxx"
    404 NOT FOUND: /xxx
    

    感谢大佬 geektutu.com 分享

  • 相关阅读:
    再说Play!framework http://hsfgo.iteye.com/blog/806974
    PlayFramework 1 自定义标签 -- FastTags http://unmi.cc/category/javajee/playframework/
    [译] 第三十天:Play Framework
    你可以使用 play framework 做5件很爽的事情http://www.anool.net/?p=629
    Play常用代码片段 http://www.anool.net/?p=625
    Play 内置模板标签(1.2.3版本)http://www.anool.net/?p=617
    for what? while 与 until 差在哪?-- Shell十三问<第十三问>
    你要 if 还是 case 呢?-- Shell十三问<第十二问>
    > 与 < 差在哪?-- Shell十三问<第十一问>
    && 与 || 差在哪?-- Shell十三问<第十问>
  • 原文地址:https://www.cnblogs.com/you-men/p/14223736.html
Copyright © 2011-2022 走看看