• 从零实现Web框架Geo教程-Http基础-01



    本教程参考:七天用Go从零实现Web框架Gee教程


    前言

    要开发一个Web应用,我们首先会考虑框架的选型,对于Javaer来说,通常Spring mvc是Web开发的首选框架;

    如果使用GoLang进行开发,可能会选择Beego,Gin,Iris等,但是无论是哪一种Web框架其设计思想都是类似的。

    在我初学GoLang的net/http标准库时,我觉得标准库提供的功能已经帮助我们封装好了很多细节,例如: http协议的解析,请求参数的提取等等…

    我们完全可以直接利用net/http标准库进行简单的web程序开发,那为什么还需要在标准库上再封装上一层皮呢?

    直接利用GoLang中的net/http标准库开发,就像直接使用Java中的servlet开发web应用一样,虽然可以,但是太多重复的关注点没有被抽取出来,会导致程序代码臃肿,难以维护。

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

    func main() {
        http.HandleFunc("/", handler)
        http.HandleFunc("/count", counter)
        log.Fatal(http.ListenAndServe("localhost:8000", nil))
    }
    
    func handler(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

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

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

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

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

    Geo 框架

    这个教程将使用 Go 语言实现一个简单的 Web 框架,起名叫做Geo。 该框架设计参考的是极客兔兔编写的Gee教程。

    废话不多说,咱们直接开始。


    HTTP基础

    标准库启动Web服务

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

    package main
    
    import (
    	"fmt"
    	"log"
    	"net/http"
    )
    
    func main() {
    	//注册静态路由
    	// “/“作为兜底处理器
    	http.HandleFunc("/", indexHandler)
    	//只拦截精确匹配到的/hello请求
    	http.HandleFunc("/hello", helloHandler)
    	log.Fatal(http.ListenAndServe(":9999", nil))
    }
    
    func indexHandler(w http.ResponseWriter, req *http.Request) {
    	fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path)
    }
    func helloHandler(w http.ResponseWriter, req *http.Request) {
    	for k, v := range req.Header {
    		fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

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

    用 curl 这个工具测试一下,将会得到如下的结果。
    在这里插入图片描述
    main 函数的最后一行,是用来启动 Web 服务的,第一个参数是地址,:9999表示在 9999 端口监听。而第二个参数则代表处理所有的HTTP请求的实例,nil 代表使用标准库中的实例处理。第二个参数,则是我们基于net/http标准库实现Web框架的入口。


    实现http.Handler接口

    package http
    
    type Handler interface {
        ServeHTTP(w ResponseWriter, r *Request)
    }
    
    func ListenAndServe(address string, h Handler) error
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    第二个参数的类型是什么呢?

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

    这是不是非常类似于Spring mvc对servlet进行的封装,servlet原生开发是一个servlet处理一个请求,而spring对此的处理则是,提供了一个统一入口DispathcerServlet入口,所有请求通通被该servlet拦截,然后执行后续请求派发操作。

    马上来试一试吧。

    package main
    
    import (
    	"fmt"
    	"log"
    	"net/http"
    )
    
    // Engine is the uni handler for all requests
    //可以类比spring mvc中的DispathcerServlet
    type Engine struct{}
    
    //类比spring mvc中DispathcerServlet提供的入口方法Dispatch
    func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    	switch req.URL.Path {
    	case "/":
    		fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path)
    	case "/hello":
    		for k, v := range req.Header {
    			fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
    		}
    	default:
    		fmt.Fprintf(w, "404 NOT FOUND: %s\n", req.URL)
    	}
    }
    
    func main() {
    	engine := new(Engine)
    	log.Fatal(http.ListenAndServe(":9999", engine))
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 我们定义了一个空的结构体Engine,实现了方法ServeHTTP。这个方法有2个参数,第二个参数是 Request ,该对象包含了该HTTP请求的所有的信息,比如请求地址、Header和Body等信息;第一个参数是 ResponseWriter ,利用 ResponseWriter 可以构造针对该请求的响应。
    • 在 main 函数中,我们给 ListenAndServe 方法的第二个参数传入了刚才创建的engine实例。至此,我们走出了实现Web框架的第一步,即,将所有的HTTP请求转向了我们自己的处理逻辑。还记得吗,在实现Engine之前,我们调用 http.HandleFunc 实现了路由和Handler的映射,也就是只能针对具体的路由写处理逻辑。比如/hello。但是在实现Engine之后,我们拦截了所有的HTTP请求,拥有了统一的控制入口。在这里我们可以自由定义路由映射的规则,也可以统一添加一些处理逻辑,例如日志、异常处理等。

    Gee框架的雏形

    我们接下来重新组织上面的代码,搭建出整个框架的雏形。

    最终的代码目录结构是这样的。

    geo/
      |--geo.go
      |--go.mod
    main.go
    go.mod
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • go.mod
    module main
    
    go 1.18
    
    require geo v0.0.0
    replace geo => ./geo
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 在 go.mod 中使用 replace 将 gee 指向 ./gee

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


    • main.go
    package main
    
    import (
    	"fmt"
    	"net/http"
        //导入自定义包
    	"geo"
    )
    
    func main() {
    	r := geo.New()
    	//注册路由
    	r.GET("/", func(w http.ResponseWriter, req *http.Request) {
    		fmt.Fprintf(w, "URL.Path = %q\n", 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\n", k, v)
    		}
    	})
        
    	r.Run(":9999")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

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

    • geo.go
    package geo
    
    import (
    	"fmt"
    	"net/http"
    )
    
    type HandlerFunc func(http.ResponseWriter, *http.Request)
    
    type Engine struct {
    	//路由表
    	router map[string]HandlerFunc
    }
    
    func New() *Engine {
    	return &Engine{router: make(map[string]HandlerFunc)}
    }
    
    func (engine *Engine) addRoute(method string, pattern string, handler HandlerFunc) {
    	key := method + "-" + pattern
    	engine.router[key] = handler
    }
    
    func (engine *Engine) GET(pattern string, handler HandlerFunc) {
    	engine.addRoute("GET", pattern, handler)
    }
    
    func (engine *Engine) POST(pattern string, handler HandlerFunc) {
    	engine.addRoute("POST", pattern, handler)
    }
    
    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 {
    	    w.WriteHeader(http.StatusNotFound)
    		fmt.Fprintf(w, "404 NOT FOUND: %s\n", req.URL)
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

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

    • 首先定义了类型HandlerFunc,这是提供给框架用户的,用来定义路由映射的处理方法。我们在Engine中,添加了一张路由映射表router,key 由请求方法和静态路由地址构成,例如GET-/、GET-/hello、POST-/hello,这样针对相同的路由,如果请求方法不同,可以映射不同的处理方法(Handler),value 是用户映射的处理方法。
    • 当用户调用(*Engine).GET()方法时,会将路由和处理方法注册到映射表 router 中,(*Engine).Run()方法,是 ListenAndServe 的包装。
    • Engine实现的 ServeHTTP 方法的作用就是,解析请求的路径,查找路由映射表,如果查到,就执行注册的处理方法。如果查不到,就返回 404 NOT FOUND 。

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

    $ curl http://localhost:9999/
    URL.Path = "/"
    $ curl http://localhost:9999/hello
    Header["Accept"] = ["*/*"]
    Header["User-Agent"] = ["curl/7.54.0"]
    curl http://localhost:9999/world
    404 NOT FOUND: /world
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

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


  • 相关阅读:
    自动化测试selenium(一)
    jq扩展机制
    快速上手kettle(三)壶中可以放些啥?
    【ArcGIS Pro二次开发】(64):多分式标注
    《花雕学AI》13:早出对策,积极应对ChatGPT带来的一系列风险和挑战
    【电源专题】案例:电源芯片规格书大标题写5A那是能输出5A吗?
    36、Java——吃货联盟订餐系统(JDBC+MySQL+Apache DBUtils)
    让AI玩《我的世界》
    css盒子模型(框模型)属性
    Centos安装docker以及mysql和redis环境
  • 原文地址:https://blog.csdn.net/m0_53157173/article/details/126547870