Fork me on GitHub

Bash 变量

Bash 变量分成环境变量和自定义变量两类。

环境变量

环境变量是 Bash 环境自带的变量,进入 Shell 时已经定义好了,可以直接使用。它们通常是系统定义好的,也可以由用户从父 Shell 传入子 Shell。

env命令或printenv命令,可以显示所有环境变量。

1
2
3
$ env
# 或者
$ printenv

常见的环境变量:

  • BASHPID:Bash 进程的进程 ID。
  • BASHOPTS:当前 Shell 的参数,可以用shopt命令修改。
  • DISPLAY:图形环境的显示器名字,通常是:0,表示 X Server 的第一个显示器。
  • EDITOR:默认的文本编辑器。
  • HOME:用户的主目录。
  • HOST:当前主机的名称。
  • IFS:词与词之间的分隔符,默认为空格。
  • LANG:字符集以及语言编码,比如zh_CN.UTF-8
  • PATH:由冒号分开的目录列表,当输入可执行程序名后,会搜索这个目录列表。
  • PS1:Shell 提示符。
  • PS2: 输入多行命令时,次要的 Shell 提示符。
  • PWD:当前工作目录。
  • RANDOM:返回一个 0 到 32767 之间的随机数。
  • SHELL:Shell 的名字。
  • SHELLOPTS:启动当前 Shell 的set命令的参数。
  • TERM:终端类型名,即终端仿真器所用的协议。
  • UID:当前用户的 ID 编号。
  • USER:当前用户的用户名。

很多环境变量很少发生变化,而且是只读的,可以视为常量。由于它们的变量名全部都是大写,所以传统上,如果用户要自己定义一个常量,也会使用全部大写的变量名。

注意,Bash 变量名区分大小写,HOMEhome是两个不同的变量。

查看单个环境变量的值,可以使用printenv命令或echo命令。

1
2
3
$ printenv PATH
# 或者
$ echo $PATH

注意,printenv命令后面的变量名,不用加前缀$

自定义变量

自定义变量是用户在当前 Shell 里面自己定义的变量,仅在当前 Shell 可用。一旦退出当前 Shell,该变量就不存在了。

set命令可以显示所有变量(包括环境变量和自定义变量),以及所有的 Bash 函数。

创建变量

用户创建变量的时候,变量名必须遵守下面的规则:

  • 字母、数字和下划线字符组成。
  • 第一个字符必须是一个字母或一个下划线,不能是数字。
  • 不能使用 Shell 里的关键字。
1
2
3
variable=value
variable='value'
variable="value"

上面命令中,等号左边是变量名,右边是变量。

注意,等号两边不能有空格。

如果变量的值包含空格,则必须将值放在引号中。

1
myvar="hello world"

Bash 没有数据类型的概念,所有的变量值都是字符串。

下面是一些自定义变量的例子。

1
2
3
4
5
6
a=z                     # 变量 a 赋值为字符串 z
b="a string" # 变量值包含空格,就必须放在引号里面
c="a string and $b" # 变量值可以引用其他变量的值
d="\t\ta string\n" # 变量值可以使用转义字符
e=$(ls -l foo.txt) # 变量值可以是命令的执行结果
f=$((5 * 7)) # 变量值可以是数学运算的结果

变量可以重复赋值,后面的赋值会覆盖前面的赋值。

1
2
3
4
$ foo=1
$ foo=2
$ echo $foo
2

如果同一行定义多个变量,必须使用分号;分隔。

1
$ foo=1;bar=2

读取变量

读取变量的时候,直接在变量名前加上$就可以了。

1
2
3
$ foo=bar
$ echo $foo
bar

每当 Shell 看到以$开头的单词时,就会尝试读取这个变量名对应的值。

如果变量不存在,Bash 不会报错,而会输出空字符。

由于$在 Bash 中有特殊含义,把它当作美元符号使用时,一定要非常小心,

1
2
$ echo The total is $100.00
The total is 00.00

上面命令的原意是输入$100,但是 Bash 将$1解释成了变量,该变量为空,因此输入就变成了 00.00。所以,如果要使用$的原义,需要在$前面放上反斜杠,进行转义。

1
2
$ echo The total is \$100.00
The total is $100.00

读取变量的时候,变量名也可以使用花括号{}包围,比如$a也可以写成${a}。这种写法可以用于变量名与其他字符连用的情况。

1
2
3
4
5
$ a=foo
$ echo $a_file

$ echo ${a}_file
foo_file

上面代码中,变量名a_file不会有任何输出,因为 Bash 将其整个解释为变量,而这个变量是不存在的。只有用花括号区分$a,Bash 才能正确解读。

事实上,读取变量的语法$foo,可以看作是${foo}的简写形式。

如果变量的值本身也是变量,可以使用${!varname}的语法,读取最终的值。

1
2
3
$ myvar=USER
$ echo ${!myvar}
ruanyf

上面的例子中,变量myvar的值是USER${!myvar}的写法将其展开成最终的值。

如果变量值包含连续空格(或制表符和换行符),最好放在双引号里面读取。

1
2
3
4
5
$ a="1 2  3"
$ echo $a
1 2 3
$ echo "$a"
1 2 3

上面示例中,变量a的值包含两个连续空格。如果直接读取,Shell 会将连续空格合并成一个。只有放在双引号里面读取,才能保持原来的格式。

删除变量

unset命令用来删除一个变量。

1
unset NAME

这个命令不是很有用。因为不存在的 Bash 变量一律等于空字符串,所以即使unset命令删除了变量,还是可以读取这个变量,值为空字符串。

所以,删除一个变量,也可以将这个变量设成空字符串。

1
2
$ foo=''
$ foo=

上面两种写法,都是删除了变量foo。由于不存在的值默认为空字符串,所以后一种写法可以在等号右边不写任何值。

输出变量,export 命令

用户创建的变量仅可用于当前 Shell,子 Shell 默认读取不到父 Shell 定义的变量。为了把变量传递给子 Shell,需要使用export命令。这样输出的变量,对于子 Shell 来说就是环境变量。

export命令用来向子 Shell 输出变量。

1
2
NAME=foo
export NAME

上面命令输出了变量NAME。变量的赋值和输出也可以在一个步骤中完成。

1
export NAME=value

上面命令执行后,当前 Shell 及随后新建的子 Shell,都可以读取变量$NAME

子 Shell 如果修改继承的变量,不会影响父 Shell。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 输出变量 $foo
$ export foo=bar

# 新建子 Shell
$ bash

# 读取 $foo
$ echo $foo
bar

# 修改继承的变量
$ foo=baz

# 退出子 Shell
$ exit

# 读取 $foo
$ echo $foo
bar

上面例子中,子 Shell 修改了继承的变量$foo,对父 Shell 没有影响。

特殊变量

Bash 提供一些特殊变量。这些变量的值由 Shell 提供,用户不能进行赋值。

$?

$?为上一个命令的退出码,用来判断上一个命令是否执行成功。返回值是 0,表示上一个命令执行成功;如果不是零,表示上一个命令执行失败。

1
2
3
4
5
$ ls doesnotexist
ls: doesnotexist: No such file or directory

$ echo $?
1

上面例子中,ls命令查看一个不存在的文件,导致报错。$?为 1,表示上一个命令执行失败。

$$

$$为当前 Shell 的进程 ID。

1
2
$ echo $$
10662

这个特殊变量可以用来命名临时文件。

1
LOGFILE=/tmp/output_log.$$

$_

$_为上一个命令的最后一个参数。

1
2
3
4
5
$ grep dictionary /usr/share/dict/words
dictionary

$ echo $_
/usr/share/dict/words

$!

$!为最近一个后台执行的异步命令的进程 ID。

1
2
3
4
5
$ firefox &
[1] 11064

$ echo $!
11064

上面例子中,firefox 是后台运行的命令,$!返回该命令的进程 ID。

$0

$0为当前 Shell 的名称(在命令行直接执行时)或者脚本名(在脚本中执行时)。

1
2
$ echo $0
bash

上面例子中,$0返回当前运行的是 Bash。

$-

$-为当前 Shell 的启动参数。

1
2
$ echo $-
himBHs

$@和$#

$#表示脚本的参数数量,$@表示脚本的参数值。

变量的默认值

Bash 提供四个特殊语法,跟变量的默认值有关,目的是保证变量不为空。

1
${varname:-word}

上面语法的含义是,如果变量varname存在且不为空,则返回它的值,否则返回word。它的目的是返回一个默认值,比如${count:-0}表示变量count不存在时返回 0。

1
${varname:=word}

上面语法的含义是,如果变量varname存在且不为空,则返回它的值,否则将它设为word,并且返回word。它的目的是设置变量的默认值,比如${count:=0}表示变量count不存在时返回 0,且将count设为0。

1
${varname:+word}

上面语法的含义是,如果变量名存在且不为空,则返回word,否则返回空值。它的目的是测试变量是否存在,比如${count:+1}表示变量count存在时返回 1(表示true),否则返回空值。

1
${varname:?message}

上面语法的含义是,如果变量varname存在且不为空,则返回它的值,否则打印出varname: message,并中断脚本的执行。如果省略了message,则输出默认的信息“parameter null or not set.”。它的目的是防止变量未定义,比如${count:?"undefined!"}表示变量count未定义时就中断执行,抛出错误,返回给定的报错信息undefined!

上面四种语法如果用在脚本中,变量名的部分可以用数字 1 到 9,表示脚本的参数。

1
filename=${1:?"filename missing."}

上面代码出现在脚本中,1 表示脚本的第一个参数。如果该参数不存在,就退出脚本并报错。

declare 命令

declare命令可以声明一些特殊类型的变量,为变量设置一些限制,比如声明只读类型的变量和整数类型的变量。

它的语法形式如下。

1
declare OPTION VARIABLE=value

declare命令的主要参数(OPTION)如下。

  • -a:声明数组变量。
  • -f:输出所有函数定义。
  • -F:输出所有函数名。
  • -i:声明整数变量。
  • -l:声明变量为小写字母。
  • -p:查看变量信息。
  • -r:声明只读变量。
  • -u:声明变量为大写字母。
  • -x:该变量输出为环境变量。

declare命令如果用在函数中,声明的变量只在函数内部有效,等同于local命令。

不带任何参数时,declare命令输出当前环境的所有变量,包括函数在内,等同于不带有任何参数的set命令。

1
$ declare

-i 参数

-i参数声明整数变量以后,可以直接进行数学运算。

1
2
3
4
5
$ declare -i val1=12 val2=5
$ declare -i result
$ result=val1*val2
$ echo $result
60

上面例子中,如果变量result不声明为整数,val1*val2会被当作字面量,不会进行整数运算。另外,val1val2其实不需要声明为整数,因为只要result声明为整数,它的赋值就会自动解释为整数运算。

注意,一个变量声明为整数以后,依然可以被改写为字符串。

1
2
3
4
$ declare -i var=12
$ var=foo
$ echo $var
0

上面例子中,变量var声明为整数,覆盖以后,Bash 不会报错,但会赋以不确定的值,上面的例子中可能输出 0,也可能输出的是 3。

-x 参数

-x参数等同于export命令,可以输出一个变量为子 Shell 的环境变量。

1
2
3
$ declare -x foo
# 等同于
$ export foo

-r 参数

-r参数可以声明只读变量,无法改变变量值,也不能unset变量。

1
2
3
4
5
6
7
8
9
10
11
$ declare -r bar=1

$ bar=2
bash: bar:只读变量
$ echo $?
1

$ unset bar
bash: bar:只读变量
$ echo $?
1

上面例子中,后两个赋值语句都会报错,命令执行失败。

-u 参数

-u参数声明变量为大写字母,可以自动把变量值转成大写字母。

1
2
3
4
$ declare -u foo
$ foo=upper
$ echo $foo
UPPER

-l 参数

-l参数声明变量为小写字母,可以自动把变量值转成小写字母。

1
2
3
4
$ declare -l bar
$ bar=LOWER
$ echo $bar
lower

-p 参数

-p参数输出变量信息。

1
2
3
4
5
$ foo=hello
$ declare -p foo
declare -- foo="hello"
$ declare -p bar
bar:未找到

上面例子中,declare -p可以输出已定义变量的值,对于未定义的变量,会提示找不到。

如果不提供变量名,declare -p输出所有变量的信息。

1
$ declare -p

-f 参数

-f参数输出当前环境的所有函数,包括它的定义。

1
$ declare -f

-F参数

-F参数输出当前环境的所有函数名,不包含函数定义。

1
$ declare -F

readonly 命令

readonly命令等同于declare -r,用来声明只读变量,不能改变变量值,也不能unset变量。

1
2
3
4
5
$ readonly foo=1
$ foo=2
bash: foo:只读变量
$ echo $?
1

上面例子中,更改只读变量foo会报错,命令执行失败。

readonly命令有三个参数:

  • -f:声明的变量为函数名。
  • -p:打印出所有的只读变量。
  • -a:声明的变量为数组。

let 命令

let命令声明变量时,可以直接执行算术表达式。

1
2
3
$ let foo=1+2
$ echo $foo
3

上面例子中,let命令可以直接计算1 + 2

let命令的参数表达式如果包含空格,就需要使用引号。

1
$ let "foo = 1 + 2"

let可以同时对多个变量赋值,赋值表达式之间使用空格分隔。

1
2
3
$ let "v1 = 1" "v2 = v1++"
$ echo $v1,$v2
2,1

上面例子中,let声明了两个变量v1v2,其中v2等于v1++,表示先返回v1的值,然后v1自增。

Bash 基本语法

Bash 的基本语法

echo 命令

echo命令的作用是在屏幕输出一行文本,可以将该命令的参数原样输出。

1
2
$ echo hello world
hello world

上面例子中,echo的参数是hello world,可以原样输出。

如果想要输出的是多行文本,即包括换行符。这时就需要把多行文本放在引号里面。

1
2
3
4
5
6
7
8
$ echo "<HTML>
<HEAD>
<TITLE>Page Title</TITLE>
</HEAD>
<BODY>
Page body.
</BODY>
</HTML>"

-n 参数

默认情况下,echo输出的文本末尾会有一个回车符。-n参数可以取消末尾的回车符,使得下一个提示符紧跟在输出内容的后面。

1
2
$ echo -n hello world
hello world$

上面例子中,world后面直接就是下一行的提示符$

1
2
3
4
5
6
$ echo a;echo b
a
b

$ echo -n a;echo b
ab

上面例子中,-n参数可以让两个echo命令的输出连在一起,出现在同一行。

-e 参数

