前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >手写一个http容器【上】决策树与路由表

手写一个http容器【上】决策树与路由表

作者头像
Jean
发布2020-02-17 14:40:34
5670
发布2020-02-17 14:40:34
举报
文章被收录于专栏:Web行业观察Web行业观察

Linux之父说过,伪代码是最好的语言,因为它能够表达所有的逻辑。所以本文所有的代码示例都是伪代码。


  • ALFP协议

如果让我来定义http协议的话,我会给他取一个完全不同的名字:ALFP(Application Layer Fetch Protocol,应用层请求协议)。2020年我甚至忘了“HTTP”的全称是什么?好像是“超文本传输协议”?然后意识到这种古老的,对新人不友好的首字母缩写还是不要拆开来读比较好,况且“超文本”这个词已经鲜为人知了,但至少“超文本”是存在于应用层的东西,再加上“fetch”这个单词能够非常形象的概括出http协议的特点:“抓取”意味着有请求有回应。所以我认为HTTP协议如果改名叫ALPF协议会更有爱,更名的灵感来自ALPN协议(应用层协议协商),如果更名成功,ALFP协议能让00后快速地了解这个协议的功能,减少他们的学习成本,同时还能满足我们老玩家的沙雕强迫症。

------------------------正经的分割线------------------------


  • 手写一个http路由器

一本正经的胡说八道结束了,下面聊聊正题。当我们用烂了Express,Koa等http后端框架就忍不住自己手写一个,如果你懒的去学习一个新的框架,仅仅是为了实现一个小的web app,手写后端框架往往是最好的选择。我将用nodejs制作一个最简单的http后端路由模型,让大家明白web框架一点也不复杂,人人都可以手写。

由于是精简版的后端框架,不用考虑什么负载均衡和容灾,在一台虚拟机服务器上只要考虑ALFP的核心理念就行,关键词就是“应用层”和“抓取”,整个http协议不过如此,我们只要考虑当一个请求进来以后我们先要做什么,再做什么,最后做完什么以后响应回去就完事。中间的每一步就是相互独立的“中间件”。

但是为了写一个通用的后端框架,还是要考察一下大多数网络app通常都有哪些架构,再将这些常见的需求组合起来创造我们自己的web框架。关于需求,Koa框架提出的“洋葱图”给了我们一些参考:

别的不用关心,洋葱图上有一些比较重要的后端功能是我们参考的对象,比如路由,会话,缓存和异常处理。绝大多数web app的架构拆开来看也就这些东西。

由于21世纪20年代以后前端架构基本统一了,浏览器端,桌面端,移动端已经没什么区别了,统统都叫app,只不过需要服务器支持的叫做网络app,不需要的就叫app,甚至所有的主流app都由JS,wasm,H5和Css这4个基本语言写成。

以上是功能,再从性能上考虑,为了不阻塞主线程,所有的中间件都要运行在事件循环引擎之上,换言之每个中间件都是一个promise。


  • 决策树与路由表

中间件之间不仅是串行的,而且是树形的:上一个中间件的计算结果有可能决定下一个中间件,所以整个中间件网络是一棵决策树,在决策树上迭代的过程就叫“路由”,路由的寻路依据就是我们的“路由表”。

路由表有多种形式,不同的业务逻辑可以设计不同的路由表,这里推荐一种常用的,根据Restful动词来构建路由表的策略。Restful动词也就是对数据所有可能操作组成的分类网,我们熟悉的“增删改查”指的就是这些动词,下图展示了这些动词的一部分:

根据数据操作动词来构建决策树不失为一个很好的选择,动词既可以写在http method头部字段里,也可以写在url路径上。至于决策树在代码上怎样体现,可根据剧情选择if/else树或者嵌套的散列表,通常散列表可以让每个决策花费的时间相等,比较适合决策树较大的情况。


  • URL路径的优雅处理

说到路径,后端框架一般都把url上所有路径存放到一个列表中,但由于url路径之间是正斜杠分隔的,为了和空格分隔符统一,多个连续的正斜杠可以看成一个,列表只存放有意义的路径名,所以/path/to和/path//to和/path/to/表达了相同的含义,对应的路由表都是['path', 'to']。生成路由列表的表达式如下:

代码语言:javascript
复制
// 生成路由表的伪代码
request.paths = request.urlPath.split("/").filter(p => p.trim());

request.paths就是路由表,保存着url路径上从左到右每一个路径,每当经过一层路由就让paths.shift()一下,然后根据request.paths[0]来选择下一个中间件。


  • 入口设计(index.html)

入口设计很简单,即当url路径为空的时候需要返回index.html这个静态文件,这时候request.paths是一个空数组。

代码语言:javascript
复制
// 网站入口的伪代码

if (request.paths.length === 0) {
  await new Promise((resolve, reject) => {
    response.setHeader("Content-Type", "text/html");

    const r = require("fs").createReadStream("path/to/index.html");
    r.on("error", err => reject(err));
    r.on("end", resolve);
    r.pipe(response);
  });
}

  • 会话层与认证

很多地方都要考虑入口的特殊性,除了index.html,还要考虑每个request进来后要做的第一件事是什么?第一件事通常是要对这个request进行认证,对不对?无论是通过用户名密码认证还是根据会话凭证来认证,这都是必须要做的(即使它请求的是只读的资源)。

根据剧情需要,可以将会话凭证之外的会话信息存放在客户端或者服务端。反正流行的凭证格式JWT是建议将其他信息存在客户端,比如一些用户的个人信息,反正加密后的数据寄存在前端没有危害,但是数据量大的情况下可以考虑存在后端。下面是一个将token存在前端的例子:

代码语言:javascript
复制
// 会话层token认证的伪代码

module.exports = async function() {
  const req = this.request;
  const res = this.response;
  // myToken是假想的一种凭证插件,类似JWT
  const myToken = require("path/to/myToken");

  req.session = "";
  //   authorization头部用来存放加密的token
  const token = req.headers["authorization"];
  if (!token) return;

  //   secretKey是一个密钥,用于加解密token
  req.session = await myToken.verifyWithKey(token, secretKey).catch(err => {
    if (err.name === "TokenExpiredError") {
      // my-token-expire这个自定义头部提示前端应该删除这个token了
      res.setHeader("my-token-expire", 1);
    } else if (err.name === "invalidTokenError")
      throw "凭证损坏:" + err.message;
  });
};

request进来的时候,我们将它携带的token中解密出来的数据存放到request自己身上,供之后的中间件使用,同时还要做好错误处理。


(未完待续。。。)

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-01-31,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 WebHub 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
消息队列 TDMQ
消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档