All posts by dotte

Gradle

Gradle 用户指南官方文档中文版

Gradle does not find tools.jar

How/when to generate Gradle wrapper files?

 在 Eclipse 下利用 gradle 构建系统

使用Gradle管理老J2EE项目(一)
Gradle脚本基础全攻略
Gradle 10分钟上手指南

用gradle来管理java项目的示例

gradle 命令及技巧 (gradle-tips)
Gradle学习系列之一——Gradle快速入门

Gradle入门系列(5):创建多项目构建

Vim入门教程

尽管网上有成打的Vim在线教程,但是要么艰深晦涩,要么太过肤浅。本教程的目标让每个阶段都有斩获,从理解它的哲学(将和你终身相伴)到超越现在编辑技巧,成为其中的牛人。

简单来说,本教程的学习方式将使你终身受益。

为什么选择vim

我相信大多数人可能由于以下三点原因而使用vim:

1、vim无所不在。学习vim你无需担心到了其他平台需要学习新编辑器。

2、可扩展性。你可以只用它来编辑配置文件,也可以将它当做你的开发平台。

3、功能强大。它的工作方式与自然语言类似。即使一开始你对vim一无所知,经过一段时间的使用学习,很快也会成为牛人。

一句话,我相信一旦你开始学习,你会觉得就像掌握母语还有基础数学这些基本技能一样,也能自然而然地掌握vim。好了,在了解这些知识之后,让我们正式开始vim技术的学习吧。

Approach

Kana大神说过,对vim的掌握有五个层次:

  • 层次0: 对vim一无所知
  • 层次1: 了解vim的基本使用
  • 层次2: 知道可视模式
  • 层次3: 知道多种移动动作
  • 层次4: 不再需要可视模式

对此我并不了解,不过我认为这种观点值得我们注意。毕竟,kana是vim大神。在本教程中,我将通过下面四个主要模块来向你展示vim。

  1. 基础介绍:这部分基础介绍能快速建立起你对vim的正确认知。
  2. 具体操作: 这块是干货。做好准备来享用吧。
  3. 进阶: 这部分我会教你怎样成为vim牛人。
  4. 常见问题: 这里会讲述一些常见问题解决技巧。

换句话说,如果你已经对vim有所了解的话,建议你从具体操作这部分开始。如果你熟练掌握这部分的话,就直接进入进阶开始学功夫。假如你看这篇教程是为了某个具体问题,那么请进入常见问题章节。

所以呢,建立vim世界观,vim基本操作,高手进阶以及常见问题——这几部分任君挑选。

配置

我曾说过,不希望这是一本超级vim配置手册,网上已经有很多这类教程了。本教程的目的在于强化你对vim的理解,帮助你熟练掌握这个工具。不过我们也会稍微介绍一些基本的配置。

首先,我建议你安装(几乎)自我管理的vim。我就曾试过Janus(vim的发行版),但是我无法确定它究竟在做什么,这让我感到很挫败。我偏好的配置就像vim自身一样,简单而优雅。

为了达到这个目的,我直接将用户主目录下的.vim目录和.vimrc作为配置文件。

A few key ~/.vimrc changes

./vimrc修改常用键

首先呢,我觉得使用键来退出插入模式实在是太老土了。Vim关注的是效率,如果不是特殊情况,手指尽量不要离开键盘中间排。而这种ESC操作实在是没效率,所以不要用这个。

inoremap jk

【注意: 有些人喜欢将映射为jj,但是我觉得映射为jk可能更自然一点】

修改leader键

反斜杠也同样存在这种问题,所以就像其它指导文档推荐的那样,我也喜欢将引导键重映射为逗号(,)。

let mapleader = “,”

通过这样的配置,可以简化我们接下来的命令执行,比如执行映射的缩写命令时,只需右手中指往下一格就可以轻松输入逗号,而不需要向上再向右去远远地按反斜杠键。

重映射CAPSLOCK

大写键不在vim的配置文件中,不过这个键的位置比较好,而它的默认功能我们又常常用不上。对我来说,CAPSLOCK键没啥用处,所以我将它映射为Ctrl键(从操作系统层面来设置)。这样如果我要执行Ctrl-XX操作时只需将左小指左移就可以了。

此外,推荐其他几项基础设置,这样使用更加方便。

filetype plugin indent on

syntax on

set encoding=utf-8

要知道,优化vimrc文件是一个非常值得花时间去研究的问题,这里说的几个建议仅仅是皮毛。推荐查看我的设置或者阅读这几篇引用文章

使用Pathogen管理插件

【请注意:如果你不熟悉或者并不喜欢插件,可以跳过这一节。等你下次想通了再来看看。】

远离Janus

对我来说,Janus最能吸引我的地方在于它的插件管理,不过我可以通过Pathogen来实现。基本上,通过以下几步就可以了:

  1. 安装Pathogen
  2. 将你需要的插件git clone 克隆到~/.vim/bundle
  3. 在配置文件~/.vimrc中添加execute pathogen#infect()

利用github来实现备份和可移植性

为了备份vim设置,我将整个~/.vim目录通过git库保存在这里。这样即便我新装了系统,也可以很方便的使用git clone https://github.com/danielmiessler/vim获取我的偏好设置。

也许你也会喜欢这样做。

简单一步clone之后,将~/.vimrc软连接到~/.vim/vimrc就可以了。

Vim即语言

Vim最成功的闪光点莫过于一旦你开始使用它,它就会让你欲罢不能。Vim就像语言一样,拥有名词、动词、副词等属性。

虽然我的这种说法从技术层面可能并不准确,但是却能帮助你更好地理解vim的工作机制。重申一次,本教程的目的不是为了取代其他教程或帮助手册——而是为了帮助你理解其他这些文档晦涩难懂的部分。

动词

所谓动词指的是我们执行的动作,这些动作可以施加在名词之上。可以看看下面几个常见的动作:

  • d: 删除
  • c: 修改
  • y: 拖拉(拷贝)
  • v: 可视化选择 (V for line vs. character)

修饰语

定语用在名词之前,表明以哪种方式来执行动作。几个例子:

  • i: 内部
  • a: 周围
  • NUM: 数字 (e.g.: 1, 2, 10)
  • t: 查找到指定字符,并跳转到这个字符的前面
  • f: 查找到指定字符,并跳转到字符所处的位置
  • /: 查找字符串 (literal or regex)

名词

在英语中,名词用来表示你所操作的对象。它们都是客体。在vim中也是这样。下面列出vim中的名词:

  • w: 单词
  • s: 句子
  • ): 句子 (另一种操作方式)
  • p: 段落
  • }: 段落 (另一种操作方式)
  • t: 标签 ( HTML/XML)
  • b: 块 (编程语言)

Nouns as motion

同样,你也可以将名词视为移动动作,这意味着你可以用名词表示在文本的跳转范围。我们可以在“移动”章节中看到例子说明。

使用vim语言来组成语句(命令)

好了,现在我们有句子的不同组成部分,该怎样将这几部分组成完整的句子呢?就像英语一样,凭感觉将动词、修饰语和名词组合起来。

对于下面这些符号,根据颜色的不同来区分句子的不同组成部分:

删除两个单词

d2w

修改所在的句子(删除当前句子并进入插入模式)

cis

拷贝当前所在的段落

yip

修改当前光标所在位置到下一个括号之间的文本内容

ct< 记住,这里的“目标”是尖括号,除此之外还可以是任意字符。从语法上来说,使用单个字母”t”来表示这个向前查找的“目标”,这样我就可以使用“dt.”表示删除当前位置到下一个句号之间的内容,”yt;”表示拷贝当前位置到下一个分号之间的内容。 看起来多漂亮!当编辑文本时采用这样的思维方式可以使得操作更加优雅,符合我们的直觉思维,而且就像其他语言一样,熟能生巧。

具体操作

现在,在了解这些基本操作后,让我们结合具体操作来更进一步学习。

处理文件

一些对文件的基本操作。

  • Vi file :使用vim打开要编辑的文件
  • :w :保存修改
  • :q! : 退出vim,后面加感叹号表示不保存修改强制退出
  • :wq: 保存修改并退出
  • :saveas ~/some/path/: 将文件保存到其他位置

【注意:相比较:wq 我更喜欢使用ZZ,因为我觉得不需要输入冒号我觉得会快一点。也可以使用:x】

  • ZZ: 同:wq

查找文本

对于任意一款编辑器,查找文本都是最基本需求。Vim拥有很强大的文本搜索功能,这一节将着重讨论vim的文本搜索。

搜索字符串

Vim中最基本也是最强大的搜索方式是”/”命令,在窗口最底端输入你需要查找的字符串并回车就可以了。

查找include字符串

/include

查找结果如下所示,所以匹配结果都被高亮显示:

搜索完成后,输入”n”依次向下查找,或者”N”向上查找。如果想要逆向查找字符串也可以使用”?”来代替”/”.

跳转到指定字符

Vim有一招特别酷,不管你在哪儿,都能查找特定字符并直接跳转过去。比如说,我正在编辑HTML文件,

向前查找并直接跳转到<字符 f< 向前查找并跳转到<的前一个字符 t< 你可以这么理解,第一种找出目标(f),直接定位到查找对象;第二种到达目标(t),向着目标对象前进,定位到目标的前面。 比较怪异的一点是,可以将它们作为命令中的名词。就比如刚才我在编辑这句时: 修改到下一个”<”之间的内容 ct< 对任意字符都可以这么做,比如说,句号,尖括号,圆括号,普通字母等等。这样你就可以查找文本并跳转,或者你知道具体位置并直接跳转。 【注意:当你搜索指定字符时,可以使用分号跳转到下一处——不管是t搜索还是f搜索。逗号可以用来反向跳转到上一处。】

搜索总结

  • /{string}: 搜索字符串
  • t : 跳转到某一字符前
  • F : 跳转到某一字符处
  • *:搜索当前光标位置单词的其他实例
  • n : 搜索完成后,跳转到下一个匹配实例
  • N :搜索完成后,跳转到上一条匹配实例
  • ; : 跳转到指定字符时,使用分号跳转到下一实例
  • , :跳转到指定字符时,使用逗号跳转到上一实例

游走于文本中

提高编辑效率至关重要的一点是:在文本内的自由移动跳转。对vim来说,充分利用我们上文所说的vim语言的基本准则,就可以既简单又优雅地做到。首先,介绍基础概念。

基本移动动作

我们从键盘中间排开始介绍。受过训练的打字员一般都习惯将右手指放在jkl;这几个键上,就让我们从这里开始使用vim。

  • j : 向下移动一行
  • k :向上移动一行
  • h :向左移动一个字符
  • l :向右移动一个字符

刚开始可能使用得不太习惯,不过只需几分钟的练习之后,就会习惯成自然,你甚至会想要在word文档里面也这样用(顺便说一下,word文档不支持)。

总结起来就是:右手食指和中指用来上移下移,右手食指和无名指用来左移右移。

行内移动

你可以在当前行内自由的移动。

  • 0: 移动到行首
  • $: 移动到行末
  • ^: 移动到行首非空字符
  • t”: 跳转到下一个引号前
  • f”: 跳转到下一个引号处

【注意: 逗号和分号会重复之前的t和f跳转】

按单词移动

除了按行和字符移动外,还可以按单词移动:

  • w: 先前移动一个单词
  • b: 向后移动一个单词
  • e: 移动到当前单词末尾

当使用大写字母时,vim会忽略掉单词间的分隔符,这样原本被分隔符隔开的多个单词会因此被视为一个大单词。

  • W: 向前移动一个大单词
  • B: 向后移动一个大单词

这种大写命令的优势在我们实际工作中会频繁地碰到。

按句子或段落移动

  • ):向前移动一条句子
  • }: 向前移动一个段落

