IcingTomato's Archive A Very Simple Knowledge Archive

The Tutorial for FPGA Player - Episode 3: Sequential Logic

上期教程中介绍了组合逻辑的使用,而本期教程则要来讲讲时序逻辑。 那么当然,第一个要回答的问题就是,组合逻辑电路和时序逻辑电路有什么区别。如果重新考虑之前做过的电路,不难发现一个特点,这些电路都是给定输入,得到输出。只要输入是一定的,输出也就是一定的。所有影响输出的因素只是输入而已。听起来很自然,没有什么问题对吧?甚至可能还觉得奇怪,如果我给定了输出,但是却不能确定输出,那不是乱套了吗?考虑这么一个需求。还是有一盏灯,有一个按钮,现在需要,按下按键,灯点亮,然后一直保持点亮,再按下按键,灯关闭,并保持关闭。不难看出,单独给定输入状态(按键按下或者没有按下)根本没法知道灯是开着还是关着,也就是输出不单单取决于当前输入,还取决于当前的状态。换句话说,电路有了自己的记忆。而在分析时,除了要考虑当前发生的事情,还需要考虑之前发生过的事情, 多了时间维度。这种电路被称为时序逻辑电路。 锁存器与触发器 记忆这事在单片机或者任何软件编程环境里都好说啊,无非是一个变量的事情。但是如果说回到电路角度,应该怎么解决这个问题呢?引入新的逻辑门,比如说 存储门什么的东西?其实完全不用,只要用之前在组合逻辑时用过的那些东西就可以做出能够存储状态的电路哦。原理图如下 看着有些奇怪是不是,这种电路应该怎么分析呢?因为一眼看过去似乎找不到什么输入,只有一个输出Q。不妨考虑下其中任意一条线为高或低,随后计算出来其它所有线的电平,发现没有出现逻辑上的冲突,也就是只要保持通电,这些电平就会一直保持这个状态。但是如果这时,由于外部的信号,改变了其中任意一条线的电平,所有的电平都会变化,并一直保持新的状态。所以呢,这也就是能够存储1个bit的电路了。 不过刚刚其实没有说清楚,什么叫由于外部信号改变了电平啊?怎么改变啊?是的,得有一种可靠的方式来修改状态。而这种方式不是唯一的,一种简单的方法如下: 图中有两条信号线,分别是S和R,表示置位(Set)和复位(Reset),分别可以让这个电路保存的值设置为1或者0。比如可以考虑,当前电路里面保存的数值是0,如果S线为1,就会使对应的NOR门状态发生变化 ,进而使得整个电路保存的数值变为1。当然如果本来就是1,那就什么都不会发生。 不过感觉不太对啊,根据以前使用单片机相关电路的经验,一般都是来一条数据线直接传输数据(高或者低)而不是分成S和R分别传输高或者低。毕竟S的时候不能R,R的时候不能S,弄两条线看起来就有些浪费。要实现直接输入数据而不是R、S,也很简单 现在,如果输入为高,那么S为高,R为低,输出就会被设置成高;如果输入为低,那么S为低,R为高,输出就会被设置为低。听起来很棒是不是?完全不是。输出只会复制输入的情况,结果就是变成了一条存在延迟的导线而已。如果输入“消失”,输出也会随之消失,也就没有什么“记忆”可言了。所以还需要有一条线,用来指示当前的输入是否有效,是否需要存储当前的输入。电路修改成如下: 带有写入使能的记忆电路 现在好了,有了一条使能(Enable)线,用来标记输入是否有效。当Enable为高的时候外部的数据可以进入这个记忆电路;而Enable为低的时候,无论输入信号怎么变,输出信号都不会发生变化, 就像是被锁住了一样,于是这种电路的名字,就叫做锁存器。而这种输入数据的就被称为D锁存器,前面那种输入S和R信号的则被称为SR锁存器。 确实,锁存器听起来像是非常可靠的存储设备,也确实在过去被大量使用,但是现在的数字电路设计中大多已经转向使用触发器来代替锁存器。所以这里我们也就来介绍一下触发器。 首先,锁存器存在一个“缺陷”。Enable为低的时候保存信号电平没有问题,然而Enable为高的时候,就有点意思了。Enable为高的时候,其实也就基本是图?中描述的导线的情况,输出就是对输入的简单复制。不如说,这时整个锁存器是“透明”的,输入信号可以直接穿过锁存器达到输出。而触发器,相比于锁存器,就并不是透明的,因为触发器并不依赖于电平来指示是否要写入,而是依赖于时钟的变化。触发器会在时钟从低到高变化(或者从高到低变化,取决于具体使用的触发器)的瞬间,储存输入的数据,并且在其余时间保持这个数值。这里就简单放一下D触发器的原理图,使用和之前同样的分析方法就可以理解其工作原理。如果不能理解,也没有关系吧,会用就行了。 当然,平时用的时候如果都画全这么一个原理图,一来太累,二来也不清晰,所以锁存器和触发器都可以画成单独的符号,需要的时候使用这个符号即可: 总结一下,这里介绍了两种存储元件,一种叫锁存器,一种叫触发器。对于锁存器来说,重要的是电平状态,高电平存储,低电平保持。而对于触发器来说,重要的是时钟边沿,时钟从低到高的瞬间存储,其它情况保持。注:当然也存在极性相反的原件,如低电平存储高电平保持的锁存器和始终从高到低瞬间存储的触发器。另外“触发器”这一名词曾经可以用来指代各种存储元件,包括现在讨论的锁存器也曾经是触发器的一种。而现在,通常而言触发器特指边沿触发的触发器,电平触发的被称为锁存器。其实锁存器和触发器的类型并不止今天介绍的这几种,但是为了保持简洁,这里只讲和我们的目标——FPGA GameBoy最相关的,其它的就不做介绍了。 实例1 灯 其实到这里位置,时序电路需要的新的组件已经介绍完了。确实,所有涉及的只不过是锁存器和触发器而已,而这两者本身也无非只是之前的基础逻辑门的组合罢了。然而,虽然说起来只不过是一些逻辑门的组合罢了,有了这两种元件之后,逻辑电路所能实现的功能瞬间强大了很多,电路的设计和分析难度也随之上升了一个台阶。前面的那个实例,还好说,只涉及一个输入和一个输出,本身比较简单,简单凑一凑设计就出来了。而如果要设计更为复杂的东西,还是束手无策。而我这里能做的,就是提供更多例子,帮助大家理解时序逻辑电路常见的“套路”。而这个实例,也就是开始的,按一下开,再按一下关闭的电灯。 首先,这个电路有一个输出,一个输入,分别是电灯的输出和按键的输入。简单考虑下不难注意到,要求里面提到了保持电灯的状态,那也就是需要一个触发器,用来记忆并保持电灯的状态,而电灯的输出直接接入到触发器的输出上。而每当按键按下的时候,电灯的状态可以发生变化,也就是触发器需要在按键按下的时候存储新的状态,那么按键也就是触发器的时钟信号了。新的状态,按照要求就是一个和当前的状态不同的状态:如果现在开着就关掉,如果现在关着就打开。于是新的状态生成也很简单了,当前状态加一个非门。 另外可以考虑下如果这里直接把触发器换成锁存器会发生什么事情。由于锁存器是透明的,在按下按键的时候,锁存器会不断试图存入输入的值,而不难发现输入的值和当前的输出相关,而透明状态下输出又会因为输入而发生变化……结果就是发生了循环,当前状态会不断在打开和关闭(1和0)之前切换,而电灯则是与之对饮进入高速闪烁状态,而最终的状态,则是放开按键时所处的状态,基本而言就是开和关之间随机一种。可以说整个是一团糟了,没有办法使用。透明带来的问题也就是在使用锁存器进行设计时必须考虑的。我们这里之后的设计,都将全部使用触发器完成,不使用锁存器。 这里顺便附上这个电路的Verilog实现,各位有兴趣的话可以在自己的实验平台上做这个实验。但是,由于按键抖动的原因(各位如果玩过单片机、Arduino一类的,应该已经很明白按键抖动是什么了吧?),可能仍然会导致按键按下状态变化多次。还是再说明一次,这里的Verilog代码仅仅只是用来实验用的,看不懂代码含义也没有关系,可以自己试着摸索摸索,不然的话之后也会有具体的介绍。 以上电路对应的Verilog代码 module light( input key, output reg light); always@(posedge key) begin light <= ~light; end endmodule 实例2 计数器 计数器,自然也有多种实现方法,不同的实现方法实现出来的计数器也有不同的特性。比如这里,希望能够实现一个不断自增的计数器,也就是数值不断+1。不知道大家还记不记得上一期出现过的加法器呢?它可以实现二进制数的相加。如果我们修改这个加法器,让它永远执行+1的操作,并且把结果喂给加法器的输入,即可实现自增的功能。不过,直接连接肯定是不行的,输出的同时输入也在发生变化,也就是像之前说的那样出现了循环,最后做出的电路也就没有什么太大的意义。需要做的,也就是用触发器把加法器的输入和输出隔离开来,确保不会立即进入输入。而触发器的时钟信号,也就是让触发器保存当前输入的信号,在这里也就真的成了控制计数器速度的时钟。比如说时钟信号是1Hz,也就是每秒会有1个电平从低到高的变化(上升沿),那么触发器就会更新一次,也就是整个计数器按照1Hz的速度向上计数的效果了。 注意到这里的加法器十分简单,简单到只剩下了两个门。上一期为了实现1位数的加法也永乐两个门把?现在只用了两个门却实现了2位数的加法,怎么做到的呢?原因也很简单,因为这里不需要考虑进位,而且也是固定的+1操作,其实整个电路就可以不用从加法器的角度去考虑,而是直接考虑这是一个输入当前计数值,输出下一个计数值的组合电路,其真值表如下: 按照要求,输出永远是输入+1,而实现这个组合逻辑电路,也就只需要两个门就足够了。按照之前的思路,把组合逻辑部分的输入和输出用触发器隔开,也就实现了需要的效果。 这里同样附上Verilog代码 module counter( input clk, output reg [2:0] count); always@(posedge clk) begin count <= count + 1; end endmodule 结语 本期简单向大家介绍了时序电路的概念,介绍了时序电路的基本组成部分:触发器和寄存器,并展示了时序电路一个基本的应用:计数器。这期的内容可能也是和以前一样,并不那么容易理解。一下子没理解没关系,再读一遍。一旦明白了,一切就很简单了。而下一期我们则将要介绍数字电路基础中非常重要的一个部分:状态机。状态机是时序电路设计中非常常用的一个“套路”,提供了一种系统的设计时序逻辑电路的方法,而不是只是像现在一样想办法凑。同时它也是本系列教程中,数字电路部分的最后一个主题。在那之后,就可以开始FPGA相关的内容啦。 也许有观众依然不理解,最终要用的就是FPGA,为什么还要费那么大力气来介绍数字电路呢?毕竟FPGA已经把很多东西都抽象了, 开发时使用的也是编程语言,而不是画逻辑门。确实没错,开发FPGA不需要任何我们现在在画的逻辑门。然而,如果没有现在的这些铺垫,就很难理解FPGA最终编程时究竟是在做什么。如果具象的东西都没理解,势必会给抽象表达的理解带来更大的困难。不过为了避免大家学的东西太多头晕,这里我只介绍了和今后FPGA最直接相关的概念,一些不太那么相关的也就没有介绍。各位如果有兴趣自然可以找更多书来看。但是反过来说,这里所有介绍的,都是重中之重,也请各位希望继续跟着玩的,务必理解这些内容。

The Tutorial for FPGA Player - Episode 2: Combinatorial Logic

在进行了两期的背景介绍后,本期也是终于是进入正题了。如果你之前有了解过数字电路,大概就听说过数字电路大致可以分为组合逻辑电路和时序逻辑电路,而大部分的电路都是这两者的结合。本期所要介绍的也就是这其中的前者:组合逻辑。顺便,上期之后,大概部分玩家购买的74芯片或者FPGA开发板也已经到货了吧?本期也会附带关于如何用74或者FPGA实现这些实验的说明。 第一个例子还是从第一期举过的例子开始好了。想要设计一个电路,里面有两个开关和一个灯泡,希望实现两个开关任意一个打开的时候灯泡点亮。当然,一个显而易见的解决方案就是把两个开关并联,如下图所示: 但是如果我们用像一般考虑单片机电路一样的思路去考虑,那么就可以把两个开关看作是两个输入口,而灯泡是一个输出口,那么这个电路就会变成这样: 而这个盒子里面所包括的就是需要实现的电路。这个电路可以是一块74,也可以是一块单片机,或者只是像图1一样简单的连接起来而已。这里就来考虑下,如果要用单片机来实现要怎么做吧。很简单,一句if语句的事情,直接翻译要求, **if ((a == 1)   (b == 1)) c = 1; else c = 0;** 可以看见逻辑运算已经出现了,输入1为高或者输入2为高时,输出1为高,否则输出1为低,这样就是这个简单例子的逻辑。另外等于1 可以省略,就变成了 **if (a   b) c = 1; else c = 0;** ,再考虑到因为逻辑运算的结果本身就是1或者0,这里甚至不需要if,只需要 **c = (a   b)** 即可。 只有一个简单的 **“或”运算(   , OR)** 的关系,如果要用图示描述出来的话: 中间这个像箭头一样的东西也就是“或”运算的符号了。除了或之外,当然还有其它的符号,以下是6个最常用的逻辑运算的符号: 现在再来一个例子吧,假设还是两个输入,现在希望有且仅有一个开关打开(也就是需要有一个开关打开,但是不能两个都打开)的时候让灯泡点亮,那要怎么实现呢?一种实现思路是,如果第一个开关打开且第二个开关没有打开,或者第二个开关打开且第一个开关没有打开,这两种情况下让灯泡点亮,否则就熄灭。用C语言的逻辑表达式表述的话大致是 **c = (((a == 1)&&(b == 0))   ((b == 1)&&(a == 0)));** 如果试图用和第一个例子一样的图示来表示逻辑关系的话,就会遇到问题:这里出现了一些新的运算。比如 “与”运算(&&, AND) ,这个在上面的常用表里面有。而 “等于” 运算虽然第一个例子里也出现了,但是被省略掉了。然而这次除了等于1之外还出现了等于0的运算,这就没法省略了。那么有没有等于运算的符号呢?回答是肯定的,然而等于并不是一个基础运算,这里并不使用。一个代替等于0运算的方法就是,先做否运算,再判断等于1,即把 (a == 0) 改写成 (!a == 1) ,这个写法可能有些奇怪,毕竟一般不会这么写,但是确实是可行的。整个逻辑运算也就变成了 **c =(a&&(!b))   (b&&(!a))** ,这样就能把图画出来了: 然而实现这个逻辑的方法并不是唯一的,如果调转一下思路,这个逻辑也可以表达为:如果有两个开关有任意一个打开,且两个开关有任意一个没有打开,那就能说明有且只有一个开关被打开了。表达式为 **c = (a   b)&&((~a)   (~b))** ,示意图如下: 虽然确实换了一种方法,但是似乎使用的逻辑门数量并没有变少啊?确实,但是如果简单看下的话,这个里面的两个非门和一个或门可以合并成一个与非(’NAND’)门,或者,其实整个逻辑可以只用一个门来完成: 有且只有一个输入为高时输出高,这其实就是 异或(XOR)门 的功能了。举上面这个例子是为了说明一个道理:使用逻辑表达式或者原理图示来说明一个逻辑,其实可能并不是最优的方案。同一个逻辑可能存在多种不同的表达方式。为了解决这个问题,可以使用 真值表(Truth table) 来表示逻辑: a(输入) b(输入) c(输出) 0 0 0 0 1 1 1 0 1 1 1 0 对于上面这个逻辑,这个真值表是唯一的。看表的方法也很简单,左边两列a和b在这里是输入,右边一列c在这里是输出,比如要知道a打开(1) b没有打开(0)时c的输出,只要找到 1 0这一行,就能看见c的输出是1。如果两者都为打开(1),那么就是最后一行,c为0。 这里稍微展开一下,没有理解没有关系,不影响玩。其实我上面三种画法,第一种和第二种其实分别对应了两种常见表示方法,一种叫 SOP(Sum of Product,乘积之和) ,另外一种叫 POS(Product of Sum,和之乘积) 。这两种的取名方式是因为,通常的逻辑算式中,与预算并非用&&表示,而是用乘法的点 (·) 而或运算也并非用 **   ** ,而是用加法的加号 (+) ,非运算是在式子上面画横线。于是图4和图5的式子写法分别是: 一个是两个乘法的结果加起来,也就是乘积之和SOP,另外一个是两个加法的结果乘起来,自然就是和之乘积POS了。而且这里其实都是和真值表一一对应的。式1里面的两个乘积项其实就对应了真值表里面输出为1的两行,而式2里面的每个加法项分别也就对应了真值表里面输出为0的两行。想要用逻辑表达任意一个真值表,只需要把每一个输出为1的行用乘法 (与运算,AND,&&) 表示出来,再全部加起来 **(或运算,OR,   )** 即可;或者也可以把输出为0的行用加法加起来,最后全部乘起来。这也分别就是POS和SOP了。当然通常这样表达出来的结果并非最优,要获得优化的结果还需要使用卡诺图等等。另外用乘法和加法来表示 && 和 **   ** 也不仅仅只是写着方便,这两个运算和一般计算数字的加法和乘法存在相似性,部分数字运算规则也可以应用到逻辑算式中,用于化简逻辑代数式,或者进行其它运算等等。由于本教程只是教大家玩FPGA,而不是教大家学离散数学或者逻辑电路,这些内容也就不展开讲了。感兴趣的可以自己去搜索一下。 所以,总结一下上面的内容,对于这样的,输出信号只取决于当前输入信号的(即只需要知道当前的输入信号就可以决定输出信号的)这类电路,就称为组合逻辑数字电路。或者也可以理解为,判断的内容只有输入信号的一条if语句。不知道各位也没有看明白呢?如果是第一次接触可能确实比较费解,有必要的话建议重新再读一遍。如果觉得没问题了,来做个实例吧,也能帮助加深理解。 实例:设计一个电路,可以计算1位二进制数的加法。 这个听起来完全不像之前的例子啊,之前都是开关,灯啊什么的,这里怎么突然就开始做算术了呢?其实并没有什么区别。1位二进制数加法,也就是要把两个1位的二进制数加起来,两个数分别可以是0或者1,换句话说,也就是两个开关。而1位二进制数加法可能有下面四种情况: 0+0=0 0+1=1 1+0=1 1+1=10 如果考虑把结果像之前一样也接上灯泡的话,那就和之前的事情非常接近了,只不过为了表示两位数字的输出现在需要两盏灯。不过,两盏灯又需要怎么设计呢……?首先还是先画真值表吧,假定两个输入分别是a和b,输出则是c和d: a(输入) b(输入) c(输出) d(输出) 0 0 0 0 0 1 0 1 1 0 0 1 1 1 1 0 不难发现这个真值表看起来和上面四种情况的算式很像,只是加上了框框,并且把十位的零都写出来了而已。确实,真值表就是这样画的,列举所有可能的情况,在表里面写上每种情况的输入和输出。 现在剩下的问题就是怎么画成之前的电路图了。其实也不难做,单独考虑每个输出对应的逻辑,最后画在一起即可。比如这里有c和d两个输出,先考虑c的情况。对于c而言,只有当a和b都为1的时候c才是1,或者说只有当两个开关都打开时灯才会点亮,其实也就是c = a&&b这么一个逻辑。而d则是之前第二个例子中相同的逻辑,只需要一个异或就能完成,表达式为d = a^b。 下一步就是具体实现这个电路了。我这里分为三个部分,74芯片,Xilinx FPGA和Intel(Altera) FPGA。各位可以根据自己有的芯片来进行实验。如果没有的话,在以后也会讲如何使用仿真工具进行开发。 74芯片 如前面所说,这个电路需要两个门,一个与门一个异或门,对应的芯片分别为74HC08和74HC86。这两款芯片的内部连接方式可以通过查找对应的数据手册找到: (74HC08) (74HC86) 电路的话也并不复杂,只是把上面的示意图里面的逻辑符号替换成芯片,并增加必要的电阻罢了: 如果没有74HC86,d输出自然也可以选择像图4或者图5那样使用多个独立的门芯片来搭建,比如完全按照图4的原理图来,需要一片74HC04非门,和一片74HC32或门。 (74HC04) (74HC32) 虽然芯片只是加了一片,不过接线变得复杂了不少,这也说明在使用74芯片搭建电路时,优化还是很重要的。 Intel FPGA 接下来讲讲如何在Intel FPGA平台上完成这个实验。就像玩单片机一样,玩FPGA需要在电脑上安装开发环境,在里面编写代码,然后再通过烧录器烧录进FPGA才能完成。通常对于单片机玩家来说,这个开发环境叫做Keil或者IAR。Intel FPGA的开发环境叫做Quartus,而且Intel提供了一个免费版本的Quartus,可以用于小规模设备的开发,通常来说只要使用免费版本的就足够了。具体的软件安装过程我这里不再赘述,如果有疑问可以在网上找到很多的帮助。 打开Quartus应用程序,在欢迎页面选择New Project Wizard或者从菜单选择File - New Project Wizard打开创建新工程的向导 自己选取要保存工程的文件夹,取个名字,Project Type选择Empty Project(空工程), 添加文件的页面直接点击下一步,直到来到这个选择器件的页面: 请按照自己开发板上的芯片型号选择。这个型号印在了FPGA上,通常也可以从开发板的用户手册、原理图钟得到。如我的是MAX10系列的10M50DAF484,就在这里选择这个型号。选择完成后可以直接点Finish完成创建。 创建完工程后还需要创建主程序文件。开发FPGA使用的并不是C语言,C语言毕竟是用于开发软件的语言,开发逻辑电路(硬件)有其专门的语言,比如本教程使用的Verilog语言。不过好在Verilog的语法和C还是较为相似的,熟悉C语言的话学习Verilog语言本身并不困难(但是适应硬件开发的思路转变就不简单了)。本期并不会系统讲解Verilog的使用,只是简单使用体验一下FPGA的使用而已。新建一个文件(不是工程),在弹出的对话框中选择Verilog HDL File。 在新的文件中输入以下代码: module lesson3( input wire a, input wire b, output wire c, output wire d ); assign c = a & b; assign d = a ^ b; endmodule 保存为lesson3.v。注意第一行后的lesson3需要和文件名匹配,如果你保存为了别的文件名,比如adder.v,那么第一行也应该相应的写成module adder才行。如果没有问题的话,保存后可以点击菜单栏的蓝色右箭头尝试进行综合(类似于程序 编译 的过程),应该可以顺利通过。如果没有,请检查刚刚保存的lesson3.v是否被加入了工程并设置为了顶层文件(可以使用左侧的面板查看)。 完成后可以看见左边几个任务都是绿钩子,或者可能是黄感叹号,都表示顺利通过了。在中间的Flow Summary里面可以看到一些报告信息,比如使用的逻辑数量,一共有49760个,这里只用到了3个。 不过这样还没有完成,在上面的程序中只是说了会有a b c d四条线,但是并没有说这四条线会连接到那些引脚上。接下来我们就来进行引脚分配的工作。首先第一步当然是要确定怎么连接。我的开发板(DE0-Lite)上有几个拨动开关,我决定就把a和b连接到拨动开关上,而c和d则是连接到两个LED输出上。通过翻原理图可以得知两个开关分别连接在C10和C11上,而两个LED分别连接在A8和A9上。打开Assignment菜单中的Pin Planner 可以注意到a b c d已经出现在了里面,而且有一些自动定义的位置。按照之前得知的信息在Location一栏填入引脚。Fitter Location可以不用理会,IO Standard这里要按照开关和LED连接的块的电源电压填写,通常也可以在原理图里看到,我这里是3.3V,也就是LVCMOS33。 完成后直接关闭窗口。重新点击蓝色右箭头进行综合。值得一提的是,不少开发板厂家会把板上的引脚配置预先定义好,要使用时只需要导入一个定义文件即可,而不需要每次这样手动查原理图配置。以后也会讲解如何使用那种方式完成,本期这么做一来是为了让大家体验这个过程,二来也是方便使用不同开发板甚至是最小系统板的玩家可以应用到自己的板子上。 现在就可以把程序(位流)烧录进FPGA测试了。使用USB线连接FPGA开发板至电脑,使用Tools菜单中的Programmer打开烧录工具。注意首次使用可能需要为烧录器安装驱动程序。如果你的Programmer窗口中左上方显示No Hardware则需要打开设备管理器为USB Blaster安装驱动。驱动可以在Quartus的安装目录中找到,如C:\intelFPGA_lite\18.0\quartus\drivers\usb-blaster。 确认左上角显示USB Blaster之后就可以点击Start开始烧录了,烧录完成后应该就可以观察到效果了。 Xilinx FPGA 如果你选购的是Xilinx FPGA的开发板,也没关系,整体过程也是大同小异的。需要注意的是,Xilinx曾经使用ISE作为开发环境,现在新的芯片已转为使用Vivado开发。鉴于目前最常用入门的芯片Spartan6仍然需要使用ISE开发,这里也使用ISE进行演示。Xilinx也像Intel一样推出了免费版本的ISE,叫做ISE WebPACK,对于我们来说完全足够了。具体安装方法这里不进行赘述,只讲使用。 在开始菜单中打开Project Navigator(ISE的主程序) 点击左侧的New Project…或者是菜单中的File - New Project打开新建工程窗口。也是一样,输入工程名称,保存位置,顶层文件类型选择HDL,在属性页中选择自己的设备。 其它不用修改,完成创建。在左侧窗格的Hierarchy中右键选择New Source建立新文件 在向导中选择Verilog Module并在右边输入文件名。 这个项目中有4个信号,a b c d,分别为两个输入和两个输出,这里就这么填写。 建立文件后注意到框架已经在了,只需要在中间插入两行逻辑即可,整体程序应该和Intel FPGA中列出的相同。 assign c = a & b; assign d = a ^ b; 当然也和Intel FPGA那边一样,需要定义引脚,不过不是使用图形化的工具,而是直接编辑约束文件。和上面一样打开添加新文件的窗口,在左侧选择Implementation Constraints File,并在右边输入文件名constraints.ucf 根据原理图可知我的开发板LED连接到了H18和L18上,而两个按钮则是在AJ6和AK7上。另外需要知道所连接的块(Bank)的电压,通常也可以在原理图里找到。我的板子上按钮为3.3V而LED为2.5V。其它开发板可能是不同值,通常可能为3.3V。在新的约束文件中,根据上面得到的信息写入以下内容: NET a LOC = AJ6; NET a IOSTANDARD = LVCMOS33; NET b LOC = AK7; NET b IOSTANDARD = LVCMOS33; NET c LOC = L18; NET c IOSTANDARD = LVCMOS25; NET d LOC = H18; NET d IOSTANDARD = LVCMOS25; 其中a b c d就是四个要定义的信号的名字了。如果不确定后面的IOSTANDARD,可以参考开发板提供的参考程序中的ucf文件。编辑完成后,双击左侧任务窗口中的Generate Programming File开始综合并生成编程文件。如果没有问题的话左侧应该是三个绿钩。 在右侧可以看到综合的报告,如逻辑的使用量等等。通过USB连接开发板到电脑,使用Tools - iMPACT打开编程工具。点击左侧的Boundary Scan进入设备检测界面,在右侧右键,点击Initialize Chain检测设备。 双击FPGA设备,在弹出的窗口中选择刚刚生成的文件。注意到下面的bypass已经变成了选择的文件名。选中FPGA,在左侧的任务列表里面双击Program。一段时间后程序就应该被写入了。按下按钮可以注意到LED变化。