-e参数会解释引号(双引号和单引号)里面的特殊字符(比如换行符\n)。如果不使用-e参数,即默认情况下,引号会让特殊字符变成普通字符,echo不解释它们,原样输出。

1
2
3
4
5
6
7
8
9
10
11
12
$ echo "Hello\nWorld"
Hello\nWorld

# 双引号的情况
$ echo -e "Hello\nWorld"
Hello
World

# 单引号的情况
$ echo -e 'Hello\nWorld'
Hello
World

上面代码中,-e参数使得\n解释为换行符,导致输出内容里面出现换行。

命令格式

命令行环境中,主要通过使用 Shell 命令,进行各种操作。Shell 命令基本都是下面的格式。

1
$ command [ arg1 ... [ argN ]]

上面代码中,command是具体的命令或者一个可执行文件,arg1 ... argN是传递给命令的参数,它们是可选的。

1
$ ls -l

上面这个命令中,ls是命令,-l是参数。

有些参数是命令的配置项,这些配置项一般都以一个连词线开头,比如上面的-l。同一个配置项往往有长和短两种形式,比如-l是短形式,--list是长形式,它们的作用完全相同。短形式便于手动输入,长形式一般用在脚本之中,可读性更好,利于解释自身的含义。

1
2
3
4
5
# 短形式
$ ls -r

# 长形式
$ ls --reverse

Bash 单个命令一般都是一行,用户按下回车键,就开始执行。有些命令比较长,写成多行会有利于阅读和编辑,这时可以在每一行的结尾加上反斜杠,Bash 就会将下一行跟当前行放在一起解释。

1
2
3
4
5
$ echo foo bar

# 等同于
$ echo foo \
bar

空格

Bash 使用空格(或 Tab 键)区分不同的参数。

1
$ command foo bar

上面命令中,foobar之间有一个空格,所以 Bash 认为它们是两个参数。

如果参数之间有多个空格,Bash 会自动忽略多余的空格。

1
2
$ echo this is a     test
this is a test

上面命令中,atest之间有多个空格,Bash 会忽略多余的空格。

分号

分号(;)是命令的结束符,使得一行可以放置多个命令,上一个命令执行结束后,再执行第二个命令。

1
$ clear; ls

上面例子中,Bash 先执行clear命令,执行完成后,再执行ls命令。

注意,使用分号时,第二个命令总是接着第一个命令执行,不管第一个命令执行成功或失败。

命令的组合符&&和||

除了分号,Bash 还提供两个命令组合符&&||,允许更好地控制多个命令之间的继发关系。

1
Command1 && Command2

上面命令的意思是,如果Command1命令运行成功,则继续运行Command2命令。

1
Command1 || Command2

上面命令的意思是,如果Command1命令运行失败,则继续运行Command2命令。

1
$ cat filelist.txt ; ls -l filelist.txt

上面例子中,只要cat命令执行结束,不管成功或失败,都会继续执行ls命令。

1
$ cat filelist.txt && ls -l filelist.txt

上面例子中,只有cat命令执行成功,才会继续执行ls命令。如果cat执行失败(比如不存在文件flielist.txt),那么ls命令就不会执行。

1
$ mkdir foo || mkdir bar

上面例子中,只有mkdir foo命令执行失败(比如foo目录已经存在),才会继续执行mkdir bar命令。如果mkdir foo命令执行成功,就不会创建bar目录了。

type 命令

Bash 本身内置了很多命令,同时也可以执行外部程序。怎么知道一个命令是内置命令,还是外部程序呢?

type命令用来判断命令的来源。

1
2
3
4
$ type echo
echo is a shell builtin
$ type ls
ls is hashed (/bin/ls)

上面代码中,type命令告诉我们,echo是内部命令,ls是外部程序(/bin/ls)。

type命令本身也是内置命令。

1
2
$ type type
type is a shell builtin

如果要查看一个命令的所有定义,可以使用type命令的-a参数。

1
2
3
4
$ type -a echo
echo is shell builtin
echo is /usr/bin/echo
echo is /bin/echo

上面代码表示,echo命令既是内置命令,也有对应的外部程序。

type命令的-t参数,可以返回一个命令的类型:别名(alias),关键词(keyword),函数(function),内置命令(builtin)和文件(file)。

1
2
3
4
$ type -t bash
file
$ type -t if
keyword

上面例子中,bash是文件,if是关键词。

快捷键

Bash 提供很多快捷键,可以大大方便操作。下面是一些最常用的快捷键。

  • Ctrl + L:清除屏幕并将当前行移到页面顶部。
  • Ctrl + C:中止当前正在执行的命令。
  • Shift + PageUp:向上滚动。
  • Shift + PageDown:向下滚动。
  • Ctrl + U:从光标位置删除到行首。
  • Ctrl + K:从光标位置删除到行尾。
  • Ctrl + W:删除光标位置前一个单词。
  • Ctrl + D:关闭 Shell 会话。
  • ↑,↓:浏览已执行命令的历史记录。

除了上面的快捷键,Bash 还具有自动补全功能。命令输入到一半的时候,可以按下 Tab 键,Bash 会自动完成剩下的部分。比如,输入tou,然后按一下 Tab 键,Bash 会自动补上ch

除了命令的自动补全,Bash 还支持路径的自动补全。有时,需要输入很长的路径,这时只需要输入前面的部分,然后按下 Tab 键,就会自动补全后面的部分。如果有多个可能的选择,按两次 Tab 键,Bash 会显示所有选项,让你选择。

引号和转义

Bash 只有一种数据类型,就是字符串。不管用户输入什么数据,Bash 都视为字符串。因此,字符串相关的引号和转义,对 Bash 来说就非常重要。

转义

某些字符在 Bash 里面有特殊含义(比如$、&、*)。

1
2
3
$ echo $date

$

上面例子中,输出$date不会有任何结果,因为$是一个特殊字符。

如果想要原样输出这些特殊字符,就必须在它们前面加上反斜杠,使其变成普通字符。这就叫做“转义”。

1
2
$ echo \$date
$date

反斜杠本身也是特殊字符,如果想要原样输出反斜杠,就需要对它自身转义,连续使用两个反斜线(\\)。

1
2
$ echo \\
\

反斜杠除了用于转义,还可以表示一些不可打印的字符。

  • \a:响铃
  • \b:退格
  • \n:换行
  • \r:回车
  • \t:制表符

如果想要在命令行使用这些不可打印的字符,可以把它们放在引号里面,然后使用echo命令的-e参数。

1
2
3
4
5
$ echo a\tb
atb

$ echo -e "a\tb"
a b

上面例子中,命令行直接输出不可打印字符\t,Bash 不能正确解释。必须把它们放在引号之中,然后使用echo命令的-e参数。

换行符是一个特殊字符,表示命令的结束,Bash 收到这个字符以后,就会对输入的命令进行解释执行。换行符前面加上反斜杠转义,就使得换行符变成一个普通字符,Bash 会将其当作长度为0的空字符处理,从而可以将一行命令写成多行。

1
2
3
4
5
6
$ mv \
/path/to/foo \
/path/to/bar

# 等同于
$ mv /path/to/foo /path/to/bar

上面例子中,如果一条命令过长,就可以在行尾使用反斜杠,将其改写成多行。这是常见的多行命令的写法。

单引号

Bash 允许字符串放在单引号或双引号之中,加以引用。

单引号用于保留字符的字面含义,各种特殊字符在单引号里面,都会变为普通字符,比如星号(*)、美元符号($)、反斜杠(\)等。

1
2
3
4
5
6
7
8
9
10
11
$ echo '*'
*

$ echo '$USER'
$USER

$ echo '$((2+2))'
$((2+2))

$ echo '$(echo foo)'
$(echo foo)

上面命令中,单引号使得 Bash 扩展、变量引用、算术运算和子命令,都失效了。如果不使用单引号,它们都会被 Bash 自动扩展。

由于反斜杠在单引号里面变成了普通字符,所以如果单引号之中,还要使用单引号,不能使用转义,需要在外层的单引号前面加上一个美元符号($),然后再对里层的单引号转义。

1
2
3
4
5
6
7
8
# 不正确
$ echo it's

# 不正确
$ echo 'it\'s'

# 正确
$ echo $'it\'s'

不过,更合理的方法是改在双引号之中使用单引号。

1
2
$ echo "it's"
it's

双引号

双引号比单引号宽松,大部分特殊字符在双引号里面,都会失去特殊含义,变成普通字符。

1
2
$ echo "*"
*

上面例子中,通配符*是一个特殊字符,放在双引号之中,就变成了普通字符,会原样输出。这一点需要特别留意,这意味着,双引号里面不会进行文件名扩展。

但是,三个特殊字符除外:美元符号($)、反引号(\``)和反斜杠(`)。这三个字符在双引号之中,依然有特殊含义,会被 Bash 自动扩展。

1
2
3
4
5
$ echo "$SHELL"
/bin/bash

$ echo "`date`"
Mon Jan 27 13:33:18 CST 2020

上面例子中,美元符号($)和反引号(`)在双引号中,都保持特殊含义。美元符号用来引用变量,反引号则是执行子命令。

1
2
3
4
5
$ echo "I'd say: \"hello!\""
I'd say: "hello!"

$ echo "\\"
\

上面例子中,反斜杠在双引号之中保持特殊含义,用来转义。所以,可以使用反斜杠,在双引号之中插入双引号,或者插入反斜杠本身。

换行符在双引号之中,会失去特殊含义,Bash 不再将其解释为命令的结束,只是作为普通的换行符。所以可以利用双引号,在命令行输入多行文本。

1
2
3
4
$ echo "hello
world"
hello
world

上面命令中,Bash 正常情况下会将换行符解释为命令结束,但是换行符在双引号之中就失去了这种特殊作用,只用来换行,所以可以输入多行。echo命令会将换行符原样输出,显示的时候正常解释为换行。

双引号的另一个常见的使用场合是,文件名包含空格。这时就必须使用双引号(或单引号),将文件名放在里面。

1
$ ls "two words.txt"

上面命令中,two words.txt是一个包含空格的文件名,如果不放在双引号里面,就会被 Bash 当作两个文件。

双引号会原样保存多余的空格。

1
2
$ echo "this is a     test"
this is a test

双引号还有一个作用,就是保存原始命令的输出格式。

1
2
3
4
5
6
7
8
9
10
11
12
13
# 单行输出
$ echo $(cal)
一月 2020 日 一 二 三 四 五 六 1 2 3 ... 31

# 原始格式输出
$ echo "$(cal)"
一月 2020
日 一 二 三 四 五 六
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31

上面例子中,如果$(cal)不放在双引号之中,echo就会将所有结果以单行输出,丢弃了所有原始的格式。

Here 文档

Here 文档是一种输入多行字符串的方法,格式如下。

1
2
3
<< token
text
token

它的格式分成开始标记(<< token)和结束标记(token)。开始标记是两个小于号+ Here文档的名称,名称可以随意取,后面必须是一个换行符;结束标记是单独一行顶格写的 Here 文档名称,如果不是顶格,结束标记不起作用。两者之间就是多行字符串的内容。

下面是一个通过 Here 文档输出 HTML 代码的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ cat << _EOF_
<html>
<head>
<title>
The title of your page
</title>
</head>

<body>
Your page content goes here.
</body>
</html>
_EOF_

Here 文档内部会发生变量替换,同时支持反斜杠转义,但是不支持通配符扩展,双引号和单引号也失去语法作用,变成了普通字符。

1
2
3
4
5
6
7
8
9
10
$ foo='hello world'
$ cat << _example_
$foo
"$foo"
'$foo'
_example_

hello world
"hello world"
'hello world'

上面例子中,变量$foo发生了替换,但是双引号和单引号都原样输出了,表明它们已经失去了引用的功能。

如果不希望发生变量替换,可以把 Here 文档的开始标记放在单引号之中。

1
2
3
4
5
6
7
8
9
10
$ foo='hello world'
$ cat << '_example_'
$foo
"$foo"
'$foo'
_example_

$foo
"$foo"
'$foo'

上面例子中,Here 文档的开始标记(_example_)放在单引号之中,导致变量替换失效了。

Here 文档的本质是重定向,它将字符串重定向输出给某个命令,相当于包含了echo命令。

1
2
3
4
5
6
7
$ command << token
string
token

# 等同于

$ echo string | command

上面代码中,Here 文档相当于echo命令的重定向。

所以,Here 字符串只适合那些可以接受标准输入作为参数的命令,对于其他命令无效,比如echo命令就不能用 Here 文档作为参数。

1
2
3
$ echo << _example_
hello
_example_

上面例子不会有任何输出,因为 Here 文档对于echo命令无效。

此外,Here 文档也不能作为变量的值,只能用于命令的参数。

Here 字符串

Here 文档还有一个变体,叫做 Here 字符串,使用三个小于号(<<<)表示。

1
<<< string

它的作用是将字符串通过标准输入,传递给命令。

有些命令直接接受给定的参数,与通过标准输入接受参数,结果是不一样的。所以才有了这个语法,使得将字符串通过标准输入传递给命令更方便,比如cat命令只接受标准输入传入的字符串。

1
2
3
$ cat <<< 'hi there'
# 等同于
$ echo 'hi there' | cat

上面的第一种语法使用了 Here 字符串,要比第二种语法看上去语义更好,也更简洁。

1
2
3
$ md5sum <<< 'ddd'
# 等同于
$ echo 'ddd' | md5sum

上面例子中,md5sum命令只能接受标准输入作为参数,不能直接将字符串放在命令后面,会被当作文件名,即md5sum ddd里面的ddd会被解释成文件名。这时就可以用 Here 字符串,将字符串传给md5sum命令。

OSPF 基础命令

掌握三条命令,就能玩转 OSPF:

  • 创建 OSPF 进程,进入配置视图
  • 创建 OSPF 区域
  • 接口激活 OSPF

创建 OSPF 进程

在系统视图下,使用 OSPF 命令,创建 OSPF 进程。

1
[Router]ospf 1 Router-id 1.1.1.1

上面的命令,是在Router上创建Process-ID为 1 的 OSPF 进程,并进入配置视图。使用的Router-ID1.1.1.1

Process-ID是可选参数,表示 OSPF 进程的编号,只在设备本身生效,也就是说,两台设备建立 OSPF 邻居时,不要求双方的Process-ID一样。如果不指定Process-ID,会分配一个默认值作为Process-ID

router-id也是可选参数,用来指定设备的Router-ID。通常,手动配置Router-ID,不会使用默认值。

创建 OSPF 区域

创建完 OSPF 进程后,就需要创建 OSPF 区域。在配置视图下,使用area命令创建区域,并指定区域 ID。

