abcbuzhiming

abcbuzhiming 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

abcbuzhiming 回答了问题 · 11月21日

解决前端工作碰到哪些业务不能做?

前端程序可以说是直接面向最终用户的,所以,做赌博游戏的,你说前端居然不知道自己做的是什么,这是装傻。

关注 8 回答 6

abcbuzhiming 收藏了文章 · 9月13日

MySQL索引凭什么能让查询效率提高这么多?

背景

我相信大家在数据库优化的时候都会说到索引,我也不例外,大家也基本上能对数据结构的优化回答个一二三,以及页缓存之类的都能扯上几句,但是有一次阿里P9的一个面试问我:你能从计算机层面开始说一下一个索引数据加载的流程么?(就是想让我聊IO)

我当场就去世了....因为计算机网络和操作系统的基础知识真的是我的盲区,不过后面我恶补了,废话不多说,我们就从计算机加载数据聊起,讲一下换个角度聊索引。

正文

MySQL的索引本质上是一种数据结构

让我们先来了解一下计算机的数据加载。

磁盘IO和预读:

先说一下磁盘IO,磁盘读取数据靠的是机械运动,每一次读取数据需要寻道、寻点、拷贝到内存三步操作。

寻道时间是磁臂移动到指定磁道所需要的时间,一般在5ms以下;

寻点是从磁道中找到数据存在的那个点,平均时间是半圈时间,如果是一个7200转/min的磁盘,寻点时间平均是600000/7200/2=4.17ms;

拷贝到内存的时间很快,和前面两个时间比起来可以忽略不计,所以一次IO的时间平均是在9ms左右。听起来很快,但数据库百万级别的数据过一遍就达到了9000s,显然就是灾难级别的了。

考虑到磁盘IO是非常高昂的操作,计算机操作系统做了预读的优化,当一次IO时,不光把当前磁盘地址的数据,而是把相邻的数据也都读取到内存缓冲区内,因为当计算机访问一个地址的数据的时候,与其相邻的数据也会很快被访问到。

每一次IO读取的数据我们称之为一页(page),具体一页有多大数据跟操作系统有关,一般为4k或8k,也就是我们读取一页内的数据时候,实际上才发生了一次IO。

(突然想到个我刚毕业被问过的问题,在64位的操作系统中,Java中的int类型占几个字节?最大是多少?为什么?)

那我们想要优化数据库查询,就要尽量减少磁盘的IO操作,所以就出现了索引。

索引是什么?

MySQL官方对索引的定义为:索引(Index)是帮助MySQL高效获取数据的数据结构。

MySQL中常用的索引在物理上分两类,B-树索引和哈希索引。

本次主要讲BTree索引。

BTree索引

BTree又叫多路平衡查找树,一颗m叉的BTree特性如下:

  • 树中每个节点最多包含m个孩子。
  • 除根节点与叶子节点外,每个节点至少有[ceil(m/2)]个孩子(ceil()为向上取整)。
  • 若根节点不是叶子节点,则至少有两个孩子。
  • 所有的叶子节点都在同一层。
  • 每个非叶子节点由n个key与n+1个指针组成,其中[ceil(m/2)-1] <= n <= m-1 。

这是一个3叉(只是举例,真实会有很多叉)的BTree结构图,每一个方框块我们称之为一个磁盘块或者叫做一个block块,这是操作系统一次IO往内存中读的内容,一个块对应四个扇区,紫色代表的是磁盘块中的数据key,黄色代表的是数据data,蓝色代表的是指针p,指向下一个磁盘块的位置。

来模拟下查找key为29的data的过程:

1、根据根结点指针读取文件目录的根磁盘块1。【磁盘IO操作1次

2、磁盘块1存储17,35和三个指针数据。我们发现17<29<35,因此我们找到指针p2。

3、根据p2指针,我们定位并读取磁盘块3。【磁盘IO操作2次

4、磁盘块3存储26,30和三个指针数据。我们发现26<29<30,因此我们找到指针p2。

5、根据p2指针,我们定位并读取磁盘块8。【磁盘IO操作3次

6、磁盘块8中存储28,29。我们找到29,获取29所对应的数据data。

由此可见,BTree索引使每次磁盘I/O取到内存的数据都发挥了作用,从而提高了查询效率。

但是有没有什么可优化的地方呢?

我们从图上可以看到,每个节点中不仅包含数据的key值,还有data值。而每一个页的存储空间是有限的,如果data数据较大时将会导致每个节点(即一个页)能存储的key的数量很小,当存储的数据量很大时同样会导致B-Tree的深度较大,增大查询时的磁盘I/O次数,进而影响查询效率。

B+Tree索引

B+Tree是在B-Tree基础上的一种优化,使其更适合实现外存储索引结构。在B+Tree中,所有数据记录节点都是按照键值大小顺序存放在同一层的叶子节点上,而非叶子节点上只存储key值信息,这样可以大大加大每个节点存储的key值数量,降低B+Tree的高度。

B+Tree相对于B-Tree有几点不同:

非叶子节点只存储键值信息, 数据记录都存放在叶子节点中, 将上一节中的B-Tree优化,由于B+Tree的非叶子节点只存储键值信息,所以B+Tree的高度可以被压缩到特别的低。

具体的数据如下:

InnoDB存储引擎中页的大小为16KB,一般表的主键类型为INT(占用4个字节)或BIGINT(占用8个字节),指针类型也一般为4或8个字节,也就是说一个页(B+Tree中的一个节点)中大概存储16KB/(8B+8B)=1K个键值(因为是估值,为方便计算,这里的K取值为〖10〗^3)。

也就是说一个深度为3的B+Tree索引可以维护10^3 10^3 10^3 = 10亿 条记录。(这种计算方式存在误差,而且没有计算叶子节点,如果计算叶子节点其实是深度为4了)

我们只需要进行三次的IO操作就可以从10亿条数据中找到我们想要的数据,比起最开始的百万数据9000秒不知道好了多少个华莱士了。

而且在B+Tree上通常有两个头指针,一个指向根节点,另一个指向关键字最小的叶子节点,而且所有叶子节点(即数据节点)之间是一种链式环结构。所以我们除了可以对B+Tree进行主键的范围查找和分页查找,还可以从根节点开始,进行随机查找。

数据库中的B+Tree索引可以分为聚集索引(clustered index)和辅助索引(secondary index)。

上面的B+Tree示例图在数据库中的实现即为聚集索引,聚集索引的B+Tree中的叶子节点存放的是整张表的行记录数据,辅助索引与聚集索引的区别在于辅助索引的叶子节点并不包含行记录的全部数据,而是存储相应行数据的聚集索引键,即主键。

当通过辅助索引来查询数据时,InnoDB存储引擎会遍历辅助索引找到主键,然后再通过主键在聚集索引中找到完整的行记录数据。

不过,虽然索引可以加快查询速度,提高 MySQL 的处理性能,但是过多地使用索引也会造成以下弊端

  • 创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加。
  • 除了数据表占数据空间之外,每一个索引还要占一定的物理空间。如果要建立聚簇索引,那么需要的空间就会更大。
  • 当对表中的数据进行增加、删除和修改的时候,索引也要动态地维护,这样就降低了数据的维护速度。
注意:索引可以在一些情况下加速查询,但是在某些情况下,会降低效率。

索引只是提高效率的一个因素,因此在建立索引的时候应该遵循以下原则:

  • 在经常需要搜索的列上建立索引,可以加快搜索的速度。
  • 在作为主键的列上创建索引,强制该列的唯一性,并组织表中数据的排列结构。
  • 在经常使用表连接的列上创建索引,这些列主要是一些外键,可以加快表连接的速度。
  • 在经常需要根据范围进行搜索的列上创建索引,因为索引已经排序,所以其指定的范围是连续的。
  • 在经常需要排序的列上创建索引,因为索引已经排序,所以查询时可以利用索引的排序,加快排序查询。
  • 在经常使用 WHERE 子句的列上创建索引,加快条件的判断速度。

现在大家知道索引为啥能这么快了吧,其实就是一句话,通过索引的结构最大化的减少数据库的IO次数,毕竟,一次IO的时间真的是太久了。。。

总结

就面试而言很多知识其实我们可以很容易就掌握了,但是要以学习为目的,你会发现很多东西我们得深入到计算机基础上才能发现其中奥秘,很多人问我怎么记住这么多东西,其实学习本身就是一个很无奈的东西,既然我们不能不学那为啥不好好学?去学会享受呢?最近我也在恶补基础,后面我会开始更新计算机基础和网络相关的知识的。

我是敖丙,你知道的越多,你不知道的越多,我们下期见!

人才们的 【三连】 就是敖丙创作的最大动力,如果本篇博客有任何错误和建议,欢迎人才们留言!

查看原文

abcbuzhiming 收藏了文章 · 8月20日

从零搭建 Node.js 企业级 Web 服务器(零):静态服务

前言

过去 5 年,我前后在菜鸟网络和蚂蚁金服做开发工作,一方面支撑业务团队开发各类业务系统,另一方面在自己的技术团队做基础技术建设。期间借着 Node.js 的锋芒做了不少 Web 系统,有的至今生气蓬勃、有的早已夭折淡出。在实践中,蚂蚁的 Chair 与淘系的 Midway 给了我不少启发,也借鉴了不少 bad case。思考过身边团队、自己团队、国外团队的各种案例之后发现,搭建一个 Node.js 企业级 Web 服务器并非难事,只是必须做好几个关键事项

在接下来的一段时间里,我会以如何 “从零搭建 Node.js 企业级 Web 服务器” 为主题,将自己的所见所闻、所思所想详尽地记录下来,每个章节最后会附上实现本章内容的源码,希望可以帮助正在学习和了解 Node.js 的朋友对 Web 服务器领域获得更清晰的理解和洞见。

阅读提示:

  • 本文着重表述 Web 后端技术相关内容,Web 前端内容采用 JavaScript Modules 进行演示。
  • 本文需要读者具备基础的编程能力以及对计算机网络的基本了解,一些常用术语限于篇幅不再展开。

准备环境

安装 Node.js

Node.js 发布版本分为 Current 和 LTS 两类,前者每 6 个月迭代一个大版本,快速提供新增功能和问题修复,后者以 30 个月为一个大周期,为生产环境提供稳定依赖。当前 Node.js 最新 LTS 版本为 12.18.2,本文以此版本作为运行环境,可以在官网下载并安装。

安装完成后,在命令行输入 node --version 查看输出是否为 v12.8.2,如果一致,那么安装成功。

另外,有兴趣的读者可以尝试通过 nvm / nvm-windows 管理多个 Node.js 版本,此处不是本文重点不再展开。

安装 Yarn

Node.js 提供了自己的包管理器 npm,npm 默认从海外官方的 registry 拉取包信息速度比较慢,需要执行以下命令设置使用国内镜像地址:

$ npm config set registry http://r.cnpmjs.org/

通过 npm 可以全局或本地安装依赖包,当本地安装时 npm 会根据 package.json 安装最新的依赖包,同时自动生成并更新 package-lock.json 文件,这就引发了一个问题:如果一个依赖包在 package.json 标记版本范围内发布了有问题的新版本,那么我们自己的项目也会跟着出问题。

为了解决这个问题,就引入了第三方包管理器 yarn,通过以下命令安装:

$ npm i -g yarn

相比 npm,yarn 会严格按照自动生成的 yarn.lock 本地安装依赖包,只有增删依赖或者 package.json 标记版本发生不可兼容的变化时才会更新 yarn.lock,这就彻底杜绝了上述 npm 的问题。考虑到企业级 Web 服务器对稳定性的要求,yarn 是必要的。

安装 Docker

一般来讲,做一个 Web 服务器会有两种部署选择,要么是传统的包部署,要么是容器镜像部署,后者较前者在编排上更方便,结合 Kubernetes 可以做到很好的伸缩,是当前发展的趋势。Docker 作为主流容器技术必须熟练掌握,可以在官网下载并安装。

写一个静态资源服务器

初始化工程

准备好了环境就可以开始编码了,先新建工程根目录,然后进入并初始化 package.json 与目录结构:

$ mkdir 00-static   # 新建工程根目录
$ cd 00-static      # 进入工程根目录

$ yarn init -y      # 初始化 package.json
yarn init v1.22.4
success Saved package.json

$ mkdir src         # 新建 src 目录存放核心逻辑
$ mkdir public      # 新建 public 目录存放静态资源

$ tree -L 1         # 展示当前目录内容结构
.
├── package.json
├── public
└── src

Express 还是 Koa?

Express 与 Koa 均是 Node.js 服务端基础框架。Express 发布于 2010 年,凭借出色的中间件机制在开源社区积累了大量的成熟模块,现在是 OpenJS 基金会 At-Large 级别项目。Koa 发布于 2013 年,相比 Express 具备了更加完善的中间件机制以及编程体验,但在开源社区模块积累的质与量上还有一定差距。在此比较几个常用模块:

模块名称功能简介Express / KoaStarContributersUsed by最近提交时间
passport认证登录Express17.7k33385k2020-06-10
koa-passport认证登录Koa737214.7k2019-07-13
connect-redis会话存储Express2.3k5126.3k2020-07-10
koa-redis会话存储Koa310132.7k2020-01-16
helmet网络安全Express7.2k25136.4k2020-07-11
koa-helmet网络安全Koa546244.1k2020-06-03

上表整理自 Github 截止 2020 年 7 月 20 日的数据。

相比 Koa 模块,Express 模块普遍在星数(Star)、贡献者数(Contributers)、使用数(Used by)上高出一个层次,同时 Express 模块的贡献者更热心于维护与更新,Koa 尽管在国内受到过一些追捧,但在更全面的考量下 Express 才是更稳健的选择。在工程根目录执行以下命令安装:

$ yarn add express  # 本地安装 Express
# ...
info Direct dependencies
└─ express@4.17.1
# ...

$ tree -L 1         # 展示当前目录内容结构
.
├── node_modules
├── package.json
├── public
├── src
└── yarn.lock

静态服务

现在可以开始写应用逻辑了,本章先做一个静态资源服务器,以 public 目录为静态资源目录:

// src/server.js
const express = require('express');
const { resolve } = require('path');
const { promisify } = require('util');

const server = express();
const port = parseInt(process.env.PORT || '9000');
const publicDir = resolve('public');

async function bootstrap() {
  server.use(express.static(publicDir));
  await promisify(server.listen.bind(server, port))();
  console.log(`> Started on port ${port}`);
}

bootstrap();
<!-- public/index.html -->
<html>
  <head>
    <meta charset="utf-8" />
  </head>
  <body>
    <h1>It works!</h1>
  </body>
</html>
$ tree -L 2 -I node_modules   # 展示除了 node_modules 之外的目录内容结构
.
├── package.json
├── public
│   └── index.html
├── src
│   └── server.js
└── yarn.lock

逻辑写好之后在 package.json 中设置启动脚本:

{
  "name": "00-static",
  "version": "1.0.0",
-  "main": "index.js",
+  "scripts": {
+    "start": "node src/server.js"
+  },
  "license": "MIT",
  "dependencies": {
    "express": "^4.17.1"
  }
}

然后就可以启动应用了:

$ yarn start
> Started on port 9000

访问 http://localhost:9000/ 即可看到 index.html 内容:

765bb72ad6058d32b641116e26f8d7338cf949f9.jpg

使用容器

接下来通过 Docker 对做好的静态资源服务器进行容器化,新建以下配置文件:

# Dockerfile
FROM node:12.18.2-slim

WORKDIR /usr/app/00-static
COPY . .
RUN yarn

EXPOSE 9000
CMD yarn start
# .dockerignore
node_modules
$ tree -L 1 -a  # 展示包括 . 开头的全部目录内容结构
.
├── .dockerignore
├── Dockerfile
├── node_modules
├── package.json
├── public
├── src
└── yarn.lock

然后构建镜像并启动容器:

$ # 构建容器镜像,命名为 00-static,标签为 1.0.0
$ docker build -t 00-static:1.0.0 .
# ...
Successfully tagged 00-static:1.0.0

$ # 以镜像 00-static:1.0.0 运行容器,命名为 00-static
$ docker run -p 9090:9000 -d --name 00-static 00-static:1.0.0

$ docker logs 00-static   # 查看 00-static 容器的日志
> Started on port 9000

$ docker stats 00-static  # 查看 00-static 容器的状态
CONTAINER ID        NAME                CPU %               MEM USAGE / LIMIT     MEM %               NET I/O             BLOCK I/O           PIDS
43c451232fa5        00-static           0.03%               37.41MiB / 1.945GiB   1.88%               8.52kB / 3.35kB     0B / 0B             24

访问 http://localhost:9090/ 即可与之前一样看到 index.html 内容:

e369752861fb54057d2683cca41c81eb01071368.jpg

本章源码

host1-tech/nodejs-server-examples - 00-static

更多阅读

从零搭建 Node.js 企业级 Web 服务器(零):静态服务
从零搭建 Node.js 企业级 Web 服务器(一):接口与分层
从零搭建 Node.js 企业级 Web 服务器(二):校验
从零搭建 Node.js 企业级 Web 服务器(三):中间件
从零搭建 Node.js 企业级 Web 服务器(四):异常处理
从零搭建 Node.js 企业级 Web 服务器(五):数据库访问
从零搭建 Node.js 企业级 Web 服务器(六):会话
从零搭建 Node.js 企业级 Web 服务器(七):认证登录
从零搭建 Node.js 企业级 Web 服务器(八):网络安全
从零搭建 Node.js 企业级 Web 服务器(九):配置项
从零搭建 Node.js 企业级 Web 服务器(十):日志
从零搭建 Node.js 企业级 Web 服务器(十一):定时任务
从零搭建 Node.js 企业级 Web 服务器(十二):远程调用
从零搭建 Node.js 企业级 Web 服务器(十三):断点调试与性能分析
从零搭建 Node.js 企业级 Web 服务器(十四):自动化测试
从零搭建 Node.js 企业级 Web 服务器(十五):总结与展望

查看原文

abcbuzhiming 收藏了文章 · 8月8日

这可能是你见过最好的工程师绘图指北

作为一名工程师,绘图可以说是必备的技能。优秀的绘图能力就像写得一手好字,总能让你在团队或者客户面前闪光,这也是你赢得团队青睐和客户赞扬的一个重要能力。

绘图的过程其实是分解任务分离关注点的过程,它和程序设计的过程几乎重叠,因此绘图和程序设计是正向相互促进的。也就是说,你在绘图的过程中发现的问题很有可能会在程序中出现,你在程序中要面对的问题很有可能在绘图的过程中就发现了,早发现早处理。

为什么别人画的图比我的好看?有什么技巧吗?画图丑是天生的吗?我能不能通过短时间的学习绘制出逼格高的程序设计图呢?

绘图是点、线、面、光影和色彩的融合,想要设计出漂亮的图,可以阅读设计领域的相关知识,跨界是目前你跟同领域对手拉开差距的优选之一。今天我们就来学习如何画得一手好图,画好图有哪些技巧和策略,并手把手带你绘制程序设计过程中常用的时序图、流程图、应用分层架构图。

Processon

ProcessOn 是一个在线作图的聚合平台, 运营方是北京大麦地信息技术有限公司。ProcessOn 的绘图基于浏览器,因此它不受操作系统限制,可以跨平台操作。ProcessOn 的画布分为两大类:思维导图画布和自由画布。思维导图画布专注于节点属性和关系的构建,图 1 是思维导图画布的模板示例。

图 1 思维导图画布的模板示例

自由画布则给我们提供了尽情挥洒的空间,我们可以在自由画布中绘制 UML 类图、功能模块组合图、事件流程图和应用架构图等,图 2 是自由画布的模板示例。

图 2 自由画布的模板示例

既然是公司运营,那么肯定需要盈利点了,ProcessOn 产品的价格分为三个等级:免费版、个人版和团队版。图 3 展示了不同版本的价格与功能差异。

图 3 ProcessOn 不同版本的价格与功能差异图示

用户注册登录后就可以使用免费版,虽说它限制了单个账户文件数量,但我们可以通过邀请好友来提升文件数量上限。值得一提的是,一个文件里可以绘制多幅图,这样文件上限的问题就缓解了。不过如果是团队使用或者商用,建议购买个人版或者团队版,一方面能够支持开发团队提供更稳定的服务和丰富的功能,另一方面也尊重原创劳动。

金山 WPS

WPS 是国内不可多得的优秀应用,与微软 Office 办公套件分庭抗礼且不落下风,实在令人佩服。WPS 近年来也大力发展除文档、表格和演示文稿外的附加功能,思维导图和流程图两大模块争相上线。图 4 为 WPS 思维导图布局模板图示。

图 4 WPS 思维导图布局模板图示

从文件导出的格式(.pos)来看,WPS 似乎是跟 ProcessOn 合作推出的思维导图和流程图模块。相对于 ProcessOn 免费版的文件数上限,WPS 更有优势,但是从 WPS 导出图片时会带有水印。如果是团队使用,ProcessOn 的多人协作看起来更好用。

Diagrams

diagrams 是一款免费开源且跨平台的绘图应用,支持离线绘图在线绘图。在线绘图和 ProcessOn 一样,在浏览器中操作即可。值得称赞的是 diagrams 提供了 macOSLinux(deb/rpm/snap/AppImage)和 Window 等主流操作系统的桌面应用,这意味着我们可以离线绘图。图 5 是 diagrams 官网给出的绘制成果图示。

图 5 diagrams 官网给出的绘制成果图示

diagrams 没有文件数量限制,它适配了 Google 云盘微软 OneDriveAtlassianDropboxGitHubNextCloud苹果的 iCloud 等云端存储,同时也支持将文件导出到本地,太棒了!

diagrams 支持自定义的导出缩放,允许我们在导出时配置图片缩放比例,例如 75% 或 300%。导出缩放能够保证图片的清晰度,是超清大图的保证。如果你要印刷设计出来的图,可以在导出时设置 DPI 的值,具体操作会在后面介绍到。

无论是 ProcessOn 还是 WPS,导出的图片(PNG/JPG)都不够清晰,WPS 甚至还带有水印。如果想要将 ProcessOn 上的绘图成果导出为超清图,那必须先导出为 SVG 格式,在此基础上通过其他应用转换为 PNG 格式,要求在转换时设定图片缩放比例。在导出图片文件的清晰度方面,diagrams 完胜。

我之前用过很长一段时间的 ProcessOn,《Python3 反爬虫原理与绕过实战》这本书创作的时候书中插图都是用在 ProcessOn 绘制的。偶然看到了 diagrams,体验过一次之后就全面转到 diagrams 了。

本篇文章将基于 diagrams 的桌面应用,请各位读者自行前往 diagrams 官网下载适合自己操作系统的桌面应用。

小知识:diagrams 还有另外一个域名,draw.io,当你看到其他地方介绍 draw 绘图工具时,大概率指的就是 diagrams。

布局概览

打开 diagrams 桌面应用,它会让我们选择创建新绘图或者打开现有绘图,悬浮窗右下角可以选择语言。

选择创建新绘图后跳转到如下图所示的画布类型选择界面,左侧是不同分类的画布模板菜单栏,右侧是对应分类的画布模板。通常我们会选择空白框图模板,如果你目标就选择自己喜欢的模板,点击右下角蓝色的创建按钮即可。

我选择的是地图分类里面的 mind map 模板,点击创建按钮后 diagrams 根据我的选择为我打开了如下图所示的新画布。

画布分为 ABCDEF 几个区域,它们对应的功能或者作用分别如下:

  • A - 菜单栏,可对文件或图形进行操作;
  • B - 工具栏,可对画布或者图形进行操作;
  • C - 图形栏,diagrams 为我们提供的基础图形元素和图形元素扩展能力;
  • D - 分页栏,可以在单个文件里添加多页画布;
  • E - 元素属性栏,可调整图形元素、画布或文本元素的属性,例如字体大小、图形颜色、网格显隐;
  • F - 画布区,主区域;