The Tutorial for FPGA Player - Episode 1: Logic Gate, FPGA, Development Board, Game Console

相信通过上一期的文章,大家已经对逻辑电路和单片机的区别有了一些了解,也看到了一些其它爱好者使用逻辑电路(FPGA)做的一些制作。今天要讲的则是一些更加实际的东西,关于相关芯片、开发板的购买,以及本系列实现教程的目标——游戏机——的过程概览。 74 or PLD? 也是按照之前的顺序,我们先讲74再讲CPLD/FPGA。74系列芯片虽然是很早以前就有的东西,功能也可以被可编程器件取代,但是它并没有被完全淘汰。74系列芯片仍然经常被应用在各种电路当中,也容易被买到。值得注意的是,74系列芯片也经常被称为TTL芯片,这是不准确的,74系列也有不少使用CMOS技术制造的芯片,且CMOS是最常使用的,而非TTL。另外提一点,虽然74系列芯片可以被认为是基础,但是完成本系列的教程并不一定需要购买使用74系列芯片,当然作为增加趣味是可以的。 74系列之所以被称为系列,是因为它里面有许多不同功能的芯片,组成了一个系列。基础的比如与或非门,稍微复杂一些的如触发器,再复杂一些如计数器、加法器,都有对应的芯片可以选择。玩过单片机的朋友可能比较熟悉595、245,这些也是74系列芯片的成员,这两者分别常用于扩展IO口和驱动总线。通常来说,74系列芯片使用14脚的DIP或SOP封装,实现一个制作通常也需要多片74芯片。因为根据不同制作和设计的不同,需要的芯片种类和个数也有可能非常不一样,这里可能很难做一个具体的推荐。但是为了完成一些基础的实验,通常来说会用到74HC04(非门)、74HC08(与门)、74HC32(或门)、74HC74(D触发器)和74HC47(LED译码器)。考虑到这些芯片的价格通常比较便宜(几毛钱到1块多人民币不等),可以在买元件的时候顺便带上几片。值得注意的是,由于74芯片确实可以实现和FPGA等价的逻辑功能(但是设计的时序问题可能就较难处理,通常只能实现较为低速的电路设计),为此有不少人热衷于使用74芯片来实现大型的电路,而不是FPGA。毕竟最初的目的只是为了好玩,用74来设计确实可以增加不少乐趣。以下是一个完全用74芯片制作的CPU——TD4: 感兴趣的朋友可以自己搜索来了解一下,也可以尝试把它做出来。 (使用74系列芯片制作的CPU——TD4) 接下来,来讲讲CPLD和FPGA。74虽然能做逻辑电路,但是,设计起来费力,制作起来费力,调试起来也费力。可编程逻辑器件就相对友好的多。可编程逻辑器件,最常见的也就是CPLD和FPGA。 CPLD or FPGA? 初学者常有的一个疑问就是,CPLD和FPGA有什么区别?在过去,CPLD和FPGA是两种不同的技术。虽然他们都用来实现类似的目的:实现逻辑电路,但是内部使用了不同的实现方法。比如一个重要的区别是CPLD是基于ROM而FPGA是基于RAM的,以至于CPLD可以上电就立即开始工作,而FPGA需要先从Flash载入配置到RAM。以及,CPLD内部使用了PAL结构的可编程块,FPGA内部使用基于查找表的逻辑单元按照矩阵排布,以至于CPLD的逻辑规模通常不及FPGA。不过,各大厂家都已经开始生产一些内置Flash的FPGA,然后当成CPLD来卖,所以上面讲的原理上的区别,对于今天的CPLD和FPGA而言,可能已经不一定成立了,毕竟你买到的CPLD里面可能实际上是个FPGA。 那么对于我们而言,CPLD和FPGA应该怎么选呢?我的建议是,它到底是CPLD还是FPGA其实对于我们而言不重要,重要的是它内部的资源。就像我们买单片机的时候,会看它的主频、内存容量等参数,FPGA也有这样的参数,而这个参数就是逻辑规模。通常而言这个逻辑规模使用等效4输入查找表的数量来描述,单位也就是LUT或者kLUT,不同厂家的说法存在不同,如Intel的单位是LE,Xilinx的单位是LC,但是含义是差不多的。注意等效这个词,在单片机的世界里,虽然多少KB就是多少KB,但是不同单片机的“KB”可能是不能直接比较的,同样的程序在不同的单片机上可能需要占用不同的内存空间,而同样MHz主频的单片机也可能性能存在巨大的差异。在CPLD/FPGA的世界里这个问题就更加严重了,同一个厂商不同系列的产品通常内部架构都有差异,以至于带有的资源数量很难直接用于比较,所以才有了等效量的说法。 那么,这个等效量,典型的值是多少呢?目前规模最小的CPLD具有的等效逻辑量只有约30(0.03K),而最大的可以到达约2.8M(2800K),可以说差距是十分的悬殊了。而对于爱好者,通常常用的规模范围,是在数百到几十K之间,太少了通常做不了什么事情,太多了其实也用不过来。举几个例子的话,通常而言入门的逻辑电路实验不会使用超过10个查找表,而实现一个简单的4位CPU通常需要100个查找表,实现一个典型的8位CPU需要500到5000个查找表,而这次教程的目标——实现一个完整的GameBoy,需要大约10000(10K)个查找表。所以如果要购买开发板实现这次的完整的目标,我也推荐购买至少达到10K逻辑规模的开发板。当然,只是为了学习的话,也没有必要全部搬到板子上运行,就像玩单片机一样,玩仿真也是玩。在FPGA开发当中仿真是极其有效且极其重要的一个手段,即使是有了开发板也是离不开仿真的。所以想要学习的话不买板子也行,或者只买个低容量的板子也行。 容量怎么看呢?很简单。市面上FPGA主要有两大厂家,一个是Intel(原先的Altera),另外一个是Xilinx。Intel的最常用的就是Cyclone系列,型号通常开头为EPaAb的格式,a和b分别表示代数和规模,A表示系列,如EP3C25,这个3表示代数,25则是表示25K的规模,C就是表示Cyclone了。一般对于初学而言,第几代关系不大。常用的是2-4代。第5代更换了命名规则,可能不容易从型号中直接看出规模了,需要查询手册。另外推荐的一个系列就是上面提到过得MAX10,作为低端FPGA性价比很不错,命名方法类似,10M50就表示MAX10系列50K单元。而Xilinx这边的命名方式是类似的,通常为XCaAb,a和b表示代数和规模,A表示系列。如XC6SLX15,6是代数,15是规模(15K),SLX是系列,表示Spartan LX。最常用的低端系列就是Spartan(S)和Artix(A),推荐可以考虑的型号如XC6SLX16和XC7A50。这个命名法适用于5-7代的产品,更早的标识方法不同。 开发板? 下一个问题,也是大家在买51单片机开发板时遇到过的问题,板子上需要有什么外设。ADC?DAC?串口?视频接口?LED?DDR内存?确实这也是个取决于你想做什么的事情,但是这里有一些大体上的建议。对于初学来说,LED和按钮/开关,是挺好用的东西,板子上最好能有。视频输出接口,最好能有,VGA/DVI/HDMI都行,如果没有的话能有配套的较大尺寸LCD也是好的。内存方面,虽然SDRAM和DDR内存容量更大,但是使用起来略微麻烦一些,如果有SRAM会很方便,但是有SDRAM和DDR也不是坏事,当然如果没有的话就会稍稍有些尴尬。另外并行Flash也是一种很好用的东西,然而除了大厂和官方出的一些开发板会有配备外,一般都没有。音频接口对于实现GameBoy而言也是必要的,当然如果没有可以用通用DA代替,或是用普通的IO+PWM/PDM代替,不是很要紧。最后的,USB、网口一类的接口,本次教程中并不会涉及到。 具体的选择?我个人虽然更加喜欢官方或者说大厂的板子(如Terasic和Digilent),但是国产的开发板有更多高性价比的选择。具体什么样还是看各位自己的喜好啦。本次的教程,演示将主要在Terasic的DE10-Lite(使用Intel MAX10系列FPGA)和Xilinx的ML505(使用Xilinx Virtex 5系列FPGA)上同步完成,方便选择任意一家板子的同学学习使用。之后也会教大家如何参考开发板提供的资料,把设计移植到自己的开发板上。如果说你决定选择这两家之外的FPGA(比如安路或者Lattice),使用方法也是大同小异的,毕竟大家都是使用的同样的编程语言。编程语言的问题我们将在之后讲到。 (DE10-Lite开发板) (ML505开发板) 最后来具体说说这次教程的安排和具体的目标。之前说了要实现一个GameBoy,但是并没有讲这个东西要怎么实现。我也问过我的一些朋友,有的告诉我这个作为教程很有趣,也有的告诉我这个目标实在太大了,做不到。我想的是,希望这样的项目相比常见教程中的一个个实验而言,足够有趣。作为一个大的项目,其本身的挑战和趣味能够一定程度上激励大家慢慢完成吧。废话不多说,来具体讲讲GameBoy。 上期提过,FPGA的一个玩法是用来实现一些老的电脑,而GameBoy也可以被看成是一种特殊的电脑。这里就简单列举一下GameBoy的一些基本硬件参数: 4MHz 类8080 8位处理器 8KB工作内存+8KB视频内存 像素处理单元(PPU)用于产生图像 音频单元,可以合成2个通道的方波,1个通道的杂波和1个通道的PCM 游戏卡带,最大容量可达8MB 而以下就是在FPGA中实现GameBoy的整体框图: (VerilogBoy框架) 东西不少,要完整实现确实是有不少活要干的。里面有复杂的部分比如一个完整的CPU,也有简单的部分比如定时器。另外还有一些常见的音视频接口的实现(AC97和VGA),还有基础协议的实现(手柄接口中的SPI)。作为一个项目而言,涵盖的部分算是挺全了,但是与此同时各部分又是模块化的,可以剥离开来单独研究、实现。在之后的教程中我们也将逐一的完成整个GameBoy。 从下一期开始,我们也将正式开始相关知识的学习。为了保证趣味性,理论和操作的内容将会穿插进行,且尽可能以,先操作后学习的顺序来,因为实际操作的经验有助于理解学习的内容。最开始的知识点自然就是逻辑电路和数字电路的基础了。基础虽然枯燥但是如果没有基础,到最后的空中楼阁就更加难办了。而有了基础之后,就可以开始设计GameBoy中简单的一些部分了,如定时器。之后慢慢就可以进行更复杂的模块的设计,如视频和音频。于此同时我也会穿插一些CPU相关的内容,毕竟也是最终需要实现的一部分。希望到最后,大家能够在自己的开发板上造出一个可以玩的GameBoy吧? (FPGA上的GameBoy Demo)

The Tutorial for FPGA Player - Episode 0: What is FPGA

想必各位读者中有不少是玩过Arduino、单片机或者是做过一些模拟电路制作的吧? 在玩过这些东西之后,经常会有的一个疑问就是,下一步玩什么?一部分人选择更加复杂的东西,比如32位单片机(如STM32)或者研究嵌入式Linux(如以“树莓派”为代表的超小型计算机),而另一部分人则是选择尽可能利用已经会的东西,做出一些有意思的制作。如果是前者,那么相信,这个所谓的“更复杂的东西”的列表里,一定会有FPGA。这个系列的文章,就是希望帮助那些已经有一定电子制作的基础,想要了解或者学习FPGA,却无从下手的人,当然可能对于正在学校学习FPGA的学生也会有一定的启发作用。本文作为这个系列文章的第一篇,先对FPGA以及相关的内容做一个概述。 如果从来接触过FPGA,那么可能直觉上会认为FPGA是一种很厉害的单片机(MCU),毕竟它看起来也就是一个体积比较大的芯片,开发板看起来也就和单片机开发板一样,甚至开发板自带的教程也像单片机的教程一样,把开发板上面自带的外设全部玩一遍。然而,实际上根本不是这样,FPGA并不是单片机,而且这两者甚至很难具有可比性。通常而言,玩单片机是玩软件编程,而玩FPGA是玩硬件编程。要了解FPGA的含义和用途,还要先从它的功能开始。 FPGA有什么功能? 首先我们来考虑一个问题。现在有一个LED,有一个按键,要实现按下按键点亮LED,应该怎么做呢?答案很简单,把电源、LED和按键全部串联在一起就行了。按下按键,电路接通,LED就会点亮。要用单片机来实现这个效果,基本的思路就是使用一个死循环,不停读取按键输入,然后把结果输出到LED。听起来用单片机完全就是多此一举嘛。那么现在来修改一下需求,有两个按键,要实现现在正好两个按键都按下或者都松开的情况时候点亮LED。用单片机来做并不需要做太多的修改,只需要在循环当中加入一些判断语句即可。如果这种情况下,还需要不使用任何芯片来控制LED,就不是连几条线的事情了。那么是不是说明单片机非常适合做这类的事情呢?其实并不是。考虑一下,如果这只是单片机需要做的事情的一部分,假如单片机还需要进行其他的处理,比如需要控制数码管刷新,也需要不断循坏。当很多这种需要不断循环的东西放到一起时,程序就不那么容易实现了,而且受限于单片机的性能,各个任务的响应速度也会受到影响。 那么有没有除了单片机以外的,更加直接一些的方法,用来实现以上的简单任务呢?比如有没有什么东西,正好有两个输入和一个输出,当且仅当两个输入全部为高或者全部为低的时候,输出高电平呢?答案是肯定的。有一种芯片,叫做74LS86,正好可以实现这个功能。功能类似的芯片还有很多,这类芯片就叫做逻辑门芯片,或者我们常说的74系列芯片(74是主要的逻辑芯片系列,但是也有很多其它系列的逻辑芯片)。他们的内部通常并不复杂,只有数量较少的三极管或者场效应管而已。有了这种芯片,电路设计就十分容易了,把按键接上74LS86的输入,把LED接上74LS86的输出,就能得到需要的效果。当然,如果有更多事情要做,就得加入更多的芯片了,因为单片74芯片通常只能实现非常有限的功能。而且,可想而知,不可能针对每一种特定的应用都会有特定的芯片,这样成本也太高了。也正是这个原因,才会有单片机这类的可编程器件,可以随时修改其实现的功能以满足不同的需要。似乎绕了一圈又回到了原点,并没有解决之前提到的问题,反而是带来了更多的问题。也确实不奇怪,发明单片机的一个目的就是代替一部分用74芯片实现的电路。 所以现在就轮到FPGA出场了。FPGA也是一种可编程器件,但是它并不是像单片机一样执行程序来实现具体的功能,而是像74芯片一样,直接通过电路连接来实现需要的功能。你可以把FPGA当作一片包含成千上万颗小的74系列芯片的大芯片,而这些小74 的连接方式和逻辑功能都是可编程的,考虑之前讲的单片机实现更多功能会降速的问题,在FPGA上,实现更多的功能只是相当于在里面放入了更多的74芯片,互相是独立的电路,并不会造成什么影响。不过需要注意的是,上面说的都是简化的模型,和实际情况会有一定出入。 听完上面的概述,你可能会觉得,实现这些逻辑功能听起来并没有什么用啊?谁没事去点LED呢?确实,单独的逻辑功能本省并没有太大的用处,甚至可能,按照一般的大学课程,学了一年的逻辑电路也没能体会到这些逻辑电路究竟能做什么。然而,逻辑功能的组合实际上是非常灵活的东西,可以实现非常多不同的功能。比如说,之前提到的FPGA和单片机的对比,你甚至可以在FPGA里面完整实现一个单片机。这并没有什么奇怪的,毕竟单片机本身就是用许许多多的逻辑门搭出来的,也就是个复杂的逻辑电路。FPGA如果能实现各种逻辑电路,自然也可以实现一个单片机。在工业应用中,FPGA通常被用于实现接口协议、数据转发一类的通信设备,也被用于进行IC设计验证。不过近年来的另外一个热点是将FPGA用于通用计算,比如神经网络模拟,但是就目前而言,FPGA相比于传统的CPU、GPU方案还并没有明显的价格优势。 FPGA与单片机有可比性吗 所以,FPGA和单片机到底能不能比呢?从他们的本质来说,这两者应该完全不具备可比性,就好比你无法比较CPU和操作系统哪个好一样。然而FPGA和单片机都可以实现控制和计算的功能,而且也确实都是通过写代码来编程的,目标应用也有一定重合,所以从这个角度来说又有一定的可比性。但我们需要知道,FPGA和单片机是两者完全不同的东西,总体来说很难讲优劣,只能说各有所长,各自适用于不同的应用场景。 学习FPGA有什么意义? 最后,学习FPGA对于爱好者而言有什么意义呢?我觉得最大的意义还是在于挑战自己。毕竟作为玩家,自己能够做到的事情比较有限,想要做出什么真正实用的东西并不容易。但是如果只是为了玩,也就大可不必去追求所谓的实用意义。如果这样说的话,FPGA大概会是一种很好玩的东西。一来是利用FPGA可以做到一些单片机不容易做到的东西,二来呢就是这个挑战本身了。就像硬核游戏玩家喜欢血缘诅咒这样的高难度游戏,也许也会有爱好者因为难度而喜欢玩FPGA的吧?这里就列举几款爱好者使用FPGA制作的有趣的作品。 (由f4hdk制作的A2Z FPGA计算机) (由Ian Hanschen制作的FPGA LED点阵控制器) 关于本教程 在接下来的文章当中,我将一步步的教大家如何用FPGA制作一个Game Boy兼容的游戏机,大体涉及的内容,从购买开发板,到数字电路基础,再到Verilog,计算机组成原理,一直到最后GameBoy的实现。不知道各位读者对于这个也没有兴趣呢?如果有兴趣的话,那就不要错过下一期的教程啦。最后附上一些,为了跟着本教程学习你首先需要知道的基础。 需要知道的: 如C/C++/Java/Rust等传统静态语言的编程,包括其中涉及到的基础,比如二进制和十六进制数的表示 基本的计算机概念,什么是CPU,什么是RAM,什么是ROM等 面包板的使用,简单的电路连接(理解按钮、拨码开关、LED、数码管等的作用) 单片机或者Arduino等微控平台的使用(提供对如串并口传输一类的基本认识) 不需要预先知道,会在教程里讲到的: Verilog或者VHDL的编程经验 汇编语言的编程经验 数字电路基础 计算机组成原理基础 计算机图形学基础 最后,感谢阅读,我们下期见

Plan of Jekyll Blog - Episode 2: Ruby-3, Jekyll-4, Git, Ubuntu and Nginx