1
2
[Router]ospf 1 Router-id 1.1.1.1
[Router-ospf-1]area 1

在 OSPF 进程 1 中,创建Area1

接口激活 OSPF

默认状态下,所有接口都没有激活 OSPF,要在接口激活 OSPF,有两种方法:

在区域视图激活 OSPF

在区域视图下,使用network命令,再加上 IP 地址和通配符掩码,满足条件的接口激活 OSPF。

1
2
3
[Router]ospf 1 Router-id 1.1.1.1
[Router-ospf-1]area 0
[Router-ospf-1-area-0.0.0.0]network 192.168.1.0 0.0.0.255

举个栗子:network 192.168.1.0 0.0.0.255,IP 地址是192.168.1.0,通配符掩码是0.0.0.255。通配符掩码中,比特位为 0 的需要匹配,比特位为 1 的不需要匹配。命令中匹配的 IP 地址是192.168.1.0192.168.1.255

计算方法是,把192.168.1.0用二进制表示,把通配符掩码0.0.0.255也换算成二进制,每个比特位对应。前 24 位全为 0,后 8 位全为 1。匹配的 IP 地址,以192.168.1开头,后面是 0 至 255 的任意值。接口 IP 地址在这个范围内,且 IP 地址掩码长度大于或等于network命令的 0 比特位数,就在接口上激活 OSPF。

有两个特殊的network命令,一个是network x.x.x.x 0.0.0.0,比如network 192.168.1.1 0.0.0.0,IP 地址是192.168.1.1的接口激活 OSPF,无论网络掩码长度多少。另一个是network 0.0.0.0 255.255.255.255,匹配任意 IP 地址,所有配置了 IP 地址的接口,都会激活 OSPF。

在指定接口激活 OSPF

上面的方法,可以使用一条命令,在多个接口激活 OSPF。另一个方法就是在指定接口激活 OSPF。先创建 OSPF 进程和区域,然后进入接口视图,使用ospf enable命令激活。

1
2
3
4
5
6
[Router]ospf 1 Router-id 1.1.1.1 #创建OSPF进程
[Router-ospf-1]area 0 #创建Area0
[Router-ospf-1-area-0.0.0.0]quit
[Router-ospf-1]quit
[Router]interface GigabitEthernet 0/0 #进入GE0/0接口视图
[Router-GigabitEthernet0/0]ospf enable 1 area 0 #激活OSPF

OSPF 单区域实验

路由器 RT1 的两个接口分别连接172.16.1.0/24172.16.2.0/24网段,另一个接口连接路由器 RT2。RT2 创建 Loopback 接口,配置 IP 地址172.16.255.2/24,模拟 RT2 的直连网段。在 RT1 和 RT2 上运行 OSPF,让 PC 可以访问全部网段。

RT1 配置如下:

1
2
3
4
5
[RT1]ospf 1 router-id 1.1.1.1
[RT1-ospf-1]area 0
[RT1-ospf-1-area-0.0.0.0]network 172.16.1.0 0.0.0.255
[RT1-ospf-1-area-0.0.0.0]network172.16.2.0 0.0.0.255
[RT1-ospf-1-area-0.0.0.0]network 172.16.12.0 0.0.0.3

RT2 配置如下:

1
2
3
4
[RT2]ospf 1 router-id 2.2.2.2
[RT2-ospf-1]area 0
[RT2-ospf-1-area-0.0.0.0]network 172.16.12.0 0.0.0.3
[RT2-ospf-1-area-0.0.0.]network 172.16.255.0 0.0.0.255

配置完成后,RT1 和 RT2 建立邻接关系,交换 LSA。查看 RT1 的邻居表:

邻接表中,RT1 在Area0发现邻居,Router-ID2.2.2.2,接口 IP 地址是172.16.12.2,邻居状态是Full,对端是Master

再看 RT1 的 OSPF 路由表:

命令display ospf routing,是查看 OSPF 路由表,并不是路由器的全局路由器。这些 OSPF 路由表能否加载到全局路由表,还要看路由表优先级等因素。这里发现 R2 的 Loopback0 接口路由,是一条主机路由,实际上,Loopback0 接口的掩码长度是 24,而不是 32。这是因为 OSPF 把 Loopback 接口作为末梢网络,无论实际掩码是多少,Type-1 LSA 中,都以255.255.255.255进行通告。

查看 RT2 的 Type-1 LSA :

如果希望 RT2 的 Type-1 LSA 描述 Loopback 接口的实际掩码信息,可以把接口的网络类型改成 Broadcast 或 NBMA,比如:

1
2
[RT2]interface LoopBack 0
[RT2-LoopBack0]ospf network-type broadcast

再看 RT2 的 Type-1 LSA:

RT2 以实际掩码信息通告Loopback0接口。现在看下 RT1 的路由表中 OSPF 路由:

RT1 把172.16.255.0/24网段加载到路由表。同时,RT2 也获得到达172.16.1.0/24172.16.2.0/24的路由。这样 PC 就能访问网络中的各个网段。

Silent-Interface

上个实验的拓扑图中,RT1 的GE0/0/0GE0/0/1接口连接终端网段,只有终端 PC,没有 OSPF 路由器。然而,接口已经激活了 OSPF,会周期性的发送Hello报文,但是 PC 无法识别、也不需要识别Hello报文。这时,可以把 RT1 的GE0/0/0GE0/0/1配置成静默接口(Silent-Interface),接口就会禁止收发Hello报文。

R1 的配置如下:

1
2
3
[RT1]ospf 1
[RT1-ospf-1]silent-interface GigabitEthernet 0/0/0
[RT1-ospf-1]silent-interface GigabitEthernet 0/0/1

虽然两个接口指定为Silent-Interface,但是已经使用 network 命令激活 OSPF,因此 RT2 还是能通过 OSPF 学习到这两个接口网段的路由。

OSPF 多区域实验

RT1 和 RT2 是两台汇聚交换机,各自下挂两个终端网段,同时上连核心交换机 RT3 。在三台路由器上部署 OSPF ,使用多区域 OSPF 的设计,实现全网各个网段的数据互通。

RT1 的配置如下:

1
2
3
4
5
6
7
[RT1]ospf 1 router-id 1.1.1.1
[RT1-ospf-1]area 1
[RT1-ospf-1-area-0.0.0.1]network 172.16.1.0 0.0.0.255
[RT1-ospf-1-area-0.0.0.1]network 172.16.2.0 0.0.0.255
[RT1-ospf-area-0.0.0.1]quit
[RT1-ospf-1]area 0
[RT1-ospf-1-area-0.0.0.o]network 172.16.0.0 0.0.0.3

RT2 的配置如下:

1
2
3
4
5
6
7
[RT2]ospf 1 router-id 2.2.2.2
[RT2-ospf-1]area 2
[RT2-ospf-1-area-0.0.0.2]network 172.16.9.0 0.0.0.255
[RT2-ospf-1-area-0.0.0.2]network 172.16.10.0 0.0.0.255
[RT2-ospf-1-area-0.0.0.2]quit
[RT2-ospf-1]area 0
[RT2-ospf-1-area-0.0.0.o]network 172.16.0.4 0.0.0.3

RT3 的配置如下:

1
2
3
4
[RT3]ospf 1 router-id 3.3.3.3
[RT3-ospf-1]area 0
[RT3-ospf-1-area-0.0.0.0]network 172.16.0.0 0.0.0.3
[RT3-ospf-1-area-0.0.0.0]network 172.16.0.4 0.0.0.3

查看 RT3 的 OSPF 邻居表:

RT3 和 RT1、RT2 建立的 FULL 的邻接关系,我们再看看 RT3 的路由表:

Area0的路由器 RT3,已经学到了 RT1 和 RT2 下连的终端网段路由。这些路由都是区域间路由,根据 RT1 和 RT2 在Area0泛洪的 Type-3 LSA 计算得出,而 RT3 要计算到达Area1Area2的区域间路由,除了这些网段的 Type-3 LSA,还需要指定 ABR 的位置。作为 ABR,RT1 和 RT2 在泛洪 Type-1 LSA 时,会把 B 比特位设置为 1。因此,通过 Area0 内泛洪的Type-1Type-2LSA ,RT3 能计算到达 ABR 的最佳路径。使用display ospf abr-asbr命令查看 ABR 和 ASBR 信息:

在 RT1 和 RT2 上查看路由表,看到两台路由器都学到了全网的路由,RT1 和 RT2 下挂的 PC 就可以到达全网各个网段。

OSPF Cost 值

R1 和 R2 连接到相同的一个网段:192.168.100.0/24,同时下连 R3。R1、R2、R3 都激活 OSPF,在相同的 Area 中,接口的 Cost 又是默认值,这时 R3 的路由表中,到达192.168.100.0/24会有两条等价路由:

如果两条链路以主备方式工作,该如何实现呢?一个最简单的方法就是调整接口 Cost 值。比如把 R3 的G0/0/2接口 Cost 值调大,到达192.168.100.0/24的报文会转发给 R1,当 R1 故障时,R3 自动把流量切到 R2。

R3 的配置:

1
2
[R3]interface GigabitEthernet 0/0/2
[R3-GigabitEthernet0/0/2]ospf cost 100

查看接口参数:

在查看 R3 的路由表:

配置生效。

OSPF 特殊区域

R1 、R2 、R3 运行 OSPF,R3 把自己的静态路由引入 OSPF,让域内的路由器学习到外部路由。

1、基本配置

R1 配置:

1
2
3
[Rl]ospf i router-id 1.1.1.1
[R1-ospf-l]area 1
[R1-ospf-1-area-0.0.0.l]network 10.1.12.0 0.0.0.255

R2 配置:

1
2
3
4
5
6
[R2]ospf  router-id 2.2.2.2
[R2-ospf-l]area 1
[R2-ospf-1-area-0.0.0.]network 10.1.12.0 0.0.0.255
[R2-ospf-1-area-0.0.0.1]quit
[R2-ospf-1]area 0
[R2-ospf-1-area-0.0.0.0]network 10.1.23.0 0.0.0.255

R3 配置:

1
2
3
4
5
6
7
[R3]ip route-static 10.3.1.0 24 NULL 0
[R3]ip route-static 10.3.2.0 24 NULL O
[R3]ospf 1 router-id 3.3.3.3
[R3-ospf-1]area 0
[R3-ospf-1-area-0.0.0.o]network 10.1.23.0 0.0.0.255
[R3-ospf-1-area-0.0.0.0]quit
[R3-ospf-1]import-route static

观察 R1 的路由表:

R1 学习到了区域间路由和外部路由,外部路由标记位 O_ASE( OSPF AS External )。再看看 R1 的 LSDB:

R1 的 LSDB,有 Type-1、Type-2、Type-3、Type-4、Type-5 LSA。Type-3 LSA 描述到达10.1.23.0/24的区域间路由。Type-4 LSA 描述到达 ASBR,也就是 R3 的路由,是由 ABR R2 产生。Type-5 LSA 描述外部路由10.3.1.0/2410.3.2.0/24,并在整个 OSPF 域内泛洪,这时 R1 有到达各个网段的路由。

2、Area1 配置为 Stub 区域

先把 Area1 配置为 Stub 区域,R1 和 R2 的配置如下:

1
2
3
[Rl]ospf 1
[R1-ospf-l]area 1
[R1-ospf-1-area-0.0.0.1]stub
1
2
3
[R2]ospf 1
[R2-ospf-l]area 1
[R2-ospf-1-area-0.0.0.1]stub

某个区域为 Stub 区域,区域内的路由器都要配置成 Stub 区域,否则无法正确建立邻居关系。Stub 区域的 ABR,即 R2,会阻挡 Type-4 、Type-5 LSA 进入区域内,减少 LSA 泛洪的数量,从而减小路由表规模,降低设备负担。

现在 R1 无法学到 OSPF 外部路由,同时 R2 会下发一条用 Type-3 LSA 描述的外部路由,让 Area1 内的路由器访问域外的网络。观察 R1 的路由表和 LSDB:

R1 的路由表减少,不再有 Type-4 和 Type-5 LSA ,只有 Type-1、Type-2、Type-3 LSA。

3、Area1 配置为 Totally-Stub 区域

如果要进一步减少 LSA 泛洪,可以把区域间的路由也阻挡。在上个实验的基础上,R2 配置如下:

1
2
3
[R2]ospf 1
[R2-ospf-l]area 1
[R2-ospf-1-area-0.0.0.1]stub no-summary

这时,R2 阻挡 Type-3、Type-4、Type-5 LSA 进入 Area1,同时自动下发一条默认路由,使用 Type-3 LSA 描述。这样当 R1 访问区域外的网络时,就使用默认路由转发数据。

查看 R1 路由表和 LSDB:

R1 路由表只有一条 0.0.0.0/0 的默认路由,极大简化了路由表。同时,R1 的 LSDB 也非常简洁。

4、Area1 配置为 NSSA

网络发生变化,Area1 的 R1 连着一个外部路由,需要引入 OSPF,让域内路由器获得外部路由,但又希望保持 Stub 区域特性,那么可以把 Area1 配置为 NSSA。在上个实验的基础上,R1 配置如下:

1
2
3
4
5
6
7
[R1]ip route-static 10.1.1.0 24 NULL 0 #模拟外部路由
[R1]ospf 1
[Rl-ospf-l]area 1
[R1-ospf-1-area-0.0.0.1]undo stub
[R1-ospf-1-area-0.0.0.]nssa
[R1-ospf-1-area-0.0.0.1]quit
[R1-ospf-1]import-route static

在 R1 创建静态路由,模拟成外部路由,先取消 Stub 配置,然后配置 NSSA,再把外部路由引入 OSPF。

R2 配置如下:

1
2
3
4
[R2]ospf 1
[R2-ospf-l]area 1
[R2-ospf-1-area-0.0.0.]undo stub
[R2-ospf-1-area-0.0.0.1]nssa

某个区域配置为 NSSA,则区域内的所有路由器都要进行相应配置,否则建立邻居关系会出现问题。Area1 区域成为 NSSA 后,会阻挡 Type-4、Type-5 LSA 进入区域。但是 ABR R2 会下发一条 Type-7 LSA 的默认路由,让区域内的路由器,通过默认路由到达域外网络。同时,会向 Area0 通告 Type-5 LSA 描述10.1.1.0/24路由,让 OSPF 其它区域的路由器都学习到这条路由。

查看 R1 的 LSDB :

R1 的 LSDB 中,有 Type-1、Type-2、Type-3、Type-7 LSA,其中两条 Type-7 LSA,一条是 R1 生成的,描述引入的外部路由10.1.1.0/24,另一条是 R2 生成的,是一条默认路由。再看看 R1 的路由表:

再看看 R3 的 LSDB:

R3 有三条 Type-5 LSA,其中两条是自己生成的,描述外部路由10.3.1.0/2410.3.2.0/24,另一条是 R2 生成的,描述外部路由10.1.1.0/24。R2 把从 Area1 收到的 Type-7 LSA 转换成 Type-5 LSA,并通告到 Area0 中。查看 R3 路由表:

5、Area1 配置为 Totally NSSA

为了进一步减少 Area1 的 LSA,把 Area1 配置成 Totally NSSA 实现。在上个实验的基础上,R2 配置如下:

1
2
3
[R2]ospf 1
[R2-ospf-l]area 1
[R2-ospf-1-area-0.0.0.1]nssa no-summary

这样 Area1 内不会有 Type-3 LSA 泛洪,R1 也学不到区域间路由。

看下 R1 的 LSDB:

R1 的 LSDB 中,只有 Type-1、Type-2、Type-7 LSA 和一条描述的默认路由 Type-3 LSA。当 NSSA 内同时存在 Type-3 LSA 和 Type-7 LSA 描述的默认路由时,路由器优先使用 Type-3 LSA 的默认路由,忽略 Type-7 LSA 的默认路由。

Virtual Link

R1、R2、R3 运行 OSPF,规划两个区域Area0Area23。R3 有两条路由到达192.168.2.0/24网段,因为 R3 不能使用非 0 区域的 Type-3 LSA 来计算区域间路由,因此无论路径的 Cost 如何,R3 都会选择 R1 到达目的网段。查看 R3 的 OSPF 路由表:

如果向让 R3 从 R2 到达192.168.2.0/24,即使用高带宽链路转发,一个简单的方法是,在 R2 和 R3 之间跨越Area23建立一条Virtual Link,通过这条Virtual Link,R2 直接把 Type-1 LSA 发送给 R3。

R2 配置如下:

1
2
3
[R2]ospf 1
[R2-ospf-l]area 23
[R2-ospf-1-area-0.0.0.23]vlink-peer 3.3.3.3

R3 配置如下:

1
2
3
[R3]ospf 1
[R3-ospf-l]area 23
[R3-ospf-1-area-0.0.0.23]vlink-peer 2.2.2.2

配置完成后,R2 和 R3 建立一条Virtual LinkVirtual Link穿过 Area23,在 R3 查看Virtual Link信息:

Virtual Link建立完成后,状态为Full,Cost 为 1,再看下 R3 的 OSPF 路由表:

192.168.2.0/24路由的下一跳变成了192.168.23.2,说明到达这个网段的下一跳切换到了 R2,达到预期目标。

网络环路

现在我们的生活已经离不开网络,如果我家断网,我会抱怨这什么破网络,影响我刷抖音、打游戏;如果公司断网,那老板估计会骂娘,因为会影响到公司正常运转,直接造成经济损失。网络通信中,通常是以一条链路能够正常工作为前提,如果链路断开或节点故障,那么互联的设备就无法正常通信了,这类网络问题叫做单点故障。没有备份的链路或节点,出现故障会直接断网。

如果要提供 7×24 小时不间断的服务,那就需要在网络中提前部署冗余。避免出现单点故障,合理的做法是在网络中的关键设备和关键链路添加冗余。在冗余的网络环境中,任意一条链路发生故障断开,都不会影响网络,直接使用其它链路继续转发数据,解决单点故障的隐患。

但同时也带来了另外的网络问题。这种组网会构成二层环路,会引发广播风暴、重复帧、MAC 地址漂移等问题,严重时会占满链路带宽,或打爆设备 CPU,导致设备无法正常工作,最终造成网络瘫痪。当然,在实际的网络中,不少二层环路是由于人为的错误操作导致的,比如接错了网线。

举个栗子:

大刘的主机想要与小美的主机进行通信,现在只知道小美主机的 IP 地址,不知道 MAC 地址。有 IP 地址,就可以通过 ARP 协议来获取小美主机的 MAC 地址。我们来看看有冗余的网络中数据交换的过程:

  1. 大刘主机向交换机 A 发送 ARP 广播帧,来解析小美主机的 MAC 地址;
  2. 交换机 A 收到广播帧后,查看自己的 MAC 地址表,没找到相应的表项,就向所有端口(除接收端口之外)泛洪这个广播帧。也就是向 G0/1 和 G0/2 两个端口泛洪广播帧;
  3. 交换机 B 和交换机 C 收到广播帧后,没有对应 MAC 地址表项,也将广播帧所有端口(除接收端口之外)泛洪出去;
  4. 小美主机终于收到了大刘发送的 ARP 广播帧,发现是查询自己的 MAC 地址后,小美主机将会通过单播帧返回自己的 MAC 地址;
  5. 这个过程看似正常,大刘主机发送的 ARP 广播帧顺利到达小美主机,小美主机也进行了响应,但是网络中广播帧的传输还没有结束。在第 3 步中,交换机 C 也把 ARP 广播帧泛洪到交换机 B 。这时交换机 B 就收到了两个相同的 ARP 广播帧,分别来自交换机 A 和交换机 C ,收到的广播帧都会泛洪出去。那么,小美主机也会收到两个相同的 ARP 广播帧,也就是重复帧。出现这种现象说明网络中存在不合理的冗余链路;
  6. 接下来我们看下交换机 C ,交换机 C 收到从交换机 A 发过来的广播帧,同时交换机 C 的 MAC 地址表添加一条表项,记录大刘主机 MAC 地址和端口 G0/0 的映射关系。交换机 C 又从交换机 B 收到相同的广播帧,大刘主机 MAC 地址的映射端口从 G0/0 变成 G0/1 。从不同的端口收到相同的数据帧,导致 MAC 地址表项发生变化的现象,就叫做 MAC 地址漂移,这种现象说明网络中可能存在环路。这样一来,交换机 C 就无法确定大刘主机到底位于自己的哪个端口;
  7. 主机收到广播帧,会进行解封装,查看上层的 IP 地址是否是发送给自己的,再进行下一步处理。交换机(只指二层交换机)收到广播帧,会直接进行泛洪。大刘主机发出的广播帧,经过交换机 A 后,从交换机 A 的 G0/1 口泛洪的广播帧,交换机 B 收到后再从 G0/2 口进行泛洪,交换机 C 收到广播帧后,又从 G0/0 口泛洪出去,结果广播帧回到了交换机 A ,交换机 A 再从 G0/1 进行泛洪,最终这个广播帧会一直逆时针、永无止境的进行泛洪;同理,交换机 A 从 G0/2 口进行泛洪的广播帧,也会按顺时针、无休止的在三台交换机上进行泛洪。这种广播帧不停泛洪的现象,叫做广播风暴。广播风暴不仅会大量消耗网络设备的带宽和 CPU 使用率,也会影响到主机。主机收到一个广播帧后,会解封装上送网络层去处理,大量的广播帧泛洪,很可能导致主机瘫痪。

通过这个简单的演示,我们看到了冗余链路带来的风险。重复帧、MAC 地址漂移和广播风暴,都是由一个广播帧引起的,可是网络中不可避免出现广播帧,也不能因为二层环路问题而忽略冗余链路增加网络可靠性的好处。

那么如何在保证网络冗余的情况下,消除二层环路呢?实际上交换机的二层环路是一个典型问题,解决方案也有不少。其中的一个解决方案就是 STP(生成树协议),能够阻断冗余链路来消除可能存在的环路,并且在网络故障时激活被阻断的冗余备份链路,恢复网络的连通性,保障业务的不间断服务。

CSMA/CA

以太网用 CSMA/CD 进行传输控制,而 IEEE 802.11 的 WLAN 采用的是 CSMA/CA。

CSMA/CD ,全称Carrier Sense Multiple Access with Collision Detection,即 载波侦听多路访问/冲突检测协议。

载波侦听(Carrier Sense),是指网络中的各个设备在发送数据前,都要确认确认线路上有没有数据传输。如果有数据传输,就不发送数据;如果没有数据传输,马上发送数据。

多路访问(Multiple Access),是指网络上所有设备收发数据,共同使用同一条线路,而且发送的数据是广播型。

冲突检测(Collision Detection),是指设备在发送数据帧的同时,还必须监听线路情况,判断是否发生冲突。也就是说,同一时刻,有没有其它设备也在发送数据帧。

以太网的冲突域是指数据发送时,检测出冲突,当发生冲突时等待一段随机时间再次发送。而在 WLAN 中,如果遇到其它设备正在发送数据,那么就在设备发送完成后,再等待一段随机时间,采继续发送数据。这就是冲突避免(CA ,Collision Avoidance)。因为在对方设备发送完后直接发送数据,也有可能会造成无线传输冲突。

以太网中,传输介质是网线或光纤,能够通过电气信号检测冲突的发生。但由于无线网络不会产生电气信号,因此需要使用 CSMA/CA 来替代 CSMA/CD。

名称 以太网 WLAN
标准 IEEE802.3 IEEE802.11
地址 MAC 地址 MAC 地址
传输介质 线缆 无线电波
接入控制 CSMA/CD CSMA/CA
传输方式 半双工/全双工 半双工

WLAN 的架构

STA ,全称Station,即工作站,是指配有无线网卡的无线终端,比如:手机、电脑等。

AP,全称Wireless Access Point,即无线 AP,用来连接 STA 和有线网络的网络设备。

IBSS,全称Independent Basic Service Set,即独立基本服务集,包含一个及以上 STA 的无线网络,也叫做 ad-hoc 无线网络,无法访问 DS 时使用的模式。

BSS,全称Basic Service Set,即基本服务集,由一个 AP 和一个及以上 STA 组成的无线网络。BSS 内所有的 STA 通信都是通过 AP 完成,AP 不仅能连接有线网络,还可以在 STA 和 其它 STA 或 DS 节点之间进行桥接。

ESS,全称Extended Service Set,即扩展服务集,同一有线网络连接的、两个及以上的 AP 组成,和一个子网概念类似。

DS,全称Distribution System,即分发系统,让不同 BSS 内的 AP 通过 DS 互连,STA 可以从一个 BSS 移动到另一个 BSS。AP 之间可以是无线互连,也可以是有线互连,通常是使用有线互连。DS 是 BSS 之间进行逻辑连接的基础,让 STA 在 BSS 之间能够实现漫游。

WLAN 的拓扑结构

WLAN 的拓扑结构分为两种,一种是终端之间直接互连的ad-hoc模式,另一种是通过 AP 连接有线网络的基础设施模式。

ad-hoc模式,全称ad-hoc mode,即 IEEE 802.11 无线网络的 BSS,在两台 STA 之间直接进行无线通信,而组成的网络,也叫做点对点的网络模式。通常是笔记本电脑和打印机进行无线连接或多台游戏机进行联机对战时使用。终端一般配置了无线网卡。这个模式下,终端是不能连接到互联网上的。

基础设施模式,全称Infrastructure Mode,是指 802.11 无线网络的 BSS 形式组网,通常是通过 AP 连接到互联网时使用。在这个模式下,除了有 STA 外,还需要有 AP 才能连接到互联网。

WLAN 的接入点

有线网络通过有线线缆把终端和交换机连接起来,组成网络。而无线网络的基础设施模式中,是通过一种叫做 AP 的设备,把多台终端连接到有线网络中。AP 也叫做接入点,接入点通常有 RJ-45 网络接口,用来连接到交换机或路由器上,从而让无线网络的终端能够访问有线网络或互联网。

WLAN 标准

和以太网一样,WLAN 的标准也是有 IEEE 组织制定的。以太网标准统称为 IEEE 802.3,而 WLAN 标准统称为 802.11。

和 IEEE 802.3 一样,IEEE 802.11 在物理层和数据链路层之间也定义了 MAC 子层。定义了 WLAN 采用哪种频带和调制方式,传输速率达到多大等传输标准,还定义了安全性、QoS 、管理等相关内容。

IEEE 标准 名称 年份 频带 最大传输速率 调制方式
802.11 WiFi0 1997年 2.4GHz 2Mbit/s DSSS
802.11b WiFi1 1999年 2.4GHz 11Mbit/s DSSS
802.11a WiFi2 1999年 5GHz 54Mbit/s OFDM
802.11g WiFi3 2003年 2.4GHz 54Mbit/s OFDM
802.11n WiFi4 2009年 2.4GHz/5GHz 600Mbit/s OFDM
802.11ac Wave 1 WiFi5 2013年 5GHz 3.46Gbit/s OFDM
802.11ac Wave 2 WiFi5 2016年 5GHz 6.96Gbit/s OFDM
802.11ax WiFi6 2018年 2.4GHz/5GHz 9.6Gbit/s OFDMA

IEEE 802.11n

IEEE 802.11n之前的标准已经不多见了。IEEE 802.11n,又叫做 WiFi 4,标准在 2009 年制定完成,最大传输速率是600 Mbit/s,使用 MIMO 多通道技术让传输速率大幅提升。也能向下兼容IEEE 802.11a、IEEE 802.11b、IEEE 802.11g

IEEE 802.11ac

IEEE 802.11ac,又叫做 WiFi 5,有两个版本,分别是:Wave 1、Wave 2。区别是 Wave 1 使用 80 Mhz 频宽和 SU-MIMO 技术,最大传输速率是3.46 Gbit/s。而 Wave 2 是使用 160 Mhz 频宽和 DL MU-MIMO 技术,最大传输速率是6.93 Gbit/s。频带和调制方式等都相同。

IEEE 802.11ax

IEEE 802.11ax,又叫做 WiFi 6,标准在 2018 年制定完成,最大传输速率是9.6 Gbit/s。WiFi 6 同时支持 2.4G 和 5G 频段,完整涵盖低速与高速设备,覆盖范围更远。支持 WPA 3 安全协议,无线网络更安全。支持 TWT 技术,能够更省电。简单的说,就是速度更快、延时更低、容量更低、更安全、更省电。

什么是 WiFi

WiFi 是基于IEEE 802.11标准的、不同厂家为产品的品牌认证。既然有IEEE 802.11标准了,为什么还要有 WiFi 认证?这是因为IEEE 802.11正式标准推出的时间周期长,而无线网络发展迅猛,各个厂家等不及正式标准,自己组成了 WiFi 联盟,并完成互联互通的认证。

实际上,并不是每个IEEE 802.11产品都申请了 WiFi 联盟的认证,那么缺少 WiFi 认证的产品,并不一定兼容 WiFi 设备。但是经过 WiFi 认证的 STA 或 AP 都能无障碍的互联互通。