在屏幕间移动

  • H:向上移动一屏
  • M:移动到屏幕中间
  • L:移动到屏幕底端
  • gg: 移动到文件顶端
  • G:移动到文件结尾
  • ^U: 向上移动半屏
  • ^D: 向下移动半屏
  • ^F: 下一页
  • ^B: 上一页

来回跳转

在普通模式下,可以在两点间来回跳转,这在实际操作中非常有用。

  • Ctrl-i: 跳转到之前所在的位置
  • Ctrl-o: 跳回你实际的位置

其他动作

  • :$line_numberH: 移动到指定行号
  • M: 移动到屏幕中间
  • L: 移动到屏幕底端
  • ^E: 向上滚动一行
  • ^Y: 向下滚动一行
  • ^U: 向上移动半屏
  • ^D: 向下移动半屏
  • ^F: 向下移动一页
  • ^B: 向上移动一页

现在将所以移动动作总结到一起:

移动命令总结

  • j: move down one line
  • k: move up one line
  • h: move left one character
  • l: move right one character
  • 0: move to the beginning of the line
  • $: move to the end of the line
  • w: move forward one word
  • b: move back one word
  • e: move to the end of your word
  • ): move forward one sentence
  • }: move forward one paragraph
  • :line_number: move to a given line number
  • H: move to the top of the screen
  • M: move to the middle of the screen
  • L: move to the bottom of the screen
  • ^E: scroll up one line
  • ^Y: scroll down one line
  • gg: go to the top of the file
  • G: go to the bottom of the file
  • ^U: move up half a page
  • ^D: move down half a page
  • ^F: move down a page
  • ^B: move up a page
  • Ctrl-i: jump to your previous navigation location
  • Ctrl-o: jump back to where you were

【注意:前面说过我将CAPSLOCK键映射到Ctrl,所有这些基于Ctrl的命令,我都可以用CASLOCK来代替】

修改文本

好了,了解一堆在文本中移动的命令后,我们可以开始学习对文本的修改了。需要记住这一点:这些移动命令会一直陪伴着我们——它们是vim语言的一部分(它们是上文提到的修饰语)。

Understanding modes

首先我们要弄清楚模式的概念。刚开始说到模式可能有点违反我们的直觉,不过一旦习惯之后就会使用得非常的自然。很多vim指南都从介绍模式开始,不过我发现如果直接从模式开始有点不好理解,从普通模式与插入模式之间的切换入手会好一点。

  • 从普通模式开始。对新手来说vim最让人郁闷一点莫过于打开文档却不能直接编辑。其实,你也可以输入一些个东西,不过如果你真这么做的话你的文档可能会变得一团糟。
  • 普通模式也叫命令行模式,因为通常在这个模式下输入vim命令。这些命令可以是移动、删除或者其他操作,之后进入插入模式。
  • 插入模式下可以直接对文件进行修改,有很多种方式可以从普通模式切换到插入模式。不过不用担心,虽然方式多样,只要多加练习就可以熟练使用了。
  • 可视化模式下可以选择文本。这个模式看起来与普通模式非常像,不过这个模式下移动操作会改变高亮选择的区域。你可以逐行选择也可以逐字符选择,不管哪种选择模式下,移动操作都会高亮选择更多的文本内容。
  • 选中之后,可视化模式的优势就体现出来了,我们可以对选中的全部文本做操作,大大提高了效率。
  • Ex模式下,光标处于屏幕最下方,冒号后可以输入命令。具体内容在后面做介绍。现在只需要知道在这种模式下可以运行一些命令行任务就可以了。

除了上面介绍的模式外vim还有一些其他的模式,不过本教程不做介绍因为它们不是首要任务。

记住vim语言

现在让我们一起回忆vim语言: 动词、修饰符、名词。假设我们从普通模式开始,现在想要切换到插入模式下修改文本。

动词可以实现这种切换,并且有好几种选择。我们可以修改(c),插入(i),或者追加(a),还有其他变种。

基础修改/插入命令

让我们从这几个可选命令开始。

  • i : 从当前光标位置之前插入
  • a : 从当前光标位置之后插入
  • I :从当前行开始位置插入
  • A :从当前行行末插入
  • o : 在当前行下新增一行,并从新行插入
  • O :在当前行上新增一行,并从新行插入
  • r : 替换当前光标所在字符
  • R :替换当前光标的字符,并一直向后替换(替换模式)
  • cm:修改文本,其中m表示移动的动作,比如,一个单词,一句或者一个段落。
  • C :修改当前所在行
  • ct?: 修改问号前的这条问句内容
  • s:替换当前字符
  • S: 替换整行

修改句子

cis

移动到行首并进入插入模式

I

在当前光标位置之后进入插入模式

a

从上面例子可知,有多种方式可以进入插入模式输入文本。还有一些简写来执行多条操作,比如说删除并进入删除模式。

删除从光标所在位置开始到行末的所有内容,并进入插入模式

C

删除当前行并进入插入模式

S

修改大小写

可以通过波浪符来变更大小写。就像你猜想的那种方式工作:不管你光标所在字符还是可视模式下选择的文本都可以。

格式化文本

Vim还可以快速格式化文本,比如格式化一整段文本,使用下面这个命令就可以很容易实现:

格式化当前段落

gq ap

gp根据预设的文本宽度testwidth以及参数指定的文本范围来工作,这意味着对于你指定的任意范围的文本,gp都可以自动校准宽度,使得文档看起来干净又整洁。

【注意: ap表示文本对象”整个段落”的覆盖范围】

删除文本

在了解怎样修改文本后,接下来看看怎样直接删除文本。可能你已经知道了,跟前面差不多——只不过实现的动作不同罢了。

基本删除命令

  • x: 删除当前光标的字符
  • X:删除当前光标的前一个字符
  • dm: 根据定义的m来确定删除范围,一个单词或者一句话或者一个段落。
  • dd: 删除当前行
  • dt. : 删除当前位置到下一个句号之间的所有内容
  • D: 删除当前光标位置到本行末的所有内容(对比上面的C,D不会进入插入模式)
  • J: 将上下两行合并(删除两行之间的换行符)

足够简单吧?

撤销与重做

如果一个编辑器连撤销和重做的功能都没有,你肯定不会用它。vim就像我们前面注意到的那样,尽其所能让我们更顺手地操作,撤销与重做也不例外。

  • u: 撤销上一个动作
  • Ctrl-r: 重做上一个动作

【注意:记得将CAPSLOCK键映射为Ctrl,这样你才能更方便快速的使用组合键。】

这两个动作都可以重复操作,你可以一直撤销啊撤销回到最初保存点,也可以一直重做啊重做到撤销前的状态。

重复动作

Vim所有命令中最强大的那个莫过于点命令,听起来有点奇怪是不是?解释一下,通过“.”这个小点可以让你做一些很棒的事情——重复之前你的任何一个操作。

使用“.”来重复上一次操作

你所做的许多操作可能都有重复的价值。比如进入插入模式添加一些文本内容。你只需执行一次,然后一直执行点命令来重复就可以了。请看下面的例子。

删除一个单词

dw

紧接着执行,再删除5个单词

5.

多么给力! 如果结合可视化模式,发挥的能力更让你大开眼界。

复制与粘贴

快速拷贝与粘贴文本同样也是文本编辑器的必备功能,vim当然也是这方面的高手。

【注意:除了点命令外,&也是一种强大的重复命令,用于重复ex模式下的上一条命令(还记得ex模式吗?)】

拷贝文本

Vim的拷贝与大家预期的方式可能略有不同。拷贝命令不是大家料想的c。不知你还记不记得,上文提到c表示“修改”,已经被占用了。Vim用拉取来表示拷贝,简写为y。

  • y: 拷贝选中内容
  • yy: 拷贝当前行

记住,这种拷贝动作与你熟悉的那种拷贝并无不同——你只是在目的处复制了一份而已。

剪切文本

剪切很简单:跟删除操作差不多。所以呢,剪切实质上就是将删除的内容挪到缓冲区,然后将这段删除的内容再粘贴就可以了。

粘贴文本

粘贴跟我们想的一样——使用p命令。所以呢,如果你执行dd命令删除了一行,你可以用p粘贴回去。

有一点要记住:粘贴的起始位置从你当前光标位置之后开始,根据你拷贝的内容,你要么粘贴字符或单词,要么粘贴行或列。如果想撤销粘贴,使用通用撤销命令“u”就可以了。

拷贝复制命令总结

  • 从当前行拷贝到下一个命令(名词)
  • 拷贝当前行的快捷键
  • 在当前光标所在行下一行黏贴拷贝或者删除的内容
  • 在当前光标所在行前一行黏贴拷贝或者删除的内容

交换两行的位置

ddp

使用这条命令可以很快速的交换两行的位置。第一部分删除你所在的行,第二部分将它粘贴回之前所在位置的下面。(ps:文章中用的是above,但是实际上拷贝回下方位置)

拼写检查

如果没有拼写检查的话,感觉会很糟糕,还好vim这方面做得很好。当然我们需要先在配置文件中配置拼写检查选项。

在~/.vimrc的某个位置中添加

找出拼写错误

如果在配置文件中设置了拼写检查的选项,那么拼错的单词会自动添加下划线。你也可以手动输入:set spell和:set nospell来启用或禁用。

不管怎么说,一旦开启了拼写检查,如果存在拼写错误的话,你就可以使用以下命令来跳转:

跳转到下一处拼写错误

]s

跳转到上一处拼写错误