This tutorial you will learn how to configure Jekyll-4 on Ubuntu 16.04/18.04. Catalogue Jekyll-4 Installation Ubuntu 16.04 Nginx Setup GitHub Local Repository Setup Git roll back to some commit Jekyll Maintaining Tutorial Jekyll Code Block for Liquid-Like Finally… My Second Jekyll Blog Git Dual Repository Setup Here is the blog repository: blog.old/blog.old3 Jekyll-4 Installation Ubuntu 16.04 First, we should install Ruby3.0.0 for Ubuntu 16.04/18.04: sudo apt-get install ruby-full build-essential zlib1g-dev nginx sudo apt-get install gcc g++ make curl -sL https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list sudo apt-get update && sudo apt-get install yarn sudo apt install curl curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash - curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list sudo apt-get update sudo apt-get install git-core zlib1g-dev build-essential libssl-dev libreadline-dev libyaml-dev libsqlite3-dev sqlite3 libxml2-dev libxslt1-dev libcurl4-openssl-dev software-properties-common libffi-dev nodejs yarn Then, we can use one of these methods to install Ruby-3: Install rbenv cd ~ git clone https://github.com/rbenv/rbenv.git ~/.rbenv echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc echo 'eval "$(rbenv init -)"' >> ~/.bashrc exec $SHELL git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build echo 'export PATH="$HOME/.rbenv/plugins/ruby-build/bin:$PATH"' >> ~/.bashrc exec $SHELL rbenv install 3.0.0 rbenv global 3.0.0 ruby -v Install RVM sudo apt-get install libgdbm-dev libncurses5-dev automake libtool bison libffi-dev gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB curl -sSL https://get.rvm.io | bash -s stable source ~/.rvm/scripts/rvm rvm install 3.0.0 rvm use 3.0.0 --default ruby -v Ruby Source Codes Installation cd wget http://ftp.ruby-lang.org/pub/ruby/3.0/ruby-3.0.0.tar.gz tar -xzvf ruby-3.0.0.tar.gz cd ruby-3.0.0/ ./configure make sudo make install ruby -v Caution: In some area, it’s difficult to visit RubyGems, so you might be change a Ruby Source. gem source -r https://rubygems.org/ gem source --add https://gems.ruby-china.com/ gem source -u Finally, install Bundler gem install bundler jekyll github-pages jekyll-paginate webrick After installation, you can clone your repository from GitHub or GitLab. git clone https://github.com/IcingTomato/blog.old.git Nginx Setup Well, you can also use Apache which use sudo apt-get install apache2 on Debian/Ubuntu or sudo yum install httpd on CentOS/RHEL/Fedora. First, install Nginx: sudo apt-get isntall nginx Then, configure nginx profile: root@blog:~# sudo nano /etc/nginx/sites-enabled/default server { listen 80 default_server; listen [::]:80 default_server; # SSL configuration # listen 443 ssl default_server; listen [::]:443 ssl default_server; # Disable SSL, because when it enabled, it blocked 80. #ssl on; ssl_certificate path/to/your_key.pem; ssl_certificate_key path/to/your_key.key; ssl_session_timeout 5m; ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_prefer_server_ciphers on; root /var/www/html; # Add index.php to the list if you are using PHP index index.html; server_name your.domain; location / { # First attempt to serve request as file, then # as directory, then fall back to displaying a 404. try_files $uri $uri/ =404; } # Rewrited the 404 page to 200 error_page 404 =200 /404.html; location /404.html { root /var/www/html/; internal; } # Rewrited the 403 page to 200 error_page 403 =200 /403.html; location /403.html { root /var/www/html/; internal; } } Ctrl+O and Ctrl+X to save and exit. Finally, restart nginx.service sudo systemctl restart nginx # On AWS, you must use systemctl to start, stop, enable, disable, restart a service. GitHub Local Repository Setup git init git add README.md git commit -m "first commit" git branch -M master git remote add origin repo_address git push -u origin master Git roll back to some commit git reset --hard 某个commit id git push -f -u origin master Jekyll Maintaining Tutorial You can make a quick start for surfing the Jekyll blog: git clone -b blank https://github.com/IcingTomato/blog.old.git cd blog.old jekyll serve # jekyll s # => Now browse to http://localhost:4000 Setup _config.yml # Site settings title: Your Blog # Your blog title SEOTitle: Jekyll | Ruby # SEO title description: "Hello from Seattle" # Your blog description # SNS settings github_username: John Doe # GitHub account RSS: true # RSS On weibo_username: # Weibo zhihu_username: # Zhihu facebook_username: # Facebook bilibili_username: # Bilibili twitter_username: # Twitter # Build settings paginate: 100 # Contents in a page You can visit Jekyll-Docs/Jekyll中文网-文档 for more support. Sidebar On PCs, tablet devices or ultra-wide scale screen there is a sidebar to display your personal profile. # Sidebar settings sidebar: true # Add Sidebar sidebar-about-description: "describe yourself" # Description sidebar-avatar: /img/avatar.jpg # Your Avatar Sidebar is a Responsive Layout*, when the display size is below 992px, the sidebar will move to the page bottom. You can visit Bootstrap3 for global CSS settings/Bootstrap 设置全局 CSS 样式 Mini About Me This module is under your avatar. It will display all your social media account. It is also a Responsive Layout. When the screen size become small, it will move to the bottom of the page. But there is a litte change when it move to the bottom. Featured Tags # Featured Tags featured-tags: true featured-condition-size: 15 # A tag will be featured if the size of it is more than this condition value featured-condition-size means if the tags’ count over 15(or the number you set), it will display on home page. Friends # Friends friends: [ { title: "John Doe", href: "http://john.doe/" },{ title: "村財 師内", href: "https://sonzai.shinai/" },{ title: "鵜祖田 陽", href: "https://usoda.you/" } ] Comment This Jekyll Blog supports both Disqus and Gitalk, it also supports Markdown Grammar. Disqus # Disqus(https://disqus.com/) disqus_username: Gitalk First, we should create a new repository for saving our comments: And then open Issues option: Besides, register a new OAuth application: Google Analytics # Google Analytics ga_track_id: 'UA-' ga_domain: # Default as 'auto', you can use your domain Write your first blog We must put the article, which named as YYYY-MM-DD-your_first_article.markdown, in _post folder. And you can change parameter in the front of the article: --- layout: post title: Your First Blog subtitle: Jekyll is Хорошо date: 1900-01-01 author: John Doe header-img: img/title.jpg catalog: true tags: - Jekyll --- Jekyll Code Block for Liquid-Like In Jekyll, some Ruby code block can’t generate perfectly. So, we can use raw tag to avoid execute liquid or liquid-like code block: {% raw %} You can use {% highlight ruby %}{% endhighlight %} for code highlight. Finally… cd your.folder git clone repo.address git pull origin master jekyll build -d /var/www/html/ My Second Jekyll Blog Here is the blog repository: blog.old2 You can use these command line for configuring the blog: cd <the folder> git clone https://github.com/IcingTomato/blog.old2.git bundle add webrick rake bundle install --path vendor/cache git pull origin master bundle exec jekyll build -d /var/www/html/ First, you need to install some Dependence like webrick rake jekyll. And then, in my case, I had a PATH problem. Bundler::GemNotFound: Could not find rake-10.3.2 in any of the sources ~/.rvm/gems/ruby-2.0.0-p451/gems/bundler-1.6.2/lib/bundler/spec_set.rb:92:in `block in materialize' ~/.rvm/gems/ruby-2.0.0-p451/gems/bundler-1.6.2/lib/bundler/spec_set.rb:85:in `map!' ~/.rvm/gems/ruby-2.0.0-p451/gems/bundler-1.6.2/lib/bundler/spec_set.rb:85:in `materialize' ~/.rvm/gems/ruby-2.0.0-p451/gems/bundler-1.6.2/lib/bundler/definition.rb:133:in `specs' ~/.rvm/gems/ruby-2.0.0-p451/gems/bundler-1.6.2/lib/bundler/definition.rb:178:in `specs_for' Show 28 more lines I used gem install rake but it was no use fixing the matter. SO, bundle install --path vendor/cache can help you fix the problem. This command line generally fixes it as that is the more common problem. Basically, my bundler path configuration is messed up. See their documentation (first paragraph) for where to find those configurations and change them manually if needed. Git Dual Repository Setup Everytime I finish my blog, I have to push to two different platform: GitHub and Gitee. Gitee is a collaboration platform for software development & code hosting in China Mainland. Because I use Ali ECS Area Hangzhou, it’s hard to visit GitHub, so I have to use Gitee. So cd 'your-repository-path' git remote add gitee 'your-gitee-repository-address' git push gitee master # Push to 'gitee' git push origin master # Push to 'github' You can also change your-repository/.git/config [core] repositoryformatversion = 0 filemode = true bare = false logallrefupdates = true [remote "origin"] url = 'your-github-repository-address' fetch = +refs/heads/*:refs/remotes/origin/* [branch "master"] remote = origin merge = refs/heads/master [remote "gitee"] url = 'your-gitee-repository-address' fetch = +refs/heads/*:refs/remotes/gitee/*

Basic Knowledge of Networking

