最近利用闲暇时间边学边写把依赖注入、中间件编写的功能基础版本搞定了(边写边吃,好像胖了^_^).
今天周日打算在发一篇文章,把路由和上下文处理这块梳理一下,逻辑可能不太通顺,希望各位大神不吝赐教。
晓亮嘚吧嘚系列,今日继续哈
代码我已上传到github,有兴趣的可以clone,别忘记给个小赞star哈,万分感谢 地址:https://github.com/zl8762385/koala
回顾上回我们对写api框架提供基本的思路和构思了框架功能,这里再列举一下
构思需要实现的功能
- 支持静态路由、参数路由、组路由
- 组件式可扩展的 JSON/TEXT/JSONP/XML等输出格式
- 支持依赖注入
- 支持中间件编写
- 统一日志管理
- HTTP上下文处理
- 内置一些可扩展的标准化组件
分析
既然是web框架那么不管任何语言的框架首先重点关注的就是路由及上下文处理,【构思需要实现的功能】1、8就是我们首要实现。
这里列举一下golang、php、java使用路由框架的不同方式
golang
方式1:
app := koala.New()
app.Add("GET", "/profile/xiaoliang", func(ctx *koala.Context) {
ctx.Text("profile.xiaoliang")
})
方式2:
app.Add("GET", "/profile/xiaoliang1", xiaoliangFunc)
func xiaoliangFunc(ctx *koala.Context) {
ctx.Text("profile.xiaoliang1")
}
PHP
路由URI:/module/file/func
由于是解释型语言,路由基本靠nginx,我这里就把路由实现原理写一下
1、nginx实现rewrite path
2、php接收/module/file/func (/模块/文件/函数)
3、/module/file/func 将path拆分,然后映射到PHP对应文件访问即可
直接贴PHP代码,有兴趣可以看看
include $module_file;
$obj_module = new $class_name();
if (!method_exists($obj_module, $method_name)) {
die("要调用的方法不存在");
} else {
if (is_callable(array($obj_module, $method_name))) {
$obj_module->$method_name($request_query, $_POST);
}
JAVA
@RequestMapping("/index2")
public String index2() {
return "index2";
}
以上是三种语言路由的使用方式,相信你已经看出不同了吧。
Golang和JAVA是编译型语言,处理方式把URL写死代码中。
PHP解释型语言各种动态处理,各种骚操作。只要能访问到具体文件函数,咋处理都不为过(PHP是世界上最好的语言).
嗷嗷,再讲一下怕跑题了,这里简单对比一下就好。
go web路由大致功能:单个路由、分组路由(主要看项目需要)等
上面对比中有golang两种代码方式效果完全一样,主要为了让初学者能够更清晰理解结构,我刚接触golang是看其他大神的框架,关于路由和http上下文整整分析一天时间(因之前是phper,对上下文这种也没有概念,导致各种理解都跟不上,弱类型等等...),我一般看开源代码喜欢带着疑问去研究,『用疑问做目标,从过程中学习到不同的知识点』,我的疑问就是以下两点
疑问1:
ctx *koala.Context
是从什么地方创建,什么地方传递过来的,为什么每个路由函数上都可以有这种闭包形式的回调呢?
疑问2:
因需要静态编译,那么我的URL PATH:/profile/xiaoliang注册到什么地方了
好了,让我们带着疑问一探究竟,就用咱们写的这个简易版框架开始吧
使用方式
package main
import (
"fmt"
"koala/v1" 使用到这个包,下一步看这里
)
func main() {
app := koala.New()
app.Add("GET", "/profile/xiaoliang", func(ctx *koala.Context) {
fmt.Println("xiaoliang")
ctx.Text("profile.xiaoliang")
})
app.Run(":8080")
}
核心文件:koala/v1/koala.go
我们整体看一下代码注解及流程,然会用分解的形式把ServeHTTP和Context讲解一下都做了哪些事
import (
"fmt"
"koala/v1/utils"
"net/http"
"sync"
)
const (
Version = "0.0.1"
FrameworkName = "koala"
Anthor = "xiaoliang"
)
// 上下文处理函数
type HandlerFunc func(*Context)
// 定义 koala引擎结构
type Koala struct {
// 调试模式
debug bool
// 版本号
version string
// 节点树
trees map[string]*node
// 临时对象池
pool sync.Pool
// 单例注入
di DIer
// 中间件
middleware []HandlerFunc
}
// 实例化Koala
func New() *Koala {
engine := &Koala{
version: Version,
}
// 初始化临时对象池
engine.pool = sync.Pool{
New: func() interface{} {
return engine.httpContext()
},
}
...代码已折叠
return engine
}
// http请求上下文
func (k *Koala) httpContext() *Context {
return &Context{koala:k}
}
...代码已折叠
// 实现net/http 需要的servehttp服务
func (k *Koala) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
// 取一条对象 默认ctx.rw是空,这里在最开始New时已经初始化完成
ctx := k.pool.Get().(*Context)
ctx.Reset(rw,req,k)
// 过滤相关文件
if _, ok := k.router().Filter(rw, req); !ok {
return
}
// 执行相关操作
ctx.Next()
k.pool.Put(ctx)
}
以上代码是核心文件中关于路由和http上下文处理的相关逻辑,并做了注释。
大致流程如下脑图:
![4e9ddaf70064b2d32c491395b36e5ca2.png](https://i-blog.csdnimg.cn/blog_migrate/5fbfbf1a59eccf7130dee349dc5d4a1a.png)
拆解
文件位置:Koala.go
// 实现net/http 需要的servehttp服务
func (k *Koala) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
...代码折叠
// 执行相关操作
ctx.Next()
...代码折叠
}
ctx.Next() 再这里执行了路由及其他操作,大概意思是,请求过来执行下一步映射
文件位置:context.go
// 上下文结构体
type Context struct {
// 继承net/http responsewriter
RW http.ResponseWriter
// 继承net/http request
Req *http.Request
// 继承 koala包
koala *Koala
// 路由名称
routerName string
// 方法名称
method string
// http 参数类型
Param Params
}
...代码折叠
// 执行句柄
// 首先处理中间件,然后处理路由句柄
func (c *Context) Next() {
...代码折叠
// 路由映射处理
c.koala.router().HandlerRouter(c)
}
1、c.koala.router().HandlerRouter(c)
http上下文传递给路由处理器
文件位置:router.go
...代码折叠
type Router struct {
koala *Koala
routerName string
method string
}
...代码折叠
// 添加路由节点
func (r *Router) Add(httpMethod, path string, router HandlerFunc) {
fmt.Printf("Add %+v %+v n", httpMethod, path)
// 检查树是否为空
if r.koala.trees == nil {
r.koala.trees = make(map[string]*node)
}
// 找到大类GET POST PUT等
root := r.koala.trees[httpMethod]
if root == nil {
// new 赋值 保存到节点上
root = new(node)
r.koala.trees[httpMethod] = root
}
root.addRoute(path, router)
}
...代码折叠
// 路由映射处理
func (r *Router) HandlerRouter(ctx *Context) {
fmt.Println("路由映射处理 start")
// 路由处理 执行对应函数
if root := ctx.koala.trees[ctx.method]; root != nil {
// ps /member/:name/:age
if handlerFunc, ps, _ := root.getValue(ctx.routerName); handlerFunc != nil {
这里是重点
ctx.Param = ps
handlerFunc(ctx)
} else if ctx.method != "CONNECT" {
// 客户端没有使用connect隧道进行代理请求
ctx.RW.Write([]byte("404"))
// 永久重定向 使用GET方法请求
fmt.Println("not connect", ctx.method)
} else {
http.NotFound(ctx.RW, ctx.Req)
}
}
fmt.Println("路由映射处理 end")
}
...代码折叠
1、添加路由到节点树
这里为了把golang的工程逻辑整明白,我直接使用了gin框架的tree包,业内号称gin的路由处理速度快全在这棵树上了。有兴趣的可以去研究研究
2、HandlerRouter
从tree节点找到我们的函数,执行各种逻辑处理。
重点在这里:handlerFunc(ctx) == func(ctx *koala.Context)
app.Add("GET", "/profile/xiaoliang", func(ctx *koala.Context) {
ctx.Text("profile.xiaoliang")
})
综合以上拆解分析有没有清晰一些了,我这里再总结一下
疑问1:
ctx *koala.Context
是从什么地方创建,什么地方传递过来的,为什么每个路由函数上都可以有这种闭包形式的回调呢?
答:
1、从什么地方创建
New实例化时创建了上下文,并且扔到临时对象池里
```
// http请求上下文
func (k *Koala) httpContext() *Context {
return &Context{koala:k}
}
```
2、什么地方传递过来的,为什么每个路由函数上都可以有这种闭包形式的回调呢?
```
// 路由映射处理
func (r *Router) HandlerRouter(ctx *Context) {
...代码折叠
ctx.Param = ps
handlerFunc(ctx)
...代码折叠
}
```
简单的说是将函数注册到节点树上,再使用到时候拿到然后执行操作
大致流程:
1、注册函数到节点树
2、用户请求时从tree节点树拿到对应函数
3、执行函数,并把上下文再传回使用者手里.
疑问2:
因需要静态编译,那么我的URL PATH:/profile/xiaoliang注册到什么地方了
答:再tree节点树上存着呢
晓亮嘚吧嘚,请听下回分解
今天的篇幅较长,如有疑问可以在下方评论,特别是phper同学再转golang时会遇到很多疑问,我也算是半个过来人,如果能有幸帮上您,我会尽我所能。
下一篇我在想想从什么地方开始讲(也取决我下面会写多少东西),嚯嚯,让我们一起加油吧。