[s

若光标处是错误单词,给出修改建议

z=

将识别的错误单词标记正确

zg

将vim视为正确的单词标记为错误单词

zw

我习惯在~/.vimrc中添加一些拼写相关的简写命令。首先就是让修正拼写错误的命令变得简单:

将拼写修正标记为f

在某些情况下,比如说我需要创造性的拼写一些单词时,我会使用下面的命令来取消拼写检查。想要重新打开拼写检查只要再执行一次这个命令就可以了。

Toggle spelling visuals with s

nnoremap s :set spell!

替换

Vim的另一个强大的功能是替换。命令分为三部分,首先指定将要被替换的字符串,之后是替换的新字符串,最后指定替换范围。

基本设置为:%s

将每行的foo替换为bar

:%s /foo/bar/g

将当前行中的foo替换为bar

:s /foo/bar/g

【注意: 两个命令的不同之处在于,单行替换s前少了%】

还有许多其他的选项,不过这些是最基本的。

PS: 前面&命令重复此类操作(:进入ex模式后执行的命令)

进阶

太棒了。我们已经讨论了大部分文本编辑器的基本功能,以及在vim中如何使用这些功能。现在可以进行高级vim的任务了——请注意,我说的高级进阶是对初学者而言,不是Kana这样的大神级人物。

重复操作

在稍早之前我们讨论了“.”点命令的重复功能。有些命令适合使用点命令来重复,有些则不适合,区分这两种命令很重要。

通常,如果你想要使用“.”(或者如Drew Neil称之为点命令)实现命令重复,说明你想用点命令缓存的命令来做比较细致的操作。

比如说你要向若干行行末添加一些内容,但是添加的前提是被添加的行包含指定的字符串。你可以使用如下方式来实现:

搜索字符串

/delinquent

现在,只要你按下“n”键都会跳转到下一个搜索到的实例。那么,从第一个匹配实例开始,我们给它追加文本。

在行末添加文本

A[DO NOT PAY] [Esc]

好,这条命令执行完成了。不过接下来还有12个地方需要你来做。点命令允许我们简单地重复执行上一条命令,而且我们还需要保存搜索结果,可以将这两条命令结合起来。

跳转到下一个匹配实例并向行末添加文本

n.

注意,这种方式将动作与缓存的命令结合起来,所以我们能按照预期地那样跳转并执行命令。

文本对象

文本对象很特殊。这些文本对象的存在使得你可以操作(动词)更加复杂的对象(名词)。比起选中某个单词并删除、跳转到句首并删除句子等简单操作来说,不管你在何种文本对象的区域内,你都可以对它们执行这些操作。

说是说不清,实践出真知嘛,我们可以看几个例子。

单词文本对象

首先来看基于单词的对象

  • iw : 单词内
  • aw :单词周围

这些都是目标(名词),所以我们可以对它们做一些操作,比如说删除、修改等等。

删除单词(包括单词周围的空格)

daw

【注意: 对象内部与对象周围这两者之间的区别在于单词旁边是否有空格】

句子文本对象

  • is: 句子内部
  • as: 句子周围

句子对象的工作原理与单词对象是一致的,想象一下,你敲了一大段句子,敲了一半后悔了想要删掉。你就可以简单的使用下面这条命令,而不用回到句子开头来申明删除整行。

修改整条句子

cis

这条命令杀伤力还是挺大的,它会删除整条句子并进入插入模式等待你输入新的内容。

更多对象类型

还有许多其他的对象类型,我只简单的来介绍一下。

  • 段落 : ip 和ap
  • 单引号 : i’和a’
  • 双引号: i”和 a”

当我编辑代码或者HTML文件时,时常会用到这些命令。关键是如果这些对象文本存在问题,你甚至并不需要处于这些对象内部就可以操作它们;如果光标当前位置在双引号外面(经测试,跟双引号处于同一行并在双引号前面),只需要输入ci”就可以删除双引号内部的所有内容,并将你挪到双引号中间进入插入模式等待你的输入。简直不要太酷。

对其他类型的某些对象同样适用,比如说各种括号(圆括号、尖括号、大括号)和标签(html)。

比如说编辑HTML链接时,URL地址一般在双引号之间,链接文本在标签中定义;通过vim这两条命令可以很方便地修改:ci”(修改URL)和cit(修改链接文本)。

文本对象总结

下面列出常见的文本对象:

  • 单词: iw 和 aw
  • 句子: is 和 as
  • 段落: ip 和 ap
  • 单引号: i’ 和 a’
  • 双引号: i” 和 a”
  • 反勾号 : i· 和 a
  • 圆括号: i( 和 a(
  • 方括号: i[ 和a[
  • 大括号: i{ 和a{
  • 标签: it 和at

总的来说,我使用最多的是单词,双引号和标签。

进入可视化模式

Vim的有许多迷人之处吸引着我们,不过它们相比较可视化模式的技能来说,还是太弱了。

也许可视化模式的最强大之处在于:我们前面学到的所有的一切命令,通过结合可视化模式使用,能力都会大有提升。这是由于在可视化模式下选中的高亮文本,可以全部作为命令的操作对象。

首先,学会进入可视化模式并选择文本。通过“v”进入可视化模式,有三种选择。

  • 基于字符选中: v
  • 基于行选中: V
  • 段落选中: Ctrl-v

在容器内部选中文本

有时候你会处于一段文本中,两头被,.({[之类的符号所包围。你可以使用下面的命令来选中这部分的内容:

选中圆括号中的内容

vi(

选中中括号之间的内容

vi[

你还可以加数字限定选择的层数(比如说你处于嵌套层)

选中两层大括号之间的所有内容

v2i{

【注意: 你也可以使用va来代替vi。请不要被这些迷惑。它们与我们所知的名词和动词没有什么不同】

基于字符的可视化选择

从基于字符的可视化开始介绍(从普通模式输入v进入可视化模式),在这种模式下你可以选中单个字符、多个字符、单词等等。相比较行选择模式来说,我比较少使用这种模式,不过只是相对而言,其实用的也还是挺多的。

这里最主要的是要理解这一点: 处于可视化模式中,你的移动动作会修改被高亮选择的文本。这就意味着,w或)之类的动作也会起作用,它们可以扩展当前高亮选择的范围。之后,这部分高亮文本就可以作为命令执行的目标。

基于行选择的可视化模式

普通模式下输入V进入这种模式,之后我们会讨论在这种模式下的操作。

基于列选择的可视化模式

可视化模式中还可以垂直地选择文本,这对操作列数据来说很给力。

可视化模式下对选中文本的执行动作

对这些文本做什么操作都是你的自由,不过最普遍的操作是删除、拷贝和粘贴。想一下你平常用鼠标选中一片文本会做什么操作?

进入可视化模式,选中两个以上单词并拷贝

vwwy

拷贝之后你可以在任何你希望的地方输入p来粘贴。

或者你可以做一些基于行的操作。

进入行选择模式,删除几行文本

Vjjd

你也可以使用文本对象,不过这种操作不太安全,最好别做。

可视化模式下选中一整个段落

vip

可视化模式下选中一整段文本并粘贴到当前段落之下。

vipyjjp

不要因为命令很长而感到崩溃。要记住,这些命令跟我们平常说话是一样的。你可以没有任何困难地、很随意地念叨:

我想去商店。

这条命令也是一样的:

拷贝这一段,下移两行,再粘贴。

将可视化模式与重复结合

可视化模式还有一个很神奇的地方,对于选中的文本,你可以通过点命令来执行之前缓存的命令。请看下面这个例子:

如果想在每行前面都加个冒号,可以现在第一行foo之前添加,然后可视化模式下选择之下所有行,执行点命令就可以了。

【注意:要想使用这个功能,必须要在配置文件中添加映射 vnoremap . :norm.

BAM!

觉得这功能没有那么酷炫? 那就想象一下: 你有个六万行的文件,每行都与例子一样,需要追加个冒号。你会怎么做?

对整个文件添加分号

0i:j0vG.

wut

大大简化了我们的操作,大杀器一枚。步骤如下:

  1. 走到第一行行首并插入一个冒号(0i:)
  2. 走到下一行定位到行首位置(j0,注意之前是插入模式,需要先回到普通模式)
  3. 选中当前行到文件末尾的内容(vG)
  4. 给选中的内容添加冒号(.)

这样对六万行文件的操作就全部完成了。你甚至不需要去记一些乱七八糟的口诀——就像学骑单车会摔倒一样,摔着摔着你就会了。相信我。

使用宏

有些人会觉得宏很可怕,其实不然。宏只用来完成这样一件事: 记录你做的一切,在你需要的时候帮你再做一次。下面是一个简单总结:

  • qa: 开始记录接下来要执行的动作,并命名为a
  • q: 停止记录
  • @a :将宏代表的动作再执行一次

很简单啊是不是?你可以保存多个宏,比如说宏a、宏b、宏c等等。当你需要的时候执行@a或者@c之类的就可以了。

什么场景下使用宏?

你可能会问:

既然可视化模式与点命令的结合已经这么强大了,我们为什么还要使用宏呢?

问得好,不过答案挺复杂。宏可以做你所做的一切,来看看这样一个流程

  1. 在当前行搜索“widget”
  2. 找到最后一个匹配到的单词并添加–maker
  3. 走到行首并添加一个分号
  4. 走到行末再添加一个点号
  5. 如果行末有空格则删除

这么一堆工作,咱们还是假设文件有六万行,试着在Microsoft word之类的工具里完成这些操作,照样会让人感到万分痛苦。

不过在vim中,这都不算事儿,你只需要把这些动作都做一遍(recording模式下),然后对每一行重新执行宏就可以了。

【注意:如果想要对可视化选中文本执行宏的话,可以执行:normal @a(或者其他你所定义的宏的名字),这条命令会临时将你切换到普通模式,针对每一行执行宏命令】

技巧

下面我们来看一些大家经常问的一些问题,总结出来节约大家时间。

删除行末空格

根据打开文件的类型不同,可能需要你对回车符和换行符做一些处理。这里介绍如何删除行末那些烦人的Ctrl-M字符。

删除行末的Ctrl-M字符

:%s/s+$//

修改文件类型

set ft=unix

set ft=html

set ft=dos

【注意:如果要显示当前文件类型,在配置文件中添加:set filetype,或者在vim中直接执行这条命令就可以了】

包装文本

借用Surround插件,我们可以很

  • cs”‘: 将光标所在单词两边的双引号修改成单引号
  • cs’ :做同样的事,不同的是将单引号修改成
  • ds” : 删除两边的的配对双引号
  • ysiw[ :在当前单词两边加上中括号
  • ysiw: 给当前单词(其他文本对象也可)添加强调标签!!!知道我为什么这么激动吗?因为这个操作可以使用点命令来重复!
  • Visual Mode: 可视化模式下,选中任意文本,输入S。vim会带你跳转到窗口最底端。这时候你可以输入任何你想给这段文本打上的标签,比如说 ,回车试试?

总结

通过本教程我希望大家能明白以下两点:

  1. vim 是可以学会的
  2. vim 很强大

上面介绍的那些vim基础用法,即便你只喜欢其中一部分,我想可能也会加深你对编辑文本的热情——这可不是小事。你对操作的编辑器越熟练,思维也就越发散,我想这是堪比史诗级的大事了。

更为重要的是,这也是为什么要求你得精通你选择的编辑器。只有精通编辑器,不被编辑器的操作所阻滞,当你脑内思绪奔腾时,你才能激扬文字跟紧思绪跳跃的步伐。

或者,你也可以将这一切都抛开,成为那些被人所取笑的人——不管怎样,我希望本教程能对你有用。

【如果你喜欢这篇文章,可以去我的地盘看看其他技术指导文章】

引用

  1. 推荐大家阅读Drew Neil的这本书《Practical Vim: Edit Text at the Speed of Thought》这是vim爱好者必备书籍。
  2. 强烈推荐《Your Problem with vim is that you don’t grok vi》,它不光对vim一般用法做了非凡概述,还介绍了一些漂亮的vim技巧。
  3. 如果你还没看过Steve Losh的《Coming Home to Vim》,强烈推荐。
  4. 一定要去看看 Kana 大神的《True Power of Vim》。
  5. 另外还有Drew的 Vimcasts.org. 这两者都从实际操作角度让你认识到vim的强大。
  6. 如果想要vim简明命令资源,下载 Vim Quick Reference
  7. 当然不要忘了 Vim Wiki,这也是学习vim的重要资源。
  8. 如果你对vimscript感兴趣, 一定去看看Steve Losh的《Learn Vimscript the Hard Way》。这是迄今为止关于vimscript的最好学习资料。
  9. 还有Openvim的《This is a really well done interactive tutorial》。
  10. The help 非常棒,不过内容有点多,如果你真的非常想要学好Vim的话,那么一定要从头到尾通读。
  11. Vim from zero to hero – Vim 从入门到精通
  12. Vim的Window窗口

from:http://blog.jobbole.com/86132/

分布式服务化系统一致性的“最佳实干”

1 背景

一致性是一个抽象的、具有多重含义的计算机术语,在不同应用场景下,有不同的定义和含义。在传统的IT时代,一致性通常指强一致性,强一致性通常体现在你中有我、我中有你、浑然一体;而在互联网时代,一致性的含义远远超出了它原有的含义,在我们讨论互联网时代的一致性之前,我们先了解一下互联网时代的特点,互联网时代信息量巨大、需要计算能力巨大,不但对用户响应速度要求快,而且吞吐量指标也要向外扩展(既:水平伸缩),于是单节点的服务器无法满足需求,服务节点开始池化,想想那个经典的故事,一只筷子一折就断,一把筷子怎么都折不断,可见人多力量大的思想是多么的重要,但是人多也不一定能解决所有事情,还得进行有序、合理的分配任务,进行有效的管理,于是互联网时代谈论最多的话题就是拆分,拆分一般分为“水平拆分”和“垂直拆分”(大家不要对应到数据库或者缓存拆分,这里主要表达一种逻辑)。这里,“水平拆分”指的是同一个功能由于单机节点无法满足性能需求,需要扩展成为多节点,多个节点具有一致的功能,组成一个服务池,一个节点服务一部分的请求量,团结起来共同处理大规模高并发的请求量。“垂直拆分”指的是按照功能拆分,秉着“专业的人干专业的事儿”的原则,把一个复杂的功能拆分到多个单一的简单的元功能,不同的元功能组合在一起,和未拆分前完成的功能是一致的,由于每个元功能职责单一、功能简单,让维护和变更都变得更简单、安全,更易于产品版本的迭代,在这样的一个互联网的时代和环境,一致性指分布式服务化系统之间的弱一致性,包括应用系统一致性和数据一致性。

无论是水平拆分还是垂直拆分,都解决了特定场景下的特定问题,凡事有好的一面,都会有坏的一面,拆分后的系统或者服务化的系统最大的问题就是一致性问题,这么多个具有元功能的模块,或者同一个功能池中的多个节点之间,如何保证他们的信息是一致的、工作步伐是一致的、状态是一致的、互相协调有序的工作呢?

本文根据作者在互联网企业的实际项目经验,对服务化系统中最难解决的一致性问题进行研究和探讨,试图从实践经验中找到规律,抽象出模式,分享给大家,希望对大家的项目实施有所帮助,在对实践的总结中也会对相关的一致性术语做最朴实的解释,希望能帮助大家彻底理解一致性的本质,并能将其应用到实践,解决读者现实中遇到的服务化系统的一致性问题,本文使用理论与实践相结合的方法,突出在实践中解决问题的模式,因此叫做《分布式服务化系统一致性的“最佳实干”》。

2 问题

本节列举不一致会导致的种种问题,这也包括一例生活中的问题。

案例1:买房

假如你想要享受生活的随意,只想买个两居,不想让房贷有太大压力,而你媳妇却想要买个三居,还得带花园的,那么你们就不一致了,不一致导致生活不愉快、不协调,严重情况下还会吵架,可见生活中的不一致问题影响很大。

案例2:转账

转账是经典的不一致案例,设想一下银行为你处理一笔转账,扣减你账户上的余额,然后增加别人账户的余额;如果扣减你的账户余额成功,增加别人账户余额失败,那么你就会损失这笔资金。反过来,如果扣减你的账户余额失败,增加别人账户余额成功,那么银行就会损失这笔资金,银行需要赔付。对于资金处理系统来说,上面任何一种场景都是不允许发生的,一旦发生就会有资金损失,后果是不堪设想的,严重情况会让一个公司瞬间倒闭,可参考案例

案例3:下订单和扣库存

电商系统中也有一个经典的案例,下订单和扣库存如何保持一致,如果先下订单,扣库存失败,那么将会导致超卖;如果下订单没有成功,扣库存成功,那么会导致少卖。两种情况都会导致运营成本的增加,严重情况下需要赔付。

案例4:同步超时

服务化的系统间调用常常因为网络问题导致系统间调用超时,即使是网络很好的机房,在亿次流量的基数下,同步调用超时也是家常便饭。系统A同步调用系统B超时,系统A可以明确得到超时反馈,但是无法确定系统B是否已经完成了预定的功能或者没有完成预定的功能。于是,系统A就迷茫了,不知道应该继续做什么,如何反馈给使用方。(曾经的一个B2B产品的客户要求接口超时重新通知他们,这个在技术上是难以实现的,因为服务器本身可能并不知道自己超时,可能会继续正常的返回数据,只是客户端并没有接受到结果罢了,因此这不是一个合理的解决方案)。

案例5:异步回调超时

此案例和上一个同步超时案例类似,不过这个场景使用了异步回调,系统A同步调用系统B发起指令,系统B采用受理模式,受理后则返回受理成功,然后系统B异步通知系统A。在这个过程中,如果系统A由于某种原因迟迟没有收到回调结果,那么两个系统间的状态就不一致,互相认知不同会导致系统间发生错误,严重情况下会影响核心事务,甚至会导致资金损失。

案例6:掉单

分布式系统中,两个系统协作处理一个流程,分别为对方的上下游,如果一个系统中存在一个请求,通常指订单,另外一个系统不存在,则导致掉单,掉单的后果很严重,有时候也会导致资金损失。

案例7:系统间状态不一致

这个案例与上面掉单案例类似,不同的是两个系统间都存在请求,但是请求的状态不一致。

案例8:缓存和数据库不一致

交易相关系统基本离不开关系型数据库,依赖关系型数据库提供的ACID特性(后面介绍),但是在大规模高并发的互联网系统里,一些特殊的场景对读的性能要求极高,服务于交易的数据库难以抗住大规模的读流量,通常需要在数据库前垫缓存,那么缓存和数据库之间的数据如何保持一致性?是要保持强一致呢还是弱一致性呢?

案例9:本地缓存节点间不一致

一个服务池上的多个节点为了满足较高的性能需求,需要使用本地缓存,使用了本地缓存,每个节点都会有一份缓存数据的拷贝,如果这些数据是静态的、不变的,那永远都不会有问题,但是如果这些数据是半静态的或者常被更新的,当被更新的时候,各个节点更新是有先后顺序的,在更新的瞬间,各个节点的数据是不一致的,如果这些数据是为某一个开关服务的,想象一下重复的请求走进了不同的节点(在failover或者补偿导致的场景下,重复请求是一定会发生的,也是服务化系统必须处理的),一个请求走了开关打开的逻辑,同时另外一个请求走了开关关闭的逻辑,这导致请求被处理两次,最坏的情况下会导致灾难性的后果,就是资金损失。

案例10:缓存数据结构不一致

这个案例会时有发生,某系统需要种某一数据结构的缓存,这一数据结构有多个数据元素组成,其中,某个数据元素都需要从数据库中或者服务中获取,如果一部分数据元素获取失败,由于程序处理不正确,仍然将不完全的数据结构存入缓存,那么缓存的消费者消费的时候很有可能因为没有合理处理异常情况而出错。

3 模式

3.1 生活中不一致问题的解决

大家回顾一下上一节列举的生活中的案例1-买房,如果置身事外来看,解决这种不一致的办法有两个,一个是避免不一致的发生,如果已经是媳妇了就不好办了:),还有一种方法就是慢慢的补偿,先买个两居,然后慢慢的等资金充裕了再换三居,买比特币赚了再换带花园的房子,于是问题最终被解决了,最终大家处于一致的状态,都开心了。这样可以解决案例1的问题,很自然由于有了过渡的方法,问题在不经意间就消失了,可见“过渡”也是解决一致性问题的一个模式。

从案例1的解决方案来看,我们要解决一致性问题,一个最直接最简单的方法就是保持强一致性,对于案例1的情况,尽量避免在结婚前两个人能够互相了解达成一致,避免不一致问题的发生;不过有些事情事已至此,发生了就是发生了,出现了不一致的问题,我们应该考虑去补偿,尽最大的努力从不一致状态修复到一致状态,避免损失全部或者一部分,也不失为一个好方法。

因此,避免不一致是上策,出现了不一致及时发现及时修复是中策,有问题不积极解决留给他人解决是下策。

3.2 酸碱平衡理论

ACID在英文中的意思是“酸”,BASE的意识是“碱”,这一段讲的是“酸碱平衡”的故事。

1. ACID(酸)

如何保证强一致性呢?计算机专业的童鞋在学习关系型数据库的时候都学习了ACID原理,这里对ACID做个简单的介绍。如果想全面的学习ACID原理,请参考ACID

关系型数据库天生就是解决具有复杂事务场景的问题,关系型数据库完全满足ACID的特性。

ACID指的是:

  • A: Atomicity,原子性
  • C: Consistency,一致性
  • I: Isolation,隔离性
  • D: Durability,持久性

具有ACID的特性的数据库支持强一致性,强一致性代表数据库本身不会出现不一致,每个事务是原子的,或者成功或者失败,事物间是隔离的,互相完全不影响,而且最终状态是持久落盘的,因此,数据库会从一个明确的状态到另外一个明确的状态,中间的临时状态是不会出现的,如果出现也会及时的自动的修复,因此是强一致的。

3个典型的关系型数据库Oracle、Mysql、Db2都能保证强一致性,Oracle和Mysql使用多版本控制协议实现,而DB2使用改进的两阶段提交协议来实现。

如果你在为交易相关系统做技术选型,交易的存储应该只考虑关系型数据库,对于核心系统,如果需要较好的性能,可以考虑使用更强悍的硬件,这种向上扩展(升级硬件)虽然成本较高,但是是最简单粗暴有效的方式,另外,Nosql完全不适合交易场景,Nosql主要用来做数据分析、ETL、报表、数据挖掘、推荐、日志处理等非交易场景。

前面提到的案例2-转账案例3-下订单和扣库存都可以利用关系型数据库的强一致性解决。

然而,前面提到,互联网项目多数具有大规模高并发的特性,必须应用拆分的理念,对高并发的压力采取“大而化小、小而化了”的方法,否则难以满足动辄亿级流量的需求,即使使用关系型数据库,单机也难以满足存储和TPS上的需求。为了保证案例2-转账可以利用关系型数据库的强一致性,在拆分的时候尽量的把转账相关的账户放入一个数据库分片,对于案例3,尽量的保证把订单和库存放入同一个数据库分片,这样通过关系型数据库自然就解决了不一致的问题。

然而,有些时候事与愿违,由于业务规则的限制,无法将相关的数据分到同一个数据库分片,这个时候我们就需要实现最终一致性。

对于案例2-转账场景,假设账户数量巨大,对账户存储进行了拆分,关系型数据库一共分了8个实例,每个实例8个库,每个库8个表,共512张表,假如要转账的两个账户正好落在了一个库里,那么可以依赖关系型数据库的事务保持强一致性。

如果要转账的两个账户正好落在了不同的库里,转账操作是无法封装在同一个数据库事务中的,这个时候会发生一个库的账户扣减余额成功,另外一个库的账户增加余额失败的情况。

对于这种情况,我们需要继续探讨解决之道,CAP原理和BASE原理,BASE原理通过记录事务的中间的临时状态,实现最终一致性。

2. CAP(帽子理论)

如果想深入的学习CAP理论,请参考CAP

由于对系统或者数据进行了拆分,我们的系统不再是单机系统,而是分布式系统,针对分布式系的帽子理论包含三个元素:

  • C:Consistency,一致性, 数据一致更新,所有数据变动都是同步的
  • A:Availability,可用性, 好的响应性能,完全的可用性指的是在任何故障模型下,服务都会在有限的时间处理响应
  • P:Partition tolerance,分区容错性,可靠性

帽子理论证明,任何分布式系统只可同时满足二点,没法三者兼顾。关系型数据库由于关系型数据库是单节点的,因此,不具有分区容错性,但是具有一致性和可用性,而分布式的服务化系统都需要满足分区容错性,那么我们必须在一致性和可用性中进行权衡,具体表现在服务化系统处理的异常请求在某一个时间段内可能是不完全的,但是经过自动的或者手工的补偿后,达到了最终的一致性。

3. BASE(碱)

BASE理论解决CAP理论提出了分布式系统的一致性和可用性不能兼得的问题,如果想全面的学习BASE原理,请参考Eventual consistency

BASE在英文中有“碱”的意思,对应本节开头的ACID在英文中“酸”的意思,基于这两个名词提出了酸碱平衡的结论,简单来说是在不同的场景下,可以分别利用ACID和BASE来解决分布式服务化系统的一致性问题。

BASE模型与ACID模型截然不同,满足CAP理论,通过牺牲强一致性,获得可用性,一般应用在服务化系统的应用层或者大数据处理系统,通过达到最终一致性来尽量满足业务的绝大部分需求。

BASE模型包含个三个元素:

  • BA:Basically Available,基本可用
  • S:Soft State,软状态,状态可以有一段时间不同步
  • E:Eventually Consistent,最终一致,最终数据是一致的就可以了,而不是时时保持强一致

BASE模型的软状态是实现BASE理论的方法,基本可用和最终一致是目标。按照BASE模型实现的系统,由于不保证强一致性,系统在处理请求的过程中,可以存在短暂的不一致,在短暂的不一致窗口请求处理处在临时状态中,系统在做每步操作的时候,通过记录每一个临时状态,在系统出现故障的时候,可以从这些中间状态继续未完成的请求处理或者退回到原始状态,最后达到一致的状态。

案例1-转账为例,我们把用户A给用户B转账分成四个阶段,第一个阶段用户A准备转账,第二个阶段从用户A账户扣减余额,第三个阶段对用户B增加余额,第四个阶段完成转账。系统需要记录操作过程中每一步骤的状态,一旦系统出现故障,系统能够自动发现没有完成的任务,然后,根据任务所处的状态,继续执行任务,最终完成任务,达到一致的最终状态。

在实际应用中,上面这个过程通常是通过持久化执行任务的状态和环境信息,一旦出现问题,定时任务会捞取未执行完的任务,继续未执行完的任务,直到执行完成为止,或者取消已经完成的部分操作回到原始状态。这种方法在任务完成每个阶段的时候,都要更新数据库中任务的状态,这在大规模高并发系统中不会有太好的性能,一个更好的办法是用Write-Ahead Log(写前日志),这和数据库的Bin Log(操作日志)相似,在做每一个操作步骤,都先写入日志,如果操作遇到问题而停止的时候,可以读取日志按照步骤进行恢复,并且继续执行未完成的工作,最后达到一致。写前日志可以利用机械硬盘的追加写而达到较好性能,因此,这是一种专业化的实现方式,多数业务系系统还是使用数据库记录的字段来记录任务的执行状态,也就是记录中间的“软状态”,一个任务的状态流转一般可以通过数据库的行级锁来实现,这比使用Write-Ahead Log实现更简单、更快速。

有了BASE理论作为基础,我们对复杂的分布式事务进行拆解,对其中的每一步骤都记录其状态,有问题的时候可以根据记录的状态来继续执行任务,达到最终的一致,通过这个方法我们可以解决案例2-转账案例3-下订单和扣库存中遇到的问题。

4. 酸碱平衡的总结

  1. 使用向上扩展(强悍的硬件)运行专业的关系型数据库(例如:Oracle或者DB2)能够保证强一致性,钱能解决的问题就不是问题
  2. 如果钱是问题,可以对廉价硬件运行的开源关系型数据库(例如:Mysql)进行分片,将相关的数据分到数据库的同一个片,仍然能够使用关系型数据库保证事务
  3. 如果业务规则限制,无法将相关的数据分到同一个片,就需要实现最终一致性,通过记录事务的软状态(中间状态、临时状态),一旦处于不一致,可以通过系统自动化或者人工干预来修复不一致的情况

3.3 分布式一致性协议

国际开放标准组织Open Group定义了DTS(分布式事务处理模型),模型中包含4个角色:应用程序、事务管理器、资源管理器、通信资源管理器四部分。事务处理器是统管全局的管理者,资源处理器和通信资源处理器是事务的参与者。

J2EE规范也包含此分布式事务处理模型的规范,并在所有的AppServer中进行实现,J2EE规范中定义了TX协议和XA协议,TX协议定义应用程序与事务管理器之间的接口,而XA协议定义了事务管理器与资源处理器之间的接口,在过去,大家使用AppServer,例如:Websphere、Weblogic、Jboss等配置数据源的时候会看见类似XADatasource的数据源,这就是实现了DTS的关系型数据库的数据源。企业级开发JEE中,关系型数据库、JMS服务扮演资源管理器的角色,而EJB容器则扮演事务管理器的角色。

下面我们就介绍两阶段提交协议三阶段提交协议以及阿里巴巴提出的TCC,它们都是根据DTS这一思想演变出来的。

1. 两阶段提交协议

上面描述的JEE的XA协议就是根据两阶段提交来保证事务的完整性,并实现分布式服务化的强一致性。

两阶段提交协议把分布式事务分成两个过程,一个是准备阶段,一个是提交阶段,准备阶段和提交阶段都是由事务管理器发起的,为了接下来讲解方便,我们把事务管理器称为协调者,把资管管理器称为参与者。

两阶段如下:

  1. 准备阶段:协调者向参与者发起指令,参与者评估自己的状态,如果参与者评估指令可以完成,参与者会写redo或者undo日志(这也是前面提起的Write-Ahead Log的一种),然后锁定资源,执行操作,但是并不提交
  2. 提交阶段:如果每个参与者明确返回准备成功,也就是预留资源和执行操作成功,协调者向参与者发起提交指令,参与者提交资源变更的事务,释放锁定的资源;如果任何一个参与者明确返回准备失败,也就是预留资源或者执行操作失败,协调者向参与者发起中止指令,参与者取消已经变更的事务,执行undo日志,释放锁定的资源

两阶段提交协议成功场景示意图如下:

两阶段提交协议

我们看到两阶段提交协议在准备阶段锁定资源,是一个重量级的操作,并能保证强一致性,但是实现起来复杂、成本较高,不够灵活,更重要的是它有如下致命的问题:

  1. 阻塞:从上面的描述来看,对于任何一次指令必须收到明确的响应,才会继续做下一步,否则处于阻塞状态,占用的资源被一直锁定,不会被释放
  2. 单点故障:如果协调者宕机,参与者没有了协调者指挥,会一直阻塞,尽管可以通过选举新的协调者替代原有协调者,但是如果之前协调者在发送一个提交指令后宕机,而提交指令仅仅被一个参与者接受,并且参与者接收后也宕机,新上任的协调者无法处理这种情况
  3. 脑裂:协调者发送提交指令,有的参与者接收到执行了事务,有的参与者没有接收到事务,就没有执行事务,多个参与者之间是不一致的

上面所有的这些问题,都是需要人工干预处理,没有自动化的解决方案,因此两阶段提交协议在正常情况下能保证系统的强一致性,但是在出现异常情况下,当前处理的操作处于错误状态,需要管理员人工干预解决,因此可用性不够好,这也符合CAP协议的一致性和可用性不能兼得的原理。

2. 三阶段提交协议

三阶段提交协议是两阶段提交协议的改进版本。它通过超时机制解决了阻塞的问题,并且把两个阶段增加为三个阶段:

  1. 询问阶段:协调者询问参与者是否可以完成指令,协调者只需要回答是还是不是,而不需要做真正的操作,这个阶段超时导致中止
  2. 准备阶段:如果在询问阶段所有的参与者都返回可以执行操作,协调者向参与者发送预执行请求,然后参与者写redo和undo日志,执行操作,但是不提交操作;如果在询问阶段任何参与者返回不能执行操作的结果,则协调者向参与者发送中止请求,这里的逻辑与两阶段提交协议的的准备阶段是相似的,这个阶段超时导致成功
  3. 提交阶段:如果每个参与者在准备阶段返回准备成功,也就是预留资源和执行操作成功,协调者向参与者发起提交指令,参与者提交资源变更的事务,释放锁定的资源;如果任何一个参与者返回准备失败,也就是预留资源或者执行操作失败,协调者向参与者发起中止指令,参与者取消已经变更的事务,执行undo日志,释放锁定的资源,这里的逻辑与两阶段提交协议的提交阶段一致

三阶段提交协议成功场景示意图如下:

三阶段提交协议

然而,这里与两阶段提交协议有两个主要的不同:

  1. 增加了一个询问阶段,询问阶段可以确保尽可能早的发现无法执行操作而需要中止的行为,但是它并不能发现所有的这种行为,只会减少这种情况的发生
  2. 在准备阶段以后,协调者和参与者执行的任务中都增加了超时,一旦超时,协调者和参与者都继续提交事务,默认为成功,这也是根据概率统计上超时后默认成功的正确性最大

三阶段提交协议与两阶段提交协议相比,具有如上的优点,但是一旦发生超时,系统仍然会发生不一致,只不过这种情况很少见罢了,好处就是至少不会阻塞和永远锁定资源。

3. TCC

上面两节讲解了两阶段提交协议和三阶段提交协议,实际上他们能解决案例2-转账案例3-下订单和扣库存中的分布式事务的问题,但是遇到极端情况,系统会发生阻塞或者不一致的问题,需要运营或者技术人工解决。无论两阶段还是三阶段方案中都包含多个参与者、多个阶段实现一个事务,实现复杂,性能也是一个很大的问题,因此,在互联网高并发系统中,鲜有使用两阶段提交和三阶段提交协议的场景。

阿里巴巴提出了新的TCC协议,TCC协议将一个任务拆分成Try、Confirm、Cancel,正常的流程会先执行Try,如果执行没有问题,再执行Confirm,如果执行过程中出了问题,则执行操作的逆操Cancel,从正常的流程上讲,这仍然是一个两阶段的提交协议,但是,在执行出现问题的时候,有一定的自我修复能力,如果任何一个参与者出现了问题,协调者通过执行操作的逆操作来取消之前的操作,达到最终的一致状态。

可以看出,从时序上,如果遇到极端情况下TCC会有很多问题的,例如,如果在Cancel的时候一些参与者收到指令,而一些参与者没有收到指令,整个系统仍然是不一致的,这种复杂的情况,系统首先会通过补偿的方式,尝试自动修复的,如果系统无法修复,必须由人工参与解决。

从TCC的逻辑上看,可以说TCC是简化版的三阶段提交协议,解决了两阶段提交协议的阻塞问题,但是没有解决极端情况下会出现不一致和脑裂的问题。然而,TCC通过自动化补偿手段,会把需要人工处理的不一致情况降到到最少,也是一种非常有用的解决方案,根据线人,阿里在内部的一些中间件上实现了TCC模式。

我们给出一个使用TCC的实际案例,在秒杀的场景,用户发起下单请求,应用层先查询库存,确认商品库存还有余量,则锁定库存,此时订单状态为待支付,然后指引用户去支付,由于某种原因用户支付失败,或者支付超时,系统会自动将锁定的库存解锁供其他用户秒杀。

TCC协议使用场景示意图如下:

TCC

总结一下,两阶段提交协议、三阶段提交协议、TCC协议都能保证分布式事务的一致性,他们保证的分布式系统的一致性从强到弱,TCC达到的目标是最终一致性,其中任何一种方法都可以不同程度的解决案例2:转账、案例3:下订单和扣库存的问题,只是实现的一致性的级别不一样而已,对于案例4:同步超时可以通过TCC的理念解决,如果同步调用超时,调用方可以使用fastfail策略,返回调用方的使用方失败的结果,同时调用服务的逆向cancel操作,保证服务的最终一致性。

3.4 保证最终一致性的模式

在大规模高并发服务化系统中,一个功能被拆分成多个具有单一功能的元功能,一个流程会有多个系统的多个元功能组合实现,如果使用两阶段提交协议和三阶段提交协议,确实能解决系统间一致性问题,除了这两个协议带来的自身的问题,这些协议的实现比较复杂、成本比较高,最重要的是性能并不好,相比来看,TCC协议更简单、容易实现,但是TCC协议由于每个事务都需要执行Try,再执行Confirm,略微显得臃肿,因此,在现实的系统中,底线要求仅仅需要能达到最终一致性,而不需要实现专业的、复杂的一致性协议,实现最终一致性有一些非常有效的、简单粗暴的模式,下面就介绍这些模式及其应用场景。

1. 查询模式

任何一个服务操作都需要提供一个查询接口,用来向外部输出操作执行的状态。服务操作的使用方可以通过查询接口,得知服务操作执行的状态,然后根据不同状态来做不同的处理操作。

为了能够实现查询,每个服务操作都需要有唯一的流水号标识,也可使用此次服务操作对应的资源ID来标志,例如:请求流水号、订单号等。

首先,单笔查询操作是必须提供的,我们也鼓励使用单笔订单查询,这是因为每次调用需要占用的负载是可控的,批量查询则根据需要来提供,如果使用了批量查询,需要有合理的分页机制,并且必须限制分页的大小,以及对批量查询的QPS需要有容量评估和流控等。

查询模式的示意图如下:

查询模式

对于案例4:同步超时、案例5:异步回调超时、案例6:掉单、案例7:系统间状态不一致,我们都需要使用查询模式来了解被调用服务的处理情况,来决定下一步做什么:补偿未完成的操作还是回滚已经完成的操作。

2. 补偿模式

有了上面的查询模式,在任何情况下,我们都能得知具体的操作所处的状态,如果整个操作处于不正常的状态,我们需要修正操作中有问题的子操作,这可能需要重新执行未完成的子操作,后者取消已经完成的子操作,通过修复使整个分布式系统达到一致,为了让系统最终一致而做的努力都叫做补偿。

对于服务化系统中同步调用的操作,业务操作发起的主动方在还没有得到业务操作执行方的明确返回或者调用超时,场景可参考案例4:同步超时,这个时候业务发起的主动方需要及时的调用业务执行方获得操作执行的状态,这里使用查询模式,获得业务操作的执行方的状态后,如果业务执行方已经完预设的工作,则业务发起方给业务的使用方返回成功,如果业务操作的执行方的状态为失败或者未知,则会立即告诉业务的使用方失败,然后调用业务操作的逆向操作,保证操作不被执行或者回滚已经执行的操作,让业务的使用方、业务发起的主动方、业务的操作方最终达成一致的状态。

补偿模式的示意图如下:

补偿模式

补偿操作根据发起形式分为:

  1. 自动恢复:程序根据发生不一致的环境,通过继续未完成的操作,或者回滚已经完成的操作,自动来达到一致
  2. 通知运营:如果程序无法自动恢复,并且设计时考虑到了不一致的场景,可以提供运营功能,通过运营手工进行补偿
  3. 通知技术:如果很不巧,系统无法自动回复,又没有运营功能,那必须通过技术手段来解决,技术手段包括走数据库变更或者代码变更来解决,这是最糟的一种场景

3. 异步确保模式

异步确保模式是补偿模式的一个典型案例,经常应用到使用方对响应时间要求并不太高,我们通常把这类操作从主流程中摘除,通过异步的方式进行处理,处理后把结果通过通知系统通知给使用方,这个方案最大的好处能够对高并发流量进行消峰,例如:电商系统中的物流、配送,以及支付系统中的计费、入账等。

实践中,将要执行的异步操作封装后持久入库,然后通过定时捞取未完成的任务进行补偿操作来实现异步确保模式,只要定时系统足够健壮,任何一个任务最终会被成功执行。

异步确保模式的示意图如下:

异步确保模式

对于案例5:异步回调超时,使用的就是异步确保模式,这种情况下对于某个操作,如果迟迟没有收到响应,我们通过查询模式和补偿模式来继续未完成的操作。

4. 定期校对模式

既然我们在系统中实现最终一致性,系统在没有达到一致之前,系统间的状态是不一致的,甚至是混乱的,需要补偿操作来达到一致的目的,但是我们如何来发现需要补偿的操作呢?

在操作的主流程中的系统间执行校对操作,我们可以事后异步的批量校对操作的状态,如果发现不一致的操作,则进行补偿,补偿操作与补偿模式中的补偿操作是一致的。

另外,实现定期校对的一个关键就是分布式系统中需要有一个自始至终唯一的ID,ID的生成请参考SnowFlake

在分布式系统中,全局唯一ID的示意图如下:

唯一ID

一般情况下,生成全局唯一ID有两种方法:

  1. 持久型:使用数据库表自增字段或者Sequence生成,为了提高效率,每个应用节点可以缓存一批次的ID,如果机器重启可能会损失一部分ID,但是这并不会产生任何问题
  2. 时间型:一般由机器号、业务号、时间、单节点内自增ID组成,由于时间一般精确到秒或者毫秒,因此不需要持久就能保证在分布式系统中全局唯一、粗略递增能特点

实践中,为了能在分布式系统中迅速的定位问题,一般的分布式系统都有技术支持系统,它能够跟踪一个请求的调用链,调用链是在二维的维度跟踪一个调用请求,最后形成一个调用树,原理可参考谷歌的论文Dapper, a Large-Scale Distributed Systems Tracing Infrastructure,一个开源的参考实现为pinpoint

在分布式系统中,调用链的示意图如下:

调用链

全局的唯一流水ID可以把一个请求在分布式系统中的流转的路径聚合,而调用链中的spanid可以把聚合的请求路径通过树形结构进行展示,让技术支持人员轻松的发现系统出现的问题,能够快速定位出现问题的服务节点,提高应急效率。

关于订单跟踪、调用链跟踪、业务链跟踪,我们会在后续文章中详细介绍。

在分布式系统中构建了唯一ID,调用链等基础设施,我们很容易对系统间的不一致进行核对,通常我们需要构建第三方的定期核对系统,以第三方的角度来监控服务执行的健康程度。

定期核对系统示意图如下:

定期核对模式

对于案例6:掉单、案例7:系统间状态不一致通常通过定期校对模式发现问题,并通过补偿模式来修复,最后完成系统间的最终一致性。

定期校对模式多应用在金融系统,金融系统由于涉及到资金安全,需要保证百分之百的准确性,所以,需要多重的一致性保证机制,包括:系统间的一致性对账、现金对账、账务对账、手续费对账等等,这些都属于定期校对模式,顺便说一下,金融系统与社交应用在技术上本质的区别在于社交应用在于量大,而金融系统在于数据的准确性。

到现在为止,我们看到通过查询模式、补偿模式、定期核对模式可以解决案例4到案例7的所有问题,对于案例4:同步超时,如果同步超时,我们需要查询状态进行补偿,对于案例5:异步回调超时,如果迟迟没有收到回调响应,我们也会通过查询状态进行补偿,对于案例6:掉单、案例7:系统间状态不一致,我们通过定期核对模式可以保证系统间操作的一致性,避免掉单和状态不一致导致问题。

5. 可靠消息模式

在分布式系统中,对于主流程中优先级比较低的操作,大多采用异步的方式执行,也就是前面提到的异步确保型,为了让异步操作的调用方和被调用方充分的解耦,也由于专业的消息队列本身具有可伸缩、可分片、可持久等功能,我们通常通过消息队列实现异步化,对于消息队列,我们需要建立特殊的设施保证可靠的消息发送以及处理机的幂等等。

消息的可靠发送

消息的可靠发送可以认为是尽最大努力发送消息通知,有两种实现方法:

第一种,发送消息之前,把消息持久到数据库,状态标记为待发送,然后发送消息,如果发送成功,将消息改为发送成功。定时任务定时从数据库捞取一定时间内未发送的消息,将消息发送。

消息发送模式1

第二种,实现方式与第一种类似,不同的是持久消息的数据库是独立的,并不耦合在业务系统中。发送消息之前,先发送一个预消息给某一个第三方的消息管理器,消息管理器将其持久到数据库,并标记状态为待发送,发送成功后,标记消息为发送成功。定时任务定时从数据库捞取一定时间内未发送的消息,回查业务系统是否要继续发送,根据查询结果来确定消息的状态。

消息发送模式2

一些公司把消息的可靠发送实现在了中间件里,通过Spring的注入,在消息发送的时候自动持久消息记录,如果有消息记录没有发送成功,定时会补偿发送。

消息处理器的幂等性

如果我们要保证消息可靠的发送,简单来说,要保证消息一定要发送出去,那么就需要有重试机制,有了重试机制,消息一定会重复,那么我们需要对重复做处理。

处理重复的最佳方式为保证操作的幂等性,幂等性的数学公式为:

f(f(x)) = f(x)

保证操作的幂等性常用的几个方法:

  1. 使用数据库表的唯一键进行滤重,拒绝重复的请求
  2. 使用分布式表对请求进行滤重
  3. 使用状态流转的方向性来滤重,通常使用行级锁来实现(后续在锁相关的文章中详细说明)
  4. 根据业务的特点,操作本身就是幂等的,例如:删除一个资源、增加一个资源、获得一个资源等

6. 缓存一致性模型

大规模高并发系统中一个常见的核心需求就是亿级的读需求,显然,关系型数据库并不是解决高并发读需求的最佳方案,互联网的经典做法就是使用缓存抗读需求,下面有一些使用缓存的保证一致性的最佳实践:

  1. 如果性能要求不是非常的高,尽量使用分布式缓存,而不要使用本地缓存
  2. 种缓存的时候一定种完全,如果缓存数据的一部分有效,一部分无效,宁可放弃种缓存,也不要把部分数据种入缓存
  3. 数据库与缓存只需要保持弱一致性,而不需要强一致性,读的顺序要先缓存,后数据库,写的顺序要先数据库,后缓存

这里的最佳实践能够解决案例8:缓存和数据库不一致、案例9:本地缓存节点间不一致、案例10:缓存数据结构不一致的问题,对于数据存储层、缓存与数据库、Nosql等的一致性是更深入的存储一致性技术,将会在后续文章单独介绍,这里的数据一致性主要是处理应用层与缓存、应用层与数据库、一部分的缓存与数据库的一致性。

3.5 专题模式

这一节介绍特殊场景下的一致性问题和解决方案。

迁移开关的设计

在大多数企业里,新项目和老项目一般会共存,大家都在努力的下掉老项目,但是由于种种原因总是下不掉,如果要彻底的下掉老项目,就必须要有非常完善的迁移方案,迁移是一项非常复杂而艰巨的任务,我会在将来的文章中详细探讨迁移方案、流程和技术,这里我们只对迁移中使用的开关进行描述。

迁移过程必须使用开关,开关一般都会基于多个维度来设计,例如:全局的、用户的、角色的、商户的、产品的等等,如果迁移过程中遇到问题,我们需要关闭开关,迁移回老的系统,这需要我们的新系统兼容老的数据,老的系统也兼容新的数据,从某种意义上来讲,迁移比实现新系统更加困难。

曾经看过很多简单的开关设计,有的开关设计在应用层次,通过一个curl语句调用,没有权限控制,这样的开关在服务池的每个节点都是不同步的、不一致的;还有的系统把开关配置放在中心化的配置系统、数据库或者缓存等,处理的每个请求都通过统一的开关来判断是否迁移等等,这样的开关有一个致命的缺点,服务请求在处理过程中,开关可能会变化,各个节点之间开关可能不同步、不一致,导致重复的请求可能走到新的逻辑又走了老的逻辑,如果新的逻辑和老的逻辑没有保证幂等性,这个请求就被重复处理了,如果是金融行业的应用,可能会导致资金损失,电商系统可能会导致发货并退款等问题。

这里面我们推荐使用订单开关,不管我们在什么维度上设计了开关,接收到服务请求后,我们在请求创建的关联实体(例如:订单)上标记开关,以后的任何处理流程,包括同步的和异步的处理流程,都通过订单上的开关来判断,而不是通过全局的或者基于配置的开关,这样在订单创建的时候,开关已经确定,不再变更,一旦一份数据不再发生变化,那么它永远是线程安全的,并且不会有不一致的问题。

这个模式在生产中使用比较频繁,建议每个企业都把这个模式作为设计评审的一项,如果不检查这一项,很多开发童鞋都会偷懒,直接在配置中或者数据库中做个开关就上线了。

4 总结

本文从一致性问题的实践出发,从大规模高并发服务化系统的实践经验中进行总结,列举导致不一致的具体问题,围绕着具体问题,总结出解决不一致的方法,并且抽象成模式,供大家在开发服务化系统的过程中参考。

另外,由于篇幅有限,还有一些关于分布式一致性的技术无法在一篇文章中与大家分享,包括:paxos算法、raft算法、zab算法、nwr算法、一致性哈希等,我会在后续文章中详细介绍。

5 反馈与建议

from:http://www.jianshu.com/p/1156151e20c8

当你在浏览器中输入 google.com 并且按下回车之后发生了什么?

这个仓库试图回答一个古老的面试问题:当你在浏览器中输入 google.com 并且按下回车之后发生了什么?

不过我们不再局限于平常的回答,而是想办法回答地尽可能具体,不遗漏任何细节。

这将是一个协作的过程,所以深入挖掘吧,并且帮助我们一起完善它。仍然有大量的细节等待着你来添加,欢迎向我们发送 Pull Requset!

这些内容使用 Creative Commons Zero 协议发布。

目录

按下”g”键

接下来的内容介绍了物理键盘和系统中断的工作原理,但是有一部分内容却没有涉及。当你按下“g”键,浏览器接收到这个消息之后,会触发自动完成机制。浏览器根据自己的算法,以及你是否处于隐私浏览模式,会在浏览器的地址框下方给出输入建议。大部分算法会优先考虑根据你的搜索历史和书签等内容给出建议。你打算输入 “google.com”,因此给出的建议并不匹配。但是输入过程中仍然有大量的代码在后台运行,你的每一次按键都会使得给出的建议更加准确。甚至有可能在你输入之前,浏览器就将 “google.com” 建议给你。

回车键按下

为了从零开始,我们选择键盘上的回车键被按到最低处作为起点。在这个时刻,一个专用于回车键的电流回路被直接地或者通过电容器间接地闭合了,使得少量的电流进入了键盘的逻辑电路系统。这个系统会扫描每个键的状态,对于按键开关的电位弹跳变化进行噪音消除(debounce),并将其转化为键盘码值。在这里,回车的码值是13。键盘控制器在得到码值之后,将其编码,用于之后的传输。现在这个传输过程几乎都是通过通用串行总线(USB)或者蓝牙(Bluetooth)来进行的,以前是通过PS/2或者ADB连接进行。

USB键盘:

  • 键盘的USB元件通过计算机上的USB接口与USB控制器相连接,USB接口中的第一号针为它提供了5V的电压
  • 键码值存储在键盘内部电路一个叫做”endpoint”的寄存器内
  • USB控制器大概每隔10ms便查询一次”endpoint”以得到存储的键码值数据,这个最短时间间隔由键盘提供
  • 键值码值通过USB串行接口引擎被转换成一个或者多个遵循低层USB协议的USB数据包
  • 这些数据包通过D+针或者D-针(中间的两个针),以最高1.5Mb/s的速度从键盘传输至计算机。速度限制是因为人机交互设备总是被声明成”低速设备”(USB 2.0 compliance)
  • 这个串行信号在计算机的USB控制器处被解码,然后被人机交互设备通用键盘驱动进行进一步解释。之后按键的码值被传输到操作系统的硬件抽象层

虚拟键盘(触屏设备):

  • 在现代电容屏上,当用户把手指放在屏幕上时,一小部分电流从传导层的静电域经过手指传导,形成了一个回路,使得屏幕上触控的那一点电压下降,屏幕控制器产生一个中断,报告这次“点击”的坐标
  • 然后移动操作系统通知当前活跃的应用,有一个点击事件发生在它的某个GUI部件上了,现在这个部件是虚拟键盘的按钮
  • 虚拟键盘引发一个软中断,返回给OS一个“按键按下”消息
  • 这个消息又返回来向当前活跃的应用通知一个“按键按下”事件

产生中断[非USB键盘]

键盘在它的中断请求线(IRQ)上发送信号,信号会被中断控制器映射到一个中断向量,实际上就是一个整型数 。CPU使用中断描述符表(IDT)把中断向量映射到对应函数,这些函数被称为中断处理器,它们由操作系统内核提供。当一个中断到达时,CPU根据IDT和中断向量索引到对应的中断处理器,然后操作系统内核出场了。

(Windows)一个 WM_KEYDOWN 消息被发往应用程序

HID把键盘按下的事件传送给 KBDHID.sys 驱动,把HID的信号转换成一个扫描码(Scancode),这里回车的扫描码是 VK_RETURN(0x0d)KBDHID.sys 驱动和 KBDCLASS.sys (键盘类驱动,keyboard class driver)进行交互,这个驱动负责安全地处理所有键盘和小键盘的输入事件。之后它又去调用 Win32K.sys ,在这之前有可能把消息传递给安装的第三方键盘过滤器。这些都是发生在内核模式。

Win32K.sys 通过 GetForegroundWindow() API函数找到当前哪个窗口是活跃的。这个API函数提供了当前浏览器的地址栏的句柄。Windows系统的”message pump”机制调用 SendMessage(hWnd, WM_KEYDOWN, VK_RETURN, lParam) 函数, lParam 是一个用来指示这个按键的更多信息的掩码,这些信息包括按键重复次数(这里是0),实际扫描码(可能依赖于OEM厂商,不过通常不会是 VK_RETURN ),功能键(alt, shift, ctrl)是否被按下(在这里没有),以及一些其他状态。

Windows的 SendMessage API直接将消息添加到特定窗口句柄 hWnd 的消息队列中,之后赋给 hWnd 的主要消息处理函数 WindowProc 将会被调用,用于处理队列中的消息。

当前活跃的句柄 hWnd 实际上是一个edit control控件,这种情况下,WindowProc 有一个用于处理 WM_KEYDOWN 消息的处理器,这段代码会查看 SendMessage 传入的第三个参数 wParam ,因为这个参数是 VK_RETURN ,于是它知道用户按下了回车键。

(Mac OS X)一个 KeyDown NSEvent被发往应用程序

中断信号引发了I/O Kit Kext键盘驱动的中断处理事件,驱动把信号翻译成键码值,然后传给OS X的 WindowServer 进程。然后, WindowServer 将这个事件通过Mach端口分发给合适的(活跃的,或者正在监听的)应用程序,这个信号会被放到应用程序的消息队列里。队列中的消息可以被拥有足够高权限的线程使用 mach_ipc_dispatch 函数读取到。这个过程通常是由 NSApplication 主事件循环产生并且处理的,通过 NSEventTypeKeyDownNSEvent

(GNU/Linux)Xorg 服务器监听键码值

当使用图形化的 X Server 时,X Server 会按照特定的规则把键码值再一次映射,映射成扫描码。当这个映射过程完成之后, X Server 把这个按键字符发送给窗口管理器(DWM,metacity, i3等等),窗口管理器再把字符发送给当前窗口。当前窗口使用有关图形API把文字打印在输入框内。

解析URL

  • 浏览器通过 URL 能够知道下面的信息:
    • Protocol “http”
      使用HTTP协议
    • Resource “/”
      请求的资源是主页(index)

输入的是 URL 还是搜索的关键字?

当协议或主机名不合法时,浏览器会将地址栏中输入的文字传给默认的搜索引擎。大部分情况下,在把文字传递给搜索引擎的时候,URL会带有特定的一串字符,用来告诉搜索引擎这次搜索来自这个特定浏览器。

转换非 ASCII 的 Unicode 字符

  • 浏览器检查输入是否含有不是 a-zA-Z0-9- 或者 . 的字符
  • 这里主机名是 google.com ,所以没有非ASCII的字符;如果有的话,浏览器会对主机名部分使用 Punycode 编码

检查 HSTS 列表···

  • 浏览器检查自带的“预加载 HSTS(HTTP严格传输安全)”列表,这个列表里包含了那些请求浏览器只使用HTTPS进行连接的网站
  • 如果网站在这个列表里,浏览器会使用 HTTPS 而不是 HTTP 协议,否则,最初的请求会使用HTTP协议发送
  • 注意,一个网站哪怕不在 HSTS 列表里,也可以要求浏览器对自己使用 HSTS 政策进行访问。浏览器向网站发出第一个 HTTP 请求之后,网站会返回浏览器一个响应,请求浏览器只使用 HTTPS 发送请求。然而,就是这第一个 HTTP 请求,却可能会使用户收到 downgrade attack 的威胁,这也是为什么现代浏览器都预置了 HSTS 列表。

DNS 查询···

  • 浏览器检查域名是否在缓存当中(要查看 Chrome 当中的缓存, 打开 chrome://net-internals/#dns)。
  • 如果缓存中没有,就去调用 gethostbyname 库函数(操作系统不同函数也不同)进行查询。
  • gethostbyname 函数在试图进行DNS解析之前首先检查域名是否在本地 Hosts 里,Hosts 的位置 不同的操作系统有所不同
  • 如果 gethostbyname 没有这个域名的缓存记录,也没有在 hosts 里找到,它将会向 DNS 服务器发送一条 DNS 查询请求。DNS 服务器是由网络通信栈提供的,通常是本地路由器或者 ISP 的缓存 DNS 服务器。
  • 查询本地 DNS 服务器
  • 如果 DNS 服务器和我们的主机在同一个子网内,系统会按照下面的 ARP 过程对 DNS 服务器进行 ARP查询
  • 如果 DNS 服务器和我们的主机在不同的子网,系统会按照下面的 ARP 过程对默认网关进行查询

ARP 过程

要想发送 ARP(地址解析协议)广播,我们需要有一个目标 IP 地址,同时还需要知道用于发送 ARP 广播的接口的 MAC 地址。

  • 首先查询 ARP 缓存,如果缓存命中,我们返回结果:目标 IP = MAC

如果缓存没有命中:

  • 查看路由表,看看目标 IP 地址是不是在本地路由表中的某个子网内。是的话,使用跟那个子网相连的接口,否则使用与默认网关相连的接口。
  • 查询选择的网络接口的 MAC 地址
  • 我们发送一个二层( OSI 模型 中的数据链路层)ARP 请求:

ARP Request:

Sender MAC: interface:mac:address:here
Sender IP: interface.ip.goes.here
Target MAC: FF:FF:FF:FF:FF:FF (Broadcast)
Target IP: target.ip.goes.here

根据连接主机和路由器的硬件类型不同,可以分为以下几种情况:

直连:

  • 如果我们和路由器是直接连接的,路由器会返回一个 ARP Reply (见下面)。

集线器:

  • 如果我们连接到一个集线器,集线器会把 ARP 请求向所有其它端口广播,如果路由器也“连接”在其中,它会返回一个 ARP Reply

交换机:

  • 如果我们连接到了一个交换机,交换机会检查本地 CAM/MAC 表,看看哪个端口有我们要找的那个 MAC 地址,如果没有找到,交换机会向所有其它端口广播这个 ARP 请求。
  • 如果交换机的 MAC/CAM 表中有对应的条目,交换机会向有我们想要查询的 MAC 地址的那个端口发送 ARP 请求
  • 如果路由器也“连接”在其中,它会返回一个 ARP Reply

ARP Reply:

Sender MAC: target:mac:address:here
Sender IP: target.ip.goes.here
Target MAC: interface:mac:address:here
Target IP: interface.ip.goes.here

现在我们有了 DNS 服务器或者默认网关的 IP 地址,我们可以继续 DNS 请求了:

  • 使用 53 端口向 DNS 服务器发送 UDP 请求包,如果响应包太大,会使用 TCP 协议
  • 如果本地/ISP DNS 服务器没有找到结果,它会发送一个递归查询请求,一层一层向高层 DNS 服务器做查询,直到查询到起始授权机构,如果找到会把结果返回

使用套接字

当浏览器得到了目标服务器的 IP 地址,以及 URL 中给出来端口号(http 协议默认端口号是 80, https 默认端口号是 443),它会调用系统库函数 socket ,请求一个 TCP流套接字,对应的参数是 AF_INET/AF_INET6SOCK_STREAM

  • 这个请求首先被交给传输层,在传输层请求被封装成 TCP segment。目标端口会被加入头部,源端口会在系统内核的动态端口范围内选取(Linux下是ip_local_port_range)
  • TCP segment 被送往网络层,网络层会在其中再加入一个 IP 头部,里面包含了目标服务器的IP地址以及本机的IP地址,把它封装成一个TCP packet。
  • 这个 TCP packet 接下来会进入链路层,链路层会在封包中加入 frame头 部,里面包含了本地内置网卡的MAC地址以及网关(本地路由器)的 MAC 地址。像前面说的一样,如果内核不知道网关的 MAC 地址,它必须进行 ARP 广播来查询其地址。

到了现在,TCP 封包已经准备好了,可以使用下面的方式进行传输:

对于大部分家庭网络和小型企业网络来说,封包会从本地计算机出发,经过本地网络,再通过调制解调器把数字信号转换成模拟信号,使其适于在电话线路,有线电视光缆和无线电话线路上传输。在传输线路的另一端,是另外一个调制解调器,它把模拟信号转换回数字信号,交由下一个 网络节点 处理。节点的目标地址和源地址将在后面讨论。

大型企业和比较新的住宅通常使用光纤或直接以太网连接,这种情况下信号一直是数字的,会被直接传到下一个 网络节点 进行处理。

最终封包会到达管理本地子网的路由器。在那里出发,它会继续经过自治区域的边界路由器,其他自治区域,最终到达目标服务器。一路上经过的这些路由器会从IP数据报头部里提取出目标地址,并将封包正确地路由到下一个目的地。IP数据报头部TTL域的值每经过一个路由器就减1,如果封包的TTL变为0,或者路由器由于网络拥堵等原因封包队列满了,那么这个包会被路由器丢弃。

上面的发送和接受过程在 TCP 连接期间会发生很多次:

  • 客户端选择一个初始序列号(ISN),将设置了 SYN 位的封包发送给服务器端,表明自己要建立连接并设置了初始序列号
  • 服务器端接收到 SYN 包,如果它可以建立连接:
    • 服务器端选择它自己的初始序列号
    • 服务器端设置 SYN 位,表明自己选择了一个初始序列号
    • 服务器端把 (客户端ISN + 1) 复制到 ACK 域,并且设置 ACK 位,表明自己接收到了客户端的第一个封包
  • 客户端通过发送下面一个封包来确认这次连接:
    • 自己的序列号+1
    • 接收端 ACK+1
    • 设置 ACK 位
  • 数据通过下面的方式传输:
    • 当一方发送了N个 Bytes 的数据之后,将自己的 SEQ 序列号也增加N
    • 另一方确认接收到这个数据包(或者一系列数据包)之后,它发送一个 ACK 包,ACK 的值设置为接收到的数据包的最后一个序列号
  • 关闭连接时:
    • 要关闭连接的一方发送一个 FIN 包
    • 另一方确认这个 FIN 包,并且发送自己的 FIN 包
    • 要关闭的一方使用 ACK 包来确认接收到了 FIN

UDP 数据包

TLS 握手

  • 客户端发送一个 Client hello 消息到服务器端,消息中同时包含了它的TLS版本,可用的加密算法和压缩算法。
  • 服务器端向客户端返回一个 Server hello 消息,消息中包含了服务器端的TLS版本,服务器选择了哪个加密和压缩算法,以及服务器的公开证书,证书中包含了公钥。客户端会使用这个公钥加密接下来的握手过程,直到协商生成一个新的对称密钥
  • 客户端根据自己的信任CA列表,验证服务器端的证书是否有效。如果有效,客户端会生成一串伪随机数,使用服务器的公钥加密它。这串随机数会被用于生成新的对称密钥
  • 服务器端使用自己的私钥解密上面提到的随机数,然后使用这串随机数生成自己的对称主密钥
  • 客户端发送一个 Finished 消息给服务器端,使用对称密钥加密这次通讯的一个散列值
  • 服务器端生成自己的 hash 值,然后解密客户端发送来的信息,检查这两个值是否对应。如果对应,就向客户端发送一个 Finished 消息,也使用协商好的对称密钥加密
  • 从现在开始,接下来整个 TLS 会话都使用对称秘钥进行加密,传输应用层(HTTP)内容

TCP 数据包

HTTP 协议···

如果浏览器是 Google 出品的,它不会使用 HTTP 协议来获取页面信息,而是会与服务器端发送请求,商讨使用 SPDY 协议。

如果浏览器使用 HTTP 协议,它会向服务器发送这样的一个请求:

GET / HTTP/1.1
Host: google.com
[其他头部]

“其他头部”包含了一系列的由冒号分割开的键值对,它们的格式符合HTTP协议标准,它们之间由一个换行符分割开来。这里我们假设浏览器没有违反HTTP协议标准的bug,同时浏览器使用 HTTP/1.1 协议,不然的话头部可能不包含 Host 字段,同时 GET 请求中的版本号会变成 HTTP/1.0 或者 HTTP/0.9

HTTP/1.1 定义了“关闭连接”的选项 “close”,发送者使用这个选项指示这次连接在响应结束之后会断开:

Connection:close

不支持持久连接的 HTTP/1.1 必须在每条消息中都包含 “close” 选项。

在发送完这些请求和头部之后,浏览器发送一个换行符,表示要发送的内容已经结束了。

服务器端返回一个响应码,指示这次请求的状态,响应的形式是这样的:

200 OK
[响应头部]

然后是一个换行,接下来有效载荷(payload),也就是 www.google.com 的HTML内容。服务器下面可能会关闭连接,如果客户端请求保持连接的话,服务器端会保持连接打开,以供以后的请求重用。

如果浏览器发送的HTTP头部包含了足够多的信息(例如包含了 Etag 头部,以至于服务器可以判断出,浏览器缓存的文件版本自从上次获取之后没有再更改过,服务器可能会返回这样的响应:

304 Not Modified
[响应头部]

这个响应没有有效载荷,浏览器会从自己的缓存中取出想要的内容。

在解析完 HTML 之后,浏览器和客户端会重复上面的过程,直到HTML页面引入的所有资源(图片,CSS,favicon.ico等等)全部都获取完毕,区别只是头部的 GET / HTTP/1.1 会变成 GET /$(相对www.google.com的URL) HTTP/1.1

如果HTML引入了 www.google.com 域名之外的资源,浏览器会回到上面解析域名那一步,按照下面的步骤往下一步一步执行,请求中的 Host 头部会变成另外的域名。

HTTP 服务器请求处理

HTTPD(HTTP Daemon)在服务器端处理请求/响应。最常见的 HTTPD 有 Linux 上常用的 Apache 和 nginx,以及 Windows 上的 IIS。

  • HTTPD 接收请求
  • 服务器把请求拆分为以下几个参数:
    • HTTP 请求方法(GET, POST, HEAD, PUT, DELETE, CONNECT, OPTIONS, 或者 TRACE)。直接在地址栏中输入 URL 这种情况下,使用的是 GET 方法
    • 域名:google.com
    • 请求路径/页面:/ (我们没有请求google.com下的指定的页面,因此 / 是默认的路径)
  • 服务器验证其上已经配置了 google.com 的虚拟主机
  • 服务器验证 google.com 接受 GET 方法
  • 服务器验证该用户可以使用 GET 方法(根据 IP 地址,身份信息等)
  • 如果服务器安装了 URL 重写模块(例如 Apache 的 mod_rewrite 和 IIS 的 URL Rewrite),服务器会尝试匹配重写规则,如果匹配上的话,服务器会按照规则重写这个请求
  • 服务器根据请求信息获取相应的响应内容,这种情况下由于访问路径是 “/” ,会访问首页文件(你可以重写这个规则,但是这个是最常用的)。
  • 服务器会使用指定的处理程序分析处理这个文件,假如 Google 使用 PHP,服务器会使用 PHP 解析 index 文件,并捕获输出,把 PHP 的输出结果返回给请求者

浏览器背后的故事

当服务器提供了资源之后(HTML,CSS,JS,图片等),浏览器会执行下面的操作:

  • 解析 —— HTML,CSS,JS
  • 渲染 —— 构建 DOM 树 -> 渲染 -> 布局 -> 绘制

浏览器

浏览器的功能是从服务器上取回你想要的资源,然后展示在浏览器窗口当中。资源通常是 HTML 文件,也可能是 PDF,图片,或者其他类型的内容。资源的位置通过用户提供的 URI(Uniform Resource Identifier) 来确定。

浏览器解释和展示 HTML 文件的方法,在 HTML 和 CSS 的标准中有详细介绍。这些标准由 Web 标准组织 W3C(World Wide Web Consortium) 维护。

不同浏览器的用户界面大都十分接近,有很多共同的 UI 元素:

  • 一个地址栏
  • 后退和前进按钮
  • 书签选项
  • 刷新和停止按钮
  • 主页按钮

浏览器高层架构

组成浏览器的组件有:

  • 用户界面 用户界面包含了地址栏,前进后退按钮,书签菜单等等,除了请求页面之外所有你看到的内容都是用户界面的一部分
  • 浏览器引擎 浏览器引擎负责让 UI 和渲染引擎协调工作
  • 渲染引擎 渲染引擎负责展示请求内容。如果请求的内容是 HTML,渲染引擎会解析 HTML 和 CSS,然后将内容展示在屏幕上
  • 网络组件 网络组件负责网络调用,例如 HTTP 请求等,使用一个平台无关接口,下层是针对不同平台的具体实现
  • UI后端 UI 后端用于绘制基本 UI 组件,例如下拉列表框和窗口。UI 后端暴露一个统一的平台无关的接口,下层使用操作系统的 UI 方法实现
  • Javascript 引擎 Javascript 引擎用于解析和执行 Javascript 代码
  • 数据存储 数据存储组件是一个持久层。浏览器可能需要在本地存储各种各样的数据,例如 Cookie 等。浏览器也需要支持诸如 localStorage,IndexedDB,WebSQL 和 FileSystem 之类的存储机制

HTML 解析

浏览器渲染引擎从网络层取得请求的文档,一般情况下文档会分成8kB大小的分块传输。

HTML 解析器的主要工作是对 HTML 文档进行解析,生成解析树。

解析树是以 DOM 元素以及属性为节点的树。DOM是文档对象模型(Document Object Model)的缩写,它是 HTML 文档的对象表示,同时也是 HTML 元素面向外部(如Javascript)的接口。树的根部是”Document”对象。整个 DOM 和 HTML 文档几乎是一对一的关系。

解析算法

HTML不能使用常见的自顶向下或自底向上方法来进行分析。主要原因有以下几点:

  • 语言本身的“宽容”特性
  • HTML 本身可能是残缺的,对于常见的残缺,浏览器需要有传统的容错机制来支持它们
  • 解析过程需要反复。对于其他语言来说,源码不会在解析过程中发生变化,但是对于 HTML 来说,动态代码,例如脚本元素中包含的 document.write() 方法会在源码中添加内容,也就是说,解析过程实际上会改变输入的内容

由于不能使用常用的解析技术,浏览器创造了专门用于解析 HTML 的解析器。解析算法在 HTML5 标准规范中有详细介绍,算法主要包含了两个阶段:标记化(tokenization)和树的构建。

解析结束之后

浏览器开始加载网页的外部资源(CSS,图像,Javascript 文件等)。

此时浏览器把文档标记为“可交互的”,浏览器开始解析处于“推迟”模式的脚本,也就是那些需要在文档解析完毕之后再执行的脚本。之后文档的状态会变为“完成”,浏览器会进行“加载”事件。

注意解析 HTML 网页时永远不会出现“语法错误”,浏览器会修复所有错误,然后继续解析。

执行同步 Javascript 代码。

CSS 解析

  • 根据 CSS词法和句法 分析CSS文件和