The foundations of networking: switches, routers, and wireless access points. Switches, routers, and wireless access points are the essential networking basics. Through them, devices connected to your network can communicate with one another and with other networks, like the Internet. 传输介质简介 可以让设备通过网卡连接互联网的介质,如同轴电缆、双绞线和光纤等。不同的传输介质具有不同的特性,这些特性直接影响到通信的诸多方面,如线路编码方式、传输速度和传输距离等。 常见的网络传输介质 同轴电缆(Coaxial Cable) 同轴电缆是一种早期使用的传输介质,同轴电缆的标准分为两种,10BASE2和10BASE5。这两种标准都支持10Mbps的传输速率,最长传输距离分别为185米和500米。一般情况下,10Base2同轴电缆使用BNC接头,10Base5同轴电缆使用N型接头。 10BASE5和10BASE2是早期的两种以太网标准,它们均采用同轴电缆作为传输介质。10BASE5和10BASE2所使用的同轴电缆的直径分别为9.5mm和5mm,所以前者又称为粗缆,后者又称为细缆。 现在,10Mbps的传输速率早已不能满足目前企业网络需求,因此同轴电缆在目前企业网络中很少应用。 双绞线(Twisted Pair) 与同轴电缆相比双绞线(Twisted Pair)具有更低的制造和部署成本,因此在企业网络中被广泛应用。双绞线可分为屏蔽双绞线(Shielded Twisted Pair,STP)和非屏蔽双绞线(Unshielded Twisted Pair,UTP)。屏蔽双绞线在双绞线与外层绝缘封套之间有一个金属屏蔽层,可以屏蔽电磁干扰。双绞线有很多种类型,不同类型的双绞线所支持的传输速率一般也不相同。例如,3类双绞线支持10Mbps传输速率;5类双绞线支持100Mbps传输速率;超5类双绞线及更高级别的双绞线支持千兆以太网传输。双绞线使用RJ-45接头连接网络设备。为保证终端能够正确收发数据,RJ-45接头中的针脚必须按照一定的线序排列 光纤(Optical Fiber) 双绞线和同轴电缆传输数据时使用的是电信号,而光纤传输数据时使用的是光信号。光纤支持的传输速率包括10Mbps,100Mbps,1Gbps,10Gbps,甚至更高。根据光纤传输光信号模式的不同,光纤又可分为单模光纤和多模光纤。单模光纤只能传输一种模式的光,不存在模间色散,因此适用于长距离高速传输。多模光纤允许不同模式的光在一根光纤上传输,由于模间色散较大而导致信号脉冲展宽严重,因此多模光纤主要用于局域网中的短距离传输。光纤连接器种类很多,常用的连接器包括ST,FC,SC,LC连接器。 串口电缆(Serial Port Cable) 网络通信中常常会用到各种各样的串口电缆。常用的串口电缆标准为RS-232,同时也是推荐的标准。但是RS-232的传输速率有限,传输距离仅为6米。其他的串口电缆标准可以支持更长的传输距离,例如RS-422和RS-485的传输距离可达1200米。RS-422和RS-485串口电缆通常使用V.35接头,这种接头在上世纪80年代已经淘汰,但是现在仍在帧中继、ATM等传统网络上使用。V.24是RS-232标准的欧洲版。RS-232本身没有定义接头标准,常用的接头类型为DB-9和DB-25。现在,RS-232已逐渐被FireWire、USB等新标准取代,新产品和新设备已普遍使用USB标准。 什么是「网络」 网络是由若干节点和连接这些节点的链路构成,表示诸多对象及其相互联系。网络是信息传输、接收、共享的虚拟平台,通过它把各个点、面、体的信息联系到一起,从而实现这些资源的共享。网络是人类发展史来最重要的发明,提高了科技和人类社会的发展。 网络设备有什么 电脑、手机、iPad、凡是能联网的设备、提供服务的设备、路由器、交换机、防火墙、无线设备…… 网卡是上网必备的工具 简单网络 两个终端,用一条能承载数据传输的物理介质(也称为传输介质)连接起来,就组成了一个最简单的网络。 网络划分为 有线网络 无线网络 双绞线的线序 T-568A 白绿 绿 白橙 蓝 白蓝 橙 白棕 棕 T-568B 白橙 橙 白绿 蓝 白蓝 绿 白棕 棕 根据网线两端的线序可分为 直通线:两端线序是一样的 交叉线:两端线序是相反的 冲突域(Collision Domain) 共享式网络中可能会出现信号冲突现象。 如图是一个10BASE5以太网,每个主机都是用同一根同轴电缆来与其它主机进行通信,因此,这里的同轴电缆又被称为共享介质,相应的网络被称为共享介质网络,或简称为共享式网络。共享式网络中,不同的主机同时发送数据时,就会产生信号冲突的问题,解决这一问题的方法一般是采用载波侦听多路访问/冲突检测技术(Carrier Sense Multiple Access/Collision Detection)。 CSMA/CD的基本工作过程如下: 终端设备不停地检测共享线路的状态。如果线路空闲,则可以发送数据;如果线路不空闲,则等待一段时间后继续检测(延时时间由退避算法决定)。 如果有另外一个设备同时发送数据,两个设备发送的数据会产生冲突。 终端设备检测到冲突之后,会马上停止发送自己的数据,并发送特殊阻塞信息,以强化冲突信号,使线路上其他站点能够尽早检测到冲突。 终端设备检测到冲突后,等待一段时间之后再进行数据发送(延时时间由退避算法决定)。 CSMA/CD的工作原理可简单总结为:先听后发,边发边听,冲突停发,随机延迟后重发。 双工模式(Duplex) 两种双工模式都支持双向数据传输。 半双工:在半双工模式(half-duplex mode)下,通信双方都能发送和接收数据,但不能同时进行。当一台设备发送时,另一台只能接收,反之亦然。对讲机是半双工的典型例子。 全双工:在全双工模式(full-duplex mode)下,通信双方都能同时接收和发送数据。电话网络是典型的全双工例子。 以太网上的通信模式包括半双工和全双工两种: 半双工模式下,共享物理介质的通信双方必须采用CSMA/CD机制来避免冲突。例如,10BASE5以太网的通信模式就必须是半双工模式。 全双工模式下,通信双方可以同时实现双向通信,这种模式不会产生冲突,因此不需要使用CSMA/CD机制。例如,10BASE-T以太网的通信模式就可以是全双工模式。 同一物理链路上相连的两台设备的双工模式必须保持一致。 以太网帧结构 网络中传输数据时需要定义并遵循一些标准,以太网是根据IEEE 802.3标准来管理和控制数据帧的。 网络通信协议 不同的协议栈用于定义和管理不同网络的数据转发规则。 20世纪60年代以来,计算机网络得到了飞速发展。各大厂商和标准组织为了在数据通信网络领域占据主导地位,纷纷推出了各自的网络架构体系和标准,如IBM公司的SNA协议,Novell公司的IPX/SPX协议,以及广泛流行的OSI参考模型和TCP/IP协议。同时,各大厂商根据这些协议生产出了不同的硬件和软件。标准组织和厂商的共同努力促进了网络技术的快速发展和网络设备种类的迅速增长。 网络通信中,“协议”和“标准”这两个词汇常常可以混用。同时,协议或标准本身又常常具有层次的特点。一般地,关注于逻辑数据关系的协议通常被称为上层协议,而关注于物理数据流的协议通常被称为底层协议。IEEE 802就是一套用来管理物理数据流在局域网中传输的标准,包括在局域网中传输物理数据的802.3以太网标准。除以太外,还有一些用来管理物理数据流在广域网中传输的标准,如PPP(Point-to-Point Protocol),高级数据链路控制HDLC(High-Level Data Link Control)。 分层模型-OSI 国际标准化组织ISO于1984年提出了OSI RM(Open System Interconnection Reference Model,开放系统互连参考模型)。OSI参考模型很快成为了计算机网络通信的基础模型。 OSI参考模型具有以下优点:简化了相关的网络操作;提供了不同厂商之间的兼容性;促进了标准化工作;结构上进行了分层;易于学习和操作。 OSI参考模型各个层次的基本功能如下: 物理层:在设备之间传输比特流,规定了电平、速度和电缆针脚。 数据链路层:将比特组合成字节,再将字节组合成帧,使用链路层地址(以太网使用MAC地址)来访问介质,并进行差错检测。 网络层:提供逻辑地址,供路由器确定路径。 传输层:提供面向连接或非面向连接的数据传递以及进行重传前的差错检测。 会话层:负责建立、管理和终止表示层实体之间的通信会话。该层的通信由不同设备中的应用程序之间的服务请求和响应组成。 表示层:提供各种用于应用层数据的编码和转换功能,确保一个系统的应用层发送的数据能被另一个系统的应用层识别。 应用层:OSI参考模型中最靠近用户的一层,为应用程序提供网络服务。 分层模型-TCP/IP TCP/IP模型同样采用了分层结构,层与层相对独立但是相互之间也具备非常密切的协作关系。 TCP/IP模型将网络分为四层。TCP/IP模型不关注底层物理介质,主要关注终端之间的逻辑数据流转发。TCP/IP模型的核心是网络层和传输层,网络层解决网络之间的逻辑转发问题,传输层保证源端到目的端之间的可靠传输。最上层的应用层通过各种协议向终端用户提供业务应用。 数据封装 应用数据需要经过TCP/IP每一层处理之后才能通过网络传输到目的端,每一层上都使用该层的协议数据单元PDU(Protocol Data Unit)彼此交换信息。不同层的PDU中包含有不同的信息,因此PDU在不同层被赋予了不同的名称。如上层数据在传输层添加TCP报头后得到的PDU被称为Segment(数据段);数据段被传递给网络层,网络层添加IP报头得到的PDU被称为Packet(数据包);数据包被传递到数据链路层,封装数据链路层报头得到的PDU被称为Frame(数据帧);最后,帧被转换为比特,通过网络介质传输。这种协议栈逐层向下传递数据,并添加报头和报尾的过程称为封装。 终端之间的通信 数据链路层控制数据帧在物理链路上传输。 数据包在以太网物理介质上传播之前必须封装头部和尾部信息,封装后的数据包称为数据帧,数据帧中封装的信息决定了数据如何传输。以太网上传输的数据帧有两种格式,选择哪种格式由TCP/IP协议簇中的网络层决定。 帧格式 以太网上使用两种标准帧格式。第一种是上世纪80年代初提出的DIX v2格式,即Ethernet II帧格式。Ethernet II后来被IEEE 802标准接纳,并写进了IEEE 802.3x-1997的3.2.6节。第二种是1983年提出的IEEE 802.3格式。这两种格式的主要区别在于Ethernet II格式中包含一个Type字段,标识以太帧处理完成之后将被发送到哪个上层协议进行处理,IEEE 802.3格式中,同样的位置是长度字段。 不同的Type字段值可以用来区别这两种帧的类型,当Type字段值小于等于1500(或者十六进制的0x05DC)时,帧使用的是IEEE 802.3格式。当Type字段值大于等于1536 (或者十六进制的0x0600)时,帧使用的是Ethernet II格式。以太网中大多数的数据帧使用的是Ethernet II格式。 以太帧中还包括源和目的MAC地址,分别代表发送者的MAC和接收者的MAC,此外还有帧校验序列字段,用于检验传输过程中帧的完整性。 Ethernet_II帧格式 Ethernet_II 帧类型值大于等于1536 (0x0600),以太网数据帧的长度在64-1518字节之间。 Ethernet_II的帧中各字段说明如下: DMAC(Destination MAC)是目的MAC地址。DMAC字段长度为6个字节,标识帧的接收者。 SMAC(Source MAC)是源MAC地址。SMAC字段长度为6个字节,标识帧的发送者。 类型字段(Type)用于标识数据字段中包含的高层协议,该字段长度为2个字节。类型字段取值为0x0800的帧代表IP协议帧;类型字段取值为0x0806的帧代表ARP协议帧。 数据字段(Data)是网络层数据,最小长度必须为46字节以保证帧长至少为64字节,数据字段的最大长度为1500字节 。 循环冗余校验字段(FCS)提供了一种错误检测机制。该字段长度为4个字节。 IEEE802.3帧格式 IEEE802.3帧长度字段值小于等于1500 (0x05DC)。 IEEE802.3帧格式类似于Ethernet_II帧,只是Ethernet_II帧的Type域被802.3帧的Length域取代,并且占用了Data字段的8个字节作为LLC和SNAP字段。 Length字段定义了Data字段包含的字节数。 逻辑链路控制LLC(Logical Link Control)由目的服务访问点DSAP(Destination Service Access Point)、源服务访问点SSAP(Source Service Access Point)和Control字段组成。 SNAP(Sub-network Access Protocol)由机构代码(Org Code)和类型(Type)字段组成。Org Code三个字节都为0。Type字段的含义与Ethernet_II帧中的Type字段相同。IEEE802.3帧根据DSAP和SSAP字段的取值又可分为以下几类: 1)当DSAP和SSAP都取特定值0xff时,802.3帧就变成了Netware-ETHERNET帧,用来承载NetWare类型的数据。 2)当DSAP和SSAP都取特定值0xaa时,802.3帧就变成了ETHERNET_SNAP帧。ETHERNET_SNAP帧可以用于传输多种协议。 3)DSAP和SSAP其他的取值均为纯IEEE802.3帧。 数据帧传输 数据链路层基于MAC地址进行帧的传输。 以太网在二层链路上通过MAC地址来唯一标识网络设备,并且实现局域网上网络设备之间的通信。MAC地址也叫物理地址,大多数网卡厂商把MAC地址烧入了网卡的ROM中。发送端使用接收端的MAC地址作为目的地址。以太帧封装完成后会通过物理层转换成比特流在物理介质上传输。 以太网的MAC地址 MAC地址由两部分组成,分别是供应商代码和序列号。其中前24位代表该供应商代码,由IEEE管理和分配。剩下的24位序列号由厂商自己分配。 如同每一个人都有一个名字一样,每一台网络设备都用物理地址来标识自己,这个地址就是MAC地址。网络设备的MAC地址是全球唯一的。MAC地址长度为48比特,通常用十六进制表示。MAC地址包含两部分:前24比特是组织唯一标识符(OUI,Organizationally Unique Identifier),由IEEE统一分配给设备制造商。例如,华为的网络产品的MAC地址前24比特是0x00e0fc。后24位序列号是厂商分配给每个产品的唯一数值,由各个厂商自行分配(这里所说的产品可以是网卡或者其他需要MAC地址的设备)。 单播(Unicast) 局域网上的帧可以通过三种方式发送。第一种是单播,指从单一的源端发送到单一的目的端。每个主机接口由一个MAC地址唯一标识,MAC地址的OUI中,第一字节第8个比特表示地址类型。对于主机MAC地址,这个比特固定为0,表示目的MAC地址为此MAC地址的帧都是发送到某个唯一的目的端。在冲突域中,所有主机都能收到源主机发送的单播帧,但是其他主机发现目的地址与本地MAC地址不一致后会丢弃收到的帧,只有真正的目的主机才会接收并处理收到的帧。 广播(Broadcast) 第二种发送方式是广播,表示帧从单一的源发送到共享以太网上的所有主机。广播帧的目的MAC地址为十六进制的FF:FF:FF:FF:FF:FF,所有收到该广播帧的主机都要接收并处理这个帧。 广播方式会产生大量流量,导致带宽利用率降低,进而影响整个网络的性能。 当需要网络中的所有主机都能接收到相同的信息并进行处理的情况下,通常会使用广播方式。 组播(Multicast) 第三种发送方式为组播,组播比广播更加高效。组播转发可以理解为选择性的广播,主机侦听特定组播地址,接收并处理目的MAC地址为该组播MAC地址的帧。 组播MAC地址和单播MAC地址是通过第一字节中的第8个比特区分的。组播MAC地址的第8个比特为1,而单播MAC地址的第8个比特为0。 当需要网络上的一组主机(而不是全部主机)接收相同信息,并且其他主机不受影响的情况下通常会使用组播方式。 数据帧的发送和接收 当主机接收到的数据帧所包含的目的MAC地址是自己时,会把以太网封装剥掉后送往上层协议。 帧从主机的物理接口发送出来后,通过传输介质传输到目的端。共享网络中,这个帧可能到达多个主机。主机检查帧头中的目的MAC地址,如果目的MAC地址不是本机MAC地址,也不是本机侦听的组播或广播MAC地址,则主机会丢弃收到的帧。 如果目的MAC地址是本机MAC地址,则接收该帧,检查帧校验序列(FCS)字段,并与本机计算的值对比来确定帧在传输过程中是否保持了完整性。如果帧的FCS值与本机计算的值不同,主机会认为帧已被破坏,并会丢弃该帧。如果该帧通过了FCS校验,则主机会根据帧头部中的Type字段来确定将帧发送给上层哪个协议处理。本例中,Type字段的值为0x0800,表明该帧需要发送到IP协议上处理。在发送给IP协议之前,帧的头部和尾部会被剥掉。 IP编址 上层协议地址 以太网帧中的Type字段值为0x0800,表示该帧的网络层协议为IP协议。 在剥掉帧的头部和尾部之前,网络设备需要根据帧头中Type字段确定下一步将帧发送到哪个上层协议进行处理。本例中的帧头部Type字段表示该帧需要上送到IP协议进行处理。以下将介绍帧的头部和尾部被剥掉后,IP协议将如何处理帧中的数据。 IP报文头部 IP报文头部信息用于指导网络设备对报文进行路由和分片。同一个网段内的数据转发通过链路层即可实现,而跨网段的数据转发需要使用网络设备的路由功能。分片是指数据包超过一定长度时,需要被划分成不同的片段使其能够在网络中传输。 IP报文头部长度为20到60字节,报文头中的信息可以用来指导网络设备如何将报文从源设备发送到目的设备。其中,版本字段表示当前支持的IP协议版本,当前的版本号为4。DS字段早期用来表示业务类型,现在用于支持QoS中的差分服务模型,实现网络流量优化。 源和目的IP地址是分配给主机的逻辑地址,用于在网络层标识报文的发送方和接收方。根据源和目的IP地址可以判断目的端是否与发送端位于同一网段,如果二者不在同一网段,则需要采用路由机制进行跨网段转发。 IP编址 IP地址分为网络部分和主机部分。 IP地址由32个二进制位组成,通常用点分十进制形式表示。 IPv4地址为32比特的二进制数,通常用点分十进制表示。IP地址用来标识网络中的设备,具有IP地址的设备可以在同一网段内或跨网段通信。IP地址包括两部分,第一部分是网络号,表示IP地址所属的网段,第二部分是主机号,用来唯一标识本网段上的某台网络设备。 每个网段上都有两个特殊地址不能分配给主机或网络设备。第一个是该网段的网络地址,该IP地址的主机位为全0,表示一个网段。第二个地址是该网段中的广播地址,目的地址为广播地址的报文会被该网段中的所有网络设备接收。广播地址的主机位为全1。除网络地址和广播地址以外的其他IP地址都可以作为网络设备的IP地址。 二进制、十进制和十六进制 在IP网络中,二进制和十六进制是常用的编码方式。 网络中的数据可以采用二进制、十进制或十六进制来表示,了解这些进制对理解IP网络基础知识很有必要。每种进制使用不同的基值表示每一位的数值。二进制每一位只有0和1两个值,基值为2,二进制数的每一位都可以用2的x次幂来表示,x表示二进制数的位数。十六进制的每一位可以有16个数值,范围为0-F(即0-9和A-F),A对应十进制的10,F对应十进制的15(二进制的1111)。 IP地址以字节为单位分为四段,每字节包含8个比特,可以表示0到255,共256个数值。从二进制到十进制转换表中可以看到每一位二进制数所代表的十进制数。上面的表格举例说明了8位二进制数转换为十进制数和十六进制数的情况。从表格中也可以看到全0和全1所对应的十进制数和十六进制数。 二进制和十进制转换 32位的IP地址分为4个字节,每个字节有256个取值。因此,理论上IPv4可以有4,294,967,296个IP地址,但实际上只有其中一部分地址可以分配给网络设备使用。本例中,IP地址的前三个字节表示网络号,最后一个字节表示该网络上网络设备可用的地址范围。将二进制格式的IP地址转换为十进制格式时,需要把二进制中每一位1所代表的值加在一起,得出IP地址的十进制值。 IP地址分类 IPv4地址被划分为A、B、C、D、E五类,每类地址的网络号包含不同的字节数。A类,B类和C类地址为可分配IP地址,每类地址支持的网络数和主机数不同。比如,A类地址可支持126个网络,每个网络支持224 (16,777,216 )个主机地址,另外每个网段中的网络地址和广播地址不能分配给主机。C类地址支持200多万个网络,每个网络支持256个主机地址,其中254个地址可以分配给主机使用。 D类地址为组播地址。主机收到以D类地址为目的地址的报文后,且该主机是该组播组成员,就会接收并处理该报文。各类IP地址可以通过第一个字节中的比特位进行区分。如A类地址第一字节的最高位固定为0,B类地址第一字节的高两位固定为10,C类地址第一字节的高三位固定为110,D类地址第一字节的高四位固定为1110,E类地址第一字节的高四位固定为1111。 IP地址类型 私有地址范围 10.0.0.0~10.255.255.255 172.16.0.0~172.31.255.255 192.168.0.0~192.168.255.255 特殊地址 127.0.0.0~127.255.255.255 0.0.0.0 255.255.255.255 IPv4中的部分IP地址被保留用作特殊用途。为节省IPv4地址,A、B、C类地址段中都预留了特定范围的地址作为私网地址。现在,世界上所有终端系统和网络设备需要的IP地址总数已经超过了32位IPv4地址所能支持的最大地址数4,294,967,296。为主机分配私网地址节省了公网地址,可以用来缓解IP地址短缺的问题。企业网络中普遍使用私网地址,不同企业网络中的私网地址可以重叠。默认情况下,网络中的主机无法使用私网地址与公网通信;当需要与公网通信时,私网地址必须转换成公网地址。还有其他一些特殊IP地址,如127.0.0.0网段中的地址为环回地址,用于诊断网络是否正常。IPv4中的第一个地址0.0.0.0表示任何网络,这个地址的作用将在路由原理中详细介绍。IPv4中的最后一个地址255.255.255.255是0.0.0.0网络中的广播地址。 网络通信 源主机必须要知道目的主机的IP地址后才能将数据发送到目的地。源主机向其他目的主机发送报文之前,需要检查目的IP地址和源IP地址是否属于同一个网段。如果是,则报文将被下发到底层协议进行以太网封装处理。如果目的地址和源地址属于不同网段,则主机需要获取下一跳路由器的IP地址,然后将报文下发到底层协议处理。 子网掩码(Mask) 子网掩码用于区分网络部分和主机部分。子网掩码与IP地址的表示方法相同。每个IP地址和子网掩码一起可以用来唯一的标识一个网段中的某台网络设备。子网掩码中的1表示网络位,0表示主机位。 默认子网掩码 每类IP地址有一个缺省子网掩码。A类地址的缺省子网掩码为8位,即第一个字节表示网络位,其他三个字节表示主机位。B类地址的缺省子网掩码为16位,因此B类地址支持更多的网络,但是主机数也相应减少。C类地址的缺省子网掩码为24位,支持的网络最多,同时也限制了单个网络中主机的数量。 地址规划 通过子网掩码可以判断主机所属的网段、网段上的广播地址以及网段上支持的主机数。图中这个例子,主机地址为192.168.1.7,子网掩码为24位(C类IP地址的缺省掩码),从中我们可以判断该主机位于192.168.1.0/24网段。将IP地址中的主机位全部置为1,并转换为十进制数,即可得到该网段的广播地址192.168.1.255。网段中支持的主机数为2n,n为主机位的个数。本例中n=8,28=256,减去本网段的网络地址和广播地址,可知该网段支持254个有效主机地址。 有类IP编址的缺陷 在设计网络时使用有类IP地址会造成地址的浪费。 如果企业网络中希望通过规划多个网段来隔离物理网络上的主机,使用缺省子网掩码就会存在一定的局限性。网络中划分多个网段后,每个网段中的实际主机数量可能很有限,导致很多地址未被使用。如图所示的场景下,如果使用缺省子网掩码的编址方案,则地址使用率很低。 变长子网掩码 采用可变长子网掩码可解决上述问题。缺省子网掩码可以进一步划分,成为变长子网掩码(VLSM)。通过改变子网掩码,可以将网络划分为多个子网。本例中的地址为C类地址,缺省子网掩码为24位。现借用一个主机位作为网络位,借用的主机位变成子网位。一个子网位有两个取值0和1,因此可划分两个子网。该比特位设置为0,则子网号为0,该比特位设置为1,则子网号为128。将剩余的主机位都设置为0,即可得到划分后的子网地址;将剩余的主机位都设置为1,即可得到子网的广播地址。每个子网中支持的主机数为27-2(减去子网地址和广播地址),即126个主机地址。 网关(Gateway) 网关用来转发来自不同网段之间的数据包。 报文转发过程中,首先需要确定转发路径以及通往目的网段的接口,然后将报文封装在以太帧中通过指定的物理接口转发出去。如果目的主机与源主机不在同一网段,报文需要先转发到网关,然后通过网关将报文转发到目的网段。 网关是指接收并处理本地网段主机发送的报文并转发到目的网段的设备。为实现此功能,网关必须知道目的网段的IP地址。网关设备上连接本地网段的接口地址即为该网段的网关地址。 IP包分片 网络中转发的IP报文的长度可以不同,但如果报文长度超过了数据链路所支持的最大长度,则报文就需要分割成若干个较小的片段才能够在链路上传输。将报文分割成多个片段的过程叫做分片。 接收端根据分片报文中的标识符(Identification),标志(Flags),及片偏移(Fragment Offset)字段对分片报文进行重组。标识符用于识别属于同一个数据包的分片,以区别于同一主机或其他主机发送的其它数据包分片,保证分片被正确的重新组合。标志字段用于判断是否已经收到最后一个分片。最后一个分片的标志字段设置为0,其他分片的标志字段设置为1,目的端在收到标志字段为0的分片后,开始重组报文。片偏移字段表示每个分片在原始报文中的位置。第一个分片的片偏移为0,第二个分片的片偏移表示紧跟第一个分片后的第一个比特的位置。比如,如果首片报文包含1259比特,那么第二分片报文的片偏移字段值就应该为1260。 生存时间 报文在网段间转发时,如果网络设备上的路由规划不合理,就可能会出现环路,导致报文在网络中无限循环,无法到达目的端。环路发生后,所有发往这个目的地的报文都会被循环转发,随着这种报文逐渐增多,网络将会发生拥塞。 为避免环路导致的网络拥塞,IP报文头中包含一个生存时间TTL(Time To Live)字段。报文每经过一台三层设备,TTL值减1。初始TTL值由源端设备设置。当报文中的TTL降为0时,报文会被丢弃。同时,丢弃报文的设备会根据报文头中的源IP地址向源端发送ICMP错误消息。 协议号 目的端的网络层在接收并处理报文以后,需要决定下一步对报文该做如何处理。IP报文头中的协议字段标识了将会继续处理报文的协议。与以太帧头中的Type字段类似,协议字段也是一个十六进制数。该字段可以标识网络层协议,如ICMP(Internet Control Message Protocol,因特网控制报文协议),也可以标识上层协议,如TCP(Transmission Control Protocol,传输控制协议,对应值0x06)、UDP(User Datagram Protocol,用户数据包协议,对应值0x11)。 常用端口列举 端口:0 服务:Reserved 说明:通常用于分析操作系统。这一方法能够工作是因为在一些系统中“0”是无效端 口,当你试图使用通常的闭合端口连接它时将产生不同的结果。一种典型的扫描,使用 IP地址为0.0.0.0,设置ACK位并在以太网层广播。 端口:1 服务:tcpmux 说明:这显示有人在寻找SGI Irix机器。Irix是实现tcpmux的主要提供者,默认情况下 tcpmux在这种系统中被打开。Irix机器在发布是含有几个默认的无密码的帐户,如: IP、GUEST UUCP、NUUCP、DEMOS 、TUTOR、DIAG、OUTOFBOX等。许多管理员在安装后忘 记删除这些帐户。因此HACKER在INTERNET上搜索tcpmux并利用这些帐户。 端口:7 服务:Echo 说明:能看到许多人搜索Fraggle放大器时,发送到X.X.X.0和X.X.X.255的信息。 端口:19 服务:Character Generator 说明:这是一种仅仅发送字符的服务。UDP版本将会在收到UDP包后回应含有垃圾字符的 包。TCP连接时会发送含有垃圾字符的数据流直到连接关闭。HACKER利用IP欺骗可以发 动DoS攻击。伪造两个chargen服务器之间的UDP包。同样Fraggle DoS攻击向目标地址的 这个端口广播一个带有伪造受害者IP的数据包,受害者为了回应这些数据而过载。 端口:20、21 服务:FTP 说明:FTP服务器所开放的端口,20用于数据传输,21用于控制。最常见的攻击者用于寻找打开 anonymous的FTP服务器的方法。这些服务器带有可读写的目录。木马Doly Trojan、 Fore、Invisible FTP、WebEx、WinCrash和Blade Runner所开放的端口。 端口:22 服务:Ssh 说明:PcAnywhere建立的TCP和这一端口的连接可能是为了寻找ssh。这一服务有许多弱 点,如果配置成特定的模式,许多使用RSAREF库的版本就会有不少的漏洞存在。 端口:23 服务:Telnet 说明:远程登录,入侵者在搜索远程登录UNIX的服务。大多数情况下扫描这一端口是为 了找到机器运行的操作系统。还有使用其他技术,入侵者也会找到密码。木马Tiny Telnet Server就开放这个端口。 端口:25 服务:SMTP 说明:SMTP服务器所开放的端口,用于发送邮件。入侵者寻找SMTP服务器是为了传递他 们的SPAM。入侵者的帐户被关闭,他们需要连接到高带宽的E-MAIL服务器上,将简单的 信息传递到不同的地址。木马Antigen、Email Password Sender、Haebu Coceda、 Shtrilitz Stealth、WinPC、WinSpy都开放这个端口。 端口:31 服务:MSG Authentication 说明:木马Master Paradise、Hackers Paradise开放此端口。 端口:42 服务:WINS Replication 说明:WINS复制 端口:53 服务:Domain Name Server(DNS) 说明:DNS服务器所开放的端口,入侵者可能是试图进行区域传递(TCP),欺骗DNS (UDP)或隐藏其他的通信。因此防火墙常常过滤或记录此端口。 端口:67 服务:Bootstrap Protocol Server 说明:通过DSL和Cable modem的防火墙常会看见大量发送到广播地址255.255.255.255 的数据。这些机器在向DHCP服务器请求一个地址。HACKER常进入它们,分配一个地址把 自己作为局部路由器而发起大量中间人(man-in-middle)攻击。客户端向68端口广播 请求配置,服务器向67端口广播回应请求。这种回应使用广播是因为客户端还不知道可 以发送的IP地址。 端口:69 服务:Trival File Transfer 说明:许多服务器与bootp一起提供这项服务,便于从系统下载启动代码。但是它们常常由于错误配置而使入侵者能从系统中窃取任何 文件。它们也可用于系统写入文件。 端口:79 服务:Finger Server 说明:入侵者用于获得用户信息,查询操作系统,探测已知的缓冲区溢出错误,回应从自己机器到其他机器Finger扫描。 端口:80 服务:HTTP 说明:用于网页浏览。木马Executor开放此端口。 端口:99 服务:Metagram Relay 说明:后门程序ncx99开放此端口。 端口:102 服务:Message transfer agent(MTA)-X.400 over TCP/IP 说明:消息传输代理。 端口:109 服务:Post Office Protocol -Version3 说明:POP3服务器开放此端口,用于接收邮件,客户端访问服务器端的邮件服务。POP3服务有许多公认的弱点。关于用户名和密码交 换缓冲区溢出的弱点至少有20个,这意味着入侵者可以在真正登陆前进入系统。成功登陆后还有其他缓冲区溢出错误。 端口:110 服务:SUN公司的RPC服务所有端口 说明:常见RPC服务有rpc.mountd、NFS、rpc.statd、rpc.csmd、rpc.ttybd、amd等 端口:113 服务:Authentication Service 说明:这是一个许多计算机上运行的协议,用于鉴别TCP连接的用户。使用标准的这种服务可以获得许多计算机的信息。但是它可作为许多服务的记录器,尤其是FTP、POP、IMAP、SMTP和IRC等服务。通常如果有许多客户通过防火墙访问这些服务,将会看到许多这个端口的连接请求。记住,如果阻断这个端口客户端会感觉到在防火墙另一边与E-MAIL服务器的缓慢连接。许多防火墙支持TCP连接的阻断过程中发回RST。这将会停止缓慢的连接。 端口:119 服务:Network News Transfer Protocol 说明:NEWS新闻组传输协议,承载USENET通信。这个端口的连接通常是人们在寻找USENET服务器。多数ISP限制,只有他们的客户才能访问他们的新闻组服务器。打开新闻组服务器将允许发/读任何人的帖子,访问被限制的新闻组服务器,匿名发帖或发送SPAM。 端口:135 服务:Location Service 说明:Microsoft在这个端口运行DCE RPC end-point mapper为它的DCOM服务。这与UNIX 111端口的功能很相似。使用DCOM和RPC的服务利用计算机上的end-point mapper注册它们的位置。远端客户连接到计算机时,它们查找end-point mapper找到服务的位置。HACKER扫描计算机的这个端口是为了找到这个计算机上运行Exchange Server吗? 什么版本?还有 端口:110 服务:pop3 说明:POP3(Post Office Protocol 3)服务器开放此端口,用于接收邮件,客户端访问服务器端的邮件服务。POP3服务有许多公认的弱点。关于用户名和密码交换缓冲区溢出的弱点至少有20个,这意味着入侵者可以在真正登陆前进入系统。成功登陆后还有其他缓冲区溢出错误。 端口:137、138、139 服务:NETBIOS Name Service 说明:其中137、138是UDP端口,当通过网上邻居传输文件时用这个端口。而139端口:通过这个端口进入的连接试图获得NetBIOS/SMB服务。这个协议被用于windows文件和打印机共享和SAMBA。还有WINS Regisrtation也用它。 端口:143 服务:Interim Mail Access Protocol v2 说明:和POP3的安全问题一样,许多IMAP服务器存在有缓冲区溢出漏洞。记住:一种LINUX蠕虫(admv0rm)会通过这个端口繁殖,因此许多这个端口的扫描来自不知情的已经被感染的用户。当REDHAT在他们的LINUX发布版本中默认允许IMAP后,这些漏洞变的很流行。这一端口还被用于IMAP2,但并不流行。 端口:161 服务:SNMP 说明:SNMP允许远程管理设备。所有配置和运行信息的储存在数据库中,通过SNMP可获得这些信息。许多管理员的错误配置将被暴露在Internet。Cackers将试图使用默认的密码public、private访问系统。他们可能会试验所有可能的组合。SNMP包可能会被错误的指向用户的网络。 端口:389 服务:LDAP、ILS 说明:轻型目录访问协议和NetMeeting Internet Locator Server共用这一端口。 端口:443 服务:Https 说明:网页浏览端口,能提供加密和通过安全端口传输的另一种HTTP。 端口:993 服务:IMAP 说明:SSL(Secure Sockets layer) 端口:1080 服务:SOCKS代理协议服务器 说明:SOCKS代理协议服务器 端口:1433 服务:SQL 说明:Microsoft的SQL服务开放的端口。 端口:1503 服务:NetMeeting T.120 说明:NetMeeting T.120 端口:1521 服务:Oracle 数据库 说明:Oracle 数据库 端口:1720 服务:NetMeeting 说明:NetMeeting H.233 call Setup。 端口:1731 服务:NetMeeting Audio Call Control 说明:NetMeeting音频调用控制。 端口:3306 服务:MySQL 数据库 说明:MySQL 数据库 端口:3389 服务:超级终端 说明:WINDOWS 2000终端开放此端口。 端口:4000 服务:QQ客户端 说明:腾讯QQ客户端开放此端口。 端口:5631 服务:pcAnywere 说明:有时会看到很多这个端口的扫描,这依赖于用户所在的位置。当用户打开pcAnywere时,它会自动扫描局域网C类网以寻找可能的代理(这里的代理是指agent而不是proxy)。入侵者也会寻找开放这种服务的计算机。,所以应该查看这种扫描的源地址。一些搜寻pcAnywere的扫描包常含端口22的UDP数据包。 端口:6970 服务:RealAudio 说明:RealAudio客户将从服务器的6970-7170的UDP端口接收音频数据流。这是由TCP-7070端口外向控制连接设置的。 端口:7323 服务: 说明:Sygate服务器端。 端口:8000 服务:OICQ 说明:腾讯QQ服务器端开放此端口。 端口:8010 服务:Wingate 说明:Wingate代理开放此端口。 端口:8080 服务:代理端口 说明:WWW代理开放此端口 传输层协议 传输层定义了主机应用程序之间端到端的连通性。传输层中最为常见的两个协议分别是传输控制协议TCP(Transmission Control Protocol)和用户数据包协议UDP(User Datagram Protocol)。 TCP TCP是一种面向连接的传输层协议,可提供可靠的传输服务。 TCP位于TCP/IP模型的传输层,它是一种面向连接的端到端协议。TCP作为传输控制协议,可以为主机提供可靠的数据传输。在本例中,两台主机在通信之前,需要TCP在它们之间建立可靠的传输通道。 TCP端口号 端口号用来区分不同的网络服务。 TCP允许一个主机同时运行多个应用进程。每台主机可以拥有多个应用端口,每对端口号、源和目标IP地址的组合唯一地标识了一个会话。端口分为知名端口和动态端口。有些网络服务会使用固定的端口,这类端口称为知名端口,端口号范围为0-1023。如FTP、HTTP、Telnet、SNMP服务均使用知名端口。动态端口号范围从1024到65535,这些端口号一般不固定分配给某个服务,也就是说许多服务都可以使用这些端口。只要运行的程序向系统提出访问网络的申请,那么系统就可以从这些端口号中分配一个供该程序使用。 TCP头部 TCP通常使用IP作为网络层协议,这时TCP数据段被封装在IP数据包内。 TCP数据段由TCP Header(头部)和TCP Data(数据)组成。TCP最多可以有60个字节的头部,如果没有Options字段,正常的长度是20字节。 TCP Header是由如上图标识的一些字段组成,这里列出几个常用字段。 16位源端口号:源主机的应用程序使用的端口号。 16位目的端口号:目的主机的应用程序使用的端口号。每个TCP头部都包含源和目的端的端口号,这两个值加上IP头部中的源IP地址和目的IP地址可以唯一确定一个TCP连接。 32位序列号:用于标识从发送端发出的不同的TCP数据段的序号。数据段在网络中传输时,它们的顺序可能会发生变化;接收端依据此序列号,便可按照正确的顺序重组数据。 32位确认序列号:用于标识接收端确认收到的数据段。确认序列号为成功收到的数据序列号加1。 4位头部长度:表示头部占32bit字的数目,它能表达的TCP头部最大长度为60字节。 16位窗口大小:表示接收端期望通过单次确认而收到的数据的大小。由于该字段为16位,所以窗口大小的最大值为65535字节,该机制通常用来进行流量控制。 16位校验和:校验整个TCP报文段,包括TCP头部和TCP数据。该值由发送端计算和记录并由接收端进行验证。 TCP建立连接的过程 TCP通过三次握手建立可靠连接。 TCP是一种可靠的,面向连接的全双工传输层协议。 TCP连接的建立是一个三次握手的过程。如图所示: 主机A(通常也称为客户端)发送一个标识了SYN的数据段,表示期望与服务器A建立连接,此数据段的序列号(seq)为a。 服务器A回复标识了SYN+ACK的数据段,此数据段的序列号(seq)为b,确认序列号为主机A的序列号加1(a+1),以此作为对主机A的SYN报文的确认。 主机A发送一个标识了ACK的数据段,此数据段的序列号(seq)为a+1,确认序列号为服务器A的序列号加1(b+1),以此作为对服务器A的SYN报文的确认。 TCP传输过程 TCP的可靠传输还体现在TCP使用了确认技术来确保目的设备收到了从源设备发来的数据,并且是准确无误的。 确认技术的工作原理如下: 目的设备接收到源设备发送的数据段时,会向源端发送确认报文,源设备收到确认报文后,继续发送数据段,如此重复。 如图所示,主机A向服务器A发送TCP数据段,为描述方便假定每个数据段的长度都是500个字节。当服务器A成功收到序列号是M+1499的字节以及之前的所有字节时,会以序列号M+1499+1=M+1500进行确认。另外,由于数据段N+3传输失败,所以服务器A未能收到序列号为M+1500的字节,因此服务器A还会再次以序列号M+1500进行确认。 TCP流量控制 TCP滑动窗口技术通过动态改变窗口大小来实现对端到端设备之间的数据传输进行流量控制。 如图所示,主机A和服务器A之间通过滑动窗口来实现流量控制。为方便理解,此例中只考虑主机A发送数据给服务器A时,服务器A通过滑动窗口进行流量控制。 主机A向服务器发送4个长度为1024字节的数据段,其中主机的窗口大小为4096个字节。服务器A收到第3个数据段后,缓存区满,第4个数据段被丢弃。服务器以ACK 3073响应,窗口大小调整为3072,表明服务器的缓冲区只能处理3072个字节的数据段。于是主机A改变其发送速率,发送窗口大小为3072的数据段。 TCP关闭连接 主机在关闭连接之前,要确认收到来自对方的ACK。 TCP支持全双工模式传输数据,这意味着同一时刻两个方向都可以进行数据的传输。在传输数据之前,TCP通过三次握手建立的实际上是两个方向的连接,因此在传输完毕后,两个方向的连接必须都关闭。 TCP连接的建立是一个三次握手的过程,而TCP连接的终止则要经过四次握手。 如图所示: 主机A想终止连接,于是发送一个标识了FIN,ACK的数据段,序列号为a,确认序列号为b。 服务器A回应一个标识了ACK的数据段,序列号为b,确认序号为a+1,作为对主机A的FIN报文的确认。 服务器A想终止连接,于是向主机A发送一个标识了FIN,ACK的数据段,序列号为b,确认序列号为a+1。 主机A回应一个标识了ACK的数据段,序列号为a+1,确认序号为b+1,作为对服务器A的FIN报文的确认。 以上四次交互便完成了两个方向连接的关闭。 UDP UDP是一种面向无连接的传输层协议,传输可靠性没有保证。 当应用程序对传输的可靠性要求不高,但是对传输速度和延迟要求较高时,可以用UDP协议来替代TCP协议在传输层控制数据的转发。UDP将数据从源端发送到目的端时,无需事先建立连接。UDP采用了简单、易操作的机制在应用程序间传输数据,没有使用TCP中的确认技术或滑动窗口机制,因此UDP不能保证数据传输的可靠性,也无法避免接收到重复数据的情况。 UDP头部 UDP头部仅占8字节,传输数据时没有确认机制。 UDP报文分为UDP报文头和UDP数据区域两部分。报头由源端口、目的端口、报文长度以及校验和组成。UDP适合于实时数据传输,如语音和视频通信。相比于TCP,UDP的传输效率更高、开销更小,但是无法保障数据传输的可靠性。UDP头部的标识如下: 16位源端口号:源主机的应用程序使用的端口号。 16位目的端口号:目的主机的应用程序使用的端口号。 16位UDP长度:是指UDP头部和UDP数据的字节长度。因为UDP头部长度为8字节,所以该字段的最小值为8。 16位UDP校验和:该字段提供了与TCP校验字段同样的功能;该字段是可选的。 UDP传输过程 使用UDP传输数据时,由应用程序根据需要提供报文到达确认、排序、流量控制等功能。 主机A发送数据包时,这些数据包是以有序的方式发送到网络中的,每个数据包独立地在网络中被发送,所以不同的数据包可能会通过不同的网络路径到达主机B。这样的情况下,先发送的数据包不一定先到达主机B。因为UDP数据包没有序号,主机B将无法通过UDP协议将数据包按照原来的顺序重新组合,所以此时需要应用程序提供报文的到达确认、排序和流量控制等功能。通常情况下,UDP采用实时传输机制和时间戳来传输语音和视频数据。 UDP不提供重传机制,占用资源小,处理效率高。 一些时延敏感的流量,如语音、视频等,通常使用UDP作为传输层协议。 UDP适合传输对时延敏感的流量,如语音和视频。 在使用TCP协议传输数据时,如果一个数据段丢失或者接收端对某个数据段没有确认,发送端会重新发送该数据段。 TCP重新发送数据会带来传输延迟和重复数据,降低了用户的体验。对于时延敏感的应用,少量的数据丢失一般可以被忽略,这时使用UDP传输将能够提升用户的体验。 数据转发过程 TCP/IP协议簇和底层协议配合,保证了数据能够实现端到端的传输。数据传输过程是一个非常复杂的过程,例如数据在转发的过程中会进行一系列的封装和解封装。对于网络工程师来说,只有深入地理解了数据在各种不同设备上的转发过程,才能够对网络进行正确的分析和检测。 数据转发过程概述 数据包在相同网段内或不同网段之间转发所依据的原理基本一致。 数据可以在同一网络内或者不同网络间传输,数据转发过程也分为本地转发和远程转发,但两者的数据转发原理是基本一样的,都是遵循TCP/IP协议簇。 本示例中,主机A需要访问服务器A的Web服务,并且假定两者之间已经建立了TCP连接。接下来会以此示例来讲解数据在不同网络间的传输过程。 TCP封装 当主机建立了到达目的地的TCP连接后,便开始对应用层数据进行封装。 主机A会对待发送的应用数据首先执行加密和压缩等相关操作,之后进行传输层封装。Web应用是基于传输层的TCP协议传输数据的。主机A使用TCP进行报文封装时,必须填充源端口和目的端口字段,初始序列号和确认序列号字段,标识位,窗口字段以及校验和字段。此例中数据段的源端口号为主机A随机选择的1027号端口,目的端口号为服务器A的TCP知名端口80。 IP封装 主机A完成传输层封装后,一般会进行网络层数据封装,在使用IP进行封装时,需要明确IP报文的源和目的地址。如果IP报文的大小大于网络的最大传输单元(MTU),则该报文有可能在传输过程中被分片。 生存时间(TTL)字段用来减少网络环路造成的影响。ARG3系列路由器产生的数据包,默认TTL值为255。路由器转发一个数据包时,该值会被减1,如果路由器发现该值被减为0,就会丢弃该数据包。这样,即使网络中存在环路,数据包也不会在网络上一直被转发。 协议字段标识了传输层所使用的协议。本例中,传输层使用的是TCP协议,所以该字段的填充值为0X06。 查找路由 主机A必须要拥有到达目的地的路由。 每个主机都会独自维护各自的路由表项。主机A在发送数据前需要先检查是否能够到达目的端,这个过程是通过查找路由来完成的。在此示例中,主机A拥有一条到达“任何网络”(在IP编址部分已经简要介绍过)的路由,它发往其他网络的数据都会通过IP地址为10.1.1.1的接口转发到下一跳,即网关10.1.1.254。 ARP 通过ARP缓存表找到下一跳的MAC地址。 如果表项里没有下一跳的MAC地址,主机A会发送ARP请求。 接下来,由于数据包要被封装成数据帧,所以主机A需要获取下一跳的MAC地址,也就是网关的MAC地址。主机首先会查询ARP缓存表。本例中,主机A的ARP缓存表中存在网关MAC地址的表项。 如果没有查找到网关的MAC地址表项,主机A会通过发送ARP请求来获取网关的MAC地址。 以太网封装 主机A在链路层封装数据帧时,会遵循IEEE 802.3或Ethernet_II标准,Ethernet_II帧头中的类型字段填充为0x0800,以表示网络层使用的是IP协议。源MAC地址为主机A的MAC地址,目的MAC地址为网关路由器E0/0接口的MAC地址。 数据帧转发过程 主机工作在半双工状态下,所以会使用CSMA/CD来检测链路是否空闲。 前导码用于使接收者进入同步状态,定界符用于指示帧的开始。 主机A工作在半双工状态下,所以会使用CSMA/CD来检测链路是否空闲。如果链路空闲,主机A会将一个前导码(Preamble)和一个帧首定界符(SFD)附加到帧头然后进行传输。前导码的作用是使接收设备进行同步并做好接收数据帧的准备。前导码是包括了7个字节的二进制“1”、“0”交替的代码,即1010…10共56位。帧首定界符是长度为1个字节的10101011二进制序列,它的作用是使接收端对帧的第一位进行定位。 同一个冲突域里的设备都会接收到主机A发送的数据帧。 只有网关(RTA)会处理该数据帧,并继续转发。 本例中,主机A发送数据帧到共享以太网,此网络中的所有网络设备都会收到该帧。设备收到帧之后,首先会进行FCS校验。如果FCS校验未能通过,则帧被立即丢弃。对于通过了FCS校验的帧,设备会检查帧中的目的MAC地址。如果帧中的目的MAC地址与自己的MAC地址不同,设备将丢弃帧,如果相同,则会继续处理。处理过程中,帧头帧尾会被剥去(也就是解封装),剩下的数据报文会被根据帧头中的类型字段的值来送到网络层中的对应协议模块去处理。 数据包转发过程 网关检查是否具有到达目的网络的路由条目。 如果存在转发路径,则为数据包添加一个新的二层帧头和帧尾,并继续转发。 RTA收到此数据报文后,网络层会对该报文进行处理。RTA首先根据IP头部信息中的校验和字段,检查IP数据报文头部的完整性,然后根据目的IP地址查看路由表,确定是否能够将数据包转发到目的端。RTA还必须对TTL的值进行处理。另外,报文大小不能超过MTU值。如果报文大小超过MTU值,则报文将被分片。 网络层处理完成后,报文将被送到数据链路层重新进行封装,成为一个新的数据帧,该帧的头部会封装新的源MAC地址和目的MAC地址。如果当前网络设备不知道下一跳的MAC地址,将会使用ARP来获得。 数据帧解封装 RTB以服务器A的MAC地址作为目的MAC继续转发。 服务器A接收到该数据帧后,发现目的MAC为自己的MAC,于是会继续处理该数据帧。 该示例中,服务器A处于一个共享以太网中,两台服务器都会收到RTB发送的数据帧。该帧的目的MAC地址与服务器B的接口MAC地址不匹配,所以会被服务器B丢弃。 服务器A成功收到该帧,并通过FCS校验。服务器A将利用帧中的类型字段来识别在网络层处理该数据的协议。该示例中,服务器A会将解封装后的此数据交给网络层的IP协议来进行处理。 数据包解封装 服务器A检查数据包的目的IP地址,发现目的IP与自己的IP地址相同。 服务器A剥掉数据包的IP头部后,会送往上层协议TCP继续进行处理。 服务器A通过IP协议来处理该报文,首先会通过校验和字段来验证报文头的完整性,然后检查IP报文头中的目的IP地址是否与自己当前的IP地址匹配。 如果在源与目的之间的数据传输期间数据发生了报文分片,则报文会被目的端重新组合。标识字段用于标识属于同一数据源的分片报文,偏移量表示该分片在原分组中的相对位置。标志字段目前只有两位有意义,标志字段最低位为1,表示后面还有分片,为0表示这已经是最后一个数据片;中间一位为1表示不能分片,为0表示允许分片。所有的分片报文必须被目的端全部接收到后才会进行重新组合。 协议字段表示此数据包携带的上层数据是哪种协议的数据。需要注意的是,下一个报头并非总是传输层报头。例如,ICMP报文也是使用IP协议封装,协议字段值为0x01。 数据段解封装 服务器A检查TCP头部的目的端口,然后将数据段发送给应用层的HTTP协议进行处理。 当IP报文头被处理完并剥离后,数据段会被发送到传输层进行处理。在此示例中,传输层协议使用的是TCP,且发送端和接收端已经通过三次握手建立了连接。传输层收到该数据段后,TCP协议会查看并处理该数据段头部信息,其中目的端口号为80,用于表示处理该数据的应用层协议为HTTP协议。TCP处理完头部信息后会将此数据段头部进行剥离,然后将剩下的应用数据发送到HTTP协议进行处理。