在酒店或公共场所中,经常看到“免费 WiFi 上网”的标识,这就表示这里的 AP 已经通过了 WiFi 认证。除了电脑和手机,家电和游戏机也可以完成 WiFi 认证。

WiFi 还定义了类似 WPA 这种无线加密的相关标准。

WLAN 的功能

关联

使用 WLAN 的终端要通过 AP 完成无线连接,才能连接到互联网或有线网络。

无线终端连接 AP 的过程叫做关联(Association)。

STA 可以和连接不同的 AP,但是同一时刻,只能连接在一个 AP 上。AP 会定期发送beacon帧,STA 根据beacon帧的内容,获取 AP 的 SSID 信息、支持的无线传输速率,以及无线信道等信息。

STA 在关联过程中,会向 AP 发送关联请求数据帧,AP 收到请求后,就向 STA 返回带有状态码的关联响应数据帧。

STA 会确认 AP 发过来的状态码,如果是successful,表示关联成功,如果返回其它信息,表示关联失败。STA 在收到successful的同时,还会分配一个 Association ID(AID)的识别号。

WLAN 认证的过程,是在关联过程之前发生的。

数据帧

IEEE 802.11 的 MAC 数据帧的字段信息如下:

  • Protocol Version:协议版本,表示 IEEE 802.11 协议的版本。
  • Type:类型,表示数据帧的功能,有控制帧、管理帧、数据帧三种。
  • Subtype:子类型,每种数据帧都有多个子类型,实现特定的功能。
  • To DSFrom DS:DS 是指分布式系统,用与 AP 和关联的 STA 之间传送的数据帧类型。值为 1 时,表示发送源是 AP,值为 0 时,表示发送源是 STA。
  • More Frag:把上层分组进行分片后,进行发送时使用。值为 1 时,表示后续存在分片数据帧。值为 0 时,表示当前数据帧是最后的分片,或不存在分片数据帧。
  • Retry:表示是否重发数据帧。值为 1 时,表示再次发送数据帧。值为 0 时,表示不再发送这个数据帧。
  • More Data:表示是否存在后续发送的分组。值为 1 时,表示存在后续分组。
  • WEP:表示是否进行 WEP 加密。值为 1 时,表示进行加密。
  • Order:值为 1 时,表示数据帧严格按照 strictly ordered(发送接收顺无法替换)的标准进行发送。

IEEE 802.11 的数据帧可分为三类:

  • 管理帧
    1、广播无线信号的beacon帧,默认情况下,每 100 毫秒 AP 广播一次。
    2、认证使用认证帧:AP 和 STA 进行信息交互时,使用的关联帧。
  • 控制帧
  • 数据帧

通常数据帧的 Address 1 表示目的地址,Address 2 表示源地址,Address 3 表示 BSSID 信息。

接入点的接入控制

由于无线电波是看不见的,会出现陌生用户在未经允许时,擅自接入 AP 使用的情况发生。只要在无线信号能够到达的范围内,并知道 SSID,STA 就能够和 AP 进行关联。为了防止未知的人使用,可以使用 ESSID 隐藏功能和 MAC 地址过滤功能。

ESSID 隐藏

SSID 信息是由 AP 的 beacon 帧定期进行广播发送的。STA 通过 beacon 帧来确认和那个 SSID 进行连接。但是,只有是无线信号能够到达的地方,任何人都可以通过 beacon 帧,使用 STA 搜索到 SSID 信息并连接。

为了防范这类风险,可以使用不发出 beacon 帧的 ESSID 隐藏功能。STA 需要通过其它途径获得 SSID 信息,并在 STA 进行相应配置,从而隐藏网络连接。

但是,由于 SSID 在无线网络的传播中,没有加密,当有 STA 使用这个隐藏的 SSID 连接 AP 时,可以通过无线监控工具获取这个无线网络的 SSID,所以 ESSI 隐藏功能不是很安全的对策。

MAC 地址过滤

在 AP 中设置允许关联的 MAC 地址列表,只有在列表里面的 STA 才能连上无线网络,防止以外的 STA 接入 AP,这个方法叫做 MAC 地址过滤或 MAC 地址认证。

除了在 AP 上设置外,还可以通过 RADIUS 服务器设置允许接入的 MAC 地址信息,在认证的同时完成 MAC 地址过滤。但是,MAC 地址也能通过工具伪装和冒充,可以对接入 WLAN 的 MAC 地址进行监听,获得具体的 MAC 地址信息,所以这也不是完善的安全策略。

接入点的认证

在 AP 上使用 ESSID 隐藏和 MAC 地址过滤功能,都不能完全阻止恶意访问,为了彻底防止恶意用户访问无线网络,需要进行认证。

IEEE 802.11 最开始有两种认证方式:开放系统认证、共享密钥认证。

开放系统认证
开发系统认证(Open System Authentication)不用 STA 输入用户名和密码等认证信息,就可以向 AP 发出认证请求。AP 能够接收所有接入认证请求,也就是说,无论是谁都可以和 AP 关联上。通常用于公共 WiFi ,结合 Portal 认证或 VPN 来完成用户访问网络的权限控制。

共享密钥认证
共享密钥认证(Shared Key Authentication)用于 AP 和 STA 进行无线加密通信。使用 WEP 或 WPA 加密标准时,AP 和 STA 预先配置相同的口令,通过这个口令就可以建立无线通信链路。这个口令叫做预共享密钥(pre-shared key),不知道预共享密钥的 STA,是无法和 AP 进行关联的。

IEEE 802.1X
IEEE 802.1X 是用户认证和访问控制协议,是从有线网络中引用过来的。

IEEE 802.1X 认证是由认证客户端、接入设备、认证服务器三部分组成。请求认证的终端叫做认证客户端,和终端连接的 AP、交换机及其它网络设备叫做接入设备。认证方式使用 EAP,客户端发起认证请求,接入设备会把收到的 EAP 消息封装成 RADIUS 数据帧,转发给认证服务器,当认证服务器完成认证后,接入设备会通知客户端并把客户端作为认证成功的客户端,之后客户端发送的数据帧都会转发到局域网或互联网上。

认证信息是使用用户名、口令、数字证书等其中一种方式即可,对应的认证协议有 EAP-MD5、EAP-TLS、EAP-TTLS 等各种类型。

WLAN 通信的加密

空气中传输的无线电波,只要是在覆盖范围内,就能被任何人收到,再加上 WLAN 数据解析工具,恶意用户就能够窃听无线网络的通信内容。

为了防止无线通信被窃听和篡改,要在无线通信过程中,对信息进行加密。WLAN 加密有 WEP、WPA、WPA2、WPA3 等标准。

WEP

WEP,全称Wired Equivalent Privacy,即有线等效保密。WEP 加密是最早在无线加密中使用的技术,基于 RC4 算法的密钥对数据进行加密,这个密钥叫做 WEP key。

WEP 一共有三种加密方式:40 bit 长度的密钥和 24 bit 长度的初始向量值组成 64 bit 长的加密方式,104 bit 长度的密钥和 24 bit 长度的初始向量值组成 128 bit 长的加密方式,128 bit 长度的密钥和 24 bit 长度的初始向量值组成 152 bit 长的加密方式。密钥长度越短,破解时间也越短,现在已经不怎么使用了。

WPA

WEP 实在太脆弱了,于是就制定了 WPA,全称WiFi Protected Access,即 WiFi 保护接入。

WPA 把 SSID 和 WEP 密钥一起加密,并且能定期自动更新用户认证功能和密钥的 TKIP。

WPA 有两种模式:个人模式和企业模式。

  • 个人模式的 WPA 主要是家庭和个人使用,也叫做 WPA-PSK,AP 和 STA 使用相同的预共享密钥(PSK)。
  • 企业模式的 WPA 主要用于企业,增加了 IEEE 802.1X 认证服务器,不同的用户使用不同的用户名和密码连接无线网络。

WPA2

WPA2 是新一代 WPA 标准,采用 AES 加密算法。AES 常用于 IPsec 和 SSL 等协议中,比 RC4 的安全性更高。AES 支持长度是 128 bit 、196 bit 、256 bit 的密钥,WPA2 使用其中的 128 bit 长度类型。WPA2 兼容上一代 WPA,支持 WPA2 的设备和只支持 WPA 的设备也能通信。AES 采用了类似 TKIP 的协议 CCMP,其中 CBC-MAC 是密码段连接/消息认证码的意思。

AP 的加密设置可以选择 WPA-PSK(TKIP)、WPA-PSK(AES)、WPA2-PSK(TKIP)或 WPA2-PSK(AES)。

WPA3

2017 年 10 月,802.11 协议中沿用 13 年的 WPA2 加密被完全破解。2018 年 6 月 26 日,WiFi 联盟宣布 WPA3 协议已最终完成,这是 WiFi 连接的新标准。

WPA3 在 WPA2 的基础上增加了新的功能,以简化 WiFi 安全保障方法、实现更可靠的身份验证,提高数据加密强度。所有的 WPA3 网络都必须进行管理帧保护 PMF,保证数据的安全性。

根据 WiFi 网络的用途和安全需求的不同,WPA3 又分为 WPA3 个人版、WPA3 企业版,即 WPA3-SAE 和 WPA3-802.1X。WPA3 为不同网络提供了额外功能:WPA3 个人版增强了对密码安全的保护,而 WPA3 企业版的用户可以选择更高级的安全协议,保护敏感数据。

WPA3 个人版
对比 WPA2 个人版,WPA3 个人版能提供更可靠的基于密码的身份验证。这是由于 WPA3 个人版使用了更安全的协议:对等实体同时验证 SAE(Simultaneous Authentication of Equals)。SAE 取代了 WPA2 个人版的 PSK 认证方式,可以有效地抵御离线字典攻击,增加暴力破解的难度。SAE 能够提供前向保密,即使攻击者知道了网络中的密码,也不能解密获取到的流量,大大提升了 WPA3 个人网络的安全。WPA3 个人版只支持 AES 加密方式。

SAE 在 WPA/WPA2-PSK 原有的四次握手前增加了 SAE 握手,实质上是为了动态协商成对主密钥 PMK。WPA/WPA2-PSK 的 PMK 只与 SSID 和预共享密钥有关,而 SAE 引入了动态随机变量,每次协商的 PMK 都是不同的,提升了安全性。

WPA3 企业版

企业、政府和金融机构为了更高的安全性可以采用 WPA3 企业版。WPA3 企业版基于 WPA2 企业版,提供一种可选模式:WPA3-Enterprise 192bit ,这个模式的优点有:

数据保护:使用 192 位的 Suite-B 安全套件,增加密钥的长度。

密钥保护:使用 HMAC-SHA-384 在 4 次握手阶段导出密钥。

流量保护:使用伽罗瓦计数器模式协议 GCMP-256( Galois Counter Mode Protocol )保护用户上线后的无线流量。

管理帧保护:使用 GMAC-256( GCMP 的伽罗瓦消息认证码,Galois Message Authentication Code )保护组播管理帧。

WPA2 企业版支持多种 EAP 方式的身份验证,但是 WPA3 企业版仅支持 EAP-TLS 的方式。

WLAN 的桥接

在无法布线的楼宇之间、在物理位置较远的站点之间,可以通过无线桥接进行无线连接。无线桥接是通过无线上连,有线下连的方式组网。

在通信距离较长时,需要使用定向天线来增强某个特定方向的电波强度。

什么是无线中继?

AP 上连无线接口当做 STA,连接其它 AP 释放的 SSID。AP 下连无线接口释放相同的 SSID,让 STA 接入,这就是无线中继。通过连接中继器,将 STA 收到的数据转发给拥有相同 SSID 的 AP,就能扩大无线网络的范围。一级中继器连接后,网络吞吐率会减半。

无线网络的最大速率

无线网络和有线网络都是有理论的最大速率,比如:常见的以太网的 10Gbit/s 和 WiFi 6 的 9.6Gbit/s,这些是说在进行数据通信时,传输的极限速率。

由于无线网络中使用了 CSMA/CA 的冲突回避协议,数据在发送时有等待的时间。因此,WiFi 6 的最大理论速率是 9.6Gbit/s,实际使用时最大速度可能不到 80%。

无线网络的速率跟距离有关吗?

STA 能够进行通信的最大距离半径叫做覆盖范围。根据 STA 和 AP 的距离不同,最大速率也会不同,离 AP 越远,通信延迟越大,数据传输速率也越低。在没有障碍物时,无线网络的覆盖范围是同心圆的形状分布。

WiFi 5

WiFi 5 ,也就是 802.11ac 标准。对比 802.11n ,WiFi 5 主要有四个方面的提升:更宽的频宽绑定、更多的空间流、更先进的调制技术、更灵活的 MIMO 机制。

信道绑定:802.11n 引入了信道绑定技术,是把两个 20MHz 的信道捆绑在一起。而 WiFi 5 能够支持 160MHz 的信道,也就是绑定了 8 个信道。如果 802.11a/b/g 是单车道的话,而 802.11n 就是双车道,到来 WiFi 5 就达到八车道。

WiFi 6

WiFi 6,其实就是802.11ax标准。对比 WiFi 5,WiFi 6 的提升是更完整的频带覆盖、更高阶的调制方式、更全面的 MU-MIMO、引入 OFDMA 技术、新增 TWT 机制。

同时支持2.4GHz5GHz频段:WiFi5 只支持 5G 频段,虽然 5G 的频段资源丰富,但穿透力差,在信号覆盖较弱。而 WiFi 6 同时支持2.4GHz5GHz频段,2.4G信号覆盖效果更好,完整涵盖低速与高速设备。

高阶调试方式:WiFi 6 支持 1024-QAM,高于 WiFi 5 的 256-QAM,单载波承载的数据量可以达到 10bit,意味着更高的数据传输速率。

完整 MU-MIMO:WiFi 5 引入 MU-MIMO 功能,但是支持数据下载,上传数据时还是走得 SU-MIMO。而 WiFi6 是完整版的 MU-MIMO 功能,支持数据上行和下行。8 × 8 MU-MIMO,表示最多可以同时支持八个终端传输数据。虽然 AP 可以接入多个终端,但是没使用 MU-MIMO 技术之前,都是一个接着一个收发数据的,AP 每次只能和一个终端传输数据,只是时间间隔是毫秒级,我们无法感知而已。WiFi 6 就是真正意义上的八个终端同时传输数据了,适用于高密的无线网络场景。

OFDMA 技术:WiFi6 在在 OFDM 的基础上加入多址(即多用户)技术,从而演进成 OFDMA。实际上 OFDMA 将帧结构重新设计,细分成若干资源单元,为多个用户服务。