⚠️ diagrams 不会自动保存画布原文件,在创建文件后按 Ctrl+S 并根据提示选择存储方式和存储路径。选择好原文件的存储后,后续我们对画布、图形元素和文字元素的改动它才会自动保存。

添加多个页面

你可能会好奇,这个分页栏的应用场景是什么:

  • 每个图一份文件,太多了!
  • 一个功能模块(例如登录)对应的几个图(例如流程设计图、原型草稿、登录时序图),分开存放太难找!
  • 关联功能互相比对,打开多个窗口太烦人!

正好,D 区分页栏就是为了这些需求量身定制的。你可通过点击 D 区分页栏的 + 号来增加画布,也可以通过点击 D 区左侧的菜单按钮 + 插入页面 来增加画布。如果想要删除画布,那么菜单按钮 + 删除 就可以了。

你还可以为每页画布起名字,菜单按钮 + 重命名。这样在打开 diagrams 的时候就知道哪页画布上面画的是什么,不用一个个点击去看了。

添加更多图形

如果左侧提供的基础图形无法满足你的绘图需求,或者你想要从 diagrams 提供的图形中寻找灵感时,你可以点击 C 区底部的更多图形选项,点击后弹出如下图所示的图形选择界面。

diagrams 提供了如安卓组件图形、服务器组件图形、云原生图形、电路图形等数十类非常丰富的基础图形,找到喜欢的图形类别后给它打上勾,接着点击右下角的应用按钮即可。

绘图基本操作实践

要想使用 C 区的图形,我们只需要点击并将其拖拽到 F 区的画布上即可,想放到那个位置就在那个位置松开鼠标。下图是拖拽矩形到画布的截图,由于鼠标未松开,画布上只是显示虚线的矩形,当鼠标松开时显示的就是我们拖拽的左上角那个实线矩形。

如果你想在画布上再放置一个矩形,你可以再次拖拽,或者用快捷键 Ctrl+C 和 Ctrl+V 复制刚才那个矩形,并拖动到合适的位置。

如何表达流程

流程和方向在图形中是一种重要的表达方式,例如一个图形元素的箭头指向另一个图形元素。在 diagrams 中,我们只需要将鼠标移动到图形元素上,它就会出现如下图所示的蓝色锚点浅蓝色四向箭头

四向箭头表示这个元素可以向四个方向移动,蓝色的锚点则是此图形元素与其他元素关联时的起点或终点。在上图浅绿色圆圈盖住的蓝色点处单击鼠标并向右拖动,即可在此锚点上生长出一个直线箭头。将鼠标的焦点移动到右侧矩形上时,右侧矩形也会出现蓝色锚点和四向箭头,将鼠标焦点置于右侧矩形左边的锚点处释放,此时画布如下图所示。

这样我们就将两个不相关的图形元素关联到一起了,由于箭头的存在,这一组图形元素看起来像是一个短的流程。

如果你点击一个图形元素,它会在四周产生天蓝色的圆点,点击并拖动远点可改变图形元素的形状,例如矩形的宽和高。将鼠标焦点移动到浅蓝色四向箭头上时,箭头会变成蓝色,点击箭头便会复制一份相同的图形元素且以箭头进行关联,具体效果如下图所示。

NPC 提示:更多操作技巧和小知识可通过亲自动手获得哦。

文字与格式

文字描述本身就具备信息传递的属性,将它用在图形中会大大增加信息传递能力。以上图为基础,鼠标双击矩形便可出现文字输入框,在左边的矩形中输入客户端、右边的矩形中输入服务端,然后将 C 区的 Text 元素拖动到直线箭头上方并输入请求,此时画布如下图所示。

明眼人一看就知道描述的是双端网络交互的请求过程——客户端向服务端发出网络请求。我们可以通过 E 区的文本面板调整文字格式,例如字体、加粗、斜体、文字位置、文字书写方向、文字颜色、透明度等,还可以为文字加上背景色、设置边框颜色、调整文字间距和字体大小。

元素样式

文字有属性,图形元素也有属性。选中图形可通过 E 区的样式面板和调整图形面板更改图形元素的属性,例如调整图形元素的颜色、更改线条风格类型、替换箭头风格类型、为图形元素设置阴影、圆角直角切换、手绘风格切换和图形层级等。了解到这些知识后,我们可以绘制一个简单的流程图,通过 E 区的几个属性面板调整元素样式,具体效果如下图所示。

看上去是不是比之前的图好看多了?

元素层级与图层层级

在实际操作中你会发现,当图形元素重叠在一起的时候,先放置的图形元素总是处在后放置的图形元素下方,后放置的图形元素会挡住先放置的图形元素。

在同一个画布当中,图形元素是有层级的,跟 Photoshop 画布的层级相似,也跟我们编程领域中的栈相似。

如上图所示,浅蓝色、浅红色和浅绿色的图形元素堆叠在同一个位置。由于浅蓝色图形元素先放置,所以它在最底层。第二个放置的是浅红色的图形元素,它在中间层。最后放置的是浅绿色的图形元素,所以它在最上层。可以通过 E 区的调整图形面板改变图形元素的层级,或者直接在图形元素上右键唤出菜单栏,选择移至最前或则移至最后即可。下图描述了将浅绿色图形元素设置移动至最后属性的位置变化。

原来的浅绿色将处于最底层,中间层则是浅蓝色,之前在中间层的浅红色现在到了最上层。

diagrams 支持类似 Photoshop 的图层,在同一个画布当中可以有多个独立的图层。层与层之间有良好的隔离,我们可以将相关联的元素放到同一层,这样就可以得到像组合和锁一样的效果。我们可以在 B 区工具栏最左侧开启图层,开启后 F 区弹出图层设置面板,点击 + 号即可添加图层。下图展示了图层之间的关系和图层设置面板的一些功能。

我们可以为图层设定名称,这有助于我们清晰地梳理自己的构思。从图中可以看到正方体和矩形的位置是重叠的,但谁在上面谁在下面由图层决定,而不是像之前一样由拖放的先后顺序或者通过调整元素层级决定,即图层的层级优先级高于我们设置的元素层级

如果你想删除某个图层,只需要选中它,然后点击图层设置面板左下角的垃圾桶图标即可。

看到图层左侧的锁图标了吗?它为我们带来图层级别的锁,只要上了锁,我们就无法改变该图层任何元素的属性、样式、排列等,这跟我们后面提到的元素锁有很大的区别,图层锁的优先级高于元素锁

换个角度思考,层与层之间的隔离像是编程中类的单一原则,这样我们就可以用面向对象的思想进行构图设计。如果你有开发经验你就可以将我们常常听到的约定优于配置组合优于继承等思想应用到绘图这件事上。

导入图片

Photoshop 、WPS 流程图和 ProcessOn 都有图片导入的功能,功能齐备的 diagrams 自然也不逊色。点击 A 区菜单栏的调整图形菜单,在子面板中选择插入,再在子面板中选择图片即可进入图片选择环节,确定选择后图片将会被导入到 diagrams 画布中。

导入图片后我们可以根据需求或者喜好将图片与 diagrams 的元素结合起来。例如导入一段代码图示后,使用箭头和文字为图片中的代码添加注释,导出结果如下。

这样看起来,是不是比在浏览器中直接看代码漂亮多了?逻辑也清晰多了?

如果要考虑移动端的读者阅读文章,那么用图片代替代码是优选方式。你想想微信公众号推文场景,用户看图片代码会比看长串的代码舒服太多了。

文件导出

绘制完图之后点击 A 区菜单栏左上角的文件,然后选择子菜单中的导出为,再选择子菜单中的文件格式即可。根据你的需求,你可以导出 PNG 格式、SVG 格式、PDF 格式甚至是 XML 格式的文件,具体界面如下图所示。

当你选择好格式后,它还会弹出一些选项让你配置。例如选择图片格式时会弹出图片对应的配置窗口,我们可以设置图片缩放比例、图片边框宽度、是否将网格效果一并导出、是否将阴影效果一并导出导出当前文件的单页或指定页内容、是否采用透明背景等,具体界面如下图所示。

配置之丰富令人满意。另外,如果你绘制的图用于印刷,那么它也能满足你对高 DPI 的需求。导出时再子菜单中选择高级选项会弹出入下图所示的配置界面。

在这里我们就可以设置想要的 DPI 了。海报、宣传单和图书印刷的 DPI 值通常是 300,如果你追求更好的效果,它也允许你设置更大的值。

填写好配置后点击右下角的导出按钮,按照指引选择文件存放位置即可。值得一提的是,导出速度相当快,正常大小的图导出速度在 1 秒内,图形元素较多的图或者 DPI 值较大的图导出速度可能会超过 1 秒,但也慢不到哪里去。

绘图操作进阶

经过上面的学习,你已经掌握了 diagrams 桌面应用的基本操作,还懂得为元素设置属性和不同的样式。但要想轻松完成绘图且绘制出更好看的图,还需要学习一些操作技巧。

多元素自动对齐

画布没有自动定位和元素对齐功能,在放置多个元素后,我们很有可能需要将它们对齐,以保持美观和专业。常用的对齐有向上对齐、向下对齐、垂直居中、左对齐、右对齐和水平居中等。

见名知意,向上对齐、向下对齐和垂直居中指的是多个元素对齐时所有元素参照最上、最下或者中间元素的位置对齐,而左对齐、右对齐和水平居中则是所有元素参照最左、最右或者中间元素的位置对齐。下图描述了多个高度不一致的元素采用向上对齐的前后对比。

可以看到,对齐后的所有元素参照的是对齐前最上的位置。如果你对它的对齐逻辑还是不太理解,建议你亲自动手操作一下,聪明的你在几分钟就能够掌握对齐规则。

多元素等距排列

很多时候我们需要在水平方向或者垂直方向放置多个元素,本着专业且美观的出发点,我们会要求元素们等距排列。这样的需求通常有两种办法:

1、借助页面网格可以做到视觉上的等距,例如元素之间间隔 1 个网格;

2、借助菜单栏中的等距分布选项;

在多个元素相同且大小一致的时候,采用第 1 种方法也是很直接有效的,但如果元素大小不一或者形状各不相同,那就不太好操作了。第 2 种方法是借助 diagrams 专门为用户提供的等距排列工具,只需要选中想要排列的多个元素,然后点击菜单栏中的调整图形并在子菜单中选择等距分布,然后按需选择水平方向或者垂直方向即可。第 2 种方法可以忽略元素大小和形状,达到等距排列的目的。多元素水平方向等距排列的具体操作和排列结果如下图所示。

要注意的是,等距分布功能选择的间隔距离是以多个元素占用的总距离进行平均划分的,并不是根据两个元素之间的最小距离或者最大距离进行划分。

多元素组合

在画布中,可操作的最小单位是 1 个元素。如果我们想要操作多个元素,那么就需要选中它们,再执行例如拖拽、改变大小、改变属性等操作。但是在元素繁多且结构复杂的画布中,当你需要将相关联的多个元素拖动到另一个位置的时候,很容易误选其他元素或者漏掉某个元素,这给我们造成了一些小麻烦。

选中想要组合的元素后点击菜单栏的调整图形选项,在弹出的子菜单中选择组合即可。上图给出了组合前后的对比,可以看出组合前的可操作单位是单个元素,组合后可操作单位变成了一组元素。如果你想解散某个组合,那么选择组合后通过菜单栏中的调整图形>取消组合就可以将它们打散。

看到子菜单中的移除组合了吗?动手试试,看看它有什么作用。

多元素旋转

单个元素的旋转很简单,选中元素后元素的右上角会出现一个可旋转的图标,鼠标悬停在旋转图标后按下并调整角度便可实现元素的旋转。多个元素的旋转有两种办法:

1、元素组合,将多个元素变成一个可操作单位,此时一组元素和单个元素的旋转操作相同;

2、选中多个元素,通过调整图形>方向>旋转调整角度,可输入准确的旋转角度值;

两种方法的应用场景各不相同,大家使用的时候根据需求选择即可。下图描述了第 2 种方法的具体操作和旋转结果。

旋转选项在 A 区菜单栏的调整图形>方向菜单中。

元素锁

锁在编程中有很多应用场景,在 diagrams 中我们可以借助锁这个工具保护元素或组合,避免在误操作等场景改变原有的元素或组合的内容、排列方式和所在画布的位置等。元素的上锁解锁在 A 区菜单栏的编辑菜单下,锁定和解锁处于同一个选项。下图描述了元素上锁前后的差异。

锁定前元素具备可操作属性,选中后可以改变其样式、大小等。锁定后元素不具备可操作属性,我们无法调整内容、排列方式和所在画布的位置,也没法删除这个元素或者组合。要注意的是,上锁后仍然可以改变单个元素的样式,例如颜色和线条类型。

在画布中查找

在一个原文件中可能有多个元素或者多组元素,查找功能可以帮助我们快速定位到指定的元素或组合。查找的对象只能是文字元素,无法查找图形元素。查找功能在 A 区的编辑菜单下,选择查找功能后会在 F 区弹出一个查找选项窗口,我们可以根据自己的需求设定查找条件。值得一提的是,它支持所有页面的查找,这简直太棒了!下图描述了查找功能的使用和查找结果的展示。

查找到相关文字后会定位到该元素或组合,无需我们手动定位。在有多个符合条件的场景中,可以通过查找按钮在多个相关结果之间跳转。

配色和风格让图形更具表现力

人靠衣装马靠鞍,优秀的配色和风格选择不仅能增加图的表现力,还能让你和你的团队看起来更专业。逼格高对于业务发展和团队形象也是很有帮助的,如果你画的图一团糟,而竞争对手画的图却显得很专业,别人能比你多收几万块不是没有道理的。

能改变映像的手绘风格

相比于工具自带的那些直来直去的图形元素来说,手绘风格给人一种亲近、舒适、用心的感觉,从下面这张图里感受一下。不得不说,手绘风格配网格底纹就像火锅配牛肚,舒坦!

由于它具备这些贴近阅读者的属性,所以手绘风格成为了现在 IT 领域知识付费专栏或者教学素材中常用的风格之一,在极客时间专栏中得以大量应用,在 IT 类的书籍中也频频登场。

在 diagrams 实现手绘风格很简单,选中元素后在右侧 E 区的样式面板中勾选手绘即可。要注意的是,有一部分基础图形是不支持手绘风格的,所以在绘图时要选择支持手绘风格的图形元素,这样就能够保持整体风格统一。

能增强效果的阴影

阴影一直都是图像设计中重要的一种效果增强方。我不是专业的设计人员,因此无法给出专业的建议或者看法,但我可以从视觉的角度描述阴影的作用和它所呈现出来的效果。

上图由一列手机图形元素和一个显示器图形元素构成,左侧 5 部手机图形和第 6 部带有阴影和配色的手机看起来是不是完全不一样,有没有一种跃然于纸上的感觉?这很像现代 IPS 硬屏显示器和前些年普通显示器的差距,你感觉它浮在上面,离你很近。

最右侧的显示器图形也设置了阴影,这让它看上去更立体,像不像是 2.5D 的图形?

光和影在设计领域一直都是相当重要的,利用好光和影能够让事物更有活力、更形象、更立体。上面的显示器图形只是光和影运用中最简单的一种,我们抛开开发者的身份,欣赏一下设计师们的作品。

虽然这些都是平面设计,但在视觉上它们却是立体的,而且还具备很强的空间感。这是一种普通开发者难以描述的美感,真的很好看,不是吗?

配色知识

说到图,那就一定得聊聊配色。我觉得色彩是人类感知世界多元化的一种途径,好的色彩搭配会让我们觉得心旷神怡,但糟糕的配色会引起我们的反感。下面两幅图,一副用了整体色调相近的浅色,一副用了整体色调差异较大的深浅混搭,你觉得哪幅图看着舒服一些?

虽然这会受到个人倾向和风格影响,但我觉得大部分读者会认为第一幅图看上去更清爽舒服,第二幅图浓墨重彩不太搭。跟上面聊阴影是一样的,我不是专业的设计师,无法给出专业的建议和看法,但这里准备了一些设计网站上关于配色的文章,大家可作为参考:

涨姿势!为什么有的颜色看起来非常高档?

设计师的读书笔记!带你重新全面认识色彩系列之红色篇

所谓配色达人,就是一次出48个方案

那些用色高手,都做过这些训练!

你的配色看起来总是不舒服?用这个策略性配色法则

里面提到了很多关于色彩搭配的技巧和策略,大家不妨去学习学习,这样你就能绘制出更美的图,而且对自己的审美提升也会有很大帮助哦。文章中提到的一些配色和策略截图如下,具体还需你亲自去阅读哦。

配色工具

相信你从上面推荐的配色文章中收获了不少知识,在实际应用中我们还需要一些辅助工具来提升设计效率或者进行一些配色测试、配色比对。你看看人家设计的图,和你设计的是不是有很大差异?

如果是的话,赶紧去学习吧!我的建议还是向专业人士看齐

讲真,这可能是目前最全的在线配色工具合集了

业界良心!2015年最优秀的20款网站配色方案(附配色工具)

第一波!2020年3月超实用设计工具大合集

16款激发灵感的配色神器,让你的创意更出彩!

9款关于配色、PS和网站风格的设计工具,超级实用

绘图实战

能看到这里的读者,我想肯定是希望能够切实提高自己的绘图水平的,并不是走马观花的泛泛之辈。接下来我将以演进的方式手把手带你绘制开发过程中常用的那些图,如果你想深入学习 UML 图的绘制,可以访问我的个人站点——夜幕镇岳剑韦世东的技术日志,上面还有很多免费公开优秀技术专栏呢。

绘制汽车解锁时序图

时序图是我们在程序设计阶段常用的一种 UML 交互图,它通过描述对象之间发送消息的时间顺序显示多个对象之间的动态协作。它可以表示用例的行为顺序,当执行一个用例行为时,其中的每条消息对应一个类操作或状态机中引起转换的触发事件。

这里以汽车钥匙的上锁和解锁过程为例,绘制一幅时序图。首先捋清楚参与其中的几个角色:钥匙拥有者、钥匙、车,由此我们可以画出如下图所示的基本结构。

然后我们捋一捋上锁和解锁的过程,钥匙拥有者按下钥匙上的按键,车钥匙通过信号将锁信号传递给汽车,汽车根据锁的原状态作对应状态切换,从而实现上锁和解锁的目的。下图描述了从钥匙拥有者按下锁按键到汽车改变锁状态的时序。

在实际生活中,我们知道汽车锁状态改变时会发出信号以表示这次操作成功了,类似于 TCP 协议中的 ACK。汽车发出的信号通常是灯光或声音,信号的实际接收者是按下锁按键的钥匙拥有者,由此我们可以再完善一下这幅时序图。

上图就是完整的汽车上锁和解锁时序图,你学会了吗?

小提示:在表达时序图角色时,冒号左边描述的是角色名称、右侧描述的是角色,如果冒号左边没有内容则代表这是一个匿名角色。如果想要更具体一些,可以改为“韦世东:钥匙拥有者”、“奔驰:汽车”。

绘制流式处理流程图

配色知识那个小节中用到的图示描述的就是流式处理,在 Kafka 流式处理的思想中(我没看过 Spark 和 Flink 的模型),流式处理是一端作为起始输入、一端作为最终输出、中间可以有多个处理环节的一种组合。由此我们可以画出如下图所示的基本结构。

然后在中间放置多个处理环节,这些处理环节实际上就是一个个程序,它们从 Kafka 中获取数据,处理完成后又放回去。

上图就是完整的流式处理流程图,你学会了吗?

绘制移动端应用分层架构图

相对于单个模块的时序图和简单环节的流程图而言,Web 应用分层架构更复杂,需要的图形也更多。假设我们要用图表达一款移动端应用的架构图,要清晰有条理地展现出整个系统的最大执行单元之间的关系,你会怎么做呢?

在已知需求和功能模块的情况下,我们要做的第一件事就是分层。分层不仅能在绘图中发挥重要作用,在软件开发和设计中也是十分重要的,它能够让执行单元之间的责任更清晰明确。我们先来简单粗略的划分一下:

  1. 能为用户提供服务的视为应用层;
  2. 负责存储应用数据的视为存储层;

由此我们可以画出如下图所示的基本结构。

再来看看实际开发中是否需要加入其他组件,例如提高查询性能的缓存削峰填谷的消息队列、负责请求转发的网关等,加上这些组件后图就变成了下面这样。

这里你不必深究每个层级的具体划分和层级名称,我们先把结构画出来。想一想,好像还需要加上监控。监控对整个工程链路负责,它是一套单独的组件,不参与分层,放到侧面表示会更合适。后端的整个结构就出来了,这里我们顺便把用户端也补上。

接下来我们将具体的应用补充进来,让整个程序看起来更丰满、更完整。

接着让我们表达出更贴近真实场景的效果,假设服务基于 Kubernetes、Redis 采用集群和、MySQL 也采用集群,那么我们可以为它们加上贴切的图标。

一些漂亮的图形可以在 C 区更多图形处添加,上图的手机图形也是这样添加进来的。

这样看上去清晰多了,当然,我们还可以为这些层级或者组件设置底色,用配色增强它们的表现力。

小结

看完这篇文章,想必你已经清晰地了解到绘图这件事在程序设计你的工程师职业生涯中能发挥出的作用有多重要了。

我们现在再回顾一下前面所学到的知识:

在学习完绘图基本操作之后,你学了自动排列、多元素组合、自动对齐和元素锁等知识,现在你的绘图效率比之前高很多。

接着,你又了解到了风格对读者的影响,并学会了使用手绘风格。在配色方面,你去阅读了设计领域的一些专业文章,了解到不同场景、不同配色带来的视觉差异,这直接提升了你绘图的逼格。

最后,你跟着实际需求的分析和假设,动手实践绘制了汽车解锁时序图、流式处理流程图和移动端应用分层架构图。

现在你已经拥有了自己的绘图思想,并掌握了绘图操作。接下来只需要多加练习,将这些技能融入到日常工作当中,相信你一定能够取得更好的成绩,设计出更贴近业务、更美观的图。


查看原文

abcbuzhiming 收藏了文章 · 7月26日

「1.4万字」玩转前端 Video 播放器 | 多图预警

Web 开发者们一直以来想在 Web 中使用音频和视频,但早些时候,传统的 Web 技术不能够在 Web 中嵌入音频和视频,所以一些像 Flash、Silverlight 的专利技术在处理这些内容上变得很受欢迎。这些技术能够正常的工作,但是却有着一系列的问题,包括无法很好的支持 HTML/CSS 特性、安全问题,以及可行性问题。

幸运的是,当 HTML5 标准公布后,其中包含许多的新特性,包括 <video><audio> 标签,以及一些 JavaScript APIs 用于对其进行控制。随着通信技术和网络技术的不断发展,目前音视频已经成为大家生活中不可或缺的一部分。此外,伴随着 5G 技术的慢慢普及,实时音视频领域还会有更大的想象空间。

接下来本文将从八个方面入手,全方位带你一起探索前端 Video 播放器和主流的流媒体技术。阅读完本文后,你将了解以下内容:

  • 为什么一些网页中的 Video 元素,其视频源地址是采用 Blob URL 的形式;
  • 什么是 HTTP Range 请求及流媒体技术相关概念;
  • 了解 HLS、DASH 的概念、自适应比特率流技术及流媒体加密技术;
  • 了解 FLV 文件结构、flv.js 的功能特性与使用限制及内部的工作原理;
  • 了解 MSE(Media Source Extensions)API 及相关的使用;
  • 了解视频播放器的原理、多媒体封装格式及 MP4 与 Fragmented MP4 封装格式的区别;

在最后的 阿宝哥有话说 环节,阿宝哥将介绍如何实现播放器截图、如何基于截图生成 GIF、如何使用 Canvas 播放视频及如何实现色度键控等功能。