Plan of Jekyll Blog - Episode 1: Jekyll-3 on Ubuntu and Nginx

This tutorial you will learn how to configure Jekyll-3 on Ubuntu 16.04 and Nginx. Jekyll On Ubuntu The release of the Ubuntu is Ubuntu 16.04 LTS (Xenial Xerus). You can follow this link Jekyll on Ubuntu to configure Jekyll. Before we install Jekyll, we need to install Ruby and other dependencies: sudo apt-get install ruby-full build-essential zlib1g-dev Caution: sudo apt-get install ruby-full command on Ubuntu 16.04 even 18.04 can only install Ruby2.5, it might take some unsuspected problem. Avoid installing RubyGems packages (called gems) as the root user. (Well, AliCloud only provides root account and I am lazy about adding a new user account. So I think it doesn’t matter to use root account.) Instead, set up a gem installation directory for your user account. The following commands will add environment variables to your ~/.bashrc file to configure the gem installation path: echo '# Install Ruby Gems to ~/gems' >> ~/.bashrc echo 'export GEM_HOME="$HOME/gems"' >> ~/.bashrc echo 'export PATH="$HOME/gems/bin:$PATH"' >> ~/.bashrc source ~/.bashrc Finally, install Jekyll and Bundler: gem install jekyll bundler You can make a quick start for surfing the Jekyll blog: jekyll new my-awesome-site cd my-awesome-site bundle exec jekyll serve # => Now browse to http://localhost:4000 Or, clone the repository from GitHub and build Jekyll blog: git clone https://github.com/IcingTomato/blog.old.git cd blog.old jekyll serve # => Now browse to http://localhost:4000 Liquid warning If you follow the step, congratulation, you cannot see any blog in your website. Just like this: Well, because Ruby2.5 made its Bundler incompatible with Jekyll-4.0, so we have to uninstall Jekyll-4.0 and install the previous versions. gem uninstall jekyll gem install jekyll -v 3.1.3 Jekyll Serve jekyll serve will publish your site on localhost:4000. But you can change the port: jekyll serve --port 4001 But you can’t visit it on public network. So you can: jekyll serve -w --host=0.0.0.0 After doing these jobs, we can’t visit our website ethier. Nginx Configuration Because Jekyll uses port 4000, so we will make a Nginx “proxy” from port 4000 to port 80. First, install Nginx: sudo apt-get install nginx Then, use your favourite text editor to edit /etc/nginx/conf.d/jekyll.conf: root@blog:~# sudo nano /etc/nginx/conf.d/jekyll.conf server { listen 80; server_name your.domain; location / { proxy_pass http://localhost:4000; } } Finally, Ctrl+O and Ctrl+X to save and exit, then restart nginx.service: sudo systemctl restart nginx jekyll serve --detach

What can One-Line-Python-Code do?

Simple is better than complex. Complex is better than complicated. Flat is better than nested. Sparse is better than dense. We can use Python Online Console to run the demo. Heart print('\n'.join([''.join([('ILOVEYOU'[(x-y) % len('ILOVEYOU')] if ((x*0.05)**2+(y*0.1)**2-1)**3-(x*0.05)**2*(y*0.1)**3 <= 0 else' ') for x in range(-30, 30)]) for y in range(30, -30, -1)])) Eight Queens [__import__('sys').stdout.write('\n'.join('.' * i + 'Q' + '.' * (8-i-1) for i in vec) + "\n========\n") for vec in __import__('itertools').permutations(range(8)) if 8 == len(set(vec[i]+i for i in range(8))) == len(set(vec[i]-i for i in range(8)))] Fibonacci print([x[0] for x in [(a[i][0], a.append([a[i][1], a[i][0]+a[i][1]])) for a in ([[1, 1]], ) for i in range(30)]])

Google Pixel XL 折腾记