以 20MHz 信道为例,在 OFDM 方案(即 WiFi 4/WiFi 5)里每一帧由 52 个数据子载波组成,但由于这一帧只为一个终端服务。传输的数据包太小时(像聊天记录)。空载的子载波也无法分配给其他终端。

而在 OFDMA 方案(即 WiFi 6)里每一帧由 234 个数据子载波组成,每 26 个子载波定义为一个 RU(资源单元),每个 RU 可以为一个终端服务,简单除一下,每一帧就可以被分成 9 份,最多可以同时为 9 个用户服务。

用卡车来货来解释这个技术最方便直观了。OFDM 技术是为每一个客户发一次货车。不管货物多少,来一单发一趟,这样不免就有货车空载的现象。而 OFDMA 技术会将多个订单合在一起发货,让卡车尽量满载上路,使得运输效率大大提升。

不但如此,WiFi6 下 OFDMA 和 MU-MIMO 的效果可以叠加。两者呈现出一种互补关系,OFDMA 适用于小数据包的并行传输提高信道利用率和传输效率。而 MU-MIMO 则适用于大数据包的并行传输,提高单用户的有效带宽,同样能减少时延。

TWT机制:WiFi 6 加入了 TWT 机制(Target Wake Time)。TWT 机制是专门针对类似智能家居这样的低速设备而设置的。例如配置 2.4GHz 频段、20MHz 频带的 WiFi 终端。AP 会自动生成一个数据交换用的唤醒时间,在网络数据传输不高的时段,依次唤醒这些低速设备进行数据交换,比如下载最新数据库、上传生成数据等操作。这样慢速设备不再长时间占用带宽,可以有效避免网络拥堵。这也是一种优化网络带宽利用率的技术手段。

WiFi 6 计算最大速率时,使用 160MHz 模式、1024-QAM 调制方式、800ns 的保护间隔时长,WiFi 6 单条流速率可达 1.2 Gbit/s,八条流速率高达 9.6Gbit/s。

WLAN 的信道

WLAN 标准中,使用2.4GHz5GHz频带,各个频带都有多条信道。在设置 AP 时,为了防止干扰,需要把相同信道的 AP 隔离开来。

2.4G 信道

WLAN 的 2.4G 信道频宽是83.5MHz,频率范围是2.4~2.4835 GHz,实际一共划分了 14 个信道,中国是使用前面的 13 个信道,信道编号是1~13。每个信道的有效宽度是20 MHz,另外还有2MHz的强制隔离频带,类似公路上的隔离带。对于中心频率是2412MHz的信道 1,频率范围是2401~2423 MHz

但并不是说,只要选择数字不同的信道,就一定不会发生干扰。信道 1 使用的频率和 信道2~5是有一部分的重合,因此还是会发生干扰。这样看来,肯定不会发生干扰的信道组合是 1、6、11。

5G 信道

WLAN 的 5G 信道频宽资源就丰富些,一共是700MHz的频宽,频率范围是5.15~5.85 GHz,中国一共有 13 个20MHz信道,信道编号是 36、40、44、48、52、56、60、64、149、153、157、161、165,并且所有信道都是互不干扰的,可以直接使用。

802.11n开始,可以同时使用相邻的信道,组成40MHz、80MHz、160MHz的频宽进行数据传输。

HTTP 与 HTTPS

HTTP 与 HTTPS 有哪些区别?

  • HTTP 是超文本传输协议,信息是明文传输,存在安全风险的问题。HTTPS 则解决 HTTP 不安全的缺陷,在 TCP 和 HTTP 网络层之间加入了 SSL/TLS 安全协议,使得报文能够加密传输。
  • HTTP 连接建立相对简单, TCP 三次握手之后便可进行 HTTP 的报文传输。而 HTTPS 在 TCP 三次握手之后,还需进行 SSL/TLS 的握手过程,才可进入加密报文传输。
  • 两者的默认端口不一样,HTTP 默认端口号是 80,HTTPS 默认端口号是 443。
  • HTTPS 协议需要向 CA(证书权威机构)申请数字证书,来保证服务器的身份是可信的。

HTTPS 解决了 HTTP 的哪些问题?

HTTP 由于是明文传输,所以安全上存在以下三个风险:

  • 窃听风险,比如通信链路上可以获取通信内容,用户号容易没。
  • 篡改风险,比如强制植入垃圾广告,视觉污染,用户眼容易瞎。
  • 冒充风险,比如冒充淘宝网站,用户钱容易没。

HTTPS 在 HTTP 与 TCP 层之间加入了 TLS 协议,可以很好的解决了上述的风险:

  • 信息加密: HTTP 交互信息是被加密的,第三方就无法被窃取;
  • 校验机制:校验信息传输过程中是否有被第三方篡改过,如果被篡改过,则会有警告提示;
  • 身份证书:证明淘宝是真的淘宝网;

可见,有了 TLS 协议,能保证 HTTP 通信是安全的了,那么在进行 HTTP 通信前,需要先进行 TLS 握手。

TLS 的握手过程,如下图:

上图简要概述了 TLS 的握手过程,其中每一个「框」都是一个记录(record),记录是 TLS 收发数据的基本单位,类似于 TCP 里的 segment。多个记录可以组合成一个 TCP 包发送,所以通常经过「四个消息」就可以完成 TLS 握手,也就是需要 2个 RTT 的时延,然后就可以在安全的通信环境里发送 HTTP 报文,实现 HTTPS 协议。

所以可以发现,HTTPS 是应用层协议,需要先完成 TCP 连接建立,然后走 TLS 握手过程后,才能建立通信安全的连接。

事实上,不同的密钥交换算法,TLS 的握手过程可能会有一些区别。

因为考虑到性能的问题,所以双方在加密应用信息时使用的是对称加密密钥,而对称加密密钥是不能被泄漏的,为了保证对称加密密钥的安全性,所以使用非对称加密的方式来保护对称加密密钥的协商,这个工作就是密钥交换算法负责的。

接下来,我们就以最简单的 RSA 密钥交换算法,来看看它的 TLS 握手过程。

RSA 握手过程

传统的 TLS 握手基本都是使用 RSA 算法来实现密钥交换的,在将 TLS 证书部署服务端时,证书文件其实就是服务端的公钥,会在 TLS 握手阶段传递给客户端,而服务端的私钥则一直留在服务端,一定要确保私钥不能被窃取。

在 RSA 密钥协商算法中,客户端会生成随机密钥,并使用服务端的公钥加密后再传给服务端。根据非对称加密算法,公钥加密的消息仅能通过私钥解密,这样服务端解密后,双方就得到了相同的密钥,再用它加密应用消息。

我用 Wireshark 工具抓了用 RSA 密钥交换的 TLS 握手过程,你可以从下面看到,一共经历了四次握手:

TLS 第一次握手

客户端首先会发一个Client Hello消息,字面意思我们也能理解到,这是跟服务器「打招呼」。

消息里面有客户端使用的 TLS 版本号、支持的密码套件列表,以及生成的随机数(Client Random),这个随机数会被服务端保留,它是生成对称加密密钥的材料之一。

TLS 第二次握手

当服务端收到客户端的Client Hello消息后,会确认 TLS 版本号是否支持,和从密码套件列表中选择一个密码套件,以及生成随机数(Server Random)。

接着,返回Server Hello消息,消息里面有服务器确认的 TLS 版本号,也给出了随机数(Server Random),然后从客户端的密码套件列表选择了一个合适的密码套件。

可以看到,服务端选择的密码套件是Cipher Suite: TLS_RSA_WITH_AES_128_GCM_SHA256

这个密码套件看起来真让人头晕,好一大串,但是其实它是有固定格式和规范的。基本的形式是「密钥交换算法 + 签名算法 + 对称加密算法 + 摘要算法」, 一般 WITH 单词前面有两个单词,第一个单词是约定密钥交换的算法,第二个单词是约定证书的验证算法。比如刚才的密码套件的意思就是:

  • 由于 WITH 单词只有一个 RSA,则说明握手时密钥交换算法和签名算法都是使用 RSA;
  • 握手后的通信使用 AES 对称算法,密钥长度 128 位,分组模式是 GCM;
  • 摘要算法 SHA256 用于消息认证和产生随机数;

就前面这两个客户端和服务端相互「打招呼」的过程,客户端和服务端就已确认了 TLS 版本和使用的密码套件,而且你可能发现客户端和服务端都会各自生成一个随机数,并且还会把随机数传递给对方。

那这个随机数有啥用呢?其实这两个随机数是后续作为生成「会话密钥」的条件,所谓的会话密钥就是数据传输时,所使用的对称加密密钥。

然后,服务端为了证明自己的身份,会发送Server Certificate给客户端,这个消息里含有数字证书。

随后,服务端发了Server Hello Done消息,目的是告诉客户端,我已经把该给你的东西都给你了,本次打招呼完毕。

客户端验证证书

在这里刹个车,客户端拿到了服务端的数字证书后,要怎么校验该数字证书是真实有效的呢?

数字证书和 CA 机构

在说校验数字证书是否可信的过程前,我们先来看看数字证书是什么,一个数字证书通常包含了:

  • 公钥;
  • 持有者信息;
  • 证书认证机构(CA)的信息;
  • CA 对这份文件的数字签名及使用的算法;
  • 证书有效期;
  • 还有一些其他额外信息;

那数字证书的作用,是用来认证公钥持有者的身份,以防止第三方进行冒充。说简单些,证书就是用来告诉客户端,该服务端是否是合法的,因为只有证书合法,才代表服务端身份是可信的。

我们用证书来认证公钥持有者的身份(服务端的身份),那证书又是怎么来的?又该怎么认证证书呢?

为了让服务端的公钥被大家信任,服务端的证书都是由 CA(Certificate Authority,证书认证机构)签名的,CA 就是网络世界里的公安局、公证中心,具有极高的可信度,所以由它来给各个公钥签名,信任的一方签发的证书,那必然证书也是被信任的。

之所以要签名,是因为签名的作用可以避免中间人在获取证书时对证书内容的篡改。

数字证书签发和验证流程

如下图图所示,为数字证书签发和验证流程:

CA 签发证书的过程,如上图左边部分:

  • 首先 CA 会把持有者的公钥、用途、颁发者、有效时间等信息打成一个包,然后对这些信息进行 Hash 计算,得到一个 Hash 值;
  • 然后 CA 会使用自己的私钥将该 Hash 值加密,生成Certificate Signature,也就是 CA 对证书做了签名;
  • 最后将Certificate Signature添加在文件证书上,形成数字证书;

客户端校验服务端的数字证书的过程,如上图右边部分:

  • 首先客户端会使用同样的 Hash 算法获取该证书的 Hash 值 H1;
  • 通常浏览器和操作系统中集成了 CA 的公钥信息,浏览器收到证书后可以使用 CA 的公钥解密Certificate Signature内容,得到一个 Hash 值 H2;
  • 最后比较 H1 和 H2,如果值相同,则为可信赖的证书,否则则认为证书不可信。

证书链

但事实上,证书的验证过程中还存在一个证书信任链的问题,因为我们向 CA 申请的证书一般不是根证书签发的,而是由中间证书签发的,比如百度的证书,从下图你可以看到,证书的层级有三级:

对于这种三级层级关系的证书的验证过程如下:

  • 客户端收到baidu.com的证书后,发现这个证书的签发者不是根证书,就无法根据本地已有的根证书中的公钥去验证 baidu.com 证书是否可信。于是,客户端根据baidu.com证书中的签发者,找到该证书的颁发机构是GlobalSign Organization Validation CA - SHA256 - G2,然后向 CA 请求该中间证书。
  • 请求到证书后发现GlobalSign Organization Validation CA - SHA256 - G2证书是由GlobalSign Root CA签发的,由于GlobalSign Root CA没有再上级签发机构,说明它是根证书,也就是自签证书。应用软件会检查此证书有否已预载于根证书清单上,如果有,则可以利用根证书中的公钥去验证GlobalSign Organization Validation CA - SHA256 - G2证书,如果发现验证通过,就认为该中间证书是可信的。
  • GlobalSign Organization Validation CA - SHA256 - G2证书被信任后,可以使用GlobalSign Organization Validation CA - SHA256 - G2证书中的公钥去验证baidu.com证书的可信性,如果验证通过,就可以信任baidu.com证书。

在这四个步骤中,最开始客户端只信任根证书GlobalSign Root CA证书的,然后GlobalSign Root CA证书信任GlobalSign Organization Validation CA - SHA256 - G2证书,而GlobalSign Organization Validation CA - SHA256 - G2证书又信任baidu.com证书,于是客户端也信任baidu.com证书。

总括来说,由于用户信任GlobalSign,所以由GlobalSign所担保的baidu.com可以被信任,另外由于用户信任操作系统或浏览器的软件商,所以由软件商预载了根证书的GlobalSign都可被信任。

这样的一层层地验证就构成了一条信任链路,整个证书信任链验证流程如下图所示:

TLS 第三次握手

客户端验证完证书后,认为可信则继续往下走。

接着,客户端就会生成一个新的随机数 (pre-master),用服务器的 RSA 公钥加密该随机数,通过Client Key Exchange消息传给服务端。

服务端收到后,用 RSA 私钥解密,得到客户端发来的随机数 (pre-master)。

至此,客户端和服务端双方都共享了三个随机数,分别是Client Random、Server Random、pre-master

于是,双方根据已经得到的三个随机数,生成会话密钥(Master Secret),它是对称密钥,用于对后续的 HTTP 请求/响应的数据加解密。

生成完「会话密钥」后,然后客户端发一个「Change Cipher Spec」,告诉服务端开始使用加密方式发送消息。

然后,客户端再发一个Encrypted Handshake Message(Finishd)消息,把之前所有发送的数据做个摘要,再用会话密钥加密一下,让服务器做个验证,验证加密通信「是否可用」和「之前握手信息是否有被中途篡改过」。

可以发现,「Change Cipher Spec」之前传输的 TLS 握手数据都是明文,之后都是对称密钥加密的密文。

TLS 第四次握手

服务器也是同样的操作,发「Change Cipher Spec」和「Encrypted Handshake Message」消息,如果双方都验证加密和解密没问题,那么握手正式完成。

最后,就用「会话密钥」加解密 HTTP 请求和响应了。

RSA 算法的缺陷

使用 RSA 密钥协商算法的最大问题是不支持前向保密。

因为客户端传递随机数(用于生成对称加密密钥的条件之一)给服务端时使用的是公钥加密的,服务端收到后,会用私钥解密得到随机数。所以一旦服务端的私钥泄漏了,过去被第三方截获的所有 TLS 通讯密文都会被破解。

