目 录CONTENT

文章目录
Go

01.跟着Geek兔兔学习-Go语言动手写Web框架-net/http标准库及http.Handler接口

Hello!你好!我是村望~!
2023-09-01 / 0 评论 / 0 点赞 / 242 阅读 / 1,907 字
温馨提示:
我不想探寻任何东西的意义,我只享受当下思考的快乐~

net/http标准库及http.Handler接口

感谢Geek 兔的教程

示例代码,解释摘抄 https://geektutu.com/ + 结合自己的理解

net/http标准库

package main

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

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,分别绑定 indexHandlerhelloHandler

根据不同的HTTP请求会调用不同的处理函数。访问/,响应是URL.Path = /,而/hello的响应则是请求头(header)中的键值对信息。

❯ curl 127.0.0.1:9999
URL.Path = "/"

❯ curl 127.0.0.1:9999/hello
Header["User-Agent"] = ["curl/7.87.0"]
Header["Accept"] = ["*/*"]

main 函数的最后一行,是用来启动 Web 服务的,

  • 第一个参数是地址,:9999表示在 9999 端口监听。
  • 而第二个参数则代表处理所有的HTTP请求的实例,nil 代表使用标准库中的默认实例处理 (DefaultServeMux)。

http.Handler接口

源码位置: go/src/net/http/server.go

image-20230901143559296

通过查看net/http的源码可以发现,Handler是一个接口,需要实现方法 ServeHTTP

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

也就是说,只要传入任何实现了 ServerHTTP 接口的实例,所有的HTTP请求,就都交给了该实例处理了。

package main

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

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

func (engine *CunEngine) 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(CunEngine)
	log.Fatal(http.ListenAndServe(":9999", engine))
}

定义了一个 CunEngine 结构体,并且实现 ServeHTTP 那么此时就可以当作 net/Http 包的请求处理实例传入参数了!

方法ServeHTTP这个方法有2个参数:

  • 第一个参数是 ResponseWriter ,利用 ResponseWriter 可以构造针对该请求的响应。
  • 第二个参数是 Request ,该对象包含了该HTTP请求的所有的信息,比如请求地址、Header和Body等信息;

main 函数中,我们给 ListenAndServe 方法的第二个参数传入了刚才创建的engine实例。将所有的HTTP请求转向了我们自己的处理逻辑。

在实现Engine之前,我们调用 http.HandleFunc 实现了路由和Handler的映射,也就是只能针对具体的路由写处理逻辑。比如/hello

但是在实现Engine之后,我们拦截了所有的HTTP请求,拥有了统一的控制入口。在这里我们可以自由定义路由映射的规则,也可以统一添加一些处理逻辑,例如日志、异常处理等。而且代码的运行结果与之前的是一致的。

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

func (engine *CunEngine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	timeNow := time.Now()
	switch req.URL.Path {
	case "/":
		log.Print("触发了 / HTTP request")
		fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path)

	case "/hello":
		log.Print("触发了 /hello HTTP request")
		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)
	}
	fmt.Printf("time.Since(timeNow): %v\n", time.Since(timeNow))
}

image-20230901144953072

框架的雏形

模仿Gin框架的使用方式~

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

使用New()创建 gee 的实例,使用 GET()方法添加路由,最后使用Run()启动Web服务。[目前还不支持动态路由~]

package gee

import (
	"fmt"
	"net/http"
)

// HandlerFunc 定义 gee 使用的请求处理程序
type HandlerFunc func(http.ResponseWriter, *http.Request)

// 实现ServeHTTP接口
type Engine struct {
	// 路由表 like: GET-/hello -> HandlerFunc 使用map去存!
	router map[string]HandlerFunc
}

// 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)
	}
}
  • 首先定义了类型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 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

至此,整个Gee框架的原型已经出来了。实现了路由映射表,提供了用户注册静态路由的方法,包装了启动服务的函数。

当然,到目前为止,我们还没有实现比net/http标准库更强大的能力,不用担心,很快就可以将动态路由、中间件等功能添加上去了。

总结一下目前的流程

1⃣️ New 初始化 Engine 以及 内部的 route map

2⃣️ func (engine *Engine) addRoute(method string, pattern string, handler HandlerFunc)

通过组装 请求方法method+Path 当作 key ,handler作为value 添加到路由表中!

  • func (engine *Engine) POST(pattern string, handler HandlerFunc)

  • func (engine *Engine) GET(pattern string, handler HandlerFunc)

这两个方法,内部调用addRoute方法,将路由添加到路由表中!

3⃣️ Run 方法执行 net/http 标准库的 http.ListenAndServe(addr, engine) 方法~并且传入我们自定义的 Engine 实例

那么至此 所有的处理 都会通过我们实现的 func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) 方法来进行处理

4⃣️ ServeHTTP 中 我们根据请求的req.Method方式和请求的路径 req.URL.path 拼装key 去查路由表 router map中是否有对应的 handle

因为handler的参数也需要 w http.ResponseWriter, req *http.Request 所以我们将这两个参数传递给handler 完成调用

handler(w, req)

否则就抛出 404!

0

评论区