七天从零实现Web框架Gee - 1

  • 该框架是参考Gin框架写的Web框架

首先,在设计框架之前,需要知道为什么要使用框架,框架能解决什么问题,只有明白了这一点,才能设计出框架中的功能

  • net/heep简单的处理请求示例
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)
}

net/http提供了基础的Web功能,即监听端口,映射静态路由,http.ResponseWriter解析HTTP报文,http.Request对其内容进行解析。但还要一些Web开发中简单的需求并不支持,需要手工实现,例如

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

可以发现,当我们离开框架,使用基础库时,需要频繁手工处理的地方,就是框架的价值所在。

HTTP基础

标准库启动Web服务

func main() {
	http.HandleFunc("/", indexHandler)
	http.HandleFunc("/hello", helloHandler)
	log.Fatal(http.ListenAndServe(":9999", nil))
}

// handler echoes r.URL.Path
func indexHandler(w http.ResponseWriter, req *http.Request) {
	fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path)
}

// handler echoes r.URL.Header
func helloHandler(w http.ResponseWriter, req *http.Request) {
	for k, v := range req.Header {
		fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
	}
}

这里设置了2个路由,/和/hello,分配绑定indexHandler和helloHandler,根据不同的http请求会调用不同的处理函数。

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

接下来看第二个参数是如何使用的

首先我们定义了一个空的Engine结构体,他的作用是为所有请求做处理的handler,然后我们把前面所有的HandlerFunc通过绑定Engine对象进行管理

实现http.Handler接口

package http

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

func ListenAndServe(address string, h Handler) error

通过源码可以看到,Handler是一个接口类型,需要实现ServeHTTP方法,也就是说,只要传入任何实现了 ServerHTTP 接口的实例,所有的HTTP请求,就都交给了该实例处理了。所以我们要自己写一个实现该接口的实例,让所有的HTTP请求由自己来写

// Engine is the uni handler for all requests
type Engine struct{}

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)
	}
}

所以我们定义一个函数来实现ServerHTTP方法,这个方法有两个参数

  • 第一个参数是 ResponseWriter ,其实就是个网络套接字,通过w来写输入给客户端(C/S)
  • 第二个参数是 Request ,该对象包含了该HTTP请求的所有的信息,比如请求地址、Header和Body等信息

在该函数中根据请求的URL的path的不同做响应的操作

func main() {
	engine := new(Engine)
	log.Fatal(http.ListenAndServe(":8090", engine))
}

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

再接下来我们就要把刚才自己实现的代码抽离出来,搭建出整个框架的雏形

gee/
  |--gee.go
main.go

在gee.go中,我们首先来定义HandlerFunc函数类型,这是提供给框架用户的,用来定义路由映射的处理方法,定义Engine结构体,Engine结构体里存放router,其中以map字典存放处理函数HandlerFunc,并实现ServerHTTP方法,结构体里存放的路由表,由addRoute方法添加kv,GET和POST这两个方法只是对addRoute的包装而已

然后定义New方法,返回Engine对象,make一个HandlerFunc,定义addRouter方法用来添加router

// 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
	engine.router[key] = handler
}

接下来我们定义两个method方法GET和POST,当我们使用GET请求和POST请求时就会调用addRouter方法添加路由。我们在Engine中,添加了一张路由映射表router,key 由请求方法和静态路由地址构成,例如GET-/、GET-/hello、POST-/hello,这样针对相同的路由,如果请求方法不同,可以映射不同的处理方法(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方法

// Run defines the method to start a http server
func (engine *Engine) Run(addr string) (err error) {
	return http.ListenAndServe(addr, engine)
}

当用户调用(*Engine).GET()方法时,会将路由和处理方法注册到映射表 router 中,当我们观察(*Engine).Run()方法,发现里面实则是调用http.ListenAndServe(addr, engine)。我们会发现所有的http请求的处理首先都会进入ServeHTTP方法,在ServeHTTP方法内,发现通过engine.router这个map传入key,来调用不同的函数来处理不同的请求。

然后让engine对象去实现ListenAndServe接口,即实现ServerHTTP方法

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\n", req.URL)
	}
}

在main.go文件的主函数中调用刚才写的Gee框架

func main() {
	r := gee.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")
}

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

猜你喜欢

转载自blog.csdn.net/qq_47431008/article/details/130654185