为了解决这个问题,后面就出现了 ECDHE 密钥协商算法,我们现在大多数网站使用的正是 ECDHE 密钥协商算法。

ECDHE 握手解析

离散对数

ECDHE 密钥协商算法是 DH 算法演进过来的,所以我们先从 DH 算法说起。

DH 算法是非对称加密算法, 因此它可以用于密钥交换,该算法的核心数学思想是离散对数。

离散对数是「离散 + 对数」的两个数学概念的组合,所以我们先来复习一遍对数。

要说起对数,必然要说指数,因为它们是互为反函数,指数就是幂运算,对数是指数的逆运算。

举个栗子,如果以 2 作为底数,那么指数和对数运算公式,如下图所示:

那么对于底数为 2 的时候, 32 的对数是 5,64 的对数是 6,计算过程如下:

对数运算的取值是可以连续的,而离散对数的取值是不能连续的,因此也以「离散」得名,

离散对数是在对数运算的基础上加了「模运算」,也就说取余数,对应编程语言的操作符是「%」,也可以用 mod 表示。离散对数的概念如下图:

上图的,底数 a 和模数 p 是离散对数的公共参数,也就说是公开的,b 是真数,i 是对数。知道了对数,就可以用上面的公式计算出真数。但反过来,知道真数却很难推算出对数。

特别是当模数 p 是一个很大的质数,即使知道底数 a 和真数 b,在现有的计算机的计算水平是几乎无法算出离散对数的,这就是 DH 算法的数学基础。

DH 算法

认识了离散对数,我们来看看 DH 算法是如何密钥交换的。

现假设小红和小明约定使用 DH 算法来交换密钥,那么基于离散对数,小红和小明需要先确定模数和底数作为算法的参数,这两个参数是公开的,用 P 和 G 来代称。

然后小红和小明各自生成一个随机整数作为私钥,双方的私钥要各自严格保管,不能泄漏,小红的私钥用 a 代称,小明的私钥用 b 代称。

现在小红和小明双方都有了 P 和 G 以及各自的私钥,于是就可以计算出公钥:

小红的公钥记作 A,A = G ^ a ( mod P );
小明的公钥记作 B,B = G ^ b ( mod P );

A 和 B 也是公开的,因为根据离散对数的原理,从真数(A 和 B)反向计算对数 a 和 b 是非常困难的,至少在现有计算机的计算能力是无法破解的,如果量子计算机出来了,那就有可能被破解,当然如果量子计算机真的出来了,那么密钥协商算法就要做大的升级了。

双方交换各自 DH 公钥后,小红手上共有 5 个数:P、G、a、A、B,小明手上也同样共有 5 个数:P、G、b、B、A。

然后小红执行运算: B ^ a ( mod P ),其结果为 K,因为离散对数的幂运算有交换律,所以小明执行运算: A ^ b ( mod P ),得到的结果也是 K。

这个 K 就是小红和小明之间用的对称加密密钥,可以作为会话密钥使用。

可以看到,整个密钥协商过程中,小红和小明公开了 4 个信息:P、G、A、B,其中 P、G 是算法的参数,A 和 B 是公钥,而 a、b 是双方各自保管的私钥,黑客无法获取这 2 个私钥,因此黑客只能从公开的 P、G、A、B 入手,计算出离散对数(私钥)。

前面也多次强调, 根据离散对数的原理,如果 P 是一个大数,在现有的计算机的计算能力是很难破解出 私钥 a、b 的,破解不出私钥,也就无法计算出会话密钥,因此 DH 密钥交换是安全的。

DHE 算法

根据私钥生成的方式,DH 算法分为两种实现:

  • static DH 算法,这个是已经被废弃了;
  • DHE 算法,现在常用的;

static DH 算法里有一方的私钥是静态的,也就说每次密钥协商的时候有一方的私钥都是一样的,一般是服务器方固定,即 a 不变,客户端的私钥则是随机生成的。

于是,DH 交换密钥时就只有客户端的公钥是变化,而服务端公钥是不变的,那么随着时间延长,黑客就会截获海量的密钥协商过程的数据,因为密钥协商的过程有些数据是公开的,黑客就可以依据这些数据暴力破解出服务器的私钥,然后就可以计算出会话密钥了,于是之前截获的加密数据会被破解,所以 static DH 算法不具备前向安全性。

既然固定一方的私钥有被破解的风险,那么干脆就让双方的私钥在每次密钥交换通信时,都是随机生成的、临时的,这个方式也就是 DHE 算法,E 全称是 ephemeral(临时性的)。

所以,即使有个牛逼的黑客破解了某一次通信过程的私钥,其他通信过程的私钥仍然是安全的,因为每个通信过程的私钥都是没有任何关系的,都是独立的,这样就保证了「前向安全」。

ECDHE 算法

DHE 算法由于计算性能不佳,因为需要做大量的乘法,为了提升 DHE 算法的性能,所以就出现了现在广泛用于密钥交换算法 —— ECDHE 算法。

ECDHE 算法是在 DHE 算法的基础上利用了 ECC 椭圆曲线特性,可以用更少的计算量计算出公钥,以及最终的会话密钥。

小红和小明使用 ECDHE 密钥交换算法的过程:

双方事先确定好使用哪种椭圆曲线,和曲线上的基点 G,这两个参数都是公开的;
双方各自随机生成一个随机数作为私钥d,并与基点 G相乘得到公钥Q(Q = dG),此时小红的公私钥为 Q1 和 d1,小明的公私钥为 Q2 和 d2;
双方交换各自的公钥,最后小红计算点(x1,y1) = d1Q2,小明计算点(x2,y2) = d2Q1,由于椭圆曲线上是可以满足乘法交换和结合律,所以d1Q2 = d1d2G = d2d1G = d2Q1,因此双方的 x 坐标是一样的,所以它是共享密钥,也就是会话密钥。
这个过程中,双方的私钥都是随机、临时生成的,都是不公开的,即使根据公开的信息(椭圆曲线、公钥、基点 G)也是很难计算出椭圆曲线上的离散对数(私钥)。

ECDHE 握手过程

知道了 ECDHE 算法基本原理后,我们就结合实际的情况来看看。

我用 Wireshark 工具抓了用 ECDHE 密钥协商算法的 TSL 握手过程,可以看到是四次握手:

细心的小伙伴应该发现了,使用了 ECDHE,在 TLS 第四次握手前,客户端就已经发送了加密的 HTTP 数据,而对于 RSA 握手过程,必须要完成 TLS 四次握手,才能传输应用数据。

所以,ECDHE 相比 RSA 握手过程省去了一个消息往返的时间,这个有点「抢跑」的意思,它被称为是「TLS False Start」,跟「TCP Fast Open」有点像,都是在还没连接完全建立前,就发送了应用数据,这样便提高了传输的效率。

接下来,分析每一个 ECDHE 握手过程。

TLS 第一次握手

客户端首先会发一个「Client Hello」消息,消息里面有客户端使用的 TLS 版本号、支持的密码套件列表,以及生成的随机数(Client Random)。

TLS 第二次握手

服务端收到客户端的「打招呼」,同样也要回礼,会返回「Server Hello」消息,消息面有服务器确认的 TLS 版本号,也给出了一个随机数(Server Random),然后从客户端的密码套件列表选择了一个合适的密码套件。

不过,这次选择的密码套件就和 RSA 不一样了,我们来分析一下这次的密码套件的意思。

「 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384」

密钥协商算法使用 ECDHE;
签名算法使用 RSA;
握手后的通信使用 AES 对称算法,密钥长度 256 位,分组模式是 GCM;
摘要算法使用 SHA384;
接着,服务端为了证明自己的身份,发送「Certificate」消息,会把证书也发给客户端。

这一步就和 RSA 握手过程有很大的区别了,因为服务端选择了 ECDHE 密钥协商算法,所以会在发送完证书后,发送「Server Key Exchange」消息。

这个过程服务器做了三件事:

选择了名为 x25519 的椭圆曲线,选好了椭圆曲线相当于椭圆曲线基点 G 也定好了,这些都会公开给客户端;
生成随机数作为服务端椭圆曲线的私钥,保留到本地;
根据基点 G 和私钥计算出服务端的椭圆曲线公钥,这个会公开给客户端。
为了保证这个椭圆曲线的公钥不被第三方篡改,服务端会用 RSA 签名算法给服务端的椭圆曲线公钥做个签名。

随后,就是「Server Hello Done」消息,服务端跟客户端表明:“这些就是我提供的信息,打招呼完毕”。

至此,TLS 两次握手就已经完成了,目前客户端和服务端通过明文共享了这几个信息:Client Random、Server Random、使用的椭圆曲线、椭圆曲线基点 G、服务端椭圆曲线的公钥,这几个信息很重要,是后续生成会话密钥的材料。

TLS 第三次握手

客户端收到了服务端的证书后,自然要校验证书是否合法,如果证书合法,那么服务端到身份就是没问题的。校验证书的过程会走证书链逐级验证,确认证书的真实性,再用证书的公钥验证签名,这样就能确认服务端的身份了,确认无误后,就可以继续往下走。

客户端会生成一个随机数作为客户端椭圆曲线的私钥,然后再根据服务端前面给的信息,生成客户端的椭圆曲线公钥,然后用「Client Key Exchange」消息发给服务端。

至此,双方都有对方的椭圆曲线公钥、自己的椭圆曲线私钥、椭圆曲线基点 G。于是,双方都就计算出点(x,y),其中 x 坐标值双方都是一样的,前面说 ECDHE 算法时候,说 x 是会话密钥,但实际应用中,x 还不是最终的会话密钥。

还记得 TLS 握手阶段,客户端和服务端都会生成了一个随机数传递给对方吗?

最终的会话密钥,就是用「客户端随机数 + 服务端随机数 + x(ECDHE 算法算出的共享密钥) 」三个材料生成的。

之所以这么麻烦,是因为 TLS 设计者不信任客户端或服务器「伪随机数」的可靠性,为了保证真正的完全随机,把三个不可靠的随机数混合起来,那么「随机」的程度就非常高了,足够让黑客计算不出最终的会话密钥,安全性更高。

算好会话密钥后,客户端会发一个「Change Cipher Spec」消息,告诉服务端后续改用对称算法加密通信。

接着,客户端会发「Encrypted Handshake Message」消息,把之前发送的数据做一个摘要,再用对称密钥加密一下,让服务端做个验证,验证下本次生成的对称密钥是否可以正常使用。

TLS 第四次握手

最后,服务端也会有一个同样的操作,发「Change Cipher Spec」和「Encrypted Handshake Message」消息,如果双方都验证加密和解密没问题,那么握手正式完成。于是,就可以正常收发加密的 HTTP 请求和响应了。

总结

RSA 和 ECDHE 握手过程的区别:
RSA 密钥协商算法「不支持」前向保密,ECDHE 密钥协商算法「支持」前向保密;
使用了 RSA 密钥协商算法,TLS 完成四次握手后,才能进行应用数据传输,而对于 ECDHE 算法,客户端可以不用等服务端的最后一次 TLS 握手,就可以提前发出加密的 HTTP 数据,节省了一个消息的往返时间(这个是 RFC 文档规定的,具体原因文档没有说明,所以这点我也不太明白);
使用 ECDHE, 在 TLS 第 2 次握手中,会出现服务器端发出的「Server Key Exchange」消息,而 RSA 握手过程没有该消息;

RIPv2 基础配置

下面的拓扑图中有三台路由器,我们在路由器上部署 RIPv2 ,让网络中的各个网段能够实现互通。

RT1 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
<Huawei>system-view
[Huawei]sysname RT1
[RT1]interface GigabitEthernet 0/0/0
[RT1-GigabitEthernet0/0/0]ip address 192.168.1.1 30
[RT1-GigabitEthernet0/0/0]quit
[RT1]interface GigabitEthernet 0/0/1
[RT1-GigabitEthernet0/0/1]ip address 172.16.1.254 24
[RT1-GigabitEthernet0/0/1]quit
[RT1]
[RT1]rip 1
[RT1-rip-1]version 2
[RT1-rip-1]network 192.168.1.0
[RT1-rip-1]network 172.16.0.0

配置说明:

  • rip 1:数字 1 表示 RIP 的进程 ID。如果不配置,系统会自动生成一个。一个设备运行不同 RIP 进程,使用进程 ID 区分,且相互独立。
  • version 2:配置 RIP 的版本,这里配置的是 RIPv2。
  • network 192.168.1.0network命令用于网段的激活。
  • network 172.16.0.0:需要注意的是network命令指定的必须是主类网络地址,而不是子网地址。如果使用network 172.16.1.0命令,那么系统会报错,因为172.16.1.0是一个子网地址,而不是主类地址。

三层交换机

三层交换机是在二层交换机的基础上,增加了路由选择功能的网络设备,能够基于 ASIC 和 FPGA 实现网络功能和转发分组。

二层交换机能够基于数据链路层的 MAC 地址,进行数据帧或 VLAN 的传输功能。三层交换机能够基于网络层的 IP 地址,实现路由选择以及分组过滤等功能。

二层交换机通过使用 VLAN 分隔广播域,位于同一个 VLAN 下的终端才能进行数据帧交互。对于不同 VLAN 的终端有通信需求时,就必须使用路由功能,也就是需要额外添加路由器。

二层交换机和路由器组合使用,才能完成跨 VLAN 的通信,但使用三层交换机就不需要其它网络设备,能够直接完成不同 VLAN 之间的通信。

现在,内部网络核心交换机都是使用三层交换机。三层交换机用于由以太网构成的 Intranet 内部转发分组,而路由器作为连接互联网和 Intranet 内网之间的网关来使用。

三层交换机和路由器的不同

三层交换机一般只支持以太网的数据链路层协议和 IP 网络的网络层协议。

名称 三层交换机 路由器
硬件 箱式、框式 桌面式、箱式、框式
数据帧处理 基于ASCI的硬件处理 基于CPU的软件处理
性能 线速处理 比三层交换机慢
接口 以太网(RJ-45、光收发器) 以太网(RJ-45、光收发器)、串口、ISDN、ATM、SDH等
不支持的协议、功能 拨号接入(PPP、PPoE)高QoS、NAT、VPN、状态检测、高安全功能、VoIP等 STP/RSTP、LAN tranking、私有VLAN等

路由器的物理层和数据链路层除了 IEEE 802 标准以外,还支持其它各种协议,包括 ATM、SDH、串口等。网络层和传输层也一样,支持 TCP/IP 协议簇以外的协议簇,比如 IPX 、AppleTalk 等。这些功能都是由运行在 CPU 上的软件来完成,对比三层交换机,速度会慢不少,但是也有很多功能必须由路由器 CPU 来处理,比如远程接入、安全功能等。