我在18年的时候,攒了好久的一笔钱,买了 Pixel XL,换掉了可怜的 iPhone5s,然后就开始了我的折腾之路。 前言 手机摔个稀碎,没法拍图了 Pixel/Pixel XL V版解锁教程 退出谷歌账号,取消屏幕密码,删除指纹 把设备恢复出厂设置 跳过设置向导所有东西,不要连接wifi 进入开发者选项开启USB调试,并授权电脑访问 电脑使用adb命令 adb shell pm uninstall --user 0 com.android.phone adb reboot 比较关键的一步,国内很多人卡在这里,主要是要让手机可以访问谷歌并且消除wifi上的X号,我的方法是先使用adb命令(或者科学上网法) adb shell settings put global captive_portal_https_url https://www.google.cn/generate_204 然后开关飞行模式,再开启外网,再连接wifi,就可以消除信号x了 设置锁屏密码为图形,再通过桌面的google程序登录谷歌账号 这时候再回去开发者选项就会看到OEM解锁可以开了 打开OEM解锁重启进入BootLoader 电源+音量+ 或 adb reboot bootloader 使用adb命令 fastboot oem unlock 或 fastboot flashing unlock 然后手机会进入一个界面,音量键选择YES(将会清除所有数据) 清除完了之后手机会跳回到BootLoader界面,可以看到底下已经是UNLOCK状态了 然后重启手机,再进入开发者选项,就会看到OEM解锁那里已经是解锁状态了 如何进入Bootloader界面? 方法一:手机关机,等待3秒钟,按住音量下1秒,不要松开,然后按住电源键,等待手机振动,手机进入bootloader界面。 方法二:手机关机,等待3秒钟,按住音量下1秒,链接电源线,等待手机振动,手机进入bootloader界面。 方法三:手机开机状态下,打开usb调试,在cmd窗口中手动输入 adb reboot bootloader 线刷包刷机教程 在谷歌Pixel镜像下载对应的线刷包 然后把所有的文件放到同一个文件夹内 手机进入Bootloader界面,链接电脑 这时候可以确认驱动是不是安装好了,打开设备管理器 双击刚才那个文件夹下的 flash-all.bat,开始刷机(刷机会清除所有数据哦) 刷机过程中手机会重启多次,请静静等待结束 为什么要解锁 Bootloader? 解锁 Bootloader 是打开玩机之门的钥匙。只有在 Bootloader 解锁的前提下,我们接下来的步骤才得以顺利进行:安装 TWRP、获取 Root 权限……最后,通过一些需要 Root 权限的特殊手段,打开位置历史记录功能,获得近乎完整的 Android 体验。 让我们直奔正题吧: (注意:解锁 Bootloader 重置设备数据,请注意备份) 首先,我们需要安装必(ni)备(dong)的工具并成功接入互联网。 前往「设置-关于手机-版本号」,猛击版本号 7 次开启「开发者选项」。 在开发者选项中开启「OEM 解锁」。若该选项显示为灰色,请检查你的网络连接是否正(ke)常(xue)USB 调试。 在 Pixel 设备中,开启 OEM 解锁需要连网 通过数据线将手机连接至电脑,以管理员身份运行命令提示符(CMD),输入 adb shell 并回车。 此时,手机端会弹出 USB 调试申请,点击「允许」。 回到命令提示符窗口,键入 adb reboot bootloader 并回车,手机会立即重启至 Bootloader 模式。 在 Bootloader 解锁界面中,使用音量键 +/- 来控制光标,选择「Yes」并按下电源键来进行 Bootloader 解锁。 稍等片刻之后,你的设备会自动重启,开机时屏幕下方出现一把打开的小锁,那 Bootloader 就解锁成功啦。 刷入第三方 Recovery —— TWRP 如果说 Bootloader 是玩机大门上的那把锁,那么 TWRP 就是你打开玩机之门后的领路人。 所以在解锁 Bootloader 之后,紧接着要做的事情就是刷入第三方 Recovery —— TWRP。由于 Pixel 与 Nexus 的系统分区方式不同,刷入 TWRP 的方式也略为复杂一些: 首先,前往 TWRP 官网下载最新版 TWRP 压缩包(.zip)和临时 TWRP 镜像文件(.img)。 将 .img 文件留在电脑上,同时将 .zip 文件拷贝至内置储存。 重启手机至 Fastboot 模式(参考上面的方法或关机后长按「电源」和「音量 -」),在电脑以管理员身份运行命令提示符。 在命令提示符窗口中输入 cd 你的 .img 文件路径 来进行定向(比如我的 .img 文件放在 E:\Android 下,那就键入 cd E:\Android 并回车),然后输入 fastboot boot xxx.img //xxx 为具体的文件名 此时手机会重启至临时 TWRP。 在临时 TWRP 中,选择「Install」,找到我们事先放在内置储存中的压缩包文件,点击刷入。 至此,我们就已经用第三方 TWRP 替换了系统自带的 Recovery,接下来的 Root 操作也得以进行: 前往 SuperSU 官网下载最新版 SuperSU 压缩包。 将压缩包拷贝至手机内置储存。 重启手机至 TWRP(关机后长按「电源」和「音量+」),找到 SuperSU 压缩包并刷入。 完成后重启手机,首次启动过程会自动中断并再次自动重启,不用担心。开机后,手机便已获得 Root 权限,SuperSU 权限管理应用也已经安装至系统当中。 做好这些工作之后,我们就可以进一步使用 LocationReportEnabler 等需要 Root 权限的应用来开启位置记录报告功能,最终获得完整的 Android 生态体验了。 如何进行系统更新 非 Root 用户很难对系统文件进行修改,但在进行系统更新时则较为轻松,在保证网络条件畅(ke)通(xue)的前提下,只需前往「设置-关于-系统更新」,便可自动检查、下载并升级至最新版本的 Android 操作系统。 但对 Root 用户而言,手动刷入工厂镜像进行系统更新的方法则更为稳妥。 首先,我们需要前往 Google 的 Nexus/Pixel 工厂镜像网站找到并下载最新版本的 Android 系统镜像,然后解压。 完整工厂镜像文件压缩包包含了这些内容 如果你是拿到手机想要优先进行系统升级的用户,将手机重启至 Fastboot 模式并连接电脑后,直接运行 flash-all.bat 即可全自动升级至最新版本;如果你想保留升级前的系统数据,则需要在运行 flash-all.bat 前对其进行一些处理。 使用文本文档、Notepad++ 等工具打开 flash-all.bat,找到 fastboot -w update image-marlin-xxxxxx.zip 字段,将其改为 fastboot update image-marlin-nof27b.zip //即去掉 -w 然后保存,即可利用修改后的 flash-all.bat 文件在保留数据的前提下进行系统更新。 另外,在运行 flash-all.bat 进行更新的过程中,很有可能会出现报错。报错文本类似于 archive does not contain 'boot.sig' archive does not contain 'recovery.sig' archive does not contain 'system.sig' 当出现这样的字段时,千万不要终止操作。耐心等待耐心等待耐心等待!只要最后出现 finished. total time: 128.109s Press any key to exit... 即是升级成功,手机也会自动重启进入新系统。 另外,在这个过程中所出现的错误往往和 platform-tools(包含 ADB、fastboot 等)版本过旧有关。所以遇到其他形式的报错也不用惊慌,前往 Google 官方网站 下载安装最新版本的 platform-tools 后,再次执行以上操作步骤即可。 Google 从年初开始提供独立的 platform-tools 下载 与非 root 系统自动更新相比,手动刷入完整版工厂镜像尽管要麻烦不少,但也更加灵活。 举个例子,我的 Pixel XL 到手时的系统版本是 Android 7.1.1,但安全更新补丁停留在去年 10 月。如果采用非 root 系统自动更新的方法,那我总计需要更新五次才能更新至最新的 3 月安全更新补丁。 手动 Fastboot 刷入工厂镜像则简单许多,只要我们刷入的是完整版工厂镜像,就可以无视版本跨度,一次性升级至最新版本。 原生安卓WiFi信号去叹号去叉教程5.0-Android P Captive Portal是安卓5引入的一种检测网络是否正常连接的机制,制作的非常有创意,通过HTTP返回的状态码是否是204来判断是否成功,如果访问得到了200带网页数据,那你就可能处在一个需要登录验证才能上网的环境里,比如说校园网,再比如说一些酒店提供的客户才能免费使用的WiFi(其实是通过DNS劫持实现的),如果连接超时(根本就连接不上)就在WiFi图标和信号图标上加一个标志,安卓5和6是叹号,安卓7改成一个叉了。只不过默认访问的是谷歌自家的验证服务器,然而由于你懂的原因,就算你连接上了网络也连不上这个服务器… 嗯…那其实还是没有连接上网络嘛… 噫…. 谷歌设计了一个开关来控制是否启用这个特性,同时也提供了一个变量来控制待验证的服务器地址,国内的修改版ROM通常都改成了高通中国的地址,还有一些ROM设计了代码在重启的时候恢复这个设置,不知道是出于什么目的。 没更新7.0的时候,一直用小狐狸的叹号杀手,很不错的应用,可惜当时他已经很久不更新了,当时安卓N不能用,后来自己做了个小工具,想了想就干脆上架酷安吧,也能帮助大家,这样有了CaptiveMgr工具,这分明就是个没有名字的名字嘛…根本就是foo, bar一样…好像也没什么好叫的了?现在代码还比较乱,要是哪天有空把这堆代码整理出来就开源了算了,毕竟纯粹体力活。 具体的原理不在这里写了,这里主要写如何去掉叹号或者叉标志。 如果有root权限直接用我这个工具算了,比较方便,毕竟用命令也就是检测一下系统然后代替执行命令而已嘛。 (PS: 如果使用SS/SSR可以通过NAT模式让系统直接连接,其内部是通过iptables实现的) 如果没有root权限就得按下面操作了,做好配置以后重启WiFi和数据流量(打开再关闭飞行模式即可)就可以看到效果了。 以下需要ADB调试,配置不赘述 5.0 - 6.x教程 5和6还不支持HTTPS,直接修改即可 检测开关相关: 先处理开关状态,这个变量删除就是默认开启的,删除操作随意执行,反正没影响,删除状态下获取这个变量会返回null。 注意:如果关闭,则无法判断当前网络是否需要登录,无法自动弹出登录页面 adb shell settings delete global captive_portal_server //删除 adb shell settings put global captive_portal_server 0 //禁用 adb shell settings get global captive_portal_server //查询状态 服务器地址相关: adb shell settings delete global captive_portal_server //删除地址就可以恢复默认的谷歌服务器 adb shell settings put global captive_portal_server captive.v2ex.co //设置一个可用地址(高通/V2EX都推荐) adb shell settings get global captive_portal_server //查询当前地址 7.0 - 7.1教程 这两个版本相比5和6没有大的更改,只是默认连接服务器的时候使用HTTPS,但是提供了一个开关用以指定是否使用HTTPS 检测开关相关:同5.0 - 6.x HTTPS开关相关: adb shell settings delete global captive_portal_use_https //删除(直接删除则默认使用HTTPS) adb shell settings put global captive_portal_use_https 0 //禁用HTTPS(写1启用 写0禁用) adb shell settings get global captive_portal_use_https //查询HTTPS开关状态 服务器地址相关:(如果启用了HTTPS需要先确定地址是否支持HTTPS)同5.0 - 6.x 7.1.1教程 这个版本把HTTPS和HTTP两个地址分开保存,并通过7.0加入的HTTPS开关来控制使用哪一个地址。 检测开关相关:同5.0 - 6.x HTTPS开关相关:同7.0 - 7.1 服务器地址相关: adb shell settings delete global captive_portal_https_url //删除(删除默认用HTTPS) adb shell settings delete global captive_portal_http_url 分别修改两个地址 adb shell settings put global captive_portal_http_url http://captive.v2ex.co/generate_204 adb shell settings put global captive_portal_https_url https://captive.v2ex.co/generate_204 7.1.2教程 此版本服务器地址判断逻辑相比7.1.1没有更改,但是检测的开关却变了。 检测开关: #删除变量:(删除以后默认启用) adb shell settings delete global captive_portal_mode #关闭检测: adb shell settings put global captive_portal_mode 0 #查看当前状态: adb shell settings get global captive_portal_mode 服务器地址相关(同7.1.1): #删除(删除默认用HTTPS) adb shell settings delete global captive_portal_https_url adb shell settings delete global captive_portal_http_url #分别修改两个地址 adb shell settings put global captive_portal_http_url http://captive.v2ex.co/generate_204 adb shell settings put global captive_portal_https_url https://captive.v2ex.co/generate_204 8.0.0和8.1.0和9.0(Android P)同上7.1.2,未做修改 隐藏导航栏和状态栏 #全屏沉浸: adb shell settings put global policy_control immersive.full=* #沉浸状态栏: adb shell settings put global policy_control immersive.status=* #沉浸导航栏: adb shell settings put global policy_control immersive.navigation=* #单独控制某一个app不沉浸,比如以下代码设定google实时界面不沉浸,其他程序沉浸: adb shell settings put global policy_control immersive.full=apps,-com.google.android.googlequicksearchbox #恢复到正常方式: adb shell settings put global policy_control null adb 查看android手机中应用的包名和安装位置 #查看是否连接手机 adb devices #进入指定的device的shell adb shell #或者 adb -s ********* shell #adb 查看所有安装的包 adb shell pm list packages #根据某个关键字查找包 pm list packages | grep tencent #查看包安装位置 pm list packages -f #同样可以进行筛选 pm list packages -f | grep tencent #将apk拉到pc中 adb pull /data/app/com.tencent.tbs-1/base.apk ~/Downloads 黑阈 adb -d shell sh /data/data/me.piebridge.brevent/brevent.sh Google Pixel 电信破解 仅适用于一代,无需双清,无需QPST,摆脱繁琐的刷机 在Android Pie下测试通过,重启后不丢失 开发者模式中开启 OEM 解锁后,重启手机进入 bootloader 模式,在电脑端运行 fastboot oem unlock 注:此过程会清除手机数据,请及时备份。 下载modem.img 刷入modem.img fastboot flash modem_a modem.img fastboot flash modem_b modem.img 理论上不支持移动和联通,如更换请重刷官方底包 注意(如果有路由器级别kexue可以忽略) 你需要2018年3月或之前的系统。原因是这样的: 有一个叫frp的分区,它的全称是——Factory Reset Protection。 开机设置读取frp,得知是否首次开机。清除frp,伪装成首次开机。 不幸的是,4月的官方镜像里的开机设置没有这种操作。开机设置才不管frp呢,必须联网。 所以在恢复出厂设置前一定要看看你的安全补丁日期是哪个月的!!如果是4月的需要先刷回3月。 2021年2月21日补记 忘记是哪一次刷机的时候Pixel和Pixel XL的包搞混了,在Pixel上刷了XL的包,结果还能用?就是屏幕的最小宽度(可以在设置 -> 系统 -> 开发者选项 -> 绘图 下面更改)变了好像?

What is TCP? Start with TCP three-way handshake and four-hand wave

