这篇文章是搬运自我的个人blog:
MashiroC的奇思妙想
最近闲来无事,捣鼓了一下Golang的Web框架。这一篇文章主要是梳理一下Web框架的执行逻辑,真正开始上手撸代码和踩坑要到下一篇。
因为是Java选手,刚开始学Go不久,代码风格可能比较Java。
另外有一些题外话,我最初开始学习Web框架是一本Java的讲解Web框架的书籍,名字叫《框架探险》。
但是Java的Web框架和Go的Web框架还是有不少的区别,学习Go的话不是很建议使用这本书。
Web框架基本设计
现代的Web后端框架中,所有框架的设计都会提供一部分共同的功能。这部分功能是Web框架的最基础功能:建立url对函数的映射,对 Request / Response 进行解析和封装。这些框架减轻了工作时的重复劳动与复杂繁琐的api调用,提升了应用的开发速度和可维护性。
框架不管是使用过滤器这样的设计(比如struts),还是直接使用一个函数处理所有的请求(比如spring、gin等等),都是使用了单一入口,即所有请求通过原生的api进入到一个统一的处理模块。
请求进来时,在这个单一模块中查找请求的路由(uri)和方法(method)所对应的处理这个路由的函数。若查找不到,则返回404(NOT FOUND)或405(MOTHOD NOW ALLOW)。
找到对应的处理函数后取出这个函数,根据解析请求中的参数,如get请求中url的参数、post请求中body里的参数,解析并封装后执行该函数,得到返回对象再进行解析通过设定好的格式(json/xml/html等)序列化,最后原生的方法返回。
下面是原生api和使用框架的对比:
我们希望使用框架能够写出如下的代码:
package main
import "begonia"
func main(){
app := application.Init()
app.Get("/hello", func(ctx *begonia.Context) {
ctx.String("hello world")
})
app.Post("/welcome", func(ctx *begonia.Context) {
name := ctx.Param("name")
ctx.String("hello " + name)
})
app.Start(1234)
}
对于一个框架来说,最基础的模块就是路由模块了,即uri对执行函数的映射。下面一个部分我们来聊聊这个路由。
路由模块
在阅读了多个语言多个框架的路由解析部分源码后,一般路由的实现大体分为两个思路:哈希表和Trie树(前缀树)。
使用哈希表的为:
- Java Spring
而我所看过的golang的框架,全部使用了Trie树来实现路由的解析分发
- beego
- gin
- echo
我们这篇文章里简单来说一下两种实现的思路。
路由的哈希表实现
由于各大语言都拥有了原生的哈希表实现,这里我们就不赘述如何实现一个哈希表了,感兴趣可以自行 百度/谷歌 一下。
Go的map、Java的HashMap、pthon的dict采用的是哈希表实现,而c++的map使用的是红黑树,哈希表是unordered_map,这里提醒一下。
我们可以针对每个方法创建一个哈希表,每添加一个路由,在对应方法的哈希表下建立一个键值对即可。
当请求进来时,根据uri查找路由,若查找得到则封装请求参数并执行。若查找不到,则去其他方法的哈希表里再查询一下,这一步查到的到返回405 method not allow
,查找不到返回404 not found
。
路由的Trie树实现
这里我聊一聊使用压缩Trie树
来添加和查找路由,代码实现我们下一篇文章来专门分析。
Trie树
又称前缀树
、字典树
,思路是将字符串按每个字符存储在树的节点上,具有公共前缀的单词共享一个或几个父节点。例如:"wechat"、"weibo"、"ware"、"hi"、"te"存储在Trie树
上的情况如图所示
但是对于url来说,会有很多的uri具有公共前缀,但是具有较长的非公共的后缀,这个时候我们就需要使用到压缩Trie树
了。
压缩Trie树
其实是上面的普通Trie树的升级版,比普通Trie树
占用更少的内存空间。大题思路是在普通Trie树
上将长链压缩为一个节点。上图所示的数据结构压缩后如下图所示:
对比上面的普通Trie树,不难发现每个节点不存在单独的子节点,没有单独的长链存在了。
写在最后
在Web框架中除了路由,还拥有其他非常重要的模块。这一篇文章只是一个简单的Web框架针对路由的一些分析。在下一篇我会详细分析并使用代码实现压缩Trie树路由