day1-HTTP基础
Q7nl1s admin

想着 Gin 框架差不多学完了,是时候动手实践一下了,就在 github 找了找可以做的项目,正好翻到这个 7天用Go从零实现系列 的项目,其中这个 7天用Go从零实现Web框架 - Gee 刚好符合为我的进度,就开始动手了。

看完回来说一句,写的整的很不错,因为是对 Gin 框架的仿刻,可以看到很多 Gin 的影子,整个设计流程都讲的很清楚,让我对整个框架的理解有了很大的突破,还是应该多看看源码,多想想为什么怎么写。

根据教程来先看看标准库net/http如何处理一个请求。

1
2
3
4
5
6
7
8
9
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报文。一些Web开发中简单的需求并不支持,需要手工实现。

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

Gee介绍

Gee 是这和框架的名字,它参考了 Gin 框架,它们码量都很小,实际码量只有几 k,一句话:小而美。

框架的设计原则根据作者的话来讲:时间关系,同时为了尽可能地简洁明了,这个框架中的很多部分实现的功能都很简单,但是尽可能地体现一个框架核心的设计原则。例如Router的设计,虽然支持的动态路由规则有限,但为了性能考虑匹配算法是用Trie树实现的,Router最重要的指标之一便是性能。

the first day

  • 简单介绍net/http库以及http.Handler接口。
  • 搭建Gee框架的雏形,代码约50行

标准库启动Web服务

Go 内置了 net/http 库,用于分装 HTTP网络编程的基础接口,Gee Web 框架就是基于 net/http 实现的。

day1-http-base/base1/main.go

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
package main

import (
"fmt"
"log"
"net/http"
)

func main() {
http.HandleFunc("/", indexHandler)
http.HandleFunc("/hello", helloHandler)
// ListenAndServe监听TCP网络地址addr,然后使用处理程序调用Serve来处理传入连接上的请求。已接受的连接被配置为启用TCP保持活动。
// 处理程序通常为nil,在这种情况下使用DefaultServeMux。
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)
}
}

上述程序我们设置了两个路由,//hello,分别绑定两个控制器 indexHandlerhelloHandler ,它们会对不同的 HTTP 请求调用不同的处理函数。如访问 / ,响应是 URL.Path = / ,而 /hello 的响应则是请求头(header)中的键值对信息。

用 postman 进行端口测试,得到如下结果:

1
2
3
4
5
6
7
8
9
$ 127.0.0.1:9999/
URL.Path = "/"

$ 127.0.0.1:9999/hello
eader["Accept"]= ["*/*"]
Header["Postman-Token"]= ["7468ff89-13d1-4961-9ca6-b344e2883d45"]
Header["Accept-Encoding"]= ["gzip, deflate, br"]
Header["Connection"]= ["keep-alive"]
Header["User-Agent"]= ["PostmanRuntime/7.29.2"]

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

实现http.Handler接口

1
2
3
4
5
6
7
package http

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

func ListenAndServe(address string, h Handler) error

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

day1-http-base/base2/main.go

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
package main

import (
"fmt"
"log"
"net/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)
}
}

func main() {
engine := new(Engine)
log.Fatal(http.ListenAndServe(":9999", engine))
}
  • 我们定义了一个空的结构体Engine,实现了方法ServeHTTP。这个方法有 2 个参数,第二个参数是 Request ,该对象包含了该HTTP请求的所有的信息,比如请求地址、Header 和 Body 等信息;第一个参数是 ResponseWriter ,利用 ResponseWriter 可以构造针对该请求的响应。
  • main 函数中,我们给 ListenAndServe 方法的第二个参数传入了刚才创建的engine实例。至此,我们走出了实现 Web 框架的第一步,即,将所有的 HTTP 请求转向了我们自己的处理逻辑。还记得吗,在实现Engine之前,我们调用 http.HandleFunc 实现了路由和 Handler 的映射,也就是只能针对具体的路由写处理逻辑。比如/hello。但是在实现Engine之后,我们拦截了所有的 HTTP 请求,拥有了统一的控制入口。在这里我们可以自由定义路由映射的规则,也可以统一添加一些处理逻辑,例如日志、异常处理等。
  • 代码的运行结果与之前的是一致的。

Gee框架的雏形

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

最终代码目录结构如下:

1
2
3
4
5
6
7
└───example.exe
├───go.mod
├───gmain.go

└───gee
├───go.mod
├───main.go

go.mod

day1-http-base/base3/go.mod

1
2
3
4
5
6
7
module example

go 1.19

require gee v0.0.0

replace gee => ./gee
  • go.mod 中使用 replace 将 gee 指向 ./gee

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

main.go

day1-http-base/base3/main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"fmt"
"net/http"

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

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

gee.go

day1-http-base/base3/gee/gee.go

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
45
46
47
48
49
50
package gee

import (
"fmt"
"net/http"
)

// HandlerFunc defines the request handler used by gee
type HandlerFunc func(http.ResponseWriter, *http.Request)

// Engine 实现 ServeHTTP 接口
type Engine struct {
router map[string]HandlerFunc
}

// New 是 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
}

// GET 函数定义添加 GET 请求的方法
func (engine *Engine) GET(pattern string, handler HandlerFunc) {
engine.addRoute("GET", pattern, handler)
}

// POST 函数定义添加 POST 请求的方法
func (engine *Engine) POST(pattern string, handler HandlerFunc) {
engine.addRoute("POST", pattern, handler)
}

// Run 函数用于启动一个 http 服务
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\n", 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,再用 postman 工具访问,结果与最开始的一致

1
2
3
4
5
6
7
8
9
$ 127.0.0.1:9999/
URL.Path = "/"

$ 127.0.0.1:9999/hello
eader["Accept"]= ["*/*"]
Header["Postman-Token"]= ["7468ff89-13d1-4961-9ca6-b344e2883d45"]
Header["Accept-Encoding"]= ["gzip, deflate, br"]
Header["Connection"]= ["keep-alive"]
Header["User-Agent"]= ["PostmanRuntime/7.29.2"]

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

参考文献:

Go语言动手写Web框架

golang导入本地自定义的包的方法与local import “./XXX“ in non-local package错误详解_Asafield的博客-CSDN博客_golang导入本地包

 Comments
Comment plugin failed to load
Loading comment plugin
Powered by Hexo & Theme Keep
Unique Visitor Page View