说到 TCP 协议,相信大家都比较熟悉了,对于 TCP 协议总能说个一二三来,但是 TCP 协议又是一个非常复杂的协议,其中有不少细节点让人头疼点。本文就是来说说这些头疼点的,浅谈一些 TCP 的疑难杂症。那么从哪说起呢?当然是从三次握手和四次挥手说起啦,可能大家都知道 TCP 是三次交互完成连接的建立,四次交互来断开一个连接,那为什么是三次握手和四次挥手呢?反过来不行吗? 疑症(1)TCP 的三次握手、四次挥手 下面两图大家再熟悉不过了,TCP 的三次握手和四次挥手见下面左边的”TCP 建立连接”、”TCP 数据传送”、”TCP 断开连接”时序图和右边的”TCP 协议状态机” 。 要弄清 TCP 建立连接需要几次交互才行,我们需要弄清建立连接进行初始化的目标是什么。TCP 进行握手初始化一个连接的目标是:分配资源、初始化序列号(通知 peer 对端我的初始序列号是多少),知道初始化连接的目标,那么要达成这个目标的过程就简单了,握手过程可以简化为下面的四次交互: client 端首先发送一个 SYN 包告诉 Server 端我的初始序列号是 X; Server 端收到 SYN 包后回复给 client 一个 ACK 确认包,告诉 client 说我收到了; 接着 Server 端也需要告诉 client 端自己的初始序列号,于是 Server 也发送一个 SYN 包告诉 client 我的初始序列号是 Y; Client 收到后,回复 Server 一个 ACK 确认包说我知道了。 整个过程 4 次交互即可完成初始化,但是,细心的同学会发现两个问题: Server 发送 SYN 包是作为发起连接的 SYN 包,还是作为响应发起者的 SYN 包呢?怎么区分?比较容易引起混淆 Server 的 ACK 确认包和接下来的 SYN 包可以合成一个 SYN ACK 包一起发送的,没必要分别单独发送,这样省了一次交互同时也解决了问题[1].这样 TCP 建立一个连接,三次握手在进行最少次交互的情况下完成了 Peer 两端的资源分配和初始化序列号的交换。 大部分情况下建立连接需要三次握手,也不一定都是三次,有可能出现四次握手来建立连接的。如下图,当 Peer 两端同时发起 SYN 来建立连接的时候,就出现了四次握手来建立连接(对于有些 TCP/IP 的实现,可能不支持这种同时打开的情况)。 在三次握手过程中,细心的同学可能会有以下疑问: 初始化序列号 X、Y 是可以是写死固定的吗,为什么不能呢? 假如 Client 发送一个 SYN 包给 Server 后就挂了或是不管了,这个时候这个连接处于什么状态呢?会超时吗?为什么呢? TCP 进行断开连接的目标是:回收资源、终止数据传输。由于 TCP 是全双工的,需要 Peer 两端分别各自拆除自己通向 Peer 对端的方向的通信信道。这样需要四次挥手来分别拆除通信信道,就比较清晰明了了。 Client 发送一个 FIN 包来告诉 Server 我已经没数据需要发给 Server 了; Server 收到后回复一个 ACK 确认包说我知道了; 然后 server 在自己也没数据发送给 client 后,Server 也发送一个 FIN 包给 Client 告诉 Client 我也已经没数据发给 client 了; Client 收到后,就会回复一个 ACK 确认包说我知道了。 到此,四次挥手,这个 TCP 连接就可以完全拆除了。在四次挥手的过程中,细心的同学可能会有以下疑问: Client 和 Server 同时发起断开连接的 FIN 包会怎么样呢,TCP 状态是怎么转移的? 左侧图中的四次挥手过程中,Server 端的 ACK 确认包能不能和接下来的 FIN 包合并成一个包呢,这样四次挥手就变成三次挥手了。 四次挥手过程中,首先断开连接的一端,在回复最后一个 ACK 后,为什么要进行 TIME_WAIT 呢(超时设置是 2*MSL,RFC793 定义了 MSL 为 2 分钟,Linux 设置成了 30s),在 TIME_WAIT 的时候又不能释放资源,白白让资源占用那么长时间,能不能省了 TIME_WAIT 呢,为什么? 疑症(2)TCP 连接的初始化序列号能否固定 如果初始化序列号(缩写为 ISN:Inital Sequence Number)可以固定,我们来看看会出现什么问题。假设 ISN 固定是 1,Client 和 Server 建立好一条 TCP 连接后,Client 连续给 Server 发了 10 个包,这 10 个包不知怎么被链路上的路由器缓存了(路由器会毫无先兆地缓存或者丢弃任何的数据包),这个时候碰巧 Client 挂掉了,然后 Client 用同样的端口号重新连上 Server,Client 又连续给 Server 发了几个包,假设这个时候 Client 的序列号变成了 5。 接着,之前被路由器缓存的 10 个数据包全部被路由到 Server 端了,Server 给 Client 回复确认号 10,这个时候,Client 整个都不好了,这是什么情况?我的序列号才到 5,你怎么给我的确认号是 10 了,整个都乱了。RFC793 中,建议 ISN 和一个假的时钟绑在一起,这个时钟会在每 4 微秒对 ISN 做加一操作,直到超过 2^32,又从 0 开始,这需要 4 小时才会产生 ISN 的回绕问题,这几乎可以保证每个新连接的 ISN 不会和旧的连接的 ISN 产生冲突。这种递增方式的 ISN,很容易让攻击者猜测到 TCP 连接的 ISN,现在的实现大多是在一个基准值的基础上进行随机的。 疑症(3)初始化连接的 SYN 超时问题 Client 发送 SYN 包给 Server 后挂了,Server 回给 Client 的 SYN-ACK 一直没收到 Client 的 ACK 确认,这个时候这个连接既没建立起来,也不能算失败。这就需要一个超时时间让 Server 将这个连接断开,否则这个连接就会一直占用 Server 的 SYN 连接队列中的一个位置,大量这样的连接就会将 Server 的 SYN 连接队列耗尽,让正常的连接无法得到处理。目前,Linux 下默认会进行 5 次重发 SYN-ACK 包,重试的间隔时间从 1s 开始,下次的重试间隔时间是前一次的双倍,5 次的重试时间间隔为 1s,2s, 4s, 8s,16s,总共 31s,第 5 次发出后还要等 32s 都知道第 5 次也超时了,所以,总共需要 1s + 2s +4s+ 8s+ 16s + 32s =63s,TCP 才会把断开这个连接。 由于,SYN 超时需要 63 秒,那么就给攻击者一个攻击服务器的机会,攻击者在短时间内发送大量的 SYN 包给 Server(俗称 SYN flood 攻击),用于耗尽 Server 的 SYN 队列。对于应对 SYN 过多的问题,linux 提供了几个 TCP 参数:tcp_syncookies、tcp_synack_retries、tcp_max_syn_backlog、tcp_abort_on_overflow 来调整应对。 疑症(4) TCP 的 Peer 两端同时断开连接 由上面的”TCP 协议状态机“图可以看出,TCP 的 Peer 端在收到对端的 FIN 包前发出了 FIN 包,那么该 Peer 的状态就变成了 FIN_WAIT1,Peer 在 FIN_WAIT1 状态下收到对端 Peer 对自己 FIN 包的 ACK 包的话,那么 Peer 状态就变成 FIN_WAIT2,Peer 在 FIN_WAIT2 下收到对端 Peer 的 FIN 包,在确认已经收到了对端 Peer 全部的 Data 数据包后,就响应一个 ACK 给对端 Peer,然后自己进入 TIME_WAIT 状态。 但是如果 Peer 在 FIN_WAIT1 状态下首先收到对端 Peer 的 FIN 包的话,那么该 Peer 在确认已经收到了对端 Peer 全部的 Data 数据包后,就响应一个 ACK 给对端 Peer,然后自己进入 CLOSEING 状态,Peer 在 CLOSEING 状态下收到自己的 FIN 包的 ACK 包的话,那么就进入 TIME WAIT 状态。于是,TCP 的 Peer 两端同时发起 FIN 包进行断开连接,那么两端 Peer 可能出现完全一样的状态转移 FIN_WAIT1——>CLOSEING——->TIME_WAIT,也就会 Client 和 Server 最后同时进入 TIME_WAIT 状态。同时关闭连接的状态转移如下图所示: 疑症(5)四次挥手能不能变成三次挥手呢?? 答案是可能的。TCP 是全双工通信,Cliet 在自己已经不会在有新的数据要发送给 Server 后,可以发送 FIN 信号告知 Server,这边已经终止 Client 到对端 Server 那边的数据传输。但是,这个时候对端 Server 可以继续往 Client 这边发送数据包。于是,两端数据传输的终止在时序上是独立并且可能会相隔比较长的时间,这个时候就必须最少需要 2+2= 4 次挥手来完全终止这个连接。但是,如果 Server 在收到 Client 的 FIN 包后,在也没数据需要发送给 Client 了,那么对 Client 的 ACK 包和 Server 自己的 FIN 包就可以合并成为一个包发送过去,这样四次挥手就可以变成三次了(似乎 linux 协议栈就是这样实现的) 疑症(6) TCP 的头号疼症 TIME_WAIT 状态 要说明 TIME_WAIT 的问题,需要解答以下几个问题 一、Peer 两端,哪一端会进入 TIME_WAIT 呢?为什么? 相信大家都知道,TCP 主动关闭连接的那一方会最后进入 TIME_WAIT。那么怎么界定主动关闭方呢?是否主动关闭是由 FIN 包的先后决定的,就是在自己没收到对端 Peer 的 FIN 包之前自己发出了 FIN 包,那么自己就是主动关闭连接的那一方。对于疑症(4)中描述的情况,那么 Peer 两边都是主动关闭的一方,两边都会进入 TIME_WAIT。为什么是主动关闭的一方进行 TIME_WAIT 呢,被动关闭的进入 TIME_WAIT 可以不呢?我们来看看 TCP 四次挥手可以简单分为下面三个过程: 过程一.主动关闭方发送 FIN; 过程二.被动关闭方收到主动关闭方的 FIN 后发送该 FIN 的 ACK,被动关闭方发送 FIN; 过程三.主动关闭方收到被动关闭方的 FIN 后发送该 FIN 的 ACK,被动关闭方等待自己 FIN 的 ACK。 问题就在过程三中,据 TCP 协议规范,不对 ACK 进行 ACK,如果主动关闭方不进入 TIME_WAIT,那么主动关闭方在发送完 ACK 就走了的话,如果最后发送的 ACK 在路由过程中丢掉了,最后没能到被动关闭方,这个时候被动关闭方没收到自己 FIN 的 ACK 就不能关闭连接,接着被动关闭方会超时重发 FIN 包,但是这个时候已经没有对端会给该 FIN 回 ACK,被动关闭方就无法正常关闭连接了,所以主动关闭方需要进入 TIME_WAIT 以便能够重发丢掉的被动关闭方 FIN 的 ACK。 二、TIME_WAIT 状态是用来解决或避免什么问题呢? TIME_WAIT 主要是用来解决以下几个问题: 上面解释为什么主动关闭方需要进入 TIME_WAIT 状态中提到的: 主动关闭方需要进入 TIME_WAIT 以便能够重发丢掉的被动关闭方 FIN 包的 ACK。如果主动关闭方不进入 TIME_WAIT,那么在主动关闭方对被动关闭方 FIN 包的 ACK 丢失了的时候,被动关闭方由于没收到自己 FIN 的 ACK,会进行重传 FIN 包,这个 FIN 包到主动关闭方后,由于这个连接已经不存在于主动关闭方了,这个时候主动关闭方无法识别这个 FIN 包,协议栈会认为对方疯了,都还没建立连接你给我来个 FIN 包?,于是回复一个 RST 包给被动关闭方,被动关闭方就会收到一个错误(我们见的比较多的:connect reset by peer,这里顺便说下 Broken pipe,在收到 RST 包的时候,还往这个连接写数据,就会收到 Broken pipe 错误了),原本应该正常关闭的连接,给我来个错误,很难让人接受。 防止已经断开的连接 1 中在链路中残留的 FIN 包终止掉新的连接 2(重用了连接 1 的所有的 5 元素(源 IP,目的 IP,TCP,源端口,目的端口)),这个概率比较低,因为涉及到一个匹配问题,迟到的 FIN 分段的序列号必须落在连接 2 的一方的期望序列号范围之内,虽然概率低,但是确实可能发生,因为初始序列号都是随机产生的,并且这个序列号是 32 位的,会回绕。 防止链路上已经关闭的连接的残余数据包(a lost duplicate packet or a wandering duplicate packet) 干扰正常的数据包,造成数据流的不正常。这个问题和 2)类似。 三、TIME_WAIT 会带来哪些问题呢? TIME_WAIT 带来的问题注意是源于:一个连接进入 TIME_WAIT 状态后需要等待 2*MSL(一般是 1 到 4 分钟)那么长的时间才能断开连接释放连接占用的资源,会造成以下问题: 作为服务器,短时间内关闭了大量的 Client 连接,就会造成服务器上出现大量的 TIME_WAIT 连接,占据大量的 tuple,严重消耗着服务器的资源。 作为客户端,短时间内大量的短连接,会大量消耗的 Client 机器的端口,毕竟端口只有 65535 个,端口被耗尽了,后续就无法在发起新的连接了。 由于上面两个问题,作为客户端需要连本机的一个服务的时候,首选 UNIX 域套接字而不是 TCP)。TIME_WAIT 很令人头疼,很多问题是由 TIME_WAIT 造成的,但是 TIME_WAIT 又不是多余的不能简单将 TIME_WAIT 去掉,那么怎么来解决或缓解 TIME_WAIT 问题呢?可以进行 TIME_WAIT 的快速回收和重用来缓解 TIME_WAIT 的问题。有没一些清掉 TIME_WAIT 的技巧呢? 四、TIME_WAIT 的快速回收和重用 【1】TIME_WAIT 快速回收 linux 下开启 TIME_WAIT 快速回收需要同时打开 tcp_tw_recycle 和 tcp_timestamps(默认打开)两选项。Linux 下快速回收的时间为 3.5* RTO(Retransmission Timeout),而一个 RTO 时间为 200ms 至 120s。开启快速回收 TIME_WAIT,可能会带来(问题一、)中说的三点危险,为了避免这些危险,要求同时满足以下三种情况的新连接要被拒绝掉: 来自同一个对端 Peer 的 TCP 包携带了时间戳; 之前同一台 peer 机器(仅仅识别 IP 地址,因为连接被快速释放了,没了端口信息)的某个 TCP 数据在 MSL 秒之内到过本 Server; Peer 机器新连接的时间戳小于 peer 机器上次 TCP 到来时的时间戳,且差值大于重放窗口戳(TCP_PAWS_WINDOW)。 初看起来正常的数据包同时满足下面 3 条几乎不可能,因为机器的时间戳不可能倒流的,出现上述的 3 点均满足时,一定是老的重复数据包又回来了,丢弃老的 SYN 包是正常的。到此,似乎启用快速回收就能很大程度缓解 TIME_WAIT 带来的问题。但是,这里忽略了一个东西就是 NAT。 在一个 NAT 后面的所有 Peer 机器在 Server 看来都是一个机器,NAT 后面的那么多 Peer 机器的系统时间戳很可能不一致,有些快,有些慢。这样,在 Server 关闭了与系统时间戳快的 Client 的连接后,在这个连接进入快速回收的时候,同一 NAT 后面的系统时间戳慢的 Client 向 Server 发起连接,这就很有可能同时满足上面的三种情况,造成该连接被 Server 拒绝掉。所以,在是否开启 tcp_tw_recycle 需要慎重考虑了 【2】TIME_WAIT 重用 linux 上比较完美的实现了 TIME_WAIT 重用问题。只要满足下面两点中的一点,一个 TW 状态的四元组(即一个 socket 连接)可以重新被新到来的 SYN 连接使用。 [1]. 新连接 SYN 告知的初始序列号比 TIME_WAIT 老连接的末序列号大; [2]. 如果开启了 tcp_timestamps,并且新到来的连接的时间戳比老连接的时间戳大。 要同时开启 tcp_tw_reuse 选项和 tcp_timestamps 选项才可以开启 TIME_WAIT 重用,还有一个条件是:重用 TIME_WAIT 的条件是收到最后一个包后超过 1s。细心的同学可能发现 TIME_WAIT 重用对 Server 端来说并没解决大量 TIME_WAIT 造成的资源消耗的问题,因为不管 TIME_WAIT 连接是否被重用,它依旧占用着系统资源。即便如此,TIME_WAIT 重用还是有些用处的,它解决了整机范围拒绝接入的问题,虽然一般一个单独的 Client 是不可能在 MSL 内用同一个端口连接同一个服务的,但是如果 Client 做了 bind 端口那就是同个端口了。时间戳重用 TIME_WAIT 连接的机制的前提是 IP 地址唯一性,得出新请求发起自同一台机器,但是如果是 NAT 环境下就不能这样保证了,于是在 NAT 环境下,TIME_WAIT 重用还是有风险的。 有些同学可能会混淆 tcp_tw_reuse 和 SO_REUSEADDR 选项,认为是相关的一个东西,其实他们是两个完全不同的东西,可以说两个半毛钱关系都没。tcp_tw_reuse 是内核选项,而 SO_REUSEADDR 用户态的选项,使用 SO_REUSEADDR 是告诉内核,如果端口忙,但 TCP 状态位于 TIME_WAIT,可以重用端口。如果端口忙,而 TCP 状态位于其他状态,重用端口时依旧得到一个错误信息,指明 Address already in use”。如果你的服务程序停止后想立即重启,而新套接字依旧使用同一端口,此时 SO_REUSEADDR 选项非常有用。但是,使用这个选项就会有(问题二、)中说的三点危险,虽然发生的概率不大。 五、清掉 TIME_WAIT 的奇技怪巧 可以用下面两种方式控制服务器的 TIME_WAIT 数量: 【1】修改 tcp_max_tw_buckets tcp_max_tw_buckets 控制并发的 TIME_WAIT 的数量,默认值是 180000。如果超过默认值,内核会把多的 TIME_WAIT 连接清掉,然后在日志里打一个警告。官网文档说这个选项只是为了阻止一些简单的 DoS 攻击,平常不要人为的降低它。 【2】利用 RST 包从外部清掉 TIME_WAIT 链接 根据 TCP 规范,收到任何的发送到未侦听端口、已经关闭的连接的数据包、连接处于任何非同步状态(LISTEN,SYS-SENT,SYN-RECEIVED)并且收到的包的 ACK 在窗口外,或者安全层不匹配,都要回执以 RST 响应(而收到滑动窗口外的序列号的数据包,都要丢弃这个数据包,并回复一个 ACK 包),内核收到 RST 将会产生一个错误并终止该连接。我们可以利用 RST 包来终止掉处于 TIME_WAIT 状态的连接,其实这就是所谓的 RST 攻击了。 为了描述方便:假设 Client 和 Server 有个连接 Connect1,Server 主动关闭连接并进入了 TIME_WAIT 状态,我们来描述一下怎么从外部使得 Server 的处于 TIME_WAIT 状态的连接 Connect1 提前终止掉。要实现这个 RST 攻击,首先我们要知道 Client 在 Connect1 中的端口 port1(一般这个端口是随机的,比较难猜到,这也是 RST 攻击较难的一个点),利用 IP_TRANSPARENT 这个 socket 选项,它可以 bind 不属于本地的地址,因此可以从任意机器绑定 Client 地址以及端口 port1,然后向 Server 发起一个连接,Server 收到了窗口外的包于是响应一个 ACK,这个 ACK 包会路由到 Client 处。 这个时候 99%的可能 Client 已经释放连接 connect1 了,这个时候 Client 收到这个 ACK 包,会发送一个 RST 包,server 收到 RST 包然后就释放连接 connect1 提前终止 TIME_WAIT 状态了。提前终止 TIME_WAIT 状态是可能会带来(问题二)中说的三点危害,具体的危害情况可以看下 RFC1337。RFC1337 中建议,不要用 RST 过早的结束 TIME_WAIT 状态。 至此,上面的疑症都解析完毕,然而细心的同学会有下面的疑问: TCP 的可靠传输是确认号来实现的,那么 TCP 的确认机制是怎样的呢?是收到一个包就马上确认,还是可以稍等一下在确认呢? 假如发送一个包,一直都没收到确认呢?什么时候重传呢?超时机制的怎样的? TCP 两端 Peer 的处理能力不对等的时候,比如发送方处理能力很强,接收方处理能力很弱,这样发送方是否能够不管接收方死活狂发数据呢?如果不能,流量控制机制的如何的? TCP 是端到端的协议,也就是 TCP 对端 Peer 只看到对方,看不到网络上的其他点,那么 TCP 的两端怎么对网络情况做出反映呢?发生拥塞的时候,拥塞控制机制是如何的? 疑症(7)TCP 的延迟确认机制 按照 TCP 协议,确认机制是累积的,也就是确认号 X 的确认指示的是所有 X 之前但不包括 X 的数据已经收到了。确认号(ACK)本身就是不含数据的分段,因此大量的确认号消耗了大量的带宽,虽然大多数情况下,ACK 还是可以和数据一起捎带传输的,但是如果没有捎带传输,那么就只能单独回来一个 ACK,如果这样的分段太多,网络的利用率就会下降。为缓解这个问题,RFC 建议了一种延迟的 ACK,也就是说,ACK 在收到数据后并不马上回复,而是延迟一段可以接受的时间,延迟一段时间的目的是看能不能和接收方要发给发送方的数据一起回去,因为 TCP 协议头中总是包含确认号的,如果能的话,就将数据一起捎带回去,这样网络利用率就提高了。 延迟 ACK 就算没有数据捎带,那么如果收到了按序的两个包,那么只要对第二包做确认即可,这样也能省去一个 ACK 消耗。由于 TCP 协议不对 ACK 进行 ACK 的,RFC 建议最多等待 2 个包的积累确认,这样能够及时通知对端 Peer,我这边的接收情况。Linux 实现中,有延迟 ACK 和快速 ACK,并根据当前的包的收发情况来在这两种 ACK 中切换。一般情况下,ACK 并不会对网络性能有太大的影响,延迟 ACK 能减少发送的分段从而节省了带宽,而快速 ACK 能及时通知发送方丢包,避免滑动窗口停等,提升吞吐率。关于 ACK 分段,有个细节需要说明一下,ACK 的确认号,是确认按序收到的最后一个字节序,对于乱序到来的 TCP 分段,接收端会回复相同的 ACK 分段,只确认按序到达的最后一个 TCP 分段。TCP 连接的延迟确认时间一般初始化为最小值 40ms,随后根据连接的重传超时时间(RTO)、上次收到数据包与本次接收数据包的时间间隔等参数进行不断调整。 疑症(8)TCP 的重传机制以及重传的超时计算 【1】TCP 的重传超时计算 TCP 交互过程中,如果发送的包一直没收到 ACK 确认,是要一直等下去吗?显然不能一直等(如果发送的包在路由过程中丢失了,对端都没收到又如何给你发送确认呢?),这样协议将不可用,既然不能一直等下去,那么该等多久呢?等太长时间的话,数据包都丢了很久了才重发,没有效率,性能差;等太短时间的话,可能 ACK 还在路上快到了,这时候却重传了,造成浪费,同时过多的重传会造成网络拥塞,进一步加剧数据的丢失。也是,我们不能去猜测一个重传超时时间,应该是通过一个算法去计算,并且这个超时时间应该是随着网络的状况在变化的。为了使我们的重传机制更高效,如果我们能够比较准确知道在当前网络状况下,一个数据包从发出去到回来的时间 RTT——Round Trip Time,那么根据这个 RTT 我们就可以方便设置 TimeOut——RTO(Retransmission TimeOut)了。 为了计算这个 RTO,RFC793 中定义了一个经典算法,算法如下: [1] 首先采样计算RTT值 [2] 然后计算平滑的RTT,称为Smoothed Round Trip Time (SRTT),SRTT = ( ALPHA * SRTT ) + ((1-ALPHA) * RTT) [3] RTO = min[UBOUND,max[LBOUND,(BETA*SRTT)]] 其中:UBOUND 是 RTO 值的上限;例如:可以定义为 1 分钟,LBOUND 是 RTO 值的下限,例如,可以定义为 1 秒;ALPHA is a smoothing factor (e.g., .8 to .9), and BETA is a delay variance factor(e.g., 1.3 to 2.0). 然而这个算法有个缺点就是:在算 RTT 样本的时候,是用第一次发数据的时间和 ack 回来的时间做 RTT 样本值,还是用重传的时间和 ACK 回来的时间做 RTT 样本值?不管是怎么选择,总会造成会要么把 RTT 算过长了,要么把 RTT 算过短了。如下图:(a)就计算过长了,而(b)就是计算过短了。 针对上面经典算法的缺陷,于是提出 Karn / Partridge Algorithm 对经典算法进行了改进(算法大特点是——忽略重传,不把重传的 RTT 做采样),但是这个算法有问题:如果在某一时间,网络闪动,突然变慢了,产生了比较大的延时,这个延时导致要重转所有的包(因为之前的 RTO 很小),于是,因为重转的不算,所以,RTO 就不会被更新,这是一个灾难。于是,为解决上面两个算法的问题,又有人推出来了一个新的算法,这个算法叫 Jacobson / Karels Algorithm(参看 FC6289),这个算法的核心是:除了考虑每两次测量值的偏差之外,其变化率也应该考虑在内,如果变化率过大,则通过以变化率为自变量的函数为主计算 RTT(如果陡然增大,则取值为比较大的正数,如果陡然减小,则取值为比较小的负数,然后和平均值加权求和),反之如果变化率很小,则取测量平均值。 公式如下:(其中的 DevRTT 是 Deviation RTT 的意思) SRTT = SRTT + α (RTT – SRTT) —— 计算平滑RTT DevRTT = (1-β)*DevRTT + β*(|RTT-SRTT|) ——计算平滑RTT和真实的差距(加权移动平均) RTO= µ * SRTT + ∂ *DevRTT —— 神一样的公式 (其中:在Linux下,α = 0.125,β = 0.25, μ = 1,∂ = 4 ——这就是算法中的“调得一手好参数”,nobody knows why, it just works…) 最后的这个算法在被用在今天的TCP协议中并工作非常好 最后的这个算法在被用在今天的 TCP 协议中并工作非常好。 知道超时怎么计算后,很自然就想到定时器的设计问题。一个简单直观的方案就是为 TCP 中的每一个数据包维护一个定时器,在这个定时器到期前没收到确认,则进行重传。这种设计理论上是很合理的,但是实现上,这种方案将会有非常多的定时器,会带来巨大内存开销和调度开销。既然不能每个包一个定时器,那么多少个包一个定时器才好呢,这个似乎比较难确定。可以换个思路,不要以包量来确定定时器,以连接来确定定时器会不会比较合理呢?目前,采取每一个 TCP 连接单一超时定时器的设计则成了一个默认的选择,并且 RFC2988 给出了每连接单一定时器的设计建议算法规则: [1].每一次一个包含数据的包被发送(包括重发),如果还没开启重传定时器,则开启它,使得它在 RTO 秒之后超时(按照当前的 RTO 值)。 [2]. 当接收到一个 ACK 确认一个新的数据; 如果所有的发出数据都被确认了,关闭重传定时器; [3].当接收到一个 ACK 确认一个新的数据,还有数据在传输,也就是还有没被确认的数据,重新启动重传定时器,使得它在 RTO 秒之后超时(按照当前的 RTO 值)。 当重传定时器超时后,依次做下列 3 件事情: [4.1]. 重传最早的尚未被 TCP 接收方 ACK 的数据包; [4.2]. 重新设置 RTO 为 RTO *2(“还原定时器”),但是新 RTO 不应该超过 RTO 的上限(RTO 有个上限值,这个上限值最少为 60s); [4.3]. 重启重传定时器。 上面的建议算法体现了一个原则:没被确认的包必须可以超时,并且超时的时间不能太长,同时也不要过早重传。规则[1][3][4.3]共同说明了只要还有数据包没被确认,那么定时器一定会是开启着的(这样满足没被确认的包必须可以超时的原则)。规则[4.2]说明定时器的超时值是有上限的(满足超时的时间不能太长)。 规则[3]说明,在一个 ACK 到来后重置定时器可以保护后发的数据不被过早重传;因为一个 ACK 到来了,说明后续的 ACK 很可能会依次到来,也就是说丢失的可能性并不大。规则[4.2]也是在一定程度上避免过早重传,因为,在出现定时器超时后,有可能是网络出现拥塞了,这个时候应该延长定时器,避免出现大量的重传进一步加剧网络的拥塞。 【2】TCP 的重传机制 通过上面我们可以知道,TCP 的重传是由超时触发的,这会引发一个重传选择问题,假设 TCP 发送端连续发了 1、2、3、4、5、6、7、8、9、10 共 10 包,其中 4、6、8 这 3 个包全丢失了,由于 TCP 的 ACK 是确认最后连续收到序号,这样发送端只能收到 3 号包的 ACK,这样在 TIME_OUT 的时候,发送端就面临下面两个重传选择: [1].仅重传 4 号包 [2].重传 3 号后面所有的包,也就是重传 4~10 号包 对于,上面两个选择的优缺点都比较明显。方案[1],优点:按需重传,能够最大程度节省带宽。缺点:重传会比较慢,因为重传 4 号包后,需要等下一个超时才会重传 6 号包。方案[2],优点:重传较快,数据能够较快交付给接收端。缺点:重传了很多不必要重传的包,浪费带宽,在出现丢包的时候,一般是网络拥塞,大量的重传又可能进一步加剧拥塞。 上面的问题是由于单纯以时间驱动来进行重传的,都必须等待一个超时时间,不能快速对当前网络状况做出响应,如果加入以数据驱动呢?TCP 引入了一种叫 Fast Retransmit(快速重传)的算法,就是在连续收到 3 次相同确认号的 ACK,那么就进行重传。这个算法基于这么一个假设,连续收到 3 个相同的 ACK,那么说明当前的网络状况变好了,可以重传丢失的包了。 快速重传解决了 timeout 的问题,但是没解决重传一个还是重传多个的问题。出现难以决定是否重传多个包问题的根源在于,发送端不知道那些非连续序号的包已经到达接收端了,但是接收端是知道的,如果接收端告诉一下发送端不就可以解决这个问题吗?于是,RFC2018 提出了 Selective Acknowledgment(SACK,选择确认)机制,SACK 是 TCP 的扩展选项,包括(1)SACK 允许选项(Kind=4,Length=2,选项只允许在有 SYN 标志的 TCP 包中),(2)SACK 信息选项 Kind=5,Length)。一个 SACK 的例子如下图,红框说明:接收端收到了 0-5500,8000-8500,7000-7500,6000-6500 的数据了,这样发送端就可以选择重传丢失的 5500-6000,6500-7000,7500-8000 的包。 SACK 依靠接收端的接收情况反馈,解决了重传风暴问题,这样够了吗?接收端能不能反馈更多的信息呢?显然是可以的,于是,RFC2883 对对 SACK 进行了扩展,提出了 D-SACK,也就是利用第一块 SACK 数据中描述重复接收的不连续数据块的序列号参数,其他 SACK 数据则描述其他正常接收到的不连续数据。这样发送方利用第一块 SACK,可以发现数据段被网络复制、错误重传、ACK 丢失引起的重传、重传超时等异常的网络状况,使得发送端能更好调整自己的重传策略。D-SACK,有几个优点: 1)发送端可以判断出,是发包丢失了,还是接收端的 ACK 丢失了。(发送方,重传了一个包,发现并没有 D-SACK 那个包,那么就是发送的数据包丢了;否则就是接收端的 ACK 丢了,或者是发送的包延迟到达了); 2)发送端可以判断自己的 RTO 是不是有点小了,导致过早重传(如果收到比较多的 D-SACK 就该怀疑是 RTO 小了); 3)发送端可以判断自己的数据包是不是被复制了。(如果明明没有重传该数据包,但是收到该数据包的 D-SACK); 4)发送端可以判断目前网络上是不是出现了有些包被 delay 了,也就是出现先发的包却后到了。 疑症(9)TCP 的流量控制 我们知道 TCP 的窗口(window)是一个 16bit 位字段,它代表的是窗口的字节容量,也就是 TCP 的标准窗口最大为 2^16-1=65535 个字节。另外在 TCP 的选项字段中还包含了一个 TCP 窗口扩大因子,option-kind 为 3,option-length 为 3 个字节,option-data 取值范围 0-14。窗口扩大因子用来扩大 TCP 窗口,可把原来 16bit 的窗口,扩大为 31bit。这个窗口是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。也就是,发送端是根据接收端通知的窗口大小来调整自己的发送速率的,以达到端到端的流量控制。尽管流量控制看起来简单明了,就是发送端根据接收端的限制来控制自己的发送就好了,但是细心的同学还是会有些疑问的。 1)发送端是怎么做到比较方便知道自己哪些包可以发,哪些包不能发呢? 2)如果接收端通知一个零窗口给发送端,这个时候发送端还能不能发送数据呢?如果不发数据,那一直等接收端口通知一个非 0 窗口吗,如果接收端一直不通知呢? 3)如果接收端处理能力很慢,这样接收端的窗口很快被填满,然后接收处理完几个字节,腾出几个字节的窗口后,通知发送端,这个时候发送端马上就发送几个字节给接收端吗?发送的话会不会太浪费了,就像一艘万吨油轮只装上几斤的油就开去目的地一样。对于发送端产生数据的能力很弱也一样,如果发送端慢吞吞产生几个字节的数据要发送,这个时候该不该立即发送呢?还是累积多点在发送? 疑问 1)的解决: 发送方要知道那些可以发,哪些不可以发,一个简明的方案就是按照接收方的窗口通告,发送方维护一个一样大小的发送窗口就可以了,在窗口内的可以发,窗口外的不可以发,窗口在发送序列上不断后移,这就是 TCP 中的滑动窗口。如下图所示,对于 TCP 发送端其发送缓存内的数据都可以分为 4 类: [1]-已经发送并得到接收端 ACK 的; [2]-已经发送但还未收到接收端 ACK 的; [3]-未发送但允许发送的(接收方还有空间); [4]-未发送且不允许发送(接收方没空间了)。 其中,[2]和[3]两部分合起来称之为发送窗口。 下面两图演示的窗口的滑动情况,收到 36 的 ACK 后,窗口向后滑动 5 个 byte。 疑问 2)的解决 由问题 1)我们知道,发送端的发送窗口是由接收端控制的。下图,展示了一个发送端是怎么受接收端控制的。 由上图我们知道,当接收端通知一个 zero 窗口的时候,发送端的发送窗口也变成了 0,也就是发送端不能发数据了。如果发送端一直等待,直到接收端通知一个非零窗口在发数据的话,这似乎太受限于接收端,如果接收端一直不通知新的窗口呢?显然发送端不能干等,起码有一个主动探测的机制。为解决 0 窗口的问题,TCP 使用了 Zero Window Probe 技术,缩写为 ZWP。发送端在窗口变成 0 后,会发 ZWP 的包给接收方,来探测目前接收端的窗口大小,一般这个值会设置成 3 次,每次大约 30-60 秒(不同的实现可能会不一样)。 如果 3 次过后还是 0 的话,有的 TCP 实现就会发 RST 掉这个连接。正如有人的地方就会有商机,那么有等待的地方就很有可能出现 DDoS 攻击点。攻击者可以在和 Server 建立好连接后,就向 Server 通告一个 0 窗口,然后 Server 端就只能等待进行 ZWP,于是攻击者会并发大量的这样的请求,把 Server 端的资源耗尽。 疑问点 3)的解决 疑点 3)本质就是一个避免发送大量小包的问题。造成这个问题原因有二: 1)接收端一直在通知一个小的窗口; 2)发送端本身问题,一直在发送小包。这个问题,TCP 中有个术语叫 Silly Window Syndrome(糊涂窗口综合症)。 解决这个问题的思路有两种,1)接收端不通知小窗口,2)发送端积累一下数据在发送。 思路 1)是在接收端解决这个问题,David D Clark’s 方案,如果收到的数据导致 window size 小于某个值,就 ACK 一个 0 窗口,这就阻止发送端在发数据过来。等到接收端处理了一些数据后 windows size 大于等于了 MSS,或者 buffer 有一半为空,就可以通告一个非 0 窗口。思路 2)是在发送端解决这个问题,有个著名的 Nagle’s algorithm。Nagle 算法的规则: [1]如果包长度达到 MSS ,则允许发送; [2]如果该包含有 FIN ,则允许发送; [3]设置了 TCP_NODELAY 选项,则允许发送; [4]设置 TCP_CORK 选项时,若所有发出去的小数据包(包长度小于 MSS)均被确认,则允许发送; [5]上述条件都未满足,但发生了超时(一般为 200ms ),则立即发送。 规则[4]指出 TCP 连接上最多只能有一个未被确认的小数据包。从规则[4]可以看出 Nagle 算法并不禁止发送小的数据包(超时时间内),而是避免发送大量小的数据包。由于 Nagle 算法是依赖 ACK 的,如果 ACK 很快的话,也会出现一直发小包的情况,造成网络利用率低。TCP_CORK 选项则是禁止发送小的数据包(超时时间内),设置该选项后,TCP 会尽力把小数据包拼接成一个大的数据包(一个 MTU)再发送出去,当然也不会一直等,发生了超时(一般为 200ms),也立即发送。Nagle 算法和 CP_CORK 选项提高了网络的利用率,但是增加是延时。从规则[3]可以看出,设置 TCP_NODELAY 选项,就是完全禁用 Nagle 算法了。 这里要说一个小插曲,Nagle 算法和延迟确认(Delayed Acknoledgement)一起,当出现(write-write-read)的时候会引发一个 40ms 的延时问题,这个问题在 HTTP svr 中体现的比较明显。场景如下: 客户端在请求下载 HTTP svr 中的一个小文件,一般情况下,HTTP svr 都是先发送 HTTP 响应头部,然后在发送 HTTP 响应 BODY(特别是比较多的实现在发送文件的实施采用的是 sendfile 系统调用,这就出现 write-write-read 模式了)。当发送头部的时候,由于头部较小,于是形成一个小的 TCP 包发送到客户端,这个时候开始发送 body,由于 body 也较小,这样还是形成一个小的 TCP 数据包,根据 Nagle 算法,HTTP svr 已经发送一个小的数据包了,在收到第一个小包的 ACK 后或等待 200ms 超时后才能在发小包,HTTP svr 不能发送这个 body 小 TCP 包。 客户端收到 http 响应头后,由于这是一个小的 TCP 包,于是客户端开启延迟确认,客户端在等待 Svr 的第二个包来在一起确认或等待一个超时(一般是 40ms)在发送 ACK 包;这样就出现了你等我、然而我也在等你的死锁状态,于是出现最多的情况是客户端等待一个 40ms 的超时,然后发送 ACK 给 HTTP svr,HTTP svr 收到 ACK 包后在发送 body 部分。大家在测 HTTP svr 的时候就要留意这个问题了。 疑症(10)TCP 的拥塞控制 谈到拥塞控制,就要先谈谈拥塞的因素和本质。本质上,网络上拥塞的原因就是大家都想独享整个网络资源,对于 TCP,端到端的流量控制必然会导致网络拥堵。这是因为 TCP 只看到对端的接收空间的大小,而无法知道链路上的容量,只要双方的处理能力很强,那么就可以以很大的速率发包,于是链路很快出现拥堵,进而引起大量的丢包,丢包又引发发送端的重传风暴,进一步加剧链路的拥塞。另外一个拥塞的因素是链路上的转发节点,例如路由器,再好的路由器只要接入网络,总是会拉低网络的总带宽,如果在路由器节点上出现处理瓶颈,那么就很容易出现拥塞。由于 TCP 看不到网络的状况,那么拥塞控制是必须的并且需要采用试探性的方式来控制拥塞,于是拥塞控制要完成两个任务:[1]公平性;[2]拥塞过后的恢复。 TCP 发展到现在,拥塞控制方面的算法很多,其中 Reno 是目前应用最广泛且较为成熟的算法,下面着重介绍一下 Reno 算法(RFC5681)。介绍该算法前,首先介绍一个概念 duplicate acknowledgment(冗余 ACK、重复 ACK)一般情况下一个 ACK 被称为冗余 ACK,要同时满足下面几个条件(对于 SACK,那么根据 SACK 的一些信息来进一步判断)。 [1] 接收 ACK 的那端已经发出了一些还没被 ACK 的数据包; [2] 该 ACK 没有捎带 data; [3] 该 ACK 的 SYN 和 FIN 位都是 off 的,也就是既不是 SYN 包的 ACK 也不是 FIN 包的 ACK; [4] 该 ACK 的确认号等于接收 ACK 那端已经收到的 ACK 的最大确认号; [5] 该 ACK 通知的窗口等接收该 ACK 的那端上一个收到的 ACK 的窗口。 Reno 算法包含 4 个部分: [1]慢热启动算法 – Slow Start; [2]拥塞避免算法 – Congestion Avoidance; [3]快速重传 - Fast Retransimit; [4]快速恢复算法 – Fast Recovery。 TCP 的拥塞控制主要原理依赖于一个拥塞窗口(cwnd)来控制,根据前面的讨论,我们知道有一个接收端通告的接收窗口(rwnd)用于流量控制;加上拥塞控制后,发送端真正的发送窗口=min(rwnd,cwnd)。关于 cwnd 的单位,在 TCP 中是以字节来做单位的,我们假设 TCP 每次传输都是按照 MSS 大小来发送数据,因此你可以认为 cwnd 按照数据包个数来做单位也可以理解,下面如果没有特别说明是字节,那么 cwnd 增加 1 也就是相当于字节数增加 1 个 MSS 大小。 【1】慢热启动算法 – Slow Start 慢启动体现了一个试探的过程,刚接入网络的时候先发包慢点,探测一下网络情况,然后在慢慢提速。不要一上来就拼命发包,这样很容易造成链路的拥堵,出现拥堵了在想到要降速来缓解拥堵这就有点成本高了,毕竟无数的先例告诫我们先污染后治理的成本是很高的。慢启动的算法如下(cwnd 全称 Congestion Window): 1)连接建好的开始先初始化 cwnd = N,表明可以传 N 个 MSS 大小的数据; 2)每当收到一个 ACK,++cwnd; 呈线性上升; 3)每当过了一个 RTT,cwnd = cwnd*2; 呈指数让升; 4)还有一个慢启动门限 ssthresh(slow start threshold),是一个上限,当 cwnd >= ssthresh 时,就会进入”拥塞避免算法 - Congestion Avoidance”。 根据 RFC5681,如果 MSS > 2190 bytes,则 N = 2;如果 MSS < 1095 bytes,则 N =4;如果 2190 bytes >= MSS >= 1095 bytes,则 N = 3;一篇 Google 的论文《An Argument for Increasing TCP’s Initial Congestion Window》建议把 cwnd 初始化成了 10 个 MSS。Linux 3.0 后采用了这篇论文的建议。 ## 【2】拥塞避免算法 – Congestion Avoidance 慢启动的时候说过,cwnd 是指数快速增长的,但是增长是有个门限 ssthresh(一般来说大多数的实现 ssthresh 的值是 65535 字节)的,到达门限后进入拥塞避免阶段。在进入拥塞避免阶段后,cwnd 值变化算法如下: 1)每收到一个 ACK,调整 cwnd 为 (cwnd + 1/cwnd) * MSS 个字节; 2)每经过一个 RTT 的时长,cwnd 增加 1 个 MSS 大小。 TCP 是看不到网络的整体状况的,那么 TCP 认为网络拥塞的主要依据是它重传了报文段。前面我们说过 TCP 的重传分两种情况: 1)出现 RTO 超时,重传数据包。这种情况下,TCP 就认为出现拥塞的可能性就很大,于是它反应非常’强烈’ [1] 调整门限 ssthresh 的值为当前 cwnd 值的 1/2; [2] reset 自己的 cwnd 值为 1; [3] 然后重新进入慢启动过程。 2)在 RTO 超时前,收到 3 个 duplicate ACK 进行重传数据包。这种情况下,收到 3 个冗余 ACK 后说明确实有中间的分段丢失,然而后面的分段确实到达了接收端,因为这样才会发送冗余 ACK,这一般是路由器故障或者轻度拥塞或者其它不太严重的原因引起的,因此此时拥塞窗口缩小的幅度就不能太大,此时进入快速重传。 【3】快速重传 - Fast Retransimit 做的事情有: 1) 调整门限 ssthresh 的值为当前 cwnd 值的 1/2; 2) 将 cwnd 值设置为新的 ssthresh 的值; 3) 重新进入拥塞避免阶段。 在快速重传的时候,一般网络只是轻微拥堵,在进入拥塞避免后,cwnd 恢复的比较慢。针对这个,“快速恢复”算法被添加进来,当收到 3 个冗余 ACK 时,TCP 最后的[3]步骤进入的不是拥塞避免阶段,而是快速恢复阶段。 【4】快速恢复算法 – Fast Recovery : 快速恢复的思想是“数据包守恒”原则,即带宽不变的情况下,在网络同一时刻能容纳数据包数量是恒定的。当“老”数据包离开了网络后,就能向网络中发送一个“新”的数据包。既然已经收到了 3 个冗余 ACK,说明有三个数据分段已经到达了接收端,既然三个分段已经离开了网络,那么就是说可以在发送 3 个分段了。于是只要发送方收到一个冗余的 ACK,于是 cwnd 加 1 个 MSS。快速恢复步骤如下(在进入快速恢复前,cwnd 和 sshthresh 已被更新为:sshthresh = cwnd /2,cwnd = sshthresh): 1)把 cwnd 设置为 ssthresh 的值加 3,重传 Duplicated ACKs 指定的数据包; 2)如果再收到 duplicated Acks,那么 cwnd = cwnd +1; 3)如果收到新的 ACK,而非 duplicated Ack,那么将 cwnd 重新设置为【3】中 1)的 sshthresh 的值。然后进入拥塞避免状态。 细心的同学可能会发现快速恢复有个比较明显的缺陷就是:它依赖于 3 个冗余 ACK,并假定很多情况下,3 个冗余的 ACK 只代表丢失一个包。但是 3 个冗余 ACK 也很有可能是丢失了很多个包,快速恢复只是重传了一个包,然后其他丢失的包就只能等待到 RTO 超时了。超时会导致 ssthresh 减半,并且退出了 Fast Recovery 阶段,多个超时会导致 TCP 传输速率呈级数下降。出现这个问题的主要原因是过早退出了 Fast Recovery 阶段。 为解决这个问题,提出了 New Reno 算法,该算法是在没有 SACK 的支持下改进 Fast Recovery 算法(SACK 改变 TCP 的确认机制,把乱序等信息会全部告诉对方,SACK 本身携带的信息就可以使得发送方有足够的信息来知道需要重传哪些包,而不需要重传哪些包),具体改进如下: 1)发送端收到 3 个冗余 ACK 后,重传冗余 ACK 指示可能丢失的那个包 segment1,如果 segment1 的 ACK 通告接收端已经收到发送端的全部已经发出的数据的话,那么就是只丢失一个包,如果没有,那么就是有多个包丢失了; 2)发送端根据 segment1 的 ACK 判断出有多个包丢失,那么发送端继续重传窗口内未被 ACK 的第一个包,直到 sliding window 内发出去的包全被 ACK 了,才真正退出 Fast Recovery 阶段。 我们可以看到,拥塞控制在拥塞避免阶段,cwnd 是加性增加的,在判断出现拥塞的时候采取的是指数递减。为什么要这样做呢?这是出于公平性的原则,拥塞窗口的增加受惠的只是自己,而拥塞窗口减少受益的是大家。这种指数递减的方式实现了公平性,一旦出现丢包,那么立即减半退避,可以给其他新建的连接腾出足够的带宽空间,从而保证整个的公平性。 至此,TCP 的疑难杂症基本介绍完毕了,总的来说 TCP 是一个有连接的、可靠的、带流量控制和拥塞控制的端到端的协议。TCP 的发送端能发多少数据,由发送端的发送窗口决定(当然发送窗口又被接收端的接收窗口、发送端的拥塞窗口限制)的,那么一个 TCP 连接的传输稳定状态应该体现在发送端的发送窗口的稳定状态上,这样的话,TCP 的发送窗口有哪些稳定状态呢?TCP 的发送窗口稳定状态主要有上面三种稳定状态: 【1】接收端拥有大窗口的经典锯齿状 大多数情况下都是处于这样的稳定状态,这是因为,一般情况下机器的处理速度就是比较快,这样 TCP 的接收端都是拥有较大的窗口,这时发送端的发送窗口就完全由其拥塞窗口 cwnd 决定了;网络上拥有成千上万的 TCP 连接,它们在相互争用网络带宽,TCP 的流量控制使得它想要独享整个网络,而拥塞控制又限制其必要时做出牺牲来体现公平性。于是在传输稳定的时候 TCP 发送端呈现出下面过程的反复: [1]用慢启动或者拥塞避免方式不断增加其拥塞窗口,直到丢包的发生; [2]然后将发送窗口将下降到 1 或者下降一半,进入慢启动或者拥塞避免阶段(要看是由于超时丢包还是由于冗余 ACK 丢包);过程如下图: 【2】接收端拥有小窗口的直线状态 这种情况下是接收端非常慢速,接收窗口一直很小,这样发送窗口就完全有接收窗口决定了。由于发送窗口小,发送数据少,网络就不会出现拥塞了,于是发送窗口就一直稳定的等于那个较小的接收窗口,呈直线状态。 【3】两个直连网络端点间的满载状态下的直线状态 这种情况下,Peer 两端直连,并且只有位于一个 TCP 连接,那么这个连接将独享网络带宽,这里不存在拥塞问题,在他们处理能力足够的情况下,TCP 的流量控制使得他们能够跑慢整个网络带宽。 通过上面我们知道,在 TCP 传输稳定的时候,各个 TCP 连接会均分网络带宽的。相信大家学生时代经常会发生这样的场景,自己在看视频的时候突然出现视频卡顿,于是就大叫起来,哪个开了迅雷,赶紧给我停了。其实简单的下载加速就是开启多个 TCP 连接来分段下载就达到加速的效果,假设宿舍的带宽是 1000K/s,一开始两个在看视频,每人平均网速是 500k/s,这速度看起视频来那叫一个顺溜。突然其中一个同学打打开迅雷开着 99 个 TCP 连接在下载爱情动作片,这个时候平均下来你能分到的带宽就剩下 10k/s,这网速下你的视频还不卡成幻灯片。 在通信链路带宽固定(假设为 W),多人公用一个网络带宽的情况下,利用 TCP 协议的拥塞控制的公平性,多开几个 TCP 连接就能多分到一些带宽(当然要忽略有些用 UDP 协议带来的影响),然而不管怎么最多也就能把整个带宽抢到,于是在占满整个带宽的情况下,下载一个大小为 FS 的文件,那么最快需要的时间是 FS/W,难道就没办法加速了吗? 答案是有的,这样因为网络是网状的,一个节点是要和很多几点互联的,这就存在多个带宽为 W 的通信链路,如果我们能够将要下载的文件,一半从 A 通信链路下载,另外一半从 B 通信链路下载,这样整个下载时间就减半了为 FS/(2W),这就是 p2p 加速。相信大家学生时代在下载爱情动作片的时候也遇到过这种情况,明明外网速度没这么快的,自己下载的爱情动作片的速度却达到几 M/s,那是因为,你的左后或右后的宿友在帮你加速中。我们都知道 P2P 模式下载会快,并且越多人下载就越快,那么问题来了,P2P 下载加速理论上的加速比是多少呢? 11.附加题 1:P2P 理论上的加速比 传统的 C/S 模式传输文件,在跑满 Client 带宽的情况下传输一个文件需要耗时 FS/BW,如果有 n 个客户端需要下载文件,那么总耗时是 n(FS/BW),当然啦,这并不一定是串行传输,可以并行来传输的,这样总耗时也就是 FS/BW 了,但是这需要服务器的带宽是 n 个 client 带宽的总和 nBW。C/S 模式一个明显的缺点是服务要传输一个文件 n 次,这样对服务器的性能和带宽带来比较大的压力,我可以换下思路,服务器将文件传给其中一个 Client 后,让这些互联的 Client 自己来交互那个文件,那服务器的压力就减少很多了。这就是 P2P 网络的好处,P2P 利用各个节点间的互联,提倡“人人为我,我为人人”。 知道 P2P 传输的好处后,我们来谈下理论上的最大加速比,为了简化讨论,一个简单的网络拓扑图如下,有 4 个相互互联的节点,并且每个节点间的网络带宽是 BW,传输一个大小为 FS 的文件最快的时间是多少呢?假设节点 N1 有个大小为 FS 的文件需要传输给 N2,N3,N4 节点,一种简单的方式就是:节点 N1 同时将文件传输给节点 N2,N3,N4 耗时 FS/BW,这样大家都拥有文件 FS 了。大家可以看出,整个过程只有节点 1 在发送文件,其他节点都是在接收,完全违反了 P2P 的“人人为我,我为人人”的宗旨。那怎么才能让大家都做出贡献了呢?解决方案是切割文件。 [1]首先,节点 N1 文件分成 3 个片段 FS2,FS3,FS4,接着将 FS2 发送给 N2,FS3 发送给 N3,FS4 发送给 N4,耗时 FS/(3 * BW); [2]然后,N2,N3,N4 执行“人人为我,我为人人”的精神,将自己拥有的 F2,F3,F4 分别发给没有的其他的节点,这样耗时 FS/(3 * BW)完成交换。 于是总耗时为 2FS/(3BW)完成了文件 FS 的传输,可以看出耗时减少为原来的 2/3 了,如果有 n 个节点,那么时间就是原来的 2/(n-1),也就是加速比是 2/(n-1),这就是加速的理论上限了吗?还没发挥最多能量的,相信大家已经看到分割文件的好处了,上面的文件分割粒度还是有点大,以至于,在第二阶段[2]传输过程中,节点 N1 无所事事。为了最大化发挥大家的作用,我们需要将 FS2,FS3,FS4 在进行分割,假设将它们都均分为 K 等份,这样就有 FS21,FS22…FS2K、FS31,FS32…FS3K、FS41,FS42…FS4K,一共 3K 个分段。于是下面就开始进行加速分发: [1]节点 N1 将分段 FS21,FS31,FS41 分别发送给 N2,N3,N4 节点。耗时,FS/(3K * BW) [2]节点 N1 将分段 FS22,FS32,FS42 分别发送给 N2,N3,N4 节点,同时节点 N2,N3,N4 将阶段[1]收到的分段相互发给没有的节点。耗时,FS/(3K*BW)。 [K]节点 N1 将分段 FS2K,FS3K,FS4K 分别发送给 N2,N3,N4 节点,同时节点 N2,N3,N4 将阶段[K-1]收到的分段相互发给没有的节点。耗时,FS/(3K * BW)。[K+1]节点 N2,N3,N4 将阶段[K]收到的分段相互发给没有的节点。耗时,FS/(3K * BW)。于是总的耗时为(K+1) (FS/(3KBW)) = FS/(3BW) +FS/(3KBW),当 K 趋于无穷大的时候,文件进行无限细分的时候,耗时变成了 FS/(3 * BW),也就是当节点是 n+1 的时候,加速比是 n。这就是理论上的最大加速比了,最大加速比是 P2P 网络节点个数减 1。 12.附加题 2:系统调用 listen() 的 backlog 参数指的是什么 要说明 backlog 参数的含义,首先需要说一下 Linux 的协议栈维护的 TCP 连接的两个连接队列:[1]SYN 半连接队列;[2]accept 连接队列。 [1]SYN 半连接队列:Server 端收到 Client 的 SYN 包并回复 SYN,ACK 包后,该连接的信息就会被移到一个队列,这个队列就是 SYN 半连接队列(此时 TCP 连接处于 非同步状态 ) [2]accept 连接队列:Server 端收到 SYN,ACK 包的 ACK 包后,就会将连接信息从[1]中的队列移到另外一个队列,这个队列就是 accept 连接队列(这个时候 TCP 连接已经建立,三次握手完成了)。 用户进程调用 accept()系统调用后,该连接信息就会从[2]中的队列中移走。相信不少同学就 backlog 的具体含义进行争论过,有些认为 backlog 指的是[1]和[2]两个队列的和。而有些则认为是 backlog 指的是[2]的大小。其实,这两个说法都对,在 linux kernel 2.2 之前 backlog 指的是[1]和[2]两个队列的和。而 2.2 以后,就指的是[2]的大小,那么在 kernel 2.2 以后,[1]的大小怎么确定的呢?两个队列的作用分别是什么呢? 【1】SYN 半连接队列的作用 对于 SYN 半连接队列的大小是由(/proc/sys/net/ipv4/tcp_max_syn_backlog)这个内核参数控制的,有些内核似乎也受 listen 的 backlog 参数影响,取得是两个值的最小值。当这个队列满了,Server 会丢弃新来的 SYN 包,而 Client 端在多次重发 SYN 包得不到响应而返回(connection time out)错误。但是,当 Server 端开启了 syncookies,那么 SYN 半连接队列就没有逻辑上的最大值了,并且/proc/sys/net/ipv4/tcp_max_syn_backlog 设置的值也会被忽略。 【2】accept 连接队列 accept 连接队列的大小是由 backlog 参数和(/proc/sys/net/core/somaxconn)内核参数共同决定,取值为两个中的最小值。当 accept 连接队列满了,协议栈的行为根据(/proc/sys/net/ipv4/tcp_abort_on_overflow)内核参数而定。如果 tcp_abort_on_overflow=1,server 在收到 SYN_ACK 的 ACK 包后,协议栈会丢弃该连接并回复 RST 包给对端,这个是 Client 会出现(connection reset by peer)错误。如果 tcp_abort_on_overflow=0,server 在收到 SYN_ACK 的 ACK 包后,直接丢弃该 ACK 包。这个时候 Client 认为连接已经建立了,一直在等 Server 的数据,直到超时出现 read timeout 错误。