三层交换机的架构

三层交换机的构成要素有:控制平面、数据平面、背板和物理接口。高端路由器和防火墙也是同样的架构。三层交换机把硬件设备内部分成两个区域,即以路由选择、管理功能为主的控制平面和以数据转发功能为主的数据平面,从而实现高速转发分组的系统架构。

硬件构成 说明
控制平面 通过基于CPU的软件处理进行硬件整体控制。负责操作系统管理、管理员用户界面、路由选择协议处理等工作
数据平面 通过基于ASIC、FPGA、网络处理器的硬件处理来进行实际的数据传输。在二层上完成MAC数据帧传输、在三层上完成IP分组传输。在传输时也会进行必要的访问控制列表和QoS相关的处理
背板 完成物理接口之间的数据传输。背板有三种方式:共享总线方式、共享内存方式、纵横通路方式。
物理接口 与其它硬件之间进行数据帧收发。在三层交换机中使用RJ-45或光收发器接头。

当硬件内部结构分为控制平面和数据平面时,分组的传输需要使用 FIB(转发信息库)和邻接表的信息。这种利用 FIB 和邻接表信息的 IP 分组传输方式叫做特快转发。

表项 说明
FIB
Forwarding Information Base
基于控制面板上路由选择表的信息在数据平面上生成的、由当前有效的目的地址子网、下一跳、输出接口的组合等信息构成的表项
邻接表
adjacency table
基于控制面板上 ARP 表的信息在数据平面上生成的、由当前有效目的地主机和输出接口对等信息构成的表项

路由器使用 CPU 完成分组转发,而三层交换机使用 ASCI 代替 CPU,分组的转发更快。

三层交换机将 FIB 和邻接表合并成一个表项,这个表项叫做 FDB(转发数据库),注册在内存中并通过硬件处理完成高速检索。

多层交换

除了二层交换机之外,三层以上功能的交换机统称为多层交换机。

拥有 IP 路由选择等网络功能、能够通过访问控制列表来对传输层的 TCP 端口编号进行访问控制的三层交换机,也叫做四层交换机。

能够支持到 TCP 层级访问控制的交换机叫做四层交换机。能够基于 HTTP 和 HTTPS 这类应用层参数进行负载均衡等操作,这类交换机叫做七层交换机。

有些厂家将处理到应用层的网络设备和路由器区分开来,作为不同类型的产品。但所谓的多层交换机,也就是基于 ASIC 和 FPGA 的硬件处理,高速进行各层业务处理的网络设备。

负载均衡设备

多个客户端同时连接一台服务器,可能导致服务器的处理能力超过负载。如果使用多台提供相同服务的服务器,通过使用负载均衡设备,就可以将客户端的请求分散到各个服务器进行处理。

负载均衡设备可以是专用设备,也可以是在服务器上运行的应用程序。专用设备会有以太网接口,可以说是多层交换机的一种。也存在拥有负载均衡功能的路由器。

负载均衡设备一般会分配虚拟 IP 地址,所有客户端的请求是通过虚拟 IP 地址完成的,通过负载均衡算法将客户端的请求转发到服务器的实际 IP 地址上。

负载均衡设备作用

使用负载均衡设备可以提高扩展性和可靠性。

提高扩展性 在服务器群处理能力不足时,负载均衡设备能够随时添加一台物理服务器,提高虚拟服务器的性能
提高可靠性 即使服务器群(即虚拟服务器)中某台服务器发生故障,虚拟服务器也会继续提供服务,因为客户端访问的是虚拟 IP 地址,其它物理服务器能够继续处理业务,确保服务不间断

负载均衡设备不仅适用于服务器,防火墙或代理服务器这种安全设备也可以使用负载均衡设备。

负载均衡算法类型

SSL 加速

SSL 加速是负载均衡专用设备的一项功能,执行这个功能的内部装置叫做 SSL 加速器。

在服务器进行 SSL 通信时,对传输的数据进行加密解密操作需要执行相当复杂的计算,这会导致服务器 CPU 的处理负载进一步加大。与不执行加密解密的 HTTP 通信对比,HTTPS 的处理负载是 HTTP 的 10 倍。

这时,通过使用 SSL 加速器对客户端的 HTTPS 请求进行解密,并转换成 HTTP 请求后再转发到实际的服务器上,这样就可以降低服务器 CPU 的处理负载。

这样一来,整个系统在提高服务器响应速度的同时,还能减少服务器的数量,在单位时间内能够转发更多 Web 服务内容。

三层交换机分类

根据形状和用途分类

和二层交换机的分类一样。

根据性能分类

根据三层交换机的背板容量,可分为高端交换机、中端交换机和低端交换机。

高端三层交换机

框式三层交换机由路由引擎、交换结构、线卡模块、风扇模块和电源模块组成,一般作为企业的核心交换机用在数据中心。

为了提高交换机的可靠性,除了线卡模块之外,其余模块都提供了冗余结构。电源或风扇模块通常采用 1+N 或 N+N 冗余结构,路由引擎通常采用 1+1 的冗余结构。三层交换机一般通过多台设备堆叠构成三层冗余结构,来提高整个系统的可用性。

中端三层交换机

中端三层交换机一般是箱式交换机或最大插槽数为 4 的框式交换机,用于企业核心交换机和接入交换机进行汇聚交换。

低端三层交换机

低端三层交换机一般为箱式交换机或桌面式交换机,作为企业的接入交换机使用,设备通常有 24 端口或 48 端口。有些作为 IP 电话或无线 LAN 的访问接入点,还能直接使用以太网的电源供电(PoE)。

三层交换机功能

尽管各个厂家的三层交换机提供的功能不同,但是这些功能大致有几个类别:认证类、管理类、路由选择协议、QoS、IP 隧道、VLAN、STP 等。

在三层交换机中,对分组进行管理的功能是由 CPU(软件)直接处理的。用户直接的通信,是由 ASIC(硬件)处理实现分组的高速转发的。

VLAN

由一台或几台集线器组成的一个广播域可以称为是一个扁平网络。相互连接的终端会接收网络发来的所有广播帧。随着连接终端数量的增加,广播数量也会增加,网络状况也就越混杂。

这种情况下,需要采用 VLAN(Virtual Lan)技术把整个扁平网络进行逻辑分段。一个 VLAN 对应一个广播域,不同 VLAN 的广播域互相隔离,因此能够控制广播域内的广播通信规模。

交换机通过设置,可以轻易的修改物理端口的属性,让这个物理端口加入到某一个 VLAN 之中,而不需要改变对应的物理线路。

VLAN 之间的通信需要使用路由选择,不借助路由器或三层交换机就无法与不同 VLAN 的终端进行通信,因此安全性也有了保障。

基于端口的 VLAN

基于端口的 VLAN 是在交换机的端口上设置 VLAN ID,拥有相同 VLAN ID 的多个端口构成一个 VLAN。通常交换机在初始状态下,所有端口默认 VLAN ID = 1(即 VLAN 1),可以对任意一个端口的 VLAN ID 进行设置。比如把修改某一个端口配置为 VLAN ID = 2,那这个端口就属于 VLAN 2。

标签 VLAN

当 VLAN 需要跨越多个交换机时,会使用中继端口(trunk port)的标签 VLAN(tag VLAN)。tag VLAN 通过中继端口完成数据帧的接收和发送,其中数据帧需要添加 4 字节 IEEE 802.1Q 定义的头部信息(即 VLAN 标签信息)。为数据帧添加标签的过程叫做tagging。当tagging完成后,数据帧的最大长度从 1518 字节变成 1522 字节,其中有12bit的 VLAN ID 信息,也就是说,最多支持的 VLAN 数是 4096 个。

在以太网中,数据帧中 TPID 的值是0x8100。如果源地址后面的值不是0x8100,那么就不是 TPID 信息,而是识别成“长度/类型”。当“长度/类型”的值为0x05DC(十进制数为 1500) 以下时,表示数据帧的长度;在0x0600以上时,表示数据帧的类型。数据帧类型的值分别是:IPv4 是0x0800,ARP 是0x0806、IPv6 是0x86DD等。

不支持 IEEE 802.1Q 的交换机,由于无法识别 TPID,会将0x8100视为数据帧类型,但是不存在0x8100类型的数据帧,交换机会作为错误帧直接丢弃。

IEEE 802.1Q 还定义了一个字段:TCI,TCI 可以分为 3 个子数据域:PCP、CFI 和 VID。

本征 VLAN

本征 VLAN(native VLAN)用于中继端口(trunk port)。如果数据帧在进入trunk port前,是没有标记的,那么trunk port会给它打上native VLAN的标记,这个数据帧就以native VLAN的身份传输。如果数据帧在进入trunk前,已经打上标记了,且trunk port允许这个 VLAN ID 通过,这个数据帧就通过。trunk port不允许通过的 VLAN 数据帧会直接丢弃。交换机默认使用 VLAN ID 为 1 的 VLAN 作为native VLANnative VLAN是可以自定义的,通常是使用 VLAN 1 以外的 VLAN 作为本征native VLAN,作为管理 VLAN。

中继端口

使用标签 VLAN(tag VLAN)向其它交换机传递 VLAN ID 时,首先设置中继端口(trunk port)。trunk port能够属于多个 VLAN,与其它交换机进行多个 VLAN 的数据帧收发通信。两台交换机trunk port之间的链路叫做中继链路(trunk link)。

trunk porttrunk link对应的,是接入端口(access port)和接入链路(access link)这两个概念。access port只属于一个 VLAN,access link也仅传输一个 VLAN 数据帧。

私有 VLAN

私有 VLAN(Private VLAN)也叫做 PVLAN,是指在 VLAN 内部再构建一层 VLAN 的功能,也叫做多层 VLAN。

PVLAN 能够进一步分割广播域,削减 VLAN 内部的广播流量并保障通信的安全性。酒店、公寓等场所使用这个功能,能够控制服务器或网关与终端的连接,让不同终端之间无法相互通信。

PVLAN 由主 VLAN(Primary VLAN)和从 VLAN(Secondary VLAN)组成,从 VLAN 与 1 个主 VLAN 关联。

使用 PVLAN 的物理端口模式:

静态 VLAN 和动态 VLAN

通过输入交换机命令,将一个交换机端口固定分配给某个 VLAN,这种 VLAN 划分方式叫做静态 VLAN。

相对的,根据连接端口的终端或用户信息自动分配某个 VLAN 的方式叫做动态 VLAN。具体来说,就是交换机根据终端的 MAC 地址来分配,或者基于 802.1X 的认证来决定端口属于哪个 VLAN。在动态 VLAN 中,无论终端与哪台交换机连接,都会获取固定的同一个 VLAN。

通过交换机内部的数据库,可以实现基于 MAC 地址的认证,但大部分情况下,动态 VLAN 的实现都是使用 RADIUS 服务器。

VLAN 之间的互通

二层交换机

在二层交换机上设置多个 VLAN 后,单台交换机内,数据帧只能在相同 VLAN 内转发,不能在不同 VLAN 之间转发。

当需要在多个 VLAN 之间转发数据时,一般会使用trunk link连接路由器,通过路由器进行 VLAN 之间的路由选择。

三层交换机

三层交换机能够在交换机内部直接完成 VLAN 之间的路由选择。

大小写转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// type 1-全大写 2-全小写 3-首字母大写
const turnCase = (str, type) => {
switch (type) {
case 1:
return str.toUpperCase()
case 2:
return str.toLowerCase()
case 3:
return str[0].toUpperCase() + str.substring(1).toLowerCase()
default:
return str
}
}

turnCase('vue', 1) // VUE
turnCase('REACT', 2) // react
turnCase('vue', 3) // Vue

getURLParameters

返回包含当前 URL 参数的对象。

1
2
3
4
5
6
7
8
const getURLParameters = url =>
(url.match(/([^?=&]+)(=([^&]*))/g) || []).reduce(
(a, v) => ((a[v.slice(0, v.indexOf('='))] = v.slice(v.indexOf('=') + 1)), a),
{}
);

getURLParameters('http://url.com/page?name=Adam&surname=Smith'); // {name: 'Adam', surname: 'Smith'}
getURLParameters('google.com'); // {}

开启全屏

1
2
3
4
5
6
7
8
9
10
11
export const launchFullscreen = (element) => {
if (element.requestFullscreen) {
element.requestFullscreen()
} else if (element.mozRequestFullScreen) {
element.mozRequestFullScreen()
} else if (element.msRequestFullscreen) {
element.msRequestFullscreen()
} else if (element.webkitRequestFullscreen) {
element.webkitRequestFullScreen()
}
}

关闭全屏

1
2
3
4
5
6
7
8
9
10
11
export const exitFullscreen = () => {
if (document.exitFullscreen) {
document.exitFullscreen()
} else if (document.msExitFullscreen) {
document.msExitFullscreen()
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen()
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen()
}
}

解析URL参数

1
2
3
4
5
6
7
8
9
10
11
export const getSearchParams = () => {
const searchPar = new URLSearchParams(window.location.search)
const paramsObj = {}
for (const [key, value] of searchPar.entries()) {
paramsObj[key] = value
}
return paramsObj
}

// 假设目前位于 https://****com/index?id=154513&age=18;
getSearchParams(); // {id: "154513", age: "18"}

滚动到页面顶部

1
2
3
4
5
6
7
export const scrollToTop = () => {
const height = document.documentElement.scrollTop || document.body.scrollTop;
if (height > 0) {
window.requestAnimationFrame(scrollToTop);
window.scrollTo(0, height - height / 8);
}
}

滚动到元素位置

1
2
3
4
5
export const smoothScroll = element =>{
document.querySelector(element).scrollIntoView({
behavior: 'smooth'
});
};

parseCookie

解析Cookie标头字符串并返回所有cookiename-value对的对象。

  • 使用String.split(';')将键值对彼此分开。
  • 使用Array.map()String.split('=')将键与每对中的值分开。
  • 使用Array.reduce()decodeURIComponent()创建一个包含所有键值对的对象。
1
2
3
4
5
6
7
8
9
10
const parseCookie = str =>
str
.split(';')
.map(v => v.split('='))
.reduce((acc, v) => {
acc[decodeURIComponent(v[0].trim())] = decodeURIComponent(v[1].trim());
return acc;
}, {});

parseCookie('foo=bar; equation=E%3Dmc%5E2'); // { foo: 'bar', equation: 'E=mc^2' }

获取选择的文本

1
2
3
const getSelectedText = () => window.getSelection().toString()

getSelectedText(); // 'Lorem ipsum'
  • Copyrights © 2017-2023 WSQ
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信