一、传统的播放模式

大多数 Web 开发者对 <video> 都不会陌生,在以下 HTML 片段中,我们声明了一个 <video> 元素并设置相关的属性,然后通过 <source> 标签设置视频源和视频格式:

<video id="mse" autoplay=true playsinline controls="controls">
   <source data-original="https://h5player.bytedance.com/video/mp4/xgplayer-demo-720p.mp4" type="video/mp4">
   你的浏览器不支持Video标签
</video>

上述代码在浏览器渲染之后,在页面中会显示一个 Video 视频播放器,具体如下图所示:

(图片来源:https://h5player.bytedance.co...

通过 Chrome 开发者工具,我们可以知道当播放 xgplayer-demo-720p.mp4 视频文件时,发了 3 个 HTTP 请求:

此外,从图中可以清楚地看到,头两个 HTTP 请求响应的状态码是 206。这里我们来分析第一个 HTTP 请求的请求头和响应头:

在上面的请求头中,有一个 range: bytes=0- 首部信息,该信息用于检测服务端是否支持 Range 请求。如果在响应中存在 Accept-Ranges 首部(并且它的值不为 “none”),那么表示该服务器支持范围请求。

在上面的响应头中, Accept-Ranges: bytes 表示界定范围的单位是 bytes 。这里 Content-Length 也是有效信息,因为它提供了要下载的视频的完整大小。

1.1 从服务器端请求特定的范围

假如服务器支持范围请求的话,你可以使用 Range 首部来生成该类请求。该首部指示服务器应该返回文件的哪一或哪几部分。

1.1.1 单一范围

我们可以请求资源的某一部分。这里我们使用 Visual Studio Code 中的 REST Client 扩展来进行测试,在这个例子中,我们使用 Range 首部来请求 www.example.com 首页的前 1024 个字节。

对于使用 REST Client 发起的 单一范围请求,服务器端会返回状态码为 206 Partial Content 的响应。而响应头中的 Content-Length 首部现在用来表示先前请求范围的大小(而不是整个文件的大小)。Content-Range 响应首部则表示这一部分内容在整个资源中所处的位置。

1.1.2 多重范围

Range 头部也支持一次请求文档的多个部分。请求范围用一个逗号分隔开。比如:

$ curl http://www.example.com -i -H "Range: bytes=0-50, 100-150"

对于该请求会返回以下响应信息:

因为我们是请求文档的多个部分,所以每个部分都会拥有独立的 Content-TypeContent-Range 信息,并且使用 boundary 参数对响应体进行划分。

1.1.3 条件式范围请求

当重新开始请求更多资源片段的时候,必须确保自从上一个片段被接收之后该资源没有进行过修改。

If-Range 请求首部可以用来生成条件式范围请求:假如条件满足的话,条件请求就会生效,服务器会返回状态码为 206 Partial 的响应,以及相应的消息主体。假如条件未能得到满足,那么就会返回状态码为 200 OK 的响应,同时返回整个资源。该首部可以与 Last-Modified 验证器或者 ETag 一起使用,但是二者不能同时使用。

1.1.4 范围请求的响应

与范围请求相关的有三种状态:

  • 在请求成功的情况下,服务器会返回 206 Partial Content 状态码。
  • 在请求的范围越界的情况下(范围值超过了资源的大小),服务器会返回 416 Requested Range Not Satisfiable (请求的范围无法满足) 状态码。
  • 在不支持范围请求的情况下,服务器会返回 200 OK 状态码。

剩余的两个请求,阿宝哥就不再详细分析了。感兴趣的小伙伴,可以使用 Chrome 开发者工具查看一下具体的请求报文。通过第 3 个请求,我们可以知道整个视频的大小大约为 7.9 MB。若播放的视频文件太大或出现网络不稳定,则会导致播放时,需要等待较长的时间,这严重降低了用户体验。

那么如何解决这个问题呢?要解决该问题我们可以使用流媒体技术,接下来我们来介绍流媒体。

二、流媒体

流媒体是指将一连串的媒体数据压缩后,经过网上分段发送数据,在网上即时传输影音以供观赏的一种技术与过程,此技术使得数据包得以像流水一样发送;如果不使用此技术,就必须在使用前下载整个媒体文件。

流媒体实际指的是一种新的媒体传送方式,有声音流、视频流、文本流、图像流、动画流等,而非一种新的媒体。流媒体最主要的技术特征就是流式传输,它使得数据可以像流水一样传输。流式传输是指通过网络传送媒体技术的总称。实现流式传输主要有两种方式:顺序流式传输(Progressive Streaming)和实时流式传输(Real Time Streaming)。

目前网络上常见的流媒体协议:

通过上表可知,不同的协议有着不同的优缺点。在实际使用过程中,我们通常会在平台兼容的条件下选用最优的流媒体传输协议。比如,在浏览器里做直播,选用 HTTP-FLV 协议是不错的,性能优于 RTMP+Flash,延迟可以做到和 RTMP+Flash 一样甚至更好。

而由于 HLS 延迟较大,一般只适合视频点播的场景,但由于它在移动端拥有较好的兼容性,所以在接受高延迟的条件下,也是可以应用在直播场景。

讲到这里相信有些小伙伴会好奇,对于 Video 元素来说使用流媒体技术之后与传统的播放模式有什么直观的区别。下面阿宝哥以常见的 HLS 流媒体协议为例,来简单对比一下它们之间的区别。

通过观察上图,我们可以很明显地看到,当使用 HLS 流媒体网络传输协议时,<video> 元素 src 属性使用的是 blob:// 协议。讲到该协议,我们就不得不聊一下 Blob 与 Blob URL。

2.1 Blob

Blob(Binary Large Object)表示二进制类型的大对象。在数据库管理系统中,将二进制数据存储为一个单一个体的集合。Blob 通常是影像、声音或多媒体文件。在 JavaScript 中 Blob 类型的对象表示不可变的类似文件对象的原始数据。

Blob 由一个可选的字符串 type(通常是 MIME 类型)和 blobParts 组成:

MIME(Multipurpose Internet Mail Extensions)多用途互联网邮件扩展类型,是设定某种扩展名的文件用一种应用程序来打开的方式类型,当该扩展名文件被访问的时候,浏览器会自动使用指定应用程序来打开。多用于指定一些客户端自定义的文件名,以及一些媒体文件打开方式。

常见的 MIME 类型有:超文本标记语言文本 .html text/html、PNG图像 .png image/png、普通文本 .txt text/plain 等。

为了更直观的感受 Blob 对象,我们先来使用 Blob 构造函数,创建一个 myBlob 对象,具体如下图所示:

如你所见,myBlob 对象含有两个属性:size 和 type。其中 size 属性用于表示数据的大小(以字节为单位),type 是 MIME 类型的字符串。Blob 表示的不一定是 JavaScript 原生格式的数据。比如 File 接口基于 Blob,继承了 blob 的功能并将其扩展使其支持用户系统上的文件。

2.2 Blob URL/Object URL

Blob URL/Object URL 是一种伪协议,允许 Blob 和 File 对象用作图像,下载二进制数据链接等的 URL 源。在浏览器中,我们使用 URL.createObjectURL 方法来创建 Blob URL,该方法接收一个 Blob 对象,并为其创建一个唯一的 URL,其形式为 blob:<origin>/<uuid>,对应的示例如下:

blob:https://example.org/40a5fb5a-d56d-4a33-b4e2-0acf6a8e5f641

浏览器内部为每个通过 URL.createObjectURL 生成的 URL 存储了一个 URL → Blob 映射。因此,此类 URL 较短,但可以访问 Blob。生成的 URL 仅在当前文档打开的状态下才有效。但如果你访问的 Blob URL 不再存在,则会从浏览器中收到 404 错误。

上述的 Blob URL 看似很不错,但实际上它也有副作用。虽然存储了 URL → Blob 的映射,但 Blob 本身仍驻留在内存中,浏览器无法释放它。映射在文档卸载时自动清除,因此 Blob 对象随后被释放。但是,如果应用程序寿命很长,那不会很快发生。因此,如果我们创建一个 Blob URL,即使不再需要该 Blob,它也会存在内存中。

针对这个问题,我们可以调用 URL.revokeObjectURL(url) 方法,从内部映射中删除引用,从而允许删除 Blob(如果没有其他引用),并释放内存。

2.3 Blob vs ArrayBuffer

其实在前端除了 Blob 对象 之外,你还可能会遇到 ArrayBuffer 对象。它用于表示通用的,固定长度的原始二进制数据缓冲区。你不能直接操纵 ArrayBuffer 的内容,而是需要创建一个 TypedArray 对象或 DataView 对象,该对象以特定格式表示缓冲区,并使用该对象读取和写入缓冲区的内容。

Blob 对象与 ArrayBuffer 对象拥有各自的特点,它们之间的区别如下:

  • 除非你需要使用 ArrayBuffer 提供的写入/编辑的能力,否则 Blob 格式可能是最好的。
  • Blob 对象是不可变的,而 ArrayBuffer 是可以通过 TypedArrays 或 DataView 来操作。
  • ArrayBuffer 是存在内存中的,可以直接操作。而 Blob 可以位于磁盘、高速缓存内存和其他不可用的位置。
  • 虽然 Blob 可以直接作为参数传递给其他函数,比如 window.URL.createObjectURL()。但是,你可能仍需要 FileReader 之类的 File API 才能与 Blob 一起使用。
  • Blob 与 ArrayBuffer 对象之间是可以相互转化的:

    • 使用 FileReader 的 readAsArrayBuffer() 方法,可以把 Blob 对象转换为 ArrayBuffer 对象;
    • 使用 Blob 构造函数,如 new Blob([new Uint8Array(data]);,可以把 ArrayBuffer 对象转换为 Blob 对象。

在前端 AJAX 场景下,除了常见的 JSON 格式之外,我们也可能会用到 Blob 或 ArrayBuffer 对象:

function GET(url, callback) {
  let xhr = new XMLHttpRequest();
  xhr.open('GET', url, true);
  xhr.responseType = 'arraybuffer'; // or xhr.responseType = "blob";
  xhr.send();

  xhr.onload = function(e) {
    if (xhr.status != 200) {
      alert("Unexpected status code " + xhr.status + " for " + url);
      return false;
    }
    callback(new Uint8Array(xhr.response)); // or new Blob([xhr.response]);
  };
}

在以上示例中,通过为 xhr.responseType 设置不同的数据类型,我们就可以根据实际需要获取对应类型的数据了。介绍完上述内容,下面我们先来介绍目前应用比较广泛的 HLS 流媒体传输协议。

三、HLS

3.1 HLS 简介

HTTP Live Streaming(缩写是 HLS)是由苹果公司提出基于 HTTP 的流媒体网络传输协议,它是苹果公司 QuickTime X 和 iPhone 软件系统的一部分。它的工作原理是把整个流分成一个个小的基于 HTTP 的文件来下载,每次只下载一些。当媒体流正在播放时,客户端可以选择从许多不同的备用源中以不同的速率下载同样的资源,允许流媒体会话适应不同的数据速率。

此外,当用户的信号强度发生抖动时,视频流会动态调整以提供出色的再现效果。

(图片来源:https://www.wowza.com/blog/hl...

最初, 仅 iOS 支持 HLS。但现在 HLS 已成为专有格式,几乎所有设备都支持它。顾名思义,HLS(HTTP Live Streaming)协议通过标准的 HTTP Web 服务器传送视频内容。这意味着你无需集成任何特殊的基础架构即可分发 HLS 内容。

HLS 拥有以下特性:

  • HLS 将播放使用 H.264 或 HEVC / H.265 编解码器编码的视频。
  • HLS 将播放使用 AAC 或 MP3 编解码器编码的音频。
  • HLS 视频流一般被切成 10 秒的片段。
  • HLS 的传输/封装格式是 MPEG-2 TS。
  • HLS 支持 DRM(数字版权管理)。
  • HLS 支持各种广告标准,例如 VAST 和 VPAID。

为什么苹果要提出 HLS 这个协议,其实它的主要是为了解决 RTMP 协议存在的一些问题。比如 RTMP 协议不使用标准的 HTTP 接口传输数据,所以在一些特殊的网络环境下可能被防火墙屏蔽掉。但是 HLS 由于使用的 HTTP 协议传输数据,通常情况下不会遇到被防火墙屏蔽的情况。除此之外,它也很容易通过 CDN(内容分发网络)来传输媒体流。

3.2 HLS 自适应比特流

HLS 是一种自适应比特率流协议。因此,HLS 流可以动态地使视频分辨率自适应每个人的网络状况。如果你正在使用高速 WiFi,则可以在手机上流式传输高清视频。但是,如果你在有限数据连接的公共汽车或地铁上,则可以以较低的分辨率观看相同的视频。

在开始一个流媒体会话时,客户端会下载一个包含元数据的 Extended M3U(m3u8)Playlist 文件,用于寻找可用的媒体流。

(图片来源:https://www.wowza.com/blog/hl...

为了便于大家的理解,我们使用 hls.js 这个 JavaScript 实现的 HLS 客户端,所提供的 在线示例,来看一下具体的 m3u8 文件。

x36xhzz.m3u8

#EXTM3U
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2149280,CODECS="mp4a.40.2,avc1.64001f",RESOLUTION=1280x720,NAME="720"
url_0/193039199_mp4_h264_aac_hd_7.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=246440,CODECS="mp4a.40.5,avc1.42000d",RESOLUTION=320x184,NAME="240"
url_2/193039199_mp4_h264_aac_ld_7.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=460560,CODECS="mp4a.40.5,avc1.420016",RESOLUTION=512x288,NAME="380"
url_4/193039199_mp4_h264_aac_7.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=836280,CODECS="mp4a.40.2,avc1.64001f",RESOLUTION=848x480,NAME="480"
url_6/193039199_mp4_h264_aac_hq_7.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=6221600,CODECS="mp4a.40.2,avc1.640028",RESOLUTION=1920x1080,NAME="1080"
url_8/193039199_mp4_h264_aac_fhd_7.m3u8

通过观察 Master Playlist 对应的 m3u8 文件,我们可以知道该视频支持以下 5 种不同清晰度的视频:

  • 1920x1080(1080P)
  • 1280x720(720P)
  • 848x480(480P)
  • 512x288
  • 320x184

而不同清晰度视频对应的媒体播放列表,会定义在各自的 m3u8 文件中。这里我们以 720P 的视频为例,来查看其对应的 m3u8 文件:

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-TARGETDURATION:11
#EXTINF:10.000,
url_462/193039199_mp4_h264_aac_hd_7.ts
#EXTINF:10.000,
url_463/193039199_mp4_h264_aac_hd_7.ts
#EXTINF:10.000,
url_464/193039199_mp4_h264_aac_hd_7.ts
#EXTINF:10.000,
...
url_525/193039199_mp4_h264_aac_hd_7.ts
#EXT-X-ENDLIST

当用户选定某种清晰度的视频之后,将会下载该清晰度对应的媒体播放列表(m3u8 文件),该列表中就会列出每个片段的信息。HLS 的传输/封装格式是 MPEG-2 TS(MPEG-2 Transport Stream),是一种传输和存储包含视频、音频与通信协议各种数据的标准格式,用于数字电视广播系统,如 DVB、ATSC、IPTV 等等。

需要注意的是利用一些现成的工具,我们是可以把多个 TS 文件合并为 mp4 格式的视频文件。 如果要做视频版权保护,那我们可以考虑使用对称加密算法,比如 AES-128 对切片进行对称加密。当客户端进行播放时,先根据 m3u8 文件中配置的密钥服务器地址,获取对称加密的密钥,然后再下载分片,当分片下载完成后再使用匹配的对称加密算法进行解密播放。

对上述过程感兴趣的小伙伴可以参考 Github 上 video-hls-encrypt 这个项目,该项目深入浅出介绍了基于 HLS 流媒体协议视频加密的解决方案并提供了完整的示例代码。

(图片来源:https://github.com/hauk0101/v...

介绍完苹果公司推出的 HLS (HTTP Live Streaming)技术,接下来我们来介绍另一种基于 HTTP 的动态自适应流 —— DASH。

四、DASH

4.1 DASH 简介

基于 HTTP 的动态自适应流(英语:Dynamic Adaptive Streaming over HTTP,缩写 DASH,也称 MPEG-DASH)是一种自适应比特率流技术,使高质量流媒体可以通过传统的 HTTP 网络服务器以互联网传递。 类似苹果公司的 HTTP Live Streaming(HLS)方案,MPEG-DASH 会将内容分解成一系列小型的基于 HTTP 的文件片段,每个片段包含很短长度的可播放内容,而内容总长度可能长达数小时。

内容将被制成多种比特率的备选片段,以提供多种比特率的版本供选用。当内容被 MPEG-DASH 客户端回放时,客户端将根据当前网络条件自动选择下载和播放哪一个备选方案。客户端将选择可及时下载的最高比特率片段进行播放,从而避免播放卡顿或重新缓冲事件。也因如此,MPEG-DASH 客户端可以无缝适应不断变化的网络条件并提供高质量的播放体验,拥有更少的卡顿与重新缓冲发生率。

MPEG-DASH 是首个基于 HTTP 的自适应比特率流解决方案,它也是一项国际标准。MPEG-DASH 不应该与传输协议混淆 —— MPEG-DASH 使用 TCP 传输协议。不同于 HLS、HDS 和 Smooth Streaming,DASH 不关心编解码器,因此它可以接受任何编码格式编码的内容,如 H.265、H.264、VP9 等。

虽然 HTML5 不直接支持 MPEG-DASH,但是已有一些 MPEG-DASH 的 JavaScript 实现允许在网页浏览器中通过 HTML5 Media Source Extensions(MSE)使用 MPEG-DASH。另有其他 JavaScript 实现,如 bitdash 播放器支持使用 HTML5 加密媒体扩展播放有 DRM 的MPEG-DASH。当与 WebGL 结合使用,MPEG-DASH 基于 HTML5 的自适应比特率流还可实现 360° 视频的实时和按需的高效流式传输。

4.2 DASH 重要概念

  • MPD:媒体文件的描述文件(manifest),作用类似 HLS 的 m3u8 文件。
  • Representation:对应一个可选择的输出(alternative)。如 480p 视频,720p 视频,44100 采样音频等都使用 Representation 描述。
  • Segment(分片):每个 Representation 会划分为多个 Segment。Segment 分为 4 类,其中,最重要的是:Initialization Segment(每个 Representation 都包含 1 个 Init Segment),Media Segment(每个 Representation 的媒体内容包含若干 Media Segment)。

(图片来源:https://blog.csdn.net/yue_hua...

在国内 Bilibili 于 2018 年开始使用 DASH 技术,至于为什么选择 DASH 技术。感兴趣的小伙伴可以阅读 我们为什么使用DASH 这篇文章。

讲了那么多,相信有些小伙伴会好奇 MPD 文件长什么样?这里我们来看一下西瓜视频播放器 DASH 示例中的 MPD 文件:

<?xml version="1.0"?>
<!-- MPD file Generated with GPAC version 0.7.2-DEV-rev559-g61a50f45-master  at 2018-06-11T11:40:23.972Z-->
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" minBufferTime="PT1.500S" type="static" mediaPresentationDuration="PT0H1M30.080S" maxSegmentDuration="PT0H0M1.000S" profiles="urn:mpeg:dash:profile:full:2011">
 <ProgramInformation moreInformationURL="http://gpac.io">
  <Title>xgplayer-demo_dash.mpd generated by GPAC</Title>
 </ProgramInformation>

 <Period duration="PT0H1M30.080S">
  <AdaptationSet segmentAlignment="true" maxWidth="1280" maxHeight="720" maxFrameRate="25" par="16:9" lang="eng">
   <ContentComponent id="1" contentType="audio" />
   <ContentComponent id="2" contentType="video" />
   <Representation id="1" mimeType="video/mp4" codecs="mp4a.40.2,avc3.4D4020" width="1280" height="720" frameRate="25" sar="1:1" startWithSAP="0" bandwidth="6046495">
    <AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"/>
    <BaseURL>xgplayer-demo_dashinit.mp4</BaseURL>
    <SegmentList timescale="1000" duration="1000">
     <Initialization range="0-1256"/>
      <SegmentURL mediaRange="1257-1006330" indexRange="1257-1300"/>
      <SegmentURL mediaRange="1006331-1909476" indexRange="1006331-1006374"/>
      ...
      <SegmentURL mediaRange="68082016-68083543" indexRange="68082016-68082059"/>
    </SegmentList>
   </Representation>
  </AdaptationSet>
 </Period>
</MPD>

(文件来源:https://h5player.bytedance.co...

在播放视频时,西瓜视频播放器会根据 MPD 文件,自动请求对应的分片进行播放。

前面我们已经提到了 Bilibili,接下来不得不提其开源的一个著名的开源项目 —— flv.js,不过在介绍它之前我们需要来了解一下 FLV 流媒体格式。

五、FLV

5.1 FLV 文件结构

FLV 是 FLASH Video 的简称,FLV 流媒体格式是随着 Flash MX 的推出发展而来的视频格式。由于它形成的文件极小、加载速度极快,使得网络观看视频文件成为可能,它的出现有效地解决了视频文件导入 Flash 后,使导出的 SWF 文件体积庞大,不能在网络上很好的使用等问题。

FLV 文件由 FLV Header 和 FLV Body 两部分构成,而 FLV Body 由一系列的 Tag 构成:

5.1.1 FLV 头文件

FLV 头文件:(9 字节)

  • 1-3:前 3 个字节是文件格式标识(FLV 0x46 0x4C 0x56)。
  • 4-4:第 4 个字节是版本(0x01)。
  • 5-5:第 5 个字节的前 5 个 bit 是保留的必须是 0。

    • 第 5 个字节的第 6 个 bit 音频类型标志(TypeFlagsAudio)。
    • 第 5 个字节的第 7 个 bit 也是保留的必须是 0。
    • 第5个字节的第8个bit视频类型标志(TypeFlagsVideo)。
  • 6-9: 第 6-9 的四个字节还是保留的,其数据为 00000009。
  • 整个文件头的长度,一般是 9(3+1+1+4)。
5.1.2 tag 基本格式

tag 类型信息,固定长度为 15 字节:

  • 1-4:前一个 tag 长度(4字节),第一个 tag 就是 0。
  • 5-5:tag 类型(1 字节);0x8 音频;0x9 视频;0x12 脚本数据。
  • 6-8:tag 内容大小(3 字节)。
  • 9-11:时间戳(3 字节,毫秒)(第 1 个 tag 的时候总是为 0,如果是脚本 tag 就是 0)。
  • 12-12:时间戳扩展(1 字节)让时间戳变成 4 字节(以存储更长时间的 flv 时间信息),本字节作为时间戳的最高位。

在 flv 回放过程中,播放顺序是按照 tag 的时间戳顺序播放。任何加入到文件中时间设置数据格式都将被忽略。

  • 13-15:streamID(3 字节)总是 0。

FLV 格式详细的结构图如下图所示:

在浏览器中 HTML5 的 <video> 是不支持直接播放 FLV 视频格式,需要借助 flv.js 这个开源库来实现播放 FLV 视频格式的功能。

5.2 flv.js 简介

flv.js 是用纯 JavaScript 编写的 HTML5 Flash Video(FLV)播放器,它底层依赖于 Media Source Extensions。在实际运行过程中,它会自动解析 FLV 格式文件并喂给原生 HTML5 Video 标签播放音视频数据,使浏览器在不借助 Flash 的情况下播放 FLV 成为可能。

5.2.1 flv.js 的特性
  • 支持播放 H.264 + AAC / MP3 编码的 FLV 文件;
  • 支持播放多段分段视频;
  • 支持播放 HTTP FLV 低延迟实时流;
  • 支持播放基于 WebSocket 传输的 FLV 实时流;
  • 兼容 Chrome,FireFox,Safari 10,IE11 和 Edge;
  • 极低的开销,支持浏览器的硬件加速。
5.2.2 flv.js 的限制
  • MP3 音频编解码器无法在 IE11/Edge 上运行;
  • HTTP FLV 直播流不支持所有的浏览器。
5.2.3 flv.js 的使用
<script data-original="flv.min.js"></script>
<video id="videoElement"></video>
<script>
    if (flvjs.isSupported()) {
        var videoElement = document.getElementById('videoElement');
        var flvPlayer = flvjs.createPlayer({
            type: 'flv',
            url: 'http://example.com/flv/video.flv'
        });
        flvPlayer.attachMediaElement(videoElement);
        flvPlayer.load();
        flvPlayer.play();
    }
</script>

5.3 flv.js 工作原理

flv.js 的工作原理是将 FLV 文件流转换为 ISO BMFF(Fragmented MP4)片段,然后通过 Media Source Extensions API 将 mp4 段喂给 HTML5 <video> 元素。flv.js 的设计架构图如下图所示:

(图片来源:https://github.com/bilibili/f...

有关 flv.js 工作原理更详细的介绍,感兴趣的小伙们可以阅读 花椒开源项目实时互动流媒体播放器 这篇文章。现在我们已经介绍了 hls.jsflv.js 这两个主流的流媒体解决方案,其实它们的成功离不开 Media Source Extensions 这个幕后英雄默默地支持。因此,接下来阿宝哥将带大家一起认识一下 MSE(Media Source Extensions)。

六、MSE

6.1 MSE API

媒体源扩展 API(Media Source Extensions) 提供了实现无插件且基于 Web 的流媒体的功能。使用 MSE,媒体串流能够通过 JavaScript 创建,并且能通过使用 audiovideo 元素进行播放。

近几年来,我们已经可以在 Web 应用程序上无插件地播放视频和音频了。但是,现有架构过于简单,只能满足一次播放整个曲目的需要,无法实现拆分/合并数个缓冲文件。早期的流媒体主要使用 Flash 进行服务,以及通过 RTMP 协议进行视频串流的 Flash 媒体服务器。

媒体源扩展(MSE)实现后,情况就不一样了。MSE 使我们可以把通常的单个媒体文件的 src 值替换成引用 MediaSource 对象(一个包含即将播放的媒体文件的准备状态等信息的容器),以及引用多个 SourceBuffer 对象(代表多个组成整个串流的不同媒体块)的元素。

为了便于大家理解,我们来看一下基础的 MSE 数据流:

MSE 让我们能够根据内容获取的大小和频率,或是内存占用详情(例如什么时候缓存被回收),进行更加精准地控制。 它是基于它可扩展的 API 建立自适应比特率流客户端(例如 DASH 或 HLS 的客户端)的基础。

在现代浏览器中创造能兼容 MSE 的媒体非常费时费力,还要消耗大量计算机资源和能源。此外,还须使用外部应用程序将内容转换成合适的格式。虽然浏览器支持兼容 MSE 的各种媒体容器,但采用 H.264 视频编码、AAC 音频编码和 MP4 容器的格式是非常常见的,所以 MSE 需要兼容这些主流的格式。此外 MSE 还为开发者提供了一个 API,用于运行时检测容器和编解码是否受支持。

6.2 MediaSource 接口

MediaSource 是 Media Source Extensions API 表示媒体资源 HTMLMediaElement 对象的接口。MediaSource 对象可以附着在 HTMLMediaElement 在客户端进行播放。在介绍 MediaSource 接口前,我们先来看一下它的结构图:

(图片来源 —— https://www.w3.org/TR/media-s...

要理解 MediaSource 的结构图,我们得先来介绍一下客户端音视频播放器播放一个视频流的主要流程:

获取流媒体 -> 解协议 -> 解封装 -> 音、视频解码 -> 音频播放及视频渲染(需处理音视频同步)。

由于采集的原始音视频数据比较大,为了方便网络传输,我们通常会使用编码器,如常见的 H.264 或 AAC 来压缩原始媒体信号。最常见的媒体信号是视频,音频和字幕。比如,日常生活中的电影,就是由不同的媒体信号组成,除运动图片外,大多数电影还含有音频和字幕。

常见的视频编解码器有:H.264,HEVC,VP9 和 AV1。而音频编解码器有:AAC,MP3 或 Opus。每个媒体信号都有许多不同的编解码器。下面我们以西瓜视频播放器的 Demo 为例,来直观感受一下音频轨、视频轨和字幕轨:

现在我们来开始介绍 MediaSource 接口的相关内容。

6.2.1 状态
enum ReadyState {
    "closed", // 指示当前源未附加到媒体元素。
    "open", // 源已经被媒体元素打开,数据即将被添加到SourceBuffer对象中
    "ended" // 源仍附加到媒体元素,但endOfStream()已被调用。
};
6.2.2 流终止异常
enum EndOfStreamError {
    "network", // 终止播放并发出网络错误信号。
    "decode" // 终止播放并发出解码错误信号。
};
6.2.3 构造器
[Constructor]
interface MediaSource : EventTarget {
    readonly attribute SourceBufferList    sourceBuffers;
    readonly attribute SourceBufferList    activeSourceBuffers;
    readonly attribute ReadyState          readyState;
             attribute unrestricted double duration;
             attribute EventHandler        onsourceopen;
             attribute EventHandler        onsourceended;
             attribute EventHandler        onsourceclose;
  
    SourceBuffer addSourceBuffer(DOMString type);
    void         removeSourceBuffer(SourceBuffer sourceBuffer);
    void         endOfStream(optional EndOfStreamError error);
    void         setLiveSeekableRange(double start, double end);
    void         clearLiveSeekableRange();
    static boolean isTypeSupported(DOMString type);
};
6.2.4 属性
  • MediaSource.sourceBuffers —— 只读:返回一个 SourceBufferList 对象,包含了这个 MediaSource 的SourceBuffer 的对象列表。
  • MediaSource.activeSourceBuffers —— 只读:返回一个 SourceBufferList 对象,包含了这个MediaSource.sourceBuffers 中的 SourceBuffer 子集的对象—即提供当前被选中的视频轨(video track),启用的音频轨(audio tracks)以及显示/隐藏的字幕轨(text tracks)的对象列表
  • MediaSource.readyState —— 只读:返回一个包含当前 MediaSource 状态的集合,即使它当前没有附着到一个 media 元素(closed),或者已附着并准备接收 SourceBuffer 对象(open),亦或者已附着但这个流已被 MediaSource.endOfStream() 关闭。
  • MediaSource.duration:获取和设置当前正在推流媒体的持续时间。
  • onsourceopen:设置 sourceopen 事件对应的事件处理程序。
  • onsourceended:设置 sourceended 事件对应的事件处理程序。
  • onsourceclose:设置 sourceclose 事件对应的事件处理程序。
6.2.5 方法
  • MediaSource.addSourceBuffer():创建一个带有给定 MIME 类型的新的 SourceBuffer 并添加到 MediaSource 的 SourceBuffers 列表。
  • MediaSource.removeSourceBuffer():删除指定的 SourceBuffer 从这个 MediaSource 对象中的 SourceBuffers 列表。
  • MediaSource.endOfStream():表示流的结束。
6.2.6 静态方法
  • MediaSource.isTypeSupported():返回一个 Boolean 值表明给定的 MIME 类型是否被当前的浏览器支持—— 这意味着是否可以成功的创建这个 MIME 类型的 SourceBuffer 对象。
6.2.7 使用示例
var vidElement = document.querySelector('video');

if (window.MediaSource) { // (1)
  var mediaSource = new MediaSource();
  vidElement.src = URL.createObjectURL(mediaSource);
  mediaSource.addEventListener('sourceopen', sourceOpen); 
} else {
  console.log("The Media Source Extensions API is not supported.")
}

function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  var mime = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"';
  var mediaSource = e.target;
  var sourceBuffer = mediaSource.addSourceBuffer(mime); // (2)
  var videoUrl = 'hello-mse.mp4';
  fetch(videoUrl) // (3)
    .then(function(response) {
      return response.arrayBuffer();
    })
    .then(function(arrayBuffer) {
      sourceBuffer.addEventListener('updateend', function(e) { (4)
        if (!sourceBuffer.updating && mediaSource.readyState === 'open') {
          mediaSource.endOfStream(); 
        }
      });
      sourceBuffer.appendBuffer(arrayBuffer); // (5)
    });
}

以上示例介绍了如何使用 MSE API,接下来我们来分析一下主要的工作流程:

  • (1) 判断当前平台是否支持 Media Source Extensions API,若支持的话,则创建 MediaSource 对象,且绑定 sourceopen 事件处理函数。
  • (2) 创建一个带有给定 MIME 类型的新的 SourceBuffer 并添加到 MediaSource 的 SourceBuffers 列表。
  • (3) 从远程流服务器下载视频流,并转换成 ArrayBuffer 对象。
  • (4) 为 sourceBuffer 对象添加 updateend 事件处理函数,在视频流传输完成后关闭流。
  • (5) 往 sourceBuffer 对象中添加已转换的 ArrayBuffer 格式的视频流数据。

上面阿宝哥只是简单介绍了一下 MSE API,想深入了解它实际应用的小伙伴,可以进一步了解一下 hls.jsflv.js 项目。接下来阿宝哥将介绍音视频基础之多媒体容器格式。

七、多媒体封装格式

一般情况下,一个完整的视频文件是由音频和视频两部分组成的。常见的 AVI、RMVB、MKV、ASF、WMV、MP4、3GP、FLV 等文件只能算是一种封装格式。H.264,HEVC,VP9 和 AV1 等就是视频编码格式,MP3、AAC 和 Opus 等就是音频编码格式。 比如:将一个 H.264 视频编码文件和一个 AAC 音频编码文件按 MP4 封装标准封装以后,就得到一个 MP4 后缀的视频文件,也就是我们常见的 MP4 视频文件了。

音视频编码的主要目的是压缩原始数据的体积,而封装格式(也称为多媒体容器),比如 MP4,MKV,是用来存储/传输编码数据,并按一定规则把音视频、字幕等数据组织起来,同时还会包含一些元信息,比如当前流中包含哪些编码类型、时间戳等,播放器可以按照这些信息来匹配解码器、同步音视频。

为了能更好地理解多媒体封装格式,我们再来回顾一下视频播放器的原理。

7.1 视频播放器原理

视频播放器是指能播放以数字信号形式存储的视频的软件,也指具有播放视频功能的电子器件产品。大多数视频播放器(除了少数波形文件外)携带解码器以还原经过压缩的媒体文件,视频播放器还要内置一整套转换频率以及缓冲的算法。大多数的视频播放器还能支持播放音频文件。

视频播放基本处理流程大致包括以下几个阶段:

(1)解协议

从原始的流媒体协议数据中删除信令数据,只保留音视频数据,如采用 RTMP 协议传输的数据,经过解协议后输出 flv 格式的数据。

(2)解封装

分离音频和视频压缩编码数据,常见的封装格式 MP4,MKV,RMVB,FLV,AVI 这些格式。从而将已经压缩编码的视频、音频数据放到一起。例如 FLV 格式的数据经过解封装后输出 H.264 编码的视频码流和 AAC 编码的音频码流。

(3)解码

视频,音频压缩编码数据,还原成非压缩的视频,音频原始数据,音频的压缩编码标准包括 AAC,MP3,AC-3 等,视频压缩编码标准包含 H.264,MPEG2,VC-1 等经过解码得到非压缩的视频颜色数据如 YUV420P,RGB 和非压缩的音频数据如 PCM 等。

(4)音视频同步

将同步解码出来的音频和视频数据分别送至系统声卡和显卡播放。

了解完视频播放器的原理,下一步我们来介绍多媒体封装格式。

7.2 多媒体封装格式

对于数字媒体数据来说,容器就是一个可以将多媒体数据混在一起存放的东西,就像是一个包装箱,它可以对音、视频数据进行打包装箱,将原来的两块独立的媒体数据整合到一起,当然也可以单单只存放一种类型的媒体数据。

有时候,多媒体容器也称封装格式,它只是为编码后的多媒体数据提供了一个 “外壳”,也就是将所有的处理好的音频、视频或字幕都包装到一个文件容器内呈现给观众,这个包装的过程就叫封装。 常用的封装格式有: MP4,MOV,TS,FLV,MKV 等。这里我们来介绍大家比较熟悉的 MP4 封装格式。

7.2.1 MP4 封装格式

MPEG-4 Part 14(MP4)是最常用的容器格式之一,通常以 .mp4 文件结尾。它用于 HTTP(DASH)上的动态自适应流,也可以用于 Apple 的 HLS 流。MP4 基于 ISO 基本媒体文件格式(MPEG-4 Part 12),该格式基于 QuickTime 文件格式。MPEG 代表动态图像专家组,是国际标准化组织(ISO)和国际电工委员会(IEC)的合作。MPEG 的成立是为了设置音频和视频压缩与传输的标准。

MP4 支持多种编解码器,常用的视频编解码器是 H.264 和 HEVC,而常用的音频编解码器是 AAC,AAC 是著名的 MP3 音频编解码器的后继产品。

MP4 是由一些列的 box 组成,它的最小组成单元是 box。MP4 文件中的所有数据都装在 box 中,即 MP4 文件由若干个 box 组成,每个 box 有类型和长度,可以将 box 理解为一个数据对象块。box 中可以包含另一个 box,这种 box 称为 container box。

一个 MP4 文件首先会有且仅有 一个 ftype 类型的 box,作为 MP4 格式的标志并包含关于文件的一些信息,之后会有且只有一个 moov 类型的 box(movie box),它是一种 container box,可以有多个,也可以没有,媒体数据的结构由 metadata 进行描述。

相信有些读者会有疑问 —— 实际的 MP4 文件结构是怎么样的?通过使用 mp4box.js 提供的在线服务,我们可以方便的查看本地或在线 MP4 文件内部的结构:

mp4box.js 在线地址:https://gpac.github.io/mp4box...

由于 MP4 文件结构比较复杂(不信请看下图),这里我们就不继续展开,有兴趣的读者,可以自行阅读相关文章。

接下来,我们来介绍 Fragmented MP4 容器格式。

7.2.2 Fragmented MP4 封装格式

MP4 ISO Base Media 文件格式标准允许以 fragmented 方式组织 box,这也就意味着 MP4 文件可以组织成这样的结构,由一系列的短的 metadata/data box 对组成,而不是一个长的 metadata/data 对。Fragmented MP4 文件结构如下图所示,图中只包含了两个 fragments:

(图片来源 —— https://alexzambelli.com/blog...

在 Fragmented MP4 文件中含有三个非常关键的 boxes:moovmoofmdat

  • moov(movie metadata box):用于存放多媒体 file-level 的元信息。
  • mdat(media data box):和普通 MP4 文件的 mdat 一样,用于存放媒体数据,不同的是普通 MP4 文件只有一个 mdat box,而 Fragmented MP4 文件中,每个 fragment 都会有一个 mdat 类型的 box。
  • moof(movie fragment box):用于存放 fragment-level 的元信息。该类型的 box 在普通的 MP4 文件中是不存在的,而在 Fragmented MP4 文件中,每个 fragment 都会有一个 moof 类型的 box。

Fragmented MP4 文件中的 fragment 由 moofmdat 两部分组成,每个 fragment 可以包含一个音频轨或视频轨,并且也会包含足够的元信息,以保证这部分数据可以单独解码。Fragment 的结构如下图所示:

(图片来源 —— https://alexzambelli.com/blog...

同样,利用 mp4box.js 提供的在线服务,我们也可以清晰的查看 Fragmented MP4 文件的内部结构:

我们已经介绍了 MP4 和 Fragmented MP4 这两种容器格式,我们用一张图来总结一下它们之间的主要区别:

八、阿宝哥有话说

8.1 如何实现视频本地预览

视频本地预览的功能主要利用 URL.createObjectURL() 方法来实现。URL.createObjectURL() 静态方法会创建一个 DOMString,其中包含一个表示参数中给出的对象的 URL。这个 URL 的生命周期和创建它的窗口中的 document 绑定。 这个新的 URL 对象表示指定的 File 对象或 Blob 对象。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>视频本地预览示例</title>
  </head>
  <body>
    <h3>阿宝哥:视频本地预览示例</h3>
    <input type="file" accept="video/*" onchange="loadFile(event)" />
    <video
      id="previewContainer"
      controls
      width="480"
      height="270"
      style="display: none;"
    ></video>

    <script>
      const loadFile = function (event) {
        const reader = new FileReader();
        reader.onload = function () {
          const output = document.querySelector("#previewContainer");
          output.style.display = "block";
          output.src = URL.createObjectURL(new Blob([reader.result]));
        };
        reader.readAsArrayBuffer(event.target.files[0]);
      };
    </script>
  </body>
</html>

8.2 如何实现播放器截图

播放器截图功能主要利用 CanvasRenderingContext2D.drawImage() API 来实现。Canvas 2D API 中的 CanvasRenderingContext2D.drawImage() 方法提供了多种方式在 Canvas 上绘制图像。

drawImage API 的语法如下:

void ctx.drawImage(image, dx, dy);

void ctx.drawImage(image, dx, dy, dWidth, dHeight);

void ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);

其中 image 参数表示绘制到上下文的元素。允许任何的 canvas 图像源(CanvasImageSource),例如:CSSImageValue,HTMLImageElement,SVGImageElement,HTMLVideoElement,HTMLCanvasElement,ImageBitmap 或者 OffscreenCanvas。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>播放器截图示例</title>
  </head>
  <body>
    <h3>阿宝哥:播放器截图示例</h3>
    <video id="video" controls="controls" width="460" height="270" crossorigin="anonymous">
      <!-- 请替换为实际视频地址 -->
      <source data-original="https://xxx.com/vid_159411468092581" />
    </video>
    <button onclick="captureVideo()">截图</button>
    <script>
      let video = document.querySelector("#video");
      let canvas = document.createElement("canvas");
      let img = document.createElement("img");
      img.crossOrigin = "";
      let ctx = canvas.getContext("2d");

      function captureVideo() {
        canvas.width = video.videoWidth;
        canvas.height = video.videoHeight;
        ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
        img.src = canvas.toDataURL();
        document.body.append(img);
      }
    </script>
  </body>
</html>

现在我们已经知道如何获取视频的每一帧,其实在结合 gif.js 这个库提供的 GIF 编码功能,我们就可以快速地实现截取视频帧生成 GIF 动画的功能。这里阿宝哥不继续展开介绍,有兴趣的小伙伴可以阅读 使用 JS 直接截取 视频片段 生成 gif 动画 这篇文章。

8.3 如何实现 Canvas 播放视频

使用 Canvas 播放视频主要是利用 ctx.drawImage(video, x, y, width, height) 来对视频当前帧的图像进行绘制,其中 video 参数就是页面中的 video 对象。所以如果我们按照特定的频率不断获取 video 当前画面,并渲染到 Canvas 画布上,就可以实现使用 Canvas 播放视频的功能。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>使用 Canvas 播放视频</title>
  </head>
  <body>
    <h3>阿宝哥:使用 Canvas 播放视频</h3>
    <video id="video" controls="controls" style="display: none;">
      <!-- 请替换为实际视频地址 -->
      <source data-original="https://xxx.com/vid_159411468092581" />
    </video>
    <canvas
      id="myCanvas"
      width="460"
      height="270"
      style="border: 1px solid blue;"
    ></canvas>
    <div>
      <button id="playBtn">播放</button>
      <button id="pauseBtn">暂停</button>
    </div>
    <script>
      const video = document.querySelector("#video");
      const canvas = document.querySelector("#myCanvas");
      const playBtn = document.querySelector("#playBtn");
      const pauseBtn = document.querySelector("#pauseBtn");
      const context = canvas.getContext("2d");
      let timerId = null;

      function draw() {
        if (video.paused || video.ended) return;
        context.clearRect(0, 0, canvas.width, canvas.height);
        context.drawImage(video, 0, 0, canvas.width, canvas.height);
        timerId = setTimeout(draw, 0);
      }

      playBtn.addEventListener("click", () => {
        if (!video.paused) return;
        video.play();
        draw();
      });

      pauseBtn.addEventListener("click", () => {
        if (video.paused) return;
        video.pause();
        clearTimeout(timerId);
      });
    </script>
  </body>
</html>

8.4 如何实现色度键控(绿屏效果)

上一个示例我们介绍了使用 Canvas 播放视频,那么可能有一些小伙伴会有疑问,为什么要通过 Canvas 绘制视频,Video 标签不 “香” 么?这是因为 Canvas 提供了 getImageDataputImageData 方法使得开发者可以动态地更改每一帧图像的显示内容。这样的话,我们就可以实时地操纵视频数据来合成各种视觉特效到正在呈现的视频画面中。

比如 MDN 上的 ”使用 canvas 处理视频“ 的教程中就演示了如何使用 JavaScript 代码执行色度键控(绿屏或蓝屏效果)。所谓的色度键控,又称色彩嵌空,是一种去背合成技术。Chroma 为纯色之意,Key 则是抽离颜色之意。把被拍摄的人物或物体放置于绿幕的前面,并进行去背后,将其替换成其他的背景。此技术在电影、电视剧及游戏制作中被大量使用,色键也是虚拟摄影棚(Virtual studio)与视觉效果(Visual effects)当中的一个重要环节。

下面我们来看一下关键代码:

processor.computeFrame = function computeFrame() {
    this.ctx1.drawImage(this.video, 0, 0, this.width, this.height);
    let frame = this.ctx1.getImageData(0, 0, this.width, this.height);
    let l = frame.data.length / 4;

    for (let i = 0; i < l; i++) {
      let r = frame.data[i * 4 + 0];
      let g = frame.data[i * 4 + 1];
      let b = frame.data[i * 4 + 2];
      if (g > 100 && r > 100 && b < 43)
        frame.data[i * 4 + 3] = 0;
    }
    this.ctx2.putImageData(frame, 0, 0);
    return;
}

以上的 computeFrame() 方法负责获取一帧数据并执行色度键控效果。利用色度键控技术,我们还可以实现纯客户端实时蒙版弹幕。这里阿宝哥就不详细介绍了,感兴趣的小伙伴可以阅读一下创宇前端 弹幕不挡人!基于色键技术的纯客户端实时蒙版弹幕 这篇文章。

九、参考资源

查看原文

abcbuzhiming 收藏了文章 · 7月26日

上亿数据怎么玩深度分页?兼容MySQL + ES + MongoDB

面试题 & 真实经历

面试题:在数据量很大的情况下,怎么实现深度分页?

大家在面试时,或者准备面试中可能会遇到上述的问题,大多的回答基本上是分库分表建索引,这是一种很标准的正确回答,但现实总是很骨感,所以面试官一般会追问你一句,<font color="red">现在工期不足,人员不足,该怎么实现深度分页?</font>

这个时候没有实际经验的同学基本麻爪,So,请听我娓娓道来。

惨痛的教训

首先必须明确一点:深度分页可以做,但是<font color="red">深度随机跳页绝对需要禁止。</font>

上一张图:

你们猜,我点一下第142360页,服务会不会爆炸?

MySQLMongoDB数据库还好,本身就是专业的数据库,处理的不好,最多就是慢,但如果涉及到ES,性质就不一样了,我们不得不利用 SearchAfter Api,去循环获取数据,这就牵扯到内存占用的问题,如果当时代码写的不优雅,直接就可能导致内存溢出。

为什么不能允许随机深度跳页

从技术的角度浅显的聊一聊为什么不能允许随机深度跳页,或者说为什么不建议深度分页

MySQL

分页的基本原理:

SELECT * FROM test ORDER BY id DESC LIMIT 10000, 20;

LIMIT 10000 , 20的意思扫描满足条件的10020行,扔掉前面的10000行,返回最后的20行。如果是LIMIT 1000000 , 100,需要扫描1000100 行,在一个高并发的应用里,每次查询需要扫描超过100W行,不炸才怪。

MongoDB

分页的基本原理:

db.t_data.find().limit(5).skip(5);

同样的,随着页码的增大,skip 跳过的条目也会随之变大,而这个操作是通过 cursor 的迭代器来实现的,对于cpu的消耗会非常明显,当页码非常大时且频繁时,必然爆炸。

ElasticSearch

从业务的角度来说,ElasticSearch不是典型的数据库,它是一个搜索引擎,如果在筛选条件下没有搜索出想要的数据,继续深度分页也不会找到想要的数据,退一步讲,假如我们把ES作为数据库来使用进行查询,在进行分页的时候一定会遇到max_result_window 的限制,看到没,官方都告诉你最大偏移量限制是一万。

查询流程:

  1. 如查询第501页,每页10条,客户端发送请求到某节点
  2. 此节点将数据广播到各个分片,各分片各自查询前 5010 条数据
  3. 查询结果返回至该节点,然后对数据进行整合,取出前 5010 条数据
  4. 返回给客户端

由此可以看出为什么要限制偏移量,另外,如果使用 Search After 这种滚动式API进行深度跳页查询,也是一样需要每次滚动几千条,可能一共需要滚动上百万,千万条数据,就为了最后的20条数据,效率可想而知。

再次和产品对线

<font color="red">俗话说的好,技术解决不了的问题,就由业务来解决!</font>

在实习的时候信了产品的邪,必须实现深度分页 + 跳页,如今必须拨乱反正,业务上必须有如下更改:

  • <font color="red">尽可能的增加默认的筛选条件,如:时间周期</font>,目的是为了减少数据量的展示
  • <font color="red">修改跳页的展现方式,改为滚动显示,或小范围跳页</font>

滚动显示参考图:

小规模跳页参考图:

通用解决方案

短时间内快速解决的方案主要是以下几点:

  • 必备:对排序字段,筛选条件务必设置好索引
  • 核心:<font color="red">利用小范围页码的已知数据,或者滚动加载的已知数据,减少偏移量</font>
  • 额外:如果遇到不好处理的情况,也可以获取多余的数据,进行一定的截取,性能影响并不大

MySQL

原分页SQL:

# 第一页
SELECT * FROM `year_score` where `year` = 2017 ORDER BY id limit 0, 20;

# 第N页
SELECT * FROM `year_score` where `year` = 2017 ORDER BY id limit (N - 1) * 20, 20; 

通过上下文关系,改写为:

# XXXX 代表已知的数据
SELECT * FROM `year_score` where `year` = 2017 and id > XXXX ORDER BY id limit 20;

没内鬼,来点干货!SQL优化和诊断 一文中提到过,LIMIT会在满足条件下停止查询,因此该方案的扫描总量会急剧减少,效率提升Max!

ES

方案和MySQL相同,此时我们就可以随用所欲的使用 FROM-TO Api,而且不用考虑最大限制的问题。

MongoDB

方案基本类似,基本代码如下:

相关性能测试:

如果非要深度随机跳页

如果你没有杠过产品经理,又该怎么办呢,没关系,还有一丝丝的机会。

SQL优化 一文中还提到过MySQL深度分页的处理技巧,代码如下:

# 反例(耗时129.570s)
select * from task_result LIMIT 20000000, 10;

# 正例(耗时5.114s)
SELECT a.* FROM task_result a, (select id from task_result LIMIT 20000000, 10) b where a.id = b.id;

# 说明
# task_result表为生产环境的一个表,总数据量为3400万,id为主键,偏移量达到2000万

该方案的核心逻辑即基于聚簇索引,在不通过回表的情况下,快速拿到指定偏移量数据的主键ID,然后利用聚簇索引进行回表查询,此时总量仅为10条,效率很高。

因此我们在处理MySQLESMongoDB时,也可以采用一样的办法:

  1. 限制获取的字段,只通过筛选条件,深度分页获取主键ID
  2. 通过主键ID定向查询需要的数据

瑕疵:当偏移量非常大时,耗时较长,如文中的 5s

最后

参考文章:MongoDB中文社区

感谢 @程大设计师 为我倾情设计的二维码😜

如果觉得对你有用的话,不要忘记点个赞啊~

查看原文

abcbuzhiming 收藏了文章 · 7月4日

《编程时间简史系列》Web Server 编年史

引言

本文是《编程时间简史系列》的第二篇文章。

全系列索引:

  1. 《编程时间简史系列》JavaScript 模块化的历史进程
  2. 《编程时间简史系列》Web Server 编年史

互联网今天已经广泛存在于人们的生活中,人们的衣食住行等方方面面早已离不开互联网的支撑,这其中离不开 Web 技术的发展。

Web 是一种典型的分布式应用架构。Web 应用中的每一次信息交换都要涉及到客户端和服务端两个层面。因此,Web 开发技术大体上也可以被分为客户端技术和服务端技术两大类。

本文将会讲述 Web 服务端技术的萌芽和演进过程,旨在使读者能更清晰地掌握 Web 服务端技术的发展脉络。

Web 服务端技术的发展与客户端技术的进步是相辅相成的,本文虽是讨论 Web 服务端,在讲述过程中却不可避免地会提及一些 Web 客户端的有关内容,但不会过多深入。对此感兴趣的读者,可以自行阅读最下方的参考链接。

同样的,不谈具体代码,只聊历史故事。

P.S. 下一篇的选题还没有敲定,如果有朋友想了解某一方面的历史又苦于没有资料,可以在文章下方给我留言。


正文

广义上的 Web Server(Web 服务器),包含硬件和软件两方面。今天我们只谈及其中的软件部分,即能向客户端提供 Web 服务的程序。

现在大家耳熟能详的 ApacheIISTomcatNginx 等等,都属于 Web Server。

那么它们之间究竟有何不同?又是由谁、在什么时间发明的?我们常说的静态网页、动态网页又是指什么?HTTPd 和 Web Server 有何不同?网上总提的 libuv,它是个啥?

让我们先带着这些疑问,回到 HTTP 协议尚未诞生的时代。

时代前夜:HTML、HTTP 与万维网

1960 年,Theodor Holm Nelson 在哈佛计算机编程的选修课程上,使用了当时哈佛大学唯一可用的计算机 —— IBM 7090。在临近课程结束的时候,Theodor Holm Nelson 决定使用机器语言编写一个计算机程序,让他能够将自己的笔记和手稿存储在计算机中,可以以各种方式修改和编辑草稿,并生成可打印的最终版本。在他的项目进行到第 4 万行左右的代码时,他开始意识到,他对这项任务的完成难度最初估计得过于乐观。

1963 年,已经从哈佛大学毕业的 Theodor Holm Nelson 决定将自己大学时的想法继续进行下去。他首次提出了名为 “HyperText”(超文本)的概念,并找到了一些志同道合、痴迷计算机的朋友,成立了 Project Xanadu,试图制订规范,并应用到实际的计算机程序中。

1969 年,IBM 公司的 Charles F. Goldfarb 发明了一种可以用于描述超文本文档的描述语言 —— Generalized Markup Language(简称为 GML,通用标记语言)。在之后的几年时间里,形成了 Standard Generalized Markup Language(简称为 SGML,标准通用标记语言)的标准规范,成为了 ISO 国际标准。

制订 SGML 的基本思想是把文档的内容与样式分开。在 SGML 中,标记分两种:一种用来描述文档显示的样式,称为程序标记;另一种用来描述文档中语句的用途,称为描述标记。一个 SGML 文件通常分三个层次:结构、内容和样式。结构为组织文档的元素提供框架,内容是信息本身,样式控制内容的显示。

不过,由于 GML/SGML 过于庞大且复杂,虽然有不少大公司在内部使用,但始终没能得到广泛的应用。

暂且按下 Theodor Holm NelsonCharles F. Goldfarb 这边不表,让我们来到 1989 年。

此时已是不惑之年的 Tim Berners-Lee,负责在 CERN(欧洲粒子物理实验室)做 IT 支持工作。由于粒子物理在当时是前沿研究领域,需要全世界的物理学家合作参与,那么如何与世界各地的物理研究所保持通信,就是一件十分重要也棘手的事情。

起初,CERN 使用传真机来传输文件,但物理传输速度极慢,且会耗费大量纸张与油墨,对于信息检索工作而言也十分不便。

后来,因 ARPANET 网络在美国军方和多所大学内成功使用,CERN 也开始采用这种使用计算机网络进行通信的方式来传输数据。

但在此时,可选择的网络协议并不多。从时间顺序上来看,有:

  • 要么是 1971 年出现的 FTP,用于传输文件。但这种方式不能直接展示文本内容,而是需要下载到本地后才能打开。更何况,即是打开了文件,如果需要同时显示包含文本、图片、音频、视频等信息的多媒体内容,那么需要特定的程序才能编辑、预览。
  • 要么是 1973 年出现的 TELNET 协议,可以与远程终端进行交互。但这种操作方式极其繁琐,且对于搞科研的物理科学家而言操作并不友好,往往还需要 Theodor Holm Nelson 这样的 IT 部门来配合。
  • 要么是 1982 年出现的 SMTP,通过电子邮件进行交流。但这种方式不适合用于信息的公开展示,只适合点对点或群组之间的信息沟通。

这一年年底,Tim Berners-Lee 向其上级提出了一项名为 Information Management: A Proposal(《关于信息化管理的建议》)的提议:使来自世界各地的远程站点的研究人员能够组织和汇集信息,在个人计算机上访问大量的科研文献,并建议在文档中链接其他文档。

在参考了 Theodor Holm Nelson 有关超文本的规范、并参考了 Charles F. GoldfarbGML/SGML 实现后,Tim Berners-Lee 于 1990 年发明了 Hypertext Markup Language(简称为 HTML,超文本标记语言)和 Hypertext Transfer Protocol(简称为 HTTP,超文本传输协议)。

1990 年,Tim Berners-Lee 创建了一款最原始的 GUI 的 HTML 浏览器(同时也是编辑器),和第一个 HTTP 服务器。

1991 年,Tim Berners-Lee 作为布道者开始广泛推广 Web 的理念,提出了 World Wide Web(万维网)的概念。

值得一提的是,在这一时期,Web 领域还有其他诸如 NNTP、Gopher 等传输协议。但它们都因为种种原因,没能像 HTTP 一样流行起来,最终消失在了历史长河之中。

1993 年,NCSA(美国国家超算应用中心)对此表现出了浓厚的兴趣,并开发了名为 Mosaic 的浏览器,于当年 4 月发布。

1994 年,第一届国际万维网大会于 4 月在瑞士日内瓦召开,World Wide Web Consortium(简称为 W3C,万维网联盟)组织正式成立,并从 IETF(互联网工程任务组)接管了 HTML 的标准制订工作。

同年 6 月,IETF 非正式地指出了之前在“民间”流传的 URL(统一资源定位符)与 URN(统一资源名称)这两种叫法的存在,并进一步地定义了一个名为 Uniform Resource Identifier(简称为 URI,统一资源标识符)的规范文法。

同年 10 月,CERN 的另一位 IT 员工 Håkon Wium Lie 吸收了当时已有的一些 Web 样式的实践经验,提出并发明了 Cascading Style Sheets(简称为 CSS,层叠样式表)。

同年 11 月,Mosaic 浏览器的开发人员创立了 Netscape(网景)公司,并发布了 Mosaic Netscape 浏览器 1.0 版本,后改名为 Netscape Navigator(网景导航者)。

1995 年,W3C 制订了 HTML 2.0 标准。

同年 5 月,Netscape 公司的工程师 Brendan Eich 发明了一门名为 LiveScript 的脚本语言,可用在 Web 服务器和浏览器。在之后与 Netscape Navigator 2.0 一同发布时,被改名为 JavaScript

同年 8 月,Microsoft(微软)旗下的 Internet Explorer(简称为 IE)1.0 版本正式发布。

1996 年,IETF 将 HTTP 列为互联网标准之一,并制订了 HTTP/1.0 协议标准。

同年 12 月,W3C 将 CSS 纳入工作范围,并在随后个几个月里制订了 CSS 1 标准。

1997 年,JavaScript 被提交给 ECMA(欧洲计算机制造商协会),并最终形成了编号 262、名为 ECMAScript 的标准规范。这一规范下包含了 Netscape 1995 年发明的 JavaScript、Microsoft 1995 年发明的 JScript、Adobe 1999 年发明的 ActionScript 等几个实现。在接下来的几年时间里,这一标准规范同样被 ISO(国际标准化组织)及 IEC (国际电工委员会)所采纳。

1997 - 1999 年,HTML 3.0HTTP/1.1HTML 4.0CSS 2ECMAScript 3 等标准先后被发布,并统治了今后二十余年的互联网。

终于,Web 时代降临。

开天辟地:CERN HTTPd 和 NCSA HTTPd

HTTPd,即 HTTP daemon 的缩写。

今天我们谈到这个名词,大部分人会把它认为是 Apache 的代名词。但这其实只是个误解(原因下文会提到)。

在类 Unix 的操作系统中,一个在后端周期性地执行某种任务或等待处理理某些事件的进程,往往被称为 “Daemon Process”(守护/幽灵进程)。HTTPd 即取此意,意思就是在后台处理 HTTP 请求的程序。

因此实际上来说,HTTPd 应该是近似等同于 Web Server。在 HTTP 协议尚未出现的时代,Web Server 一般指 FTP 服务器。但 HTTP 协议出现后,Web Server 就立刻变成了指代 HTTP 服务器。今天的 Web Server 一定会、但不仅仅只会支持 HTTP 协议及其衍生协议,还可能支持诸如 FTP、SMTP、MQTT 甚至是更底层的 TCP、UDP 协议。

1990 年年底,Tim Berners-Lee 在一台运行着 NeXTSTEP 系统的 NeXT Computer 上编写了首个 HTTPd 程序,起名为 Common Library。这是一个由 C 语言编写的组件,只能处理 HTTP 请求中的 GET 谓词,并不是一个独立且完整的程序。因其属于 CERN 项目的一部分,所以也被称为 CERN HTTPd

1993 年,Tim Berners-LeeCommon Library 从 CERN 项目中独立出来,更名为 libwww 并开源。

同年,NCSA 在此基础之上扩展并开发出了 NCSA HTTPd

1994 年,libwww 的开发维护工作转交给了 W3C,在此阶段,libwww 新增了很多特性,诸如兼容 HTTP/1.0、支持 CGI、虚拟主机等。此时它也被称为 W3C HTTPd

1996 年,W3C 的工作重心已经不在 libwww 上,迟迟没有新版本发布,并最终于 2003 年宣告项目中止。

libwww 提供了基础的 HTTP 协议的解析与包装方式,既可用于服务端,也可用于服务端,被广泛地使用在包括 MosaicLynxArenaMacWWW 在内的诸多早期 Web 程序中。

胎死腹中:夭折的 Jigsaw

上一小节提到,1996 年时 W3C 的工作重心已经不在 libwww 上,因为他们已经另有其他重点工作。

由于 libwww 只能被编译到类 Unix 的操作系统中,且只支持静态网页。随着 Web 技术的不断发展,以及 Windows 系统的广泛流行,W3C 亟需一种可以跨平台的的 Web 服务器。因此,W3C 将目光放在了横空出世、发展迅猛的一种跨平台编程语言 —— Java。

W3C 联合当时的拥有 Java 的 Sun(升阳)公司,开发了一个名为 Jigsaw 的程序。

它由 Java 编写,起初只作为 JDK 1.2 版本的一个模块发布,意图让开发者能快速搭建一个跨平台 Web 服务器项目。它采用了多线程的处理方式,兼容 HTTP/1.1,支持 SSL/TLS、WebDAV 等新特性,同时也是首个支持 Servlet 和 JSP 的服务器程序。由于 Java 的跨平台特性,它可以运行在 BeOS、AS-400、AIX、Unix、Solaris 2、OS/2、MacOS、Linux、Windows 95、Windows NT 等操作系统上。

但遗憾的是,W3C 组织内的大部分成员,都是 IT 巨头公司,随着它们分别发布了各自的 Web 服务器商业产品后,Jigsaw 项目已经在事实上被废弃。虽然 W3C 没有明确地宣布项目中止,但从提交记录上来看,2007 年以后已经没有新特性被引入了,仅仅在 2007 - 2010 四年时间里修复了三五个 Bug,从此就悄无声息。

虽然 Jigsaw 命运早夭,但因为它是第一个由 Java 编写的 Web Server,起到了很多纲领性的指导作用,为后续 Java 技术在 Web 领域的扩展打下了坚实的基础。

值得一提的是,JDK 9 中新引入了与 Jigsaw 同名的模块化方案,但与 Jigsaw HTTPd 并没有什么关联。

萌芽初生:SSI 的诞生与 CGI 的兴起

最早的 Web 服务器只是简单地响应浏览器发来的 HTTP 请求,并将存储在服务器上的 HTML 文件返回给浏览器。可以近似理解为拥有文档预览功能的 FTP。文档内容在文件未修改前就是不变的,所有访问 Web 的用户看到的内容都是相同的。

这也就是前文提到的所谓的“静态网页”,这显然满足不了人们对信息丰富性和多样性的强烈需求。

由此,Web 技术的发展出现了两条分支路线。一条是尝试向客户端、即浏览器引入动态交互,例如 Sun 公司的 Java Applet、Netscape 公司的 JavaScript、Microsoft 公司的 JScriptVBScript、Adobe 公司的 FlashActionScript 等等。另一条是试图从服务端、即 Web Server 入手,想在返回给客户端时就输出动态的内容。这两条路线都在未来有了十分迅猛的发展,我们今天按下客户端不表,只谈服务端这面。

1991 年,NCSA 首次提出了 Server Side Includes(简称为 SSI,服务端嵌入) 的概念,并在之后发布的 NCSA HTTPd 中实现这一技术。

不过 SSI 的功能十分有限,通常用于实现网站页面的公共部分引用,比如在网页底部重复出现的版权声明等信息。它既不支持运算表达式,也不能根据逻辑条件判断是否输出特定内容,更遑论支持数据库这种 “高级操作” 了。所以虽然早期的 Web Server 都支持这种技术,但它并没有流行起来。

1993 年,在 NCSA 发布 NCSA HTTPd 的同时,NCSA 又提出了 Common Gateway Interface(简称为 CGI,通用网关接口)这一概念,并在未来几年内先后制订了 CGI 1.0CGI 1.1CGI 1.2 等标准。

CGI 本质上来说,就是接受一个输入、并返回一个输出的程序。CGI 独立于 Web Server 存在,在收到特定请求后(这些请求通常以 /cgi-bin/ 路径开头),Web Server 将 HTTP 请求的内容作为参数传递给 CGI 程序,并将 CGI 程序的返回值作为 HTTP 响应返回给客户端。所以 CGI 程序不能独立运行,需要配合 Web Server 才能工作。

早期通常是在 Web Server 接受到一个请求后,开启一个新的进程来执行 CGI 程序,这种方式在请求量稍微大一些时,就会严重拖累服务器的性能。

所以,随后又诞生了 FastCGI(简称为 FCGI)技术。简单来说,就是一个常驻内存的进程池技术,可以复用进程,使得 CGI 的工作负载性能大大提升。

在今天,由于 CGI 编写的复杂难度过大,已经很少有人再直接应用这种技术(间接的还有很多)。但它的出现,给其他编程语言带来了启发,诸如 FCGISCGIWSGIServlet 乃至后来的动态脚本语言等技术不断涌现,它们都滥觞于 CGI

承前启后:WebServer 之 Apache HTTP Server

1995 年,在随着 NCSA HTTPd 1.3 版本的发布,NCSA 就逐渐放缓了对 NCSA HTTPd 版本的开发工作。但为了满足日益丰富的 Web 服务端技术的需要,NCSA HTTPd 的社区成员在 Brian Behlendorf 的领导下,决定在 NCSA HTTPd 1.3 版本的基础上创建一个新的分支,并取名为 Apache

为什么取名为 Apache?其中一个流传最广的解释是,Apache 是在 NCSA HTTPd 的基础上修改的,因此是一个 “修补过的”(a patchy)Web Server。

但后来在 Apache 的 2.0 版本里,Apache 社区已将 NCSA HTTPd 的源代码全部移除,二者在今天已经没有了直接关系。

Apache 在前人的基础上,支持了很多新的 Web 特性。例如:多种身份认证方案、支持 CGI、支持 SSL/TLS、支持 IPv6、反向代理、URL 重写、定制化日志文件等等。与此同时,在其 2.0 版本中还加入了对非 Unix 操作系统的跨平台支持。

随着 Apache 逐渐发展壮大,它成为了首个最为广泛使用的开源 Web Server,曾一度占领了 70% 以上的市场份额,现在是主流的 Web Server 之一。加之其可执行文件名为 httpd,所以很多后人也将 HTTPd 理解成 Apache 的代名词,但这只是个误解。

Apache 的设计理念,影响了很多后来的 Web Server,是开源世界和 Web 历史中不能不提的一环。

值得一提的是,Apache 社区在 1999 年成立了 Apache Software Foundation(Apache 软件基金会)组织,致力于支持开源软件事业。我们今天谈及 Apache,即指的是最初的 Apache HTTP Server,也指 Apache 软件基金会。

正如前文提到的那样,虽然被称为 Apache HTTP Server,但它不仅仅支持 HTTP 协议及其衍生协议,还可以通过插件的形式支持其他协议。

异军突起:WebServer 之 IIS

1995 年 5 月,在令世界为之疯狂的 Windows 95 上市的前三个月,Windows NT 3.51 发布,这是 Windows NT 3.X 系列中的最后一个版本,也是第一个支持全中文的 Windows 操作系统。

随着这一版本的发布,一个名为 Internet Information Services(简称为 IIS,互联网信息服务)的系统可选组件悄然到来。

由于 IIS 是在 Windows 操作系统平台下开发的,这也限制了它只能在 Windows 下运行,但它是首个支持以 GUI 方式配置的 Web Server。

Apache 一样,IIS 也支持 HTTP 协议及其衍生协议、FTP 协议、SMTP 协议等。

随着 Windows 的流行,IIS 也不断进行版本迭代,它曾一度接近 Apache 的市场份额,现在也是主流的 Web Server 之一。

诸神崛起:PHP、JSP 还是 ASP?

CGI 程序一般由 C、C++、Pascal 等语言编写,并在目标平台上编译成二进制可执行程序,这给开发维护工作带来了很多麻烦。

为了简化 CGI 程序的修改、编译和发布流程,人们开始探寻用无需编译的脚本语言来实现 CGI 应用的道路。

很快,第一个用 Perl 写成的 CGI 程序问世。很快,Perl 在 CGI 编程领域的风头就盖过了它的前辈 C 语言。随后,Python 等著名的脚本语言也陆续加入了 CGI 编程语言的行列。不过随着 CGI 技术本身的衰落,Perl 最终在其后续版本中移除了 CGI 的相关模块。

1994 年,丹麦裔加拿大人 Rasmus Lerdorf 用 Perl 编写了一个简单的程序,用于统计他的个人主页的访问者。后来,Rasmus Lerdorf 用 C 语言重新编写了一遍这个程序,并在 1995 年以 Personal Home Page Tools(简称为 PHP Tools,个人主页工具)的名义开源了 PHP 1.0 版本。

在这早期的版本中,提供了访客留言本、访客计数器等简单的功能。以后越来越多的网站使用了 PHP,并且强烈要求增加如循环语句、数组变量等新特性,在新的社区成员加入开发行列后,1995 年,PHP 2.0 版本顺利发布。在这个版本中,PHP 添加了对 MySQL 数据库的支持,从此建立了其在动态网页开发上的地位。

PHP 最早使用 CGI 的工作方式(即 php-cgi),后因为这种方式的性能损耗很大,所以又开发了基于 FastCGI 的版本(即 php-fpmPHP FastCGI Process Manager 的缩写)。

但与早期 CGI 不同的是,PHP 首次将 HTML 代码和 PHP 指令合成为完整的服务端文档,Web 应用的开发者可以用一种更加简便、快捷的方式实现动态 Web 网页功能。

1996 年,Microsoft 公司在借鉴了 PHP 的思想后,在其产品 IIS 3.0 版本中引入了名为 Active Server Pages(简称为 ASP,动态服务器网页)的技术。

ASP 使用的脚本语言是 JScript 和 VBScript。借助 Microsot Office FrontPage、Microsoft Visual Studio 等开发工具在市场上的成功,ASP 迅速成为了 Windows 系统下 Web 服务端的主流开发技术。

需要说明的,Microsoft 在之后的 .NET Framework 和 .NET Core 体系中,还分别引入的名为 ASP .NETASP .NET Core 的技术。如果说后两者还师出同门,只不过一个只在 Windows 上运行、一个能跨平台运行;而 ASP 则和后两者只有名字上得到了传承,实际上已经没什么关系了。

当然,以 Sun 公司为首的 Java 阵营也不会示弱。1997 年,Servlet 技术问世。1998 年,Java Server Pages(简称为 JSP,Java 服务器页面)技术诞生。

其中 Servlet 类似于 CGI/FastCGI 处理;JSP 则类似于 PHP 的 HTML 模版。前者对于拼接 HTML 不是很擅长,后者对于运算和逻辑写起来又很繁琐,那么有没有可以把二者优势相结合的办法呢?

答案是肯定的,这也就是著名的 MVCModel-View-Controller)架构。虽然 MVC 架构早在 1978 年就在 Smalltalk 上提出,在 GUI 领域上也有 Microsoft 推出的 Microsoft Foundation Classes(简称为 MFC,微软基础类库)丰富实践,但这还是首次在 Web 领域得到应用。

这种 Servlet + JSP 组合的方式,后来也反过来影响了之前出现的 PHPASP,二者最终在后续版本中引入了类似的功能。

至此,扩展到 Web 领域的语言(如 Perl、Python),以及专为 Web 而生的语言(如 PHP、ASP、JSP),这些主流的脚本语言已全部出现,它们最终引领了 Web 下一个时代的前进方向。

容器之路:WebServer 之 Apache Tomcat

上文提到,无论 Apache 也好、IIS 也罢,本身并不直接生成动态页面,而是需要以 CGI/FastCGI 的方式将 HTTP 请求转发给相应的处理程序,才能返回动态页面。

PHPASPJSP 等脚本语言的出现,虽然已经不需要 Web 开发人员手工编写复杂的 CGI/FastCGI 程序来解析、封装 HTTP 报文,而是专注于业务逻辑本身即可。但这种方式其实质还是 Web Server + CGI/FastCGI 二者独立运行的方式。

那么有没有直接能生成动态 HTML 内容、无需 CGI/FastCGI 配合的 Web Server 呢?

1999 年,Tomcat 应运而生。Tomcat 既是 HTTP Web Server,也是 Java 执行容器,它由 Catalina Servlet 容器、Coyote 连接器、Jasper JSP 引擎等组件组成,可以直接输出动态 HTML 文档。

由于 Tomcat 也是 Apache 软件基金会的顶级项目之一,所以也被称为 Apache Tomcat

早期的 Tomcat 由于性能不佳(尤其是针对纯静态内容),通常还是要与 Apache HTTP Server 或者其他 Web Server 一起工作,除了用于开发过程中的调试以及那些对速度要求很低的开发者,很少会将 Tomcat 单独作为 Web Server。

这也给很多人造成了误解,以为 Tomcat 是那些基于 CGI/FastCGI 技术的脚本语言类似,是专门运行 ServletJSP 的程序。其实这也是一种误解,无论是 Servelet 还是 JSP,它们都比 Tomcat 面世的要早;而 Tomcat 完全可以脱离 Apache HTTP Server 独立运行,充当 Web Server。

但随着 Tomcat 版本的不断迭代,以及 Web Server 集群技术的广泛使用,正有越来越多的开发者将其单独作为 Web Server。

为了和早期那种只支持静态网页的 Web Server 加以区分,我们把这类 Web Server 也称之为 Application Server,即应用服务器。

Tomcat 这种 Web Server + 执行容器的双重身份的方式,后来也有越来越多的 Java 开源产品采用,诸如 JettyNettyUnderow 等等。

值得一提的是,2014 年,Microsoft 发布了 ASP .NET vNext 首个预览版,也就是后来的 ASP .NET Core,从这一版本开始,Microsoft 也实现了类似的产品,名为 Kestrel Server

风起云涌:libevent、libev、libuv,C10K 的法宝

网络通信,本质上就是对网卡或网络虚拟设备进行 I/O 操作。

早期的操作系统,基本都是阻塞 I/O(即 BIO),这种方式在面对大量并发时,会显得力不从心。上文提到的各种 Web Server 都是基于这种实现方式。

在这一时期,很多 Web Server 都会遇到著名的 “C10K” 问题,即:当请求的并发数量达到一万后,Web Server 的性能会随之急剧下降。

为了缓解并发问题,后来又出现了非阻塞 I/O(即 NIO)、异步 I/O (即 AIO)、I/O 多路复用等模型。例如 Unix 系统下的 pollselect,Solaris 系统下的 /dev/poll,BSD 系统下的 kqueue,Linux 系统下的 epoll,Windows 系统下的 IOCP 等等。它们各自的区别和优缺点我们这里不做展开,感兴趣的朋友可以自己搜索相关资料。

2000 年,libevent 问世。这是一个由 C 语言编写的、轻量级的开源高性能事件驱动编程库。起初它只兼容类 Unix 操作系统,在其他系统上性能并不高,后来在社区的推动下才慢慢支持 Windows 等操作系统的 IOCP 模型。不过因为它历史悠久,社区活跃,很多出生较早的项目基本都会选择它作为网络编程库。

目前使用 libevent 的知名项目有:MemcachedGoogle ChromentpdTor(洋葱路由)等等。

2007 年,为解决 libevent 多线程全局变量不安全、组件质量参差不齐等问题,Marc Lehmann 决定精简 libevent,去掉多余的组件(如 HTTP 和 DNS),只专注于事件驱动,并最终形成了 libev。可以理解为 libevlibevent 的一个分支版本。目前这一分支作者已停止维护,而且 libeventlibuv 却在社区推动下飞速发展,所以最后很多项目都不再使用 libev

目前 libev 使用它的知名项目有 ShçdôwSôcks(河蟹拼法)、Node.js 早期版本。

2011 年,在使用了 libev 作为内置 Web Server 仅仅两年后,Node.js 社区意识到了一些问题。一是前面提到项目维护问题;二是因为 Node.js 的日益流行,迫切需要跨平台支持。因此,由 Node.js 之父 Ryan Dahl 主导的 libuv 诞生。它也是由 C 语言编写,提供对基于事件循环的异步 I/O 的跨平台支持。最终,在 Node.js 0.9 版本中,libuv 完全取代了 libev

目前使用 libuv 的知名项目有:Node.jsASP .NET CoreCMakeJulia 等等。

事件驱动编程的流行,给 Web Server 开发带来来新的活力,很多编程语言都加入了对它们的封装引用,可以很方便、快捷地搭建出一个简单的 Web Server。但通常来说,都是用于快速搭建开发测试环境,目前还有没有一款基于此的、独立的 Web Server 产品出现。

后起之秀:WebServer 之 Nginx

2004 年,俄罗斯人 Igor Vladimirovich Sysoev 在经过了两年的开发后,发布了名为 Nginx 的 Web Server。

NginxEngine X 的缩写,即“超级引擎”之意。在设计之初,Nginx 就被赋予了一个明确的目标:全面超越 Apache HTTP Server 的性能。

Nginx 同时支持 NIO、AIO 两种 I/O 模型,在能支持大量并发连接的情况下,降低了内存占用,并提高了系统稳定性,完美地解决了 C10K 问题。

虽然 Nginx 在 Windows 系统上不如 Apache 表现稳定,更遑论 Microsoft 的亲儿子 IIS 了。但它的可扩展性和高性能,仍然吸引着大量开发者使用。

不过随着云平台的兴起,Nginx 又成为了很多云厂商的首选。例如:

  • Kubernetes 选择其作为 Ingress-Controller 组件的官方实现。
  • OpenRestry 选择其作为公司旗下平台产品的基础组件。
  • 阿里巴巴集团选择其二次开发,命名为 Tengine,是阿里云负载均衡器产品的基础组件,也是淘宝系统的重要组成部分。

截止目前为止,Nginx 已占据了 36% 以上的 Web Server 市场份额,正逐渐蚕食着 ApacheIIS 的市场份额。

长江后浪:WebServer 之 Netty

2011 年,在从 RedHad(红帽)公司独立出来并开源后,Netty,这个脱胎于 JBoss 的项目,在被 RedHat 收购之后,才终于迎来了它的高速发展期。

由于诞生日期很晚,在吸收了早期其他 Web Server 的经验教训后,Netty 直接采用了 NIO 的 I/O 模型,实现了其更高的并发性能。

Tomcat 一样,Netty 也是一个 Java 实现的 Web Server。这里要指出的是,后来 Tomcat 也支持了 NIO,还新引入了 APR 技术,所以目前 Netty 带来的性能优势已经不是很明显。

但与 Tomcat 是支持七层的 HTTP 等协议不同的是,而 Netty 是从四层开始支持 TCPUDP 等协议,除了充作 HTTP Web Server 外,还可以实现自己的高性能私有协议(如 RPC 协议)Web Server。


群星璀璨:其他知名 Web Server

本文着重介绍了早期的、和一些现阶段流行的 Web Server。

实际上,Web Server 领域曾经有无数的优秀作用,也正兴起着更多的、功能更强大的产品。

下面按发布时间顺序,列举另外一批比较出名的 Web Server:

  • thttpd:1995 年由 Jeffrey A. Poskanzer 开源的项目,由 C 语言编写。其得名于 Tiny HTTPd,意为“微小的 HTTPd”。因其功能简单,且只支持类 Unix 系统,所以占用资源消耗可以优化到很低,曾被视为是 Apache 的轻量级替代品,现在常被用于如路由器一类的嵌入式设备。该项目目前仍在维护,最新一个版本是 2018 年推出的 2.29 版。
  • Jetty:1995 年由 Greg Wilkins 开发的项目。最初起名为 IssueTrackerMBServler,后在使用 Java 重构后更名为 Jetty。2009 年项目被移交给 Eclipse 基金会。随着大数据技术的兴起,Jetty 因被集成在 Apache Hadoop 项目中而得以名声大噪,现在是 Eclipse IDE 和 Spring Boot 的内置容器之一。该项目目前仍在维护,最新一个版本是 2020 年推出的 11.0.0 版。
  • WebLogic:1997 年由 Oracle(甲骨文)公司推出的商业产品,由 Java 编写。最初的产品名为 WebLogic Tengah,后更名为 WebLogic Server。它是世界上第一个成功商业化的 J2EE 应用服务器。该产品目前仍在维护,最新一个版本是 2014 年推出的 12.1.3 版。
  • WebSphere:1998 年由 IBM 公司推出的商业产品,由 Java 编写,同样也是一款 J2EE 应用服务器。该产品目前仍在维护,最新一个版本是 2018 年推出的 9.0.5 版本。
  • lighttpd:2003 年由 Jan Kneschke 开源的项目,由 C 语言编写。其得名于 Lighty HTTPd,意为 “轻量级 HTTPd”。lighttpd 的源码十分简洁精练,有着很多拥趸。Bloglines、Meebo、YouTube(油管)、Wikipedia(维基百科)等著名网站都使用过 lighttpd 作为 Web Server,也被如路由器等很多嵌入式设备使用。该项目目前仍在维护,最新一个版本是 2020 年推出的 1.4.55 版。
  • Jexus:2008 年由 @宇内流云(本名刘冰)推出的免费产品,基于 Mono 的 .NET 跨平台 Web Server,可理解为 Linux 系统下的 IIS。支持 ASP .NET、ASP .NET Core、PHP、Node.js 等语言。搭配 Jexus Manager 可实现 GUI 化配置。该产品目前仍在维护,最新一个版本是 2018 年推出的 6.2 版。
  • Cherokee:2010 年由 Álvaro López Ortega 开源的项目,由 C 语言编写。号称比 Nginx 性能更高,但内存消耗会更大一些。功能丰富,支持 GUI 配置界面。该项目目前仍在维护,最新一个版本是 2013 年推出的 1.2.103 版。
  • Mongoose:2011 年由 Sergey Lyubka 开源的项目,由 C 语言编写。除 HTTP 协议及其衍生协议外,还支持 MQTT 和更底层的 TCP 协议,所以现在常被用于物联网智能设备中。该项目目前仍在维护,最新一个版本是 2020 年推出的 6.18 版。(注意:要与 MongoDB 数据库中的 Mongoose 相区分,两者没有关系)
  • Underow:2013 年由 RedHat 公司开源的项目,由 Java 编写。同样是 Spring Boot 内置容器之一。该项目目前仍在维护,最新一个版本是 2020 年推出的 2.1.3 版。
  • Caddy:2015 年由 Matthew Holt 开源的项目,由 Golang 编写。以开箱即用著称,内置 Markdown 预览功能,实现了 HTTPS 证书自动续约,支持丰富的扩展插件。这是一款新兴的 Web Server,目前还没有得到大规模的企业级服务端应用,反倒在搭建私人网站、网络硬盘等方面受到了个人用户欢迎。该项目目前仍在维护,最新一个版本是 2020 年推出的 2.0.0 版。

尾声

本文以时间线为基准,谈到了几个流行的 Web Server 及动态网页的发展。

其实无论是 Web Server,还是可用作动态网页的编程语言,都远不止提到的这些,但其他的都没有这些流行,这里也就不费笔墨。

下面我们再总结梳理一下时间线:

时间事件
1990 年Tim Berners-Lee 创造了 Common Library,又名 CERN HTTPd
1991 年NCSA 提出 SSI 概念。
1993 年Common Library 开源,更名为 libwww
1993 年NCSA 仿 libwww 创造了 NCSA HTTPd,实现了 SSI,并提出了 CGI 概念。
1994 年libwww 项目移交给 W3C,又称 W3C HTTPd
1995 年Jeffrey A. Poskanzer 开源 thttpd
1995 年Rasmus Lerdorf 开源 PHP 技术。
1995 年Microsoft 公司发布 IIS
1995 年NCSA HTTPd 的基础上,Brian Behlendorf 领导产生了新的分支 Apache
1995 年Greg Wilkins 开发了 IssueTracker,又名 MBServler
1996 年W3C 开源 Jigsaw
1996 年Microsoft 公司发布 ASP 技术。
1997 年Sun 公司发布 Java Servlet 技术。
1997 年Oracle 公司发布 WebLogic
1998 年Sun 公司发布 Java JSP 技术。
1998 年IBM 公司发布 WebSphere
1999 年Apache 社区开源 Tomcat
2000 年libevent 编程库问世。
2003 年Jan Kneschke 开源 lighttpd
2004 年Igor Vladimirovich Sysoev 开源 Nginx
2005 年libev 编程库问世。
2008 年@宇内流云(本名刘冰)发布 Jexus
2009 年IssueTracker 项目移交给 Eclipse 基金会,更名为 Jetty
2010 年Álvaro López Ortega 开源了 Cherokee
2011 年libuv 编程库问世。
2011 年Sergey Lyubka 开源了 Mongoose
2011 年RedHat 公司开源了 Netty
2013 年RedHat 公司开源了 Underow
2014 年Microsoft 公司开源了 Kestrel Server
2015 年Matthew Holt 开源了 Caddy

注:文章中的所有人物、事件、时间、地点,均来自于互联网公开内容,由本人进行搜集整理,其中如有谬误之处,还请多多指教。


参考阅读


首发于 Segmentfault.com,欢迎转载,转载请注明来源和作者。

RHQYZ, Write at 2020.06.29.

查看原文

abcbuzhiming 赞了文章 · 7月4日

《编程时间简史系列》Web Server 编年史

引言

本文是《编程时间简史系列》的第二篇文章。

全系列索引:

  1. 《编程时间简史系列》JavaScript 模块化的历史进程
  2. 《编程时间简史系列》Web Server 编年史

互联网今天已经广泛存在于人们的生活中,人们的衣食住行等方方面面早已离不开互联网的支撑,这其中离不开 Web 技术的发展。

Web 是一种典型的分布式应用架构。Web 应用中的每一次信息交换都要涉及到客户端和服务端两个层面。因此,Web 开发技术大体上也可以被分为客户端技术和服务端技术两大类。

本文将会讲述 Web 服务端技术的萌芽和演进过程,旨在使读者能更清晰地掌握 Web 服务端技术的发展脉络。

Web 服务端技术的发展与客户端技术的进步是相辅相成的,本文虽是讨论 Web 服务端,在讲述过程中却不可避免地会提及一些 Web 客户端的有关内容,但不会过多深入。对此感兴趣的读者,可以自行阅读最下方的参考链接。

同样的,不谈具体代码,只聊历史故事。

P.S. 下一篇的选题还没有敲定,如果有朋友想了解某一方面的历史又苦于没有资料,可以在文章下方给我留言。


正文

广义上的 Web Server(Web 服务器),包含硬件和软件两方面。今天我们只谈及其中的软件部分,即能向客户端提供 Web 服务的程序。

现在大家耳熟能详的 ApacheIISTomcatNginx 等等,都属于 Web Server。

那么它们之间究竟有何不同?又是由谁、在什么时间发明的?我们常说的静态网页、动态网页又是指什么?HTTPd 和 Web Server 有何不同?网上总提的 libuv,它是个啥?

让我们先带着这些疑问,回到 HTTP 协议尚未诞生的时代。

时代前夜:HTML、HTTP 与万维网

1960 年,Theodor Holm Nelson 在哈佛计算机编程的选修课程上,使用了当时哈佛大学唯一可用的计算机 —— IBM 7090。在临近课程结束的时候,Theodor Holm Nelson 决定使用机器语言编写一个计算机程序,让他能够将自己的笔记和手稿存储在计算机中,可以以各种方式修改和编辑草稿,并生成可打印的最终版本。在他的项目进行到第 4 万行左右的代码时,他开始意识到,他对这项任务的完成难度最初估计得过于乐观。

1963 年,已经从哈佛大学毕业的 Theodor Holm Nelson 决定将自己大学时的想法继续进行下去。他首次提出了名为 “HyperText”(超文本)的概念,并找到了一些志同道合、痴迷计算机的朋友,成立了 Project Xanadu,试图制订规范,并应用到实际的计算机程序中。

1969 年,IBM 公司的 Charles F. Goldfarb 发明了一种可以用于描述超文本文档的描述语言 —— Generalized Markup Language(简称为 GML,通用标记语言)。在之后的几年时间里,形成了 Standard Generalized Markup Language(简称为 SGML,标准通用标记语言)的标准规范,成为了 ISO 国际标准。

制订 SGML 的基本思想是把文档的内容与样式分开。在 SGML 中,标记分两种:一种用来描述文档显示的样式,称为程序标记;另一种用来描述文档中语句的用途,称为描述标记。一个 SGML 文件通常分三个层次:结构、内容和样式。结构为组织文档的元素提供框架,内容是信息本身,样式控制内容的显示。

不过,由于 GML/SGML 过于庞大且复杂,虽然有不少大公司在内部使用,但始终没能得到广泛的应用。

暂且按下 Theodor Holm NelsonCharles F. Goldfarb 这边不表,让我们来到 1989 年。

此时已是不惑之年的 Tim Berners-Lee,负责在 CERN(欧洲粒子物理实验室)做 IT 支持工作。由于粒子物理在当时是前沿研究领域,需要全世界的物理学家合作参与,那么如何与世界各地的物理研究所保持通信,就是一件十分重要也棘手的事情。

起初,CERN 使用传真机来传输文件,但物理传输速度极慢,且会耗费大量纸张与油墨,对于信息检索工作而言也十分不便。

后来,因 ARPANET 网络在美国军方和多所大学内成功使用,CERN 也开始采用这种使用计算机网络进行通信的方式来传输数据。

但在此时,可选择的网络协议并不多。从时间顺序上来看,有:

  • 要么是 1971 年出现的 FTP,用于传输文件。但这种方式不能直接展示文本内容,而是需要下载到本地后才能打开。更何况,即是打开了文件,如果需要同时显示包含文本、图片、音频、视频等信息的多媒体内容,那么需要特定的程序才能编辑、预览。
  • 要么是 1973 年出现的 TELNET 协议,可以与远程终端进行交互。但这种操作方式极其繁琐,且对于搞科研的物理科学家而言操作并不友好,往往还需要 Theodor Holm Nelson 这样的 IT 部门来配合。
  • 要么是 1982 年出现的 SMTP,通过电子邮件进行交流。但这种方式不适合用于信息的公开展示,只适合点对点或群组之间的信息沟通。

这一年年底,Tim Berners-Lee 向其上级提出了一项名为 Information Management: A Proposal(《关于信息化管理的建议》)的提议:使来自世界各地的远程站点的研究人员能够组织和汇集信息,在个人计算机上访问大量的科研文献,并建议在文档中链接其他文档。

在参考了 Theodor Holm Nelson 有关超文本的规范、并参考了 Charles F. GoldfarbGML/SGML 实现后,Tim Berners-Lee 于 1990 年发明了 Hypertext Markup Language(简称为 HTML,超文本标记语言)和 Hypertext Transfer Protocol(简称为 HTTP,超文本传输协议)。

1990 年,Tim Berners-Lee 创建了一款最原始的 GUI 的 HTML 浏览器(同时也是编辑器),和第一个 HTTP 服务器。

1991 年,Tim Berners-Lee 作为布道者开始广泛推广 Web 的理念,提出了 World Wide Web(万维网)的概念。

值得一提的是,在这一时期,Web 领域还有其他诸如 NNTP、Gopher 等传输协议。但它们都因为种种原因,没能像 HTTP 一样流行起来,最终消失在了历史长河之中。

1993 年,NCSA(美国国家超算应用中心)对此表现出了浓厚的兴趣,并开发了名为 Mosaic 的浏览器,于当年 4 月发布。

1994 年,第一届国际万维网大会于 4 月在瑞士日内瓦召开,World Wide Web Consortium(简称为 W3C,万维网联盟)组织正式成立,并从 IETF(互联网工程任务组)接管了 HTML 的标准制订工作。

同年 6 月,IETF 非正式地指出了之前在“民间”流传的 URL(统一资源定位符)与 URN(统一资源名称)这两种叫法的存在,并进一步地定义了一个名为 Uniform Resource Identifier(简称为 URI,统一资源标识符)的规范文法。

同年 10 月,CERN 的另一位 IT 员工 Håkon Wium Lie 吸收了当时已有的一些 Web 样式的实践经验,提出并发明了 Cascading Style Sheets(简称为 CSS,层叠样式表)。

同年 11 月,Mosaic 浏览器的开发人员创立了 Netscape(网景)公司,并发布了 Mosaic Netscape 浏览器 1.0 版本,后改名为 Netscape Navigator(网景导航者)。

1995 年,W3C 制订了 HTML 2.0 标准。

同年 5 月,Netscape 公司的工程师 Brendan Eich 发明了一门名为 LiveScript 的脚本语言,可用在 Web 服务器和浏览器。在之后与 Netscape Navigator 2.0 一同发布时,被改名为 JavaScript

同年 8 月,Microsoft(微软)旗下的 Internet Explorer(简称为 IE)1.0 版本正式发布。

1996 年,IETF 将 HTTP 列为互联网标准之一,并制订了 HTTP/1.0 协议标准。

同年 12 月,W3C 将 CSS 纳入工作范围,并在随后个几个月里制订了 CSS 1 标准。

1997 年,JavaScript 被提交给 ECMA(欧洲计算机制造商协会),并最终形成了编号 262、名为 ECMAScript 的标准规范。这一规范下包含了 Netscape 1995 年发明的 JavaScript、Microsoft 1995 年发明的 JScript、Adobe 1999 年发明的 ActionScript 等几个实现。在接下来的几年时间里,这一标准规范同样被 ISO(国际标准化组织)及 IEC (国际电工委员会)所采纳。

1997 - 1999 年,HTML 3.0HTTP/1.1HTML 4.0CSS 2ECMAScript 3 等标准先后被发布,并统治了今后二十余年的互联网。

终于,Web 时代降临。

开天辟地:CERN HTTPd 和 NCSA HTTPd

HTTPd,即 HTTP daemon 的缩写。

今天我们谈到这个名词,大部分人会把它认为是 Apache 的代名词。但这其实只是个误解(原因下文会提到)。

在类 Unix 的操作系统中,一个在后端周期性地执行某种任务或等待处理理某些事件的进程,往往被称为 “Daemon Process”(守护/幽灵进程)。HTTPd 即取此意,意思就是在后台处理 HTTP 请求的程序。

因此实际上来说,HTTPd 应该是近似等同于 Web Server。在 HTTP 协议尚未出现的时代,Web Server 一般指 FTP 服务器。但 HTTP 协议出现后,Web Server 就立刻变成了指代 HTTP 服务器。今天的 Web Server 一定会、但不仅仅只会支持 HTTP 协议及其衍生协议,还可能支持诸如 FTP、SMTP、MQTT 甚至是更底层的 TCP、UDP 协议。

1990 年年底,Tim Berners-Lee 在一台运行着 NeXTSTEP 系统的 NeXT Computer 上编写了首个 HTTPd 程序,起名为 Common Library。这是一个由 C 语言编写的组件,只能处理 HTTP 请求中的 GET 谓词,并不是一个独立且完整的程序。因其属于 CERN 项目的一部分,所以也被称为 CERN HTTPd

1993 年,Tim Berners-LeeCommon Library 从 CERN 项目中独立出来,更名为 libwww 并开源。

同年,NCSA 在此基础之上扩展并开发出了 NCSA HTTPd

1994 年,libwww 的开发维护工作转交给了 W3C,在此阶段,libwww 新增了很多特性,诸如兼容 HTTP/1.0、支持 CGI、虚拟主机等。此时它也被称为 W3C HTTPd

1996 年,W3C 的工作重心已经不在 libwww 上,迟迟没有新版本发布,并最终于 2003 年宣告项目中止。

libwww 提供了基础的 HTTP 协议的解析与包装方式,既可用于服务端,也可用于服务端,被广泛地使用在包括 MosaicLynxArenaMacWWW 在内的诸多早期 Web 程序中。

胎死腹中:夭折的 Jigsaw

上一小节提到,1996 年时 W3C 的工作重心已经不在 libwww 上,因为他们已经另有其他重点工作。

由于 libwww 只能被编译到类 Unix 的操作系统中,且只支持静态网页。随着 Web 技术的不断发展,以及 Windows 系统的广泛流行,W3C 亟需一种可以跨平台的的 Web 服务器。因此,W3C 将目光放在了横空出世、发展迅猛的一种跨平台编程语言 —— Java。

W3C 联合当时的拥有 Java 的 Sun(升阳)公司,开发了一个名为 Jigsaw 的程序。

它由 Java 编写,起初只作为 JDK 1.2 版本的一个模块发布,意图让开发者能快速搭建一个跨平台 Web 服务器项目。它采用了多线程的处理方式,兼容 HTTP/1.1,支持 SSL/TLS、WebDAV 等新特性,同时也是首个支持 Servlet 和 JSP 的服务器程序。由于 Java 的跨平台特性,它可以运行在 BeOS、AS-400、AIX、Unix、Solaris 2、OS/2、MacOS、Linux、Windows 95、Windows NT 等操作系统上。

但遗憾的是,W3C 组织内的大部分成员,都是 IT 巨头公司,随着它们分别发布了各自的 Web 服务器商业产品后,Jigsaw 项目已经在事实上被废弃。虽然 W3C 没有明确地宣布项目中止,但从提交记录上来看,2007 年以后已经没有新特性被引入了,仅仅在 2007 - 2010 四年时间里修复了三五个 Bug,从此就悄无声息。

虽然 Jigsaw 命运早夭,但因为它是第一个由 Java 编写的 Web Server,起到了很多纲领性的指导作用,为后续 Java 技术在 Web 领域的扩展打下了坚实的基础。

值得一提的是,JDK 9 中新引入了与 Jigsaw 同名的模块化方案,但与 Jigsaw HTTPd 并没有什么关联。

萌芽初生:SSI 的诞生与 CGI 的兴起

最早的 Web 服务器只是简单地响应浏览器发来的 HTTP 请求,并将存储在服务器上的 HTML 文件返回给浏览器。可以近似理解为拥有文档预览功能的 FTP。文档内容在文件未修改前就是不变的,所有访问 Web 的用户看到的内容都是相同的。

这也就是前文提到的所谓的“静态网页”,这显然满足不了人们对信息丰富性和多样性的强烈需求。

由此,Web 技术的发展出现了两条分支路线。一条是尝试向客户端、即浏览器引入动态交互,例如 Sun 公司的 Java Applet、Netscape 公司的 JavaScript、Microsoft 公司的 JScriptVBScript、Adobe 公司的 FlashActionScript 等等。另一条是试图从服务端、即 Web Server 入手,想在返回给客户端时就输出动态的内容。这两条路线都在未来有了十分迅猛的发展,我们今天按下客户端不表,只谈服务端这面。

1991 年,NCSA 首次提出了 Server Side Includes(简称为 SSI,服务端嵌入) 的概念,并在之后发布的 NCSA HTTPd 中实现这一技术。

不过 SSI 的功能十分有限,通常用于实现网站页面的公共部分引用,比如在网页底部重复出现的版权声明等信息。它既不支持运算表达式,也不能根据逻辑条件判断是否输出特定内容,更遑论支持数据库这种 “高级操作” 了。所以虽然早期的 Web Server 都支持这种技术,但它并没有流行起来。

1993 年,在 NCSA 发布 NCSA HTTPd 的同时,NCSA 又提出了 Common Gateway Interface(简称为 CGI,通用网关接口)这一概念,并在未来几年内先后制订了 CGI 1.0CGI 1.1CGI 1.2 等标准。

CGI 本质上来说,就是接受一个输入、并返回一个输出的程序。CGI 独立于 Web Server 存在,在收到特定请求后(这些请求通常以 /cgi-bin/ 路径开头),Web Server 将 HTTP 请求的内容作为参数传递给 CGI 程序,并将 CGI 程序的返回值作为 HTTP 响应返回给客户端。所以 CGI 程序不能独立运行,需要配合 Web Server 才能工作。

早期通常是在 Web Server 接受到一个请求后,开启一个新的进程来执行 CGI 程序,这种方式在请求量稍微大一些时,就会严重拖累服务器的性能。

所以,随后又诞生了 FastCGI(简称为 FCGI)技术。简单来说,就是一个常驻内存的进程池技术,可以复用进程,使得 CGI 的工作负载性能大大提升。

在今天,由于 CGI 编写的复杂难度过大,已经很少有人再直接应用这种技术(间接的还有很多)。但它的出现,给其他编程语言带来了启发,诸如 FCGISCGIWSGIServlet 乃至后来的动态脚本语言等技术不断涌现,它们都滥觞于 CGI

承前启后:WebServer 之 Apache HTTP Server

1995 年,在随着 NCSA HTTPd 1.3 版本的发布,NCSA 就逐渐放缓了对 NCSA HTTPd 版本的开发工作。但为了满足日益丰富的 Web 服务端技术的需要,NCSA HTTPd 的社区成员在 Brian Behlendorf 的领导下,决定在 NCSA HTTPd 1.3 版本的基础上创建一个新的分支,并取名为 Apache

为什么取名为 Apache?其中一个流传最广的解释是,Apache 是在 NCSA HTTPd 的基础上修改的,因此是一个 “修补过的”(a patchy)Web Server。

但后来在 Apache 的 2.0 版本里,Apache 社区已将 NCSA HTTPd 的源代码全部移除,二者在今天已经没有了直接关系。

Apache 在前人的基础上,支持了很多新的 Web 特性。例如:多种身份认证方案、支持 CGI、支持 SSL/TLS、支持 IPv6、反向代理、URL 重写、定制化日志文件等等。与此同时,在其 2.0 版本中还加入了对非 Unix 操作系统的跨平台支持。

随着 Apache 逐渐发展壮大,它成为了首个最为广泛使用的开源 Web Server,曾一度占领了 70% 以上的市场份额,现在是主流的 Web Server 之一。加之其可执行文件名为 httpd,所以很多后人也将 HTTPd 理解成 Apache 的代名词,但这只是个误解。

Apache 的设计理念,影响了很多后来的 Web Server,是开源世界和 Web 历史中不能不提的一环。

值得一提的是,Apache 社区在 1999 年成立了 Apache Software Foundation(Apache 软件基金会)组织,致力于支持开源软件事业。我们今天谈及 Apache,即指的是最初的 Apache HTTP Server,也指 Apache 软件基金会。

正如前文提到的那样,虽然被称为 Apache HTTP Server,但它不仅仅支持 HTTP 协议及其衍生协议,还可以通过插件的形式支持其他协议。

异军突起:WebServer 之 IIS

1995 年 5 月,在令世界为之疯狂的 Windows 95 上市的前三个月,Windows NT 3.51 发布,这是 Windows NT 3.X 系列中的最后一个版本,也是第一个支持全中文的 Windows 操作系统。

随着这一版本的发布,一个名为 Internet Information Services(简称为 IIS,互联网信息服务)的系统可选组件悄然到来。

由于 IIS 是在 Windows 操作系统平台下开发的,这也限制了它只能在 Windows 下运行,但它是首个支持以 GUI 方式配置的 Web Server。

Apache 一样,IIS 也支持 HTTP 协议及其衍生协议、FTP 协议、SMTP 协议等。

随着 Windows 的流行,IIS 也不断进行版本迭代,它曾一度接近 Apache 的市场份额,现在也是主流的 Web Server 之一。

诸神崛起:PHP、JSP 还是 ASP?

CGI 程序一般由 C、C++、Pascal 等语言编写,并在目标平台上编译成二进制可执行程序,这给开发维护工作带来了很多麻烦。

为了简化 CGI 程序的修改、编译和发布流程,人们开始探寻用无需编译的脚本语言来实现 CGI 应用的道路。

很快,第一个用 Perl 写成的 CGI 程序问世。很快,Perl 在 CGI 编程领域的风头就盖过了它的前辈 C 语言。随后,Python 等著名的脚本语言也陆续加入了 CGI 编程语言的行列。不过随着 CGI 技术本身的衰落,Perl 最终在其后续版本中移除了 CGI 的相关模块。

1994 年,丹麦裔加拿大人 Rasmus Lerdorf 用 Perl 编写了一个简单的程序,用于统计他的个人主页的访问者。后来,Rasmus Lerdorf 用 C 语言重新编写了一遍这个程序,并在 1995 年以 Personal Home Page Tools(简称为 PHP Tools,个人主页工具)的名义开源了 PHP 1.0 版本。

在这早期的版本中,提供了访客留言本、访客计数器等简单的功能。以后越来越多的网站使用了 PHP,并且强烈要求增加如循环语句、数组变量等新特性,在新的社区成员加入开发行列后,1995 年,PHP 2.0 版本顺利发布。在这个版本中,PHP 添加了对 MySQL 数据库的支持,从此建立了其在动态网页开发上的地位。

PHP 最早使用 CGI 的工作方式(即 php-cgi),后因为这种方式的性能损耗很大,所以又开发了基于 FastCGI 的版本(即 php-fpmPHP FastCGI Process Manager 的缩写)。

但与早期 CGI 不同的是,PHP 首次将 HTML 代码和 PHP 指令合成为完整的服务端文档,Web 应用的开发者可以用一种更加简便、快捷的方式实现动态 Web 网页功能。

1996 年,Microsoft 公司在借鉴了 PHP 的思想后,在其产品 IIS 3.0 版本中引入了名为 Active Server Pages(简称为 ASP,动态服务器网页)的技术。

ASP 使用的脚本语言是 JScript 和 VBScript。借助 Microsot Office FrontPage、Microsoft Visual Studio 等开发工具在市场上的成功,ASP 迅速成为了 Windows 系统下 Web 服务端的主流开发技术。

需要说明的,Microsoft 在之后的 .NET Framework 和 .NET Core 体系中,还分别引入的名为 ASP .NETASP .NET Core 的技术。如果说后两者还师出同门,只不过一个只在 Windows 上运行、一个能跨平台运行;而 ASP 则和后两者只有名字上得到了传承,实际上已经没什么关系了。

当然,以 Sun 公司为首的 Java 阵营也不会示弱。1997 年,Servlet 技术问世。1998 年,Java Server Pages(简称为 JSP,Java 服务器页面)技术诞生。

其中 Servlet 类似于 CGI/FastCGI 处理;JSP 则类似于 PHP 的 HTML 模版。前者对于拼接 HTML 不是很擅长,后者对于运算和逻辑写起来又很繁琐,那么有没有可以把二者优势相结合的办法呢?

答案是肯定的,这也就是著名的 MVCModel-View-Controller)架构。虽然 MVC 架构早在 1978 年就在 Smalltalk 上提出,在 GUI 领域上也有 Microsoft 推出的 Microsoft Foundation Classes(简称为 MFC,微软基础类库)丰富实践,但这还是首次在 Web 领域得到应用。

这种 Servlet + JSP 组合的方式,后来也反过来影响了之前出现的 PHPASP,二者最终在后续版本中引入了类似的功能。

至此,扩展到 Web 领域的语言(如 Perl、Python),以及专为 Web 而生的语言(如 PHP、ASP、JSP),这些主流的脚本语言已全部出现,它们最终引领了 Web 下一个时代的前进方向。

容器之路:WebServer 之 Apache Tomcat

上文提到,无论 Apache 也好、IIS 也罢,本身并不直接生成动态页面,而是需要以 CGI/FastCGI 的方式将 HTTP 请求转发给相应的处理程序,才能返回动态页面。

PHPASPJSP 等脚本语言的出现,虽然已经不需要 Web 开发人员手工编写复杂的 CGI/FastCGI 程序来解析、封装 HTTP 报文,而是专注于业务逻辑本身即可。但这种方式其实质还是 Web Server + CGI/FastCGI 二者独立运行的方式。

那么有没有直接能生成动态 HTML 内容、无需 CGI/FastCGI 配合的 Web Server 呢?

1999 年,Tomcat 应运而生。Tomcat 既是 HTTP Web Server,也是 Java 执行容器,它由 Catalina Servlet 容器、Coyote 连接器、Jasper JSP 引擎等组件组成,可以直接输出动态 HTML 文档。

由于 Tomcat 也是 Apache 软件基金会的顶级项目之一,所以也被称为 Apache Tomcat

早期的 Tomcat 由于性能不佳(尤其是针对纯静态内容),通常还是要与 Apache HTTP Server 或者其他 Web Server 一起工作,除了用于开发过程中的调试以及那些对速度要求很低的开发者,很少会将 Tomcat 单独作为 Web Server。

这也给很多人造成了误解,以为 Tomcat 是那些基于 CGI/FastCGI 技术的脚本语言类似,是专门运行 ServletJSP 的程序。其实这也是一种误解,无论是 Servelet 还是 JSP,它们都比 Tomcat 面世的要早;而 Tomcat 完全可以脱离 Apache HTTP Server 独立运行,充当 Web Server。

但随着 Tomcat 版本的不断迭代,以及 Web Server 集群技术的广泛使用,正有越来越多的开发者将其单独作为 Web Server。

为了和早期那种只支持静态网页的 Web Server 加以区分,我们把这类 Web Server 也称之为 Application Server,即应用服务器。

Tomcat 这种 Web Server + 执行容器的双重身份的方式,后来也有越来越多的 Java 开源产品采用,诸如 JettyNettyUnderow 等等。

值得一提的是,2014 年,Microsoft 发布了 ASP .NET vNext 首个预览版,也就是后来的 ASP .NET Core,从这一版本开始,Microsoft 也实现了类似的产品,名为 Kestrel Server

风起云涌:libevent、libev、libuv,C10K 的法宝

网络通信,本质上就是对网卡或网络虚拟设备进行 I/O 操作。

早期的操作系统,基本都是阻塞 I/O(即 BIO),这种方式在面对大量并发时,会显得力不从心。上文提到的各种 Web Server 都是基于这种实现方式。

在这一时期,很多 Web Server 都会遇到著名的 “C10K” 问题,即:当请求的并发数量达到一万后,Web Server 的性能会随之急剧下降。

为了缓解并发问题,后来又出现了非阻塞 I/O(即 NIO)、异步 I/O (即 AIO)、I/O 多路复用等模型。例如 Unix 系统下的 pollselect,Solaris 系统下的 /dev/poll,BSD 系统下的 kqueue,Linux 系统下的 epoll,Windows 系统下的 IOCP 等等。它们各自的区别和优缺点我们这里不做展开,感兴趣的朋友可以自己搜索相关资料。

2000 年,libevent 问世。这是一个由 C 语言编写的、轻量级的开源高性能事件驱动编程库。起初它只兼容类 Unix 操作系统,在其他系统上性能并不高,后来在社区的推动下才慢慢支持 Windows 等操作系统的 IOCP 模型。不过因为它历史悠久,社区活跃,很多出生较早的项目基本都会选择它作为网络编程库。

目前使用 libevent 的知名项目有:MemcachedGoogle ChromentpdTor(洋葱路由)等等。

2007 年,为解决 libevent 多线程全局变量不安全、组件质量参差不齐等问题,Marc Lehmann 决定精简 libevent,去掉多余的组件(如 HTTP 和 DNS),只专注于事件驱动,并最终形成了 libev。可以理解为 libevlibevent 的一个分支版本。目前这一分支作者已停止维护,而且 libeventlibuv 却在社区推动下飞速发展,所以最后很多项目都不再使用 libev

目前 libev 使用它的知名项目有 ShçdôwSôcks(河蟹拼法)、Node.js 早期版本。

2011 年,在使用了 libev 作为内置 Web Server 仅仅两年后,Node.js 社区意识到了一些问题。一是前面提到项目维护问题;二是因为 Node.js 的日益流行,迫切需要跨平台支持。因此,由 Node.js 之父 Ryan Dahl 主导的 libuv 诞生。它也是由 C 语言编写,提供对基于事件循环的异步 I/O 的跨平台支持。最终,在 Node.js 0.9 版本中,libuv 完全取代了 libev

目前使用 libuv 的知名项目有:Node.jsASP .NET CoreCMakeJulia 等等。

事件驱动编程的流行,给 Web Server 开发带来来新的活力,很多编程语言都加入了对它们的封装引用,可以很方便、快捷地搭建出一个简单的 Web Server。但通常来说,都是用于快速搭建开发测试环境,目前还有没有一款基于此的、独立的 Web Server 产品出现。

后起之秀:WebServer 之 Nginx

2004 年,俄罗斯人 Igor Vladimirovich Sysoev 在经过了两年的开发后,发布了名为 Nginx 的 Web Server。

NginxEngine X 的缩写,即“超级引擎”之意。在设计之初,Nginx 就被赋予了一个明确的目标:全面超越 Apache HTTP Server 的性能。

Nginx 同时支持 NIO、AIO 两种 I/O 模型,在能支持大量并发连接的情况下,降低了内存占用,并提高了系统稳定性,完美地解决了 C10K 问题。

虽然 Nginx 在 Windows 系统上不如 Apache 表现稳定,更遑论 Microsoft 的亲儿子 IIS 了。但它的可扩展性和高性能,仍然吸引着大量开发者使用。

不过随着云平台的兴起,Nginx 又成为了很多云厂商的首选。例如:

  • Kubernetes 选择其作为 Ingress-Controller 组件的官方实现。
  • OpenRestry 选择其作为公司旗下平台产品的基础组件。
  • 阿里巴巴集团选择其二次开发,命名为 Tengine,是阿里云负载均衡器产品的基础组件,也是淘宝系统的重要组成部分。

截止目前为止,Nginx 已占据了 36% 以上的 Web Server 市场份额,正逐渐蚕食着 ApacheIIS 的市场份额。

长江后浪:WebServer 之 Netty

2011 年,在从 RedHad(红帽)公司独立出来并开源后,Netty,这个脱胎于 JBoss 的项目,在被 RedHat 收购之后,才终于迎来了它的高速发展期。

由于诞生日期很晚,在吸收了早期其他 Web Server 的经验教训后,Netty 直接采用了 NIO 的 I/O 模型,实现了其更高的并发性能。

Tomcat 一样,Netty 也是一个 Java 实现的 Web Server。这里要指出的是,后来 Tomcat 也支持了 NIO,还新引入了 APR 技术,所以目前 Netty 带来的性能优势已经不是很明显。

但与 Tomcat 是支持七层的 HTTP 等协议不同的是,而 Netty 是从四层开始支持 TCPUDP 等协议,除了充作 HTTP Web Server 外,还可以实现自己的高性能私有协议(如 RPC 协议)Web Server。


群星璀璨:其他知名 Web Server

本文着重介绍了早期的、和一些现阶段流行的 Web Server。

实际上,Web Server 领域曾经有无数的优秀作用,也正兴起着更多的、功能更强大的产品。

下面按发布时间顺序,列举另外一批比较出名的 Web Server:

  • thttpd:1995 年由 Jeffrey A. Poskanzer 开源的项目,由 C 语言编写。其得名于 Tiny HTTPd,意为“微小的 HTTPd”。因其功能简单,且只支持类 Unix 系统,所以占用资源消耗可以优化到很低,曾被视为是 Apache 的轻量级替代品,现在常被用于如路由器一类的嵌入式设备。该项目目前仍在维护,最新一个版本是 2018 年推出的 2.29 版。
  • Jetty:1995 年由 Greg Wilkins 开发的项目。最初起名为 IssueTrackerMBServler,后在使用 Java 重构后更名为 Jetty。2009 年项目被移交给 Eclipse 基金会。随着大数据技术的兴起,Jetty 因被集成在 Apache Hadoop 项目中而得以名声大噪,现在是 Eclipse IDE 和 Spring Boot 的内置容器之一。该项目目前仍在维护,最新一个版本是 2020 年推出的 11.0.0 版。
  • WebLogic:1997 年由 Oracle(甲骨文)公司推出的商业产品,由 Java 编写。最初的产品名为 WebLogic Tengah,后更名为 WebLogic Server。它是世界上第一个成功商业化的 J2EE 应用服务器。该产品目前仍在维护,最新一个版本是 2014 年推出的 12.1.3 版。
  • WebSphere:1998 年由 IBM 公司推出的商业产品,由 Java 编写,同样也是一款 J2EE 应用服务器。该产品目前仍在维护,最新一个版本是 2018 年推出的 9.0.5 版本。
  • lighttpd:2003 年由 Jan Kneschke 开源的项目,由 C 语言编写。其得名于 Lighty HTTPd,意为 “轻量级 HTTPd”。lighttpd 的源码十分简洁精练,有着很多拥趸。Bloglines、Meebo、YouTube(油管)、Wikipedia(维基百科)等著名网站都使用过 lighttpd 作为 Web Server,也被如路由器等很多嵌入式设备使用。该项目目前仍在维护,最新一个版本是 2020 年推出的 1.4.55 版。
  • Jexus:2008 年由 @宇内流云(本名刘冰)推出的免费产品,基于 Mono 的 .NET 跨平台 Web Server,可理解为 Linux 系统下的 IIS。支持 ASP .NET、ASP .NET Core、PHP、Node.js 等语言。搭配 Jexus Manager 可实现 GUI 化配置。该产品目前仍在维护,最新一个版本是 2018 年推出的 6.2 版。
  • Cherokee:2010 年由 Álvaro López Ortega 开源的项目,由 C 语言编写。号称比 Nginx 性能更高,但内存消耗会更大一些。功能丰富,支持 GUI 配置界面。该项目目前仍在维护,最新一个版本是 2013 年推出的 1.2.103 版。
  • Mongoose:2011 年由 Sergey Lyubka 开源的项目,由 C 语言编写。除 HTTP 协议及其衍生协议外,还支持 MQTT 和更底层的 TCP 协议,所以现在常被用于物联网智能设备中。该项目目前仍在维护,最新一个版本是 2020 年推出的 6.18 版。(注意:要与 MongoDB 数据库中的 Mongoose 相区分,两者没有关系)
  • Underow:2013 年由 RedHat 公司开源的项目,由 Java 编写。同样是 Spring Boot 内置容器之一。该项目目前仍在维护,最新一个版本是 2020 年推出的 2.1.3 版。
  • Caddy:2015 年由 Matthew Holt 开源的项目,由 Golang 编写。以开箱即用著称,内置 Markdown 预览功能,实现了 HTTPS 证书自动续约,支持丰富的扩展插件。这是一款新兴的 Web Server,目前还没有得到大规模的企业级服务端应用,反倒在搭建私人网站、网络硬盘等方面受到了个人用户欢迎。该项目目前仍在维护,最新一个版本是 2020 年推出的 2.0.0 版。

尾声

本文以时间线为基准,谈到了几个流行的 Web Server 及动态网页的发展。

其实无论是 Web Server,还是可用作动态网页的编程语言,都远不止提到的这些,但其他的都没有这些流行,这里也就不费笔墨。

下面我们再总结梳理一下时间线:

时间事件
1990 年Tim Berners-Lee 创造了 Common Library,又名 CERN HTTPd
1991 年NCSA 提出 SSI 概念。
1993 年Common Library 开源,更名为 libwww
1993 年NCSA 仿 libwww 创造了 NCSA HTTPd,实现了 SSI,并提出了 CGI 概念。
1994 年libwww 项目移交给 W3C,又称 W3C HTTPd
1995 年Jeffrey A. Poskanzer 开源 thttpd
1995 年Rasmus Lerdorf 开源 PHP 技术。
1995 年Microsoft 公司发布 IIS
1995 年NCSA HTTPd 的基础上,Brian Behlendorf 领导产生了新的分支 Apache
1995 年Greg Wilkins 开发了 IssueTracker,又名 MBServler
1996 年W3C 开源 Jigsaw
1996 年Microsoft 公司发布 ASP 技术。
1997 年Sun 公司发布 Java Servlet 技术。
1997 年Oracle 公司发布 WebLogic
1998 年Sun 公司发布 Java JSP 技术。
1998 年IBM 公司发布 WebSphere
1999 年Apache 社区开源 Tomcat
2000 年libevent 编程库问世。
2003 年Jan Kneschke 开源 lighttpd
2004 年Igor Vladimirovich Sysoev 开源 Nginx
2005 年libev 编程库问世。
2008 年@宇内流云(本名刘冰)发布 Jexus
2009 年IssueTracker 项目移交给 Eclipse 基金会,更名为 Jetty
2010 年Álvaro López Ortega 开源了 Cherokee
2011 年libuv 编程库问世。
2011 年Sergey Lyubka 开源了 Mongoose
2011 年RedHat 公司开源了 Netty
2013 年RedHat 公司开源了 Underow
2014 年Microsoft 公司开源了 Kestrel Server
2015 年Matthew Holt 开源了 Caddy

注:文章中的所有人物、事件、时间、地点,均来自于互联网公开内容,由本人进行搜集整理,其中如有谬误之处,还请多多指教。


参考阅读


首发于 Segmentfault.com,欢迎转载,转载请注明来源和作者。

RHQYZ, Write at 2020.06.29.

查看原文

赞 23 收藏 8 评论 4

abcbuzhiming 关注了专栏 · 7月4日

编程时间简史

编程考古学,不写代码,只谈历史,适合作为程序员们吹逼的谈资。

关注 324

abcbuzhiming 收藏了文章 · 7月4日

《编程时间简史系列》JavaScript 模块化的历史进程

引言

昨天在思否上闲逛,发现了一个有意思的问题(点此传送)。

因为这个问题,我产生了写一个系列文章的想法,试图从站在历史的角度上来看待编程世界中林林总总的问题和解决方案。

目前中文网络上充斥着大量互相“转载”的内容,基本是某一个技术问题的解决方案(what?how?),却不涉及为什么这么做和历史缘由(why?when?)。比如你要搜 “JavaScript 有哪些模块化方案?它们有什么区别?”,能得到一万个有用的结果;但要想知道 “为什么 JavaScript 有这么多模块化方案?它们是谁创建的?”,却几乎不可能。

因此,这一系列文章内会尽可能的不涉及具体代码,只谈历史故事。但会在文末提供包含部分代码的参考链接,以供感兴趣的朋友自行阅读。

这个系列暂定为十篇文章,内容会涉及前端、后端、编程语言、开发工具、操作系统等等。也给自己立个 Flag,在今年年底之前把整个系列写完。如果没完成目标……就当我没说过这句话(逃

全系列索引:

  1. 《编程时间简史系列》JavaScript 模块化的历史进程
  2. 《编程时间简史系列》Web Server 编年史

正文

模块化,是前端绕不过去的话题。

随着 Node.js 和三大框架的流行,越来越多的前端开发者们脑海中都会时常浮现一个问题:

为什么 JavaScript 有这么多模块化方案?

自从 1995 年 5 月,Brendan Eich 写下了第一行 JavaScript 代码起,JavaScript 已经诞生了 25 年。

但这门语言早期仅仅作为轻量级的脚本语言,用于在 Web 上与用户进行少量的交互,并没有依赖管理的概念。

随着 AJAX 技术得以广泛使用,Web 2.0 时代迅猛发展,浏览器承载了愈来愈多的内容与逻辑,JavaScript 代码越来越复杂,全局变量冲突、依赖管理混乱等问题始终萦绕在前端开发者们的心头。此时,JavaScript 亟需一种在其他语言中早已得到良好应用的功能 —— 模块化。

其实,JavaScript 本身的标准化版本 ECMAScript 6.0 (ES6/ES2015) 中,已经提供了模块化方案,即 ES Module。但目前在 Node.js 体系下,最常见的方案其实是 CommonJS。再加上大家耳熟能详的 AMDCMDUMD,模块化的事实标准如此之多。

那么为什么有如此之多的模块化方案?它们又是在怎样的背景下诞生的?为什么没有一个方案 “千秋万代,一统江湖”?

接下来,我会按照时间顺序讲述模块化的发展历程,顺带也就回答了上述几个问题。

萌芽初现:从 YUI Library 和 jQuery 说起

时间回到 2006 年 1 月,当时还是国际互联网巨头的 Yahoo(雅虎),开源了其内部使用已久的组件库 YUI Library

YUI Library 采用了类似于 Java 命名空间的方式,来隔离各个模块之间的变量,避免全局变量造成的冲突。其写法类似于:

YUI.util.module.doSomthing();

这种写法无论是封装还是调用时都十分繁琐,而且当时的 IDE 对于 JavaScript 来说智能感知非常弱,开发者很难知道他需要的某个方法存在于哪个命名空间下,经常需要频繁地查阅开发手册,导致开发体验十分不友好。

在 YUI 发布之后不久,John Resig 发布了 jQuery。当时年仅 23 岁的他,不会知道自己这一时兴起在 BarCamp 会议上写下的代码,将占据未来十几年的 Web 领域。

jQuery 使用了一种新的组织方式,它利用了 JavaScript 的 IIFE(立即执行函数表达式)和闭包的特性,将所依赖的外部变量传给一个包装了自身代码的匿名函数,在函数内部就可以使用这些依赖,最后在函数的结尾把自身暴露给 window。这种写法被很多后来的框架所模仿,其写法类似于:

(function(root){
    // balabala
    root.jQuery = root.$ = jQuery;
})(window);

这种写法虽然灵活性大大提升,可以很方便地添加扩展,但它并未解决根本问题:所需依赖还是得外部提前提供,还是会增加全局变量。

从以上的尝试中,可以归纳出 JavaScript 模块化需要解决哪些问题:

  1. 如何给模块一个唯一标识?
  2. 如何在模块中使用依赖的外部模块?
  3. 如何安全地(不污染模块外代码)包装一个模块?
  4. 如何优雅地(不增加全局变量)把模块暴漏出去?

围绕着这些问题,JavaScript 模块化开始了一段曲折的探索之路。

探索之路:CommonJS 与 Node.js 的诞生

让我们来到 2009 年 1 月,此时距离 ES6 发布尚有 5 年的时间,但前端领域已经迫切地需要一套真正意义上的模块化方案,以解决全局变量污染和依赖管理混乱等问题。

Mozilla 旗下的工程师 Kevin Dangoor,在工作之余,与同事们一起制订了一套 JavaScript 模块化的标准规范,并取名为 ServerJS

ServerJS 最早用于服务端 JavaScript,旨在为配合自动化测试等工作而提供模块导入功能。

这里插一句题外话,其实早期 1995 年,Netsacpe(网景)公司就提供了有在服务端执行 JavaScript 能力的产品,名为 Netscape Enterprise Server。但此时服务端能做的 JavaScript 还是基于浏览器来实现的,本身没有脱离其自带的 API 范围。直到 2009 年 5 月,Node.js 诞生,赋予了其文件系统、I/O 流、网络通信等能力,才真正意义上的成为了一门服务端编程语言。

2009 年年初,Ryan Dahl 产生了创造一个跨平台编程框架的想法,想要基于 Google(谷歌)的 Chromium V8 引擎来实现。经过几个月紧张的开发工作,在 5 月中旬,Node.js 首个预览版本的开发工作已全部结束。同年 8 月,欧洲 JSConf 开发者大会上,Node.js 惊艳亮相。

但在此刻,Node.js 还没有一款包管理工具,外部依赖依然要手动下载到项目目录内再引用。欧洲 JSConf 大会结束后,Isaac Z. Schlueter 注意到了 Ryan DahlNode.js,两人一拍即合,决定开发一款包管理工具,也就是后来大名鼎鼎的 Node Package Manager(即 npm)。

在开发之初,摆在二人面前的第一个问题就是,采用何种模块化方案?。二人将目光锁定在了几个月前(2009 年 4 月)在华盛顿特区举办的美国 JSConf 大会上公布的 ServerJS。此时的 ServerJS 已经更名为 CommonJS,并重新制订了标准规范,即Modules/1.0,展现了更大的野心,企图一统所有编程语言的模块化方案。

具体来说,Modules/1.0标准规范包含以下内容:

  1. 模块的标识应遵循一定的书写规则。
  2. 定义全局函数 require(dependency),通过传入模块标识来引入其他依赖模块,执行的结果即为别的模块暴漏出来的 API。
  3. 如果被 require 函数引入的模块中也包含外部依赖,则依次加载这些依赖。
  4. 如果引入模块失败,那么 require 函数应该抛出一个异常。
  5. 模块通过变量 exports 来向外暴露 API,exports 只能是一个 object 对象,暴漏的 API 须作为该对象的属性。

由于这个规范简单而直接,Node.jsnpm 很快就决定采用这种模块化的方案。至此,第一个 JavaScript 模块化方案正式登上了历史舞台,成为前端开发中必不可少的一环。

需要注意的是,CommonJS 是一系列标准规范的统称,它包含了多个版本,从最早 ServerJS 时的 Modules/0.1,到更名为 CommonJS 后的 Modules/1.0,再到现在成为主流的 Modules/1.1。这些规范有很多具体的实现,且不只局限于 JavaScript 这一种语言,只要遵循了这一规范,都可以称之为 CommonJS。其中,Node.js 的实现叫做 Common Node ModulesCommonJS 的其他实现,感兴趣的朋友可以阅读本文最下方的参考链接。

值得一提的是,CommonJS 虽然没有进入 ECMAScript 标准范围内,但 CommonJS 项目组的很多成员,也都是 TC39(即制订 ECMAScript 标准的委员会组织)的成员。这也为日后 ES6 引入模块化特性打下了坚实的基础。

分道扬镳:CommonJS 历史路口上的抉择

在推出 Modules/1.0 规范后,CommonJSNode.js 等环境下取得了很不错的实践。

但此时的 CommonJS 有两个重要问题没能得到解决,所以迟迟不能推广到浏览器上:

  1. 由于外层没有 function 包裹,被导出的变量会暴露在全局中。
  2. 在服务端 require 一个模块,只会有磁盘 I/O,所以同步加载机制没什么问题;但如果是浏览器加载,一是会产生开销更大的网络 I/O,二是天然异步,就会产生时序上的错误。

因此,社区意识到,要想在浏览器环境中也能顺利使用 CommonJS,势必重新制订新的标准规范。但新的规范怎么制订,成为了激烈争论的焦点,分歧和冲突由此诞生,逐步形成了三大流派:

  • Modules/1.x 派:这派的观点是,既然 Modules/1.0 已经在服务器端有了很好的实践经验,那么只需要将它移植到浏览器端就好。在浏览器加载模块之前,先通过工具将模块转换成浏览器能运行的代码了。我们可以理解为他们是“保守派”。
  • Modules/Async 派:这派认为,既然浏览器环境于服务器环境差异过大,那么就不应该继续在 Modules/1.0 的基础上小修小补,应该遵循浏览器本身的特点,放弃 require 方式改为回调,将同步加载模块变为异步加载模块,这样就可以通过 ”下载 -> 回调“ 的方式,避免时序问题。我们可以理解为他们是“激进派”。
  • Modules/2.0 派:这派同样也认为不应该沿用 Modules/1.0,但也不向激进派一样过于激进,认为 require 等规范还是有可取之处,不应该随随便便放弃,而是要尽可能的保持一致;但激进派的优点也应该吸收,比如 exports 也可以导出其他类型、而不仅局限于 object 对象。我们可以理解为他们是“中间派”。

其中保守派的思路跟今天通过 babel 等工具,将 JavaScript 高版本代码转译为低版本代码如出一辙,主要目的就是为了兼容。有了这种想法,这派人马提出了 Modules/Transport 规范,用于规定模块如何转译。browserify 就是这一观点下的产物。

激进派也提出了自己的规范 Modules/AsynchronousDefinition,奈何这一派的观点并没有得到 CommonJS 社区的主流认可。

中间派同样也有自己的规范 Modules/Wrappings,但这派人马最后也不了了之,没能掀起什么风浪。

激进派、中间派与保守派的理念不和,最终为 CommonJS 社区分裂埋下伏笔。

百家争鸣:激进派 —— AMD 的崛起

激进派的 James Burke 在 2009 年 9 月开发出了 RequireJS 这一模块加载器,以实践证明自己的观点。

但激进派的想法始终得不到 CommonJS 社区主流认可。双方的分歧点主要在于执行时机问题,Modules/1.0 是延迟加载、且同一模块只执行一次,而 Modules/AsynchronousDefinition 却是提前加载,加之破坏了就近声明(就近依赖)原则,还引入了 define 等新的全局函数,双方的分歧越来越大。

最终,在 James BurkeKarl Westin 等人的带领下,激进派于同年年底宣布离开 CommonJS 社区,自立门户。

激进派在离开社区后,起初专注于 RequireJS 的开发工作,并没有过多的涉足社区工作,也没有此草新的标准规范。

2011 年 2 月,在 RequireJS 的拥趸们的共同努力下,由 Kris Zyp 起草的 Async Module Definition(简称 AMD)标准规范正式发布,并在 RequireJS 社区的基础上建立了 AMD 社区。

AMD 标准规范主要包含了以下几个内容:

  1. 模块的标识遵循 CommonJS Module Identifiers
  2. 定义全局函数 define(id, dependencies, factory),用于定义模块。dependencies 为依赖的模块数组,在 factory 中需传入形参与之一一对应。
  3. 如果 dependencies 的值中有 requireexportsmodule,则与 CommonJS 中的实现保持一致。
  4. 如果 dependencies 省略不写,则默认为 ['require', 'exports', 'module']factory 中也会默认传入三者。
  5. 如果 factory 为函数,模块可以通过以下三种方式对外暴漏 API:return 任意类型;exports.XModule = XModulemodule.exports = XModule
  6. 如果 factory 为对象,则该对象即为模块的导出值。

其中第三、四两点,即所谓的 Modules/Wrappings,是因为 AMD 社区对于要写一堆回调这种做法颇有微辞,最后 RequireJS 团队妥协,搞出这么个部分兼容支持。

因为 AMD 符合在浏览器端开发的习惯方式,也是第一个支持浏览器端的 JavaScript 模块化解决方案,RequireJS 迅速被广大开发者所接受。

但有 CommonJS 珠玉在前,很多开发者对于要写很多回调的方式颇有微词。在呼吁高涨声中,RequireJS 团队最终妥协,搞出个 Simplified CommonJS wrapping(简称 CJS)的兼容方式,即上文的第三、四两点。但由于背后实际还是 AMD,所以只是写法上做了兼容,实际上并没有真正做到 CommonJS 的延迟加载。

CommonJS 规范有众多实现不同的是,AMD 只专注于 JavaScript 语言,且实现并不多,目前只有 RequireJSDojo Toolkit,其中后者已经停止维护。

一波三折:中间派 —— CMD 的衰落

由于 AMD 的提前加载的问题,被很多开发者担心会有性能问题而吐槽。

例如,如果一个模块依赖了十个其他模块,那么在本模块的代码执行之前,要先把其他十个模块的代码都执行一遍,不管这些模块是不是马上会被用到。这个性能消耗是不容忽视的。

为了避免这个问题,上文提到,中间派试图保留 CommonJS 书写方式和延迟加载、就近声明(就近依赖)等特性,并引入异步加载机制,以适配浏览器特性。

其中一位中间派的大佬 Wes Garland,本身是 CommonJS 的主要贡献者之一,在社区中很受尊重。他在 CommonJS 的基础之上,起草了 Modules/2.0,并给出了一个名为 BravoJS 的实现。

另一位中间派大佬 @khs4473 提出了 Modules/Wrappings,并给出了一个名为 FlyScript 的实现。

Wes Garland 本人是学院派,理论功底十分扎实,但写出的作品却既不优雅也不实用。而实战派的 @khs4473 则在与 James Burke 发生了一些争论,最后删除了自己的 GitHub 仓库并停掉了 FlyScript 官网。

到此为止,中间一派基本已全军覆灭,空有理论,没有实践。

让我们前进到 2011 年 4 月,国内阿里巴巴集团的前端大佬玉伯(本名王保平),在给 RequireJS 不断提出建议却被拒绝之后,萌生了自己写一个模块加载器的想法。

在借鉴了 CommonJSAMD 等模块化方案后,玉伯写出了 SeaJS,不过这一实现并没有严格遵守 Modules/Wrappings 的规范,所以严格来说并不能称之为 Modules/2.0。在此基础上,玉伯提出了 Common Module Definition(简称 CMD)这一标准规范。

CMD 规范的主要内容与 AMD 大致相同,不过保留了 CommonJS 中最重要的延迟加载、就近声明(就近依赖)特性。

随着国内互联网公司之间的技术交流,SeaJS 在国内得到了广泛使用。不过在国外,也许是因为语言障碍等原因,并没有得到非常大范围的推广。

兼容并济:UMD 的统一

2014 年 9 月,美籍华裔 Homa Wong 提交了 UMD 第一个版本的代码。

UMDUniversal Module Definition 的缩写,它本质上并不是一个真正的模块化方案,而是将 CommonJSAMD 相结合。

UMD 作出了如下内容的规定:

  1. 优先判断是否存在 exports 方法,如果存在,则采用 CommonJS 方式加载模块;
  2. 其次判断是否存在 define 方法,如果存在,则采用 AMD 方式加载模块;
  3. 最后判断 global 对象上是否定义了所需依赖,如果存在,则直接使用;反之,则抛出异常。

这样一来,模块开发者就可以使自己的模块同时支持 CommonJSAMD 的导出方式,而模块使用者也无需关注自己依赖的模块使用的是哪种方案。

姗姗来迟:钦定的 ES6/ES2015

时间前进到 2016 年 5 月,经过了两年的讨论,ECMAScript 6.0 终于正式通过决议,成为了国际标准。

在这一标准中,首次引入了 importexport 两个 JavaScript 关键字,并提供了被称为 ES Module 的模块化方案。

在 JavaScript 出生的第 21 个年头里,JavaScript 终于迎来了属于自己的模块化方案。

但由于历史上的先行者已经占据了优势地位,所以 ES Module 迟迟没有完全替换上文提到的几种方案,甚至连浏览器本身都没有立即作出支持。

2017 年 9 月上旬,Chrome 61.0 版本发布,首次在浏览器端原生支持了 ES Module

2017 年 9 月中旬,Node.js 迅速跟随,发布了 8.5.0,以支持原生模块化,这一特性被称之为 ECMAScript Modules(简称 MJS)。不过到目前为止,这一特性还处于试验性阶段。

不过随着 babelWebpackTypeScript 等工具的兴起,前端开发者们已经不再关心以上几种方式的兼容问题,习惯写哪种就写哪种,最后由工具统一转译成浏览器所支持的方式。

因此,预计在今后很长的一段时间里,几种模块化方案都会在前端开发中共存。


尾声

本文以时间线为基准,从作者、社区、理念等几个维度谈到了 JavaScript 模块化的几大方案。

其实模块化方案远不止提到的这些,但其他的都没有这些流行,这里也就不费笔墨。

文中并没有提及各个模块化方案是如何实现的,也没有给出相关的代码示例,感兴趣的朋友可以自行阅读下方的参考阅读链接。

下面我们再总结梳理一下时间线:

时间事件
1995.05Brendan Eich 开发 JavaScript。
2006.01Yahoo 开源 YUI Library,采用命名空间方式管理模块。
2006.01John Resig 开发 jQuery,采用 IIFE + 闭包管理模块。
2009.01Kevin Dangoor 起草 ServerJS,并公布第一个版本 Modules/0.1
2009.04Kevin Dangoor 在美国 JSConf 公布 CommonJS
2009.05Ryan Dahl 开发 Node.js
2009.08Ryan Dahl 在欧洲 JSConf 公布 Node.js
2009.08Kevin DangoorServerJS 改名为 CommonJS,并起草第二个版本 Modules/1.0
2009.09James Burke 开发 RequireJS
2010.01Isaac Z. Schlueter 开发 npm,实现了基于 CommonJS 模块化方案的 Common Node Modules
2010.02Kris Zyp 起草 AMDAMD/RequireJS 社区成立。
2011.01玉伯开发 SeaJS,起草 CMDCMD/SeaJS 社区成立。
2014.08Homa Wong 开发 UMD
2015.05ES6 发布,新增特性 ES Module
2017.09ChromeNode.js 开始原生支持 ES Module

注:文章中的所有人物、事件、时间、地点,均来自于互联网公开内容,由本人进行搜集整理,其中如有谬误之处,还请多多指教。


参考阅读


首发于 Segmentfault.com,欢迎转载,转载请注明来源和作者。

RHQYZ, Write at 2020.06.24.

查看原文

认证与成就

  • 获得 45 次点赞
  • 获得 18 枚徽章 获得 0 枚金徽章, 获得 5 枚银徽章, 获得 13 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-07-19
个人主页被 556 人浏览