跳转至

Java 基础面试题(爪哇程序员)

说下面向对象四大特性

原文:https://zwmst.com/217.html

封装、继承、多态、抽象。

什么是Java程序的主类?应用程序和小程序的主类有何不同?

原文:https://zwmst.com/239.html

一个程序中可以有多个类,但只能有一个类是主类。在Java应用程序中,这个主类是指包含 main()方法的类。而在Java小程序中,这个主类是一个继承自系统类JApplet或Applet的子 类。应用程序的主类不一定要求是public类,但小程序的主类要求必须是public类。主类是 Java程序执行的入口点。

Java语言有哪些特点

原文:https://zwmst.com/244.html

封装、继承、多态、抽象。

访问修饰符public,private,protected,以及不写(默认)时的区别?

原文:https://zwmst.com/251.html

修饰符 当前类 同包 子类 其他包
public
protected ×
default × ×
private × × ×

类的成员不写访问修饰时默认为default。默认对于同一个包中的其他类相当于公开 (public),对于不是同一个包中的其他类相当于私有(private)。受保护(protected)对 子类相当于公开,对不是同一包中的没有父子关系的类相当于私有。Java中,外部类的修饰符 只能是public或默认,类的成员(包括内部类)的修饰符可以是以上四种。

float f=3.4;是否正确?

原文:https://zwmst.com/253.html

不正确。3.4是双精度数,将双精度型(double)赋值给浮点型(float)属于下转型(downcasting,也称为窄化)会造成精度损失,因此需要强制类型转换float f =(float)3.4; 或者写 成float f =3.4F;。

Java有没有goto?

原文:https://zwmst.com/255.html

goto 是Java中的保留字,在目前版本的Java中没有使用。(根据James Gosling(Java之 父)编写的《The Java Programming Language》一书的附录中给出了一个Java关键字列 表,其中有goto和const,但是这两个是目前无法使用的关键字,因此有些地方将其称之为保 留字,其实保留字这个词应该有更广泛的意义,因为熟悉C语言的程序员都知道,在系统类库 中使用过的有特殊意义的单词或单词的组合都被视为保留字)

&和&&的区别?

原文:https://zwmst.com/257.html

&运算符有两种用法:(1)按位与;(2)逻辑与。

&&运算符是短路与运算。

逻辑与跟短路与的差别是非常巨大的,虽然二者都要求运算符左右两端的布尔值都是true整个 表达式的值才是true。&&之所以称为短路运算是因为,如果&&左边的表达式的值是false,右 边的表达式会被直接短路掉,不会进行运算。很多时候我们可能都需要用&&而不是&,例如在 验证用户登录时判定用户名不是null而且不是空字符串,应当写为:username != null &&!username.equals(""),二者的顺序不能交换,更不能用&运算符,因为第一个条件如果 不成立,根本不能进行字符串的equals比较,否则会产生NullPointerException异常。注 意:逻辑或运算符(|)和短路或运算符(||)的差别也是如此。

Math.round(11.5) 等于多少?Math.round(-11.5)等于多少?

原文:https://zwmst.com/259.html

Math.round(11.5)的返回值是12,Math.round(-11.5)的返回值是-11。四舍五入的原理是在 参数上加0.5然后进行下取整。

用最有效率的方法计算2乘以8?

原文:https://zwmst.com/261.html

2 << 3

什么是Java注释

原文:https://zwmst.com/263.html

定义:用于解释说明程序的文字

Java注释的分类

单行注释 格式:

  • 单行注释

    • 格式: // 注释文字
    • 多行注释

    • 格式: / 注释文字 /

    • 文档注释

    • 格式:/ 注释文字* /

Java注释的作用

在程序中,尤其是复杂的程序中,适当地加入注释可以增加程序的可读性,有利于程序的修 改、调试和交流。注释的内容在程序编译的时候会被忽视,不会产生目标代码,注释的部分不 会对程序的执行结果产生任何影响。

注意事项:多行和文档注释都不能嵌套使用。

Java有哪些数据类型

原文:https://zwmst.com/265.html

定义:Java语言是强类型语言,对于每一种数据都定义了明确的具体的数据类型,在内存中分 配了不同大小的内存空间。

  • 基本数据类型

    • 数值型
    • 整数类型(byte,short,int,long)
    • 浮点类型(float,double)
    • 字符型(char)
    • 布尔型(boolean)
    • 引用数据类型

    • 类(class)

    • 接口(interface)
    • 数组([])

final 有什么用?

原文:https://zwmst.com/267.html

用于修饰类、属性和方法;

  • 被final修饰的类不可以被继承

  • 被final修饰的方法不可以被重写

  • 被final修饰的变量不可以被改变,被final修饰不可变的是变量的引用,而不是引用指向 的内容,引用指向的内容是可以改变的

final finally finalize的区别

原文:https://zwmst.com/269.html

  • final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能 被重写、修饰变量表 示该变量是一个常量不能被重新赋值。
  • finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代 码方法finally代码块 中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。
  • finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,该方法一 般由垃圾回收器来调 用,当我们调用System.gc() 方法的时候,由垃圾回收器调用finalize(),回收垃圾,一 个对象是否可回收的 最后判断。

String str = “i” 和String str = new String(“1”)一样吗?

原文:https://zwmst.com/271.html

不一样,因为内存的分配方式不一样。String str = "i"的方式JVM会将其分配到常量池中,而 String str = new String("i")JVM会将其分配到堆内存中。

Java 中操作字符串都有哪些类?它们之间有什么区别?

原文:https://zwmst.com/273.html

操作字符串的类有:String、StringBuffer、StringBuilder。

String 和 StringBuffer、StringBuilder 的区别在于 String 声明的是不可变的对象,每次操 作都会生成新的 String 对象,再将指针指向新的 String 对象,而 StringBuffer 、 StringBuilder 可以在原有对象的基础上进行操作,所以在经常改变字符串内容的情况下最好 不要使用 String。

StringBuffer 和 StringBuilder 最大的区别在于,StringBuffer 是线程安全的,而 StringBuilder 是非线程安全的,但 StringBuilder 的性能却高于 StringBuffer,所以在单线 程环境下推荐使用 StringBuilder,多线程环境下推荐使用 StringBuffer。

Java中为什么要用 clone?

原文:https://zwmst.com/275.html

在实际编程过程中,我们常常要遇到这种情况:有一个对象 A,在某一时刻 A 中已经包含了一 些有效值,此时可能会需要一个和 A 完全相同新对象 B,并且此后对 B 任何改动都不会影响到 A 中的值,也就是说,A 与 B 是两个独立的对象,但 B 的初始值是由 A 对象确定的。在 Java 语言中,用简单的赋值语句是不能满足这种需求的。要满足这种需求虽然有很多途径,但 clone()方法是其中最简单,也是最高效的手段。

深克隆和浅克隆?

原文:https://zwmst.com/277.html

浅克隆:创建一个新对象,新对象的属性和原来对象完全相同,对于非基本类型属性,仍指向 原有属性所指向的对象的内存地址。

深克隆:创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址。

new一个对象的过程和clone一个对象的区别?

原文:https://zwmst.com/279.html

new 操作符的本意是分配内存。程序执行到 new 操作符时,首先去看 new 操作符后面的类 型,因为知道了类型,才能知道要分配多大的内存空间。分配完内存之后,再调用构造函数, 填充对象的各个域,这一步叫做对象的初始化,构造方法返回后,一个对象创建完毕,可以把 他的引用(地址)发布到外部,在外部就可以使用这个引用操纵这个对象。

clone 在第一步是和 new 相似的,都是分配内存,调用 clone 方法时,分配的内存和原对象 (即调用 clone 方法的对象)相同,然后再使用原对象中对应的各个域,填充新对象的域,填 充完成之后,clone方法返回,一个新的相同的对象被创建,同样可以把这个新对象的引用发 布到外部。

Java中实现多态的机制是什么?

原文:https://zwmst.com/281.html

Java中的多态靠的是父类或接口定义的引用变量可以指向子类或具体实现类的实例对象,而程 序调用的方法在运行期才动态绑定,就是引用变量所指向的具体实例对象的方法,也就是内存 里正在运行的那个对象的方法,而不是引用变量的类型中定义的方法。

谈谈你对多态的理解?

原文:https://zwmst.com/283.html

多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程 时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该 引用变量发出的方法调用到底是哪个类中实现的方法,必须在程序运行期间才能决定。因为在 程序运行时才确定具体的类,这样,不用修改源代码,就可以让引用变量绑定到各种不同的对 象上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所 绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。

构造器(constructor)是否可被重写(override)?

原文:https://zwmst.com/285.html

构造器不能被继承,因此不能被重写,但可以被重载。

两个对象值相同(x.equals(y) == true),但却可有不同的hash code,这句话对 不对?

原文:https://zwmst.com/287.html

不对,如果两个对象x和y满足x.equals(y) == true,它们的哈希码(hash code)应当相同。

Java对于eqauls方法和hashCode方法是这样规定的:

(1)如果两个对象相同(equals方法返回true),那么它们的hashCode值一定要相同;

(2)如果两个对象的hashCode相同,它们并不一定相同。

当然,你未必要按照要求去做,但是如果你违背了上述原则就会发现在使用容器时,相同的对 象可以出现在Set集合中,同时增加新元素的效率会大大下降(对于使用哈希存储的系统,如 果哈希码频繁的冲突将会造成存取性能急剧下降)。

是否可以继承String类?

原文:https://zwmst.com/289.html

String 类是final类,不可以被继承。

补充:继承String本身就是一个错误的行为,对String类型最好的重用方式是关联关系 (Has-A)和依赖关系(Use-A)而不是继承关系(Is-A)。

String类的常用方法有哪些?

原文:https://zwmst.com/291.html

  • indexof();返回指定字符的的索引。

  • charAt();返回指定索引处的字符。

  • replace();字符串替换。

  • trim();去除字符串两端空格。

  • splt();字符串分割,返回分割后的字符串数组。

  • getBytes();返回字符串byte类型数组。

  • length();返回字符串长度。

  • toLowerCase();将字符串转换为小写字母。 •toUpperCase();将字符串转换为大写字母。

  • substring();字符串截取。

  • equals();比较字符串是否相等。

char型变量中能否能不能存储一个中文汉字,为什么?

原文:https://zwmst.com/293.html

char可以存储一个中文汉字,因为Java中使用的编码是Unicode(不选择任何特定的编码,直 接使用字符在字符集中的编号,这是统一的唯一方法),一个char 类型占2个字节(16 比 特),所以放一个中文是没问题的。

String类的常用方法有哪些?

原文:https://zwmst.com/295.html

  • indexof();返回指定字符的的索引。

  • charAt();返回指定索引处的字符。

  • replace();字符串替换。

  • trim();去除字符串两端空格。

  • splt();字符串分割,返回分割后的字符串数组。

  • getBytes();返回字符串byte类型数组。

  • length();返回字符串长度。

  • toLowerCase();将字符串转换为小写字母。 •toUpperCase();将字符串转换为大写字母。

  • substring();字符串截取。

  • equals();比较字符串是否相等。

super关键字的用法

原文:https://zwmst.com/297.html

super可以理解为是指向自己超(父)类对象的一个指针,而这个超类指的是离自己最近的一 个父类。

super也有三种用法:

1.普通的直接引用

与this类似,super相当于是指向当前对象的父类的引用,这样就可以用super.xxx来引用父类的成员。

2.子类中的成员变量或方法与父类中的成员变量或方法同名时,用super进行区分

class Person {
    protected String name;

    public Person(String name) {
        this.name = name;
    }
}

class Student extends Person {
    private String name;

    public Student(String name, String name1) {
        super(name);
        this.name = name1;
    }

    public void getInfo() {
        System.out.println(this.name);
        System.out.println(super.name);
    }
}

public class Test {
    public static void main(String[] args) {

        Student s1 = new Student("Father", "Child");

        s1.getInfo();
    }
}

3.引用父类构造函数

  • super(参数):调用父类中的某一个构造函数(应该为构造函数中的第一条语句)。
  • this(参数):调用本类中另一种形式的构造函数(应该为构造函数中的第一条语句)。

this与super的区别

原文:https://zwmst.com/299.html

  • super:它引用当前对象的直接父类中的成员(用来访问直接父类中被隐藏的父类中成员
  • 数据或函数,基类与派生类中有相同成员定义时如:super.变量名 super.成员函数据名 (实参)
  • this:它代表当前对象名(在程序中易产生二义性之处,应使用this来指明当前对象;如果函数的形参与类中的成员数据同名,这时需用this来指明成员变量名)
  • super()和this()类似,区别是,super()在子类中调用父类的构造方法,this()在本类内
  • 调用本类的其它构造方法。
  • super()和this()均需放在构造方法内第一行。
  • 尽管可以用this调用一个构造器,但却不能调用两个。
  • this和super不能同时出现在一个构造函数里面,因为this必然会调用其它的构造函数, 其它的构造函数必然也会有super语句的存在,所以在同一个构造函数里面有相同的语 句,就失去了语句的意义,编译器也不会通过。
  • this()和super()都指的是对象,所以,均不可以在static环境中使用。包括:static变
  • 量,static方法,static语句块。
  • 从本质上讲,this是一个指向本对象的指针, 然而super是一个Java关键字。

static存在的主要意义

原文:https://zwmst.com/301.html

static的主要意义是在于创建独立于具体对象的域变量或者方法。 以致于即使没有创建对象, 也能使用属性和调用方法 !

static关键字还有一个比较关键的作用就是 用来形成静态代码块以优化程序性能 。static块可 以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,会按照static块的 顺序来执行每个static块,并且只会执行一次。

为什么说static块可以用来优化程序性能,是因为它的特性:只会在类加载的时候执行一次。因 此,很多时候会将一些只需要进行一次的初始化操作都放在static代码块中进行。

static的独特之处

原文:https://zwmst.com/303.html

1、被static修饰的变量或者方法是独立于该类的任何对象,也就是说,这些变量和方法 不属于 任何一个实例对象,而是被类的实例对象所共享 。

怎么理解 “被类的实例对象所共享” 这句话呢?就是说,一个类的静态成员,它是属于大伙的 【大伙指的是这个类的多个对象实例,我们都知道一个类可以创建多个实例!】,所有的类对 象共享的,不像成员变量是自个的【自个指的是这个类的单个实例对象】…我觉得我已经讲的 很通俗了,你明白了咩?

2、在该类被第一次加载的时候,就会去加载被static修饰的部分,而且只在类第一次使用时加 载并进行初始化,注意这是第一次用就要初始化,后面根据需要是可以再次赋值的。

3、static变量值在类加载的时候分配空间,以后创建类对象的时候不会重新分配。赋值的话, 是可以任意赋值的!

4、被static修饰的变量或者方法是优先于对象存在的,也就是说当一个类加载完毕之后,即便 没有创建对象,也可以去访问。

static应用场景

原文:https://zwmst.com/316.html

因为static是被类的实例对象所共享,因此如果 某个成员变量是被所有对象所共享的,那么这 个成员变量就应该定义为静态变量 。

因此比较常见的static应用场景有:

1、修饰成员变量

2、修饰成员方法

3、静态代码块

4、修饰类【只能修饰内部类也就是静态内 部类】

5、静态导包

static注意事项

原文:https://zwmst.com/318.html

1、静态只能访问静态。

2、非静态既可以访问非静态的,也可以访问静态的。

break ,continue ,return 的区别及作用

原文:https://zwmst.com/320.html

break 跳出总上一层循环,不再执行循环(结束当前的循环体)

continue 跳出本次循环,继续执行下次循环(结束正在执行的循环 进入下一个循环条件)

return 程序返回,不再执行下面的代码(结束当前的方法 直接返回)

在Java中定义一个不做事且没有参数的构造方法的作用

原文:https://zwmst.com/322.html

Java程序在执行子类的构造方法之前,如果没有用super()来调用父类特定的构造方法,则会 调用父类中“没有参数的构造方法”。因此,如果父类中只定义了有参数的构造方法,而在子类 的构造方法中又没有用super()来调用父类中特定的构造方法,则编译时将发生错误,因为 Java程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加上一个不做事 且没有参数的构造方法。

构造方法有哪些特性?

原文:https://zwmst.com/324.html

名字与类名相同;

没有返回值,但不能用void声明构造函数;

生成类的对象时自动执行,无需调用。

静态变量和实例变量区别

原文:https://zwmst.com/326.html

静态变量: 静态变量由于不属于任何实例对象,属于类的,所以在内存中只会有一份,在类的 加载过程中,JVM只为静态变量分配一次内存空间。

实例变量: 每次创建对象,都会为每个对象分配成员变量内存空间,实例变量是属于实例对象 的,在内存中,创建几次对象,就有几份成员变量。

静态方法和实例方法有何不同?

原文:https://zwmst.com/328.html

静态方法和实例方法的区别主要体现在两个方面:

  1. 在外部调用静态方法时,可以使用"类名.方法名"的方式,也可以使用"对象名.方法名"的 方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。
  2. 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法), 而不允许访问实例成员变量和实例方法;实例方法则无此限制

什么是方法的返回值?返回值的作用是什么?

原文:https://zwmst.com/330.html

方法的返回值是指我们获取到的某个方法体中的代码执行后产生的结果!(前提是该方法可能 产生结果)。返回值的作用:接收出结果,使得它可以用于其他的操作!

什么是内部类?

原文:https://zwmst.com/332.html

在Java中,可以将一个类的定义放在另外一个类的定义内部,这就是 内部类 。内部类本身就 是类的一个属性,与其他属性定义方式一致。

内部类的分类有哪些

原文:https://zwmst.com/334.html

内部类可以分为四种: 成员内部类、局部内部类、匿名内部类和静态内部类 。

Java中异常分为哪些种类?

原文:https://zwmst.com/336.html

按照异常需要处理的时机分为编译时异常(也叫受控异常)也叫 CheckedException 和运行时异 常(也叫非受控异常)也叫 UnCheckedException。Java认为Checked异常都是可以被处理的 异常,所以Java程序必须显式处理Checked异常。如果程序没有处理Checked 异常,该程序 在编译时就会发生错误无法编译。这体现了Java 的设计哲学:没有完善错误处理的代码根本没 有机会被执行。对Checked异常处理方法有两种:

  • 第一种:当前方法知道如何处理该异常,则用try…catch块来处理该异常。

  • 第二种:当前方法不知道如何处理,则在定义该方法时声明抛出该异常。

运行时异常只有当代码在运行时才发行的异常,编译的时候不需要try…catch。Runtime如除 数是0和数组下标越界等,其产生频繁,处理麻烦,若显示申明或者捕获将会对程序的可读性 和运行效率影响很大。所以由系统自动检测并将它们交给缺省的异常处理程序。当然如果你有 处理要求也可以显示捕获它们。

hashCode 与 equals (重要)

原文:https://zwmst.com/338.html

HashSet如何检查重复 两个对象的 hashCode() 相同,则 equals() 也一定为 true,对吗?

hashCode和equals方法的关系 面试官可能会问你:“你重写过 hashcode 和 equals 么,为什么重写equals时必须重写 hashCode方法?”

hashCode()介绍

原文:https://zwmst.com/340.html

hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码 的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在JDK的Object.java中,这就 意味着Java中的任何类都包含有hashCode()函数。

散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这 其中就利用到了散列码!(可以快速找到所需要的对象)

为什么要有 hashCode

原文:https://zwmst.com/342.html

我们以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode:

当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位 置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的hashcode, HashSet会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让 其加入操作成功。如果不同的话,就会重新散列到其他位置。(摘自我的Java启蒙书《Head first java》第二版)。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。

hashCode()与equals()的相关规定

如果两个对象相等,则hashcode一定也是相同的

两个对象相等,对两个对象分别调用equals方法都返回true

两个对象有相同的hashcode值,它们也不一定是相等的

因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖

hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)

对象的相等与指向他们的引用相等,两者有什么不同?

对象的相等 比的是内存中存放的内容是否相等而 引用相等 比较的是他们指向的内存地址是否 相等。

抽象类和接口(Java7)的区别

原文:https://zwmst.com/345.html

  1. 抽象类可以提供成员方法的实现细节,而接口中只能存在public abstract 方法;

  2. 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final 类型的;

  3. 接口中不能含有静态代码块以及静态方法,而抽象类可以有静态代码块和静态方法;

  4. 一个类只能继承一个抽象类,而一个类却可以实现多个接口。

Java 8的接口新增了哪些特性?

原文:https://zwmst.com/347.html

增加了default方法和static方法,这2种方法可以有方法体。

重写和重载的区别

原文:https://zwmst.com/349.html

重写是子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变。即 外壳不变,核心重写!

重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现 父类的方法。

重写方法不能抛出新的检查异常或者比被重写方法申明更加宽泛的异常。

重载(overloading) 是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以 不同。

每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。

ArrayList和LinkedList有什么区别?

原文:https://zwmst.com/351.html

  1. ArrayList和LinkedList的差别主要来自于Array和LinkedList数据结构的不同。 ArrayList是基于数组实现的,LinkedList是基于双链表实现的。另外LinkedList类不 仅是List接口的实现类,可以根据索引来随机访问集合中的元素,除此之外, LinkedList还实现了Deque接口,Deque接口是Queue接口的子接口,它代表一个双向 队列,因此LinkedList可以作为双向队列 ,栈(可以参见Deque提供的接口方法)和 List集合使用,功能强大。

  2. 因为Array是基于索引(index)的数据结构,它使用索引在数组中搜索和读取数据是很快 的,可以直接返回数组中index位置的元素,因此在随机访问集合元素上有较好的性能。 Array获取数据的时间复杂度是O(1),但是要插入、删除数据却是开销很大的,因为这需 要移动数组中插入位置之后的的所有元素。

  3. 相对于ArrayList,LinkedList的随机访问集合元素时性能较差,因为需要在双向列表中 找到要index的位置,再返回;但在插入,删除操作是更快的。因为LinkedList不像 ArrayList一样,不需要改变数组的大小,也不需要在数组装满的时候要将所有的数据重 新装入一个新的数组,这是ArrayList最坏的一种情况,时间复杂度是O(n),而 LinkedList中插入或删除的时间复杂度仅为O(1)。ArrayList在插入数据时还需要更新索 引(除了插入数组的尾部)。

  4. LinkedList需要更多的内存,因为ArrayList的每个索引的位置是实际的数据,而 LinkedList中的每个节点中存储的是实际的数据和前后节点的位置。

静态代理和动态代理的区别

原文:https://zwmst.com/353.html

静态代理中代理类在编译期就已经确定,而动态代理则是JVM运行时动态生成,静态代理的效 率相对动态代理来说相对高一些,但是静态代理代码冗余大,一单需要修改接口,代理类和委 托类都需要修改。

JDK动态代理和CGLIB动态代理的区别

原文:https://zwmst.com/355.html

JDK动态代理只能对实现了接口的类生成代理,而不能针对类。

CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法。因为是继承, 所以该类或方法最好不要声明成final。

Java 的接口和 C++的虚类的相同和不同处

原文:https://zwmst.com/2390.html

答:由于 Java 不支持多继承,而有可能某个类或对象要使用分别在几个类或对象里面的方法或属性,现有的单继承机制就不能满足要求。 与继承相比,接口有更高的灵活性,因为接口中没有任何实现代码。 当一个类实现了接口以后,该类要实现接口里面所有的方法和属性,并且接口里面的属性在默认状态下面都是public static,所有方法默认情况下是 public.一个类可以实现多个接口。

一个“.java”源文件中是否可以包含多个类(不是内部类)有什么限制

原文:https://zwmst.com/2392.html

答:可以;必须只有一个类名与文件名相同。

说出一些常用的类,包,接口,请各举 5 个

原文:https://zwmst.com/2394.html

答:常用的类:

  • BufferedReader
  • BufferedWriter
  • FileReader FileWirter
  • String Integer; 常用的包:
    • java.lang
    • java.awt
    • java.io
    • java.util
    • java.sql; 常用的接口:
    • Remote
    • List
    • Map
    • Document
    • NodeList

Anonymous Inner Class (匿名内部类) 是否可以 extends(继承)其它类是否可以 implements(实现)interface(接口)

原文:https://zwmst.com/2396.html

答:可以继承其他类或实现其他接口,在 swing 编程中常用此方式。

内部类可以引用他包含类的成员吗有没有什么限制

原文:https://zwmst.com/2398.html

答:一个内部类对象可以访问创建它的外部类对象的内容。

java 中实现多态的机制是什么

原文:https://zwmst.com/2400.html

答:方法的覆盖 Overriding 和重载 Overloading 是 java 多态性的不同表现;

  • 覆盖 Overriding 是父类与子类之间多态性的一种表现,
  • 重载 Overloading 是一个类中多态性的一种表现。

在 java 中一个类被声明为 final 类型,表示了什么意思

原文:https://zwmst.com/2403.html

答:表示该类不能被继承,是顶级类。

下面哪些类可以被继承

原文:https://zwmst.com/2405.html

  • 1)java.lang.Thread (T)
  • 2)java.lang.Number (T)
  • 3)java.lang.Double (F)
  • 4)java.lang.Math (F)
  • 5)java.lang.Void (F)
  • 6)java.lang.Class (F)
  • 7)java.lang.ClassLoader (T) 答:1、2、7 可以被继承。

指出下面程序的运行结果

原文:https://zwmst.com/2407.html

class A{ 
 static{ 
 System.out.print("1"); 
 } 
 public A(){ 
 System.out.print("2"); 
 } 
} 
class B extends A{ 
 static{ 
 System.out.print("a"); 
 } 
 public B(){ 
 System.out.print("b"); 
 } 
} 
public class Hello{ 
 public static void main(String[] ars){ 
 A ab = new B(); //执行到此处,结果: 1a2b 
 ab = new B(); //执行到此处,结果: 1a2b2b 
 } 
} 

答:输出结果为 1a2b2b; 类的 static 代码段,可以看作是类首次加载(虚拟机加 载)执行的代码,而对于类加载,首先要执行其基类的构造,再执行其本身的构造。

继承时候类的执行顺序问题,一般都是选择题,问你将会打印出什么

原文:https://zwmst.com/2409.html

父类:

package test; 
public class FatherClass { 
 public FatherClass() { 
 System.out.println("FatherClass Create"); 
 } 
} 

子类:

package test; 
import test.FatherClass; 
public class ChildClass extends FatherClass { 
 public ChildClass() { 
 System.out.println("ChildClass Create"); 
 } 
 public static void main(String[] args) { 
 FatherClass fc = new FatherClass(); 
 ChildClass cc = new ChildClass(); 
 } 
} 

答:输出结果为:

 FatherClass Create 
 FatherClass Create 
 ChildClass Create 

内部类的实现方式

原文:https://zwmst.com/2411.html

答:示例代码如下:

package test; 
public class OuterClass { 
 private class InterClass { 
 public InterClass() { 
 System.out.println("InterClass Create"); 
 } 
 } 
 public OuterClass() { 
 InterClass ic = new InterClass(); 
 System.out.println("OuterClass Create"); 
 } 
 public static void main(String[] args) { 
 OuterClass oc = new OuterClass(); 
 } 
} 

输出结果为:

InterClass Create 
OuterClass Create 

关于内部类

原文:https://zwmst.com/2413.html

public class OuterClass { 
 private double d1 = 1.0; 
 //insert code here 
} 
 You need to insert an inner class declaration at line 3,Which two inner 
class declarations are valid?(Choose two.) 
A. class InnerOne{ 
 public static double methoda() {return d1;} 
 } 

B. public class InnerOne{ 
 static double methoda() {return d1;} 
 } 
C. private class InnerOne{ 
 double methoda() {return d1;} 
 } 
D. static class InnerOne{ 
 protected double methoda() {return d1;} 
 } 
E. abstract class InnerOne{ 
 public abstract double methoda(); 
 } 

答:答案为 C、E;说明如下:

  • 1)静态内部类可以有静态成员,而非静态内部类则不能有静态成员;故 A、B 错;
  • 2)静态内部类的非静态成员可以访问外部类的静态变量,而不可访问外部类的非静态变量;故 D 错;
  • 3)非静态内部类的非静态成员可以访问外部类的非静态变量;故 C 正确 。

数据类型之间的转换

原文:https://zwmst.com/2415.html

  • 1)如何将数值型字符转换为数字?
  • 2)如何将数字转换为字符?
  • 3)如何取小数点前两位并四舍五入? 【基础】 答:
  • 1)调用数值类型相应包装类中的方法 parse***(String)或 valueOf(String) 即可返回相应基本类型或包装类型数值;
  • 2)将数字与空字符串相加即可获得其所对应的字符串;另外对于基本类型 数字还可调用 String 类中的 valueOf(…)方法返回相应字符串,而对于包装类型数字则可调用其 toString()方法获得相应字符串;
  • 3)可用该数字构造一 java.math.BigDecimal 对象,再利用其 round()方法 进行四舍五入到保留小数点后两位,再将其转换为字符串截取最后两位。

字符串操作:如何实现字符串的反转及替换

原文:https://zwmst.com/2417.html

答:可用字符串构造一 StringBuffer 对象,然后调用 StringBuffer 中的 reverse方法即可实现字符串的反转,调用 replace 方法即可实现字符串的替换。

编码转换:怎样将 GB2312 编码的字符串转换为 ISO-8859-1 编码的字符串

原文:https://zwmst.com/2419.html

答:示例代码如下:

 String s1 = "你好"; 
 String s2 = new String(s1.getBytes("GB2312"), "ISO-8859-1"); 

写一个函数,要求输入一个字符串和一个字符长度,对该字符串进行分隔

原文:https://zwmst.com/2421.html

答:函数代码如下:

 public String[] split(String str, int chars){ 
 int n = (str.length()+ chars - 1)/chars; 
 String ret[] = new String[n]; 
 for(int i=0; i<n; i++){ 
 if(i < n-1){ 
 ret[i] = str.substring(i*chars , (i+1)*chars); 
 }else{ 
 ret[i] = str.substring(i*chars); 
 } 
 } 
 return ret; 
 } 

写一个函数,2 个参数,1 个字符串,1 个字节数,返回截取的字符串,要求字符串中的中文不能出现乱码:如(“我 ABC”,4)应该截为“我 AB”,输入(“我ABC 汉 DEF”,6)应该输出为“我 ABC”而不是“我 ABC+汉的半个”

原文:https://zwmst.com/2423.html

答:代码如下:

 public String subString(String str, int subBytes) { 
 int bytes = 0; // 用来存储字符串的总字节数 
 for (int i = 0; i < str.length(); i++) { 
 if (bytes == subBytes) { 
 return str.substring(0, i); 
 } 
 char c = str.charAt(i); 
 if (c < 256) { 
 bytes += 1; // 英文字符的字节数看作 1 
 } else { 
 bytes += 2; // 中文字符的字节数看作 2 
 if(bytes - subBytes == 1){ 
 return str.substring(0, i); 
 } 
 } 
 } 
 return str; 
 } 

日期和时间

原文:https://zwmst.com/2425.html

  • 1)如何取得年月日、小时分秒?
  • 2)如何取得从 1970 年到现在的毫秒数?
  • 3)如何取得某个日期是当月的最后一天?
  • 4)如何格式化日期?【基础】 答:
  • 1)创建 java.util.Calendar 实例(Calendar.getInstance()),调用其 get() 方法传入不同的参数即可获得参数所对应的值,如:

    calendar.get(Calendar.YEAR);//获得年

  • 2)以下方法均可获得该毫秒数:

    Calendar.getInstance().getTimeInMillis(); System.currentTimeMillis();

  • 3)示例代码如下:

    Calendar time = Calendar.getInstance(); time.set(Calendar.DAY_OF_MONTH, time.getActualMaximum(Calendar.DAY_OF_MONTH));

  • 4)利用 java.text.DataFormat 类中的 format()方法可将日期格式化。

Java 编程,打印昨天的当前时刻

原文:https://zwmst.com/2427.html

答:

public class YesterdayCurrent{ 
 public static void main(String[] args){ 
 Calendar cal = Calendar.getInstance(); 
 cal.add(Calendar.DATE, -1); 
 System.out.println(cal.getTime()); 
 } 
 } 

java 和 javasciprt 的区别

原文:https://zwmst.com/2429.html

答:JavaScript 与 Java 是两个公司开发的不同的两个产品。 Java 是 SUN 公司推出的新一代面向对象的程序设计语言,特别适合于 Internet 应用程序开发; 而JavaScript 是 Netscape 公司的产品,其目的是为了扩展 Netscape Navigator功能,而开发的一种可以嵌入 Web 页面中的基于对象和事件驱动的解释性语言, 它的前身是 Live Script; 而 Java 的前身是 Oak 语言。 下面对两种语言间的异同作如下比较:

1)基于对象和面向对象:

  • Java 是一种真正的面向对象的语言,即使是开发简单的程序,必须设计对象;
  • JavaScript 是种脚本语言,它可以用来制作与网络无关的,与用户交互作用的复杂软件。它是一种基于对象(Object Based)和事件驱动(Event Driver)的编程语言。因而它本身提供了非常丰富的内部对象供设计人员使用;

    2)解释和编译:

  • Java 的源代码在执行之前,必须经过编译;

  • JavaScript 是一种解释性编程语言,其源代码不需经过编译,由浏览器解释执行;

    3)强类型变量和类型弱变量:

  • Java 采用强类型变量检查,即所有变量在编译之前必须作声明;

  • JavaScript 中变量声明,采用其弱类型。即变量在使用前不需作声明,而是解释器在运行时检查其数据类型;

    4)代码格式不一样。

什么时候用 assert

原文:https://zwmst.com/2431.html

答:assertion(断言)在软件开发中是一种常用的调试方式,很多开发语言中都支持这种机制。 一般来说,assertion 用于保证程序最基本、关键的正确性。 assertion 检查通常在开发和测试时开启。 为了提高性能,在软件发布后,assertion 检查通常是关闭的。 在实现中,断言是一个包含布尔表达式的语句,在执行这个语句时假定该表达式为 true;如果表达式计算为 false,那么系统会报告一个 Assertionerror。

断言用于调试目的:

 assert(a > 0); // throws an Assertionerror if a <= 0 

断言可以有两种形式:

  • assert Expression1 ;
  • assert Expression1 : Expression2 ; Expression1 应该总是产生一个布尔值。 Expression2 可以是得出一个值的任意表达式;这个值用于生成显示更多调试信息的 String 消息。 断言在默认情况下是禁用的,要在编译时启用断言,需使用 source 1.4 标记:

    javac -source 1.4 Test.java

    要在运行时启用断言,可使用 -enableassertions 或者 -ea 标记。 要在运行时选择禁用断言,可使用 -da 或者 -disableassertions 标记。 要在系统类中启用断言,可使用 -esa 或者 -dsa 标记。 还可以在包的基础上启用或者禁用断言。可以在预计正常情况下不会到达的任何位置上放置断言。 断言可以用于验证传递给私有方法的参数。不过,断言不应该用于验证传递给公有方法的参数,因为不管是否启用了断言,公有方法都必须检查其参数。 不过,既可以在公有方法中,也可以在非公有方法中利用断言测试后置条件。另外,断言不应该以任何方式改变程序的状态。

Java 中的异常处理机制的简单原理和应用

原文:https://zwmst.com/2433.html

答:当 JAVA 程序违反了 JAVA 的语义规则时,JAVA 虚拟机就会将发生的错误表示为一个异常。 违反语义规则包括 2 种情况。

  • 一种是 JAVA 类库内置的语义检查。 例如数组下标越界,会引发 IndexOutOfBoundsException;访问 null 的对象时会引发 NullPointerException。
  • 另一种情况就是 JAVA 允许程序员扩展这种语义检查,程序员可以创建自己的异常,并自由选择在何时用 throw 关键字引发异常。 所有的异常都是 java.lang.Thowable 的子类。

error 和 exception 有什么区别

原文:https://zwmst.com/2435.html

答:error 表示系统级的错误和程序不必处理的异常,是恢复不是不可能但很困难的情况下的一种严重问题;比如内存溢出,不可能指望程序能处理这样的情况; exception 表示需要捕捉或者需要程序进行处理的异常,是一种设计或实现问题;也就是说,它表示如果程序运行正常,从不会发生的情况。

try {}里有一个 return 语句,那么紧跟在这个 try 后的 finally {}里的 code会不会被执行,什么时候被执行,在 return 前还是后

原文:https://zwmst.com/2437.html

答:会执行,在 return 前执行。

JAVA 语言如何进行异常处理,关键字:throws,throw,try,catch,finally分别代表什么意义在 try 块中可以抛出异常吗

原文:https://zwmst.com/2439.html

答:Java 通过面向对象的方法进行异常处理,把各种不同的异常进行分类,并提供了良好的接口。 在 Java 中,每个异常都是一个对象,它是 Throwable 类或其它子类的实例。 当一个方法出现异常后便抛出一个异常对象,该对象中包含有异常信息,调用这个对象的方法可以捕获到这个异常并进行处理。 Java 的异常处理是通过 5 个关键词来实现的:try、catch、throw、throws 和 finally。 一般情况下是用 try 来执行一段程序,如果出现异常,系统会抛出(throws)一个异常,这时候你可以通过它的类型来捕捉(catch)它,或最后(finally)由缺省处理器来处理;

  • try 用来指定一块预防所有“异常”的程序;
  • catch 子句紧跟在 try 块后面,用来指定你想要捕捉的“异常”的类型;
  • throw 语句用来明确地抛出一个“异常”;
  • throws 用来标明一个成员函数可能抛出的各种“异常”;
  • Finally 为确保一段代码不管发生什么“异常”都被执行一段代码;

    可以在一个成员函数调用的外面写一个 try 语句,在这个成员函数内部写另一个 try 语句保护其他代码。 每当遇到一个 try 语句,“异常”的框架就放到堆栈上面,直到所有的 try 语句都完成。 如果下一级的 try 语句没有对某种“异常”进行处理,堆栈就会展开,直到遇到有处理这种“异常”的 try 语句。

运行时异常与一般异常有何异同

原文:https://zwmst.com/2441.html

答:异常表示程序运行过程中可能出现的非正常状态,运行时异常表示虚拟机的通常操作中可能遇到的异常,是一种常见运行错误。 java 编译器要求方法必须声明抛出可能发生的非运行时异常,但是并不要求必须声明抛出未被捕获的运行时异常。

给我一个你最常见到的 runtime exception

原文:https://zwmst.com/2445.html

答: ArithmeticException, ArrayStoreException, BufferOverflowException, BufferUnderflowException, CannotRedoException, CannotUndoException, ClassCastException, CMMException, ConcurrentModificationException, DOMException, EmptyStackException, IllegalArgumentException, IllegalMonitorStateException, IllegalPathStateException, IllegalStateException, ImagingOpException, IndexOutOfBoundsException, MissingResourceException, NegativeArraySizeException, NoSuchElementException, NullPointerException, ProfileDataException, ProviderException, RasterFormatException, SecurityException, SystemException, UndeclaredThrowableException, UnmodifiableSetException, UnsupportedOperationException

final, finally, finalize 的区别

原文:https://zwmst.com/2447.html

答:final:修饰符(关键字);如果一个类被声明为 final,意味着它不能再派生出新的子类,不能作为父类被继承,因此一个类不能既被声明为 abstract的,又被声明为 final 的;将变量或方法声明为 final,可以保证它们在使用中不被改变;被声明为 final 的变量必须在声明时给定初值,而在以后的引用中只能读取,不可修改;被声明为 final 的方法也同样只能使用,不能重载。

finally:再异常处理时提供 finally 块来执行任何清除操作;如果抛出一个异常,那么相匹配的 catch 子句就会执行,然后控制就会进入 finally 块(如果有的话)。

finalize:方法名;Java 技术允许使用 finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的。它是在 Object 类中定义的,因此所有的类都继承了它。子类覆盖 finalize() 方法以整理系统资源或者执行其他清理工作。finalize() 方法是在垃圾收集器删除对象之前对这个对象调用的。

类 Example A 继承 Exception,类 ExampleB 继承 Example A

原文:https://zwmst.com/2449.html

有如下代码片断:

try{ 
 throw new ExampleB(“b”); 
}catch(ExampleA e){ 
 System.out.printfln(“ExampleA”); 
}catch(Exception e){ 
 System.out.printfln(“Exception”); 
} 

输出的内容应该是: A:ExampleA B:Exception C:b D:无 答:输出为 A。

介绍 JAVA 中的 Collection FrameWork(及如何写自己的数据结构)

原文:https://zwmst.com/2451.html

答:Collection FrameWork 如下:

Collection 
├List 
│├LinkedList 
│├ArrayList 
│└Vector 
│ └Stack 
└Set 
Map 
├Hashtable 
├HashMap 
└WeakHashMap 

Collection 是最基本的集合接口,一个 Collection 代表一组 Object,即Collection 的元素(Elements);Map 提供 key 到 value 的映射。

List,Set,Map 是否继承自 Collection 接口

原文:https://zwmst.com/2453.html

答:List,Set 是;Map 不是。

你所知道的集合类都有哪些主要方法

原文:https://zwmst.com/2455.html

答:最常用的集合类是 List 和 Map。List 的具体实现包括 ArrayList 和 Vector,它们是可变大小的列表,比较适合构建、存储和操作任何类型对象的元素列表。 List 适用于按数值索引访问元素的情形。 Map 提供了一个更通用的元素存储方法。 Map 集合类用于存储元素对(称作“键”和“值”),其中每个键映射到一个值。

说出 ArrayList,Vector, LinkedList 的存储性能和特性

原文:https://zwmst.com/2457.html

答:ArrayList 和 Vector 都是使用数组方式存储数据,此数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢,Vector 由于使用了 synchronized 方法(线程安全),通常性能上较 ArrayList 差,而LinkedList 使用双向链表实现存储,按序号索引数据需要进行前向或后向遍历,但是插入数据时只需要记录本项的前后项即可,所以插入速度较快。

Collection 和 Collections 的区别

原文:https://zwmst.com/2459.html

答:Collection 是 java.util 下的接口,它是各种集合的父接口,继承于它的接口主要有 Set 和 List; Collections 是个 java.util 下的类,是针对集合的帮助类,提供一系列静态方法实现对各种集合的搜索、排序、线程安全化等操作。

HashMap 和 Hashtable 的区别

原文:https://zwmst.com/2461.html

答:二者都实现了 Map 接口,是将惟一键映射到特定的值上;主要区别在于:

  • 1)HashMap 没有排序,允许一个 null 键和多个 null 值,而 Hashtable 不允许;
  • 2)HashMap 把 Hashtable 的 contains 方法去掉了,改成 containsvalue 和containsKey,因为 contains 方法容易让人引起误解;
  • 3)Hashtable 继承自 Dictionary 类,HashMap 是 Java1.2 引进的 Map 接口的实现;
  • 4)Hashtable 的方法是 Synchronize 的,而 HashMap 不是,在多个线程访问Hashtable 时,不需要自己为它的方法实现同步,而 HashMap 就必须为之提供外同步。 Hashtable 和 HashMap 采用的 hash/rehash 算法大致一样,所以性能不会有很大的差异。

Arraylist 与 Vector 区别

原文:https://zwmst.com/2463.html

答:就 ArrayList 与 Vector 主要从二方面来说:

  • 1)同步性:Vector 是线程安全的(同步),而 ArrayList 是线程序不安全的;
  • 2)数据增长:当需要增长时,Vector 默认增长一倍,而 ArrayList 却是一半。

List、Map、Set 三个接口,存取元素时,各有什么特点

原文:https://zwmst.com/2465.html

答:List 以特定次序来持有元素,可有重复元素。 Set 无法拥有重复元素,内部排序。 Map 保存 key-value 值,value 可多值。

Set 里的元素是不能重复的,那么用什么方法来区分重复与否呢 是用==还是 equals() 它们有何区别

原文:https://zwmst.com/2467.html

答:Set 里的元素是不能重复的,用 equals ()方法来区分重复与否。 覆盖 equals()方法用来判断对象的内容是否相同,而”==”判断地址是否相等,用来决定引用值是否指向同一对象。

用程序给出随便大小的 10 个数,序号为 1-10,按从小到大顺序输出,并输出相应的序号

原文:https://zwmst.com/2469.html

答:代码如下:

 package test; 
 import java.util.ArrayList; 
 import java.util.Collections; 
 import java.util.Iterator; 
 import java.util.List; 
 import java.util.Random; 
 public class RandomSort { 
 public static void printRandomBySort() { 
 Random random = new Random(); // 创建随机数生成器 
 List list = new ArrayList(); 
 // 生成 10 个随机数,并放在集合 list 中 
 for (int i = 0; i < 10; i++) { 
 list.add(random.nextInt(1000)); 
 } 
 Collections.sort(list); // 对集合中的元素进行排序 
 Iterator it = list.iterator(); 
 int count = 0; 
 while (it.hasNext()) { // 顺序输出排序后集合中的元素 
 System.out.println(++count + ": " + it.next()); 
 } 
 } 
 public static void main(String[] args) { 
 printRandomBySort(); 
 } 
 } 

用 JAVA 实现一种排序,JAVA 类实现序列化的方法? 在 COLLECTION 框架中,实现比较要实现什么样的接口

原文:https://zwmst.com/2471.html

答:用插入法进行排序代码如下:

 package test; 
 import java.util.*; 
 class InsertSort { 
 ArrayList al; 
 public InsertSort(int num,int mod) { 
 al = new ArrayList(num); 
 Random rand = new Random(); 
 System.out.println("The ArrayList Sort Before:"); 
 for (int i=0;i<num al.add="" integer="" mod="" system.out.println="" public="" void="" sortit="" tempint="" int="" maxsize="1;" for="" i="1;i<al.size();i++){" if="">= 
 ((Integer)al.get(MaxSize-1)).intValue()){ 
 al.add(MaxSize,tempInt); 
 MaxSize++; 
 System.out.println(al.toString()); 
 }else{ 
 for (int j=0;j<maxsize if="">=tempInt.intValue()){ 
 al.add(j,tempInt); 
 MaxSize++; 
 System.out.println(al.toString()); 
 break; 
 } 
 } 
 } 
 } 
 System.out.println("The ArrayList Sort After:"); 
 for(int i=0;i</maxsize></num>

JAVA 类实现序例化的方法是实现 java.io.Serializable 接口; Collection 框架中实现比较要实现 Comparable 接口和 Comparator 接口。

sleep() 和 wait() 有什么区别

原文:https://zwmst.com/2473.html

答:sleep 是线程类(Thread)的方法,导致此线程暂停执行指定时间,给执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用 sleep 不会释放对象锁。 wait 是 Object 类的方法,对此对象调用 wait 方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出 notify 方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。

当一个线程进入一个对象的一个 synchronized 方法后,其它线程是否可进入此对象的其它方法

原文:https://zwmst.com/2475.html

答:其它线程只能访问该对象的其它非同步方法,同步方法则不能进入。

请说出你所知道的线程同步的方法

原文:https://zwmst.com/2477.html

答:

  • wait():使一个线程处于等待状态,并且释放所持有的对象的 lock;
  • sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉 InterruptedException 异常;
  • notify():唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且不是按优先级;
  • notityAll():唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争。

多线程有几种实现方法,都是什么同步有几种实现方法,都是什么

原文:https://zwmst.com/2479.html

答:多线程有两种实现方法,分别是继承 Thread 类与实现 Runnable 接口 ,同步的实现方面有两种,分别是 synchronized,wait 与 notify。

同步和异步有何异同,在什么情况下分别使用他们举例说明

原文:https://zwmst.com/2481.html

答:如果数据将在线程间共享。例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就是共享数据,必须进行同步存取。当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率。

启动一个线程是用 run()还是 start()

原文:https://zwmst.com/2483.html

答:启动一个线程是调用 start()方法,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由 JVM 调度并执行。这并不意味着线程就会立即运行。run()方法可以产生必须退出的标志来停止一个线程。

线程的基本概念、线程的基本状态以及状态之间的关系

原文:https://zwmst.com/2490.html

答:线程指在程序执行过程中,能够执行程序代码的一个执行单位,每个程序至少都有一个线程,也就是程序本身; Java 中的线程有四种状态分别是:运行、就绪、挂起、结束。

简述 synchronized 和 java.util.concurrent.locks.Lock 的异同

原文:https://zwmst.com/2492.html

答:

  • 主要相同点:Lock 能完成 synchronized 所实现的所有功能;
  • 主要不同点:Lock 有比 synchronized 更精确的线程语义和更好的性能。
  • synchronized 会自动释放锁,而 Lock 一定要求程序员手工释放,并且必须在finally 从句中释放。

java 中有几种方法可以实现一个线程用什么关键字修饰同步方法 stop()和 suspend()方法为何不推荐使用

原文:https://zwmst.com/2494.html

答:有两种实现方法,分别是继承 Thread 类与实现 Runnable 接口; 用 synchronized 关键字修饰同步方法; 反对使用 stop(),是因为它不安全。它会解除由线程获取的所有锁定,而且如果对象处于一种不连贯状态,那么其他线程能在那种状态下检查和修改它们。结果很难检查出真正的问题所在; suspend()方法容易发生死锁。 调用 suspend()的时候,目标线程会停下来,但却仍然持有在这之前获得的锁定。此时,其他任何线程都不能访问锁定的资源,除非被“挂起”的线程恢复运行。对任何线程来说,如果它们想恢复目标线程,同时又试图使用任何一个锁定的资源,就会造成死锁。 故不应该使用 suspend(),而应在自己的 Thread 类中置入一个标志,指出线程应该活动还是挂起。若标志指出线程应该挂起,便用 wait()命其进入等待状态。若标志指出线程应当恢复,则用一个 notify()重新启动线程。

设计 4 个线程,其中两个线程每次对 j 增加 1,另两个线程对 j 每次减少 1;写出程序

原文:https://zwmst.com/2496.html

答:以下程序使用内部类实现线程,对 j 增减的时候没有考虑顺序问题:

 public class TestThread { 
 private int j; 
 public TestThread(int j) {this.j = j;} 
 private synchronized void inc(){ 
 j++; 
 System.out.println(j + "--Inc--" + 
 Thread.currentThread().getName()); 
 } 
 private synchronized void dec(){ 
 j--; 
 System.out.println(j + "--Dec--" + 
 Thread.currentThread().getName()); 
 } 
 public void run() { 
 (new Dec()).start(); 
 new Thread(new Inc()).start(); 
 (new Dec()).start(); 
 new Thread(new Inc()).start(); 
 } 
 class Dec extends Thread { 
 public void run() { 
 for(int i=0; i<100; i++){ 
 dec(); 
 } 
 } 
 } 
 class Inc implements Runnable { 
 public void run() { 
 for(int i=0; i<100; i++){ 
 inc(); 
 } 
 } 
 } 
 public static void main(String[] args) { 
 (new TestThread(5)).run(); 
 } 
} 

什么是 java 序列化,如何实现 java 序列化

原文:https://zwmst.com/2498.html

答:序列化就是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化。 可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。 序列化是为了解决在对对象流进行读写操作时所引发的问题; 序列化的实现:将需要被序列化的类实现 Serializable 接口,该接口没有需实现的方法,implements Serializable 只是为了标注该对象是可被序列化的,然后使用一个输出流(如 FileOutputStream)来构造一个 ObjectOutputStream(对象流)对象,接着,使用 ObjectOutputStream 对象的 writeObject(Object obj)方法就可以将参数为 obj 的对象写出(即保存其状态),要恢复的话则用输入流。

java 中有几种类型的流JDK 为每种类型的流提供了一些抽象类以供继承,请说出他们分别是哪些类

原文:https://zwmst.com/2500.html

答:字节流,字符流。 字节流继承于 InputStream、OutputStream,字符流继承于 Reader、Writer。在 java.io 包中还有许多其他的流,主要是为了提高性能和使用方便。

文件和目录(IO)操作

原文:https://zwmst.com/2502.html

  • 1)如何列出某个目录下的所有文件?
  • 2)如何列出某个目录下的所有子目录?
  • 3)如何判断一个文件或目录是否存在?
  • 4)如何读写文件?【基础】 答:1)示例代码如下:

    File file = new File("e:\\总结"); File[] files = file.listFiles(); for(int i=0; i<files.length i="" if="" system.out.println=""></files.length>

    2)示例代码如下:

    File file = new File("e:\\总结"); File[] files = file.listFiles(); for(int i=0; i<files.length i="" if="" system.out.println=""></files.length>

    3)创建 File 对象,调用其 exsit()方法即可返回是否存在,如:

    System.out.println(new File("d:\\t.txt").exists());

    4)示例代码如下:

    //读文件: FileInputStream fin = new FileInputStream("e:\\tt.txt"); byte[] bs = new byte[100]; while(true){ int len = fin.read(bs); if(len <= 0) break; System.out.print(new String(bs,0,len)); } fin.close(); //写文件: FileWriter fw = new FileWriter("e:\\test.txt"); fw.write("hello world!" + System.getProperty("line.separator")); fw.write("你好!北京!"); fw.close();

写一个方法,输入一个文件名和一个字符串,统计这个字符串在这个文件中出现的次数

原文:https://zwmst.com/2504.html

答:代码如下:

 public int countWords(String file, String find) throws Exception { 
 int count = 0; 
 Reader in = new FileReader(file); 
 int c; 
 while ((c = in.read()) != -1) { 
 while (c == find.charAt(0)) { 
 for (int i = 1; i < find.length(); i++) { 
 c = in.read(); 
 if (c != find.charAt(i)) break; 
 if (i == find.length() - 1) count++; 
 } 
 } 
 } 
 return count; 
 } 

Java 的通信编程,编程题(或问答),用 JAVA SOCKET 编程,读服务器几个字符,再写入本地显示

原文:https://zwmst.com/2506.html

答:Server 端程序:

 package test; 
 import java.net.*; 
 import java.io.*; 
 public class Server{ 
 private ServerSocket ss; 
 private Socket socket; 
 private BufferedReader in; 
 private PrintWriter out; 
 public Server(){ 
 try { 
 ss=new ServerSocket(10000); 
 while(true){ 
 socket = ss.accept(); 
 String RemoteIP = 
 socket.getInetAddress().getHostAddress(); 
 String RemotePort = ":"+socket.getLocalPort(); 
 System.out.println("A client come in!IP:" 
 + RemoteIP+RemotePort); 
 in = new BufferedReader(new 
 InputStreamReader(socket.getInputStream())); 
 String line = in.readLine(); 
 System.out.println("Cleint send is :" + line); 
 out = 
 new PrintWriter(socket.getOutputStream(),true); 
 out.println("Your Message Received!"); 
 out.close(); 
 in.close(); 
 socket.close(); 
 } 
 }catch (IOException e){ 
 out.println("wrong"); 
 } 
 } 
 public static void main(String[] args){ 
 new Server(); 
 } 
 } 

Client 端程序:

 package test; 
 import java.io.*; 
 import java.net.*; 
 public class Client { 
 Socket socket; 
 BufferedReader in; 
 PrintWriter out; 
 public Client(){ 
 try { 
 System.out.println("Try to Connect to 127.0.0.1:10000"); 
 socket = new Socket("127.0.0.1",10000); 
 System.out.println("The Server Connected!"); 
 System.out.println("Please enter some Character:"); 
 BufferedReader line = new BufferedReader(new 
 InputStreamReader(System.in)); 
 out = new PrintWriter(socket.getOutputStream(),true); 
 out.println(line.readLine()); 
 in = new BufferedReader( 
 new InputStreamReader(socket.getInputStream())); 
 System.out.println(in.readLine()); 
 out.close(); 
 in.close(); 
 socket.close(); 
 }catch(IOException e){ 
 out.println("Wrong"); 
 } 
 } 
 public static void main(String[] args) { 
 new Client(); 
 } 
 } 

UML 是什么?常用的几种图

原文:https://zwmst.com/2508.html

答:UML 是标准建模语言;常用图包括:用例图,静态图(包括类图、对象图和包图),行为图,交互图(顺序图,合作图),实现图。

编程题写一个 Singleton出来

原文:https://zwmst.com/2510.html

答:Singleton 模式主要作用是保证在 Java 应用程序中,一个类 Class 只有一个实例存在。 举例:定义一个类,它的构造函数为 private 的,它有一个 static的 private 的该类变量,在类初始化时实例话,通过一个 public 的 getInstance方法获取对它的引用,继而调用其中的方法。 第一种形式:

public class Singleton { 
 private Singleton(){} 
 private static Singleton instance = new Singleton(); 
 public static Singleton getInstance(){ 
 return instance; 
 } 
} 

第二种形式:

public class Singleton { 
 private static Singleton instance = null; 
 public static synchronized Singleton getInstance(){ 
 if (instance==null) 
 instance=new Singleton(); 
 return instance; 
 } 
} 

其他形式: 定义一个类,它的构造函数为 private 的,所有方法为 static 的。 一般认为第一种形式要更加安全些 。

说说你所熟悉或听说过的j2ee中的几种常用模式及对设计模式的一些看法

原文:https://zwmst.com/2512.html

答:

  • Session Facade Pattern:使用 SessionBean 访问 EntityBean;
  • Message Facade Pattern:实现异步调用;
  • EJB Command Pattern:使用 Command JavaBeans 取代 SessionBean,实现轻量级访问;
  • Data Transfer Object Factory:通过 DTO Factory 简化 EntityBean 数据提供特性;
  • Generic Attribute Access:通过 AttibuteAccess 接口简化 EntityBean 数据提供特性;
  • Business Interface:通过远程(本地)接口和 Bean 类实现相同接口规范业务逻辑一致性; EJB 架构的设计好坏将直接影响系统的性能、可扩展性、可维护性、组件可重用性及开发效率。 项目越复杂,项目队伍越庞大则越能体现良好设计的重要性。

Java 中常用的设计模式 说明工厂模式

原文:https://zwmst.com/2514.html

答:Java 中的 23 种设计模式:

  • Factory(工厂模式),
  • Builder(建造模式),
  • Factory Method(工厂方法模式),
  • Prototype(原始模型模式),
  • Singleton(单例模式),
  • Facade(门面模式),
  • Adapter(适配器模式),
  • Bridge(桥梁模式),
  • Composite(合成模式),
  • Decorator(装饰模式),
  • Flyweight(享元模式),
  • Proxy(代理模式),
  • Command(命令模式),
  • Interpreter(解释器模式),
  • Visitor(访问者模式),
  • Iterator(迭代子模式),
  • Mediator(调停者模式),
  • Memento(备忘录模式),
  • Observer(观察者模式),
  • State(状态模式),
  • Strategy(策略模式),
  • Template Method(模板方法模式),
  • Chain Of Responsibleity(责任链模式)。 工厂模式:工厂模式是一种经常被使用到的模式,根据工厂模式实现的类可以根据提供的数据生成一组类中某一个类的实例,通常这一组类有一个公共的抽象父类并且实现了相同的方法,但是这些方法针对不同的数据进行了不同的操作。 首先需要定义一个基类,该类的子类通过不同的方法实现了基类中的方法。 然后需要定义一个工厂类,工厂类可以根据条件生成不同的子类实例。 当得到子类的实例后,开发人员可以调用基类中的方法而不必考虑到底返回的是哪一个子类的实例。

开发中都用到了那些设计模式 用在什么场合

原文:https://zwmst.com/2516.html

答:每个模式都描述了一个在我们的环境中不断出现的问题,然后描述了该问题的解决方案的核心。 通过这种方式,你可以无数次地使用那些已有的解决方案,无需在重复相同的工作。 主要用到了 MVC 的设计模式,用来开发 JSP/Servlet或者 J2EE 的相关应用;及简单工厂模式等。

XML 文档定义有几种形式?它们之间有何本质区别?解析 XML 文档有哪几种方式

原文:https://zwmst.com/2518.html

答:

  • 1)两种形式:dtd 以及 schema;
  • 2)本质区别:schema 本身是 xml 的,可以被 XML 解析器解析(这也是从 DTD上发展 schema 的根本目的);
  • 3)解析方式:有 DOM,SAX,STAX 等:
    • DOM:处理大型文件时其性能下降的非常厉害。这个问题是由 DOM 的树结构所造成的,这种结构占用的内存较多,而且 DOM 必须在解析文件之前把整个文档装入内存,适合对 XML 的随机访问;
    • SAX:不同于 DOM,SAX 是事件驱动型的 XML 解析方式。它顺序读取 XML 文件,不需要一次全部装载整个文件。当遇到像文件开头,文档结束,或者标签开头与标签结束时,它会触发一个事件,用户通过在其回调事件中写入处理代码来处理 XML 文件,适合对 XML 的顺序访问;
    • STAX:Streaming API for XML (StAX)。

你对软件开发中迭代的含义的理解

原文:https://zwmst.com/2520.html

答:软件开发中,各个开发阶段不是顺序执行的,应该是并行执行,也就是迭代的意思。 这样对于开发中的需求变化,及人员变动都能得到更好的适应。

你在项目中用到了 xml 技术的哪些方面 如何实现的

原文:https://zwmst.com/2522.html

答:用到了数据存贮,信息配置两方面。 在做数据交换平台时,将不能数据源的数据组装成 XML 文件,然后将 XML 文件压缩打包加密后通过网络传送给接收者,接收解密与解压缩后再同 XML 文件中还原相关信息进行处理。 在做软件配置时,利用 XML 可以很方便的进行,软件的各种配置参数都存贮在 XML 文件中。

用 jdom 解析 xml 文件时如何解决中文问题 如何解析

原文:https://zwmst.com/2524.html

答:看如下代码,用编码方式加以解决

 package test; 
 import java.io.*; 
 public class DOMTest{ 
 private String inFile = "c:\people.xml"; 
 private String outFile = "c:\people.xml"; 
 public static void main(String args[]){ 
 new DOMTest(); 
 } 
 public DOMTest(){ 
 try{ 
 javax.xml.parsers.DocumentBuilder builder = 
 javax.xml.parsers.DocumentBuilderFactory. 
 newInstance().newDocumentBuilder(); 
 org.w3c.dom.Document doc = builder.newDocument(); 
 org.w3c.dom.Element root = doc.createElement("老师"); 
 org.w3c.dom.Element wang = doc.createElement("王"); 
 org.w3c.dom.Element liu = doc.createElement("刘"); 
 wang.appendChild(doc.createTextNode("我是王老师")); 
 root.appendChild(wang); 
 doc.appendChild(root); 
 javax.xml.transform.Transformer transformer = 
 javax.xml.transform.TransformerFactory. 
 newInstance().newTransformer(); 
 transformer.setOutputProperty( 
 javax.xml.transform.OutputKeys.ENCODING,"gb2312"); 
 transformer.setOutputProperty( 
 javax.xml.transform.OutputKeys.INDENT, "yes"); 
 transformer.transform(new 
 javax.xml.transform.dom.DOMSource(doc), 
 new javax.xml.transform.stream.StreamResult(outFile)); 
 }catch (Exception e){ 
 System.out.println (e.getMessage()); 
 } 
 } 
 } 

编程用 JAVA 解析 XML 的方式

原文:https://zwmst.com/2526.html

答:用 SAX 方式解析 XML,XML 文件如下:

 <person> 
 <name>王小明</name> 
 <college>信息学院</college> 
 <telephone>6258113</telephone> 
 <notes>男,1955 年生,博士,95 年调入海南大学</notes> 
 </person> 

事件回调类 SAXHandler.java :

 import java.io.*; 
 import java.util.Hashtable; 
 import org.xml.sax.*; 
 public class SAXHandler extends HandlerBase{ 
 private Hashtable table = new Hashtable(); 
 private String currentElement = null; 
 private String currentValue = null; 
 public void setTable(Hashtable table){ 
 this.table = table; 
 } 
 public Hashtable getTable(){ 
 return table; 
 } 
 public void startElement(String tag, AttributeList attrs) 
 throws SAXException{ 
 currentElement = tag; 
 } 
 public void characters(char[] ch, int start, int length) 
 throws SAXException{ 
 currentValue = new String(ch, start, length); 
 } 
 public void endElement(String name) throws SAXException{ 
 if (currentElement.equals(name)) 
 table.put(currentElement, currentValue); 
 } 
 } 

JSP 内容显示源码,SaxXml.jsp:

 <title>剖析 XML 文件 people.xml</title> 

 " + 
 "教师信息表"); 
 out.println("姓名" + "" + 
 (String)hashTable.get(new String("name")) + ""); 
 out.println("学院" + "" + 
 (String)hashTable.get(new String("college")) 
 +""); 
 out.println("电话" + "" + 
 (String)hashTable.get(new String("telephone")) 
 + ""); 
 out.println("备注" + "" + 
 (String)hashTable.get(new String("notes")) 
 + ""); 
 out.println(""); 
 %> 

有 3 个表(15 分钟)

原文:https://zwmst.com/2528.html

  • Student 学生表 (学号,姓名,性别,年龄,组织部门)
  • Course 课程表 (编号,课程名称)
  • Sc 选课表 (学号,课程编号,成绩) 表结构如下:
  • 1)写一个 SQL 语句,查询选修了’计算机原理’的学生学号和姓名(3 分钟)
  • 2)写一个 SQL 语句,查询’周星驰’同学选修了的课程名字(3 分钟)
  • 3)写一个 SQL 语句,查询选修了 5 门课程的学生学号和姓名(9 分钟) 答:1)SQL 语句如下:

    select stu.sno, stu.sname from Student stu where (select count(*) from sc where sno=stu.sno and cno = (select cno from Course where cname='计算机原理')) != 0;

    2)SQL 语句如下:

    select cname from Course where cno in ( select cno from sc where sno = (select sno from Student where sname='周星驰'));

    3)SQL 语句如下:

    select stu.sno, stu.sname from student stu where (select count(*) from sc where sno=stu.sno) = 5;

有三张表,学生表 S,课程 C,学生课程表 SC,学生可以选修多门课程,一门课程可以被多个学生选修,通过 SC 表关联

原文:https://zwmst.com/2532.html

1)写出建表语句; 2)写出 SQL 语句,查询选修了所有选修课程的学生; 3)写出 SQL 语句,查询选修了至少 5 门以上的课程的学生。 答:1)建表语句如下(mysql 数据库):

 create table s(id integer primary key, name varchar(20)); 
 create table c(id integer primary key, name varchar(20)); 
 create table sc( 
 sid integer references s(id), 
 cid integer references c(id), 
 primary key(sid,cid) 
 ); 

2)SQL 语句如下:

 select stu.id, stu.name from s stu 
 where (select count(*) from sc where sid=stu.id) 
 = (select count(*) from c); 

3)SQL 语句如下:

 select stu.id, stu.name from s stu 
 where (select count(*) from sc where sid=stu.id)>=5; 

列出所有年龄比所属主管年龄大的人的 ID 和名字

原文:https://zwmst.com/2534.html

ID NAME AGE MANAGER(所属主管人 ID)
106 A 30 104
109 B 19 104
104 C 20 111
107 D 35 109
112 E 25 120
119 F 45 NULL

要求:列出所有年龄比所属主管年龄大的人的 ID 和名字? 答:SQL 语句如下:

select employee.name from test employee where employee.age > (select manager.age from test manager where manager.id=employee.manager);

有如下两张表

原文:https://zwmst.com/2536.html

表 city: 表 state:

CityNo CityName StateNo
BJ 北京 (Null)
SH 上海 (Null)
GZ 广州 GD
DL 大连 LN
State No State Name
GD 广东
LN 辽宁
SD 山东
NMG 内蒙古

欲得到如下结果:

City No City Name State No State Name
BJ 北京 (Null) (Null)
DL 大连 LN 辽宁
GZ 广州 GD 广东
SH 上海 (Null) (Null)

写相应的 SQL 语句。 答:SQL 语句为:

 SELECT C.CITYNO, C.CITYNAME, C.STATENO, S.STATENAME 
 FROM CITY C, STATE S 
 WHERE C.STATENO=S.STATENO(+) 
 ORDER BY(C.CITYNO); 

数据库,比如 100 用户同时来访,要采取什么技术解决

原文:https://zwmst.com/2538.html

答:可采用连接池。

什么是 ORM

原文:https://zwmst.com/2540.html

答:对象关系映射(Object—Relational Mapping,简称 ORM)是一种为了解决面向对象与面向关系数据库存在的互不匹配的现象的技术; 简单的说,ORM 是通过使用描述对象和数据库之间映射的元数据,将 java 程序中的对象自动持久化到关系数据库中;本质上就是将数据从一种形式转换到另外一种形式。

Hibernate 有哪 5 个核心接口

原文:https://zwmst.com/2544.html

答:

  • Configuration 接口:配置 Hibernate,根据其启动 hibernate,创建SessionFactory 对象;
  • SessionFactory 接口:初始化 Hibernate,充当数据存储源的代理,创建session 对象,sessionFactory 是线程安全的,意味着它的同一个实例可以被应用的多个线程共享,是重量级、二级缓存;
  • Session 接口:负责保存、更新、删除、加载和查询对象,是线程不安全的,避免多个线程共享同一个 session,是轻量级、一级缓存;
  • Transaction 接口:管理事务;
  • Query 和 Criteria 接口:执行数据库的查询。

关于 hibernate

原文:https://zwmst.com/2546.html

1)在 hibernate 中,在配置文件呈标题一对多,多对多的标签是什么; 2)Hibernate 的二级缓存是什么; 3)Hibernate 是如何处理事务的; 答:

  • 1)一对多的标签为\(<\)one-to-many\(>\) ;多对多的标签为\(<\)many-to-many\(>\)
  • 2)sessionFactory 的缓存为 hibernate 的二级缓存;
  • 3)Hibernate 的事务实际上是底层的 JDBC Transaction 的封装或者是 JTA Transaction 的封装;默认情况下使用 JDBCTransaction。

Hibernate 的应用(Hibernate 的结构)

原文:https://zwmst.com/2548.html

答:

//首先获得 SessionFactory 的对象 
 SessionFactory sessionFactory = new Configuration().configure(). 
 buildSessionFactory(); 
 //然后获得 session 的对象 
 Session session = sessionFactory.openSession(); 
 //其次获得 Transaction 的对象 
 Transaction tx = session.beginTransaction(); 
 //执行相关的数据库操作:增,删,改,查 
 session.save(user); //增加, user 是 User 类的对象 
 session.delete(user); //删除 
 session.update(user); //更新 
 Query query = session.createQuery(“from User”); //查询 
 List list = query.list(); 
 //提交事务 
 tx.commit(); 
 //如果有异常,我们还要作事务的回滚,恢复到操作之前 
 tx.rollback(); 
 //最后还要关闭 session,释放资源 
 session.close(); 

什么是重量级?什么是轻量级

原文:https://zwmst.com/2550.html

答:轻量级是指它的创建和销毁不需要消耗太多的资源,意味着可以在程序中经常创建和销毁 session 的对象; 重量级意味不能随意的创建和销毁它的实例,会占用很多的资源。

数据库的连接字符串

原文:https://zwmst.com/2552.html

答:MS SQL Server

 //第二种连接方式 
 Class.forName(“com.microsoft.jdbc.sqlserver.SQLServerDriver”). 
 newInstance(); 
 conn = DriverManager.getConnection(“jdbc:Microsoft:sqlserver 
 ://localhost:1433;DatabaseName=pubs”,”sa”,””);
 //Oracle 
 Class.forName(“oracle.jdbc.driver.OracleDriver”).newInstance(); 
 conn = DriverManager.getConnection(“jdbc:oracle:thin: 
 @localhost:1521:sid”, uid, pwd); 
 //Mysql 
 Class.forName(“org.git.mm.mysql.Driver”).newInstance(); 
 conn = DriverManager.getConnection(“jdbc:mysql 
 ://localhost:3306/pubs”,”root”,””); 

处理中文的问题:

 jdbc:mysql://localhost:3306/pubs?useUnicode=true 
 &characterEncoding=GB2312 

事务处理

原文:https://zwmst.com/2554.html

答:Connection 类中提供了 3 个事务处理方法:

  • setAutoCommit(Boolean autoCommit):设置是否自动提交事务,默认为自动提交事务,即为 true,通过设置 false 禁止自动提交事务;
  • commit():提交事务;
  • rollback():回滚事务。

Java中访问数据库的步骤?Statement和 PreparedStatement之间的区别

原文:https://zwmst.com/2556.html

答:Java 中访问数据库的步骤如下:

  • 1)注册驱动;
  • 2)建立连接;
  • 3)创建 Statement;
  • 4)执行 sql 语句;
  • 5)处理结果集(若 sql 语句为查询语句);
  • 6)关闭连接。 PreparedStatement 被创建时即指定了 SQL 语句,通常用于执行多次结构相同的 SQL 语句。

用你熟悉的语言写一个连接 ORACLE 数据库的程序,能够完成修改和查询工作

原文:https://zwmst.com/2558.html

答:JDBC 示例程序如下:

 public void testJdbc(){ 
 Connection con = null; 
 PreparedStatement ps = null; 
 ResultSet rs = null; 
 try{ 
 //step1:注册驱动; 
 Class.forName("oracle.jdbc.driver.OracleDriver"); 
 //step 2:获取数据库连接; 
 con=DriverManager.getConnection( 
 "jdbc:oracle:thin:@192.168.0.39:1521:TARENADB", 
 "sd0605","sd0605"); 
 /************************查 询************************/ 
 //step 3:创建 Statement; 
 String sql = "SELECT id, fname, lname, age, FROM Person_Tbl"; 
 ps = con.prepareStatement(sql); 
 //step 4 :执行查询语句,获取结果集; 
 rs = ps.executeQuery(); 
 //step 5:处理结果集—输出结果集中保存的查询结果; 
 while (rs.next()){ 
 System.out.print("id = " + rs.getLong("id")); 
 System.out.print(" , fname = " + rs.getString("fname")); 
 System.out.print(" , lname = " + rs.getString("lname")); 
 System.out.print(" , age = " + rs.getInt("age")); 
 } 
 /************************JDBC 修 改*********************/ 
 sql = "UPDATE Person_Tbl SET age=23 WHERE id = ?"; 
 ps = con.prepareStatement(sql); 
 ps.setLong(1, 88); 
 int rows = ps.executeUpdate(); 
 System.out.println(rows + " rows affected."); 
 } catch (Exception e){ 
 e.printStackTrace(); 
 } finally{ 
 try{ 
 con.close(); //关闭数据库连接,以释放资源。 
 } catch (Exception e1) { 
 } 
 } 
 } 

JDBC,Hibernate 分页怎样实现

原文:https://zwmst.com/2560.html

答:方法分别为:

  • 1) Hibernate 的分页:

    Query query = session.createQuery("from Student"); query.setFirstResult(firstResult);//设置每页开始的记录号 query.setMaxResults(resultNumber);//设置每页显示的记录数 Collection students = query.list();

  • 2) JDBC 的分页:根据不同的数据库采用不同的 sql 分页语句 例如: Oracle 中的 sql 语句为:

    "SELECT * FROM (SELECT a.*, rownum r FROM TB_STUDENT) WHERE r between 2 and 10"

    查询从记录号 2 到记录号 10 之间的所有记录

javascript 的优缺点和内置对象

原文:https://zwmst.com/2564.html

答:

  • 1)优点:简单易用,与 Java 有类似的语法,可以使用任何文本编辑工具编写,只需要浏览器就可执行程序,并且事先不用编译,逐行执行,无需进行严格的变量声明,而且内置大量现成对象,编写少量程序可以完成目标;
  • 2)缺点:不适合开发大型应用程序;
  • 3)Javascript 有 11 种内置对象: Array、String、Date、Math、Boolean、Number、Function、Global、Error、RegExp、Object。

EJB 与 JAVA BEAN 的区别

原文:https://zwmst.com/2566.html

答:Java Bean 是可复用的组件,对 Java Bean 并没有严格的规范,理论上讲,任何一个 Java 类都可以是一个 Bean。 但通常情况下,由于 Java Bean 是被容器所创建(如 Tomcat)的,所以 Java Bean 应具有一个无参的构造器,另外,通常 Java Bean 还要实现 Serializable 接口用于实现 Bean 的持久性。Java Bean实际上相当于微软 COM模型中的本地进程内 COM组件,它是不能被跨进程访问的。 Enterprise Java Bean 相当于 DCOM,即分布式组件。它是基于 Java 的远程方法调用(RMI)技术的,所以 EJB 可以被远程访问(跨进程、跨计算机)。但 EJB必须被布署在诸如 Webspere、WebLogic 这样的容器中,EJB 客户从不直接访问真正的 EJB 组件,而是通过其容器访问。EJB 容器是 EJB 组件的代理,EJB 组件由容器所创建和管理。客户通过容器来访问真正的 EJB 组件。

EJB 的几种类型

原文:https://zwmst.com/2568.html

答:会话(Session)Bean、实体(Entity)Bean、消息驱动的(Message Driven)Bean; 会话 Bean 又可分为有状态(Stateful)和无状态(Stateless)两种; 实体 Bean 可分为 Bean 管理的持续性(BMP)和容器管理的持续性(CMP)两种。

remote 接口和 home 接口主要作用

原文:https://zwmst.com/2570.html

答: remote 接口定义了业务方法,用于 EJB 客户端调用业务方法; home 接口是 EJB 工厂用于创建和移除查找 EJB 实例。

客服端口调用 EJB 对象的几个基本步骤

原文:https://zwmst.com/2575.html

答:设置 JNDI 服务工厂以及 JNDI 服务地址系统属性,查找 Home 接口,从 Home接口调用 Create 方法创建 Remote 接口,通过 Remote 接口调用其业务方法。

EJB 的角色和三个对象

原文:https://zwmst.com/2577.html

答:一个完整的基于 EJB 的分布式计算结构由六个角色组成,这六个角色可以由不同的开发商提供,每个角色所作的工作必须遵循 Sun 公司提供的 EJB 规范,以保证彼此之间的兼容性。 这六个角色分别是

  • EJB 组件开发者(Enterprise Bean Provider) 、
  • 应用组合者(Application Assembler)、
  • 部署者(Deployer)、
  • EJB 服务器提供者(EJB Server Provider)、
  • EJB 容器提供者(EJB Container Provider)、
  • 系统管理员(System Administrator), 这里面,EJB 容器是 EJB之所以能够运行的核心。EJB 容器管理着 EJB 的创建,撤消,激活,去活,与数据库的连接等等重要的核心工作;三个对象是 Remote(Local)接口、Home(LocalHome)接口,Bean 类。

EJB 是基于哪些技术实现的?并说出 SessionBean 和 EntityBean 的区别,StatefulBean 和 StatelessBean 的区别

原文:https://zwmst.com/2579.html

答:EJB 包括 Session Bean、Entity Bean、Message Driven Bean,基于 JNDI、RMI、JTA 等技术实现。 SessionBean 在 J2EE 应用程序中被用来完成一些服务器端的业务操作,例如访问数据库、调用其他 EJB 组件。EntityBean 被用来代表应用系统中用到的数据。 对于客户机,SessionBean 是一种非持久性对象,它实现某些在服务器上运行的业务逻辑。 对于客户机,EntityBean 是一种持久性对象,它代表一个存储在持久性存储器中的实体的对象视图,或是一个由现有企业应用程序实现的实体。 Session Bean 还可以再细分为 Stateful Session Bean 与 Stateless Session Bean ,这两种的 Session Bean 都可以将系统逻辑放在 method 之中执行,不同的是 Stateful Session Bean 可以记录呼叫者的状态,因此通常来说,一个使用者会有一个相对应的 Stateful Session Bean 的实体。 Stateless Session Bean 虽然也是逻辑组件,但是他却不负责记录使用者状态,也就是说当使用者呼叫 Stateless Session Bean 的时候,EJB Container 并不会找寻特定的 Stateless Session Bean 的实体来执行这个 method。换言之,很可能数个使用者在执行某个 Stateless Session Bean 的 methods 时,会是同一个 Bean 的 Instance 在执行。从内存方面来看, Stateful Session Bean 与 Stateless Session Bean 比较, Stateful Session Bean 会消耗 J2EE Server 较多的内存,然而 Stateful Session Bean 的优势却在于他可以维持使用者的状态。

bean 实例的生命周期

原文:https://zwmst.com/2581.html

答:对于 Stateless Session Bean、Entity Bean、Message Driven Bean 一般存在缓冲池管理,而对于 Entity Bean 和 Statefull Session Bean 存在 Cache管理,通常包含创建实例,设置上下文、创建 EJB Object(create)、业务方法调用、remove 等过程,对于存在缓冲池管理的 Bean,在 create 之后实例并不 从内存清除,而是采用缓冲池调度机制不断重用实例,而对于存在 Cache 管理的Bean 则通过激活和去激活机制保持 Bean 的状态并限制内存中实例数量。

EJB 的激活机制

原文:https://zwmst.com/2583.html

答:以 Stateful Session Bean 为例:其 Cache 大小决定了内存中可以同时存在的 Bean 实例的数量,根据 MRU 或 NRU 算法,实例在激活和去激活状态之间迁移,激活机制是当客户端调用某个 EJB 实例业务方法时,如果对应 EJB Object发现自己没有绑定对应的 Bean 实例则从其去激活 Bean 存储中(通过序列化机制存储实例)回复(激活)此实例。状态变迁前会调用对应的 ejbActive 和ejbPassivate 方法。

EJB 包括(SessionBean,EntityBean)说出他们的生命周期,及如何管理事务的

原文:https://zwmst.com/2585.html

答:SessionBean:Stateless Session Bean 的生命周期是由容器决定的,当客户机发出请求要建立一个 Bean 的实例时,EJB 容器不一定要创建一个新的 Bean的实例供客户机调用,而是随便找一个现有的实例提供给客户机。当客户机第一次调用一个 Stateful Session Bean 时,容器必须立即在服务器中创建一个新的 Bean 实例,并关联到客户机上,以后此客户机调用 Stateful Session Bean 的方法时容器会把调用分派到与此客户机相关联的 Bean 实例。 EntityBean:Entity Beans 能存活相对较长的时间,并且状态是持续的。只要数据库中的数据存在,Entity beans 就一直存活。而不是按照应用程序或者服务进程来说的。即使 EJB容器崩溃了,Entity beans 也是存活的。Entity Beans 生命周期能够被容器或者 Beans 自己管理。EJB 通过以下技术管理事务:对象管理组织(OMG)的对象实务服务(OTS),Sun Microsystems 的 Transaction Service(JTS)、Java Transaction API(JTA),开发组(X/Open)的 XA 接口。

EJB 的事务是如何实现的?何时进行回滚

原文:https://zwmst.com/2587.html

答:是通过使用容器或 Bean 自身管理事务的; 当产生一个系统异常时容器就自动回滚事务。

EJB 容器提供的服务

原文:https://zwmst.com/2589.html

答:主要提供生命周期管理、代码产生、持续性管理、安全、事务管理、锁和并发行管理等服务。

EJB 需直接实现它的业务接口或 Home 接口吗?请简述理由

原文:https://zwmst.com/2591.html

答:远程接口和 Home 接口不需要直接实现,他们的实现代码是由服务器产生的,程序运行中对应实现类会作为对应接口类型的实例被使用。

请对以下在 J2EE 中常用的名词进行解释(或简单描述)

原文:https://zwmst.com/2593.html

答:

web 容器:给处于其中的应用程序组件(JSP,SERVLET)提供一个环境,使JSP,SERVLET 直接跟容器中的环境变量接口交互,不必关注其它系统问题。

主要由 WEB 服务器来实现。 例如:TOMCAT,WEBLOGIC,WEBSPHERE 等。 该容器提供的接口严格遵守 J2EE 规范中的 WEB APPLICATION 标准。 我们把遵守以上标准的 WEB服务器就叫做 J2EE 中的 WEB 容器;

EJB 容器:Enterprise java bean 容器。

更具有行业领域特色。 他提供给运行在其中的组件 EJB 各种管理功能。 只要满足 J2EE 规范的 EJB 放入该容器,马上就会被容器进行高效率的管理。 并且可以通过现成的接口来获得系统级别的服务。 例如邮件服务、事务管理;

JNDI:(Java Naming & Directory Interface)JAVA 命名目录服务。

主要提供的功能是:提供一个目录系统,让其它各地的应用程序在其上面留下自己的索引,从而满足快速查找和定位分布式应用程序的功能;

JMS:(Java Message Service)JAVA 消息服务。

主要实现各个应用程序之间的通讯。包括点对点和广播;

JTA:(Java Transaction API)JAVA 事务服务。

提供各种分布式事务服务。 应用程序只需调用其提供的接口即可;

JAF:(Java Action FrameWork)JAVA 安全认证框架。

提供一些安全控制方面的框架。让开发者通过各种部署和自定义实现自己的个性安全控制策略;

RMI/IIOP:(Remote Method Invocation /internet 对象请求中介协议)他们主要用于通过远程调用服务。

例如,远程有一台计算机上运行一个程序,它提供股票分析服务,我们可以在本地计算机上实现对其直接调用。当然这是要通过一定的规范才能在异构的系统之间进行通信。RMI 是 JAVA 特有的。

J2EE 是什么

原文:https://zwmst.com/2595.html

答:J2EE 是 Sun 公司提出的多层(multi-diered),分布式(distributed),基于组件(component-base)的企业级应用模型(enterpriese application model).在这样的一个应用系统中,可按照功能划分为不同的组件,这些组件又可在不同计算机上,并且处于相应的层次(tier)中。 所属层次包括

  • 客户层(clietn tier)组件,
  • web 层和组件,
  • Business 层和组件,
  • 企业信息系统(EIS)层。

J2EE 是技术还是平台还是框架

原文:https://zwmst.com/2597.html

答:J2EE 本身是一个标准,一个为企业分布式应用的开发提供的标准平台; J2EE 也是一个框架,包括 JDBC、JNDI、RMI、JMS、EJB、JTA 等技术。

请写出 spring 中 I0C 的三种实现机制

原文:https://zwmst.com/2599.html

答:三种机制为:通过 setter 方法注入、通过构造方法注入和接口注入。

写出你熟悉的开源框架以及各自的作用

原文:https://zwmst.com/2601.html

答:框架:

  • hibernate、spring、struts;
  • Hibernate 主要用于数据持久化;
  • Spring 的控制反转能起到解耦合的作用;
  • Struts 主要用于流程控制。

EJB 规范规定 EJB 中禁止的操作有哪些

原文:https://zwmst.com/2603.html

答:

  • 1)不能操作线程和线程 API(线程 API 指非线程对象的方法,如 notify,wait等);
  • 2)不能操作 awt;
  • 3)不能实现服务器功能;
  • 4)不能对静态属性存取;
  • 5)不能使用 IO 操作直接存取文件系统;
  • 6)不能加载本地库;
  • 7)不能将 this 作为变量和返回;
  • 8)不能循环调用。

一个 byte 几个单位

原文:https://zwmst.com/2605.html

答:8bit。

常用 UNIX 命令(Linux 的常用命令)(至少 10 个)

原文:https://zwmst.com/2609.html

答:ls pwd mkdir rm cp mv cd ps ftp telnet ping env more echo

后序遍历下列二叉树,访问结点的顺序是

原文:https://zwmst.com/2611.html

A / \ B C / \ \ D E F / / \ G N I / \ J K 答:顺序为:DJGEBKNIFCA 。

排序都有哪几种方法?请列举。用 JAVA 实现一个快速排序

原文:https://zwmst.com/2613.html

答:排序的方法有:插入排序(直接插入排序、希尔排序),交换排序(冒泡排序、快速排序),选择排序(直接选择排序、堆排序),归并排序,分配排序(箱排序、基数排序); 快速排序的伪代码:

//使用快速排序方法对 a[ 0 :n- 1 ]排序 
从 a[ 0 :n- 1 ]中选择一个元素作为 middle,该元素为支点; 
把余下的元素分割为两段 left 和 right,使得 left 中的元素都小于等于支点,
而 right 中的元素都大于等于支点; 
递归地使用快速排序方法对 left 进行排序; 
递归地使用快速排序方法对 right 进行排序; 
所得结果为 left + middle + right。 

写一种常见排序

原文:https://zwmst.com/2615.html

答:C++中冒泡排序:

 void swap( int& a, int& b ){ 
 int c=a; a = b; b = c; 
 } 
 void bubble( int* p, int len ){ 
 bool bSwapped; 
 do { 
 bSwapped = false; 
 for( int i=1; i<len; i++ ){ 
 if( p[i-1]>p[i] ){ 
 swap( p[i-1], p[i] ); 
 bSwapped = true; 
 } 
 } 
 }while( bSwapped ); 
 } 

写一个一小段程序检查数字是否为质数;以上的程序你采用的哪种语言写的?采用该种语言的理由是什么

原文:https://zwmst.com/2617.html

答:代码如下:

 #include <math.h>bool prime( int n ){ 
 if(n<=0) exit(0); 
 for( int i=2; i<=n; i++ ) 
 for( int j=2; j<=sqrt(i); j++) 
 if((n%j==0) && (j!=n)) 
 return false; 
 return true; 
 }</math.h> 

采用 C++,因为其运行效率高。

编程题:设有n个人依围成一圈,从第1个人开始报数,数到第m个人出列,然后从出列的下一个人开始报数,数到第m个人又出列,⋯,如此反复到所有的人全部出列为止。设n个人的编号分别为 1,2,⋯,n,打印出出列的顺序;要求用 java 实现

原文:https://zwmst.com/2620.html

答:代码如下:

 package test; 
 public class CountGame { 
 private static boolean same(int[] p,int l,int n){ 
 for(int i=0;i <l if="" return="" true="" false="" public="" static="" void="" play="" playernum="" int="" step="" p="new" counter="1;" while="">playerNum*step){ 
 break; 
 } 
 for(int i=1;i <playernum while="" if="" break="" else="" i="i+1;">playerNum)break; 
 if(counter%step==0){ 
 System.out.print(i + " "); 
 p[counter/step-1]=i; 
 } 
 counter+=1; 
 } 
 } 
 System.out.println(); 
 } 
 public static void main(String[] args) { 
 play(10, 7); 
 } 
 }</playernum></l> 

写一个方法 1000 的阶乘

原文:https://zwmst.com/2622.html

答:C++的代码实现如下:

 #include <iostream> 
 #include <iomanip> 
 #include <vector> 
 using namespace std; 
 class longint { 
 private: 
 vector <int>iv; 
 public: 
 longint(void) { iv.push_back(1); } 
 longint& multiply(const int &); 
 friend ostream& operator<::const_reverse_iterator iv_iter = v.iv.rbegin(); 
 os << *iv_iter++; 
 for ( ; iv_iter < v.iv.rend(); ++iv_iter) { 
 os << setfill('0') << setw(4) << *iv_iter; 
 } 
 return os; 
 } 
 longint& longint::multiply(const int &rv) { 
 vector<int>::iterator iv_iter = iv.begin(); 
 int overflow = 0, product = 0; 
 for ( ; iv_iter < iv.end(); ++iv_iter) { 
 product = (*iv_iter) * rv; 
 product += overflow; 
 overflow = 0; 
 if (product > 10000) { 
 overflow = product / 10000; 
 product -= overflow * 10000; 
 } 
 iv_iter = product; 
 } 
 if (0 != overflow) { 
 iv.push_back(overflow); 
 } 
 return *this; 
 } 
 int main(int argc, char **argv) { 
 longint result; 
 int l = 0; 
 if(argc==1){ 
 cout << "like: multiply 1000" << endl; 
 exit(0); 
 } 
 sscanf(argv[1], "%d", &l); 
 for (int i = 2; i <= l; ++i) { 
 result.multiply(i); 
 } 
 cout << result << endl; 
 return 0; 
 }</int></int></vector></iomanip></iostream>

以下三条输出语句分别输出什么

原文:https://zwmst.com/2624.html

char str1[] = "abc"; 
char str2[] = "abc"; 
const char str3[] = "abc"; 
const char str4[] = "abc"; 
const char* str5 = "abc"; 
const char* str6 = "abc"; 
cout << boolalpha << (str1==str2) << endl; //输出什么? 
cout << boolalpha << (str3==str4) << endl; //输出什么? 
cout << boolalpha << (str5==str6) << endl; //输出什么? 

答:输出为:false、false、true。

以下反向遍历 array 数组的方法有什么错误

原文:https://zwmst.com/2626.html

vector<int> array; 
array.push_back(1); 
array.push_back(2); 
array.push_back(3); 
//反向遍历 array 数组: 
for(vector<int>::size_type i=array.size()-1; i>=0; --i){ 
 cout << array[i] << endl; 
} 

答:for 循环中的变量 i 的类型不应定义为 vector\(<\)int\(>\)::size_type, 因为该类型为无符号数值类型,故循环条件将恒成立,为死循环,应将其类型定义为有符号的 int 类型。

以下代码有什么问题

原文:https://zwmst.com/2628.html

cout << (true ? 1 : "1") << endl; 

答:运算符中两个可选值的类型不同。

以下代码有什么问题

原文:https://zwmst.com/2630.html

typedef vector<int> IntArray; 
IntArray array; 
array.push_back(1); 
array.push_back(2); 
array.push_back(2); 
array.push_back(3); 
//删除 array 数组中所有的 2 
for(IntArray::iterator itor=array.begin(); itor!=array.end(); 
 ++itor){ 
 if(2==*itor) { 
 array.erase(itor); 
 } 
} 

答:for 循环中的 if 语句后的 array.erase(itor)语句,它将迭代器 itor 所指向的元素删除后会自动下移一位,故应在其后加上语句:itor–;

以下代码中的两个 sizeof 用法有问题吗

原文:https://zwmst.com/2632.html

void upperCase(char str[]){ //将 str 中的小写字母转换成大写字母 
 for(int i=0; i<sizeof(str)/sizeof(str[0]); ++i){ 
 if('a'<=str[i] && str[i]<='z') 
 str[i] -= ('a'-'A'); 
 } 
} 
int main(){ 
 char str[] = "aBcDe"; 
 cout << "str 字符串长度为:" << sizeof(str)/sizeof(str[0]); 
 cout << endl; 
 upperCase(str); 
 cout << str << endl; 
 return 0; 
}

答:在 upperCase 方法中,for 循环的 sizeof(str)的值将总是 4,所以该方法只能将参数中的字符串的前四个字符转换成大写字母。

以下代码能够编译通过吗?为什么

原文:https://zwmst.com/2634.html

 unsigned int const size1 = 2; 
 char str1[size1]; 
 unsigned int temp = 0; 
 cin >> temp; 
 unsigned int const size2 = temp; 
 char str2[size2]; 

答:能;

以下代码有什么问题

原文:https://zwmst.com/2636.html

struct Test{ 
 Test(int){} 
 Test(){} 
 void fun(){} 
}; 
void main(void){ 
 Test a(1); 
 a.fun(); 
 Test b(); 
 b.fun(); 
} 

答:main 函数的返回类型应为 int;不能对 b 调用 fun()方法。

以下代码中的输出语句输出 0 吗?为什么

原文:https://zwmst.com/2638.html

 struct CLS{ 
 int m_i; 
 CLS(int i):m_i(i){ } 
 CLS(){ CLS(0);} 
 }; 
 int main(){ 
 CLS obj; 
 cout <

答:输出不是 0;

C++中的空类,默认产生哪些类成员函数

原文:https://zwmst.com/2640.html

答:空类中默认包含的成员函数如下:

 class Empty{ 
 public: 
 Empty(); //缺省构造函数 
 Empty( const Empty& ); //拷贝构造函数 
 ~Empty(); //析构函数 
 Empty& operator=( const Empty& ); //赋值运算符 
 Empty* operator&(); //取址运算符 
 const Empty* operator&() const; //取址运算符 const 
 }; 

统计一篇文章中单词个数

原文:https://zwmst.com/2642.html

答:代码如下:

 include <iostream>#include <fstream>using namespace std; 
 int main(){ 
 ifstream fin("t.txt"); 
 if(!fin){ 
 cout<>buf; 
 if(fin2.eof()) 
 break; 
 count++; 
 } 
 cout<</fstream></iostream>

写一个函数,完成内存之间的拷贝

原文:https://zwmst.com/2644.html

答:代码如下:

 void* mymemcpy(void* dest, const void* src, size_t count){ 
 char* pdest = static_cast<char>(dest); 
 const char* psrc = static_cast<const char="">(src); 
 if(pdest>psrc && pdest</const></char>

非 C++内建类型 A 和 B,在哪几种情况下 B 能隐式转化为 A

原文:https://zwmst.com/2646.html

答:

  • a)class B : public A{……}//B 公有继承自 A,可以是间接继承的
  • b)class B{operator A();}//B 实现了隐式转化为 A 的转化
  • c)class A{ A(const B&);}//A 实现了 non-explicit 的参数为 B 构造函数 (可以有其他带带默认值的参数)
  • d)A& operator= (const A&);//赋值操作,虽不是正宗的隐式类型转换, 但也可以勉强算一个

以下两条输出语句分别输出什么

原文:https://zwmst.com/2648.html

float a = 1.0f; 
cout << (int)a << endl; 
cout << (int&)a << endl; 
cout << boolalpha << ((int)a==(int&)a) << endl; //输出什么 
float b = 0.0f; 
cout << (int)b << endl; 
cout << (int&)b << endl; 
cout << boolalpha << ((int)b==(int&)b) << endl;//输出什么 

答:第一处输出 false,第二处输出 true。

程序的机器级表示

原文:https://zwmst.com/2653.html

汇编代码是计算机的一种低级表示,它是一种低级语言,可以从字面角度去理解它,包括处理数据、管理内存、读写存储设备上的数据,以及利用网络通信等。编译器生成机器码经过了一系列的转换,这些转换遵循编程语言、目标机器的指令集操作系统

指令集

指令集就是指挥计算机工作的指令,因为程序就是按照一定执行顺序排列的指令。因为计算机的执行控制权由 CPU 操作,所以指令集就是 CPU 中用来计算和控制计算机的一系列指令的集合。每个 CPU 在产出时都规定了与硬件电路相互配合工作的指令集。

指令集有不少分类,但是一般分为两种,一种是精简指令集,一种是复杂指令集。具体描述如下

精简指令集

精简指令的英文是 reduced instruction set computer, RISC,原意是精简指令集计算,简称为精简指令集,是 CPU 的一种 设计模式,可以把 CPU 想象成一家流水线工厂,对指令数目寻址方式都做了精简,使其实现更容易,指令并行执行程度更好,编译器的效率更高。

常见的精简指令集处理器包括 ARM、AVR、MIPS、PARISC、RISC-V 和 SPARC

所以你就能理解

这本书是讲啥的了。

它主要是基于 MIPS 体系结构把冯诺依曼体系的五大组件进行了逐一的硬件实现 + 软件设计介绍,更为重要的是引入了诸多并行计算的内容,这是大部分教材中忽略或者内容较少的,会根据这个思路把并行相关的内容,结合 OpenMP, CUDA 和 Hadoop/Spark 整体融入到新书中,毕竟这是未来发展的趋势

还有这本书

这本书又是讲啥的。

这本书是讲 RISC-V 指令集的,因为指令集的不同也区分了三个版本,三个版本???嗯,还有下面这个

这本书是讲 ARM 指令集的。

所以一般在看 CASPP 的时候并发的看看这本书是非常不错的选择。

精简指令集一般具有如下特征

  • 统一的指令编码
  • 通用的寄存器,一般会区分整数和浮点数
  • 简单的寻址模式,复杂寻址模式被简单指令序列来取代
  • 支持很少偏门的类型,例如 RISC 支持字节字符串类型。

复杂指令集

复杂指令集的英文是 Complex Instruction Set Computing, CISC,是一种微处理器指令集架构,也被译为复杂指令集。

复杂指令集包括 System/360、VAX、x86 等

复杂指令集可以说是在精简指令集之上作出的改变。

复杂指令集的特点是指令数目多而复杂,每条指令字长并不相等,计算机必须加以判读,并为此付出了性能的代价。

一般来说,提升 CPU 性能的方法有如下这几种

  • 增加寄存器的大小
  • 增进内部的并行性
  • 增加高速缓存的大小
  • 增加核心时脉的速度
  • 加入其他功能,例如 IO 和计时器
  • 加入向量处理器
  • 硬件多线程技术

比较抽象,我们后面会组织成文章具体介绍一下。

C 编译器会接收其他操作并把其转换为汇编语言输出,汇编语言是机器级别的代码表示。我们之前介绍过,C 语言程序的执行过程分为下面这几步

下面我们更多的讨论都是基于汇编代码来讨论。

我们日常所接触的高级语言,都是经过了层层封装的结果,所以我们平常是接触不到汇编语言的,更不会用汇编语言来进行编程,这就和你不知道操作系统的存在一样,但其实你每个操作,甚至你双击一个图标都和操作系统有关系。

高级语言的抽象级别很高,但是经过了层层抽象之后,高级语言的执行效率肯定没有汇编语言高,也没有汇编语言可靠。

但是高级语言有更大的优点是其编译后能够在不同的机器上运行,汇编语言针对不同的指令集有不同的表示。并且高级语言学习来更加通俗易懂,降低计算机门槛,让内卷更加严重(当然这是开个玩笑,冒犯到请别当真)。

话不多说,了解底层必须了解汇编语言。否则一个 synchronized 底层实现就能够让你头疼不已。而且,天天飘着也不好,迟早要落地。

了解汇编代码也有助于我们优化程序代码,分析代码中隐含的低效率,并且这种优化方法一旦优化成功,将是量级的提高,而不是改改 if…else ,使用一个新特性所能比的。

机器级代码

计算机系统使用了多种不同形式的抽象,可以通过一个简单的抽象模型来隐藏实现细节。对于机器级别的程序来说,有两点非常重要。

首先第一点,定义机器级别程序的格式和行为被称为 指令集体系结构或指令集架构(instruction set architecture), ISA。ISA 定义了进程状态、指令的格式和每一个指令对状态的影响。大部分的指令集架构包括 ISA 用来描述进程的行为就好像是顺序执行的,一条指令执行结束后,另外一条指令再开始。处理器硬件的描述要更复杂,它可以同时并行执行许多指令,但是它采用了安全措施来确保整体行为与 ISA 规定的顺序一致。

第二点,机器级别对内存地址的描述就是 虚拟地址(virtual address),它提供了一个内存模型来表示一个巨大的字节数组。

编译器在整个编译的过程中起到了至关重要的作用,把 C 语言转换为处理器执行的基本指令。汇编代码非常接近于机器代码,只不过与二进制机器代码相比,汇编代码的可读性更强,所以理解汇编是理解机器工作的第一步。

一些进程状态对机器可见,但是 C 语言程序员却看不到这些,包括

  • 程序计数器(Program counter),它存储下一条指令的地址,在 x86-64 架构中用 %rip 来表示。

程序执行时,PC 的初始值为程序第一条指令的地址,在顺序执行程序时, CPU 首先按程序计数器所指出的指令地址从内存中取出一条指令,然后分析和执行该指令,同时将 PC 的值加 1 并指向下一条要执行的指令。

比如下面一个例子。

这是一段数值进行相加的操作,程序启动,在经过编译解析后会由操作系统把硬盘中的程序复制到内存中,示例中的程序是将 123 和 456 执行相加操作,并将结果输出到显示器上。由于使用机器语言难以描述,所以这是经过翻译后的结果,实际上每个指令和数据都可能分布在不同的地址上,但为了方便说明,把组成一条指令的内存和数据放在了一个内存地址上。

  • 整数寄存器文件(register file)包含 16 个命名的位置,用来存储 64 位的值。这些寄存器可以存储地址和整型数据。有些寄存器用于跟踪程序状态,而另一些寄存器用于保存临时数据,例如过程的参数和局部变量,以及函数要返回的值。这个 文件 是和磁盘文件无关的,它只是 CPU 内部的一块高速存储单元。有专用的寄存器,也有通用的寄存器用来存储操作数。
  • 条件码寄存器 用来保存有关最近执行的算术或逻辑指令的状态信息。这些用于实现控件或数据流中的条件更改,例如实现 if 和 while 语句所需的条件更改。我们都学过高级语言,高级语言中的条件控制流程主要分为三种:顺序执行、条件分支、循环判断三种,顺序执行是按照地址的内容顺序的执行指令。条件分支是根据条件执行任意地址的指令。循环是重复执行同一地址的指令。
    • 顺序执行的情况比较简单,每执行一条指令程序计数器的值就是 + 1。
    • 条件和循环分支会使程序计数器的值指向任意的地址,这样一来,程序便可以返回到上一个地址来重复执行同一个指令,或者跳转到任意指令。

下面以条件分支为例来说明程序的执行过程(循环也很相似)

程序的开始过程和顺序流程是一样的,CPU 从 0100 处开始执行命令,在 0100 和 0101 都是顺序执行,PC 的值顺序+1,执行到 0102 地址的指令时,判断 0106 寄存器的数值大于 0,跳转(jump)到 0104 地址的指令,将数值输出到显示器中,然后结束程序,0103 的指令被跳过了,这就和我们程序中的 if() 判断是一样的,在不满足条件的情况下,指令会直接跳过。所以 PC 的执行过程也就没有直接+1,而是下一条指令的地址。

  • 一组 向量寄存器用来存储一个或者多个整数或者浮点数值,向量寄存器是对一维数据上进行操作。

机器指令只会执行非常简单的操作,例如将存放在寄存器的两个数进行相加,把数据从内存转移到寄存器中或者是条件分支转移到新的指令地址。编译器必须生成此类指令的序列,以实现程序构造,例如算术表达式求值,循环或过程调用和返回

认识汇编

我相信各位应该都知道汇编语言的出现背景吧,那就是二进制表示数据,太复杂太庞大了,为了解决这个问题,出现了汇编语言,汇编语言和机器指令的区别就在于表示方法上,汇编使用操作数来表示,机器指令使用二进制来表示,我之前多次提到机器码就是汇编,你也不能说我错,但是不准确。

但是汇编适合二进制代码存在转换关系的。

汇编代码需要经过 汇编器 编译后才产生二进制代码,这个二进制代码就是目标代码,然后由链接器将其连接起来运行。

汇编语言主要分为以下三类

  • 汇编指令:它是一种机器码的助记符,它有对应的机器码
  • 伪指令:没有对应的机器码,由编译器执行,计算机并不执行
  • 其他符号,比如 +、-、*、/ 等,由编译器识别,没有对应的机器码

汇编语言的核心是汇编指令,而我们对汇编的探讨也是基于汇编指令展开的。

与汇编有关的硬件和概念

CPU

CPU 是计算机的大脑,它也是整个计算机的核心,它也是执行汇编语言的硬件,CPU 的内部包含有寄存器,而寄存器是用于存储指令和数据的,汇编语言的本质也就是 CPU 内部操作数所执行的一系列计算。

内存

没有内存,计算机就像是一个没有记忆的人类,只会永无休止的重复性劳动。CPU 所需的指令和数据都由内存来提供,CPU 指令经由内存提供,经过一系列计算后再输出到内存。

磁盘

磁盘也是一种存储设备,它和内存的最大区别在于永久存储,程序需要在内存装载后才能运行,而提供给内存的程序都是由磁盘存储的。

总线

一般来说,内存内部会划分多个存储单元,存储单元用来存储指令和数据,就像是房子一样,存储单元就是房子的门牌号。而 CPU 与内存之间的交互是通过地址总线来进行的,总线从逻辑上分为三种

  • 地址线
  • 数据线
  • 控制线

CPU 与存储器之间的读写主要经过以下几步

读操作步骤

  • CPU 通过地址线发出需要读取指令的位置
  • CPU 通过控制线发出读指令
  • 内存把数据放在数据线上返回给 CPU

写操作步骤

  • CPU 通过地址线发出需要写出指令的位置
  • CPU 通过控制线发出写指令
  • CPU 把数据通过数据线写入内存

下面我们就来具体了解一下这三类总线

地址总线

通过我们上面的探讨,我们知道 CPU 通过地址总线来指定存储位置的,地址总线上能传送多少不同的信息,CPU 就可以对多少个存储单元进行寻址。

上图中 CPU 和内存中间信息交换通过了 10 条地址总线,每一条线能够传递的数据都是 0 或 1 ,所以上图一次 CPU 和内存传递的数据是 2 的十次方。

所以,如果 CPU 有 N 条地址总线,那么可以说这个地址总线的宽度是 N 。这样 CPU 可以寻找 2 的 N 次方个内存单元。

数据总线

CPU 与内存或其他部件之间的数据传送是由数据总线来完成的。数据总线的宽度决定了 CPU 和外界的数据传输速度。8 根数据总线可以一次传送一个 8 位二进制数据(即一个字节)。16 根数据总线一次可以传输两个字节,32 根数据总线可以一次传输四个字节。。。。。。

控制总线

CPU 与其他部件之间的控制是通过 控制总线 来完成的。有多少根控制总线,就意味着 CPU 提供了对外部器件的多少种控制。所以,控制总线的宽度决定了 CPU 对外部部件的控制能力。

一次内存的读取过程

**内存结构

内存 IC 是一个完整的结构,它内部也有电源、地址信号、数据信号、控制信号和用于寻址的 IC 引脚来进行数据的读写。下面是一个虚拟的 IC 引脚示意图

图中 VCC 和 GND 表示电源,A0 – A9 是地址信号的引脚,D0 – D7 表示的是控制信号、RD 和 WR 都是好控制信号,我用不同的颜色进行了区分,将电源连接到 VCC 和 GND 后,就可以对其他引脚传递 0 和 1 的信号,大多数情况下,+5V 表示1,0V 表示 0

我们都知道内存是用来存储数据,那么这个内存 IC 中能存储多少数据呢?D0 – D7 表示的是数据信号,也就是说,一次可以输入输出 8 bit = 1 byte 的数据。A0 – A9 是地址信号共十个,表示可以指定 00000 00000 – 11111 11111 共 2 的 10次方 = 1024个地址。每个地址都会存放 1 byte 的数据,因此我们可以得出内存 IC 的容量就是 1 KB。

如果我们使用的是 512 MB 的内存,这就相当于是 512000(512 * 1000) 个内存 IC。当然,一台计算机不太可能有这么多个内存 IC ,然而,通常情况下,一个内存 IC 会有更多的引脚,也就能存储更多数据。

**内存读取过程

下面是一次内存的读取过程。

来详细描述一下这个过程,假设我们要向内存 IC 中写入 1byte 的数据的话,它的过程是这样的:

  • 首先给 VCC 接通 +5V 的电源,给 GND 接通 0V 的电源,使用 A0 - A9 来指定数据的存储场所,然后再把数据的值输入给 D0 - D7 的数据信号,并把 WR(write)的值置为 1,执行完这些操作后,即可以向内存 IC 写入数据
  • 读出数据时,只需要通过 A0 – A9 的地址信号指定数据的存储场所,然后再将 RD 的值置为 1 即可。
  • 图中的 RD 和 WR 又被称为控制信号。其中当WR 和 RD 都为 0 时,无法进行写入和读取操作。

总结

此篇文章我们主要探讨了指令集、指令集的分类,与汇编有关的硬件,总线都有哪些,分别的作用都是什么,然后我们以一次内存读取过程来连接一下 CPU 和内存的交互过程。

寄存器

原文:https://zwmst.com/2656.html

下面我们就来介绍一下关于寄存器的相关内容。我们知道,寄存器是 CPU 内部的构造,它主要用于信息的存储。除此之外,CPU 内部还有运算器,负责处理数据;控制器控制其他组件;外部总线连接 CPU 和各种部件,进行数据传输;内部总线负责 CPU 内部各种组件的数据处理。

那么对于我们所了解的汇编语言来说,我们的主要关注点就是 寄存器

为什么会出现寄存器?因为我们知道,程序在内存中装载,由 CPU 来运行,CPU 的主要职责就是用来处理数据。那么这个过程势必涉及到从存储器中读取和写入数据,因为它涉及通过控制总线发送数据请求并进入存储器存储单元,通过同一通道获取数据,这个过程非常的繁琐并且会涉及到大量的内存占用,而且有一些常用的内存页存在,其实是没有必要的,因此出现了寄存器,存储在 CPU 内部。

认识寄存器

寄存器的官方叫法有很多,Wiki 上面的叫法是 Processing Register, 也可以称为 CPU Register,计算机中经常有一个东西多种叫法的情况,反正你知道都说的是寄存器就可以了。

认识寄存器之前,我们首先先来看一下 CPU 内部的构造。

CPU 从逻辑上可以分为 3 个模块,分别是控制单元、运算单元和存储单元,这三部分由 CPU 内部总线连接起来。

几乎所有的冯·诺伊曼型计算机的 CPU,其工作都可以分为5个阶段:取指令、指令译码、执行指令、访存取数、结果写回

  • 取指令阶段是将内存中的指令读取到 CPU 中寄存器的过程,程序寄存器用于存储下一条指令所在的地址
  • 指令译码阶段,在取指令完成后,立马进入指令译码阶段,在指令译码阶段,指令译码器按照预定的指令格式,对取回的指令进行拆分和解释,识别区分出不同的指令类别以及各种获取操作数的方法。
  • 执行指令阶段,译码完成后,就需要执行这一条指令了,此阶段的任务是完成指令所规定的各种操作,具体实现指令的功能。
  • 访问取数阶段,根据指令的需要,有可能需要从内存中提取数据,此阶段的任务是:根据指令地址码,得到操作数在主存中的地址,并从主存中读取该操作数用于运算。
  • 结果写回阶段,作为最后一个阶段,结果写回(Write Back,WB)阶段把执行指令阶段的运行结果数据写回到 CPU 的内部寄存器中,以便被后续的指令快速地存取;

计算机架构中的寄存器

寄存器是一块速度非常快的计算机内存,下面是现代计算机中具有存储功能的部件比对,可以看到,寄存器的速度是最快的,同时也是造价最高昂的。

我们以 intel 8086 处理器为例来进行探讨,8086 处理器是 x86 架构的前身。在 8086 后面又衍生出来了 8088 。

在 8086 CPU 中,地址总线达到 20 根,因此最大寻址能力是 2^20 次幂也就是 1MB 的寻址能力,8088 也是如此。

在 8086 架构中,所有的内部寄存器、内部以及外部总线都是 16 位宽,可以存储两个字节,因为是完全的 16 位微处理器。8086 处理器有 14 个寄存器,每个寄存器都有一个特有的名称,即

**AX,BX,CX,DX,SP,BP,SI,DI,IP,FLAG,CS,DS,SS,ES

这 14 个寄存器有可能进行具体的划分,按照功能可以分为三种

  • 通用寄存器
  • 控制寄存器
  • 段寄存器

下面我们分别介绍一下这几种寄存器

通用寄存器

通用寄存器主要有四种 ,即 AX、BX、CX、DX 同样的,这四个寄存器也是 16 位的,能存放两个字节。 AX、BX、CX、DX 这四个寄存器一般用来存放数据,也被称为 数据寄存器。它们的结构如下

8086 CPU 的上一代寄存器是 8080 ,它是一类 8 位的 CPU,为了保证兼容性,8086 在 8080 上做了很小的修改,8086 中的通用寄存器 AX、BX、CX、DX 都可以独立使用两个 8 位寄存器来使用。

在细节方面,AX、BX、CX、DX 可以再向下进行划分

  • AX(Accumulator Register) : 累加寄存器,它主要用于输入/输出和大规模的指令运算。
  • BX(Base Register):基址寄存器,用来存储基础访问地址
  • CX(Count Register):计数寄存器,CX 寄存器在迭代的操作中会循环计数
  • DX(data Register):数据寄存器,它也用于输入/输出操作。它还与 AX 寄存器以及 DX 一起使用,用于涉及大数值的乘法和除法运算。

这四种寄存器可以分为上半部分和下半部分,用作八个 8 位数据寄存器

  • **AX 寄存器可以分为两个独立的 8 位的 AH 和 AL 寄存器;
  • **BX 寄存器可以分为两个独立的 8 位的 BH 和 BL 寄存器;
  • **CX 寄存器可以分为两个独立的 8 位的 CH 和 CL 寄存器;
  • **DX 寄存器可以分为两个独立的 8 位的 DH 和 DL 寄存器;

除了上面 AX、BX、CX、DX 寄存器以外,其他寄存器均不可以分为两个独立的 8 位寄存器

如下图所示。

合起来就是

AX 的低位(0 – 7)位构成了 AL 寄存器,高 8 位(8 – 15)位构成了 AH 寄存器。AH 和 AL 寄存器是可以使用的 8 位寄存器,其他同理。

在认识了寄存器之后,我们通过一个示例来看一下数据的具体存储方式。

比如数据 19 ,它在 16 位存储器中所存储的表示如下

寄存器的存储方式是先存储低位,如果低位满足不了就存储高位,如果低位能够满足,高位用 0 补全,在其他低位能满足的情况下,其余位也用 0 补全。

8086 CPU 可以一次存储两种类型的数据

  • 字节(byte): 一个字节由 8 bit 组成,这是一种恒定不变的存储方式
  • 字(word):字是由指令集或处理器硬件作为单元处理的固定大小的数据,对于 intel 来说,一个字长就是两个字节,字是计算机一个非常重要的特征,针对不同的指令集架构来说,计算机一次处理的数据也是不同的。也就是说,针对不同指令集的机器,一次能处理不用的字长,有字、双字(32位)、四字(64位)等。

AX 寄存器

我们上面探讨过,AX 的另外一个名字叫做累加寄存器或者简称为累加器,其可以分为 2 个独立的 8 位寄存器 AH 和 AL;在编写汇编程序中,AX 寄存器可以说是使用频率最高的寄存器。

下面是几段汇编代码

mov ax,20       /* 将 20 送入寄存器 AX*/
mov ah,80   /* 将 80 送入寄存器 AH*/
add ax,10     /* 将寄存器 AX 中的数值加上 8 */

这里注意下:上面代码中出现的是 ax、ah ,而注释中确是 AX、AH ,其实含义是一样的,不区分大小写。

AX 相比于其他通用寄存器来说,有一点比较特殊,AX 具有一种特殊功能的使用,那就是使用 DIV 和 MUL 指令式使用。

DIV 是 8086 CPU 中的除法指令。

MUL 是 8086 CPU 中的乘法指令。

BX 寄存器

BX 被称为数据寄存器,即表明其能够暂存一般数据。同样为了适应以前的 8 位 CPU ,而可以将 BX 当做两个独立的 8 位寄存器使用,即有 BH 和 BL。BX 除了具有暂存数据的功能外,还用于 寻址,即寻找物理内存地址。BX 寄存器中存放的数据一般是用来作为偏移地址 使用的,因为偏移地址当然是在基址地址上的偏移了。偏移地址是在段寄存器中存储的,关于段寄存器的介绍,我们后面再说。

CX 寄存器

CX 也是数据寄存器,能够暂存一般性数据。同样为了适应以前的 8 位 CPU ,而可以将 CX 当做两个独立的 8 位寄存器使用,即有 CH 和 CL。除此之外,CX 也是有其专门的用途的,CX 中的 C 被翻译为 Counting 也就是计数器的功能。当在汇编指令中使用循环 LOOP 指令时,可以通过 CX 来指定需要循环的次数,每次执行循环 LOOP 时候,CPU 会做两件事

  • 一件事是计数器自动减 1

  • 还有一件就是判断 CX 中的值,如果 CX 中的值为 0 则会跳出循环,而继续执行循环下面的指令,

    当然如果 CX 中的值不为 0 ,则会继续执行循环中所指定的指令 。

DX 寄存器

DX 也是数据寄存器,能够暂存一般性数据。同样为了适应以前的 8 位 CPU ,DX 的用途其实在前面介绍 AX 寄存器时便已经有所介绍了,那就是支持 MUL 和 DIV 指令。同时也支持数值溢出等。

段寄存器

CPU 包含四个段寄存器,用作程序指令,数据或栈的基础位置。实际上,对 IBM PC 上所有内存的引用都包含一个段寄存器作为基本位置。

段寄存器主要包含

  • CS(Code Segment) : 代码寄存器,程序代码的基础位置
  • DS(Data Segment): 数据寄存器,变量的基本位置
  • SS(Stack Segment): 栈寄存器,栈的基础位置
  • ES(Extra Segment): 其他寄存器,内存中变量的其他基本位置。

索引寄存器

索引寄存器主要包含段地址的偏移量,索引寄存器主要分为

  • BP(Base Pointer):基础指针,它是栈寄存器上的偏移量,用来定位栈上变量
  • SP(Stack Pointer): 栈指针,它是栈寄存器上的偏移量,用来定位栈顶
  • SI(Source Index): 变址寄存器,用来拷贝源字符串
  • DI(Destination Index): 目标变址寄存器,用来复制到目标字符串

状态和控制寄存器

就剩下两种寄存器还没聊了,这两种寄存器是指令指针寄存器和标志寄存器:

  • IP(Instruction Pointer): 指令指针寄存器,它是从 Code Segment 代码寄存器处的偏移来存储执行的下一条指令
  • FLAG : Flag 寄存器用于存储当前进程的状态,这些状态有
    • 位置 (Direction):用于数据块的传输方向,是向上传输还是向下传输
    • 中断标志位 (Interrupt) :1 – 允许;0 – 禁止
    • 陷入位 (Trap) :确定每条指令执行完成后,CPU 是否应该停止。1 – 开启,0 – 关闭
    • 进位 (Carry) : 设置最后一个无符号算术运算是否带有进位
    • 溢出 (Overflow) : 设置最后一个有符号运算是否溢出
    • 符号 (Sign) : 如果最后一次算术运算为负,则设置 1 =负,0 =正
    • 零位 (Zero) : 如果最后一次算术运算结果为零,1 = 零
    • 辅助进位 (Aux Carry) :用于第三位到第四位的进位
    • 奇偶校验 (Parity) : 用于奇偶校验

物理地址

我们大家都知道, CPU 访问内存时,需要知道访问内存的具体地址,内存单元是内存的基本单位,每一个内存单元在内存中都有唯一的地址,这个地址即是 物理地址。而 CPU 和内存之间的交互有三条总线,即数据总线、控制总线和地址总线。

CPU 通过地址总线将物理地址送入存储器,那么 CPU 是如何形成的物理地址呢?这将是我们接下来的讨论重点。

现在,我们先来讨论一下和 8086 CPU 有关的结构问题。

cxuan 和你聊了这么久,你应该知道 8086 CPU 是 16 位的 CPU 了,那么,什么是 16 位的 CPU 呢?

你可能大致听过这个回答,16 位 CPU 指的是 CPU 一次能处理的数据是 16 位的,能回答这个问题代表你的底层还不错,但是不够全面,其实,16 位的 CPU 指的是

  • CPU 内部的运算器一次最多能处理 16 位的数据

运算器其实就是 ALU,运算控制单元,它是 CPU 内部的三大核心器件之一,主要负责数据的运算。

  • 寄存器的最大宽度为 16 位

这个寄存器的最大宽度值得就是通用寄存器能处理的二进制数的最大位数

  • 寄存器和运算器之间的通路为 16 位

这个指的是寄存器和运算器之间的总线,一次能传输 16 位的数据

好了,现在你应该知道为什么叫做 16 位 CPU 了吧。

在你知道上面这个问题的答案之后,我们下面就来聊一聊如何计算物理地址。

8086 CPU 有 20 位地址总线,每一条总线都可以传输一位的地址,所以 8086 CPU 可以传送 20 位地址,也就是说,8086 CPU 可以达到 2^20 次幂的寻址能力,也就是 1MB。8086 CPU 又是 16 位的结构,从 8086 CPU 的结构看,它只能传输 16 位的地址,也就是 2^16 次幂也就是 64 KB,那么它如何达到 1MB 的寻址能力呢?

原来,8086 CPU 的内部采用两个 16 位地址合成的方式来传输一个 20 位的物理地址,如下图所示

叙述一下上图描述的过程

CPU 中相关组件提供两个地址:段地址和偏移地址,这两个地址都是 16 位的,他们经由地址加法器变为 20 位的物理地址,这个地址即是输入输出控制电路传递给内存的物理地址,由此完成物理地址的转换。

地址加法器采用 物理地址 = 段地址* 16 + 偏移地址 的方法用段地址和偏移地址合成物理地址。

下面是地址加法器的工作流程

其实段地址 16 ,就是左移 4 位。在上面的叙述中,物理地址 = 段地址 16 + 偏移地址,其实就是基础地址 + 偏移地址 = 物理地址 寻址模式的一种具体实现方案。基础地址其实就等于段地址 * 16。

你可能不太清楚 的概念,下面我们就来探讨一下。

什么是段

段这个概念经常出现在操作系统中,比如在内存管理中,操作系统会把不同的数据分成 来存储,比如 代码段、数据段、bss 段、rodata 段 等。

但是这些的划分并不是内存干的,cxuan 告诉你是谁干的,这其实是幕后 Boss CPU 搞的,内存当作了声讨的对象。

其实,内存没有进行分段,分段完全是由 CPU 搞的,上面聊过的通过基础地址 + 偏移地址 = 物理地址的方式给出内存单元的物理地址,使得我们可以分段管理 CPU。

如图所示

这是两个 16 KB 的程序分别被装载进内存的示意图,可以看到,这两个程序的段地址的大小都是 16380。

这里需要注意一点, 8086 CPU 段地址的计算方式是段地址 * 16,所以,16 位的寻址能力是 2^16 次方,所以一个段的长度是 64 KB。

段寄存器

cxuan 在上面只是简单为你介绍了一下段寄存器的概念,介绍的有些浅,而且介绍段寄存器不介绍段也有不知庐山真面目的感觉,现在为你详细的介绍一下,相信看完上面的段的概念之后,段寄存器也是手到擒来。

我们在合成物理地址的那张图提到了 相关部件 的概念,这个相关部件其实就是段寄存器,即 CS、DS、SS、ES 。8086 的 CPU 在访问内存时,由这四个寄存器提供内存单元的段地址。

CS 寄存器

要聊 CS 寄存器,那么 IP 寄存器是你绕不过去的曾经。CS 和 IP 都是 8086 CPU 非常重要的寄存器,它们指出了 CPU 当前需要读取指令的地址。

CS 的全称是 Code Segment,即代码寄存器;而 IP 的全称是 Instruction Pointer ,即指令指针。现在知道这两个为什么一起出现了吧!

在 8086 CPU 中,由 CS:IP 指向的内容当作指令执行。如下图所示

说明一下上图

在 CPU 内部,由 CS、IP 提供段地址,由加法器负责转换为物理地址,输入输出控制电路负责输入/输出数据,指令缓冲器负责缓冲指令,指令执行器负责执行指令。在内存中有一段连续存储的区域,区域内部存储的是机器码、外面是地址和汇编指令。

上面这幅图的段地址和偏移地址分别是 2000 和 0000,当这两个地址进入地址加法器后,会由地址加法器负责将这两个地址转换为物理地址

然后地址加法器负责将指令输送到输入输出控制电路中

输入输出控制电路将 20 位的地址总线送到内存中。

然后取出对应的数据,也就是 B8、23、01,图中的 B8、BB 都是操作数。

控制输入/输出电路会将 B8 23 01 送入指令缓存器中。

此时这个指令就已经具备执行条件,此时 IP 也就是指令指针会自动增加。我们上面说到 IP 其实就是从 Code Segment 也就是 CS 处偏移的地址,也就是偏移地址。它会知道下一个需要读取指令的地址,如下图所示

在这之后,指令执行执行取出的 B8 23 01 这条指令。

然后下面再把 2000 和 0003 送到地址加法器中再进行后续指令的读取。后面的指令读取过程和我们上面探讨的如出一辙,这里 cxuan 就不再赘述啦。

通过对上面的描述,我们能总结一下 8086 CPU 的工作过程

  • 段寄存器提供段地址和偏移地址给地址加法器
  • 由地址加法器计算出物理地址通过输入输出控制电路将物理地址送到内存中
  • 提取物理地址对应的指令,经由控制电路取回并送到指令缓存器中
  • IP 继续指向下一条指令的地址,同时指令执行器执行指令缓冲器中的指令

什么是 Code Segment

Code Segment 即代码段,它就是我们上面聊到就是 CS 寄存器中存储的基础地址,也就是段地址,段地址其本质上就是一组内存单元的地址,例如上面的 mov ax,0123H 、mov bx, 0003H。我们可以将长度为 N 的一组代码,存放在一组连续地址、其实地址为 16 的倍数的内存单元中,我们可以认为,这段内存就是用来存放代码的。

DS 寄存器

CPU 在读写一个内存单元的时候,需要知道这个内存单元的地址。在 8086 CPU 中,有一个 DS 寄存器,通常用来存放访问数据的段地址。如果你想要读取一个 10000H 的数据,你可能会需要下面这段代码

mov bx,10000H
mov ds,bx
mov a1,[0]

上面这三条指令就把 10000H 读取到了 a1 中。

在上面汇编代码中,mov 指令有两种传送方式

  • 一种是把数据直接送入寄存器
  • 一种是将一个寄存器的内容送入另一个寄存器

但是不仅仅如此,mov 指令还具有下面这几种表达方式

描述 举例
mov 寄存器,数据 比如:mov ax,8
mov 寄存器,寄存器 比如:mov ax,bx
mov 寄存器,内存单元 比如:mov ax,[0]
mov 内存单元,寄存器 比如:mov[0], ax
mov 段寄存器,寄存器 比如:mov ds,ax

栈我相信大部分小伙伴已经非常熟悉了,是一种具有特殊的访问方式的存储空间。它的特殊性就在于,先进入栈的元素,最后才出去,也就是我们常说的 先入后出

它就像一个大的收纳箱,你可以往里面放相同类型的东西,比如书,最先放进收纳箱的书在最下面,最后放进收纳箱的书在最上面,如果你想拿书的话, 必须从最上面开始取,否则是无法取出最下面的书籍的。

栈的数据结构就是这样,你把书籍压入收纳箱的操作叫做压入(push),你把书籍从收纳箱取出的操作叫做弹出(pop),它的模型图大概是这样

入栈相当于是增加操作,出栈相当于是删除操作,只不过叫法不一样。栈和内存不同,它不需要指定元素的地址。它的大概使用如下

// 压入数据
Push(123);
Push(456);
Push(789);

// 弹出数据
j = Pop();
k = Pop();
l = Pop();

在栈中,LIFO 方式表示栈的数组中所保存的最后面的数据(Last In)会被最先读取出来(First Out)。

栈和 SS 寄存器

下面我们就通过一段汇编代码来描述一下栈的压入弹出的过程

8086 CPU 提供入栈和出栈指令,最基本的两个是 PUSH(入栈)POP(出栈)。比如 push ax 会把 ax 寄存器中的数据压入栈中,pop ax 表示从栈顶取出数据送入 ax 寄存器中。

这里注意一点:8086 CPU 中的入栈和出栈都是以字为单位进行的。

我这里首先有一个初始的栈,没有任何指令和数据。

然后我们向栈中 push 数据后,栈中数据如下

涉及的指令有

mov ax,2345H
push ax

注意,数据会用两个单元存放,高地址单元存放高 8 位地址,低地址单元存放低 8 位。

再向栈中 push 数据

其中涉及的指令有

mov bx,0132H
push bx

现在栈中有两条数据,现在我们执行出栈操作

其中涉及的指令有

pop ax
/* ax = 0132H */

再继续取出数据

涉及的指令有

pop bx
/* bx = */

完整的 push 和 pop 过程如下

现在 cxuan 问你一个问题,我们上面描述的是 10000H ~ 1000FH 这段空间来作为 push 和 pop 指令的存取单元。但是,你怎么知道这个栈单元就是 10000H ~ 1000FH 呢?也就是说,你如何选择指定的栈单元进行存取?

事实上,8086 CPU 有一组关于栈的寄存器 SSSP。SS 是段寄存器,它存储的是栈的基础位置,也就是栈顶的位置,而 SP 是栈指针,它存储的是偏移地址。在任意时刻,SS:SP 都指向栈顶元素。push 和 pop 指令执行时,CPU 从 SS 和 SP 中得到栈顶的地址。

现在,我们可以完整的描述一下 push 和 pop 过程了,下面 cxuan 就给你推导一下这个过程。

上面这个过程主要涉及到的关键变化如下。

当使用 PUSH 指令向栈中压入 1 个字节单元时,SP = SP – 1;即栈顶元素会发生变化;

而当使用 PUSH 指令向栈中压入 2 个字节的字单元时,SP = SP – 2 ;即栈顶元素也要发生变化;

当使用 POP 指令从栈中弹出 1 个字节单元时, SP = SP + 1;即栈顶元素会发生变化;

当使用 POP 指令从栈中弹出 2 个字节单元的字单元时, SP = SP + 2 ;即栈顶元素会发生变化;

栈顶越界问题

现在我们知道,8086 CPU 可以使用 SS 和 SP 指示栈顶的地址,并且提供 PUSH 和 POP 指令实现入栈和出栈,所以,你现在知道了如何能够找到栈顶位置,但是你如何能保证栈顶的位置不会越界呢?栈顶越界会产生什么影响呢?

比如如下是一个栈顶越界的示意图

第一开始,SS:SP 寄存器指向了栈顶,然后向栈空间 push 一定数量的元素后,SS:SP 位于栈空间顶部,此时再向栈空间内部 push 元素,就会出现栈顶越界问题。

栈顶越界是危险的,因为我们既然将一块区域空间安排为栈,那么在栈空间外部也可能存放了其他指令和数据,这些指令和数据有可能是其他程序的,所以如此操作会让计算机懵逼

我们希望 8086 CPU 能自己解决问题,毕竟 8086 CPU 已经是个成熟的 CPU 了,要学会自己解决问题了。

然鹅(故意的),这对于 8086 CPU 来说,这可能是它一辈子的 夙愿 了,真实情况是,8086 CPU 不会保证栈顶越界问题,也就是说 8086 CPU 只会告诉你栈顶在哪,并不会知道栈空间有多大,所以需要程序员自己手动去保证。。。

程序员需要了解的硬核知识之CPU

原文:https://zwmst.com/2658.html

大家都是程序员,大家都是和计算机打交道的程序员,大家都是和计算机中软件硬件打交道的程序员,大家都是和CPU打交道的程序员,所以,不管你是玩儿硬件的还是做软件的,你的世界都少不了计算机最核心的 – CPU

CPU是什么

CPU 的全称是 Central Processing Unit,它是你的电脑中最硬核的组件,这种说法一点不为过。CPU 是能够让你的计算机叫计算机的核心组件,但是它却不能代表你的电脑,CPU 与计算机的关系就相当于大脑和人的关系。它是一种小型的计算机芯片,它嵌入在台式机、笔记本电脑或者平板电脑的主板上。通过在单个计算机芯片上放置数十亿个微型晶体管来构建 CPU。 这些晶体管使它能够执行运行存储在系统内存中的程序所需的计算,也就是说 CPU 决定了你电脑的计算能力。

CPU 实际做什么

CPU 的核心是从程序或应用程序获取指令并执行计算。此过程可以分为三个关键阶段:提取,解码和执行。CPU从系统的 RAM 中提取指令,然后解码该指令的实际内容,然后再由 CPU 的相关部分执行该指令。

RAM : 随机存取存储器(英语:Random Access Memory,缩写:RAM),也叫主存,是与 CPU 直接交换数据的内部存储器。它可以随时读写(刷新时除外),而且速度很快,通常作为操作系统或其他正在运行中的程序的**临时数据存储介质

CPU 的内部结构

说了这么多 CPU 的重要性,那么 CPU 的内部结构是什么呢?又是由什么组成的呢?下图展示了一般程序的运行流程(以 C 语言为例),可以说了解程序的运行流程是掌握程序运行机制的基础和前提。

在这个流程中,CPU 负责的就是解释和运行最终转换成机器语言的内容。

CPU 主要由两部分构成:控制单元算术逻辑单元(ALU)

  • 控制单元:从内存中提取指令并解码执行
  • 算数逻辑单元(ALU):处理算数和逻辑运算

CPU 是计算机的心脏和大脑,它和内存都是由许多晶体管组成的电子部件。它接收数据输入,执行指令并处理信息。它与输入/输出(I / O)设备进行通信,这些设备向 CPU 发送数据和从 CPU 接收数据。

从功能来看,CPU 的内部由寄存器、控制器、运算器和时钟四部分组成,各部分之间通过电信号连通。

  • 寄存器是中央处理器内的组成部分。它们可以用来暂存指令、数据和地址。可以将其看作是内存的一种。根据种类的不同,一个 CPU 内部会有 20 – 100个寄存器。
  • 控制器负责把内存上的指令、数据读入寄存器,并根据指令的结果控制计算机
  • 运算器负责运算从内存中读入寄存器的数据
  • 时钟 负责发出 CPU 开始计时的时钟信号

接下来简单解释一下内存,为什么说 CPU 需要讲一下内存呢,因为内存是与 CPU 进行沟通的桥梁。计算机所有程序的运行都是在内存中运行的,内存又被称为主存,其作用是存放 CPU 中的运算数据,以及与硬盘等外部存储设备交换的数据。只要计算机在运行中,CPU 就会把需要运算的数据调到主存中进行运算,当运算完成后CPU再将结果传送出来,主存的运行也决定了计算机的稳定运行。

主存通过控制芯片与 CPU 进行相连,由可读写的元素构成,每个字节(1 byte = 8 bits)都带有一个地址编号,注意是一个字节,而不是一个位。CPU 通过地址从主存中读取数据和指令,也可以根据地址写入数据。注意一点:当计算机关机时,内存中的指令和数据也会被清除。

CPU 是寄存器的集合体

在 CPU 的四个结构中,我们程序员只需要了解寄存器就可以了,其余三个不用过多关注,为什么这么说?因为程序是把寄存器作为对象来描述的。

说到寄存器,就不得不说到汇编语言,我大学是学信息管理与信息系统的,我就没有学过汇编这门课(就算有这门课也不会好好学hhhh),出来混总是要还的,要想作为一个硬核程序员,不能不了解这些概念。说到汇编语言,就不得不说到高级语言,说到高级语言就不得不牵扯出语言这个概念。

计算机语言

我们生而为人最明显的一个特征是我们能通过讲话来实现彼此的交流,但是计算机听不懂你说的话,你要想和他交流必须按照计算机指令来交换,这就涉及到语言的问题,计算机是由二进制构成的,它只能听的懂二进制也就是机器语言,但是普通人是无法看懂机器语言的,这个时候就需要一种电脑既能识别,人又能理解的语言,最先出现的就是汇编语言。但是汇编语言晦涩难懂,所以又出现了像是 C,C++,Java 的这种高级语言。

所以计算机语言一般分为两种:低级语言(机器语言,汇编语言)和高级语言。使用高级语言编写的程序,经过编译转换成机器语言后才能运行,而汇编语言经过汇编器才能转换为机器语言。

汇编语言

首先来看一段用汇编语言表示的代码清单

mov eax, dword ptr [ebp-8]   /* 把数值从内存复制到 eax */
add eax, dword ptr [ebp-0Ch] /* 把 eax 的数值和内存的数值相加 */
mov dword ptr [ebp-4], eax /* 把 eax 的数值(上一步的结果)存储在内存中*/

这是采用汇编语言(assembly)编写程序的一部分。汇编语言采用 助记符(memonic) 来编写程序,每一个原本是电信号的机器语言指令会有一个与其对应的助记符,例如 mov,add 分别是数据的存储(move)和相加(addition)的简写。汇编语言和机器语言是一一对应的。这一点和高级语言有很大的不同,通常我们将汇编语言编写的程序转换为机器语言的过程称为 汇编;反之,机器语言转化为汇编语言的过程称为 反汇编

汇编语言能够帮助你理解计算机做了什么工作,机器语言级别的程序是通过寄存器来处理的,上面代码中的 eax,ebp 都是表示的寄存器,是 CPU 内部寄存器的名称,所以可以说 CPU 是一系列寄存器的集合体。在内存中的存储通过地址编号来表示,而寄存器的种类则通过名字来区分。

不同类型的 CPU ,其内部寄存器的种类,数量以及寄存器存储的数值范围都是不同的。不过,根据功能的不同,可以将寄存器划分为下面这几类

种类 功能
累加寄存器 存储运行的数据和运算后的数据。
标志寄存器 用于反应处理器的状态和运算结果的某些特征以及控制指令的执行。
程序计数器 程序计数器是用于存放下一条指令所在单元的地址的地方。
基址寄存器 存储数据内存的起始位置
变址寄存器 存储基址寄存器的相对地址
通用寄存器 存储任意数据
指令寄存器 储存正在被运行的指令,CPU内部使用,程序员无法对该寄存器进行读写
栈寄存器 存储栈区域的起始位置

其中程序计数器、累加寄存器、标志寄存器、指令寄存器和栈寄存器都只有一个,其他寄存器一般有多个

程序计数器

程序计数器(Program Counter)是用来存储下一条指令所在单元的地址。

程序执行时,PC的初值为程序第一条指令的地址,在顺序执行程序时,控制器首先按程序计数器所指出的指令地址从内存中取出一条指令,然后分析和执行该指令,同时将PC的值加1指向下一条要执行的指令。

我们还是以一个事例为准来详细的看一下程序计数器的执行过程

这是一段进行相加的操作,程序启动,在经过编译解析后会由操作系统把硬盘中的程序复制到内存中,示例中的程序是将 123 和 456 执行相加操作,并将结果输出到显示器上。由于使用机器语言难以描述,所以这是经过翻译后的结果,实际上每个指令和数据都可能分布在不同的地址上,但为了方便说明,把组成一条指令的内存和数据放在了一个内存地址上。

地址 0100 是程序运行的起始位置。Windows 等操作系统把程序从硬盘复制到内存后,会将程序计数器作为设定为起始位置 0100,然后执行程序,每执行一条指令后,程序计数器的数值会增加1(或者直接指向下一条指令的地址),然后,CPU 就会根据程序计数器的数值,从内存中读取命令并执行,也就是说,程序计数器控制着程序的流程

条件分支和循环机制

我们都学过高级语言,高级语言中的条件控制流程主要分为三种:顺序执行、条件分支、循环判断三种,顺序执行是按照地址的内容顺序的执行指令。条件分支是根据条件执行任意地址的指令。循环是重复执行同一地址的指令。

  • 顺序执行的情况比较简单,每执行一条指令程序计数器的值就是 + 1。
  • 条件和循环分支会使程序计数器的值指向任意的地址,这样一来,程序便可以返回到上一个地址来重复执行同一个指令,或者跳转到任意指令。

下面以条件分支为例来说明程序的执行过程(循环也很相似)

程序的开始过程和顺序流程是一样的,CPU 从0100处开始执行命令,在0100和0101都是顺序执行,PC 的值顺序+1,执行到0102地址的指令时,判断0106寄存器的数值大于0,跳转(jump)到0104地址的指令,将数值输出到显示器中,然后结束程序,0103 的指令被跳过了,这就和我们程序中的 if() 判断是一样的,在不满足条件的情况下,指令会直接跳过。所以 PC 的执行过程也就没有直接+1,而是下一条指令的地址。

标志寄存器

条件和循环分支会使用到 jump(跳转指令),会根据当前的指令来判断是否跳转,上面我们提到了标志寄存器,无论当前累加寄存器的运算结果是正数、负数还是零,标志寄存器都会将其保存(也负责溢出和奇偶校验)

溢出(overflow):是指运算的结果超过了寄存器的长度范围

奇偶校验(parity check):是指检查运算结果的值是偶数还是奇数

CPU 在进行运算时,标志寄存器的数值会根据当前运算的结果自动设定,运算结果的正、负和零三种状态由标志寄存器的三个位表示。标志寄存器的第一个字节位、第二个字节位、第三个字节位各自的结果都为1时,分别代表着正数、零和负数。

CPU 的执行机制比较有意思,假设累加寄存器中存储的 XXX 和通用寄存器中存储的 YYY 做比较,执行比较的背后,CPU 的运算机制就会做减法运算。而无论减法运算的结果是正数、零还是负数,都会保存到标志寄存器中。结果为正表示 XXX 比 YYY 大,结果为零表示 XXX 和 YYY 相等,结果为负表示 XXX 比 YYY 小。程序比较的指令,实际上是在 CPU 内部做减法运算。

函数调用机制

接下来,我们继续介绍函数调用机制,哪怕是高级语言编写的程序,函数调用处理也是通过把程序计数器的值设定成函数的存储地址来实现的。函数执行跳转指令后,必须进行返回处理,单纯的指令跳转没有意义,下面是一个实现函数跳转的例子

图中将变量 a 和 b 分别赋值为 123 和 456 ,调用 MyFun(a,b) 方法,进行指令跳转。图中的地址是将 C 语言编译成机器语言后运行时的地址,由于1行 C 程序在编译后通常会变为多行机器语言,所以图中的地址是分散的。在执行完 MyFun(a,b)指令后,程序会返回到 MyFun(a,b) 的下一条指令,CPU 继续执行下面的指令。

函数的调用和返回很重要的两个指令是 callreturn 指令,再将函数的入口地址设定到程序计数器之前,call 指令会把调用函数后要执行的指令地址存储在名为栈的主存内。函数处理完毕后,再通过函数的出口来执行 return 指令。return 指令的功能是把保存在栈中的地址设定到程序计数器。MyFun 函数在被调用之前,0154 地址保存在栈中,MyFun 函数处理完成后,会把0154的地址保存在程序计数器中。这个调用过程如下

在一些高级语言的条件或者循环语句中,函数调用的处理会转换成 call 指令,函数结束后的处理则会转换成 return 指令。

通过地址和索引实现数组

接下来我们看一下基址寄存器和变址寄存器,通过这两个寄存器,我们可以对主存上的特定区域进行划分,来实现类似数组的操作,首先,我们用十六进制数将计算机内存上的 00000000 – FFFFFFFF 的地址划分出来。那么,凡是该范围的内存地址,只要有一个 32 位的寄存器,便可查看全部地址。但如果想要想数组那样分割特定的内存区域以达到连续查看的目的的话,使用两个寄存器会更加方便。

例如,我们用两个寄存器(基址寄存器和变址寄存器)来表示内存的值

这种表示方式很类似数组的构造,数组是指同样长度的数据在内存中进行连续排列的数据构造。用数组名表示数组全部的值,通过索引来区分数组的各个数据元素,例如: a[0] – a[4],[]内的 0 – 4 就是数组的下标。

CPU 指令执行过程

那么 CPU 是如何执行一条条的指令的呢?

几乎所有的冯·诺伊曼型计算机的CPU,其工作都可以分为5个阶段:取指令、指令译码、执行指令、访存取数、结果写回

  • 取指令阶段是将内存中的指令读取到 CPU 中寄存器的过程,程序寄存器用于存储下一条指令所在的地址
  • 指令译码阶段,在取指令完成后,立马进入指令译码阶段,在指令译码阶段,指令译码器按照预定的指令格式,对取回的指令进行拆分和解释,识别区分出不同的指令类别以及各种获取操作数的方法。
  • 执行指令阶段,译码完成后,就需要执行这一条指令了,此阶段的任务是完成指令所规定的各种操作,具体实现指令的功能。
  • 访问取数阶段,根据指令的需要,有可能需要从内存中提取数据,此阶段的任务是:根据指令地址码,得到操作数在主存中的地址,并从主存中读取该操作数用于运算。
  • 结果写回阶段,作为最后一个阶段,结果写回(Write Back,WB)阶段把执行指令阶段的运行结果数据“写回”到某种存储形式:结果数据经常被写到CPU的内部寄存器中,以便被后续的指令快速地存取;

总结

本篇文章我们主要讲述了

  • CPU 是什么,CPU 的重要性,CPU 执行程序的过程
  • 还讲述了 CPU 的内部结构,它的组成部分
  • 提到了汇编语言和高级语言
  • 提到了CPU 与 寄存器的关系
  • 提到了主要的寄存器的功能,程序计数器,标志寄存器,基址寄存器和变址寄存器
  • 还提到了函数调用机制是怎样的。
  • CPU 指令的执行过程

程序员需要了解的硬核知识之汇编语言

原文:https://zwmst.com/2660.html

之前的系列文章从 CPU 和内存方面简单介绍了一下汇编语言,但是还没有系统的了解一下汇编语言,汇编语言作为第二代计算机语言,会用一些容易理解和记忆的字母,单词来代替一个特定的指令,作为高级编程语言的基础,有必要系统的了解一下汇编语言,那么本篇文章希望大家跟我一起来了解一下汇编语言。

汇编语言和本地代码

我们在之前的文章中探讨过,计算机 CPU 只能运行本地代码(机器语言)程序,用 C 语言等高级语言编写的代码,需要经过编译器编译后,转换为本地代码才能够被 CPU 解释执行。

但是本地代码的可读性非常差,所以需要使用一种能够直接读懂的语言来替换本地代码,那就是在各本地代码中,附带上表示其功能的英文缩写,比如在加法运算的本地代码加上add(addition) 的缩写、在比较运算符的本地代码中加上cmp(compare)的缩写等,这些通过缩写来表示具体本地代码指令的标志称为 助记符,使用助记符的语言称为汇编语言。这样,通过阅读汇编语言,也能够了解本地代码的含义了。

不过,即使是使用汇编语言编写的源代码,最终也必须要转换为本地代码才能够运行,负责做这项工作的程序称为编译器,转换的这个过程称为汇编。在将源代码转换为本地代码这个功能方面,汇编器和编译器是同样的。

用汇编语言编写的源代码和本地代码是一一对应的。因而,本地代码也可以反过来转换成汇编语言编写的代码。把本地代码转换为汇编代码的这一过程称为反汇编,执行反汇编的程序称为反汇编程序

哪怕是 C 语言编写的源代码,编译后也会转换成特定 CPU 用的本地代码。而将其反汇编的话,就可以得到汇编语言的源代码,并对其内容进行调查。不过,本地代码变成 C 语言源代码的反编译,要比本地代码转换成汇编代码的反汇编要困难,这是因为,C 语言代码和本地代码不是一一对应的关系。

通过编译器输出汇编语言的源代码

我们上面提到本地代码可以经过反汇编转换成为汇编代码,但是只有这一种转换方式吗?显然不是,C 语言编写的源代码也能够通过编译器编译称为汇编代码,下面就来尝试一下。

首先需要先做一些准备,需要先下载 Borland C++ 5.5 编译器,为了方便,我这边直接下载好了读者直接从我的百度网盘提取即可 (链接:https://pan.baidu.com/s/19LqVICpn5GcV88thD2AnlA 密码:hz1u)

下载完毕,需要进行配置,下面是配置说明 (https://wenku.baidu.com/view/22e2f418650e52ea551898ad.html),教程很完整跟着配置就可以,下面开始我们的编译过程

首先用 Windows 记事本等文本编辑器编写如下代码

// 返回两个参数值之和的函数
int AddNum(int a,int b){
  return a + b;
}

// 调用 AddNum 函数的函数
void MyFunc(){
  int c;
  c = AddNum(123,456);
}

编写完成后将其文件名保存为 Sample4.c ,C 语言源文件的扩展名,通常用.c 来表示,上面程序是提供两个输入参数并返回它们之和。

在 Windows 操作系统下打开 命令提示符,切换到保存 Sample4.c 的文件夹下,然后在命令提示符中输入

bcc32 -c -S Sample4.c

bcc32 是启动 Borland C++ 的命令,-c 的选项是指仅进行编译而不进行链接,-S 选项被用来指定生成汇编语言的源代码

作为编译的结果,当前目录下会生成一个名为Sample4.asm 的汇编语言源代码。汇编语言源文件的扩展名,通常用.asm 来表示,下面就让我们用编辑器打开看一下 Sample4.asm 中的内容

 .386p
    ifdef ??version
    if    ??version GT 500H
    .mmx
    endif
    endif
    model flat
    ifndef  ??version
    ?debug  macro
    endm
    endif
    ?debug  S "Sample4.c"
    ?debug  T "Sample4.c"
_TEXT   segment dword public use32 'CODE'
_TEXT   ends
_DATA   segment dword public use32 'DATA'
_DATA   ends
_BSS    segment dword public use32 'BSS'
_BSS    ends
DGROUP  group   _BSS,_DATA
_TEXT   segment dword public use32 'CODE'
_AddNum proc    near
?live1@0:
   ;    
   ;    int AddNum(int a,int b){
   ;    
    push      ebp
    mov       ebp,esp
   ;    
   ;    
   ;        return a + b;
   ;    
@1:
    mov       eax,dword ptr [ebp+8]
    add       eax,dword ptr [ebp+12]
   ;    
   ;    }
   ;    
@3:
@2:
    pop       ebp
    ret 
_AddNum endp
_MyFunc proc    near
?live1@48:
   ;    
   ;    void MyFunc(){
   ;    
    push      ebp
    mov       ebp,esp
   ;    
   ;        int c;
   ;        c = AddNum(123,456);
   ;    
@4:
    push      456
    push      123
    call      _AddNum
    add       esp,8
   ;    
   ;    }
   ;    
@5:
    pop       ebp
    ret 
_MyFunc endp
_TEXT   ends
    public  _AddNum
    public  _MyFunc
    ?debug  D "Sample4.c" 20343 45835
    end

这样,编译器就成功的把 C 语言转换成为了汇编代码了。

不会转换成本地代码的伪指令

第一次看到汇编代码的读者可能感觉起来比较难,不过实际上其实比较简单,而且可能比 C 语言还要简单,为了便于阅读汇编代码的源代码,需要注意几个要点

汇编语言的源代码,是由转换成本地代码的指令(后面讲述的操作码)和针对汇编器的伪指令构成的。伪指令负责把程序的构造以及汇编的方法指示给汇编器(转换程序)。不过伪指令是无法汇编转换成为本地代码的。下面是上面程序截取的伪指令

_TEXT   segment dword public use32 'CODE'
_TEXT   ends
_DATA   segment dword public use32 'DATA'
_DATA   ends
_BSS    segment dword public use32 'BSS'
_BSS    ends
DGROUP  group   _BSS,_DATA

_AddNum proc    near
_AddNum endp

_MyFunc proc    near
_MyFunc endp

_TEXT   ends
    end

由伪指令 segmentends 围起来的部分,是给构成程序的命令和数据的集合体上加一个名字而得到的,称为段定义。段定义的英文表达具有区域的意思,在这个程序中,段定义指的是命令和数据等程序的集合体的意思,一个程序由多个段定义构成。

上面代码的开始位置,定义了3个名称分别为 _TEXT、_DATA、_BSS 的段定义,_TEXT 是指定的段定义,_DATA 是被初始化(有初始值)的数据的段定义,_BSS 是尚未初始化的数据的段定义。这种定义的名称是由 Borland C++ 定义的,是由 Borland C++ 编译器自动分配的,所以程序段定义的顺序就成为了 _TEXT、_DATA、_BSS ,这样也确保了内存的连续性

_TEXT   segment dword public use32 'CODE'
_TEXT   ends
_DATA   segment dword public use32 'DATA'
_DATA   ends
_BSS    segment dword public use32 'BSS'
_BSS    ends

段定义( segment ) 是用来区分或者划分范围区域的意思。汇编语言的 segment 伪指令表示段定义的起始,ends 伪指令表示段定义的结束。段定义是一段连续的内存空间

group 这个伪指令表示的是将 _BSS和_DATA 这两个段定义汇总名为 DGROUP 的组

DGROUP  group   _BSS,_DATA

围起 _AddNum_MyFun_TEXT segment 和 _TEXT ends ,表示_AddNum_MyFun 是属于 _TEXT 这一段定义的。

_TEXT   segment dword public use32 'CODE'
_TEXT   ends

因此,即使在源代码中指令和数据是混杂编写的,经过编译和汇编后,也会转换成为规整的本地代码。

_AddNum proc_AddNum endp 围起来的部分,以及_MyFunc proc_MyFunc endp 围起来的部分,分别表示 AddNum 函数和 MyFunc 函数的范围。

_AddNum proc    near
_AddNum endp

_MyFunc proc    near
_MyFunc endp

编译后在函数名前附带上下划线_ ,是 Borland C++ 的规定。在 C 语言中编写的 AddNum 函数,在内部是以 _AddNum 这个名称处理的。伪指令 proc 和 endp 围起来的部分,表示的是 过程(procedure) 的范围。在汇编语言中,这种相当于 C 语言的函数的形式称为过程。

末尾的 end 伪指令,表示的是源代码的结束。

汇编语言的语法是 操作码 + 操作数

在汇编语言中,一行表示一对 CPU 的一个指令。汇编语言指令的语法结构是 操作码 + 操作数,也存在只有操作码没有操作数的指令。

操作码表示的是指令动作,操作数表示的是指令对象。操作码和操作数一起使用就是一个英文指令。比如从英语语法来分析的话,操作码是动词,操作数是宾语。比如这个句子 Give me money这个英文指令的话,Give 就是操作码,me 和 money 就是操作数。汇编语言中存在多个操作数的情况,要用逗号把它们分割,就像是 Give me,money 这样。

能够使用何种形式的操作码,是由 CPU 的种类决定的,下面对操作码的功能进行了整理。

本地代码需要加载到内存后才能运行,内存中存储着构成本地代码的指令和数据。程序运行时,CPU会从内存中把数据和指令读出来,然后放在 CPU 内部的寄存器中进行处理。

如果 CPU 和内存的关系你还不是很了解的话,请阅读作者的另一篇文章 程序员需要了解的硬核知识之CPU 详细了解。

寄存器是 CPU 中的存储区域,寄存器除了具有临时存储和计算的功能之外,还具有运算功能,x86 系列的主要种类和角色如下图所示

指令解析

下面就对 CPU 中的指令进行分析

**最常用的 mov 指令

指令中最常使用的是对寄存器和内存进行数据存储的 mov 指令,mov 指令的两个操作数,分别用来指定数据的存储地和读出源。操作数中可以指定寄存器、常数、标签(附加在地址前),以及用方括号([]) 围起来的这些内容。如果指定了没有用([]) 方括号围起来的内容,就表示对该值进行处理;如果指定了用方括号围起来的内容,方括号的值则会被解释为内存地址,然后就会对该内存地址对应的值进行读写操作。让我们对上面的代码片段进行说明

 mov       ebp,esp
    mov       eax,dword ptr [ebp+8]

mov ebp,esp 中,esp 寄存器中的值被直接存储在了 ebp 中,也就是说,如果 esp 寄存器的值是100的话那么 ebp 寄存器的值也是 100。

而在 mov eax,dword ptr [ebp+8] 这条指令中,ebp 寄存器的值 + 8 后会被解析称为内存地址。如果 ebp

寄存器的值是100的话,那么 eax 寄存器的值就是 100 + 8 的地址的值。dword ptr 也叫做 double word pointer 简单解释一下就是从指定的内存地址中读出4字节的数据

**对栈进行 push 和 pop

程序运行时,会在内存上申请分配一个称为栈的数据空间。栈(stack)的特性是后入先出,数据在存储时是从内存的下层(大的地址编号)逐渐往上层(小的地址编号)累积,读出时则是按照从上往下进行读取的。

栈是存储临时数据的区域,它的特点是通过 push 指令和 pop 指令进行数据的存储和读出。向栈中存储数据称为 入栈 ,从栈中读出数据称为 出栈,32位 x86 系列的 CPU 中,进行1次 push 或者 pop,即可处理 32 位(4字节)的数据。

函数的调用机制

下面我们一起来分析一下函数的调用机制,我们以上面的 C 语言编写的代码为例。首先,让我们从MyFunc 函数调用AddNum 函数的汇编语言部分开始,来对函数的调用机制进行说明。栈在函数的调用中发挥了巨大的作用,下面是经过处理后的 MyFunc 函数的汇编处理内容

_MyFunc      proc    near
    push            ebp       ; 将 ebp 寄存器的值存入栈中                                      (1) 
    mov             ebp,esp ; 将 esp 寄存器的值存入 ebp 寄存器中                            (2)
    push            456         ; 将 456 入栈                                  (3)
    push            123         ; 将 123 入栈                              (4)
    call            _AddNum ; 调用 AddNum 函数                              (5)
    add             esp,8       ; esp 寄存器的值 + 8                     (6)
    pop             ebp         ; 读出栈中的数值存入 ebp 寄存器中            (7)
    ret                             ; 结束 MyFunc 函数,返回到调用源           (8)
_MyFunc         endp

代码解释中的(1)、(2)、(7)、(8)的处理适用于 C 语言中的所有函数,我们会在后面展示 AddNum 函数处理内容时进行说明。这里希望大家先关注(3) – (6) 这一部分,这对了解函数调用机制至关重要。

(3) 和 (4) 表示的是将传递给 AddNum 函数的参数通过 push 入栈。在 C 语言源代码中,虽然记述为函数 AddNum(123,456),但入栈时则会先按照 456,123 这样的顺序。也就是位于后面的数值先入栈。这是 C 语言的规定。(5) 表示的 call 指令,会把程序流程跳转到 AddNum 函数指令的地址处。在汇编语言中,函数名表示的就是函数所在的内存地址。AddNum 函数处理完毕后,程序流程必须要返回到编号(6) 这一行。call 指令运行后,call 指令的下一行(也就指的是 (6) 这一行)的内存地址(调用函数完毕后要返回的内存地址)会自动的 push 入栈。该值会在 AddNum 函数处理的最后通过 ret 指令 pop 出栈,然后程序会返回到 (6) 这一行。

(6) 部分会把栈中存储的两个参数 (456 和 123) 进行销毁处理。虽然通过两次的 pop 指令也可以实现,不过采用 esp 寄存器 + 8 的方式会更有效率(处理 1 次即可)。对栈进行数值的输入和输出时,数值的单位是4字节。因此,通过在负责栈地址管理的 esp 寄存器中加上4的2倍8,就可以达到和运行两次 pop 命令同样的效果。虽然内存中的数据实际上还残留着,但只要把 esp 寄存器的值更新为数据存储地址前面的数据位置,该数据也就相当于销毁了。

我在编译 Sample4.c 文件时,出现了下图的这条消息

图中的意思是指 c 的值在 MyFunc 定义了但是一直未被使用,这其实是一项编译器优化的功能,由于存储着 AddNum 函数返回值的变量 c 在后面没有被用到,因此编译器就认为 该变量没有意义,进而也就没有生成与之对应的汇编语言代码

下图是调用 AddNum 这一函数前后栈内存的变化

函数的内部处理

上面我们用汇编代码分析了一下 Sample4.c 整个过程的代码,现在我们着重分析一下 AddNum 函数的源代码部分,分析一下参数的接收、返回值和返回等机制

_AddNum         proc        near
    push            ebp                                                            (1)
    mov             ebp,esp                                                             (2)
    mov             eax,dword ptr[ebp+8]                                     (3)
    add             eax,dword ptr[ebp+12]                                   (4)
    pop             ebp                                   (5)
    ret                                                                                       (6)
_AddNum         endp

ebp 寄存器的值在(1)中入栈,在(5)中出栈,这主要是为了把函数中用到的 ebp 寄存器的内容,恢复到函数调用前的状态。

(2) 中把负责管理栈地址的 esp 寄存器的值赋值到了 ebp 寄存器中。这是因为,在 mov 指令中方括号内的参数,是不允许指定 esp 寄存器的。因此,这里就采用了不直接通过 esp,而是用 ebp 寄存器来读写栈内容的方法。

(3) 使用[ebp + 8] 指定栈中存储的第1个参数123,并将其读出到 eax 寄存器中。像这样,不使用 pop 指令,也可以参照栈的内容。而之所以从多个寄存器中选择了 eax 寄存器,是因为 eax 是负责运算的累加寄存器。

通过(4) 的 add 指令,把当前 eax 寄存器的值同第2个参数相加后的结果存储在 eax 寄存器中。[ebp + 12] 是用来指定第2个参数456的。在 C 语言中,函数的返回值必须通过 eax 寄存器返回,这也是规定。也就是 函数的参数是通过栈来传递,返回值是通过寄存器返回的

(6) 中 ret 指令运行后,函数返回目的地内存地址会自动出栈,据此,程序流程就会跳转返回到(6) (Call _AddNum) 的下一行。这时,AddNum 函数入口和出口处栈的状态变化,就如下图所示

全局变量和局部变量

在熟悉了汇编语言后,接下来我们来了解一下全局变量和局部变量,在函数外部定义的变量称为全局变量,在函数内部定义的变量称为局部变量,全局变量可以在任意函数中使用,局部变量只能在函数定义局部变量的内部使用。下面,我们就通过汇编语言来看一下全局变量和局部变量的不同之处。

下面定义的 C 语言代码分别定义了局部变量和全局变量,并且给各变量进行了赋值,我们先看一下源代码部分

// 定义被初始化的全局变量
int a1 = 1;
int a2 = 2;
int a3 = 3;
int a4 = 4;
int a5 = 5;

// 定义没有初始化的全局变量
int b1,b2,b3,b4,b5;

// 定义函数
void MyFunc(){
  // 定义局部变量
  int c1,c2,c3,c4,c5,c6,c7,c8,c9,c10;

  // 给局部变量赋值
  c1 = 1;
  c2 = 2;
  c3 = 3;
  c4 = 4;
  c5 = 5;
  c6 = 6;
  c7 = 7;
  c8 = 8;
  c9 = 9;
  c10 = 10;

  // 把局部变量赋值给全局变量
  a1 = c1;
  a2 = c2;
  a3 = c3;
  a4 = c4;
  a5 = c5;
  b1 = c6;
  b2 = c7;
  b3 = c8;
  b4 = c9;
  b5 = c10;
}

上面的代码挺暴力的,不过没关系,能够便于我们分析其汇编源码就好,我们用 Borland C++ 编译后的汇编代码如下,编译完成后的源码比较长,这里我们只拿出来一部分作为分析使用(我们改变了一下段定义顺序,删除了部分注释)

_DATA segment dword public use32 'DATA'
   align 4
  _a1 label dword
            dd 1
   align 4
  _a2 label dword
            dd 2
   align 4
  _a3 label dword
            dd 3
   align 4
  _a4 label dword
            dd 4
   align 4
  _a5 label dword
            dd 5
_DATA ends

_BSS segment dword public use32 'BSS'
 align 4
  _b1 label dword
            db 4 dup(?)
   align 4
  _b2 label dword
            db 4 dup(?)
   align 4
  _b3 label dword
            db 4 dup(?)
   align 4
  _b4 label dword
            db 4 dup(?)
   align 4
  _b5 label dword
            db 4 dup(?)
_BSS ends

_TEXT segment dword public use32 'CODE'
_MyFunc proc near

 push      ebp
 mov       ebp,esp
 add       esp,-20
 push      ebx
 push      esi
 mov       eax,1
 mov       edx,2
 mov       ecx,3
 mov       ebx,4
 mov       esi,5
 mov       dword ptr [ebp-4],6
 mov       dword ptr [ebp-8],7
 mov       dword ptr [ebp-12],8
 mov       dword ptr [ebp-16],9
 mov       dword ptr [ebp-20],10
 mov       dword ptr [_a1],eax
 mov       dword ptr [_a2],edx
 mov       dword ptr [_a3],ecx
 mov       dword ptr [_a4],ebx
 mov       dword ptr [_a5],esi
 mov       eax,dword ptr [ebp-4]
 mov       dword ptr [_b1],eax
 mov       edx,dword ptr [ebp-8]
 mov       dword ptr [_b2],edx
 mov       ecx,dword ptr [ebp-12]
 mov       dword ptr [_b3],ecx
 mov       eax,dword ptr [ebp-16]
 mov       dword ptr [_b4],eax
 mov       edx,dword ptr [ebp-20]
 mov       dword ptr [_b5],edx
 pop       esi
 pop       ebx
 mov       esp,ebp
 pop       ebp
 ret

_MyFunc   endp
_TEXT   ends

编译后的程序,会被归类到名为段定义的组。

  • 初始化的全局变量,会汇总到名为 _DATA 的段定义中
_DATA segment dword public use32 'DATA'
...
_DATA ends
  • 没有初始化的全局变量,会汇总到名为 _BSS 的段定义中
_BSS segment dword public use32 'BSS'
 ...
_BSS ends
  • 被段定义 _TEXT 围起来的汇编代码则是 Borland C++ 的定义
_TEXT segment dword public use32 'CODE'
_MyFunc proc near
...
_MyFunc   endp
_TEXT   ends

我们在分析上面汇编代码之前,先来认识一下更多的汇编指令,此表是对上面部分操作码及其功能的接续

操作码 操作数 功能
add A,B 把A和B的值相加,并把结果赋值给A
call A 调用函数A
cmp A,B 对A和B进行比较,比较结果会自动存入标志寄存器中
inc A 对A的值 + 1
ige 标签名 和 cmp 命令组合使用。跳转到标签行
jl 标签名 和 cmp 命令组合使用。跳转到标签行
jle 标签名 和 cmp 命令组合使用。跳转到标签行
jmp 标签名 和 cmp 命令组合使用。跳转到标签行
mov A,B 把 B 的值赋给 A
pop A 从栈中读取数值并存入A
push A 把A的值存入栈中
ret 将处理返回到调用源
xor A,B A和B的位进行亦或比较,并将结果存入A中

我们首先来看一下 _DATA 段定义的内容。 _a1 label dword 定义了 _a1 这个标签。标签表示的是相对于段定义起始位置的位置。由于_a1_DATA 段定义的开头位置,所以相对位置是0。 _a1 就相当于是全局变量a1。编译后的函数名和变量名前面会加一个(_),这也是 Borland C++ 的规定。dd 1 指的是,申请分配了4字节的内存空间,存储着1这个初始值。 dd指的是 define double word 表示有两个长度为2的字节领域(word),也就是4字节的意思。

Borland C++ 中,由于int 类型的长度是4字节,因此汇编器就把 int a1 = 1 变换成了 _a1 label dword 和 dd 1。同样,这里也定义了相当于全局变量的 a2 – a5 的标签 _a2 - _a5,它们各自的初始值 2 – 5 也被存储在各自的4字节中。

接下来,我们来说一说 _BSS 段定义的内容。这里定义了相当于全局变量 b1 – b5 的标签 _b1 - _b5。其中的db 4dup(?) 表示的是申请分配了4字节的领域,但值尚未确定(这里用 ? 来表示)的意思。db(define byte) 表示有1个长度是1字节的内存空间。因而,db 4 dup(?) 的情况下,就是4字节的内存空间。

注意:db 4 dup(?) 不要和 dd 4 混淆了,前者表示的是4个长度是1字节的内存空间。而 db 4 表示的则是双字节( = 4 字节) 的内存空间中存储的值是 4

临时确保局部变量使用的内存空间

我们知道,局部变量是临时保存在寄存器和栈中的。函数内部利用栈进行局部变量的存储,函数调用完成后,局部变量值被销毁,但是寄存器可能用于其他目的。所以,局部变量只是函数在处理期间临时存储在寄存器和栈中的

回想一下上述代码是不是定义了10个局部变量?这是为了表示存储局部变量的不仅仅是栈,还有寄存器。为了确保 c1 – c10 所需的域,寄存器空闲的时候就会使用寄存器,寄存器空间不足的时候就会使用栈。

让我们继续来分析上面代码的内容。_TEXT段定义表示的是 MyFunc 函数的范围。在 MyFunc 函数中定义的局部变量所需要的内存领域。会被尽可能的分配在寄存器中。大家可能认为使用高性能的寄存器来替代普通的内存是一种资源浪费,但是编译器不这么认为,只要寄存器有空间,编译器就会使用它。由于寄存器的访问速度远高于内存,所以直接访问寄存器能够高效的处理。局部变量使用寄存器,是 Borland C++ 编译器最优化的运行结果。

代码清单中的如下内容表示的是向寄存器中分配局部变量的部分

mov       eax,1
mov       edx,2
mov       ecx,3
mov       ebx,4
mov       esi,5

仅仅对局部变量进行定义是不够的,只有在给局部变量赋值时,才会被分配到寄存器的内存区域。上述代码相当于就是给5个局部变量 c1 – c5 分别赋值为 1 – 5。eax、edx、ecx、ebx、esi 是 x86 系列32位 CPU 寄存器的名称。至于使用哪个寄存器,是由编译器来决定的 。

x86 系列 CPU 拥有的寄存器中,程序可以操作的是十几,其中空闲的最多会有几个。因而,局部变量超过寄存器数量的时候,可分配的寄存器就不够用了,这种情况下,编译器就会把栈派上用场,用来存储剩余的局部变量。

在上述代码这一部分,给局部变量c1 – c5 分配完寄存器后,可用的寄存器数量就不足了。于是,剩下的5个局部变量c6 – c10 就被分配给了栈的内存空间。如下面代码所示

mov       dword ptr [ebp-4],6
mov       dword ptr [ebp-8],7
mov       dword ptr [ebp-12],8
mov       dword ptr [ebp-16],9
mov       dword ptr [ebp-20],10

函数入口 add esp,-20 指的是,对栈数据存储位置的 esp 寄存器(栈指针)的值做减20的处理。为了确保内存变量 c6 – c10 在栈中,就需要保留5个 int 类型的局部变量(4字节 * 5 = 20 字节)所需的空间。 mov ebp,esp这行指令表示的意思是将 esp 寄存器的值赋值到 ebp 寄存器。之所以需要这么处理,是为了通过在函数出口处 mov esp ebp 这一处理,把 esp 寄存器的值还原到原始状态,从而对申请分配的栈空间进行释放,这时栈中用到的局部变量就消失了。这也是栈的清理处理。在使用寄存器的情况下,局部变量则会在寄存器被用于其他用途时自动消失,如下图所示。

 mov       dword ptr [ebp-4],6
 mov       dword ptr [ebp-8],7
 mov       dword ptr [ebp-12],8
 mov       dword ptr [ebp-16],9
 mov       dword ptr [ebp-20],10

这五行代码是往栈空间代入数值的部分,由于在向栈申请内存空间前,借助了 mov ebp, esp 这个处理,esp 寄存器的值被保存到了 ebp 寄存器中,因此,通过使用[ebp – 4]、[ebp – 8]、[ebp – 12]、[ebp – 16]、[ebp – 20] 这样的形式,就可以申请分配20字节的栈内存空间切分成5个长度为4字节的空间来使用。例如, mov dword ptr [ebp-4],6 表示的就是,从申请分配的内存空间的下端(ebp寄存器指示的位置)开始向前4字节的地址([ebp – 4]) 中,存储着6这一4字节数据。

循环控制语句的处理

上面说的都是顺序流程,那么现在就让我们分析一下循环流程的处理,看一下 for 循环以及 if 条件分支等 c 语言程序的 流程控制是如何实现的,我们还是以代码以及编译后的结果为例,看一下程序控制流程的处理过程。

// 定义MySub 函数
void MySub(){
  // 不做任何处理

}

// 定义MyFunc 函数
void Myfunc(){
  int i;
  for(int i = 0;i < 10;i++){
    // 重复调用MySub十次
    MySub();
  }
}

上述代码将局部变量 i 作为循环条件,循环调用十次MySub 函数,下面是它主要的汇编代码

 xor         ebx, ebx    ; 将寄存器清0
@4  call        _MySub      ; 调用MySub函数
        inc         ebx             ; ebx寄存器的值 + 1
        cmp         ebx,10      ;   将ebx寄存器的值和10进行比较
        jl          short @4    ; 如果小于10就跳转到 @4

C 语言中的 for 语句是通过在括号中指定循环计数器的初始值(i = 0)、循环的继续条件(i < 10)、循环计数器的更新(i++) 这三种形式来进行循环处理的。与此相对的汇编代码就是通过比较指令(cmp)跳转指令(jl)来实现的。

下面我们来对上述代码进行说明

MyFunc 函数中用到的局部变量只有 i ,变量 i 申请分配了 ebx 寄存器的内存空间。for 语句括号中的 i = 0 被转换为 xor ebx,ebx 这一处理,xor 指令会对左起第一个操作数和右起第二个操作数进行 XOR 运算,然后把结果存储在第一个操作数中。由于这里把第一个操作数和第二个操作数都指定为了 ebx,因此就变成了对相同数值的 XOR 运算。也就是说不管当前寄存器的值是什么,最终的结果都是0。类似的,我们使用 mov ebx,0 也能得到相同的结果,但是 xor 指令的处理速度更快,而且编译器也会启动最优化功能。

XOR 指的就是异或操作,它的运算规则是 如果a、b两个值不相同,则异或结果为1。如果a、b两个值相同,异或结果为0

相同数值进行 XOR 运算,运算结果为0。XOR 的运算规则是,值不同时结果为1,值相同时结果为0。例如 01010101 和 01010101 进行运算,就会分别对各个数字位进行 XOR 运算。因为每个数字位都相同,所以运算结果为0。

ebx 寄存器的值初始化后,会通过 call 指定调用 _MySub 函数,从 _MySub 函数返回后,会执行inc ebx 指令,对 ebx 的值进行 + 1 操作,这个操作就相当于 i++ 的意思,++ 表示的就是当前数值 + 1。

这里需要知道 i++ 和 ++i 的区别

i++ 是先赋值,复制完成后再对 i执行 + 1 操作

++i 是先进行 +1 操作,完成后再进行赋值

inc 下一行的 cmp 是用来对第一个操作数和第二个操作数的数值进行比较的指令。 cmp ebx,10 就相当于 C 语言中的 i < 10 这一处理,意思是把 ebx 寄存器的值与10进行比较。汇编语言中比较指令的结果,会存储在 CPU 的标志寄存器中。不过,标志寄存器的值,程序是无法直接参考的。那如何判断比较结果呢?

汇编语言中有多个跳转指令,这些跳转指令会根据标志寄存器的值来判断是否进行跳转操作,例如最后一行的 jl,它会根据 cmp ebx,10 指令所存储在标志寄存器中的值来判断是否跳转,jl 这条指令表示的就是 jump on less than(小于的话就跳转)。发现如果 i 比 10 小,就会跳转到 @4 所在的指令处继续执行。

那么汇编代码的意思也可以用 C 语言来改写一下,加深理解

 i ^= i;
L4: MySub();
        i++;
        if(i < 10) goto L4;

代码第一行 i ^= i 指的就是 i 和 i 进行异或运算,也就是 XOR 运算,MySub() 函数用 L4 标签来替代,然后进行 i 自增操作,如果i 的值小于 10 的话,就会一直循环 MySub() 函数。

条件分支的处理方法

条件分支的处理方式和循环的处理方式很相似,使用的也是 cmp 指令和跳转指令。下面是用 C 语言编写的条件分支的代码

// 定义MySub1 函数
void MySub1(){

 // 不做任何处理
}

// 定义MySub2 函数
void MySub2(){

 // 不做任何处理
}

// 定义MySub3 函数
void MySub3(){

 // 不做任何处理
}

// 定义MyFunc 函数
void MyFunc(){

 int a = 123;
 // 根据条件调用不同的函数
 if(a > 100){
  MySub1();
 }
 else if(a < 50){
  MySub2();
 }
 else
 {
  MySub3();
 }

}

很简单的一个实现了条件判断的 C 语言代码,那么我们把它用 Borland C++ 编译之后的结果如下

_MyFunc proc near
 push      ebp              
 mov       ebp,esp
 mov       eax,123          ; 把123存入 eax 寄存器中
 cmp       eax,100          ; 把 eax 寄存器的值同100进行比较
 jle       short @8         ; 比100小时,跳转到@8标签
 call      _MySub1          ; 调用MySub1函数
 jmp             short @11      ; 跳转到@11标签
@8:
 cmp       eax,50               ; 把 eax 寄存器的值同50进行比较
 jge       short @10        ; 比50大时,跳转到@10标签
 call      _MySub2          ; 调用MySub2函数
 jmp             short @11      ; 跳转到@11标签
@10:
 call      _MySub3          ; 调用MySub3函数
@11:
 pop       ebp
 ret 
_MyFunc endp

上面代码用到了三种跳转指令,分别是jle(jump on less or equal) 比较结果小时跳转,jge(jump on greater or equal) 比较结果大时跳转,还有不管结果怎样都会进行跳转的jmp,在这些跳转指令之前还有用来比较的指令 cmp,构成了上述汇编代码的主要逻辑形式。

了解程序运行逻辑的必要性

通过对上述汇编代码和 C 语言源代码进行比较,想必大家对程序的运行方式有了新的理解,而且,从汇编源代码中获取的知识,也有助于了解 Java 等高级语言的特性,比如 Java 中就有 native 关键字修饰的变量,那么这个变量的底层就是使用 C 语言编写的,还有一些 Java 中的语法糖只有通过汇编代码才能知道其运行逻辑。在某些情况下,对于查找 bug 的原因也是有帮助的。

上面我们了解到的编程方式都是串行处理的,那么串行处理有什么特点呢?

串行处理最大的一个特点就是专心只做一件事情,一件事情做完之后才会去做另外一件事情。

计算机是支持多线程的,多线程的核心就是 CPU切换,如下图所示

我们还是举个实际的例子,让我们来看一段代码

// 定义全局变量
int counter = 100;

// 定义MyFunc1()
void MyFunc(){
  counter *= 2;
}

// 定义MyFunc2()
void MyFunc2(){
  counter *= 2;
}

上述代码是更新 counter 的值的 C 语言程序,MyFunc1() 和 MyFunc2() 的处理内容都是把 counter 的值扩大至原来的二倍,然后再把 counter 的值赋值给 counter 。这里,我们假设使用多线程处理,同时调用了一次MyFunc1 和 MyFunc2 函数,这时,全局变量 counter 的值,理应编程 100 2 2 = 400。如果你开启了多个线程的话,你会发现 counter 的数值有时也是 200,对于为什么出现这种情况,如果你不了解程序的运行方式,是很难找到原因的。

我们将上面的代码转换成汇编语言的代码如下

mov eax,dword ptr[_counter]     ; 将 counter 的值读入 eax 寄存器
add eax,eax                                     ; 将 eax 寄存器的值扩大2倍。
mov dword ptr[_counter],eax     ; 将 eax 寄存器的值存入 counter 中。

在多线程程序中,用汇编语言表示的代码每运行一行,处理都有可能切换到其他线程中。因而,假设 MyFun1 函数在读出 counter 数值100后,还未来得及将它的二倍值200写入 counter 时,正巧 MyFun2 函数读出了 counter 的值100,那么结果就将变为 200 。

为了避免该bug,我们可以采用以函数或 C 语言代码的行为单位来禁止线程切换的锁定方法,或者使用某种线程安全的方式来避免该问题的出现。

现在基本上没有人用汇编语言来编写程序了,因为 C、Java等高级语言的效率要比汇编语言快很多。不过,汇编语言的经验还是很重要的,通过借助汇编语言,我们可以更好的了解计算机运行机制。

程序员需要了解的硬核知识之控制硬件

原文:https://zwmst.com/2662.html

应用和硬件的关系

我们作为程序员一般很少直接操控硬件,我们一般通过 C、Java 等高级语言编写的程序起到间接控制硬件的作用。所以大家很少直接接触到硬件的指令,硬件的控制是由 Windows 操作系统 全权负责的。

你一定猜到我要说什么了,没错,我会说但是,任何事情没有绝对性,环境的不同会造成结果的偏差。虽然程序员没法直接控制硬件,并且 Windows 屏蔽了控制硬件的细节,但是 Windows 却为你开放了 系统调用功能来实现对硬件的控制。在 Windows 中,系统调用称为 API,API 就是应用调用的函数,这些函数的实体被存放在 DLL 文件中。

下面我们来看一个通过系统调用来间接控制硬件的实例

假如要在窗口中显示字符串,就可以使用 Windows API 中的 TextOut 函数。TextOut 函数的语法(C 语言)如下

BOOL TextOut{
  HDC hdc,                          // 设备描述表的句柄
  int nXStart,                                  // 显示字符串的 x 坐标
  int nYStart,                                  // 显示字符串的 y 坐标
  LPCTSTR lpString,                         // 指向字符串的指针
  int cbString                                  // 字符串的文字数
}

那么,在处理 TextOut 函数的内容时,Windows 做了些什么呢?从结果来看,Windows 直接控制了作为硬件的显示器。但 Windows 本身也是软件,由此可见,Windows 应该向 CPU 传递了某种指令,从而通过软件控制了硬件。

Windows 提供的 TextOut 函数 API 可以向窗口和打印机输出字符。C 语言提供的 printf 函数,是用来在命令提示符中显示字符串的函数。使用 printf 函数是无法向打印机输出字符的。

支持硬件输入输出的 IN 指令和 OUT 指令

Windows 控制硬件借助的是输入和输出指令。其中具有代表性的两个输入输出指令就是 INOUT 指令。这些指令也是汇编语言的助记符。

可以通过 IN 和 OUT 指令来实现对数据的读入和输出,如下图所示

也就是说,IN 指令通过指定的端口号输入数据,OUT 指令则是把 CPU 寄存器中存储的数据输出到指定端口号的端口。

那么这个端口号端口是什么呢?你感觉它像不像港口一样?通过标注哪个港口然后进行货物的运送和运出?

下面我们来看一下官方是如何定义端口号和端口的

还记得计算机组成原理中计算机的五大组成部分吗,再来回顾一下:运算器、控制器、存储器、输入设备和输出设备。我们今天不谈前三个,就说说后面两个输入设备和输出设备,这两个与我们本节主题息息相关。

那么问题来了,IO设备如何实现输入和输出的呢?计算机主机中,附带了用来连接显示器以及键盘等外围设备的连接器。 而连接器的内部,都连接有用来交换计算机主机同外围设备之间电流特性的 IC。如果 IC 你不明白是什么的话,可以参考作者的文章 程序员需要了解的硬核知识之内存 进行了解。这些 IC 统称为 IO 控制器

IO 是 Input/Output 的缩写。显示器、键盘等外围设备都有各自专用的 I/O 控制器。I/O 控制器中有用于临时保存输入输出数据的内存。这个内存就是 端口(port)。端口你就可以把它理解为我们上述说的 港口。IO 控制器内部的内存,也被称为寄存器,不要慌,这个寄存器和内存中的寄存器不一样。CPU 内存的寄存器是用于进行数据运算处理的,而IO中的寄存器是用于临时存储数据的。

在 I/O 设备内部的 IC 中,有多个端口。由于计算机中连接着很多外围设备,因此也就有很多 I/O 控制器。当然也会有多个端口,一个 I/O 控制器可以控制多个设备,不仅仅只能控制一个。各端口之间通过 端口号 进行区分。

端口号也被称为 I/O地址 。IN 指令和 OUT 指令在端口号指定的端口和 CPU 之间进行数据的输入和输出。这跟通过内存的地址来对内存进行读写是一样的道理。

测试输入和输出程序

首先让我们利用 IN 指令和 OUT 指令,来进行一个直接控制硬件的实验。假如试验的目的是让一个计算机内置的喇叭(蜂鸣器)发出声音。蜂鸣器封装在计算机内部,但它也是外围设备的一种。

用汇编语言比较繁琐,这次我们用 C 语言来实现。在大部分 C 语言的处理(编译器的种类)中,只要使用 _asm{ 和 }括起来,就可以在其中记述助记符。也就是说,采用这种方式就能够使用 C 语言和汇编语言混合的源代码。

在 AT 兼容机中,蜂鸣器的默认端口号是 61H ,末尾的 H 表示的是十六进制数的意思。用 IN 指令通过该端口号输入数据,并将数据的低2位设定为 ON,然后再通过该端口号用 OUT 指令输出数据,这时蜂鸣器就会发出声音。同样的方法,将数据的低2位设定为 OFF 并输出后,蜂鸣器就停止工作。

位设定为 ON 指的是将该位设定为1,位设定为 OFF 指的是将该位设定为0 。把位设定为 ON,只需要把想要设定为 ON 的位设定为1,其他位设定为0后进行 OR 运算即可。由于这里需要把低2位置为1,因此就是和 03H 进行 OR 运算。03H 用8为二进制来表示的话是 00000011。由于即便高6位存在着具体意义。和0进行OR运算后也不会发生变化,因而就和 03H 进行 OR 运算。把位设定为 OFF,只需要把想要置 OFF 的位设定为0,其他位设定为1后进行 AND 运算即可。由于这里需要把低2位设定为0,因此就要和 FCH 进行 AND 运算。在源代码中,FCH 是用 0FCH 来记述的。在前面加 0 是汇编语言的规定,表示的是以 A – F 这些字符开头的十六进制数是数值的意思。0FCH 用8位二进制数来表示的话是 11111100。由于即便高6位存在着具体意义,和1进行 AND 运算后也不会产生变化,因而就是同 0FCH 进行 OR 运算。

void main(){

  // 计数器
  int i;

  // 蜂鸣器发声
  _asm{
    IN  EAX, 61H
    OR  EAX, 03H
    OUT 61H, EAX
  }

  // 等待一段时间
  for(i = 0;i < 1000000;i++);

  // 蜂鸣器停止发生
  _asm{
    IN  EAX, 61H
    AND EAX, 0FCH
    OUT 61H, EAX
  }
}

我们对上面的代码进行说明,main 是 C 语言程序起始位置的函数。在该函数中,有两个用 _asm{} 围起来的部分,它们中间有一个使用 for 循环的空循环

首先是蜂鸣器发声的部分,通过 IN EAX,61H(助记符不区分大小写)指令,把端口 61H 的数据存储到 CPU 的 EAX 寄存器中。接下来,通过 OR EAX,03H 指令,把 EAX 寄存器的低2位设定成 ON。最后,通过 OUT 61H,EAX 指令,把 EAX 寄存器的内容输出到61端口。使蜂鸣器开始发音。虽然 EAX 寄存器的长度是 32 位,不过由于蜂鸣器端口是8位,所以只需对下8位进行OR运算和AND运算就可以正常工作了。

其次是一个重复100次的空循环,主要是为了在蜂鸣器开始发音和停止发音之间稍微加上一些时间间隔。因为现在计算机器的运行速度非常快,哪怕是 100 万次循环,也几乎是瞬时间完成的。

然后是用来控制器蜂鸣器停止发声的部分。首先,通过 IN EAX,61H 指令,把端口 61H 的数据存储到 CPU 的 EAX 寄存器中。接下来,通过 AND EAX,0FCH 指令,把 EAX 寄存器的低2位设定为 OFF。最后,通过 OUT 61H,EAX 指令,把寄存器的 EAX 内容输出到61号端口,使蜂鸣器停止发音。

外围设备的中断请求

IRQ(Interrupt Request) 代表的就是中断请求。IRQ 用来暂停当前正在运行的程序,并跳转到其他程序运行的必要机制。该机制被称为 处理中断。中断处理在硬件控制中担当着重要的角色。因为如果没有中断处理,就有可能无法顺畅进行处理的情况。

从中断处理开始到请求中断的程序(中断处理程序)运行结束之前,被中断的程序(主程序)的处理是停止的。这种情况就类似于在处理文档的过程中有电话打进来,电话就相当于是中断处理。假如没有中断处理的发生,就必须等到文档处理完成后才能够接听电话。由此可见,中断处理有着巨大的价值,就像是接听完电话后会返回原来的文档作业一样,中断程序处理完成后,也会返回到主程序中继续。

实施中断请求的是连接外围设备的 I/O 控制器,负责实施中断处理的是 CPU,外围设备的中断请求会使用不同于 I/O 端口的其他编号,该编号称为中断编号。在控制面板中查看软盘驱动器的属性时,IRQ处现实的数值是 06,表示的就是用06号来识别软盘驱动器发出的请求。还有就是操作系统以及 BIOS 则会提供响应中断编号的中断处理程序。

BIOS(Basic Input Output System): 位于计算机主板或者扩张卡上内置的 ROM 中,里面记录了用来控制外围设备的程序和数据。

假如有多个外围设备进行中断请求的话, CPU 需要做出选择进行处理,为此,我们可以在 I/O 控制器和 CPU 中间加入名为中断控制器的 IC 来进行缓冲。中断控制器会把从多个外围设备发出的中断请求有序的传递给 CPU。中断控制器的功能相当于就是缓冲。下面是中断控制器功能的示意图

CPU 在接受到中断请求后,会把当前正在运行的任务中断,并切换到中断处理程序。中断处理程序的第一步处理,就是把 CPU 所有寄存器的数值保存到内存的栈中。在中断处理程序中完成外围设备的输入和输出后,把栈中保存的数值还原到 CPU 寄存器中,然后再继续进行对主程序的处理。

假如 CPU 寄存器数值还没有还原的话,就会影响到主程序的运行,甚至还有可能会使程序意外停止或发生运行时异常。这是因为主程序在运行过程中,会用到 CPU 寄存器进行处理,这时候如果突然插入其他程序的运行结果,此时 CPU 必然会受到影响。所以,在处理完中断请求后,各个寄存器的值必须要还原。只要寄存器的值保持不变,主程序就可以像没有发生过任何事情一样继续处理。

用中断来实现实时处理

中断是指计算机运行过程中,出现某些意外情况需主机干预时,机器能自动停止正在运行的程序并转入处理新情况的程序,处理完毕后又返回原被暂停的程序继续运行。

在程序的运行过程中,几乎无时无刻都会发生中断,其原因就是为了实时处理外部输入的数据,虽然程序也可以在不会中断的基础上处理外部数据,但是那种情况下,主程序就会频繁的检查外围设备是否会有数据输入。由于外围设备会有很多个,因此有必要按照顺序来调查。按照顺序检查多个外围设备的状态称为 轮询。对于计算机来说,这种采用轮询的方式不是很合理,如果你正在检查是否有鼠标输入,这时候发生了键盘输入该如何处理呢?结果必定会导致文字的实时处理效率。所以即时的中断能够提高程序的运行效率。

上面只是中断的一种好处,下面汇总一下利用中断能够带来的正面影响

  • 提高计算机系统效率。计算机系统中处理机的工作速度远高于外围设备的工作速度。通过中断可以协调它们之间的工作。当外围设备需要与处理机交换信息时,由外围设备向处理机发出中断请求,处理机及时响应并作相应处理。不交换信息时,处理机和外围设备处于各自独立的并行工作状态。
  • 维持系统可靠正常工作。现代计算机中,程序员不能直接干预和操纵机器,必须通过中断系统向操作系统发出请求,由操作系统来实现人为干预。主存储器中往往有多道程序和各自的存储空间。在程序运行过程中,如出现越界访问,有可能引起程序混乱或相互破坏信息。为避免这类事件的发生,由存储管理部件进行监测,一旦发生越界访问,向处理机发出中断请求,处理机立即采取保护措施。
  • 满足实时处理要求。在实时系统中,各种监测和控制装置随机地向处理机发出中断请求,处理机随时响应并进行处理。
  • 提供故障现场处理手段。处理机中设有各种故障检测和错误诊断的部件,一旦发现故障或错误,立即发出中断请求,进行故障现场记录和隔离,为进一步处理提供必要的依据。

利用 DMA 实现短时间内大量数据传输

上面我们介绍了 I/O 处理和中断的关系,下面我们来介绍一下另外一个机制,这个机制就是 DMA(Direct Memory Access)。DMA 是指在不通过 CPU 的情况下,外围设备直接和主存进行数据传输。磁盘等硬件设备都用到了 DMA 机制,通过 DMA,大量数据可以在短时间内实现传输,之所以这么快,是因为 CPU 作为中介的时间被节省了,下面是 DMA 的传输过程

I/O 端口号、IRQ、DMA 通道可以说是识别外围设备的3点组合。不过,IRQ、DMA 通道并不是所有外围设备都具备的。计算机主机通过软件控制硬件时所需要的信息的最低限,是外围设备的 I/O 端口号。IRQ 只对需要中断处理的外围设备来说是必须的,DMA 通道则只对需要 DMA 机制的外围设备来说是必须的。假如多个外围设备都设定成相同的端口号、IRQ 和 DMA 通道的话,计算机就无法正常工作,会提示 设备冲突

文字和图片的显示机制

你知道文字和图片是如何显示出来的吗?事实上,如果用一句话来简单的概括一下该机制,那就是显示器中显示的信息一直存储在某内存中。该内存称为VRAM(Video RAM)。在程序中,只要往 VRAM 中写入数据,该数据就会在显示器中显示出来。实现该功能的程序,是由操作系统或者 BIOS 提供,并借助中断来进行处理。

MS-DOS 时代,对于大部分计算机来说,VRAM 都是主内存的一部分。在现代计算机中,显卡等专用硬件中一般都配置有与主内存相独立的 VRAM 和 GPU(Graphics Processing Unit),也叫做图形处理器或者图形芯片。这是因为,对经常描绘图形的 windows 来说,数百兆的 VRAM 都是必需的。

用软件来控制硬件听起来好像很难,但实际上只是利用输入输出指令同外围设备进行输入输出而已。中断处理是根据需要来使用的功能选项。DMA 则直接交给对应的外围设备即可。

虽然计算机领域新技术在不断涌现,但是计算机所能处理的事情,始终只是对输入的数据进行运算,并把结果输出,这一点是永远不会发生变化的。

程序员需要了解的硬核知识之操作系统入门

原文:https://zwmst.com/2664.html

操作系统环境

程序中包含着运行环境这一内容,可以说 运行环境 = 操作系统 + 硬件 ,操作系统又可以被称为软件,它是由一系列的指令组成的。我们不介绍操作系统,我们主要来介绍一下硬件的识别。

我们肯定都玩儿过游戏,你玩儿游戏前需要干什么?是不是需要先看一下自己的笔记本或者电脑是不是能肝的起游戏?下面是一个游戏的配置(怀念一下 wow)

图中的主要配置如下

  • 操作系统版本:说的就是应用程序运行在何种系统环境,现在市面上主要有三种操作系统环境,Windows 、Linux 和 Unix ,一般我们玩儿的大型游戏几乎都是在 Windows 上运行,可以说 Windows 是游戏的天堂。Windows 操作系统也会有区分,分为32位操作系统和64位操作系统,互不兼容。

  • 处理器:处理器指的就是 CPU,你的电脑的计算能力,通俗来讲就是每秒钟能处理的指令数,如果你的电脑觉得卡带不起来的话,很可能就是 CPU 的计算能力不足导致的。想要加深理解,请阅读博主的另一篇文章:程序员需要了解的硬核知识之CPU

  • 显卡:显卡承担图形的输出任务,因此又被称为图形处理器(Graphic Processing Unit,GPU),显卡也非常重要,比如我之前玩儿的剑灵开五档(其实就是图像变得更清晰)会卡,其实就是显卡显示不出来的原因。

  • 内存:内存即主存,就是你的应用程序在运行时能够动态分析指令的这部分存储空间,它的大小也能决定你电脑的运行速度,想要加深理解,请阅读博主的另一篇文章 程序员需要了解的硬核知识之内存

  • 存储空间:存储空间指的就是应用程序安装所占用的磁盘空间,由图中可知,此游戏的最低存储空间必须要大于 5GB,其实我们都会遗留很大一部分用来安装游戏。

从程序的运行环境这一角度来考量的话,CPU 的种类是特别重要的参数,为了使程序能够正常运行,必须满足 CPU 所需的最低配置。

CPU 只能解释其自身固有的语言。不同的 CPU 能解释的机器语言的种类也是不同的。机器语言的程序称为 本地代码(native code),程序员用 C 等高级语言编写的程序,仅仅是文本文件。文本文件(排除文字编码的问题)在任何环境下都能显示和编辑。我们称之为源代码。通过对源代码进行编译,就可以得到本地代码。下图反映了这个过程。

Windows 操作系统克服了CPU以外的硬件差异

计算机的硬件并不仅仅是由 CPU 组成的,还包括用于存储程序指令的数据和内存,以及通过 I/O 连接的键盘、显示器、硬盘、打印机等外围设备。

在 WIndows 软件中,键盘输入、显示器输出等并不是直接向硬件发送指令。而是通过向 Windows 发送指令实现的。因此,程序员就不用注意内存和 I/O 地址的不同构成了。Windows 操作的是硬件而不是软件,软件通过操作 Windows 系统可以达到控制硬件的目的。

不同操作系统的 API 差异性

接下来我们看一下操作系统的种类。同样机型的计算机,可安装的操作系统类型也会有多种选择。例如:AT 兼容机除了可以安装 Windows 之外,还可以采用 Unix 系列的 Linux 以及 FreeBSD (也是一种Unix操作系统)等多个操作系统。当然,应用软件则必须根据不同的操作系统类型来专门开发。CPU 的类型不同,所对应机器的语言也不同,同样的道理,操作系统的类型不同,应用程序向操作系统传递指令的途径也不同

应用程序向系统传递指令的途径称为 API(Application Programming Interface)。Windows 以及 Linux 操作系统的 API,提供了任何应用程序都可以利用的函数组合。因为不同操作系统的 API 是有差异的。所以,如何要将同样的应用程序移植到另外的操作系统,就必须要覆盖应用所用到的 API 部分。

键盘输入、鼠标输入、显示器输出、文件输入和输出等同外围设备进行交互的功能,都是通过 API 提供的。

**这也就是为什么 Windows 应用程序不能直接移植到 Linux 操作系统上的原因,API 差异太大了。

在同类型的操作系统下,不论硬件如何,API 几乎相同。但是,由于不同种类 CPU 的机器语言不同,因此本地代码也不尽相同。

FreeBSD Port 帮你轻松使用源代码

不知道你有没有这个想法:“既然 CPU 不同会导致本地代码不同,那为何不将源代码直接发送给程序呢?”这确实是一种解决办法,Unix 系列的 FreeBSD 操作系统就使用了这种方式。

Unix 系列操作系统 FreeBSD 中,存在一种名为 Ports 的机制。该机制能够结合当前运行环境的硬件环境来编译应用的源代码,进而得到可以运行的本地代码。如果目标应用的源代码在硬件上找不到,Ports 就会自动使用 FTP 连接到相应站点下载代码。

全球有很多站点都提供适用于 FreeBSD 的应用源代码。通过使用 Ports 可以利用的程序源代码,大约有 16000 种。根据不同的领域进行分类,可以随时使用。

FreeBSD 上应用的源代码,大部分是用 C 语言来标注的,C 编译器可以结合 FreeBSD 的运行环境来生成合适的本地代码。

FTP( File Transfer Protocol) 是连接到互联网上的计算机之间的传送文件的协议。

可以使用虚拟机获取其他环境

即使不通过应用程序的移植,在同一个操作系统上仍然可以使用其他的操作系统,那就是使用 虚拟机软件。虚拟机(Virtual Machine)指通过软件的具有完整硬件系统功能的、运行在一个完全隔离环境中的完整计算机系统。在实体计算机中能够完成的工作在虚拟机中都能够实现。

提供相同运行环境的 Java 虚拟机

总算是提到大 Java 了, Java 大法好,除了虚拟机的方法之外,还有一种方法能够提供不依赖于特定硬件和操作系统的程序运行环境,那就是 Java。

大家说的 Java 其实有两层意思,一种是作为编程语言的 Java;一种是作为程序运行环境的 Java。Java 与其他语言相同,都是通过源代码编译后运行的。不过,编译后生成的不是特定 CPU 使用的本地代码,而是名为字节代码 的程序。直接代码的运行环境就称为 Java 虚拟机(Java Virtual Machine)。Java 虚拟机是一边把 Java 字节代码逐一转换为本地代码一边在运行着。

程序运行时,将编译后的字节代码转换为本地代码,这样的操作看上去有些迂回,但由此可以实现相同的字节码可以在不同的操作系统环境下运行。

**想象一下,你开发完成的应用部署到 Linux 环境下,是不是什么都不用管?

Windows 有专门的 Windows 虚拟机,Macintosh 有 Macintosh 专门的虚拟机。从操作系统来看,Java虚拟机就是一个应用,从运行环境上来看,Java 虚拟机就是运行环境。

BIOS 和引导

最后对一些比较基础的部分做一些补充说明。程序的运行环境,存在着名为 BIOS(Basic Input/Output System)的系统。BIOS 存储在 ROM 中,是预先内置在计算机主机内部的程序。BIOS 除了键盘、磁盘和显卡等基本控制外,还有引导程序的功能。引导程序是存储在启动驱动器启示区域的小程序。操作系统的启动驱动器一般硬盘。不过有时也可能是 CD-ROM 或软盘。

电脑开机后,BIOS 会确认硬件是否正常运行,没有异常的话会直接启动引导程序。引导程序的功能是把在硬盘等记录的 OS 加载到内存中运行。虽然启动应用是 OS 的功能,但 OS 不能启动自己,是通过引导程序来启动的。

程序员需要了解的硬核知识之内存

原文:https://zwmst.com/2666.html

我们都知道,计算机是处理数据的设备,而数据的主要存储位置就是磁盘内存,并且对于程序员来讲,CPU 和内存是我们必须了解的两个物理结构,它是你通向高阶程序员很重要的桥梁,那么本篇文章我们就来介绍一下基本的内存知识。

什么是内存

内存(Memory)是计算机中最重要的部件之一,它是程序与CPU进行沟通的桥梁。计算机中所有程序的运行都是在内存中进行的,因此内存对计算机的影响非常大,内存又被称为主存,其作用是存放 CPU 中的运算数据,以及与硬盘等外部存储设备交换的数据。只要计算机在运行中,CPU 就会把需要运算的数据调到主存中进行运算,当运算完成后CPU再将结果传送出来,主存的运行也决定了计算机的稳定运行。

内存的物理结构

在了解一个事物之前,你首先得先需要过它,你才会有印象,才会有想要了解的兴趣,所以我们首先需要先看一下什么是内存以及它的物理结构是怎样的。

内存的内部是由各种IC电路组成的,它的种类很庞大,但是其主要分为三种存储器

  • 随机存储器(RAM): 内存中最重要的一种,表示既可以从中读取数据,也可以写入数据。当机器关闭时,内存中的信息会 丢失
  • 只读存储器(ROM):ROM 一般只能用于数据的读取,不能写入数据,但是当机器停电时,这些数据不会丢失。
  • 高速缓存(Cache):Cache 也是我们经常见到的,它分为一级缓存(L1 Cache)、二级缓存(L2 Cache)、三级缓存(L3 Cache)这些数据,它位于内存和 CPU 之间,是一个读写速度比内存更快的存储器。当 CPU 向内存写入数据时,这些数据也会被写入高速缓存中。当 CPU 需要读取数据时,会直接从高速缓存中直接读取,当然,如需要的数据在Cache中没有,CPU会再去读取内存中的数据。

内存 IC 是一个完整的结构,它内部也有电源、地址信号、数据信号、控制信号和用于寻址的 IC 引脚来进行数据的读写。下面是一个虚拟的 IC 引脚示意图

图中 VCC 和 GND 表示电源,A0 – A9 是地址信号的引脚,D0 – D7 表示的是数据信号、RD 和 WR 都是控制信号,我用不同的颜色进行了区分,将电源连接到 VCC 和 GND 后,就可以对其他引脚传递 0 和 1 的信号,大多数情况下,+5V 表示1,0V 表示 0

我们都知道内存是用来存储数据,那么这个内存 IC 中能存储多少数据呢?D0 – D7 表示的是数据信号,也就是说,一次可以输入输出 8 bit = 1 byte 的数据。A0 – A9 是地址信号共十个,表示可以指定 00000 00000 – 11111 11111 共 2 的 10次方 = 1024个地址。每个地址都会存放 1 byte 的数据,因此我们可以得出内存 IC 的容量就是 1 KB。

如果我们使用的是 512 MB 的内存,这就相当于是 512000(512 * 1000) 个内存 IC。当然,一台计算机不太可能有这么多个内存 IC ,然而,通常情况下,一个内存 IC 会有更多的引脚,也就能存储更多数据。

内存的读写过程

让我们把关注点放在内存 IC 对数据的读写过程上来吧!我们来看一个对内存IC 进行数据写入和读取的模型

来详细描述一下这个过程,假设我们要向内存 IC 中写入 1byte 的数据的话,它的过程是这样的:

  • 首先给 VCC 接通 +5V 的电源,给 GND 接通 0V 的电源,使用 A0 - A9 来指定数据的存储场所,然后再把数据的值输入给 D0 - D7 的数据信号,并把 WR(write)的值置为 1,执行完这些操作后,即可以向内存 IC 写入数据
  • 读出数据时,只需要通过 A0 – A9 的地址信号指定数据的存储场所,然后再将 RD 的值置为 1 即可。
  • 图中的 RD 和 WR 又被称为控制信号。其中当WR 和 RD 都为 0 时,无法进行写入和读取操作。

内存的现实模型

为了便于记忆,我们把内存模型映射成为我们现实世界的模型,在现实世界中,内存的模型很想我们生活的楼房。在这个楼房中,1层可以存储一个字节的数据,楼层号就是地址,下面是内存和楼层整合的模型图

我们知道,程序中的数据不仅只有数值,还有数据类型的概念,从内存上来看,就是占用内存大小(占用楼层数)的意思。即使物理上强制以 1 个字节为单位来逐一读写数据的内存,在程序中,通过指定其数据类型,也能实现以特定字节数为单位来进行读写。

下面是一个以特定字节数为例来读写指令字节的程序的示例

// 定义变量
char a;
short b;
long c;

// 变量赋值
a = 123;
b = 123;
c = 123;

我们分别声明了三个变量 a,b,c ,并给每个变量赋上了相同的 123,这三个变量表示内存的特定区域。通过变量,即使不指定物理地址,也可以直接完成读写操作,操作系统会自动为变量分配内存地址。

这三个变量分别表示 1 个字节长度的 char,2 个字节长度的 short,表示4 个字节的 long。因此,虽然数据都表示的是 123,但是其存储时所占的内存大小是不一样的。如下所示

这里的 123 都没有超过每个类型的最大长度,所以 short 和 long 类型为所占用的其他内存空间分配的数值是0,这里我们采用的是低字节序列的方式存储

低字节序列:将数据低位存储在内存低位地址。

高字节序列:将数据的高位存储在内存地位的方式称为高字节序列。

内存的使用

指针

指针是 C 语言非常重要的特征,指针也是一种变量,只不过它所表示的不是数据的值,而是内存的地址。通过使用指针,可以对任意内存地址的数据进行读写。

在了解指针读写的过程前,我们先需要了解如何定义一个指针,和普通的变量不同,在定义指针时,我们通常会在变量名前加一个 * 号。例如我们可以用指针定义如下的变量

char *d; // char类型的指针 d 定义
short *e; // short类型的指针 e 定义
long *f; // long类型的指针 f 定义

我们以32位计算机为例,32位计算机的内存地址是 4 字节,在这种情况下,指针的长度也是 32 位。然而,变量 d e f 却代表了不同的字节长度,这是为什么呢?

实际上,这些数据表示的是从内存中一次读取的字节数,比如 d e f 的值都为 100,那么使用 char 类型时就能够从内存中读写 1 byte 的数据,使用 short 类型就能够从内存读写 2 字节的数据, 使用 long 就能够读写 4 字节的数据,下面是一个完整的类型字节表

类型 32位 64位
char 1 1
short int 2 2
int 4 4
unsigned int 4 4
float 4 4
double 8 8
long 4 8
long long 8 8
unsigned long 4 8

我们可以用图来描述一下这个读写过程

数组是内存的实现

数组是指多个相同的数据类型在内存中连续排列的一种形式。作为数组元素的各个数据会通过下标编号来区分,这个编号也叫做索引,如此一来,就可以对指定索引的元素进行读写操作。

首先先来认识一下数组,我们还是用 char、short、long 三种元素来定义数组,数组的元素用[value] 扩起来,里面的值代表的是数组的长度,就像下面的定义

char g[100];
short h[100];
long i[100];

数组定义的数据类型,也表示一次能够读写的内存大小,char 、short 、long 分别以 1 、2 、4 个字节为例进行内存的读写。

数组是内存的实现,数组和内存的物理结构完全一致,尤其是在读写1个字节的时候,当字节数超过 1 时,只能通过逐个字节来读取,下面是内存的读写过程

数组是我们学习的第一个数据结构,我们都知道数组的检索效率是比较快的,至于数组的检索效率为什么这么快并不是我们这篇文章讨论的重点。

栈和队列

我们上面提到数组是内存的一种实现,使用数组能够使编程更加高效,下面我们就来认识一下其他数据结构,通过这些数据结构也可以操作内存的读写。

栈(stack)是一种很重要的数据结构,栈采用 LIFO(Last In First Out)即后入先出的方式对内存进行操作。它就像一个大的收纳箱,你可以往里面放相同类型的东西,比如书,最先放进收纳箱的书在最下面,最后放进收纳箱的书在最上面,如果你想拿书的话, 必须从最上面开始取,否则是无法取出最下面的书籍的。

栈的数据结构就是这样,你把书籍压入收纳箱的操作叫做压入(push),你把书籍从收纳箱取出的操作叫做弹出(pop),它的模型图大概是这样

入栈相当于是增加操作,出栈相当于是删除操作,只不过叫法不一样。栈和内存不同,它不需要指定元素的地址。它的大概使用如下

// 压入数据
Push(123);
Push(456);
Push(789);

// 弹出数据
j = Pop();
k = Pop();
l = Pop();

在栈中,LIFO 方式表示栈的数组中所保存的最后面的数据(Last In)会被最先读取出来(First On)。

队列

队列和栈很相似但又不同,相同之处在于队列也不需要指定元素的地址,不同之处在于队列是一种 先入先出(First In First Out) 的数据结构。队列在我们生活中的使用很像是我们去景区排队买票一样,第一个排队的人最先买到票,以此类推,俗话说: 先到先得。它的使用如下

// 往队列中写入数据
EnQueue(123);
EnQueue(456);
EnQueue(789);

// 从队列中读出数据
m = DeQueue();
n = DeQueue();
o = DeQueue();

向队列中写入数据称为 EnQueue()入列,从队列中读出数据称为DeQueue()

与栈相对,FIFO 的方式表示队列中最先所保存的数据会优先被读取出来。

队列的实现一般有两种:顺序队列循环队列,我们上面的事例使用的是顺序队列,那么下面我们看一下循环队列的实现方式

**环形缓冲区

循环队列一般是以环状缓冲区(ring buffer)的方式实现的,它是一种用于表示一个固定尺寸、头尾相连的缓冲区的数据结构,适合缓存数据流。假如我们要用 6 个元素的数组来实现一个环形缓冲区,这时可以从起始位置开始有序的存储数据,然后再按照存储时的顺序把数据读出。在数组的末尾写入数据后,后一个数据就会从缓冲区的头开始写。这样,数组的末尾和开头就连接了起来。

链表

下面我们来介绍一下链表二叉树,它们都是可以不用考虑索引的顺序就可以对元素进行读写的方式。通过使用链表,可以高效的对数据元素进行添加删除操作。而通过使用二叉树,则可以更高效的对数据进行检索

在实现数组的基础上,除了数据的值之外,通过为其附带上下一个元素的索引,即可实现链表。数据的值和下一个元素的地址(索引)就构成了一个链表元素,如下所示

对链表的添加和删除都是非常高效的,我们来叙述一下这个添加和删除的过程,假如我们要删除地址为 p[2] 的元素,链表该如何变化呢?

我们可以看到,删除地址为 p[2] 的元素后,直接将链表剔除,并把 p[2] 前一个位置的元素 p[1] 的指针域指向 p[2] 下一个链表元素的数据区即可。

那么对于新添加进来的链表,需要确定插入位置,比如要在 p[2] 和 p[3] 之间插入地址为 p[6] 的元素,需要将 p[6] 的前一个位置 p[2] 的指针域改为 p[6] 的地址,然后将 p[6] 的指针域改为 p[3] 的地址即可。

链表的添加不涉及到数据的移动,所以链表的添加和删除很快,而数组的添加设计到数据的移动,所以比较慢,通常情况下,使用数组来检索数据,使用链表来进行添加和删除操作。

二叉树

二叉树也是一种检索效率非常高的数据结构,二叉树是指在链表的基础上往数组追加元素时,考虑到数组的大小关系,将其分成左右两个方向的表现形式。假如我们把 50 这个值保存到了数组中,那么,如果接下来要进行值写入的话,就需要和50比较,确定谁大谁小,比50数值大的放右边,小的放左边,下图是二叉树的比较示例

二叉树是由链表发展而来,因此二叉树在追加和删除元素方面也是同样有效的。

这一切的演变都是以内存为基础的。

程序员需要了解的硬核知识之磁盘

原文:https://zwmst.com/2668.html

我们大家知道,计算机的五大基础部件是 存储器控制器运算器输入和输出设备,其中从存储功能的角度来看,可以把存储器分为内存磁盘,内存我们上面的文章已经介绍过了,那么此篇文章我们来介绍一下磁盘以及内存和磁盘的关系。

认识磁盘

首先,磁盘和内存都具有存储功能,它们都是存储设备。区别在于,内存是通过电流 来实现存储;磁盘则是通过磁记录技术来实现存储。内存是一种高速,造假昂贵的存储设备;而磁盘则是速度较慢、造假低廉的存储设备;电脑断电后,内存中的数据会丢失,而磁盘中的数据可以长久保留。内存是属于内部存储设备,硬盘是属于 外部存储设备。一般在我们的计算机中,磁盘和内存是相互配合共同作业的。

一般内存指的就是主存(负责存储CPU中运行的程序和数据);早起的磁盘指的是软磁盘(soft disk,简称软盘),就是下面这个

(2000年的时候我曾经我姑姑家最早的计算机中见到过这个,当时还不知道这是啥,现在知道了。)

如今常用的磁盘是硬磁盘(hard disk,简称硬盘),就是下面这个

程序不读入内存就无法运行

在了解磁盘前,还需要了解一下内存的运行机制是怎样的,我们的程序被保存在存储设备中,通过使用 CPU 读入来实现程序指令的执行。这种机制称为存储程序方式,现在看来这种方式是理所当然的,但在以前程序的运行都是通过改变计算机的布线来读写指令的。

计算机最主要的存储部件是内存和磁盘。磁盘中存储的程序必须加载到内存中才能运行,在磁盘中保存的程序是无法直接运行的,这是因为负责解析和运行程序内容的 CPU 是需要通过程序计数器来指定内存地址从而读出程序指令的。

磁盘构件

磁盘缓存

我们上面提到,磁盘往往和内存是互利共生的关系,相互协作,彼此持有良好的合作关系。每次内存都需要从磁盘中读取数据,必然会读到相同的内容,所以一定会有一个角色负责存储我们经常需要读到的内容。 我们大家做软件的时候经常会用到缓存技术,那么硬件层面也不例外,磁盘也有缓存,磁盘的缓存叫做磁盘缓存

磁盘缓存指的是把从磁盘中读出的数据存储到内存的方式,这样一来,当接下来需要读取相同的内容时,就不会再通过实际的磁盘,而是通过磁盘缓存来读取。某一种技术或者框架的出现势必要解决某种问题的,那么磁盘缓存就大大改善了磁盘访问的速度

Windows 操作系统提供了磁盘缓存技术,不过,对于大部分用户来说是感受不到磁盘缓存的,并且随着计算机的演进,对硬盘的访问速度也在不断演进,实际上磁盘缓存到 Windows 95/98 就已经不怎么使用了。

把低速设备的数据保存在高速设备中,需要时可以直接将其从高速设备中读出,这种缓存方式在web中应用比较广泛,web 浏览器是通过网络来获取远程 web 服务器的数据并将其显示出来。因此,在读取较大的图片的时候,会耗费不少时间,这时 web 浏览器可以把获取的数据保存在磁盘中,然后根据需要显示数据,再次读取的时候就不用重新加载了。

虚拟内存

虚拟内存是内存和磁盘交互的第二个媒介。虚拟内存是指把磁盘的一部分作为假想内存来使用。这与磁盘缓存是假想的磁盘(实际上是内存)相对,虚拟内存是假想的内存(实际上是磁盘)。

虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续可用的内存(一个完整的地址空间),但是实际上,它通常被分割成多个物理碎片,还有部分存储在外部磁盘管理器上,必要时进行数据交换。

计算机中的程序都要通过内存来运行,如果程序占用内存很大,就会将内存空间消耗殆尽。为了解决这个问题,WINDOWS 操作系统运用了虚拟内存技术,通过拿出一部分硬盘来当作内存使用,来保证程序耗尽内存仍然有可以存储的空间。虚拟内存在硬盘上的存在形式就是 PAGEFILE.SYS 这个页面文件。

通过借助虚拟内存,在内存不足时仍然可以运行程序。例如,在只剩 5MB 内存空间的情况下仍然可以运行 10MB 的程序。由于 CPU 只能执行加载到内存中的程序,因此,虚拟内存的空间就需要和内存中的空间进行置换(swap),然后运行程序。

虚拟内存与内存的交换方式

刚才我们提到虚拟内存需要和内存中的部分内容做置换才可让 CPU 继续执行程序,那么做置换的方式是怎样的呢?又分为哪几种方式呢?

虚拟内存的方法有分页式分段式 两种。Windows 采用的是分页式。该方式是指在不考虑程序构造的情况下,把运行的程序按照一定大小的页进行分割,并以为单位进行置换。在分页式中,我们把磁盘的内容读到内存中称为 Page In,把内存的内容写入磁盘称为 Page Out。Windows 计算机的页大小为 4KB ,也就是说,需要把应用程序按照 4KB 的页来进行切分,以页(page)为单位放到磁盘中,然后进行置换。

为了实现内存功能,Windows 在磁盘上提供了虚拟内存使用的文件(page file,页文件)。该文件由 Windows 生成和管理,文件的大小和虚拟内存大小相同,通常大小是内存的 1 – 2 倍。

节约内存

Windows 是以图形界面为基础的操作系统。它的前身是 MS-DOC,最初的版本可以在 128kb 的内存上运行程序,但是现在想要 Windows 运行流畅的花至少要需要 512MB 的内存,但通常往往是不够的。

也许许多人认为可以使用虚拟内存来解决内存不足的情况,而虚拟内存确实能够在内存不足的时候提供补充,但是使用虚拟内存的 Page In 和 Page Out 通常伴随着低速的磁盘访问,这是一种得不偿失的表现。所以虚拟内存无法从根本上解决内存不足的情况。

为了从根本上解决内存不足的情况,要么是增加内存的容量,加内存条;要么是优化应用程序,使其尽可能变小。第一种建议往往需要衡量口袋的银子,所以我们只关注第二种情况。

注意:以下的篇幅会涉及到 C 语言的介绍,是每个程序员(不限语言)都需要知道和了解的知识。

通过 DLL 文件实现函数共有

DLL(Dynamic Link Library)文件,是一种动态链接库 文件,顾名思义,是在程序运行时可以动态加载 Library(函数和数据的集合)的文件。此外,多个应用可以共有同一个 DLL 文件。而通过共有一个 DLL 文件则可以达到节约内存的效果。

例如,假设我们编写了一个具有某些处理功能的函数 MyFunc()。应用 A 和 应用 B 都需要用到这个函数,然后在各自的应用程序中内置 MyFunc()(这个称为Static Link,静态链接)后同时运行两个应用,内存中就存在了同一个函数的两个程序,这会造成资源浪费。

为了改变这一点,使用 DLL 文件而不是应用程序的执行文件(EXE文件)。因为同一个 DLL 文件内容在运行时可以被多个应用共有,因此内存中存在函数 MyFunc()的程序就只有一个

Windows 操作系统其实就是无数个 DLL 文件的集合体。有些应用在安装时,DLL文件也会被追加。应用程序通过这些 DLL 文件来运行,既可以节约内存,也可以在不升级 EXE 文件的情况下,通过升级 DLL 文件就可以完成更新。

通过调用 _stdcall 来减少程序文件的大小

通过调用 _stdcall 来减小程序文件的方法,是用 C 语言编写应用时可以利用的高级技巧。我们来认识一下什么是 _stdcall。

_stdcall 是 standard call(标准调用)的缩写。Windows 提供的 DLL 文件内的函数,基本上都是通过 _stdcall 调用方式来完成的,这主要是为了节约内存。另一方面,用 C 语言编写的程序默认都不是 _stdcall 。C 语言特有的调用方式称为 C 调用。C 语言默认不使用 _stdcall 的原因是因为 C 语言所对应的函数传入参数是可变的,只有函数调用方才能知道到底有多少个参数,在这种情况下,栈的清理作业便无法进行。不过,在 C 语言中,如果函数的参数和数量固定的话,指定 _stdcall 是没有任何问题的。

C 语言和 Java 最主要的区别之一在于 C 语言需要人为控制释放内存空间

C 语言中,在调用函数后,需要人为执行栈清理指令。把不需要的数据从接收和传递函数的参数时使用的内存上的栈区域中清理出去的操作叫做 栈清理处理

例如如下代码

// 函数调用方
void main(){
  int a;
  a = MyFunc(123,456);
}

// 被调用方
int MyFunc(int a,int b){
  ...
}

代码中,从 main 主函数调用到 MyFunc() 方法,按照默认的设定,栈的清理处理会附加在 main 主函数这一方。在同一个程序中,有可能会多次调用,导致 MyFunc() 会进行多次清理,这就会造成内存的浪费。

汇编之后的代码如下

push 1C8h                               // 将参数 456( = 1C8h) 存入栈中
push 7Bh                                // 将参数 123( = 7Bh) 存入栈中
call @LTD+15 (MyFunc)(00401014)         // 调用 MyFunc 函数
add esp,8                               // 运行栈清理

C 语言通过栈来传递函数的参数,使用 push 是往栈中存入数据的指令,pop 是从栈中取出数据的指令。32 位 CPU 中,1次 push 指令可以存储 4 个字节(32 位)的数据。上述代码由于进行了两次 push 操作,所以存储了 8 字节的数据。通过 call 指令来调用函数,调用完成后,栈中存储的数据就不再需要了。于是就通过 add esp,8 这个指令,使存储着栈数据的 esp 寄存器前进 8 位(设定为指向高 8 位字节的地址),来进行数据清理。由于栈是在各种情况下都可以利用的内存领域,因此使用完毕后有必要将其恢复到原始状态。上述操作就是执行栈的清理工作。另外,在 C 语言中,函数的返回值,是通过寄存器而非栈来返回的。

栈执行清理工作,在调用方法处执行清理工作和在反复调用方法处执行清理工作不同,使用 _stdcall 标准调用的方式称为反复调用方法,在这种情况下执行栈清理开销比较小。

磁盘的物理结构

之前我们介绍了CPU、内存的物理结构,现在我们来介绍一下磁盘的物理结构。磁盘的物理结构指的是磁盘存储数据的形式

磁盘是通过其物理表面划分成多个空间来使用的。划分的方式有两种:可变长方式扇区方式。前者是将物理结构划分成长度可变的空间,后者是将磁盘结构划分为固定长度的空间。一般 Windows 所使用的硬盘和软盘都是使用扇区这种方式。扇区中,把磁盘表面分成若干个同心圆的空间就是 磁道,把磁道按照固定大小的存储空间划分而成的就是 扇区

扇区是对磁盘进行物理读写的最小单位。Windows 中使用的磁盘,一般是一个扇区 512 个字节。不过,Windows 在逻辑方面对磁盘进行读写的单位是扇区整数倍簇。根据磁盘容量不同功能,1簇可以是 512 字节(1 簇 = 1扇区)、1KB(1簇 = 2扇区)、2KB、4KB、8KB、16KB、32KB( 1 簇 = 64 扇区)。簇和扇区的大小是相等的。

不管是硬盘还是软盘,不同的文件是不能存储在同一簇中的,否则就会导致只有一方的文件不能删除。所以,不管多小的文件,都会占用 1 簇的空间。这样一来,所有的文件都会占用 1 簇的整数倍的空间。

我们使用软盘做实验会比较简单一些,我们先对软盘进行格式化,格式化后的软盘空间如下

接下来,我们保存一个 txt 文件,并在文件输入一个字符,这时候文件其实只占用了一个字节,但是我们看一下磁盘的属性却占用了 512 字节

然后我们继续写入一些东西,当文件大小到达 512 个字节时,已用空间也是 512 字节,但是当我们继续写入一个字符时,我们点开属性会发现磁盘空间会变为 1024 个字节(= 2 簇),通过这个实验我们可以证明磁盘是以簇为单位来保存的。

程序员需要了解的硬核知识之二进制

原文:https://zwmst.com/2670.html

我们都知道,计算机的底层都是使用二进制数据进行数据流传输的,那么为什么会使用二进制表示计算机呢?或者说,什么是二进制数呢?在拓展一步,如何使用二进制进行加减乘除?二进制数如何表示负数呢?本文将一一为你揭晓。

为什么用二进制表示

我们大家知道,计算机内部是由IC电子元件组成的,其中 CPU内存 也是 IC 电子元件的一种,CPU和内存图如下

CPU 和 内存使用IC电子元件作为基本单元,IC电子元件有不同种形状,但是其内部的组成单元称为一个个的引脚。有人说CPU 和 内存内部都是超大规模集成电路,其实IC 就是集成电路(Integrated Circuit)。

IC元件两侧排列的四方形块就是引脚,IC的所有引脚,只有两种电压: 0V5V,IC的这种特性,也就决定了计算机的信息处理只能用 0 和 1 表示,也就是二进制来处理。一个引脚可以表示一个 0 或 1 ,所以二进制的表示方式就变成 0、1、10、11、100、101等,虽然二进制数并不是专门为 引脚 来设计的,但是和 IC引脚的特性非常吻合。

计算机的最小集成单位为 ,也就是 比特(bit),二进制数的位数一般为 8位、16位、32位、64位,也就是 8 的倍数,为什么要跟 8 扯上关系呢? 因为在计算机中,把 8 位二进制数称为 一个字节, 一个字节有 8 位,也就是由 8个bit构成。

为什么1个字节等于8位呢?因为 8 位能够涵盖所有的字符编码,这个记住就可以了。

字节是最基本的计量单位,位是最小单位。

用字节处理数据时,如果数字小于存储数据的字节数 ( = 二进制的位数),那么高位就用 0 填补,高位和数学的数字表示是一样的,左侧表示高位,右侧表示低位。比如 这个六位数用二进制数来表示就是 100111,只有6位,高位需要用 0 填充,填充完后是 00100111,占一个字节,如果用 16 位表示 就是 0000 0000 0010 0111占用两个字节。

我们一般口述的 32 位和 64位的计算机一般就指的是处理位数,32 位一次可以表示 4个字节,64位一次可以表示8个字节的二进制数。

我们一般在软件开发中用十进制数表示的逻辑运算等,也会被计算机转换为二进制数处理。对于二进制数,计算机不会区分他是 图片、音频文件还是数字,这些都是一些数据的结合体。

什么是二进制数

那么什么是二进制数呢?为了说明这个问题,我们先把 00100111 这个数转换为十进制数看一下,二进制数转换为十进制数,直接将各位置上的值 * 位权即可,那么我们将上面的数值进行转换

也就是说,二进制数代表的 00100111 转换成十进制就是 39,这个 39 并不是 3 和 9 两个数字连着写,而是 3 10 + 9 1,这里面的 10 , 1 就是位权,以此类推,上述例子中的位权从高位到低位依次就是 7 6 5 4 3 2 1 0 。这个位权也叫做次幂,那么最高位就是2的7次幂,2的6次幂 等等。二进制数的运算每次都会以2为底,这个2 指得就是基数,那么十进制数的基数也就是 10 。在任何情况下位权的值都是 数的位数 – 1,那么第一位的位权就是 1 – 1 = 0, 第二位的位权就睡 2 – 1 = 1,以此类推。

那么我们所说的二进制数其实就是 用0和1两个数字来表示的数,它的基数为2,它的数值就是每个数的位数 位权再求和得到的结果,我们一般来说数值指的就是十进制数,那么它的数值就是 3 10 + 9 * 1 = 39。

移位运算和乘除的关系

在了解过二进制之后,下面我们来看一下二进制的运算,和十进制数一样,加减乘除也适用于二进制数,只要注意逢 2 进位即可。二进制数的运算,也是计算机程序所特有的运算,因此了解二进制的运算是必须要掌握的。

首先我们来介绍移位 运算,移位运算是指将二进制的数值的各个位置上的元素坐左移和右移操作,见下图

上述例子中还是以 39 为例,我们先把十进制的39 转换为二进制的 0010 0111,然后向左移位 << 一个字节,也就变成了 0100 1110,那么再把此二进制数转换为十进制数就是上面的78, 十进制的78 竟然是 十进制39 的2倍关系。我们在让 0010 0111 左移两位,也就是 1001 1100,得出来的值是 156,相当于扩大了四倍!

因此你可以得出来此结论,左移相当于是数值扩大的操作,那么右移 >> 呢?按理说右移应该是缩小 1/2,1/4 倍,但是39 缩小二倍和四倍不就变成小数了吗?这个怎么表示呢?请看下一节

便于计算机处理的补数

刚才我们没有介绍右移的情况,是因为右移之后空出来的高位数值,有 0 和 1 两种形式。要想区分什么时候补0什么时候补1,首先就需要掌握二进制数表示负数的方法。

二进制数中表示负数值时,一般会把最高位作为符号来使用,因此我们把这个最高位当作符号位。 符号位是 0 时表示正数,是 1 时表示 负数。那么 -1 用二进制数该如何表示呢?可能很多人会这么认为: 因为 1 的二进制数是 0000 0001,最高位是符号位,所以正确的表示 -1 应该是 1000 0001,但是这个答案真的对吗?

计算机世界中是没有减法的,计算机在做减法的时候其实就是在做加法,也就是用加法来实现的减法运算。比如 100 – 50 ,其实计算机来看的时候应该是 100 + (-50),为此,在表示负数的时候就要用到二进制补数,补数就是用正数来表示的负数。

为了获得补数,我们需要将二进制的各数位的数值全部取反,然后再将结果 + 1 即可,先记住这个结论,下面我们来演示一下。

具体来说,就是需要先获取某个数值的二进制数,然后对二进制数的每一位做取反操作(0 —> 1 , 1 —> 0),最后再对取反后的数 +1 ,这样就完成了补数的获取。

补数的获取,虽然直观上不易理解,但是逻辑上却非常严谨,比如我们来看一下 1 – 1 的这个过程,我们先用上面的这个 1000 0001(它是1的补数,不知道的请看上文,正确性先不管,只是用来做一下计算)来表示一下

奇怪,1 – 1 会变成 130 ,而不是0,所以可以得出结论 1000 0001 表示 -1 是完全错误的。

那么正确的该如何表示呢?其实我们上面已经给出结果了,那就是 1111 1111,来论证一下它的正确性

我们可以看到 1 – 1 其实实际上就是 1 + (-1),对 -1 进行上面的取反 + 1 后变为 1111 1111, 然后与 1 进行加法运算,得到的结果是九位的 1 0000 0000,结果发生了溢出,计算机会直接忽略掉溢出位,也就是直接抛掉 最高位 1 ,变为 0000 0000。也就是 0,结果正确,所以 1111 1111 表示的就是 -1 。

所以负数的二进制表示就是先求其补数,补数的求解过程就是对原始数值的二进制数各位取反,然后将结果 + 1

当然,结果不为 0 的运算同样也可以通过补数求得正确的结果。不过,有一点需要注意,当运算结果为负的时候,计算结果的值也是以补数的形式出现的,比如 3 – 5 这个运算,来看一下解析过程

3 – 5 的运算,我们按着上面的思路来过一遍,计算出来的结果是 1111 1110,我们知道,这个数值肯定表示负数,但是负数无法直接用十进制表示,需要对其取反+ 1,算出来的结果是 2,因为 1111 1110的高位是 1,所以最终的结果是 -2。

编程语言的数据类型中,有的可以处理负数,有的不可以。比如 C语言中不能处理负数的 unsigned short类型,也有能处理负数的short类型 ,都是两个字节的变量,它们都有 2 的十六次幂种值,但是取值范围不一样,short 类型的取值范围是 -32768 – 32767 , unsigned short 的取值范围是 0 – 65536。

仔细思考一下补数的机制,就能明白 -32768 比 32767 多一个数的原因了,最高位是 0 的正数有 0 ~ 32767 共 32768 个,其中包括0。最高位是 1 的负数,有 -1 ~ -32768 共 32768 个,其中不包含0。0 虽然既不是正数也不是负数,但是考虑到其符号位,就将其归为了正数。

算数右移和逻辑右移的区别

在了解完补数后,我们重新考虑一下右移这个议题,右移在移位后空出来的最高位有两种情况 0 和 1。当二进制数的值表示图形模式而非数值时,移位后需要在最高位补0,类似于霓虹灯向右平移的效果,这就被称为逻辑右移

将二进制数作为带符号的数值进行右移运算时,移位后需要在最高位填充移位前符号位的值( 0 或 1)。这就被称为算数右移。如果数值使用补数表示的负数值,那么右移后在空出来的最高位补 1,就可以正确的表示 1/2,1/4,1/8等的数值运算。如果是正数,那么直接在空出来的位置补 0 即可。

下面来看一个右移的例子。将 -4 右移两位,来各自看一下移位示意图

如上图所示,在逻辑右移的情况下, -4 右移两位会变成 63, 显然不是它的 1/4,所以不能使用逻辑右移,那么算数右移的情况下,右移两位会变为 -1,显然是它的 1/4,故而采用算数右移。

那么我们可以得出来一个结论:**左移时,无论是图形还是数值,移位后,只需要将低位补 0 即可;右移时,需要根据情况判断是逻辑右移还是算数右移。

下面介绍一下符号扩展:**将数据进行符号扩展是为了产生一个位数加倍、但数值大小不变的结果,以满足有些指令对操作数位数的要求,例如倍长于除数的被除数,再如将数据位数加长以减少计算过程中的误差。

以8位二进制为例,符号扩展就是指在保持值不变的前提下将其转换成为16位和32位的二进制数。将0111 1111这个正的 8位二进制数转换成为 16位二进制数时,很容易就能够得出0000 0000 0111 1111这个正确的结果,但是像 1111 1111这样的补数来表示的数值,该如何处理?直接将其表示成为1111 1111 1111 1111就可以了。也就是说,不管正数还是补数表示的负数,只需要将 0 和 1 填充高位即可。

逻辑运算的窍门

掌握逻辑和运算的区别是:将二进制数表示的信息作为四则运算的数值来处理就是算数,像图形那样,将数值处理为单纯的 01 的罗列就是逻辑

计算机能够处理的运算,大体可分为逻辑运算和算数运算,算数运算指的是加减乘除四则运算;逻辑运算指的是对二进制各个数位的 0 和 1分别进行处理的运算,包括逻辑非(NOT运算)、逻辑与(AND运算)、逻辑或(OR运算)和逻辑异或(XOR运算)四种。

  • 逻辑非 指的是将 0 变成 1,1 变成 0 的取反操作
  • 逻辑与 指的是"两个都是 1 时,运算结果才是 1,其他情况下是 0"
  • 逻辑或 指的是"至少有一方是 1 时,运算结果为 1,其他情况下运算结果都是 0"
  • 逻辑异或 指的是 "其中一方是 1,另一方是 0时运算结果才是 1,其他情况下是 0"

掌握逻辑运算的窍门,就是要摒弃二进制数表示数值这一个想法。大家不要把二进制数表示的值当作数值,应该把它看成是 开关上的 ON/OFF

程序员需要了解的硬核知识之操作系统和应用

原文:https://zwmst.com/2672.html

利用计算机运行程序大部分都是为了提高处理效率。例如,Microsoft Word 这样的文字处理软件,是用来提高文本文件处理效率的程序,Microsoft Excel 等表格计算软件,是用来提高账本处理效率的程序。这种为了提高特定处理效率的程序统称为 应用

程序员的工作就是编写各种各样的应用来提高工作效率,程序员一般不编写操作系统,但是程序员编写的应用离不开操作系统,此篇文章我们就针对 Windows 操作系统来说明一下操作系统和应用之间的关系。

操作系统功能的历史

操作系统其实也是一种软件,任何新事物的出现肯定都有它的历史背景,那么操作系统也不是凭空出现的,肯定有它的历史背景。

在计算机尚不存在操作系统的年代,完全没有任何程序,人们通过各种按钮来控制计算机,这一过程非常麻烦。于是,有人开发出了仅具有加载和运行功能的监控程序,这就是操作系统的原型。通过事先启动监控程序,程序员可以根据需要将各种程序加载到内存中运行。虽然仍旧比较麻烦,但比起在没有任何程序的状态下进行开发,工作量得到了很大的缓解。

随着时代的发展,人们在利用监控程序编写程序的过程中发现很多程序都有公共的部分。例如,通过键盘进行文字输入,显示器进行数据展示等,如果每编写一个新的应用程序都需要相同的处理的话,那真是太浪费时间了。因此,基本的输入输出部分的程序就被追加到了监控程序中。初期的操作系统就是这样诞生了。

类似的想法可以共用,人们又发现有更多的应用程序可以追加到监控程序中,比如硬件控制程序编程语言处理器(汇编、编译、解析)以及各种应用程序等,结果就形成了和现在差异不大的操作系统,也就是说,其实操作系统是多个程序的集合体。

我在 程序员需要了解的硬核知识之CPU这篇文章中提到了汇编语言,这里简单再提一下。

汇编语言是一种低级语言,也被称为符号语言。汇编语言是第二代计算机语言,在汇编语言中,用助记符代替机器指令的操作码,用地址符号或标号代替指令或操作数的地址。用一些容易理解和记忆的字母,单词来代替一个特定的指令,比如:用ADD代表数字逻辑上的加减,MOV代表数据传递等等,通过这种方法,人们很容易去阅读已经完成的程序或者理解程序正在执行的功能,对现有程序的bug修复以及运营维护都变得更加简单方便

可以说共用思想真是人类前进的一大步,对于解放生产力而言简直是太重要了

要把操作系统放在第一位

对于程序员来说,程序员创造的不是硬件,而是各种应用程序,但是如果程序员只做应用不懂硬件层面的知识的话,是无法成为硬核程序员的。现在培训机构培养出了一批怎么用的人才,却没有培训出为什么这么做的人才,毕竟为什么不是培训机构教的,而是学校教的,我很相信耗子叔说的话:学习没有速成这回事。言归正题。

在操作系统诞生之后,程序员不需要在硬件层面考虑问题,所以程序员的数量就增加了。哪怕自称对硬件一窍不通的人也可能制作出一个有模有样的程序。不过,要想成为一个全面的程序员,有一点需要清楚的就是,掌握硬件的基本知识,并借助操作系统进行抽象化,可以大大提高编程效率。

下面就看一下操作系统是如何给开发人员带来便利的,在 Windows 操作系统下,用 C 语言制作一个具有表示当前时间功能的应用。time() 是用来取得当前日期和时间的函数,printf() 是把结果打印到显示器上的函数,如下:

#include <stdio.h>
#include <time.h>

void main(){
  // 保存当前日期和时间信息
  time_t tm;

  // 取得当前的日期和时间
  time(&tm);

  // 在显示器上显示日期和时间
  printf("%s\n", ctime(&tm));
}

读者可以自行运行程序查看结果,我们主要关注硬件在这段代码中做了什么事情

  • 通过 time_t tm,为 time_t 类型的变量申请分配内存空间;
  • 通过 time(&tm) ,将当前的日期和时间数据保存到变量的内存空间中
  • 通过 printf("%s\n",ctime(&tm)), 把变量内存空间的内容输出到显示器上。

应用的可执行文件指的是,计算机的 CPU 可以直接解释并运行的本地代码,不过这些代码是无法直接控制硬件的,事实上,这些代码是通过操作系统来间接控制硬件的。变量中涉及到的内存分配情况,以及 time() 和 printf() 这些函数的运行结果,都不是面向硬件而是面向操作系统的。操作系统收到应用发出的指令后,首先会对该指令进行解释,然后会对 时钟IC 和显示器用的 I/O 进行控制。

计算机中都安装有保存日期和时间的实时时钟(Real-time clock),上面提到的时钟IC 就是值该实时时钟。

系统调用和编程语言的移植性

操作系统控制硬件的功能,都是通过一些小的函数集合体的形式来提供的。这些函数以及调用函数的行为称为系统调用,也就是通过应用进而调用操作系统的意思。在前面的程序中用到了 time() 以及 printf() 函数,这些函数内部也封装了系统调用。

C 语言等高级编程语言并不依存于特定的操作系统,这是因为人们希望不管是Windows 操作系统还是 Linux 操作系统都能够使用相同的源代码。因此,高级编程语言的机制就是,使用独自的函数名,然后在编译的时候将其转换为系统调用的方式(也有可能是多个系统调用的组合)。也就是说,高级语言编写的应用在编译后,就转换成了利用系统调用的本地代码

不过,在高级语言中也存在直接调用系统调用的编程语言,不过,利用这种方式做成应用,移植性并不友好。

移植性:移植性指的是同样的程序在不同操作系统下运行时所花费的时间,时间越少证明移植性越好。

操作系统和高级编程语言使硬件抽象化

通过使用操作系统提供的系统调用,程序员不必直接编写控制硬件的程序,而且,通过使用高级编程语言,有时也无需考虑系统调用的存在,系统调用往往是自动触发的,操作系统和高级编程语言能够使硬件抽象化,这很了不起。

下面让我们看一个硬件抽象化的具体实例

#include <stdio.h>

void main(){

  // 打开文件
  FILE *fp = fopen("MyFile.txt","w");

  // 写入文件
  fputs("你好", fp);

  // 关闭文件
  fclose(fp);
}

上述代码使用 C 编写的程序,fputs() 是用来往文件中写入字符串的函数,fclose() 是用来关闭文件的函数。

上述应用在编译运行后,会向文件中写入 "你好" 字符串。文件是操作系统对磁盘空间的抽象化,就如同我们在 程序员需要了解的硬核知识之磁盘 这篇文章提到的一样,磁盘就如同树的年轮,磁盘的读写是以扇区为单位的,通过磁道来寻址,如果直接对硬件读写的话,那么就会变为通过向磁盘用的 I/O 指定扇区位置来对数据进行读写了。

但是,在上面代码中,扇区压根就没有出现过传递给 fopen() 函数的参数,是文件名 MyFile.txt 和指定文件写入的 w。传递给 fputs() 的参数,是往文件中写入的字符串"你好" 和 fp,传递给 fclose() 的参数,也仅仅是 fp,也就是说磁盘通过打开文件这个操作,把磁盘抽象化了,打开文件这个操作就可以说是操作硬件的指令。

下面让我们来看一下代码清单中 fp 的功能,变量 fp 中被赋予的是 fopen() 函数的返回值,该值被称为文件指针。应用打开文件后,操作系统就会自动申请分配用来管理文件读写的内存空间。内存地址可以通过 fopen() 函数的返回值获得。用 fopen() 打开文件后,接下来就是通过制定的文件指针进行操作,正因为如此,fputs() 和 fclose() 以及 fclose() 参数中都制定了文件指针。

由此我们可以得出一个结论,应用程序是通过系统调用,磁盘抽象来实现对硬盘的控制的。

Windows 操作系统的特征

Windows 操作系统是世界上用户数量最庞大的群体,作为 Windows 操作系统的资深用户,你都知道 Windows 操作系统有哪些特征吗?下面列举了一些 Windows 操作系统的特性

  • Windows 操作系统有两个版本:32位和64位
  • 通过 API 函数集成来提供系统调用
  • 提供了采用图形用户界面的用户界面
  • 通过 WYSIWYG 实现打印输出,WYSIWYG 其实就是 What You See Is What You Get ,值得是显示器上显示的图形和文本都是可以原样输出到打印机打印的。
  • 提供多任务功能,即能够同时开启多个任务
  • 提供网络功能和数据库功能
  • 通过即插即用实现设备驱动的自设定

这些是对程序员来讲比较有意义的一些特征,下面针对这些特征来进行分别的介绍

32位操作系统

这里表示的32位操作系统表示的是处理效率最高的数据大小。Windows 处理数据的基本单位是 32 位。这与最一开始在 MS-DOS 等16位操作系统不同,因为在16位操作系统中处理32位数据需要两次,而32位操作系统只需要一次就能够处理32位的数据,所以一般在 windows 上的应用,它们的最高能够处理的数据都是 32 位的。

比如,用 C 语言来处理整数数据时,有8位的 char 类型,16位的short类型,以及32位的long类型三个选项,使用位数较大的 long 类型进行处理的话,增加的只是内存以及磁盘的开销,对性能影响不大。

现在市面上大部分都是64位操作系统了,64位操作系统也是如此。

通过 API 函数集来提供系统调用

Windows 是通过名为 API 的函数集来提供系统调用的。API是联系应用程序和操作系统之间的接口,全称叫做 Application Programming Interface,应用程序接口。

当前主流的32位版 Windows API 也称为 Win32 API,之所以这样命名,是需要和不同的操作系统进行区分,比如最一开始的 16 位版的 Win16 API,和后来流行的 Win64 API

API 通过多个 DLL 文件来提供,各个 API 的实体都是用 C 语言编写的函数。所以,在 C 语言环境下,使用 API 更加容易,比如 API 所用到的 MessageBox() 函数,就被保存在了 Windows 提供的 user32.dll 这个 DLL 文件中。

提供采用了 GUI 的用户界面

GUI(Graphical User Interface) 指得就是图形用户界面,通过点击显示器中的窗口以及图标等可视化的用户界面,举个例子:Linux 操作系统就有两个版本,一种是简洁版,直接通过命令行控制硬件,还有一种是可视化版,通过光标点击图形界面来控制硬件。

通过 WYSIWYG 实现打印输出

WYSIWYG 指的是显示器上输出的内容可以直接通过打印机打印输出。在 Windows 中,显示器和打印机被认作同等的图形输出设备处理的,该功能也为 WYSIWYG 提供了条件。

借助 WYSIWYG 功能,程序员可以轻松不少。最初,为了是现在显示器中显示和在打印机中打印,就必须分别编写各自的程序,而在 Windows 中,可以借助 WYSIWYG 基本上在一个程序中就可以做到显示和打印这两个功能了。

提供多任务功能

多任务指的就是同时能够运行多个应用程序的功能,Windows 是通过时钟分割技术来实现多任务功能的。时钟分割指的是短时间间隔内,多个程序切换运行的方式。在用户看来,就好像是多个程序在同时运行,其底层是 CPU 时间切片,这也是多线程多任务的核心。

提供网络功能和数据库功能

Windows 中,网络功能是作为标准功能提供的。数据库(数据库服务器)功能有时也会在后面追加。网络功能和数据库功能虽然并不是操作系统不可或缺的,但因为它们和操作系统很接近,所以被统称为中间件而不是应用。意思是处于操作系统和应用的中间层,操作系统和中间件组合在一起,称为系统软件。应用不仅可以利用操作系统,也可以利用中间件的功能。

相对于操作系统一旦安装就不能轻易更换,中间件可以根据需要进行更换,不过,对于大部分应用来说,更换中间件的话,会造成应用也随之更换,从这个角度来说,更换中间件也不是那么容易。

通过即插即用实现设备驱动的自动设定

即插即用(Plug-and-Play)指的是新的设备连接(plug) 后就可以直接使用的机制,新设备连接计算机后,计算机就会自动安装和设定用来控制该设备的驱动程序

设备驱动是操作系统的一部分,提供了同硬件进行基本的输入输出的功能。键盘、鼠标、显示器、磁盘装置等,这些计算机中必备的硬件的设备驱动,一般都是随操作系统一起安装的。

有时 DLL 文件也会同设备驱动文件一起安装。这些 DLL 文件中存储着用来利用该新追加的硬件API,通过 API ,可以制作出运行该硬件的心应用。

程序员需要了解的硬核知识之压缩算法

原文:https://zwmst.com/2674.html

认识压缩算法

我们想必都有过压缩解压缩文件的经历,当文件太大时,我们会使用文件压缩来降低文件的占用空间。比如微信上传文件的限制是100 MB,我这里有个文件夹无法上传,但是我解压完成后的文件一定会小于 100 MB,那么我的文件就可以上传了。

此外,我们把相机拍完的照片保存到计算机上的时候,也会使用压缩算法进行文件压缩,文件压缩的格式一般是JPEG

那么什么是压缩算法呢?压缩算法又是怎么定义的呢?在认识算法之前我们需要先了解一下文件是如何存储的

文件存储

文件是将数据存储在磁盘等存储媒介的一种形式。程序文件中最基本的存储数据单位是字节。文件的大小不管是 xxxKB、xxxMB等来表示,就是因为文件是以字节 B = Byte 为单位来存储的。

文件就是字节数据的集合。用 1 字节(8 位)表示的字节数据有 256 种,用二进制表示的话就是 0000 0000 – 1111 1111 。如果文件中存储的数据是文字,那么该文件就是文本文件。如果是图形,那么该文件就是图像文件。在任何情况下,文件中的字节数都是连续存储的。

压缩算法的定义

上面介绍了文件的集合体其实就是一堆字节数据的集合,那么我们就可以来给压缩算法下一个定义。

压缩算法(compaction algorithm)指的就是数据压缩的算法,主要包括压缩和还原(解压缩)的两个步骤。

其实就是在不改变原有文件属性的前提下,降低文件字节空间和占用空间的一种算法。

根据压缩算法的定义,我们可将其分成不同的类型:

**有损和无损

无损压缩:能够无失真地从压缩后的数据重构,准确地还原原始数据。可用于对数据的准确性要求严格的场合,如可执行文件和普通文件的压缩、磁盘的压缩,也可用于多媒体数据的压缩。该方法的压缩比较小。如差分编码、RLE、Huffman编码、LZW编码、算术编码。

有损压缩:有失真,不能完全准确地恢复原始数据,重构的数据只是原始数据的一个近似。可用于对数据的准确性要求不高的场合,如多媒体数据的压缩。该方法的压缩比较大。例如预测编码、音感编码、分形压缩、小波压缩、JPEG/MPEG。

**对称性

如果编解码算法的复杂性和所需时间差不多,则为对称的编码方法,多数压缩算法都是对称的。但也有不对称的,一般是编码难而解码容易,如 Huffman 编码和分形编码。但用于密码学的编码方法则相反,是编码容易,而解码则非常难。

**帧间与帧内

在视频编码中会同时用到帧内与帧间的编码方法,帧内编码是指在一帧图像内独立完成的编码方法,同静态图像的编码,如 JPEG;而帧间编码则需要参照前后帧才能进行编解码,并在编码过程中考虑对帧之间的时间冗余的压缩,如 MPEG。

**实时性

在有些多媒体的应用场合,需要实时处理或传输数据(如现场的数字录音和录影、播放MP3/RM/VCD/DVD、视频/音频点播、网络现场直播、可视电话、视频会议),编解码一般要求延时 ≤50 ms。这就需要简单/快速/高效的算法和高速/复杂的CPU/DSP芯片。

**分级处理

有些压缩算法可以同时处理不同分辨率、不同传输速率、不同质量水平的多媒体数据,如JPEG2000、MPEG-2/4。

这些概念有些抽象,主要是为了让大家了解一下压缩算法的分类,下面我们就对具体的几种常用的压缩算法来分析一下它的特点和优劣

几种常用压缩算法的理解

RLE 算法的机制

接下来就让我们正式看一下文件的压缩机制。首先让我们来尝试对 AAAAAABBCDDEEEEEF 这 17 个半角字符的文件(文本文件)进行压缩。虽然这些文字没有什么实际意义,但是很适合用来描述 RLE 的压缩机制。

由于半角字符(其实就是英文字符)是作为 1 个字节保存在文件中的,所以上述的文件的大小就是 17 字节。如图

(这里有个问题需要读者思考一下:为什么 17 个字符的大小是 17 字节,而占用空间却很大呢? 这个问题此篇文章暂不讨论)

那么,如何才能压缩该文件呢?大家不妨也考虑一下,只要是能够使文件小于 17 字节,我们可以使用任何压缩算法。

最显而易见的一种压缩方式我觉得你已经想到了,就是把相同的字符去重化,也就是 字符 * 重复次数 的方式进行压缩。所以上面文件压缩后就会变成下面这样

从图中我们可以看出,AAAAAABBCDDEEEEEF 的17个字符成功被压缩成了 A6B2C1D2E5F1 的12个字符,也就是 12 / 17 = 70%,压缩比为 70%,压缩成功了。

像这样,把文件内容用 数据 * 重复次数 的形式来表示的压缩方法成为 RLE(Run Length Encoding, 行程长度编码) 算法。RLE 算法是一种很好的压缩方法,经常用于压缩传真的图像等。因为图像文件的本质也是字节数据的集合体,所以可以用 RLE 算法进行压缩

RLE 算法的缺点

RLE 的压缩机制比较简单,所以 RLE 算法的程序也比较容易编写,所以使用 RLE 的这种方式更能让你体会到压缩思想,但是 RLE 只针对特定序列的数据管用,下面是 RLE 算法压缩汇总

文件类型 压缩前文件大小 压缩后文件大小 压缩比率
文本文件 14862字节 29065字节 199%
图像文件 96062字节 38328字节 40%
EXE文件 24576字节 15198字节 62%

通过上表可以看出,使用 RLE 对文本文件进行压缩后的数据不但没有减小反而增大了!几乎是压缩前的两倍!因为文本字符种连续的字符并不多见。

就像上面我们探讨的这样,RLE 算法只针对连续的字节序列压缩效果比较好,假如有一连串不相同的字符该怎么压缩呢?比如说ABCDEFGHIJKLMNOPQRSTUVWXYZ,26个英文字母所占空间应该是 26 个字节,我们用 RLE 压缩算法压缩后的结果为 A1B1C1D1E1F1G1H1I1J1K1L1M1N1O1P1Q1R1S1T1U1V1W1X1Y1Z1 ,所占用 52 个字节,压缩完成后的容量没有减少反而增大了!这显然不是我们想要的结果,所以这种情况下就不能再使用 RLE 进行压缩。

哈夫曼算法和莫尔斯编码

下面我们来介绍另外一种压缩算法,即哈夫曼算法。在了解哈夫曼算法之前,你必须舍弃半角英文数字的1个字符是1个字节(8位)的数据。下面我们就来认识一下哈夫曼算法的基本思想。

文本文件是由不同类型的字符组合而成的,而且不同字符出现的次数也是不一样的。例如,在某个文本文件中,A 出现了 100次左右,Q仅仅用到了 3 次,类似这样的情况很常见。哈夫曼算法的关键就在于 多次出现的数据用小于 8 位的字节数表示,不常用的数据则可以使用超过 8 位的字节数表示。A 和 Q 都用 8 位来表示时,原文件的大小就是 100次 8 位 + 3次 8 位 = 824位,假设 A 用 2 位,Q 用 10 位来表示就是 2 100 + 3 10 = 230 位。

不过要注意一点,最终磁盘的存储都是以8位为一个字节来保存文件的。

哈夫曼算法比较复杂,在深入了解之前我们先吃点甜品,了解一下 莫尔斯编码,你一定看过美剧或者战争片的电影,在战争中的通信经常采用莫尔斯编码来传递信息,例如下面

接下来我们来讲解一下莫尔斯编码,下面是莫尔斯编码的示例,大家把 1 看作是短点(嘀),把 11 看作是长点(嗒)即可。

莫尔斯编码一般把文本中出现最高频率的字符用短编码 来表示。如表所示,假如表示短点的位是 1,表示长点的位是 11 的话,那么 E(嘀)这一数据的字符就可以用 1 来表示,C(滴答滴答)就可以用 9 位的 110101101来表示。在实际的莫尔斯编码中,如果短点的长度是 1 ,长点的长度就是 3,短点和长点的间隔就是1。这里的长度指的就是声音的长度。比如我们想用上面的 AAAAAABBCDDEEEEEF 例子来用莫尔斯编码重写,在莫尔斯曼编码中,各个字符之间需要加入表示时间间隔的符号。这里我们用 00 加以区分。

所以,AAAAAABBCDDEEEEEF 这个文本就变为了 A 6 次 + B 2次 + C 1次 + D 2次 + E 5次 + F 1次 + 字符间隔 16 = 4 位 6次 + 8 位 2次 + 9 位 1 次 + 6位 2次 + 1位 5次 + 8 位 1次 + 2位 16次 = 106位 = 14字节。

所以使用莫尔斯电码的压缩比为 14 / 17 = 82%。效率并不太突出。

用二叉树实现哈夫曼算法

刚才已经提到,莫尔斯编码是根据日常文本中各字符的出现频率来决定表示各字符的编码数据长度的。不过,在该编码体系中,对 AAAAAABBCDDEEEEEF 这种文本来说并不是效率最高的。

下面我们来看一下哈夫曼算法。哈夫曼算法是指,为各压缩对象文件分别构造最佳的编码体系,并以该编码体系为基础来进行压缩。因此,用什么样的编码(哈夫曼编码)对数据进行分割,就要由各个文件而定。用哈夫曼算法压缩过的文件中,存储着哈夫曼编码信息和压缩过的数据。

接下来,我们在对 AAAAAABBCDDEEEEEF 中的 A – F 这些字符,按照出现频率高的字符用尽量少的位数编码来表示这一原则进行整理。按照出现频率从高到低的顺序整理后,结果如下,同时也列出了编码方案。

字符 出现频率 编码(方案) 位数
A 6 0 1
E 5 1 1
B 2 10 2
D 2 11 2
C 1 100 3
F 1 101 3

在上表的编码方案中,随着出现频率的降低,字符编码信息的数据位数也在逐渐增加,从最开始的 1位、2位依次增加到3位。不过这个编码体系是存在问题的,你不知道100这个3位的编码,它的意思是用 1、0、0这三个编码来表示 E、A、A 呢?还是用10、0来表示 B、A 呢?还是用100来表示 C 呢。

而在哈夫曼算法中,通过借助哈夫曼树的构造编码体系,即使在不使用字符区分符号的情况下,也可以构建能够明确进行区分的编码体系。不过哈夫曼树的算法要比较复杂,下面是一个哈夫曼树的构造过程。

自然界树的从根开始生叶的,而哈夫曼树则是叶生枝

哈夫曼树能够提升压缩比率

使用哈夫曼树之后,出现频率越高的数据所占用的位数越少,这也是哈夫曼树的核心思想。通过上图的步骤二可以看出,枝条连接数据时,我们是从出现频率较低的数据开始的。这就意味着出现频率低的数据到达根部的枝条也越多。而枝条越多则意味着编码的位数随之增加。

接下来我们来看一下哈夫曼树的压缩比率,用上图得到的数据表示 AAAAAABBCDDEEEEEF 为 000000000000 100100 110 101101 0101010101 111,40位 = 5 字节。压缩前的数据是 17 字节,压缩后的数据竟然达到了惊人的5 字节,也就是压缩比率 = 5 / 17 = 29% 如此高的压缩率,简直是太惊艳了。

大家可以参考一下,无论哪种类型的数据,都可以用哈夫曼树作为压缩算法

文件类型 压缩前 压缩后 压缩比率
文本文件 14862字节 4119字节 28%
图像文件 96062字节 9456字节 10%
EXE文件 24576字节 4652字节 19%

可逆压缩和非可逆压缩

最后,我们来看一下图像文件的数据形式。图像文件的使用目的通常是把图像数据输出到显示器、打印机等设备上。常用的图像格式有 : BMPJPEGTIFFGIF 格式等。

  • BMP : 是使用 Windows 自带的画笔来做成的一种图像形式
  • JPEG:是数码相机等常用的一种图像数据形式
  • TIFF: 是一种通过在文件中包含"标签"就能够快速显示出数据性质的图像形式
  • GIF: 是由美国开发的一种数据形式,要求色数不超过 256个

图像文件可以使用前面介绍的 RLE 算法和哈夫曼算法,因为图像文件在多数情况下并不要求数据需要还原到和压缩之前一摸一样的状态,允许丢失一部分数据。我们把能还原到压缩前状态的压缩称为 可逆压缩,无法还原到压缩前状态的压缩称为非可逆压缩

一般来说,JPEG格式的文件是非可逆压缩,因此还原后有部分图像信息比较模糊。GIF 是可逆压缩

计算机网络运输层

原文:https://zwmst.com/2676.html

运输层位于应用层和网络层之间,是 OSI 分层体系中的第四层,同时也是网络体系结构的重要部分。运输层主要负责网络上的端到端通信。

运输层为运行在不同主机上的应用程序之间的通信起着至关重要的作用。下面我们就来一起探讨一下关于运输层的协议部分

运输层概述

计算机网络的运输层非常类似于高速公路,高速公路负责把人或者物品从一端运送到另一端,而计算机网络的运输层则负责把报文从一端运输到另一端,这个端指的就是 端系统。在计算机网络中,任意一个可以交换信息的介质都可以称为端系统,比如手机、网络媒体、电脑、运营商等。

在运输层运输报文的过程中,会遵守一定的协议规范,比如一次传输的数据限制、选择什么样的运输协议等。运输层实现了让两个互不相关的主机进行逻辑通信的功能,看起来像是让两个主机相连一样。

运输层协议是在端系统中实现的,而不是在路由器中实现的。路由只是做识别地址并转发的功能。这就比如快递员送快递一样,当然是要由地址的接受人也就是 xxx 号楼 xxx 单元 xxx 室的这个人来判断了!

TCP 如何判断是哪个端口的呢?

还记得数据包的结构吗,这里来回顾一下

数据包经过每层后,该层协议都会在数据包附上包首部,一个完整的包首部图如上所示。

在数据传输到运输层后,会为其附上 TCP 首部,首部包含着源端口号和目的端口号。

在发送端,运输层将从发送应用程序进程接收到的报文转化成运输层分组,分组在计算机网络中也称为 报文段(segment)。运输层一般会将报文段进行分割,分割成为较小的块,为每一块加上运输层首部并将其向目的地发送。

在发送过程中,可选的运输层协议(也就是交通工具) 主要有 TCPUDP ,关于这两种运输协议的选择及其特性也是我们着重探讨的重点。

TCP 和 UDP 前置知识

在 TCP/IP 协议中能够实现传输层功能的,最具代表性的就是 TCP 和 UDP。提起 TCP 和 UDP ,就得先从这两个协议的定义说起。

TCP 叫做传输控制协议(TCP,Transmission Control Protocol),通过名称可以大致知道 TCP 协议有控制传输的功能,主要体现在其可控,可控就表示着可靠,确实是这样的,TCP 为应用层提供了一种可靠的、面向连接的服务,它能够将分组可靠的传输到服务端。

UDP 叫做 用户数据报协议(UDP,User Datagram Protocol),通过名称可以知道 UDP 把重点放在了数据报上,它为应用层提供了一种无需建立连接就可以直接发送数据报的方法。

怎么计算机网络中的术语对一个数据的描述这么多啊?

在计算机网络中,在不同层之间会有不同的描述。我们上面提到会将运输层的分组称为报文段,除此之外,还会将 TCP 中的分组也称为报文段,然而将 UDP 的分组称为数据报,同时也将网络层的分组称为数据报

但是为了统一,一般在计算机网络中我们统一称 TCP 和 UDP 的报文为 报文段,这个就相当于是约定,到底如何称呼不用过多纠结啦。

套接字

在 TCP 或者 UDP 发送具体的报文信息前,需要先经过一扇 ,这个门就是套接字(socket),套接字向上连接着应用层,向下连接着网络层。在操作系统中,操作系统分别为应用和硬件提供了接口(Application Programming Interface)。而在计算机网络中,套接字同样是一种接口,它也是有接口 API 的。

使用 TCP 或 UDP 通信时,会广泛用到套接字的 API,使用这套 API 设置 IP 地址、端口号,实现数据的发送和接收。

现在我们知道了, Socket 和 TCP/IP 没有必然联系,Socket 的出现只是方便了 TCP/IP 的使用,如何方便使用呢?你可以直接使用下面 Socket API 的这些方法。

方法 描述
create() 创建一个 socket
bind() 套接字标识,一般用于绑定端口号
listen() 准备接收连接
connect() 准备充当发送者
accept() 准备作为接收者
write() 发送数据
read() 接收数据
close() 关闭连接

套接字类型

套接字的主要类型有三种,下面我们分别介绍一下

  • 数据报套接字(Datagram sockets):数据报套接字提供一种无连接的服务,而且并不能保证数据传输的可靠性。数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。数据报套接字使用UDP( User DatagramProtocol)协议进行数据的传输。由于数据报套接字不能保证数据传输的可靠性,对于有可能出现的数据丢失情况,需要在程序中做相应的处理。
  • 流套接字(Stream sockets):流套接字用于提供面向连接、可靠的数据传输服务。能够保证数据的可靠性、顺序性。流套接字之所以能够实现可靠的数据服务,原因在于其使用了传输控制协议,即 TCP(The Transmission Control Protocol)协议
  • 原始套接字(Raw sockets): 原始套接字允许直接发送和接收 IP 数据包,而无需任何特定于协议的传输层格式,原始套接字可以读写内核没有处理过的 IP 数据包。

套接字处理过程

在计算机网络中,要想实现通信,必须至少需要两个端系统,至少需要一对两个套接字才行。下面是套接字的通信过程。

  1. socket 中的 API 用于创建通信链路中的端点,创建完成后,会返回描述该套接字的套接字描述符

就像使用文件描述符来访问文件一样,套接字描述符用来访问套接字。

  1. 当应用程序具有套接字描述符后,它可以将唯一的名称绑定在套接字上,服务器必须绑定一个名称才能在网络中访问
  2. 在为服务端分配了 socket 并且将名称使用 bind 绑定到套接字上后,将会调用 listen api。listen 表示客户端愿意等待连接的意愿,listen 必须在 accept api 之前调用。
  3. 客户端应用程序在流套接字(基于 TCP)上调用 connect 发起与服务器的连接请求。
  4. 服务器应用程序使用acceptAPI 接受客户端连接请求,服务器必须先成功调用 bind 和 listen 后,再调用 accept api。
  5. 在流套接字之间建立连接后,客户端和服务器就可以发起 read/write api 调用了。
  6. 当服务器或客户端要停止操作时,就会调用 close API 释放套接字获取的所有系统资源。

虽然套接字 API 位于应用程序层和传输层之间的通信模型中,但是套接字 API 不属于通信模型。套接字 API 允许应用程序与传输层和网络层进行交互。

在往下继续聊之前,我们先播放一个小插曲,简单聊一聊 IP。

聊聊 IP

IPInternet Protocol(网际互连协议)的缩写,是 TCP/IP 体系中的网络层协议。设计 IP 的初衷主要想解决两类问题

  • 提高网络扩展性:实现大规模网络互联
  • 对应用层和链路层进行解藕,让二者独立发展。

IP 是整个 TCP/IP 协议族的核心,也是构成互联网的基础。为了实现大规模网络的互通互联,IP 更加注重适应性、简洁性和可操作性,并在可靠性做了一定的牺牲。IP 不保证分组的交付时限和可靠性,所传送分组有可能出现丢失、重复、延迟或乱序等问题。

我们知道,TCP 协议的下一层就是 IP 协议层,既然 IP 不可靠,那么如何保证数据能够准确无误地到达呢?

这就涉及到 TCP 传输机制的问题了,我们后面聊到 TCP 的时候再说。

端口号

在聊端口号前,先来聊一聊文件描述以及 socket 和端口号的关系

为了方便资源的使用,提高机器的性能、利用率和稳定性等等原因,我们的计算机都有一层软件叫做操作系统,它用于帮我们管理计算机可以使用的资源,当我们的程序要使用一个资源的时候,可以向操作系统申请,再由操作系统为我们的程序分配和管理资源。通常当我们要访问一个内核设备或文件时,程序可以调用系统函数,系统就会为我们打开设备或文件,然后返回一个文件描述符fd(或称为ID,是一个整数),我们要访问该设备或文件,只能通过该文件描述符。可以认为该编号对应着打开的文件或设备。

而当我们的程序要使用网络时,要使用到对应的操作系统内核的操作和网卡设备,所以我们可以向操作系统申请,然后系统会为我们创建一个套接字 Socket,并返回这个 Socket 的ID,以后我们的程序要使用网络资源,只要向这个 Socket 的编号 ID 操作即可。而我们的每一个网络通信的进程至少对应着一个 Socket。向 Socket 的 ID 中写数据,相当于向网络发送数据,向 Socket 中读数据,相当于接收数据。而且这些套接字都有唯一标识符——文件描述符 fd。

端口号是 16 位的非负整数,它的范围是 0 – 65535 之间,这个范围会分为三种不同的端口号段,由 Internet 号码分配机构 IANA 进行分配

  • 周知/标准端口号,它的范围是 0 – 1023
  • 注册端口号,范围是 1024 – 49151
  • 私有端口号,范围是 49152 – 6553

一台计算机上可以运行多个应用程序,当一个报文段到达主机后,应该传输给哪个应用程序呢?你怎么知道这个报文段就是传递给 HTTP 服务器而不是 SSH 服务器的呢?

是凭借端口号吗?当报文到达服务器时,是端口号来区分不同应用程序的,所以应该借助端口号来区分。

举个例子反驳一下 cxuan,假如到达服务器的两条数据都是由 80 端口发出的你该如何区分呢?或者说到达服务器的两条数据端口一样,协议不同,该如何区分呢?

所以仅凭端口号来确定某一条报文显然是不够的。

互联网上一般使用 源 IP 地址、目标 IP 地址、源端口号、目标端口号 来进行区分。如果其中的某一项不同,就被认为是不同的报文段。这些也是多路分解和多路复用 的基础。

确定端口号

在实际通信之前,需要先确定一下端口号,确定端口号的方法分为两种:

  • 标准既定的端口号

标准既定的端口号是静态分配的,每个程序都会有自己的端口号,每个端口号都有不同的用途。端口号是一个 16 比特的数,其大小在 0 – 65535 之间,0 – 1023 范围内的端口号都是动态分配的既定端口号,例如 HTTP 使用 80 端口来标识,FTP 使用 21 端口来标识,SSH 使用 22 来标识。这类端口号有一个特殊的名字,叫做 周知端口号(Well-Known Port Number)

  • 时序分配的端口号

第二种分配端口号的方式是一种动态分配法,在这种方法下,客户端应用程序可以完全不用自己设置端口号,凭借操作系统进行分配,操作系统可以为每个应用程序分配互不冲突的端口号。这种动态分配端口号的机制即使是同一个客户端发起的 TCP 连接,也能识别不同的连接。

多路复用和多路分解

我们上面聊到了在主机上的每个套接字都会分配一个端口号,当报文段到达主机时,运输层会检查报文段中的目的端口号,并将其定向到相应的套接字,然后报文段中的数据通过套接字进入其所连接的进程。下面我们来聊一下什么是多路复用和多路分解的概念。

多路复用和多路分解分为两种,即无连接的多路复用(多路分解)和面向连接的多路复用(多路分解)

无连接的多路复用和多路分解

开发人员会编写代码确定端口号是周知端口号还是时序分配的端口号。假如主机 A 中的一个 10637 端口要向主机 B 中的 45438 端口发送数据,运输层采用的是 UDP 协议,数据在应用层产生后,会在运输层中加工处理,然后在网络层将数据封装得到 IP 数据报,IP 数据包通过链路层尽力而为的交付给主机 B,然后主机 B 会检查报文段中的端口号判断是哪个套接字的,这一系列的过程如下所示

UDP 套接字就是一个二元组,二元组包含目的 IP 地址和目的端口号。

所以,如果两个 UDP 报文段有不同的源 IP 地址和/或相同的源端口号,但是具有相同的目的 IP 地址和目的端口号,那么这两个报文会通过套接字定位到相同的目的进程。

这里思考一个问题,主机 A 给主机 B 发送一个消息,为什么还需要知道源端口号呢?比如我给妹子表达出我对你有点意思的信息,妹子还需要知道这个信息是从我的哪个器官发出的吗?知道是我这个人对你有点意思不就完了?实际上是需要的,因为妹子如果要表达出她对你也有点意思,她是不是可能会亲你一口,那她得知道往哪亲吧?

这就是,在 A 到 B 的报文段中,源端口号会作为 返回地址 的一部分,即当 B 需要回发一个报文段给 A 时,B 需要从 A 到 B 中的源端口号取值,如下图所示

面向连接的多路复用与多路分解

如果说无连接的多路复用和多路分解指的是 UDP 的话,那么面向连接的多路复用与多路分解指的是 TCP 了,TCP 和 UDP 在报文结构上的差别是,UDP 是一个二元组而 TCP 是一个四元组,即源 IP 地址、目标 IP 地址、源端口号、目标端口号 ,这个我们上面也提到了。当一个 TCP 报文段从网络到达一台主机时,这个主机会根据这四个值拆解到对应的套接字上。

上图显示了面向连接的多路复用和多路分解的过程,图中主机 C 向主机 B 发起了两个 HTTP 请求,主机 A 向主机 C 发起了一个 HTTP 请求,主机 A、B、C 都有自己唯一的 IP 地址,当主机 C 发出 HTTP 请求后,主机 B 能够分解这两个 HTTP 连接,因为主机 C 发出请求的两个源端口号不同,所以对于主机 B 来说,这是两条请求,主机 B 能够进行分解。对于主机 A 和主机 C 来说,这两个主机有不同的 IP 地址,所以对于主机 B 来说,也能够进行分解。

UDP

终于,我们开始了对 UDP 协议的探讨,淦起!

UDP 的全称是 用户数据报协议(UDP,User Datagram Protocol),UDP 为应用程序提供了一种无需建立连接就可以发送封装的 IP 数据包的方法。如果应用程序开发人员选择的是 UDP 而不是 TCP 的话,那么该应用程序相当于就是和 IP 直接打交道的。

从应用程序传递过来的数据,会附加上多路复用/多路分解的源和目的端口号字段,以及其他字段,然后将形成的报文传递给网络层,网络层将运输层报文段封装到 IP 数据报中,然后尽力而为的交付给目标主机。最关键的一点就是,使用 UDP 协议在将数据报传递给目标主机时,发送方和接收方的运输层实体间是没有握手的。正因为如此,UDP 被称为是无连接的协议。

UDP 特点

UDP 协议一般作为流媒体应用、语音交流、视频会议所使用的传输层协议,我们大家都知道的 DNS 协议底层也使用了 UDP 协议,这些应用或协议之所以选择 UDP 主要是因为以下这几点

  • 速度快,采用 UDP 协议时,只要应用进程将数据传给 UDP,UDP 就会将此数据打包进 UDP 报文段并立刻传递给网络层,然后 TCP 有拥塞控制的功能,它会在发送前判断互联网的拥堵情况,如果互联网极度阻塞,那么就会抑制 TCP 的发送方。使用 UDP 的目的就是希望实时性。
  • 无须建立连接,TCP 在数据传输之前需要经过三次握手的操作,而 UDP 则无须任何准备即可进行数据传输。因此 UDP 没有建立连接的时延。如果使用 TCP 和 UDP 来比喻开发人员:TCP 就是那种凡事都要设计好,没设计不会进行开发的工程师,需要把一切因素考虑在内后再开干!所以非常靠谱;而 UDP 就是那种上来直接干干干,接到项目需求马上就开干,也不管设计,也不管技术选型,就是干,这种开发人员非常不靠谱,但是适合快速迭代开发,因为可以马上上手!
  • 无连接状态,TCP 需要在端系统中维护连接状态,连接状态包括接收和发送缓存、拥塞控制参数以及序号和确认号的参数,在 UDP 中没有这些参数,也没有发送缓存和接受缓存。因此,某些专门用于某种特定应用的服务器当应用程序运行在 UDP 上,一般能支持更多的活跃用户
  • 分组首部开销小,每个 TCP 报文段都有 20 字节的首部开销,而 UDP 仅仅只有 8 字节的开销。

这里需要注意一点,并不是所有使用 UDP 协议的应用层都是不可靠的,应用程序可以自己实现可靠的数据传输,通过增加确认和重传机制。所以使用 UDP 协议最大的特点就是速度快。

UDP 报文结构

下面来一起看一下 UDP 的报文结构,每个 UDP 报文分为 UDP 报头和 UDP 数据区两部分。报头由 4 个 16 位长(2 字节)字段组成,分别说明该报文的源端口、目的端口、报文长度和校验值。

  • 源端口号(Source Port) :这个字段占据 UDP 报文头的前 16 位,通常包含发送数据报的应用程序所使用的 UDP 端口。接收端的应用程序利用这个字段的值作为发送响应的目的地址。这个字段是可选项,有时不会设置源端口号。没有源端口号就默认为 0 ,通常用于不需要返回消息的通信中。
  • 目标端口号(Destination Port): 表示接收端端口,字段长为 16 位
  • 长度(Length): 该字段占据 16 位,表示 UDP 数据报长度,包含 UDP 报文头和 UDP 数据长度。因为 UDP 报文头长度是 8 个字节,所以这个值最小为 8,最大长度为 65535 字节。
  • 校验和(Checksum):UDP 使用校验和来保证数据安全性,UDP 的校验和也提供了差错检测功能,差错检测用于校验报文段从源到目标主机的过程中,数据的完整性是否发生了改变。发送方的 UDP 对报文段中的 16 比特字的和进行反码运算,求和时遇到的位溢出都会被忽略,比如下面这个例子,三个 16 比特的数字进行相加

​ 这些 16 比特的前两个和是

​ 然后再将上面的结果和第三个 16 比特的数进行相加

最后一次相加的位会进行溢出,溢出位 1 要被舍弃,然后进行反码运算,反码运算就是将所有的 1 变为 0 ,0 变为 1。因此 1000 0100 1001 0101 的反码就是 0111 1011 0110 1010,这就是校验和,如果在接收方,数据没有出现差错,那么全部的 4 个 16 比特的数值进行运算,同时也包括校验和,如果最后结果的值不是 1111 1111 1111 1111 的话,那么就表示传输过程中的数据出现了差错。

下面来想一个问题,为什么 UDP 会提供差错检测的功能?

这其实是一种 端到端 的设计原则,这个原则说的是要让传输中各种错误发生的概率降低到一个可以接受的水平

文件从主机A传到主机B,也就是说AB主机要通信,需要经过三个环节:首先是主机A从磁盘上读取文件并将数据分组成一个个数据包packet,,然后数据包通过连接主机A和主机B的网络传输到主机B,最后是主机B收到数据包并将数据包写入磁盘。在这个看似简单其实很复杂的过程中可能会由于某些原因而影响正常通信。比如:磁盘上文件读写错误、缓冲溢出、内存出错、网络拥挤等等这些因素都有可能导致数据包的出错或者丢失,由此可见用于通信的网络是不可靠的。

由于实现通信只要经过上述三个环节,那么我们就想是否在其中某个环节上增加一个检错纠错机制来用于对信息进行把关呢?

网络层肯定不能做这件事,因为网络层的最主要目的是增大数据传输的速率,网络层不需要考虑数据的完整性,数据的完整性和正确性交给端系统去检测就行了,因此在数据传输中,对于网络层只能要求其提供尽可能好的数据传输服务,而不可能寄希望于网络层提供数据完整性的服务。

UDP 不可靠的原因是它虽然提供差错检测的功能,但是对于差错没有恢复能力更不会有重传机制

TCP

UDP 是一种没有复杂的控制,提供无连接通信服务的一种协议,换句话说,它将部分控制部分交给应用程序去处理,自己只提供作为传输层协议最基本的功能。

而与 UDP 不同的是,同样作为传输层协议,TCP 协议要比 UDP 的功能多很多。

TCP 的全称是 Transmission Control Protocol,它被称为是一种面向连接(connection-oriented) 的协议,这是因为一个应用程序开始向另一个应用程序发送数据之前,这两个进程必须先进行握手,握手是一个逻辑连接,并不是两个主机之间进行真实的握手。

这个连接是指各种设备、线路或者网络中进行通信的两个应用程序为了相互传递消息而专有的、虚拟的通信链路,也叫做虚拟电路。

一旦主机 A 和主机 B 建立了连接,那么进行通信的应用程序只使用这个虚拟的通信线路发送和接收数据就可以保证数据的传输,TCP 协议负责控制连接的建立、断开、保持等工作。

TCP 连接是全双工服务(full-duplex service) 的,全双工是什么意思?全双工指的是主机 A 与另外一个主机 B 存在一条 TCP 连接,那么应用程数据就可以从主机 B 流向主机 A 的同时,也从主机 A 流向主机 B。

TCP 只能进行 点对点(point-to-point) 连接,那么所谓的多播,即一个主机对多个接收方发送消息的情况是不存在的,TCP 连接只能连接两个一对主机。

TCP 的连接建立需要经过三次握手,这个我们下面再说。一旦 TCP 连接建立后,主机之间就可以相互发送数据了,客户进程通过套接字传送数据流。数据一旦通过套接字后,它就由客户中运行的 TCP 协议所控制。

TCP 会将数据临时存储到连接的发送缓存(send buffer) 中,这个 send buffer 是三次握手之间设置的缓存之一,然后 TCP 在合适的时间将发送缓存中的数据发送到目标主机的接收缓存中,实际上,每一端都会有发送缓存和接收缓存,如下所示

主机之间的发送是以 报文段(segment) 进行的,那么什么是 Segement 呢?

TCP 会将要传输的数据流分为多个块(chunk),然后向每个 chunk 中添加 TCP 标头,这样就形成了一个 TCP 段也就是报文段。每一个报文段可以传输的长度是有限的,不能超过最大数据长度(Maximum Segment Size),俗称 MSS。在报文段向下传输的过程中,会经过链路层,链路层有一个 Maximum Transmission Unit ,最大传输单元 MTU, 即数据链路层上所能通过最大数据包的大小,最大传输单元通常与通信接口有关。

那么 MSS 和 MTU 有啥关系呢?

因为计算机网络是分层考虑的,这个很重要,不同层的称呼不一样,对于传输层来说,称为报文段而对网络层来说就叫做 IP 数据包,所以,MTU 可以认为是网络层能够传输的最大 IP 数据包,而 MSS(Maximum segment size)可以认为是传输层的概念,也就是 TCP 数据包每次能够传输的最大量

TCP 报文段结构

在简单聊了聊 TCP 连接后,下面我们就来聊一下 TCP 的报文段结构,如下图所示

TCP 报文段结构相比 UDP 报文结构多了很多内容。但是前两个 32 比特的字段是一样的。它们是 源端口号目标端口号,我们知道,这两个字段是用于多路复用和多路分解的。另外,和 UDP 一样,TCP 也包含校验和(checksum field) ,除此之外,TCP 报文段首部还有下面这些

  • 32 比特的序号字段(sequence number field) 和 32 比特的确认号字段(acknowledgment number field) 。这些字段被 TCP 发送方和接收方用来实现可靠的数据传输。

  • 4 比特的首部字段长度字段(header length field),这个字段指示了以 32 比特的字为单位的 TCP 首部长度。TCP 首部的长度是可变的,但是通常情况下,选项字段为空,所以 TCP 首部字段的长度是 20 字节。

  • 16 比特的 接受窗口字段(receive window field) ,这个字段用于流量控制。它用于指示接收方能够/愿意接受的字节数量

  • 可变的选项字段(options field),这个字段用于发送方和接收方协商最大报文长度,也就是 MSS 时使用

  • 6 比特的 标志字段(flag field)ACK 标志用于指示确认字段中的值是有效的,这个报文段包括一个对已被成功接收报文段的确认;RSTSYNFIN 标志用于连接的建立和关闭;CWRECE 用于拥塞控制;PSH 标志用于表示立刻将数据交给上层处理;URG 标志用来表示数据中存在需要被上层处理的 紧急 数据。紧急数据最后一个字节由 16 比特的紧急数据指针字段(urgeent data pointer field) 指出。一般情况下,PSH 和 URG 并没有使用。

TCP 的各种功能和特点都是通过 TCP 报文结构来体现的,在聊完 TCP 报文结构之后,我们下面就来聊一下 TCP 有哪些功能及其特点了。

序号、确认号实现传输可靠性

TCP 报文段首部中两个最重要的字段就是 序号确认号,这两个字段是 TCP 实现可靠性的基础,那么你肯定好奇如何实现可靠性呢?要了解这一点,首先我们得先知道这两个字段里面存了哪些内容吧?

一个报文段的序号就是数据流的字节编号 。因为 TCP 会把数据流分割成为一段一段的字节流,因为字节流本身是有序的,所以每一段的字节编号就是标示是哪一段的字节流。比如,主机 A 要给主机 B 发送一条数据。数据经过应用层产生后会有一串数据流,数据流会经过 TCP 分割,分割的依据就是 MSS,假设数据是 10000 字节,MSS 是 2000 字节,那么 TCP 就会把数据拆分成 0 – 1999 , 2000 – 3999 的段,依次类推。

所以,第一个数据 0 – 1999 的首字节编号就是 0 ,2000 – 3999 的首字节编号就是 2000 。。。。。。

然后,每个序号都会被填入 TCP 报文段首部的序号字段中。

至于确认号的话,会比序号要稍微麻烦一些。这里我们先拓展下几种通信模型。

  • 单工通信:单工数据传输只支持数据在一个方向上传输;在同一时间只有一方能接受或发送信息,不能实现双向通信,比如广播、电视等。
  • 双工通信是一种点对点系统,由两个或者多个在两个方向上相互通信的连接方或者设备组成。双工通信模型有两种:*全双工(FDX)和半双工(HDX)
    • 全双工:在全双工系统中,连接双方可以相互通信,一个最常见的例子就是电话通信。全双工通信是两个单工通信方式的结合,它要求发送设备和接收设备都有独立的接收和发送能力。
    • 半双工:在半双工系统中,连接双方可以彼此通信,但不能同时通信,比如对讲机,只有把按钮按住的人才能够讲话,只有一个人讲完话后另外一个人才能讲话。

单工、半双工、全双工通信如下图所示

TCP 是一种全双工的通信协议,因此主机 A 在向主机 B 发送消息的过程中,也在接受来自主机 B 的数据。主机 A 填充进报文段的确认号是期望从主机 B 收到的下一字节的序号。稍微有点绕,我们来举个例子看一下。比如主机 A 收到了来自主机 B 发送的编号为 0 – 999 字节的报文段,这个报文段会写入序号中,随后主机 A 期望能够从主机 B 收到 1000 – 剩下的报文段,因此,主机 A 发送到主机 B 的报文段中,它的确认号就是 1000 。

累积确认

这里再举出一个例子,比如主机 A 在发送 0 – 999 报文段后,期望能够接受到 1000 之后的报文段,但是主机 B 却给主机 A 发送了一个 1500 之后的报文段,那么主机 A 是否还会继续进行等待呢?

答案显然是会的,因为 TCP 只会确认流中至第一个丢失字节为止的字节,因为 1500 虽然属于 1000 之后的字节,但是主机 B 没有给主机 A 发送 1000 – 1499 之间的字节,所以主机 A 会继续等待。

在了解完序号和确认号之后,我们下面来聊一下 TCP 的发送过程。下面是一个正常的发送过程

TCP 通过肯定的确认应答(ACK) 来实现可靠的数据传输,当主机 A将数据发出之后会等待主机 B 的响应。如果有确认应答(ACK),说明数据已经成功到达对端。反之,则数据很可能会丢失。

如下图所示,如果在一定时间内主机 A 没有等到确认应答,则认为主机 B 发送的报文段已经丢失,并进行重发。

主机 A 给主机 B 的响应可能由于网络抖动等原因无法到达,那么在经过特定的时间间隔后,主机 A 将重新发送报文段。

主机 A 没有收到主机 B 的响应还可能是因为主机 B 在发送给主机 A 的过程中丢失。

如上图所示,由主机 B 返回的确认应答,由于网络拥堵等原因在传送的过程中丢失,并没有到达主机 A。主机 A 会等待一段时间,如果在这段时间内主机 A 仍没有等到主机 B 的响应,那么主机 A 会重新发送报文段。

那么现在就存在一个问题,如果主机 A 给主机 B 发送了一个报文段后,主机 B 接受到报文段发送响应,此刻由于网络原因,这个报文段并未到达,等到一段时间后主机 A 重新发送报文段,然后此时主机 B 发送的响应在主机 A 第二次发送后失序到达主机 A,那么主机 A 应该如何处理呢?

TCP RFC 并未为此做任何规定,也就是说,我们可以自己决定如何处理失序到达的报文段。一般处理方式有两种

  • 接收方立刻丢弃失序的报文段
  • 接收方接受时许到达的报文段,并等待后续的报文段

一般来说通常采取的做法是第二种。

传输控制

利用窗口控制提高速度

前面我们介绍了 TCP 是以数据段的形式进行发送,如果经过一段时间内主机 A 等不到主机 B 的响应,主机 A 就会重新发送报文段,接受到主机 B 的响应,再会继续发送后面的报文段,我们现在看到,这一问一答的形式还存在许多条件,比如响应未收到、等待响应等,那么对崇尚性能的互联网来说,这种形式的性能应该不会很高。

那么如何提升性能呢?

为了解决这个问题,TCP 引入了 窗口 这个概念,即使在往返时间较长、频次很多的情况下,它也能控制网络性能的下降,听起来很牛批,那它是如何实现的呢?

如下图所示

我们之前每次请求发送都是以报文段的形式进行的,引入窗口后,每次请求都可以发送多个报文段,也就是说一个窗口可以发送多个报文段。窗口大小就是指无需等待确认应答就可以继续发送报文段的最大值。

在这个窗口机制中,大量使用了 缓冲区 ,通过对多个段同时进行确认应答的功能。

如下图所示,发送报文段中高亮部分即是我们提到的窗口,在窗口内,即是没有收到确认应答也可以把请求发送出去。不过,在整个窗口的确认应答没有到达之前,如果部分报文段丢失,那么主机 A 将仍会重传。为此,主机 A 需要设置缓存来保留这些需要重传的报文段,直到收到他们的确认应答。

在滑动窗口以外的部分是尚未发送的报文段和已经接受到的报文段,如果报文段已经收到确认则不可进行重发,此时报文段就可以从缓冲区中清除。

在收到确认的情况下,会将窗口滑动到确认应答中确认号的位置,如上图所示,这样可以顺序的将多个段同时发送,用以提高通信性能,这种窗口也叫做 滑动窗口(Sliding window)

窗口控制和重发

报文段的发送和接收,必然伴随着报文段的丢失和重发,窗口也是同样如此,如果在窗口中报文段发送过程中出现丢失怎么办?

首先我们先考虑确认应答没有返回的情况。在这种情况下,主机 A 发送的报文段到达主机 B,是不需要再进行重发的。这和单个报文段的发送不一样,如果发送单个报文段,即使确认应答没有返回,也要进行重发

窗口在一定程度上比较大时,即使有少部分确认应答的丢失,也不会重新发送报文段。

我们知道,如果在某个情况下由于发送的报文段丢失,导致接受主机未收到请求,或者主机返回的响应未到达客户端的话,会经过一段时间重传报文。那么在使用窗口的情况下,报文段丢失会怎么样呢?

如下图所示,报文段 0 – 999 丢失后,但是主机 A 并不会等待,主机 A 会继续发送余下的报文段,主机 B 发送的确认应答却一直是 1000,同一个确认号的应答报文会被持续不断的返回,如果发送端主机在连续 3 次收到同一个确认应答后,就会将其所对应的数据重发,这种机制要比之前提到的超时重发更加高效,这种机制也被称为 高速重发控制。这种重发的确认应答也被称为 冗余 ACK(响应)

主机 B 在没有接收到自己期望序列号的报文段时,会对之前收到的数据进行确认应答。发送端则一旦收到某个确认应答后,又连续三次收到同样的确认应答,那么就会认为报文段已经丢失。需要进行重发。使用这种机制可以提供更为快速的重发服务

流量控制

前面聊的是传输控制,下面 cxuan 再和你聊一下 流量控制。我们知道,在每个 TCP 连接的一侧主机都会有一个 socket 缓冲区,缓冲区会为每个连接设置接收缓存和发送缓存,当 TCP 建立连接后,从应用程序产生的数据就会到达接收方的接收缓冲区中,接收方的应用程序并不一定会马上读区缓冲区的数据,它需要等待操作系统分配时间片。如果此时发送方的应用程序产生数据过快,而接收方读取接受缓冲区的数据相对较慢的话,那么接收方中缓冲区的数据将会溢出

但是还好,TCP 有 流量控制服务(flow-control service) 用于消除缓冲区溢出的情况。流量控制是一个速度匹配服务,即发送方的发送速率与接受方应用程序的读取速率相匹配。

TCP 通过使用一个 接收窗口(receive window) 的变量来提供流量控制。接受窗口会给发送方一个指示到底还有多少可用的缓存空间。发送端会根据接收端的实际接受能力来控制发送的数据量。

接收端主机向发送端主机通知自己可以接收数据的大小,发送端会发送不超过这个限度的数据,这个大小限度就是窗口大小,还记得 TCP 的首部么,有一个接收窗口,我们上面聊的时候说这个字段用于流量控制。它用于指示接收方能够/愿意接受的字节数量。

那么只知道这个字段用于流量控制,那么如何控制呢?

发送端主机会定期发送一个窗口探测包,这个包用于探测接收端主机是否还能够接受数据,当接收端的缓冲区一旦面临数据溢出的风险时,窗口大小的值也随之被设置为一个更小的值通知发送端,从而控制数据发送量。

下面是一个流量控制示意图

发送端主机根据接收端主机的窗口大小进行流量控制。由此也可以防止发送端主机一次发送过大数据导致接收端主机无法处理。

如上图所示,当主机 B 收到报文段 2000 – 2999 之后缓冲区已满,不得不暂时停止接收数据。然后主机 A 发送窗口探测包,窗口探测包非常小仅仅一个字节。然后主机 B 更新缓冲区接收窗口大小并发送窗口更新通知给主机 A,然后主机 A 再继续发送报文段。

在上面的发送过程中,窗口更新通知可能会丢失,一旦丢失发送端就不会发送数据,所以窗口探测包会随机发送,以避免这种情况发生。

连接管理

在继续介绍下面有意思的特性之前,我们先来把关注点放在 TCP 的连接管理上,因为没有 TCP 连接,也就没有后续的一系列 TCP 特性什么事儿了。假设运行在一台主机上的进程想要和另一台主机上的进程建立一条 TCP 连接,那么客户中的 TCP 会使用下面这些步骤与服务器中的 TCP 建立连接。

  • 首先,客户端首先向服务器发送一个特殊的 TCP 报文段。这个报文段首部不包含应用层数据,但是在报文段的首部中有一个 SYN 标志位 被置为 1。因此,这个特殊的报文段也可以叫做 SYN 报文段。然后,客户端随机选择一个初始序列号(client_isn) ,并将此数字放入初始 TCP SYN 段的序列号字段中,SYN 段又被封装在 IP 数据段中发送给服务器。

  • 一旦包含 IP 数据段到达服务器后,服务端会从 IP 数据段中提取 TCP SYN 段,将 TCP 缓冲区和变量分配给连接,然后给客户端发送一个连接所允许的报文段。这个连接所允许的报文段也不包括任何应用层数据。然而,它却包含了三个非常重要的信息。

    这些缓冲区和变量的分配使 TCP 容易受到称为 SYN 泛洪的拒绝服务攻击。

    • 首先,SYN 比特被置为 1 。
    • 然后,TCP 报文段的首部确认号被设置为 client_isn + 1
    • 最后,服务器选择自己的初始序号(server_isn),并将其放置到 TCP 报文段首部的序号字段中。

    如果用大白话解释下就是,我收到了你发起建立连接的 SYN 报文段,这个报文段具有首部字段 client_isn。我同意建立该连接,我自己的初始序号是 server_isn。这个允许连接的报文段被称为 SYNACK 报文段

  • 第三步,在收到 SYNACK 报文段后,客户端也要为该连接分配缓冲区和变量。客户端主机向服务器发送另外一个报文段,最后一个报文段对服务器发送的响应报文做了确认,确认的标准是客户端发送的数据段中确认号为 server_isn + 1,因为连接已经建立,所以 SYN 比特被置为 0 。以上就是 TCP 建立连接的三次数据段发送过程,也被称为 三次握手

一旦完成这三个步骤,客户和服务器主机就可以相互发送报文段了,在以后的每一个报文段中,SYN 比特都被置为 0 ,整个过程描述如下图所示

在客户端主机和服务端主机建立连接后,参与一条 TCP 连接的两个进程中的任何一个都能终止 TCP 连接。连接结束后,主机中的缓存和变量将会被释放。假设客户端主机想要终止 TCP 连接,它会经历如下过程

客户应用进程发出一个关闭命令,客户 TCP 向服务器进程发送一个特殊的 TCP 报文段,这个特殊的报文段的首部标志 FIN 被设置为 1 。当服务器收到这个报文段后,就会向发送方发送一个确认报文段。然后,服务器发送它自己的终止报文段,FIN 位被设置为 1 。客户端对这个终止报文段进行确认。此时,在两台主机上用于该连接的所有资源都被释放了,如下图所示

在一个 TCP 连接的生命周期内,运行在每台主机中的 TCP 协议都会在各种 TCP 状态(TCP State) 之间进行变化,TCP 的状态主要有 LISTEN、SYN-SEND、SYN-RECEIVED、ESTABLISHED、FIN-WAIT-1、FIN-WAIT-2、CLOSE-WAIT、CLOSING、LAST-ACK、TIME-WAIT 和 CLOSED 。这些状态的解释如下

  • LISTEN: 表示等待任何来自远程 TCP 和端口的连接请求。
  • SYN-SEND: 表示发送连接请求后等待匹配的连接请求。
  • SYN-RECEIVED: 表示已接收并发送连接请求后等待连接确认,也就是 TCP 三次握手中第二步后服务端的状态
  • ESTABLISHED: 表示已经连接已经建立,可以将应用数据发送给其他主机

上面这四种状态是 TCP 三次握手所涉及的。

  • FIN-WAIT-1: 表示等待来自远程 TCP 的连接终止请求,或者等待先前发送的连接终止请求的确认。

  • FIN-WAIT-2: 表示等待来自远程 TCP 的连接终止请求。

  • CLOSE-WAIT: 表示等待本地用户的连接终止请求。

  • CLOSING: 表示等待来自远程 TCP 的连接终止请求确认。

  • LAST-ACK: 表示等待先前发送给远程 TCP 的连接终止请求的确认(包括对它的连接终止请求的确认)。

  • TIME-WAIT: 表示等待足够的时间以确保远程 TCP 收到其连接终止请求的确认。

  • CLOSED: 表示连接已经关闭,无连接状态。

上面 7 种状态是 TCP 四次挥手,也就是断开链接所设计的。

TCP 的连接状态会进行各种切换,这些 TCP 连接的切换是根据事件进行的,这些事件由用户调用:OPEN、SEND、RECEIVE、CLOSE、ABORT 和 STATUS。涉及到 TCP 报文段的标志有 SYN、ACK、RST 和 FIN ,当然,还有超时。

我们下面加上 TCP 连接状态后,再来看一下三次握手和四次挥手的过程。

三次握手建立连接

下图画出了 TCP 连接建立的过程。假设图中左端是客户端主机,右端是服务端主机,一开始,两端都处于CLOSED(关闭)状态。

  1. 服务端进程准备好接收来自外部的 TCP 连接,一般情况下是调用 bind、listen、socket 三个函数完成。这种打开方式被认为是 被动打开(passive open)。然后服务端进程处于 LISTEN 状态,等待客户端连接请求。
  2. 客户端通过 connect 发起主动打开(active open),向服务器发出连接请求,请求中首部同步位 SYN = 1,同时选择一个初始序号 sequence ,简写 seq = x。SYN 报文段不允许携带数据,只消耗一个序号。此时,客户端进入 SYN-SEND 状态。
  3. 服务器收到客户端连接后,,需要确认客户端的报文段。在确认报文段中,把 SYN 和 ACK 位都置为 1 。确认号是 ack = x + 1,同时也为自己选择一个初始序号 seq = y。请注意,这个报文段也不能携带数据,但同样要消耗掉一个序号。此时,TCP 服务器进入 SYN-RECEIVED(同步收到) 状态。
  4. 客户端在收到服务器发出的响应后,还需要给出确认连接。确认连接中的 ACK 置为 1 ,序号为 seq = x + 1,确认号为 ack = y + 1。TCP 规定,这个报文段可以携带数据也可以不携带数据,如果不携带数据,那么下一个数据报文段的序号仍是 seq = x + 1。这时,客户端进入 ESTABLISHED (已连接) 状态
  5. 服务器收到客户的确认后,也进入 ESTABLISHED 状态。

TCP 建立一个连接需要三个报文段,释放一个连接却需要四个报文段。

四次挥手

数据传输结束后,通信的双方可以释放连接。数据传输结束后的客户端主机和服务端主机都处于 ESTABLISHED 状态,然后进入释放连接的过程。

TCP 断开连接需要历经的过程如下

  1. 客户端应用程序发出释放连接的报文段,并停止发送数据,主动关闭 TCP 连接。客户端主机发送释放连接的报文段,报文段中首部 FIN 位置为 1 ,不包含数据,序列号位 seq = u,此时客户端主机进入 FIN-WAIT-1(终止等待 1) 阶段。

  2. 服务器主机接受到客户端发出的报文段后,即发出确认应答报文,确认应答报文中 ACK = 1,生成自己的序号位 seq = v,ack = u + 1,然后服务器主机就进入 CLOSE-WAIT(关闭等待) 状态,这个时候客户端主机 -> 服务器主机这条方向的连接就释放了,客户端主机没有数据需要发送,此时服务器主机是一种半连接的状态,但是服务器主机仍然可以发送数据。

  3. 客户端主机收到服务端主机的确认应答后,即进入 FIN-WAIT-2(终止等待2) 的状态。等待客户端发出连接释放的报文段。

  4. 当服务器主机没有数据发送后,应用进程就会通知 TCP 释放连接。这时服务端主机会发出断开连接的报文段,报文段中 ACK = 1,序列号 seq = w,因为在这之间可能已经发送了一些数据,所以 seq 不一定等于 v + 1。ack = u + 1,在发送完断开请求的报文后,服务端主机就进入了 LAST-ACK(最后确认)的阶段。

  5. 客户端收到服务端的断开连接请求后,客户端需要作出响应,客户端发出断开连接的报文段,在报文段中,ACK = 1, 序列号 seq = u + 1,因为客户端从连接开始断开后就没有再发送数据,ack = w + 1,然后进入到 TIME-WAIT(时间等待) 状态,请注意,这个时候 TCP 连接还没有释放。必须经过时间等待的设置,也就是 2MSL 后,客户端才会进入 CLOSED 状态,时间 MSL 叫做最长报文段寿命(Maximum Segment Lifetime)

  6. 服务端主要收到了客户端的断开连接确认后,就会进入 CLOSED 状态。因为服务端结束 TCP 连接时间要比客户端早,而整个连接断开过程需要发送四个报文段,因此释放连接的过程也被称为四次挥手。

什么是 TIME-WAIT

我上面只是简单提到了一下 TIME-WAIT 状态和 2MSL 是啥,下面来聊一下这两个概念。

MSL 是 TCP 报文段可以存活或者驻留在网络中的最长时间。RFC 793 定义了 MSL 的时间是两分钟,但是具体的实现还要根据程序员来指定,一些实现采用了 30 秒的这个最大存活时间。

那么为什么要等待 2MSL 呢?

主要是因为两个理由

  • 为了保证最后一个响应能够到达服务器,因为在计算机网络中,最后一个 ACK 报文段可能会丢失,从而致使客户端一直处于 LAST-ACK 状态等待客户端响应。这时候服务器会重传一次 FINACK 断开连接报文,客户端接收后再重新确认,重启定时器。如果客户端不是 2MSL ,在客户端发送 ACK 后直接关闭的话,如果报文丢失,那么双方主机会无法进入 CLOSED 状态。
  • 还可以防止已失效的报文段。客户端在发送最后一个 ACK 之后,再经过经过 2MSL,就可以使本链接持续时间内所产生的所有报文段都从网络中消失。从保证在关闭连接后不会有还在网络中滞留的报文段去骚扰服务器。

这里注意一点:在服务器发送了 FIN-ACK 之后,会立即启动超时重传计时器。客户端在发送最后一个 ACK 之后会立即启动时间等待计时器。

说好的 RST 呢

说好的 RSTSYNFIN 标志用于连接的建立和关闭,那么 SYN 和 FIN 都现身了,那 RST 呢?也是啊,我们上面探讨的都是一种理想的情况,就是客户端服务器双方都会接受传输报文段的情况,还有一种情况是当主机收到 TCP 报文段后,其 IP 和端口号不匹配的情况。假设客户端主机发送一个请求,而服务器主机经过 IP 和端口号的判断后发现不是给这个服务器的,那么服务器就会发出一个 RST 特殊报文段给客户端。

因此,当服务端发送一个 RST 特殊报文段给客户端的时候,它就会告诉客户端没有匹配的套接字连接,请不要再继续发送了

上面探讨的是 TCP 的情况,那么 UDP 呢?

使用 UDP 作为传输协议后,如果套接字不匹配的话,UDP 主机就会发送一个特殊的 ICMP 数据报。

SYN 洪泛攻击

下面我们来讨论一下什么是 SYN 洪泛攻击

我们在 TCP 的三次握手中已经看到,服务器为了响应一个收到的 SYN,分配并初始化变量连接和缓存,然后服务器发送一个 SYNACK 作为响应,然后等待来自于客户端的 ACK 报文。如果客户端不发送 ACK 来完成最后一步的话,那么这个连接就处在一个挂起的状态,也就是半连接状态。

攻击者通常在这种情况下发送大量的 TCP SYN 报文段,服务端继续响应,但是每个连接都完不成三次握手的步骤。随着 SYN 的不断增加,服务器会不断的为这些半开连接分配资源,导致服务器的连接最终被消耗殆尽。这种攻击也是属于 Dos 攻击的一种。

抵御这种攻击的方式是使用 SYN cookie ,下面是它的工作流程介绍

  • 当服务器收到一个 SYN 报文段时,它并不知道这个报文段是来自哪里,是来自攻击者主机还是客户端主机(虽然攻击者也是客户端,不过这么说更便于区分) 。因此服务器不会为报文段生成一个半开连接。与此相反,服务器生成一个初始的 TCP 序列号,这个序列号是 SYN 报文段的源和目的 IP 地址与端口号这个四元组构造的一个复杂的散列函数,这个散列函数生成的 TCP 序列号就是 SYN Cookie,用于缓存 SYN 请求。然后,服务器会发送带着 SYN Cookie 的 SYNACK 分组。有一点需要注意的是,服务器不会记忆这个 Cookie 或 SYN 的其他状态信息
  • 如果客户端不是攻击者的话,它就会返回一个 ACK 报文段。当服务器收到这个 ACK 后,需要验证这个 ACK 与 SYN 发送的是否相同,验证的标准就是确认字段中的确认号和序列号,源和目的 IP 地址与端口号以及和散列函数的是否一致,散列函数的结果 + 1 是否和 SYNACK 中的确认值相同。(大致是这样,说的不对还请读者纠正) 。如果有兴趣读者可以自行深入了解。如果是合法的,服务器就会生成一个具有套接字的全开连接。
  • 如果客户端没有返回 ACK,即认为是攻击者,那么这样也没关系,服务器没有收到 ACK,不会分配变量和缓存资源,不会对服务器产生危害。

拥塞控制

有了 TCP 的窗口控制后,使计算机网络中两个主机之间不再是以单个数据段的形式发送了,而是能够连续发送大量的数据包。然而,大量数据包同时也伴随着其他问题,比如网络负载、网络拥堵等问题。TCP 为了防止这类问题的出现,使用了 拥塞控制 机制,拥塞控制机制会在面临网络拥塞时遏制发送方的数据发送。

拥塞控制主要有两种方法

  • 端到端的拥塞控制: 因为网络层没有为运输层拥塞控制提供显示支持。所以即使网络中存在拥塞情况,端系统也要通过对网络行为的观察来推断。TCP 就是使用了端到端的拥塞控制方式。IP 层不会向端系统提供有关网络拥塞的反馈信息。那么 TCP 如何推断网络拥塞呢?如果超时或者三次冗余确认就被认为是网络拥塞,TCP 会减小窗口的大小,或者增加往返时延来避免
  • 网络辅助的拥塞控制: 在网络辅助的拥塞控制中,路由器会向发送方提供关于网络中拥塞状态的反馈。这种反馈信息就是一个比特信息,它指示链路中的拥塞情况。

下图描述了这两种拥塞控制方式

TCP 拥塞控制

如果你看到这里,那我就暂定认为你了解了 TCP 实现可靠性的基础了,那就是使用序号和确认号。除此之外,另外一个实现 TCP 可靠性基础的就是 TCP 的拥塞控制。如果说

TCP 所采用的方法是让每一个发送方根据所感知到的网络的拥塞程度来限制发出报文段的速率,如果 TCP 发送方感知到没有什么拥塞,则 TCP 发送方会增加发送速率;如果发送方感知沿着路径有阻塞,那么发送方就会降低发送速率。

但是这种方法有三个问题

  1. TCP 发送方如何限制它向其他连接发送报文段的速率呢?
  2. 一个 TCP 发送方是如何感知到网络拥塞的呢?
  3. 当发送方感知到端到端的拥塞时,采用何种算法来改变其发送速率呢?

我们先来探讨一下第一个问题,TCP 发送方如何限制它向其他连接发送报文段的速率呢

我们知道 TCP 是由接收缓存、发送缓存和变量(LastByteRead, rwnd,等)组成。发送方的 TCP 拥塞控制机制会跟踪一个变量,即 拥塞窗口(congestion window) 的变量,拥塞窗口表示为 cwnd,用于限制 TCP 在接收到 ACK 之前可以发送到网络的数据量。而接收窗口(rwnd) 是一个用于告诉接收方能够接受的数据量。

一般来说,发送方未确认的数据量不得超过 cwnd 和 rwnd 的最小值,也就是

*LastByteSent – LastByteAcked <= min(cwnd,rwnd)

由于每个数据包的往返时间是 RTT,我们假设接收端有足够的缓存空间用于接收数据,我们就不用考虑 rwnd 了,只专注于 cwnd,那么,该发送方的发送速率大概是 cwnd/RTT 字节/秒 。通过调节 cwnd,发送方因此能调整它向连接发送数据的速率。

一个 TCP 发送方是如何感知到网络拥塞的呢

这个我们上面讨论过,是 TCP 根据超时或者 3 个冗余 ACK 来感知的。

当发送方感知到端到端的拥塞时,采用何种算法来改变其发送速率呢 ?

这个问题比较复杂,且容我娓娓道来,一般来说,TCP 会遵循下面这几种指导性原则

  • 如果在报文段发送过程中丢失,那就意味着网络拥堵,此时需要适当降低 TCP 发送方的速率。
  • 一个确认报文段指示发送方正在向接收方传递报文段,因此,当对先前未确认报文段的确认到达时,能够增加发送方的速率。为啥呢?因为未确认的报文段到达接收方也就表示着网络不拥堵,能够顺利到达,因此发送方拥塞窗口长度会变大,所以发送速率会变快
  • 带宽探测,带宽探测说的是 TCP 可以通过调节传输速率来增加/减小 ACK 到达的次数,如果出现丢包事件,就会减小传输速率。因此,为了探测拥塞开始出现的频率, TCP 发送方应该增加它的传输速率。然后慢慢使传输速率降低,进而再次开始探测,看看拥塞开始速率是否发生了变化。

在了解完 TCP 拥塞控制后,下面我们就该聊一下 TCP 的 拥塞控制算法(TCP congestion control algorithm) 了。TCP 拥塞控制算法主要包含三个部分:慢启动、拥塞避免、快速恢复,下面我们依次来看一下

慢启动

当一条 TCP 开始建立连接时,cwnd 的值就会初始化为一个 MSS 的较小值。这就使得初始发送速率大概是 MSS/RTT 字节/秒 ,比如要传输 1000 字节的数据,RTT 为 200 ms ,那么得到的初始发送速率大概是 40 kb/s 。实际情况下可用带宽要比这个 MSS/RTT 大得多,因此 TCP 想要找到最佳的发送速率,可以通过 慢启动(slow-start) 的方式,在慢启动的方式中,cwnd 的值会初始化为 1 个 MSS,并且每次传输报文确认后就会增加一个 MSS,cwnd 的值会变为 2 个 MSS,这两个报文段都传输成功后每个报文段 + 1,会变为 4 个 MSS,依此类推,每成功一次 cwnd 的值就会翻倍。如下图所示

发送速率不可能会一直增长,增长总有结束的时候,那么何时结束呢?慢启动通常会使用下面这几种方式结束发送速率的增长。

  • 如果在慢启动的发送过程出现丢包的情况,那么 TCP 会将发送方的 cwnd 设置为 1 并重新开始慢启动的过程,此时会引入一个 ssthresh(慢启动阈值) 的概念,它的初始值就是产生丢包的 cwnd 的值 / 2,即当检测到拥塞时,ssthresh 的值就是窗口值的一半。

  • 第二种方式是直接和 ssthresh 的值相关联,因为当检测到拥塞时,ssthresh 的值就是窗口值的一半,那么当 cwnd > ssthresh 时,每次翻番都可能会出现丢包,所以最好的方式就是 cwnd 的值 = ssthresh ,这样 TCP 就会转为拥塞控制模式,结束慢启动。

  • 慢启动结束的最后一种方式就是如果检测到 3 个冗余 ACK,TCP 就会执行一种快速重传并进入恢复状态。

拥塞避免

当 TCP 进入拥塞控制状态后,cwnd 的值就等于拥塞时值的一半,也就是 ssthresh 的值。所以,无法每次报文段到达后都将 cwnd 的值再翻倍。而是采用了一种相对保守的方式,每次传输完成后只将 cwnd 的值增加一个 MSS,比如收到了 10 个报文段的确认,但是 cwnd 的值只增加一个 MSS。这是一种线性增长模式,它也会有增长逾值,它的增长逾值和慢启动一样,如果出现丢包,那么 cwnd 的值就是一个 MSS,ssthresh 的值就等于 cwnd 的一半;或者是收到 3 个冗余的 ACK 响应也能停止 MSS 增长。如果 TCP 将 cwnd 的值减半后,仍然会收到 3 个冗余 ACK,那么就会将 ssthresh 的值记录为 cwnd 值的一半,进入 快速恢复 状态。

快速恢复

在快速恢复中,对于使 TCP 进入快速恢复状态缺失的报文段,对于每个收到的冗余 ACK,cwnd 的值都会增加一个 MSS 。当对丢失报文段的一个 ACK 到达时,TCP 在降低 cwnd 后进入拥塞避免状态。如果在拥塞控制状态后出现超时,那么就会迁移到慢启动状态,cwnd 的值被设置为 1 个 MSS,ssthresh 的值设置为 cwnd 的一半。

ICMP协议

原文:https://zwmst.com/2678.html

我们之前的文章中了解过 TCP/IP 协议,我那时候码了一句

原文链接见如下:

TCP/IP 基础知识总结

下面我们就来真正认识一下 ICMP 协议

什么是 ICMP

ICMP 的全称是 Internet Control Message Protocol(互联网控制协议),它是一种互联网套件,它用于IP 协议中发送控制消息。也就是说,ICMP 是依靠 IP 协议来完成信息发送的,它是 IP 的主要部分,但是从体系结构上来讲,它位于 IP 之上,因为 ICMP 报文是承载在 IP 分组中的,就和 TCP 与 UDP 报文段作为 IP 有效载荷被承载那样。这也就是说,当主机收到一个指明上层协议为 ICMP 的 IP 数据报时,它会分解出该数据报的内容给 ICMP,就像分解数据报的内容给 TCP 和 UDP 一样。

ICMP 协议和 TCP、UDP 等协议不同,它不用于传输数据,只是用来发送消息。因为 IP 协议现在有两类版本:IPv4 和 IPv6 ,所以 ICMP 也有两个版本:ICMPv4 和 ICMPv6

ICMP 的主要功能

对于 ICMP 的功能,主要分为两个

  • ICMP 的第一个功能是确认 IP 包是否能够成功到达目标地址,当两个设备通过互联网相连时,任意一个设备发送给另一个设备的 IP 包如果没有到达,就会生成 ICMP 数据包发送给设备共享。
  • ICMP 的第二个功能是进行网络诊断,经常使用 ICMP 数据包的两个终端程序是 pingtraceroute,traceroute 程序用于显示两台互联网设备之间可能的路径并测量数据包在 IP 网络上的时延。ping 程序是 traceroute 的简化版本,我们经常使用 ping 命令来测试两台设备之间是否互联,ping 通常用来测试两台主机之间的连接速度,并准确报告数据包到达目的地并返回后所花费的时间。

现在我们知道了,如果在 IP 通信过程中由于某个 IP 包由于某种原因未能到达目标主机,那么这个具体的原因将由 ICMP 进行通知,下面是一个 ICMP 的通知示意图

上面我们只是画出了路由器 2 给主机 A 发送了一个 ICMP 数据包,而没有画出具体的通知类型,但实际情况是,上面发送的是目标不可达类型(Destination unreachable),ICMP 也是具有不同的通知类型的,下面我们汇总了 ICMP 数据包的具体通知类型。

上表显示的 ICMP 通知类型主要分为两类:有关 IP 数据报传递的 ICMP 报文,这类报文也叫做差错报文(error message),以及有关信息采集和配置的 ICMP 报文,这类报文也被称为查询 query 或者信息类报文。

信息类报文包括回送请求和回送应答(类型 8 和 类型 0 ),路由器公告和路由器请求(类型 9 和 类型 0 )。最常见的差错报文类型包括目标不可达(类型 3 )、重定向(类型 5)、超时(类型 11)。

ICMP 在 IPv4 和 IPv6 的封装

我们知道,ICMP 是承载在 IP 内部的,而且 IPv4 和 IPv6 的封装位置不同:

ICMP 在 IPv4 协议中的封装

ICMP 在 IPv6 协议中的封装

上面两张图显示了 ICMPV4 和 ICMPv6 的报文格式。开头的 4 个字节在所有的报文中都是一样的。但是其余部分在不同的报文中却不一样。

ICMP 头部包含了整个 ICMP 数据段的校验和,具体格式如下

所有的 ICMP 报文都以 8 位的类型(Type)代码(Code) 字段开始,其后的 16 位校验和涵盖了整个报文,ICMPv4 和 ICMPv6 种的类型和代码字段是不同的。

ICMP 的主要消息

ICMP 目标不可达(类型 3)

我们知道,路由器无法将 IP 数据报发送给目标地址时,会给发送端主机返回一个目标不可达(Destination Unreachable Message) 的 ICMP 消息,并且会在消息中显示不可达的具体原因。

实际通信过程中会显示各种各样的不可达信息,比如错误代码时 1 表示主机不可达,它指的是路由表中没有主机的信息,或者主机没有连接到网络的意思。一些 ICMP 不可达信息的具体原因如下

ICMP 重定向消息(类型 5)

如果路由器发现发送端主机使用了次优的路径发送数据,那么它会返回一个 ICMP 重定向(ICMP Redirect Message) 的消息给这个主机。这个 ICMP 重定向消息包含了最合适的路由信息和源数据。这种情况会发生在路由器持有更好的路由信息的情况下。路由器会通过这样的 ICMP 消息给发送端主机一个更合适的发送路由。

主机 Host 的 IP 地址为 10.0.0.100。主机的路由表中有一个默认路由条目,指向路由器 G1 的 IP 地址 10.0.0.1 作为默认网关。路由器 G1 在将数据包转发到目的网络 X 时,会使用路由器 G2 的 IP 地址 10.0.0.2 作为下一跳。

当主机向目的网络 X 发送数据包时,会发生以下情况

  1. IP 地址为 10.0.0.1 的网关 G1 在其所连接的网络上接收来自 10.0.0.100 的数据包。

  2. 网关 G1 检查其路由表,并在通往数据包目的网络 X 的路由中获取下一个网关 G2 的 IP 地址 10.0.0.2。

  3. 如果 G2 和 IP 数据包的源地址标识的主机位于同一网络中(也就是 Host 主机),那么 G1 会向主机发送 ICMP 重定向消息。ICMP 重定向消息建议主机直接将发送到网络 X 的数据包发送至 G2,因为 Host – G2 这是通往目的地的较短路径。

  4. 网关 G1 将原始数据包转发到其目的地。

当然,根据主机的配置,Host 主机也可以选择忽略 G1 给它发送的 ICMP 重定向消息。但是,这样就享受不到 ICMP 重定向带来的两大好处,即

  • 优化数据在网络中的转发路径;流量更快到达目的地
  • 降低网络资源利用率,例如带宽和路由器 CPU 负载

如果 Host 主机采用了 ICMP 提供的重定向路径的话,那么 Host 就会直接把数据包发送至网络 X,如下图所示

在主机为 G2 作为下一跳的网络 X 创建路由缓存条目后,这些优势在网络中可见:

  • 交换机和路由器 G1 之间链路的带宽利用率在两个方向上都会降低
  • 由于从主机到网络 X 的流量不再流经此节点,因此路由器 G1 的 CPU 使用率降低
  • 主机和网络 X 之间的端到端网络延迟得到改善。

ICMP 重定向示例如下

ICMP 超时消息(类型 11)

在 IP 数据包中有一个叫做 TTL(Time To Live, 生存周期) ,它的值在每经过路由器一跳之后都会减 1,IP 数据包减为 0 时会被丢弃。此时,IP 路由器会发送一个 ICMP 超时消息(ICMP TIme Exceeded Message, 错误号 0)发送给主机,通知该包已经被丢弃。

设置生存周期的主要目的就是为了防止路由器控制遇到问题发生循环状况时,避免 IP 包无休止的在网络上转发,如下图所示

这里给大家推荐一款比较好用的追踪超时消息的工具 traceroute,它可以显示出由执行程序的主机到达特定主机之前需要经过多少路由器。 traceroute 的官网如下 http://www.traceroute.org

ICMP 回送消息(类型 0 和 类型 8)

ICMP 回送消息用于判断相互通信的主机之间是否连通,也就是判断所发送的数据包是否能够到达目标主机。可以向对端主机发送回送请求的消息(ICMP Echo Request Message,类型 8),也可以接收对端主机发送来的回送消息(ICMP Echo Reply Message, 类型 0 )。网络上最常用的 ping 命令就是利用这个实现的。

其他 ICMP 消息

ICMP 原点抑制消息(类型 4)

在使用低速率网络的情况下,网络通信可能会遇到网络拥堵的情况下,ICMP 的原点抑制就是为了应对这种情况的。当路由器向低速线路发送数据时,其发送队列的残存数据报变为 0 从而无法发送时,可以向 IP 数据报的源地址发送一个 ICMP 原点抑制(ICMP Source Quench Message) 消息,收到这个消息的主机了解到线路某处发生了拥堵,从而抑制 IP 数据报的发送。

不过这个 ICMP 消息可能会引起不公平的网络通信,一般不被使用。

ICMP 路由器探索消息(类型 9、10)

ICMP 路由器探索消息主要用于路由器发现(Router Discovery, RD),它主要分为两种,路由器请求(Router Solicitation, 类型 10)路由器响应(Router Advertisement, 类型 9)。主机会在任意路由连接组播的网络上发送一个 RS 消息,想要选择一个路由器进行学习,以此来作为默认路由,而相对应的该路由会发送一个 RA 消息来作为默认路由的响应。

ICMP 地址掩码消息(类型 17、18)

主要用于主机或者路由器想要了解子网掩码的情况。可以向那些目标主机或路由器发送 ICMP 地址掩码请求消息(ICMP Address Mask Request, 类型 17)ICMP 地址掩码应答消息(ICMP Address Mask Reply, 类型 18) 获取子网掩码信息。

ICMPv6

ICMPv6 的作用

IPv4 中 ICMP 仅仅作为一个辅助作用支持 IPv4。也就是说,在 IPv4 时期,即使没有 ICMP,也能进行正常的 IP 数据包的发送和接收,也就是 IP 通信。但是在 IPv6 中,ICMP 的作用被放大了,如果没有 ICMP,则不能进行正常的 IP 通信。

尤其在 IPv6 中,从 IP 定位 MAC 地址的协议从 ARP 转为 ICMP 的邻居探索消息(Neighbor Discovery) 。这种邻居探索消息融合了 IPv4 的 ARP、ICMP 重定向以及 ICMP 的路由选择等功能于一体。甚至还提供了自动设置 IP 的功能。

在 IPv6 中,ICMP 消息主要分为两类:一类是错误消息,一类是信息消息。0 – 127 属于错误消息;128 – 255 属于信息消息。

RFC 2463 中描述了以下消息类型:

ICMPv6 除了包含 ICMPv4 的所有功能外,还有两个额外的功能。

ICMPv6 邻居探索

邻居探索是 ICMPv6 非常重要的功能,主要表示的类型是 133 – 137 之间的消息叫做邻居探索消息。这种邻居探索消息对于 IPv6 通信起到举足轻重的作用。邻居请求消息用于查询 IPv6 地址于 MAC 地址的对应关系。邻居请求消息利用 IPv6 的多播地址实现传输。

此外,由于 IPv6 实现了即插即用的功能,所以在没有 DHCP 服务器的环境下也能实现 IP 地址的自动获取。如果是一个没有路由器的网络,就使用 MAC 地址作为链路本地单播地址。如果在一个有路由器的网络环境中,可以从路由器获得 IPv6 地址的前面部分,后面部分使用 MAC 地址进行设置。此时可以利用路由器请求消息和路由器公告消息进行设置。

ICMPv6 的组播收听发现协议

组播收听发现协议(MLD,Multicast Listener Discovery)由子网内的组播成员管理。MLD 协议定义了3条ICMPv6 消息:

  • 组播收听查询消息:组播路由器向子网内的组播收听者发送此消息,以获取组播收听者的状态。
  • 组播收听者报告消息:组播收听者向组播路由器汇报当前状态,包括离开某个组播组。
  • 组播收听者。

与 ICMP 有关的攻击

涉及 ICMP 攻击主要分为 3 类:泛洪(flood)、炸弹(bomb) 和信息泄露(information disclsure)

  • 泛洪将会产生大量流量,导致针对一台或者多台计算机的有效 Dos 攻击。
  • 炸弹指的是发送经过特殊构造的报文,这类报文能够导致 IP 或者 ICMP 的处理失效或者崩溃。
  • 信息泄露本身不会造成危害,但是能够帮助辅助其他攻击。

针对 TCP 的 ICMP 攻击已经记录在了 RFC5927 中。

计算机网络的应用层

原文:https://zwmst.com/2680.html

文章的整体脉络如下

在有了之前两篇文章的介绍后,相信读者对计算机网络有了初步的认识,那么下面我们就要对不同的协议层进行分类介绍了,我们还是采用自上而下的方式来介绍,这种介绍对读者来说更容易接纳,吸收程度更好(说白了就是更容易给我的文章点赞,逃)。

一般情况下,用户不太在意网络应用程序实际上是按照怎样的机制运行的,但我们是程序员吖,就套用朱伟的一句话说:你觉得计算机网络程序员不了解,你指着互联网用户去了解吗?有内个味儿没?

应用层指的是 OSI 标准模型的第 5、6、7层,也就是会话层、表现层、应用层

我们介绍的时候都会使用 OSI 标准模型来介绍,因为这样涵盖的层次比较多,这样对于 TCP/IP 模型来说,你也能加深理解。

应用层概念

应用层协议的定义

现如今,越来越多的应用程序利用网络进行通信,这些应用有 Web 浏览器、远程登录、电子邮件、文件传输、文件下载等,应用层的协议正是进行这些行为活动的规则和标准。

应用层协议(application layer protocol) 定义了在不同端系统上的应用程序进程如何相互传递报文。一般来说,会定义如下内容

  • 交换的报文类型:是请求报文还是相应报文
  • 报文字段的解释:对报文中各个字段的详细描述
  • 报文字段的语义:报文各个字段的含义是什么
  • 进程何时、以什么方式发送报文以及响应

应用层体系结构

应用层体系结构 的英文是 Application Architecture,它指的是应用层的结构,一般来说,应用层有两种主流体系结构

  • 客户 – 服务器体系结构 ( client-server architecture )
  • 对等体系结构 ( P2P architecture )

下面我们先来聊一下客户 – 服务器体系结构的概念

在客户-服务器体系结构中,有一个总是打开的主机称为 服务器(Server),它提供来自于 客户(client) 的服务。我们最常见的服务器就是 Web 服务器,Web 服务器服务于来自 浏览器 的请求。

当 Web 服务器通过浏览器接收到用户请求后,它会经过一系列的处理把信息或者页面等通过浏览器呈现给应用。这种模式就是客户 – 服务器模式。

有两点需要注意

  • 在客户 – 服务器模式下,通常客户彼此之间是并不互相通信的。
  • 服务器通常具有固定的、周知的 IP 地址可以提供访问。

客户 – 服务器模式通常会出现随着客户数量的急剧增加导致单台服务器无法完成大量客户请求的情况。为此,通常需要配备大量主机的 数据中心(data center) ,用来跟踪所有的用户请求。

于此相反,P2P 也就是对等体系结构对这种数据中心的依赖性很低,因为在 P2P 体系结构中,应用程序在两个主机之间直接通信,这些主机被称为对等方,与有中心服务器的中央网络系统不同,对等网络的每个用户端既是一个节点,也有服务器的功能。常见的 P2P 体系结构的应用有 文件共享、视频会议、网络电话等。

P2P 一个最大的特点就是 扩展性(self-scalability),因为 P2P 网络的一个重要的目标就是让所有的客户端都能提供资源、获取资源,共享带宽,存储空间等。因此,当有更多节点加入且对系统请求增多,整个系统的容量也增大。这是具有一组固定服务器的客户 – 服务器结构不具备的,这也就是 P2P 的扩展性。

进程通信

我们上面说到了两种体系结构,一种是客户 – 服务器模式,一种是 P2P 对等模式。我们都知道一个计算机允许同时运行多个应用程序,在我们看起来这些应用程序好像是同时运行的,那么它们之间是如何通信的呢?

用操作系统的术语来说,进行通信实际上是 进程(process)而不是程序。一个进程可以被认为是运行在端系统中的程序。当多个进程运行在相同的端系统上时,它们使用进程间的通信机制相互通信。进程间的通信规则由操作系统来确定。

进程与计算机网络之间的接口

计算机是庞大且繁杂的,计算机网络也是,应用程序不可能只有一个进程组成,它同样是多个进程共同作用协商运行,然而,分布在多个端系统之间的进程是如何进行通信的呢?实际上,每个进程之间会有一个 套接字(socket) 的软件接口存在,套接字是应用程序的内部接口,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。

通过一个实例来简单类比一下套接字和网络进程:进程可类比一座房子,而它的套接字相当于是房子的门,当一个进程想要与其他进程进行通信时,它会把报文推出门外,然后通过运输设备把报文运输到另外一座房子,通过门进入房子内部使用。

下图是一个通过套接字进行通信的流程图

从图可以看到,Socket 属于主机或者服务进程的内部接口,由应用程序开发人员进行控制,两台端系统之间进行通信会通过 TCP 的缓冲区经由网络传输到另一个端系统的 TCP 缓冲区,Socket 从 TCP 缓冲区读取报文供应用程序内部使用。

套接字是建立网络应用程序的可编程接口,因此套接字也被称为应用程序和网络之间的 应用程序编程接口(Application Programming Interface,API)。应用程序开发人员可以控制套接字内部细节,但是无法控制运输层的传输,只能对运输层的传输协议进行选择,还可以对运输层的传输参数进行选择,比如最大缓存和最大报文长度等。

进程寻址

我们上面提到网络应用程序之间会相互发送报文,那么你怎么知道你应该向哪里发送报文呢?是不是存在某种机制能够让你知道你能够发到哪里?这就好比你要发送电子邮件,你写好了内容但是你不知道发发往哪里,所以这个时候必须要有一种知道对方地址的机制,这种机制能够辨明对方唯一的一个地址,这种地址就是 IP地址。我们会在后面的文章中详细讨论 IP 地址的内容,目前只需要知道 IP 是一个 32 比特的量并且能够唯一标示互联网中任意一台主机的地址就可以了。

只知道 IP 地址是否就可以了呢?我们知道一台计算机可能回运行多个网络应用程序,那么如何确定是哪个网络应用程序接受发送过来的报文呢?所以这时候还需要知道网络应用程序的 端口号(port number)。例如, Web 应用程序需要用 80 端口来标示,邮件服务器程序需要使用 25 来标示。

应用程序如何选择运输服务

我们知道应用程序是属于互联网四层协议的 应用层 协议,并且四层协议必须彼此协助共同完成工作。好了,这时候我们只有应用层协议,我们需要发送报文,我们如何发送报文呢?这就好比你知道目的地是哪里了,你该如何到达目的地呢?是走路,公交,地铁还是打车?

应用程序发送报文的交通工具的选择也有很多,我们可以从 数据传输是否可靠、吞吐量、定时和安全性 来考虑,下面是你需要考虑的具体内容。

  • 数据传输是否可靠

我们之前探讨过,分组在计算机网络中会存在丢包问题,丢包问题的严重性跟网络应用程序的性质有关,如果像是电子邮件、文件传输、远程主机、Web 文档传输的过程中出现问题,数据丢失可能会造成非常严重的后果。如果像是网络游戏,多人视频会议造成的影响可能比较小。鉴于此,数据传输的可靠性也是首先需要考虑的问题。因此,如果一个协议提供了这样的确保数据交付的服务,就认为提供了 可靠数据传输(reliable data transfer),能够忍受数据丢失的应用被称为 容忍丢失的应用(loss-tolerant application)

  • 吞吐量

在之前的文章中我们引入了吞吐量的概念,吞吐量就是在网络应用中数据传输过程中,发送进程能够向接收进程交付比特的速率。具有吞吐量要求的应用程序被称为 带宽敏感的应用(bandwidth-sensitive application)。带宽敏感的应用具有特定的吞吐量要求,而 弹性应用(elastic application) 能够根据当时可用的带宽或多或少地利用可供使用的吞吐量。

  • 定时

定时是什么意思?定时能够确保网络中两个应用程序的收发是否能够在指定的时间内完成,这也是应用程序选择运输服务需要考虑的一个因素,这听起来很自然,你网络应用发送和接收数据包肯定要加以时间的概念,比如在游戏中,你一包数据迟迟发送不过去,对面都推塔了你还卡在半路上呢。

  • 安全性

最后,选择运输协议一定要能够为应用程序提供一种或多种安全性服务。

因特网能够提供的运输服务

说完运输服务的选型,接下来该聊一聊因特网能够提供哪些服务了。实际上,因特网为应用程序提供了两种运输层的协议,即 UDPTCP,下面是一些网络应用的选择要求,可以根据需要来选择适合的运输层协议。

应用 数据丢失 带宽 时间敏感
文件传输 不能丢失 弹性 不敏感
电子邮件 不能丢失 弹性 不敏感
Web 文档 不能丢失 弹性 不敏感
因特网电话/视频会议 容忍丢失 弹性 敏感,100ms
流式存储音频/视频 容忍丢失 弹性 敏感,几秒
交互式游戏 容忍丢失 弹性 是,100ms
智能手机消息 不能丢失 弹性 无所谓

下面我们就来聊一聊这两种运输协议的应用场景

TCP

TCP 服务模型的特性主要有下面几种

  • 面向连接的服务

在应用层数据报发送后, TCP 让客户端和服务器互相交换运输层控制信息。这个握手过程就是提醒客户端和服务器需要准备好接受数据报。握手阶段后,一个 TCP 连接(TCP Connection) 就建立了。这是一条全双工的连接,即连接双方的进程都可以在此连接上同时进行收发报文。当应用程序结束报文发送后,必须拆除连接。

  • 可靠的数据传输

通信进程能够依靠 TCP,无差错、按适当顺序交付所有发送的数据。应用程序能够依靠 TCP 将相同的字节流交付给接收方的套接字,没有字节的丢失和冗余。

  • 拥塞控制

TCP 的拥塞控制并不一定为通信进程带来直接好处,但能为因特网带来整体好处。当接收方和发送方之间的网络出现拥塞时,TCP 的拥塞控制会抑制发送进程(客户端或服务器),我们会在后面具体探讨拥塞控制

UDP

UDP 是一种轻量级的运输协议,它仅提供最小服务。UDP 是无连接的,因此在两个进程通信前没有握手过程。UDP 也不会保证报文是否传输到服务端,它就像是一个撒手掌柜。不仅如此,到达接收进程的报文也可能是乱序到达的。

下面是上表列出来的一些应用所选择的协议

应用 应用层协议 支撑的运输协议
电子邮件 SMTP TCP
远程终端访问 Telnet TCP
Web HTTP TCP
文件传输 FTP TCP
流式多媒体 HTTP TCP
因特网电话 SIP、RTP TCP 或 UDP

应用层协议

下面我们着重介绍一下应用层都有哪些比较重要的应用协议

WWW 和 HTTP

万维网(WWW, World Wide Web) 是将互联网中的信息以超文本的形式展现的系统,也就是 Web 。用来显示 WWW 结果的客户端被称为 Web 浏览器,通过浏览器,我们无需关注想要访问的内容在哪个服务器上,我们只需要知道我们想访问的内容就可以了。

WWW 定义了三个比较重要的概念,这些概念主要有

  • URI,定义了访问信息的手段和位置
  • HTML, 定义了信息的表现形式
  • HTTP,定义了 WWW 的访问规范

URI / URL

URI的全称是(Uniform Resource Identifier),中文名称是统一资源标识符,使用它就能够唯一地标记互联网上资源。

URL的全称是(Uniform Resource Locator),中文名称是统一资源定位符,也就是我们俗称的网址,它实际上是 URI 的一个子集。

URI 不仅包括 URL,还包括 URN(统一资源名称),它们之间的关系如下

URI 已经不局限于标识互联网资源,它可以作为所有资源的识别码。

HTML

HTML 称为超文本标记语言,是一种标识性的语言。它包括一系列标签.通过这些标签可以将网络上的文档格式统一,使分散的 Internet 资源连接为一个逻辑整体。HTML 文本是由 HTML 命令组成的描述性文本,HTML 命令可以说明文字,图形、动画、声音、表格、链接等。

HTTP

Web 的应用层协议就是 HTTP(HyperText Transfer Protocol, HTTP), 超文本传输协议,它是 Web 的核心协议。下面我们需要了解一下 HTTP 中的几个核心概念。

Web 页面

Web 页面也叫做 Web Page,它是由对象组成,一个对象(object) 简单来说就是一个文件,这个文件可以是 HTML 文件、一个图片、一段 Java 应用程序等,它们都可以通过 URI 来找到。一个 Web 页面包含了很多对象,Web 页面可以说是对象的集合体。

浏览器

就如同各大邮箱使用电子邮件传送协议 SMTP 一样,浏览器是使用 HTTP 协议的主要载体,说到浏览器,你能想起来几种?是的,随着网景大战结束后,浏览器迅速发展,至今已经出现过的浏览器主要有

Web 服务器

Web 服务器的正式名称叫做 Web Server,Web 服务器可以向浏览器等 Web 客户端提供文档,也可以放置网站文件,让全世界浏览;可以放置数据文件,让全世界下载。目前最主流的三个 Web 服务器是 Apache、 Nginx 、IIS。

CDN

CDN 的全称是Content Delivery Network,即内容分发网络,它应用了 HTTP 协议里的缓存和代理技术,代替源站响应客户端的请求。CDN 是构建在现有网络基础之上的网络,它依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。CDN 的关键技术主要有内容存储分发技术

打比方说你要去亚马逊上买书,之前你只能通过购物网站购买后从美国发货过海关等重重关卡送到你的家里,现在在中国建立一个亚马逊分基地,你就不用通过美国进行邮寄,从中国就能把书尽快给你送到。

WAF

WAF 是一种 Web 应用程序防护系统(Web Application Firewall,简称 WAF),它是一种通过执行一系列针对 HTTP / HTTPS的安全策略来专门为 Web 应用提供保护的一款产品,它是应用层面的防火墙,专门检测 HTTP 流量,是防护 Web 应用的安全技术。

WAF 通常位于 Web 服务器之前,可以阻止如 SQL 注入、跨站脚本等攻击,目前应用较多的一个开源项目是 ModSecurity,它能够完全集成进 Apache 或 Nginx。

WebService

WebService 是一种 Web 应用程序,WebService 是一种跨编程语言和跨操作系统平台的远程调用技术

WebService 是一种由 W3C 定义的应用服务开发规范,使用 client-server 主从架构,通常使用 WSDL 定义服务接口,使用 HTTP 协议传输 XML 或 SOAP 消息,它是一个基于 Web(HTTP)的服务架构技术,既可以运行在内网,也可以在适当保护后运行在外网。

HTTP

HTTP 是一个在计算机世界里专门在两点之间传输文字、图片、音频、视频等超文本数据的约定和规范。HTTP 是一种应用层协议,它使用 TCP 作为运输层协议,因为文档、数据这些信息在我们看来是一种重要的信息,不可丢失。

HTTP 请求响应过程

让我们通过一个例子来探讨一下 HTTP 的请求响应过程,我们假设访问的 URL 地址为 http://www.someSchool.edu/someDepartment/home.index,当我们输入网址并点击回车时,浏览器内部会进行如下操作

  • DNS服务器会首先进行域名的映射,找到访问www.someSchool.edu所在的地址,然后HTTP 客户端进程在 80 端口发起一个到服务器 www.someSchool.edu 的 TCP 连接(80 端口是 HTTP 的默认端口)。在客户和服务器进程中都会有一个套接字与其相连。
  • HTTP 客户端通过它的套接字向服务器发送一个 HTTP 请求报文。该报文中包含了路径 someDepartment/home.index 的资源,我们后面会详细讨论 HTTP 请求报文。
  • HTTP 服务器通过它的套接字接受该报文,进行请求的解析工作,并从其存储器(RAM 或磁盘)中检索出对象 www.someSchool.edu/someDepartment/home.index,然后把检索出来的对象进行封装,封装到 HTTP 响应报文中,并通过套接字向客户进行发送。
  • HTTP 服务器随即通知 TCP 断开 TCP 连接,实际上是需要等到客户接受完响应报文后才会断开 TCP 连接。
  • HTTP 客户端接受完响应报文后,TCP 连接会关闭。HTTP 客户端从响应中提取出报文中是一个 HTML 响应文件,并检查该 HTML 文件,然后循环检查报文中其他内部对象。
  • 检查完成后,HTTP 客户端会把对应的资源通过显示器呈现给用户。

至此,键入网址再按下回车的全过程就结束了。上述过程描述的是一种简单的请求-响应全过程,真实的请求-响应情况可能要比上面描述的过程复杂很多。

HTTP 请求特征

从上面整个过程中我们可以总结出 HTTP 进行分组传输是具有以下特征

  • 支持客户 – 服务器模式
  • 简单快速:客户向服务器请求服务时,只需传送请求方法和路径。请求方法常用的有 GET、HEAD、POST。每种方法规定了客户与服务器联系的类型不同。由于 HTTP 协议简单,使得 HTTP 服务器的程序规模小,因而通信速度很快。
  • 灵活:HTTP 允许传输任意类型的数据对象。正在传输的类型由 Content-Type 加以标记。
  • 无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
  • 无状态:HTTP 协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。

持久链接和非持久链接

我们上面描述的 HTTP 请求响应过程就是一种非持久链接,因为每次 TCP 在传递完报文后,都会关闭 TCP 链接,每个 TCP 连接只传输一个请求报文和响应报文。

非持久性连接有一些缺点

  • 第一,必须为每个请求的对象建立和维护一个全新的连接。
  • 第二,对于每个这样的连接来说,在客户端和服务器中都要分配 TCP 的缓冲区和保持 TCP 变量,这给 Web 服务器带来了严重的负担。因为一台 Web 服务器可能要同时服务于数百甚至上千个客户请求。

在采用 HTTP 1.1 持续连接的情况下,服务器在发送响应后保持该 TCP 连接打开不关闭。在相同的客户与服务器之间,后续的请求和响应报文能够通过相同的连接进行传送。一般来说,如果一跳连接经过一定的时间间隔(可配置)后仍未使用,HTTP 服务器就应该关闭其连接。

HTTP 报文格式

我们上面描述了一下 HTTP 的请求响应过程,相信你对 HTTP 有了更深的认识,下面我们就来一起认识一下 HTTP 的报文格式是怎样的。

HTTP 协议主要由三大部分组成:

  • 起始行(start line):描述请求或响应的基本信息;
  • 头部字段(header):使用 key-value 形式更详细地说明报文;
  • 消息正文(entity):实际传输的数据,它不一定是纯文本,可以是图片、视频等二进制数据。

其中起始行和头部字段并成为 请求头 或者 响应头,统称为 Header;消息正文也叫做实体,称为 body。HTTP 协议规定每次发送的报文必须要有 Header,但是可以没有 body,也就是说头信息是必须的,实体信息可以没有。而且在 header 和 body 之间必须要有一个空行(CRLF)。如果用一幅图来表示一下 HTTP 请求的话,我觉得应该是下面这样

如果细化一点的话,那就是下面这样

这幅图需要注意一下,如果使用 GET 方法,是没有实体体的,如果你使用的是 POST 方法,才会有实体体。当用户提交表单时,HTTP 客户端通常使用 POST 方法;与此相反,HTML 表单的获取通常使用 GET 方法。HEAD 方法类似于 GET 方法,只不过 HEAD 方法不会返回对象。

下面我们来看一下 HTTP 响应报文

可以看到,请求报文和响应报文只有请求头是不同的,其他信息均一致。

请求报文请求行:

GET /some/page.html HTTP/1.1

响应报文:

HTTP/1.1 200 OK

HTTP 协议是一种无状态协议,即每次服务端接收到客户端的请求时,都是一个全新的请求,服务器并不知道客户端的历史请求记录;Session 和 Cookie 的主要目的就是为了弥补 HTTP 的无状态特性。

Session 是什么

客户端请求服务端,服务端会为这次请求开辟一块内存空间,这个对象便是 Session 对象,存储结构为 ConcurrentHashMap。Session 弥补了 HTTP 无状态特性,服务器可以利用 Session 存储客户端在同一个会话期间的一些操作记录。

Session 如何判断是否是同一会话

服务器第一次接收到请求时,开辟了一块 Session 空间(创建了Session对象),同时生成一个 sessionId ,并通过响应头的 Set-Cookie:JSESSIONID=XXXXXXX 命令,向客户端发送要求设置 Cookie 的响应; 客户端收到响应后,在本机客户端设置了一个 JSESSIONID=XXXXXXX 的 Cookie 信息,该 Cookie 的过期时间为浏览器会话结束;

接下来客户端每次向同一个网站发送请求时,请求头都会带上该 Cookie信息(包含 sessionId ), 然后,服务器通过读取请求头中的 Cookie 信息,获取名称为 JSESSIONID 的值,得到此次请求的 sessionId。

Session 的缺点

Session 机制有个缺点,比如 A 服务器存储了 Session,就是做了负载均衡后,假如一段时间内 A 的访问量激增,会转发到 B 进行访问,但是 B 服务器并没有存储 A 的 Session,会导致 Session 的失效。

Cookies 是什么

HTTP 协议中的 Cookie 包括 Web Cookie浏览器 Cookie,它是服务器发送到 Web 浏览器的一小块数据。服务器发送到浏览器的 Cookie,浏览器会进行存储,并与下一个请求一起发送到服务器。通常,它用于判断两个请求是否来自于同一个浏览器,例如用户保持登录状态。

HTTP Cookie 机制是 HTTP 协议无状态的一种补充和改良

Cookie 主要用于下面三个目的

  • 会话管理

登陆、购物车、游戏得分或者服务器应该记住的其他内容

  • 个性化

用户偏好、主题或者其他设置

  • 追踪

记录和分析用户行为

Cookie 曾经用于一般的客户端存储。虽然这是合法的,因为它们是在客户端上存储数据的唯一方法,但如今建议使用现代存储 API。Cookie 随每个请求一起发送,因此它们可能会降低性能(尤其是对于移动数据连接而言)。

当接收到客户端发出的 HTTP 请求时,服务器可以发送带有响应的 Set-Cookie 标头,Cookie 通常由浏览器存储,然后将 Cookie 与 HTTP 标头一同向服务器发出请求。

Set-Cookie HTTP 响应标头将 cookie 从服务器发送到用户代理。下面是一个发送 Cookie 的例子

此标头告诉客户端存储 Cookie

现在,随着对服务器的每个新请求,浏览器将使用 Cookie 头将所有以前存储的 Cookie 发送回服务器。

有两种类型的 Cookies,一种是 Session Cookies,一种是 Persistent Cookies,如果 Cookie 不包含到期日期,则将其视为会话 Cookie。会话 Cookie 存储在内存中,永远不会写入磁盘,当浏览器关闭时,此后 Cookie 将永久丢失。如果 Cookie 包含有效期 ,则将其视为持久性 Cookie。在到期指定的日期,Cookie 将从磁盘中删除。

还有一种是 Cookie的 Secure 和 HttpOnly 标记,下面依次来介绍一下

会话 Cookies

上面的示例创建的是会话 Cookie ,会话 Cookie 有个特征,客户端关闭时 Cookie 会删除,因为它没有指定ExpiresMax-Age 指令。

但是,Web 浏览器可能会使用会话还原,这会使大多数会话 Cookie 保持永久状态,就像从未关闭过浏览器一样。

永久性 Cookies

永久性 Cookie 不会在客户端关闭时过期,而是在特定日期(Expires)特定时间长度(Max-Age)外过期。例如

Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT;

尽管 Cookie 能够简化用户的网络活动,但是 Cookie 的使用存在争议,因为不少人认为它对用户是一种侵权行为。因为结合 Cookie 和用户提供的账户信息,Web 站点可以知道更多关于用户的信息。

Web 缓存

Web 缓存(Web cache) 也叫做 代理服务器(proxy server),它是代表 HTTP 服务器来满足用户需求的网络实体。Web 缓存器有自己的磁盘存储空间,并会在存储空间内保存最近请求过的对象,如下图所示

Web 缓存可以在用户的浏览器中进行配置,一旦配置后,用户首先访问的就不是初始服务器了,需要先访问代理服务器判断请求的对象是否存在,如果代理服务器没有,再由代理服务器来请求初始服务器把对象返回给客户,同时在自己的磁盘空间保存对象。

这里需要注意,客户和初始服务器的架构是 客户-服务器模式,而代理服务器不仅能当服务器使用,也可以当作客户端使用。

代理服务器一般由 ISP(Internet Service Provider),提供。注意不是老色批。。。ISP 也就是我们常说的运营商,你懂的。

那么为什么需要代理服务器的存在呢?相信你看完上面的描述应该能大致猜到它的作用。

  • 首先,代理服务器可以大大减少对客户请求的响应时间,能够更快给用户响应。
  • 其次,代理服务器可以减少一个机构接入链路到网络的通信量,降低网络带宽,降低运营商成本。
  • 然后,代理服务器可以分担初始服务器的压力,改善应用程序的性能。

DASH

通过上面的描述我们知道 HTTP 是可以传输普通文件、音频、视频的,这些传输的信息统称为 MIME 类型。HTTP 在传递视频中,也只是把视频当作对象来传输,而一个对象其实就是一个文件,一个文件都在 HTTP 中都可以用 URL 来表示。当用户在看视频时,客户与服务器建立一个 TCP 连接并发送对该 URL 的 GET 请求,然后服务器响应给客户端时,客户端会缓存一定量的字节数据,当数据超过预先设定的门限时,客户应用程序就开始播放视频。

这种方式有一种局限性就是对每个客户端来说,尽管每个客户端可用的带宽量不同,但所有客户端都收到相同的视频编码。这就造成带宽浪费。这就相当我是一个 2兆的网络和 50 兆的光纤都能收到相同的视频编码,以几乎相同的等待时间开始播放视频,那么我为什么还要花 50 兆光纤的钱呢?

为了改善这一现象,出现了 HTTP 的 DASH,DASH 即 Dynamic Adaptive Streaming HTTP,动态适应流。它的理念是针对不同流量的网络来说,所能够传输的比特数据也不相同。DASH 允许客户使用不同的因特网传输速率可以播放不同编码速率的视频。对于 3G 用户和光纤用户自然会选择以不同的速率传输比特数据,从而最大限度的使用带宽。

CDN

随着互联网的接入用户变得越来越多,视频逐渐成为了比特传输的瓶颈和用户的强烈需求。作为一个因特网视频公司,最一开始提供流式服务最直接的方式是建立单一的大规模数据中心。在数据中心内缓存所有视频,并直接从数据中心向世界范围内传播视频。但是这种方式存在三种问题

  • 如果客户远离数据中心,那么服务器到客户分组会跨越许多通信链路并且可能通过许多 ISP,这样你的视频播放能快到哪去?
  • 每次视频数据都会重新传递给客户端,这样会严重浪费网络带宽,而且视频公司会支付重复的带宽费用
  • 单点故障问题,只要视频数据中心宕机或者其他事故,直接导致全球范围内的视频无法播放。

为了应对能够向全世界的用户 24 小时不间断的分发视频,几乎所有的主流视频公司都会使用 内容分发网(Content Distribution Network, CDN) 。CDN 管理分布在多个地理位置上的服务器,在每个服务器上缓存各种视频、音频、文件等。

CDN 内容选择策略

CDN 管理分布在多个地理位置上的服务器,在它的服务器上存储视频副本,并且所有试图将每个用户请求定向到一个提供最好用户体验的 CDN 位置。那么服务器如何选址呢?事实上有两种服务器安置原则

  • 深入,它的主要目标是靠近用户,通过减少端用户和 CDN 集群之间链路和路由器的数量,从而改善了用户感受的时延和吞吐量。
  • 邀请做客,这个原则是通过在少量(例如 10 个)关键位置建造大集群来邀请 ISP 来做客,与深入设计原则相比,邀请做客设计通常产生较低的维护和管理开销。

CDN 工作流程

CDN 可以是专用 CDN(private CDN), 即它由内容提供商自己所拥有;另一种 CDN 是 第三方 CDN(third-party CDN),它代表多个内容提供商分发内容。

下面我们来聊一下 CDN 工作流程,如下图所示

  • 用户想要访问指定网站的内容

  • 用户首先发起对本地 DNS,LDNS 的查询,LDNS 会将请求中继到网站 DNS 服务器,网站的 DNS 服务器会返回给 LDNS 一个网站 CDN 权威服务器的地址

  • LDNS 服务器会发送第二个请求给网站 CDN 权威服务器,希望获取网站内容分发服务器的地址,网站 CDN 会把 CDN 内容分发服务器的地址发送给本地 DNS 服务器

  • 本地 DNS 服务器会把网站 CDN 内容分发服务器的地址发送给用户

  • 用户知道网站 CDN 内容分发服务器的地址后,无需额外操作,直接和网站 CDN 内容分发服务器建立 TCP 连接,并且发出 HTTP GET 请求,如果使用了 DASH 流,会根据不同 URL 的版本选择不同速率的块发送给用户。

CDN 集群选择策略

任何 CDN 的部署,其核心是 集群选择策略(cluster selection strategy), 即动态的将客户定向到 CDN 中某个服务器集群或数据中心的机制。一种简单的策略是指派客户到 地理上最为临近(geographically closest) 的集群。这种选择策略忽略了时延和可用带宽随因特网路径时间而变化,总是为特定的客户指派相同的集群;还有一种选择策略是 实时测量(real-time measurement),该机制是基于集群和客户之间的时延和丢包性能执行周期性检查。

DNS 因特网目录服务协议

试想一个问题,我们人类可以有多少种识别自己的方式?可以通过身份证来识别,可以通过社保卡号来识别,也可以通过驾驶证来识别,尽管我们有多种识别方式,但在特定的环境下,某种识别方法可能比另一种方法更为适合。因特网上的主机和人类一样,可以使用多种识别方式进行标识。互联网上主机的一种标识方法是使用它的 主机名(hostname) ,如 www.facebook.com、 www.google.com 等。但是这是我们人类的记忆方式,路由器不会这么理解,路由器喜欢定长的、有层次结构的 IP地址,so,还记得 IP 是什么吗?

IP 地址现在简单表述一下,就是一个由 4 字节组成,并有着严格的层次结构。例如 121.7.106.83 这样一个 IP 地址,其中的每个字节都可以用 . 进行分割,表示了 0 - 255 的十进制数字。(具体的 IP 我们会在后面讨论)

然而,路由器喜欢的是 IP 地址进行解析,我们人类却便于记忆的是网址,那么路由器如何把 IP 地址解析为我们熟悉的网址地址呢?这时候就需要 DNS 出现了。

DNS 的全称是 Domain Name System,DNS ,它是一个由分层的 DNS 服务器(DNS server)实现的分布式数据库;它还是一个使得主机能够查询分布式数据库的应用层协议。DNS 服务器通常是运行 BIND(Berkeley Internet Name Domain) 软件的 UNIX 机器。DNS 协议运行在 UDP 之上,使用 53 端口。

DNS 基本概述

与 HTTP、FTP 和 SMTP 一样,DNS 协议也是应用层的协议,DNS 使用客户-服务器模式运行在通信的端系统之间,在通信的端系统之间通过下面的端到端运输协议来传送 DNS 报文。但是 DNS 不是一个直接和用户打交道的应用。DNS 是为因特网上的用户应用程序以及其他软件提供一种核心功能。

DNS 通常不是一门独立的协议,它通常为其他应用层协议所使用,这些协议包括 HTTP、SMTP 和 FTP,将用户提供的主机名解析为 IP 地址。

下面根据一个示例来描述一下这个 DNS 解析过程,这个和你输入网址后,浏览器做了什么操作有异曲同工之处

你在浏览器键入 www.someschool.edu/index.html 时会发生什么现象?为了使用户主机能够将一个 HTTP 请求报文发送到 Web 服务器 www.someschool.edu ,会经历如下操作

  • 同一台用户主机上运行着 DNS 应用的客户端
  • 浏览器从上述 URL 中抽取出主机名 www.someschool.edu ,并将这台主机名传给 DNS 应用的客户端
  • DNS 客户向 DNS 服务器发送一个包含主机名的请求。
  • DNS 客户最终会收到一份回答报文,其中包含该目标主机的 IP 地址
  • 一旦浏览器收到目标主机的 IP 地址后,它就能够向位于该 IP 地址 80 端口的 HTTP 服务器进程发起一个 TCP 连接。

除了提供 IP 地址到主机名的转换,DNS 还提供了下面几种重要的服务

  • 主机别名(host aliasing),有着复杂的主机名的主机能够拥有一个或多个其他别名,比如说一台名为 relay1.west-coast.enterprise.com 的主机,同时会拥有 enterprise.com 和 www.enterprise.com 的两个主机别名,在这种情况下,relay1.west-coast.enterprise.com 也称为 规范主机名,而主机别名要比规范主机名更加容易记忆。应用程序可以调用 DNS 来获得主机别名对应的规范主机名以及主机的 IP地址。
  • 邮件服务器别名(mail server aliasing),同样的,电子邮件的应用程序也可以调用 DNS 对提供的主机名进行解析。
  • 负载分配(load distribution),DNS 也用于冗余的服务器之间进行负载分配。繁忙的站点例如 cnn.com 被冗余分布在多台服务器上,每台服务器运行在不同的端系统之间,每个都有着不同的 IP 地址。由于这些冗余的 Web 服务器,一个 IP 地址集合因此与同一个规范主机名联系。DNS 数据库中存储着这些 IP 地址的集合。由于客户端每次都会发起 HTTP 请求,所以 DNS 就会在所有这些冗余的 Web 服务器之间循环分配了负载。

DNS 工作概述

DNS 是一个复杂的系统,我们在这里只是就其运行的主要方面进行学习,下面给出一个 DNS 工作过程的总体概述

假设运行在用户主机上的某些应用程序(如 Web 浏览器或邮件阅读器) 需要将主机名转换为 IP 地址。这些应用程序将调用 DNS 的客户端,并指明需要被转换的主机名。用户主机上的 DNS 收到后,会使用 UDP 通过 53 端口向网络上发送一个 DNS 查询报文,经过一段时间后,用户主机上的 DNS 会收到一个主机名对应的 DNS 回答报文。因此,从用户主机的角度来看,DNS 就像是一个黑盒子,其内部的操作你无法看到。但是实际上,实现 DNS 这个服务的黑盒子非常复杂,它由分布于全球的大量 DNS 服务器以及定义了 DNS 服务器与查询主机通信方式的应用层协议组成。

DNS 最早的一种简单设计只是在因特网上使用一个 DNS 服务器。该服务器会包含所有的映射。这是一种集中式的设计,这种设计并不适用于当今的互联网,因为互联网有着数量巨大并且持续增长的主机,这种集中式的设计会存在以下几个问题

  • 单点故障(a single point of failure),如果 DNS 服务器崩溃,那么整个网络随之瘫痪。
  • 通信容量(traaffic volume),单个 DNS 服务器不得不处理所有的 DNS 查询,这种查询级别可能是上百万上千万级
  • 远距离集中式数据库(distant centralized database),单个 DNS 服务器不可能 邻近 所有的用户,假设在美国的 DNS 服务器不可能临近让澳大利亚的查询使用,其中查询请求势必会经过低速和拥堵的链路,造成严重的时延。
  • 维护(maintenance),维护成本巨大,而且还需要频繁更新。

所以 DNS 不可能集中式设计,它完全没有可扩展能力,因此采用分布式设计,所以这种设计的特点如下

**分布式、层次数据库

首先分布式设计首先解决的问题就是 DNS 服务器的扩展性问题,因此 DNS 使用了大量的 DNS 服务器,它们的组织模式一般是层次方式,并且分布在全世界范围内。没有一台 DNS 服务器能够拥有因特网上所有主机的映射。相反,这些映射分布在所有的 DNS 服务器上。

大致来说有三种 DNS 服务器:根 DNS 服务器顶级域(Top-Level Domain, TLD) DNS 服务器权威 DNS 服务器 。这些服务器的层次模型如下图所示

假设现在一个 DNS 客户端想要知道 www.amazon.com 的 IP 地址,那么上面的域名服务器是如何解析的呢?首先,客户端会先根服务器之一进行关联,它将返回顶级域名 com 的 TLD 服务器的 IP 地址。该客户则与这些 TLD 服务器之一联系,它将为 amazon.com 返回权威服务器的 IP 地址。最后,该客户与 amazom.com 权威服务器之一联系,它为 www.amazom.com 返回其 IP 地址。

我们现在来讨论一下上面域名服务器的层次系统

  • 根 DNS 服务器 ,有 400 多个根域名服务器遍及全世界,这些根域名服务器由 13 个不同的组织管理。根域名服务器的清单和组织机构可以在 https://root-servers.org/ 中找到,根域名服务器提供 TLD 服务器的 IP 地址。
  • 顶级域 DNS 服务器,对于每个顶级域名比如 com、org、net、edu 和 gov 和所有的国家级域名 uk、fr、ca 和 jp 都有 TLD 服务器或服务器集群。所有的顶级域列表参见 https://tld-list.com/ 。TDL 服务器提供了权威 DNS 服务器的 IP 地址。
  • 权威 DNS 服务器,在因特网上具有公共可访问的主机,如 Web 服务器和邮件服务器,这些主机的组织机构必须提供可供访问的 DNS 记录,这些记录将这些主机的名字映射为 IP 地址。一个组织机构的权威 DNS 服务器收藏了这些 DNS 记录。

一般域名服务器的层次结构主要是以上三种,除此之外,还有另一类重要的 DNS 服务器,它是 本地 DNS 服务器(local DNS server)。严格来说,本地 DNS 服务器并不属于上述层次结构,但是本地 DNS 服务器又是至关重要的。每个 ISP(Internet Service Provider) 比如居民区的 ISP 或者一个机构的 ISP 都有一台本地 DNS 服务器。当主机和 ISP 进行连接时,该 ISP 会提供一台主机的 IP 地址,该主机会具有一台或多台其本地 DNS 服务器的 IP地址。通过访问网络连接,用户能够容易的确定 DNS 服务器的 IP地址。当主机发出 DNS 请求后,该请求被发往本地 DNS 服务器,它起着代理的作用,并将该请求转发到 DNS 服务器层次系统中。

DNS 缓存

DNS 缓存(DNS caching) 有时也叫做 DNS 解析器缓存,它是由操作系统维护的临时数据库,它包含有最近的网站和其他 Internet 域的访问记录。也就是说, DNS 缓存只是计算机为了满足快速的响应速度而把已加载过的资源缓存起来,再次访问时可以直接快速引用的一项技术和手段。那么 DNS 的缓存是如何工作的呢?

**DNS 缓存的工作流程

在浏览器向外部发出请求之前,计算机会拦截每个请求并在 DNS 缓存数据库中查找域名,该数据库包含有最近的域名列表,以及 DNS 首次发出请求时 DNS 为它们计算的地址。

DNS 记录和报文

共同实现 DNS 分布式数据库的所有 DNS 服务器存储了资源记录(Resource Record, RR),RR 提供了主机名到 IP 地址的映射。每个 DNS 回答报文中会包含一条或多条资源记录。RR 记录用于回复客户端查询。

资源记录是一个包含了下列字段的 4 元组

(Name, Value, Type, TTL)

RR 会有不同的类型,下面是不同类型的 RR 汇总表

DNS RR 类型 解释
A 记录 IPv4 主机记录,用于将域名映射到 IPv4 地址
AAAA 记录 IPv6 主机记录,用于将域名映射到 IPv6 地址
CNAME 记录 别名记录,用于映射 DNS 域名的别名
MX 记录 邮件交换器,用于将 DNS 域名映射到邮件服务器
PTR 记录 指针,用于反向查找(IP地址到域名解析)
SRV 记录 SRV记录,用于映射可用服务。

**DNS 报文

DNS 有两种报文,一种是查询报文,一种是响应报文,并且这两种报文有着相同的格式,下面是 DNS 的报文格式

下面对报文格式进行解释

  • 前 12 个报文是 首部区域,也就是说首部区域有 12 个字节,第一个字段(标识符)是一个 16 比特的数,用于标示该查询。这个标识符会被复制到对查询的回答报文中,以便让客户用它来匹配发送的请求和接受到的回答。 标志字段含有若干标志,标志字段表示为 1 比特,它用于指出报文是 0-查询报文还是 1-响应报文。

  • 问题区域包含着正在进行的查询信息。这个区域包括:1) 名字字段,包含正在被查询的主机名字;2) 类型字段,指出有关该名字的正被询问的问题类型,例如主机地址是与一个名字相关联(类型 A)还是与某个名字的邮件服务器相关联(类型 MX)。

  • 在来自 DNS 服务器的回答中,回答区域包含了对最初请求的名字的资源记录。上面说过 DNS RR记录是个四元组,而且元组中的 Type 会有不同的类型。在回答报文的回答区域中可以包含多条 RR,因此一个主机名能够有多个 IP 地址。

  • 权威区域 包含了其他权威服务器的记录

  • 附加区域 包含了其他有帮助的记录。

关于具体 DNS 记录的详细介绍我会出一篇文章专门探讨。

P2P 文件分发

我们上面探讨的协议 HTTP、SMTP、DNS 都采用了客户-服务器 模式,这种模式会极大依赖总是打开的基础设施服务器。而 P2P 是客户端与客户端模式,对总是打开的基础设施服务器有最小的依赖。

P2P 的全称是 Peer-to-peer, P2P ,是一种分布式体系结构的计算机网络。在 P2P 体系中,所有的计算机和设备都被称为对等体,他们互相交换工作。对等网络中的每个对等方都等于其他对等方。网络中没有特权对等体,也没有主管理员设备。

从某种意义上说,对等网络是计算机世界中最平等的网络。每个对等方都相等,并且每个对等方具有与其他对等方相同的权利和义务。对等体同时是客户端和服务器。

实际上,对等网络中可用的每个资源都是在对等之间共享的,而无需任何中央服务器。P2P 网络中的共享资源可以是诸如处理器使用率,磁盘存储容量或网络带宽等。

P2P 用来做什么

P2P 的主要目标是共享资源并帮助计算机和设备协同工作,提供特定服务或执行特定任务。如前面说到的,P2P 用于共享各种计算资源,例如网络带宽或磁盘存储空间。 但是,对等网络最常见的例子是 Internet 上的文件共享。 对等网络非常适合文件共享,因为它们允许连接到它们计算机等同时接收文件和发送文件。

BitTorrent 是 P2P 使用的主要协议。

P2P 网络的作用

P2P 网络具有一些使它们有用的特征

  • 很难完全掉线,即使其中的一个对等方掉线,其他对等方仍在运行并进行通信。 为了使 P2P(对等)网络停止工作,你必须关闭所有对等网络。对等网络具有很强的可扩展性。 添加新的对等节点很容易,因为你无需在中央服务器上进行任何中央配置。
  • 当涉及到文件共享时,对等网络越大,速度越快。 在 P2P 网络中的许多对等点上存储相同的文件意味着当某人需要下载文件时,该文件会同时从多个位置下载。

TELNET

TELNET 又称为远程登录,是一种应用层协议,它为用户提供了在本地机器上就能够操控远程主机工作的能力。例如下面这幅图所示

主机 A 可以直接通过 TELNET 协议访问主机 B。

TELNET 利用 TCP 的一条连接,通过一条连接向主机发送文字命令并在主机上执行。

使用 TELNET 协议进行远程登录时需要满足一下几个条件

  • 必须知道远程主机的 IP 地址或者域名
  • 必须知道登录标识和口令

TELNET 远程登录一般使用 23 端口

TELNET 的工作过程如下

  • 本地主机与远程主机建立连接,这个连接其实是 TCP 连接,用户需要知道指定主机的 IP 地址或者域名
  • 与远程主机建立连接后,在本地主机终端上输入的字符都会以 NVT(Net Virtual Terminal) 的形式发送至远程主机,这个过程实际上是发送一个数据包到远程主机。
  • 远程主机接受数据包后,产生的输出会以 NVT 的格式发送给本地主机一个数据包,包括输入命令回显和命令执行结果
  • 最后,本地主机终端对远程主机撤销链接,这个过程实际上就是 TCP 断开连接的过程。

SSH

TELNET 有一个非常明显的缺点,那就是在主机和远程主机的发送数据包的过程中是明文传输,未经任何安全加密,这样的后果是容易被互联网上不法分子嗅探到数据包来搞一些坏事,为了数据的安全性,我们一般使用 SSH 进行远程登录。

SSH 是加密的远程登录系统。使用 SSH 可以加密通信内容,即时数据包被嗅探和抓取也无法破解所包含的信息,除此之外,SSH 还有一些其他功能

  • SSH 可以使用更强的认证机制
  • SSH 可以转发文件
  • SSH 可以使用端口转发功能

端口转发(Port forwarding)是 SSH 为网络安全通信使用的一种方法。SSH 可以利用端口转发技术来传输其他 TCP/IP 协议的报文,当使用这种方式时,SSH 就为其他服务在客户端和服务器端建立了一条安全的传输管道端口转发是指将特定端口号所收到的消息转发到指定 IP 地址和端口号的一种机制

FTP

FTP(File Transfer Protocol,文件传输协议) 是应用层协议之一。FTP 协议包括两个组成部分,分为 FTP 服务器和 FTP 客户端。其中 FTP 服务器用来存储文件,用户可以使用 FTP 客户端通过 FTP 协议访问位于 FTP 服务器上的资源。

由于 FTP 传输效率非常高,一般用来在网络上传输大的文件。

默认情况下 FTP 协议使用 TCP 端口中的 20 和 21 这两个端口,其中 20 用于传输数据,21 用于传输控制信息。FTP TCP 21 号端口上进行文件传输时,每次都会建立一个用于数据传输的 TCP 连接,数据传输完毕后,传输数据的这条连接也会被断开,在控制用的连接上继续进行命令或应答的处理。

SMTP

提供电子邮件服务的协议叫做 SMTP(Simple Mail Transfer Protocol), SMTP 在传输层也是用了 TCP 协议。

早起电子邮件是在发送端主机和接收端主机之间直接建立 TCP 连接。发送方编写好邮件之后会将邮件保存在磁盘中,然后与接受主机建立 TCP 连接,将邮件发送到接受主机的磁盘中。当发送方把邮件发送后,再从本地磁盘中删除邮件。如果接受主机因为特殊情况无法接收,发送端将等待一段时间后重新发送。

这种方法虽然能够保证电子邮件的完整性和有效性,但却不适合当今的互联网,因为早期的电子邮件只能在线发送,这种方式显然不够成熟。

针对于此,提出了邮件服务器的概念。邮件服务器构成了整个邮件系统的核心。每个接收方在其中的邮件服务器上会有一个邮箱(mailbox) 存在。用户的邮箱管理和维护发送给他的报文。

一个典型的邮件发送过程是:从发送方的用户代理开始,传输到发送方的邮件服务器,再传输到接收方的邮件服务器,然后在这里被分发到接收方的邮箱中。用接收方的用户想要从邮箱中读取邮件时,他的邮件服务器会对用户进行认证。如果发送方发送的邮件无法正确交付给接收方的服务器,那么发送方的用户代理会把邮件存储在一个报文队列(message queue)中,并在以后尝试再次发送,通常每 30 分钟发送一次,如果一段时间后还发送不成功,服务器就会删除报文队列中的邮件并以电子邮件的方式通知发送方。

现在你知道了两台邮件服务器邮件发送的大体过程,那么,SMTP 是如何将邮件从 Alice 邮件服务器发送到 Bob 的邮件服务器的呢?主要分为下面三个阶段

  • 建立连接:在这一阶段,SMTP 客户请求与服务器的25端口建立一个 TCP 连接。一旦连接建立,SMTP 服务器和客户就开始相互通告自己的域名,同时确认对方的域名。
  • 邮件传送:一旦连接建立后,就开始邮件传输。SMTP 依靠 TCP 能够将邮件准确无误地传输到接收方的邮件服务器中。SMTP 客户将邮件的源地址、目的地址和邮件的具体内容传递给 SMTP 服务器,SMTP 服务器进行相应的响应并接收邮件。
  • 连接释放:SMTP 客户发出退出命令,服务器在处理命令后进行响应,随后关闭 TCP 连接。

MIME 类型

最一开始,互联网中的电子邮件只能处理文本格式,后来也逐渐扩展为 MIME 类型,我们上面也简单提到了一句 MIME 类型,MIME(Multipurpose Internet Mail Extensions) 是用途互联网邮件扩展类型。

它是一个互联网标准,扩展了电子邮件标准,使其能够支持很多格式,这些格式如下

  • 超文本标记语言文本 .html text/html
  • xml文档 .xml text/xml
  • 普通文本 .txt text/plain
  • PNG图像 .png image/png
  • GIF图形 .gif image/gif
  • JPEG图形 .jpeg,.jpg image/jpeg
  • AVI 文件 .avi video/x-msvideo 等。

后记

文章涵盖了许多应用层协议,包括 HTTP、DNS、SMTP、FTP、TELNET 协议等

这些应用层协议我们在日常工作中都会用到,我们不仅仅是用户,还是程序员,势必要对其进行了解,我给你画了一些图帮助你理解清楚这些协议,简化的背后却是复杂而艰巨的规范标准和开发的复杂。

DHCP协议

原文:https://zwmst.com/2682.html

哈喽小伙伴们大家好啊,这里是 cxuan 计算机网络连载系列的文章第 11 篇,本篇文章我们来聊一聊 DHCP 协议。在聊之前,先想象一个场景。

你现在站在地铁上或者坐在办公室中,你的手机也好,电脑也好都有一个 IP 地址假如这个 IP 地址是你动输入的,你需要写下面这些东西 ……

电脑配置这些还好,直接咔咔咔的配置完了,如果你用的是手机,那么你需要点到 IP 地址,输入 IP 地址,点到子网掩码,输入子网掩码,点到默认路由,输入路由,点到 DNS 服务器,输入 DNS 服务器 …… 这玩意这么麻烦啊,恰好你刚配置完,领导叫你开会,得嘞,刚配置好的地址白瞎了。换了一个环境,需要重新配置 IP 地址,于是你把上面的步骤再重复了一遍,这时候散会了,然后你炸了。。。。。。

我们还省去了你有可能配置错误的时候。

上面这段描述最让人恼火的就是你需要手动配置 IP 地址,woc,为啥不能设置成自动配置 IP 地址呢?谁说不能的,能!那就是用 DHCP, 这也是我们下面要聊的内容。

认识 DHCP

DHCP 的全称是 Dynamic Host Configuration Protocol 动态主机配置协议。使用 DHCP 就能实现自动设置 IP 地址、统一管理 IP 地址分配。也就是不管你是在开会还是在工位干活,都省去了手动配置 IP 地址这一步繁琐的操作,同时 DHCP 也大大减少了可能由于你手动分配 IP 地址导致错误的几率。

DHCP 与 IP 密切相关,它是 IP 网络上所使用的协议。如果你想要使用 DHCP 提供服务的话,那么在整条通信链路上就需要 DHCP 服务器的存在,连接到网络的设备使用 DHCP 协议从 DHCP 服务器请求 IP 地址。DHCP 服务器会为设备分配一个唯一的 IP 地址。

除了 IP 地址外,DHCP 服务器还会把子网掩码,默认路由,DNS 服务器告诉你。

DHCP 服务器

现在,你不需要手动配置 IP 地址,也不再需要管理 IP 地址了,管理权已经移交给了 DHCP 服务器,DHCP 服务器会维护 IP 地址池,在网络上启动时会将地址租借给启用 DHCP 的客户端。

由于 IP 地址是动态的(临时分配)而不是静态的(永久分配),因此不再使用的 IP 地址会自动返回 IP 地址池中进行重新分配。

那么 DHCP 服务器由谁维护呢?

网络管理员负责建立 DHCP 服务器,并以租约的形式向启用 DHCP 的客户端提供地址配置,啊,既然不需要我管理,那就很舒服了~

好了,现在你能舒舒服服的开发了,你用 postman 配了一条 192.168.1.4/x/x 的接口进行请求,请求能够顺利进行,但是过了一段时间后,你发现 192.168.1.4/x/x 这个接口请求不通了,这是为啥呢?然后你用 ipconfig 查询了一下自己的 IP 地址,发现 IP 地址变成了 192.168.1.7,怎么我用着用着 IP 地址还改了?DHCP 是个垃圾,破玩意!!@#¥%¥%……¥%

其实,这也是一个 DHCP 服务器的一个功能,DHCP 服务器通常为每个客户端分配一个唯一的动态 IP 地址,当该 IP 地址的客户端租约到期时,该地址就会更改。

唯一意思说的就是,如果你手动设置了一个静态 IP,同时 DHCP 服务器分配了一个动态 IP,这个动态 IP 和静态 IP 一样,那么必然会有一个客户端无法上网。

我就遇到过这种情况,我使用虚拟机配置的静态 IP 是192.168.1.8,手机使用 DHCP 也同样配置了 192.168.1.8 的 IP 地址,此时我的虚拟机还没有接入网络,当我接入网络时,我怎样也连不上虚拟机了,一查才发现 IP 地址冲突了 ……

虽然 DHCP 服务器能提供 IP 地址,但是他怎么知道哪些 IP 地址空闲,哪些 IP 地址正在使用呢?

实际上,这些信息都配置在了数据库中,下面我们就来一起看一下 DHCP 服务器维护了哪些信息。

  • 网络上所有有效的 TCP/IP 配置参数

这些参数主要包括主机名(Host name)、DHCP 客户端(DHCP client)、域名(Domain name)、IP 地址IP address)、网关(Netmask)、广播地址(Broadcast address)、默认路由(default rooter)

  • 有效的 IP 地址和排除的 IP 地址,保存在 IP 地址池中等待分配给客户端
  • 为某些特定的 DHCP 客户端保留的地址,这些地址是静态 IP,这样可以将单个 IP 地址一致地分配给单个DHCP 客户端

好了,现在你知道 DHCP 服务器都需要保存哪些信息了,并且看过上面的内容,你应该知道一个 DHCP 的组件有哪些了,下面我们就来聊一聊 DHCP 中都有哪些组件,这些组件缺一不可。

DHCP 的组件

使用 DHCP 时,了解所有的组件很重要,下面我为你列出了一些 DHCP 的组件和它们的作用都是什么。

  • DHCP Server,DHCP 服务器,这个大家肯定都知道,因为我们上面就一直在探讨 DHCP 服务器的内容,使用 DHCP ,是一定要有 DHCP 服务器的,要不然谁给你提供服务呢?
  • DHCP Client,DHCP 客户端,这个大家应该也知道,毕竟只有一个服务端不行啊,没有客户端你为谁服务啊?DHCP 的客户端可以是计算机、移动设备或者其他需要连接到网络的任何设备,默认情况下,大多数配置为接收 DHCP 信息
  • Ip address pool: 你得有 IP 地址池啊,虽然说你 DHCP 提供服务,但是你也得有工具啊,没有工具玩儿啥?IP 地址池是 DHCP 客户端可用的地址范围,这个地址范围通常由最低 -> 最高顺序发送。
  • Subnet:这个组件是子网,IP 网络可以划分一段一段的子网,子网更有助于网络管理。
  • Lease:租期,这个表示的就是 IP 地址续约的期限,同时也代表了客户端保留 IP 地址信息的时间长度,一般租约到期时,客户端必须续约。
  • DHCP relay:DHCP 中继器,这个一般比较难想到,DHCP 中继器一般是路由器或者主机。DHCP 中继器通常应对 DHCP 服务器和 DHCP 客户端不再同一个网断的情况,如果 DHCP 服务器和 DHCP 客户端在同一个网段下,那么客户端可以正确的获得动态分配的 IP 地址;如果不在的话,就需要使用 DHCP 中继器进行中继代理。

现在 DHCP 的组件你了解后,下面我就要和你聊聊 DHCP 的工作机制了。

DHCP 工作机制

在聊 DHCP 工作机制前,先来看一下 DHCP 的报文消息

DHCP 报文

DHCP 报文共有一下几种:

  • DHCP DISCOVER :客户端开始 DHCP 过程发送的包,是 DHCP 协议的开始
  • DHCP OFFER :服务器接收到 DHCPDISCOVER 之后做出的响应,它包括了给予客户端的 IP 租约过期时间、服务器的识别符以及其他信息
  • DHCP REQUEST :客户端对于服务器发出的 DHCPOFFER 所做出的响应。在续约租期的时候同样会使用。
  • DHCP ACK :服务器在接收到客户端发来的 DHCPREQUEST 之后发出的成功确认的报文。在建立连接的时候,客户端在接收到这个报文之后才会确认分配给它的 IP 和其他信息可以被允许使用。
  • DHCP NAK :DHCPACK 的相反的报文,表示服务器拒绝了客户端的请求。
  • DHCP RELEASE :一般出现在客户端关机、下线等状况。这个报文将会使 DHCP 服务器释放发出此报文的客户端的 IP 地址
  • DHCP INFORM :客户端发出的向服务器请求一些信息的报文
  • DHCP DECLINE :当客户端发现服务器分配的 IP 地址无法使用(如 IP 地址冲突时),将发出此报文,通知服务器禁止使用该 IP 地址。

DHCP 的工作机制比较简单,无非就是客户端向服务器租借 IP ,服务器提供 IP 给客户端的这个过程呗。嗯,你很聪明,大致是这样的,不过有一些细节需要注意下,下面我通过两张图来和你聊一下。

关于从 DHCP 中获取 IP 地址的流程,主要分为两个阶段。

第一个阶段是 DHCP 查找包的阶段

查找包的阶段主要分为两步:第一步是 DHCP 发现包,第二步是 DHCP 提供包。

DHCP 客户端在通信链路上发起广播,看看链路上有没有能提供 DHCP 包的服务器,然后通信链路上的各个节点会检查自己是否能够提供 DHCP 包,这时 DHCP 服务器说它能够提供 DHCP 包,然后 DHCP 就发出一个 DHCP 包沿着通信链路返回给 DHCP 客户端。

第二个阶段是 DHCP 的请求阶段。

DHCP 的请求包也分为两步:第一步是 DHCP 请求包,第二步是 DHCP 确认包。

DHCP 客户端在通信链路上发起 DHCP 请求包,请求包主要是告诉 DHCP 服务器,它想要用上一步提供的网络设置,然后 DHCP 服务器向 DHCP 客户端发送确认包,表示允许 DHCP 客户端使用第二步发送的网络设置。

至此,DHCP 的网络设置就结束了,然后通信链路上的主机之间就可以进行 TCP/IP 通信了。

当不需要 IP 地址时,可以发送 DHCP 解除包(DHCP RELEASE)进行解除。另外,DHCP 的设置中通常会有一个租期时间的设定,DHCP 客户端在这个时限内可以发送 DHCP 请求包通知想要延长这个期限。

DHCP 状态机

我们上面知道 DHCP 会发送几种请求包,我们知道,动作肯定伴随着状态的更改,DHCP 也是一样的,在 DHCP 发送/接收各种包的时候,其状态也在发生相应的改变。DHCP 协议可以在客户端和服务器上运行状态机。状态决定了协议接下来要处理的消息类型。

状态之间的转换(箭头)是由于接收和发送消息或者计时器到期才发生的转换。下面是 DHCP 的状态轮转图。

客户端在开始时没有消息,此时处于 INIT 状态,然后客户端会在通信链路上发起一个广播 DHCP DISCOVER

Selecting 选择状态下,客户端会收集 DHCPOFFER 消息,直到确定要使用的地址和服务器为止。

一旦 DHCP 客户端做好选择后,它就会发送 DHCPREQUEST 消息并进入 Requesting 状态,在这个状态下它很可能收到并不需要的 ACK 响应,如果这个状态下没有找到合适的地址的话,那么客户端就会发送DHCPDECLINE 并恢复为 INIT 状态,但是这种发生的概率比较小。

在处于 Requesting 状态下的客户端很可能接受发送过来的 DHCPACK 消息,获取超时时间 T1T2,然后进入 Bound 绑定状态,在这个状态下可以使用地址直到地址过期。

在第一个计时器 T1 到期时,客户端会进入 renewing 续订状态,并重新尝试建立租约时间,如果收到新的 ACK 消息就表示续订成功,然后就恢复为 Bound 状态。

如果没有收到 ACK 那么 T2 会最终过期进入 Rebinding 状态,进入这个状态的客户端会重新尝试获取地址,如果最终的租约到期,那么客户端必须放弃租约地址,并且如果没有其他地址或网络连接要使用,客户端将断开连接。

DHCP 冲突

现在我们讨论一下 DHCP 冲突的问题,DHCP 冲突其实就是 IP 重了,当一个子网中两个或者更多主机配置了相同的 IP 地址时,就会发生 IP 冲突的现象。发生这种情况可能导致的后果是两个冲突的主机混在一起,一台主机可能接收了另一台主机的数据包。

那么造成这种情况的原因是啥呢?

造成这种情况的原因有很多,这里我列举两个可能出现的情况:

  • 第一种情况是一台主机配置了静态 IP 地址,这台主机联网后,其 IP 地址不会在 DHCP 服务器中,然后另外一个主机入网,DHCP 服务器给这台主机自动分配了相同的 IP 地址,这两个地址就产生了 IP 冲突。

  • 第二种情况是,客户端从 DHCP 服务器获得了 IP 地址,然后这台主机下线了,随着租约到期,DHCP 会将这个 IP 地址又分配给了其他主机,等到这个主机重新上线后,由于某种原因,计算机无法访问 DHCP 服务器,这种情况下会造成 IP 冲突。

当检测到 IP 冲突时,通常 Windows 系统和 Mac 系统会弹出 IP 冲突的弹窗。

DHCP 中继代理

常规家庭网络(土豪除外)中大多数都只有一个以太网,也就是 LAN 网段,一个 DHCP 服务器完全可以满足 LAN 中的客户机使用。但是,在更复杂的网络中,比如企业或者学校,一台 DHCP 服务器显然就无法满足了。因此,这种情况下,往往需要 DHCP 的统一管理,具体实现方式可以通过 DHCP 中继代理 来转发 DHCP 流量,如下图所示。

如上图所示,存在两个网段 A 和网段 B,DHCP 客户机和 DHCP 服务器不在一个网段内,所以我们在通信链路上架设了一个中继代理,DHCP 客户机通过访问中继代理以达到访问 DHCP 服务器的目的。

使用这种方式,我们不再需要在每个网段都设置一个 DHCP 服务器,只需要在每个网段架设一个中继代理即可。它可以设置 DHCP 服务器的 IP 地址,从而可以在 DHCP 服务器上为每个网段注册 IP 地址的分配范围。

DHCP 客户端会向 DHCP 中继代理发送 DHCP 请求包,而 DHCP 中继代理在收到这个广播包之后再以单播的形式发送给 DHCP 服务器。服务器收到该包以后再向 DHCP 中继代理返回应答,并由 DHCP 中继代理将此包发送给 DHCP 客户端。

DHCP 认证

我们总是假想所有情况都能够顺利进行,害怕出问题,这也许意味着我永远只是个初级程序员吧。我们上面探讨的 DHCP 服务器都是合理的、合法的,但是互联网是一把双刃剑,不是所有人都是合法公民。如果假设了一个未经授权的 DHCP 服务器怎么办?它很可能会对网络造成影响。

为了避免这些问题,在 [RFC3118] 中指定了一种认证 DHCP 消息的方法。 它定义了一个 DHCP 选项,即Authentication 选项,如下所示

认证选项的主要目的就是确定 DHCP 消息是否来自一个授权的发送方

身份验证的代码(code)属性值是 90,而长度(Length)给出了选项中的字节数(不包括代码和长度字段的字节)。如果协议(Protocol)和算法(Algorithm)属性被设置为 0 ,则认证信息字段将保存一个简单的共享配置的 token,token 大家开发应该都接触过把,就是一条认证信息。只要配置令牌在客户端和服务器上匹配,这条消息就会被接受。

我们上面聊到的只是其中的一种,还有一种更安全的方法是涉及所谓的延迟身份认证,如果协议和算法都被设置为 1,就表示使用了延迟身份认证。在这种情况下,客户端的 DHCPDISCOVER 消息或 DHCPINFORM 消息包括身份验证选项,并且服务器以其 DHCPOFFER 或 DHCPACK 消息中包含的身份验证信息进行响应。这个认证信息中包括一个消息认证码,它提供对发送方的认证和消息的完整性校验。RDM 表示中继检测,中继检测包括一个单项递增的值,只要经过一个代理中继,那么这个中继检测的值就会 + 1。

虽然 DHCP 认证能够确保安全性,但是它没有被广泛使用,原因有两点:

  • 首先,该方法要求在 DHCP 服务器和每个需要身份验证的客户端之间分配共享密钥。
  • 其次,在 DHCP 已经被广泛使用之后,才指定了 Authentication 选项。

总结

这篇文章我和你探讨了计算机网络中一个比较容易忽视的概念,为什么说他容易忽视呢?因为我们平常开发过程中基本上不会管 IP 地址的配置的,也就是环境搭建的时候会用到一些,但是要系统学习计算机网络的话,DHCP 的重要性不可忽视,DHCP 包括工作机制、DHCP 报文消息,DHCP 状态机、DHCP 认证这些都是需要你了解并掌握的。

DNS协议

原文:https://zwmst.com/2684.html

试想一个问题,我们人类可以有多少种识别自己的方式?可以通过身份证来识别,可以通过社保卡号来识别,也可以通过驾驶证来识别,尽管我们有多种识别方式,但在特定的环境下,某种识别方法可能比另一种方法更为适合。因特网上的主机和人类一样,可以使用多种识别方式进行标识。互联网上主机的一种标识方法是使用它的 主机名(hostname) ,如 www.facebook.com、 www.google.com 等。但是这是我们人类的记忆方式,路由器不会这么理解,路由器喜欢定长的、有层次结构的 IP地址

如果你还不理解 IP 的话,可以翻阅一下我的这篇文章计算机网络层

IP 地址现在简单表述一下,就是一个由 4 字节组成,并有着严格的层次结构。例如 121.7.106.83 这样一个 IP 地址,其中的每个字节都可以用 . 进行分割,表示了 0 - 255 的十进制数字。

然而,路由器喜欢的是 IP 地址进行解析,我们人类却便于记忆的是网址,那么路由器如何把 IP 地址解析为我们熟悉的网址地址呢?这时候就需要 DNS 出现了。

DNS 的全称是 Domain Name System,DNS ,它是一个由分层的 DNS 服务器(DNS server)实现的分布式数据库;它还是一个使得主机能够查询分布式数据库的应用层协议。DNS 服务器通常是运行 BIND(Berkeley Internet Name Domain) 软件的 UNIX 机器。DNS 协议运行在 UDP 之上,使用 53 端口。

DNS 基本概述

与 HTTP、FTP 和 SMTP 一样,DNS 协议也是应用层的协议,DNS 使用客户-服务器模式运行在通信的端系统之间,在通信的端系统之间通过下面的端到端运输协议来传送 DNS 报文。但是 DNS 不是一个直接和用户打交道的应用。DNS 是为因特网上的用户应用程序以及其他软件提供一种核心功能。

DNS 通常不是一门独立的协议,它通常为其他应用层协议所使用,这些协议包括 HTTP、SMTP 和 FTP,将用户提供的主机名解析为 IP 地址。

下面根据一个示例来描述一下这个 DNS 解析过程,这个和你输入网址后,浏览器做了什么操作有异曲同工之处

你在浏览器键入 www.someschool.edu/index.html 时会发生什么现象?为了使用户主机能够将一个 HTTP 请求报文发送到 Web 服务器 www.someschool.edu ,会经历如下操作

  • 同一台用户主机上运行着 DNS 应用的客户端
  • 浏览器从上述 URL 中抽取出主机名 www.someschool.edu ,并将这台主机名传给 DNS 应用的客户端
  • DNS 客户向 DNS 服务器发送一个包含主机名的请求。
  • DNS 客户最终会收到一份回答报文,其中包含该目标主机的 IP 地址
  • 一旦浏览器收到目标主机的 IP 地址后,它就能够向位于该 IP 地址 80 端口的 HTTP 服务器进程发起一个 TCP 连接。

除了提供 IP 地址到主机名的转换,DNS 还提供了下面几种重要的服务

  • 主机别名(host aliasing),有着复杂的主机名的主机能够拥有一个或多个其他别名,比如说一台名为 relay1.west-coast.enterprise.com 的主机,同时会拥有 enterprise.com 和 www.enterprise.com 的两个主机别名,在这种情况下,relay1.west-coast.enterprise.com 也称为 规范主机名,而主机别名要比规范主机名更加容易记忆。应用程序可以调用 DNS 来获得主机别名对应的规范主机名以及主机的 IP地址。
  • 邮件服务器别名(mail server aliasing),同样的,电子邮件的应用程序也可以调用 DNS 对提供的主机名进行解析。
  • 负载分配(load distribution),DNS 也用于冗余的服务器之间进行负载分配。繁忙的站点例如 cnn.com 被冗余分布在多台服务器上,每台服务器运行在不同的端系统之间,每个都有着不同的 IP 地址。由于这些冗余的 Web 服务器,一个 IP 地址集合因此与同一个规范主机名联系。DNS 数据库中存储着这些 IP 地址的集合。由于客户端每次都会发起 HTTP 请求,所以 DNS 就会在所有这些冗余的 Web 服务器之间循环分配了负载。

DNS 工作概述

假设运行在用户主机上的某些应用程序(如 Web 浏览器或邮件阅读器) 需要将主机名转换为 IP 地址。这些应用程序将调用 DNS 的客户端,并指明需要被转换的主机名。用户主机上的 DNS 收到后,会使用 UDP 通过 53 端口向网络上发送一个 DNS 查询报文,经过一段时间后,用户主机上的 DNS 会收到一个主机名对应的 DNS 回答报文。因此,从用户主机的角度来看,DNS 就像是一个黑盒子,其内部的操作你无法看到。但是实际上,实现 DNS 这个服务的黑盒子非常复杂,它由分布于全球的大量 DNS 服务器以及定义了 DNS 服务器与查询主机通信方式的应用层协议组成。

DNS 最早的设计是只有一台 DNS 服务器。这台服务器会包含所有的 DNS 映射。这是一种集中式的设计,这种设计并不适用于当今的互联网,因为互联网有着数量巨大并且持续增长的主机,这种集中式的设计会存在以下几个问题

  • 单点故障(a single point of failure),如果 DNS 服务器崩溃,那么整个网络随之瘫痪。
  • 通信容量(traaffic volume),单个 DNS 服务器不得不处理所有的 DNS 查询,这种查询级别可能是上百万上千万级
  • 远距离集中式数据库(distant centralized database),单个 DNS 服务器不可能 邻近 所有的用户,假设在美国的 DNS 服务器不可能临近让澳大利亚的查询使用,其中查询请求势必会经过低速和拥堵的链路,造成严重的时延。
  • 维护(maintenance),维护成本巨大,而且还需要频繁更新。

所以 DNS 不可能集中式设计,它完全没有可扩展能力,因此采用分布式设计,所以这种设计的特点如下

分布式、层次数据库

首先分布式设计首先解决的问题就是 DNS 服务器的扩展性问题,因此 DNS 使用了大量的 DNS 服务器,它们的组织模式一般是层次方式,并且分布在全世界范围内。没有一台 DNS 服务器能够拥有因特网上所有主机的映射。相反,这些映射分布在所有的 DNS 服务器上。

大致来说有三种 DNS 服务器:根 DNS 服务器顶级域(Top-Level Domain, TLD) DNS 服务器权威 DNS 服务器 。这些服务器的层次模型如下图所示

假设现在一个 DNS 客户端想要知道 www.amazon.com 的 IP 地址,那么上面的域名服务器是如何解析的呢?首先,客户端会先根服务器之一进行关联,它将返回顶级域名 com 的 TLD 服务器的 IP 地址。该客户则与这些 TLD 服务器之一联系,它将为 amazon.com 返回权威服务器的 IP 地址。最后,该客户与 amazom.com 权威服务器之一联系,它为 www.amazom.com 返回其 IP 地址。

DNS 层次结构

我们现在来讨论一下上面域名服务器的层次系统

  • 根 DNS 服务器 ,有 400 多个根域名服务器遍及全世界,这些根域名服务器由 13 个不同的组织管理。根域名服务器的清单和组织机构可以在 https://root-servers.org/ 中找到,根域名服务器提供 TLD 服务器的 IP 地址。
  • 顶级域 DNS 服务器,对于每个顶级域名比如 com、org、net、edu 和 gov 和所有的国家级域名 uk、fr、ca 和 jp 都有 TLD 服务器或服务器集群。所有的顶级域列表参见 https://tld-list.com/ 。TDL 服务器提供了权威 DNS 服务器的 IP 地址。
  • 权威 DNS 服务器,在因特网上具有公共可访问的主机,如 Web 服务器和邮件服务器,这些主机的组织机构必须提供可供访问的 DNS 记录,这些记录将这些主机的名字映射为 IP 地址。一个组织机构的权威 DNS 服务器收藏了这些 DNS 记录。

DNS 查询步骤

下面我们描述一下 DNS 的查询步骤,从 DNS 解析 IP 再到 DNS 返回的一系列流程。

注意:通常情况下 DNS 会将查找的信息缓存在浏览器或者计算机本地中,如果有相同的请求到来时,就不再会进行 DNS 查找,而会直接返回结果。

通常情况下,DNS 的查找会经历下面这些步骤

  1. 用户在浏览器中输入网址 www.example.com 并点击回车后,查询会进入网络,并且由 DNS 解析器进行接收。

  2. DNS 解析器会向根域名发起查询请求,要求返回顶级域名的地址。

  3. 根 DNS 服务器会注意到请求地址的前缀并向 DNS 解析器返回 com 的顶级域名服务器(TLD) 的 IP 地址列表。

  4. 然后,DNS 解析器会向 TLD 服务器发送查询报文

  5. TLD 服务器接收请求后,会根据域名的地址把权威 DNS 服务器的 IP 地址返回给 DNS 解析器。

  6. 最后,DNS 解析器将查询直接发送到权威 DNS 服务器

  7. 权威 DNS 服务器将 IP 地址返回给 DNS 解析器

  8. DNS 解析器将会使用 IP 地址响应 Web 浏览器

一旦 DNS 查找的步骤返回了 example.com 的 IP 地址,浏览器就可以请求网页了。

整个流程如下图所示

DNS 解析器

进行 DNS 查询的主机和软件叫做 DNS 解析器,用户所使用的工作站和个人电脑都属于解析器。一个解析器要至少注册一个以上域名服务器的 IP 地址。DNS 解析器是 DNS 查找的第一站,其负责与发出初始请求的客户端打交道。解析器启动查询序列,最终使 URL 转换为必要的 IP 地址。

DNS 递归查询和 DNS 递归解析器不同,该查询是指向需要解析该查询的 DNS 解析器发出请求。DNS 递归解析器是一种计算机,其接受递归查询并通过发出必要的请求来处理响应。

DNS 查询类型

DNS 查找中会出现三种类型的查询。通过组合使用这些查询,优化的 DNS 解析过程可缩短传输距离。在理想情况下,可以使用缓存的记录数据,从而使 DNS 域名服务器能够直接使用非递归查询。

  1. 递归查询:在递归查询中,DNS 客户端要求 DNS 服务器(一般为 DNS 递归解析器)将使用所请求的资源记录响应客户端,或者如果解析器无法找到该记录,则返回错误消息。

  2. 迭代查询:在迭代查询中,如果所查询的 DNS 服务器与查询名称不匹配,则其将返回对较低级别域名空间具有权威性的 DNS 服务器的引用。然后,DNS 客户端将对引用地址进行查询。此过程继续使用查询链中的其他 DNS 服务器,直至发生错误或超时为止。

  3. 非递归查询:当 DNS 解析器客户端查询 DNS 服务器以获取其有权访问的记录时通常会进行此查询,因为其对该记录具有权威性,或者该记录存在于其缓存内。DNS 服务器通常会缓存 DNS 记录,查询到来后能够直接返回缓存结果,以防止更多带宽消耗和上游服务器上的负载。

DNS 缓存

DNS 缓存(DNS caching) 有时也叫做 DNS 解析器缓存,它是由操作系统维护的临时数据库,它包含有最近的网站和其他 Internet 域的访问记录。也就是说, DNS 缓存只是计算机为了满足快速的响应速度而把已加载过的资源缓存起来,再次访问时可以直接快速引用的一项技术和手段。那么 DNS 的缓存是如何工作的呢?

DNS 缓存的工作流程

在浏览器向外部发出请求之前,计算机会拦截每个请求并在 DNS 缓存数据库中查找域名,该数据库包含有最近的域名列表,以及 DNS 首次发出请求时 DNS 为它们计算的地址。

DNS 缓存方式

DNS 数据可缓存到各种不同的位置上,每个位置均将存储 DNS 记录,它的生存时间由 TTL(DNS 字段) 来决定。

浏览器缓存

现如今的 Web 浏览器设计默认将 DNS 记录缓存一段时间。因为越靠近 Web 浏览器进行 DNS 缓存,为检查缓存并向 IP 地址发出请求的次数就越少。发出对 DNS 记录的请求时,浏览器缓存是针对所请求的记录而检查的第一个位置。

chrome 浏览器中,你可以使用 chrome://net-internals/#dns 查看 DNS 缓存的状态。这是基于 Windows 下查询的,我的 Mac 电脑输入上面 url 后无法查看 DNS ,只能 clear host cache,我也不知道为啥,可能是哪里设置的原因?

操作系统内核缓存

在浏览器缓存查询后,会进行操作系统级 DNS 解析器的查询,操作系统级 DNS 解析器是 DNS 查询离开你的计算机前的第二站,也是本地查询的最后一个步骤。

DNS 报文

共同实现 DNS 分布式数据库的所有 DNS 服务器存储了资源记录(Resource Record, RR),RR 提供了主机名到 IP 地址的映射。每个 DNS 回答报文中会包含一条或多条资源记录。RR 记录用于回复客户端查询。

资源记录是一个包含了下列字段的 4 元组

(Name, Value, Type, TTL)

RR 会有不同的类型,下面是不同类型的 RR 汇总表

DNS RR 类型 解释
A 记录 IPv4 主机记录,用于将域名映射到 IPv4 地址
AAAA 记录 IPv6 主机记录,用于将域名映射到 IPv6 地址
CNAME 记录 别名记录,用于映射 DNS 域名的别名
MX 记录 邮件交换器,用于将 DNS 域名映射到邮件服务器
PTR 记录 指针,用于反向查找(IP地址到域名解析)
SRV 记录 SRV记录,用于映射可用服务。

DNS 有两种报文,一种是查询报文,一种是响应报文,并且这两种报文有着相同的格式,下面是 DNS 的报文格式

上图显示了 DNS 的报文格式,其中事务 ID、标志、问题数量、回答资源记录数、权威名称服务器计数、附加资源记录数这六个字段是 DNS 的报文段首部,报文段首部一共有 12 个字节。

报文段首部

报文段首部是 DNS 报文的基础结构部分,下面我们对报文段首部中的每个字节进行描述

  • 事务 ID: 事务 ID 占用 2 个字节。它是 DNS 的标识,又叫做 标识符,对于请求报文和响应报文来说,这个字段的值是一样的,通过标识符可以区分 DNS 应答报文是对哪个请求进行响应的。
  • 标志:标志字段占用 2 个字节。标志字段有很多,而且也比较重要,下面列出来了所有的标志字段。

每个字段的含义如下

  • QR(Response): 1 bit 的 QR 标识报文是查询报文还是响应报文,查询报文时 QR = 0,响应报文时 QR = 1。

  • OpCode: 4 bit 的 OpCode 表示操作码,其中,0 表示标准查询,1 表示反向查询,2 表示服务器状态请求。

  • AA(Authoritative): 1 bit 的 AA 代表授权应答,这个 AA 只在响应报文中有效,值为 1 时,表示名称服务器是权威服务器;值为 0 时,表示不是权威服务器。

  • TC(Truncated): 截断标志位,值为 1 时,表示响应已超过 512 字节并且已经被截断,只返回前 512 个字节。

  • RD(Recursion Desired): 这个字段是期望递归字段,该字段在查询中设置,并在响应中返回。该标志告诉名称服务器必须处理这个查询,这种方式被称为一个递归查询。如果该位为 0,且被请求的名称服务器没有一个授权回答,它将返回一个能解答该查询的其他名称服务器列表。这种方式被称为迭代查询。

  • RA(Recursion Available): 可用递归字段,这个字段只出现在响应报文中。当值为 1 时,表示服务器支持递归查询。

  • zero: 保留字段,在所有的请求和应答报文中,它的值必须为 0。

  • AD: 这个字段表示信息是否是已授权。

  • CD: 这个字段表示是否禁用安全检查。

  • rcode(Reply code):这个字段是返回码字段,表示响应的差错状态。当值为 0 时,表示没有错误;当值为 1 时,表示报文格式错误(Format error),服务器不能理解请求的报文;当值为 2 时,表示域名服务器失败(Server failure),因为服务器的原因导致没办法处理这个请求;当值为 3 时,表示名字错误(Name Error),只有对授权域名解析服务器有意义,指出解析的域名不存在;当值为 4 时,表示查询类型不支持(Not Implemented),即域名服务器不支持查询类型;当值为 5 时,表示拒绝(Refused),一般是服务器由于设置的策略拒绝给出应答,如服务器不希望对某些请求者给出应答。

相信读者跟我一样,只看这些字段没什么意思,下面我们就通过抓包的方式,看一下具体的 DNS 报文。

现在我们可以看一下具体的 DNS 报文,通过 query 可知这是一个请求报文,这个报文的标识符是 0xcd28,它的标志如下

  • QR = 0 实锤了这就是一个请求。
  • 然后是四个字节的 OpCode,它的值是 0,表示这是一个标准查询。
  • 因为这是一个查询请求,所以没有 AA 字段出现。
  • 然后是截断标志位 Truncated,表示没有被截断。
  • 紧随其后的 RD = 1,表示希望得到递归回答。
  • 请求报文中没有 RA 字段出现。
  • 然后是保留字段 zero。
  • 紧随其后的 0 表示未经身份验证的数据是不可接受的。
  • 没有 rcode 字段的值

然后我们看一下响应报文

可以看到,标志位也是 0xcd28,可以说明这就是上面查询请求的响应。

查询请求已经解释过的报文我们这里就不再说明了,现在只解释一下请求报文中没有的内容

  • 紧随在 OpCode 后面的 AA 字段已经出现了,它的值为 0 ,表示不是权威 DNS 服务器的响应
  • 最后是 rcode 字段的响应,值为 0 时,表示没有错误。

问题区域

问题区域通常指报文格式中查询问题的区域部分。这部分用来显示 DNS 查询请求的问题,包括查询类型和查询类

这部分中每个字段的含义如下

  • 查询名:指定要查询的域名,有时候也是 IP 地址,用于反向查询。
  • 查询类型:DNS 查询请求的资源类型,通常查询类型为 A 类型,表示由域名获取对应的 IP 地址。
  • 查询类:地址类型,通常为互联网地址,值为 1 。

同样的,我们再使用 wireshark 查看一下问题区域

可以看到,这是对 mobile-gtalk.l.google.com 发起的 DNS 查询请求,查询类型是 A,那么得到的响应类型应该也是 A

如上图所示,响应类型是 A ,查询类的值通常是 1、254 和 255,分别表示互联网类、没有此类和所有类,这些是我们感兴趣的值,其他值通常不用于 TCP/IP 网络。

资源记录部分

资源记录部分是 DNS 报文的最后三个字段,包括回答问题区域、权威名称服务器记录、附加信息区域,这三个字段均采用一种称为资源记录的格式,如下图所示

资源记录部分的字段含义如下

  • 域名:DNS 请求的域名。
  • 类型:资源记录的类型,与问题部分中的查询类型值是一样的。
  • 类:地址类型、与问题中的查询类值一样的。
  • 生存时间:以秒为单位,表示资源记录的生命周期。
  • 资源数据长度:资源数据的长度。
  • 资源数据:表示按查询段要求返回的相关资源记录的数据。

资源记录部分只有在 DNS 响应包中才会出现。下面我们就来通过响应报文看一下具体的字段示例

其中,域名的值是 mobile-gtalk.l.google.com ,类型是 A,类是 1,生存时间是 5 秒,数据长度是 4 字节,资源数据表示的地址是 63.233.189.188。

SOA 记录

如果是权威 DNS 服务器的响应的话,会显示记录存储有关区域的重要信息,这种信息就是 SOA 记录。所有 的DNS 区域都需要一个 SOA 记录才能符合 IETF 标准。 SOA 记录对于区域传输也很重要。

SOA 记录除具有 DNS 解析器响应的字段外,还具有一些额外的字段,如下

具体字段含义

  • PNAME:即 Primary Name Server,这是区域的主要名称服务器的名称。
  • RNAME:即 Responsible authority’s mailbox,RNAME 代表管理员的电子邮件地址,@ 用 . 来表示,也就是说 admin.example.com 等同于 admin@example.com。
  • 序列号: 即 Serial Number ,区域序列号是该区域的唯一标识符。
  • 刷新间隔:即 Refresh Interval,在请求主服务器提供 SOA 记录以查看其是否已更新之前,辅助服务器应等待的时间(以秒为单位)。
  • 重试间隔:服务器应等待无响应的主要名称服务器再次请求更新的时间。
  • 过期限制:如果辅助服务器在这段时间内没有收到主服务器的响应,则应停止响应对该区域的查询。

上面提到了主要名称服务器和服务名称服务器,他们之间的关系如下

这块我们主要解释了 RR 类型为 A(IPv4) 和 SOA 的记录,除此之外还有很多类型,这篇文章就不再详细介绍了,读者朋友们可以阅读 《TCP/IP 卷一 协议》和 cloudflare 的官网 https://www.cloudflare.com/learning/dns/dns-records/ 查阅,值得一提的是,cloudflare 是一个学习网络协议非常好的网站。

DNS 安全

几乎所有的网络请求都会经过 DNS 查询,而且 DNS 和许多其他的 Internet 协议一样,系统设计时并未考虑到安全性,并且存在一些设计限制,这为 DNS 攻击创造了机会。

DNS 攻击主要有下面这几种方式

  • 第一种是 Dos 攻击,这种攻击的主要形式是使重要的 DNS 服务器比如 TLD 服务器或者根域名服务器过载,从而无法响应权威服务器的请求,使 DNS 查询不起作用。
  • 第二种攻击形式是 DNS 欺骗,通过改变 DNS 资源内容,比如伪装一个官方的 DNS 服务器,回复假的资源记录,从而导致主机在尝试与另一台机器连接时,连接至错误的 IP 地址。
  • 第三种攻击形式是 DNS 隧道,这种攻击使用其他网络协议通过 DNS 查询和响应建立隧道。攻击者可以使用 SSH、TCP 或者 HTTP 将恶意软件或者被盗信息传递到 DNS 查询中,这种方式使防火墙无法检测到,从而形成 DNS 攻击。
  • 第四种攻击形式是 DNS 劫持,在 DNS 劫持中,攻击者将查询重定向到其他域名服务器。这可以通过恶意软件或未经授权的 DNS 服务器修改来完成。尽管结果类似于 DNS 欺骗,但这是完全不同的攻击,因为它的目标是名称服务器上网站的 DNS 记录,而不是解析程序的缓存。
  • 第五章攻击形式是 DDoS 攻击,也叫做分布式拒绝服务带宽洪泛攻击,这种攻击形式相当于是 Dos 攻击的升级版

那么该如何防御 DNS 攻击呢?

防御 DNS 威胁的最广为人知的方法之一就是采用 DNSSEC 协议

DNSSEC

DNSSEC 又叫做 DNS 安全扩展,DNSSEC 通过对数据进行数字签名来保护其有效性,从而防止受到攻击。它是由 IETF 提供的一系列 DNS 安全认证的机制。DNSSEC 不会对数据进行加密,它只会验证你所访问的站点地址是否有效。

DNS 防火墙

有一些攻击是针对服务器进行的,这就需要 DNS 防火墙的登场了,DNS 防火墙是一种可以为 DNS 服务器提供许多安全和性能服务的工具。DNS 防火墙位于用户的 DNS 解析器和他们尝试访问的网站或服务的权威名称服务器之间。防火墙提供 限速访问,以关闭试图淹没服务器的攻击者。如果服务器确实由于攻击或任何其他原因而导致停机,则 DNS 防火墙可以通过提供来自缓存的 DNS 响应来使操作员的站点或服务正常运行。

除了上述两种防御手段外,本身 DNS 区域的运营商就会采取进步一措施保护 DNS 服务器,比如配置 DNS 基础架构,来防止 DDoS 攻击。

更多关于 DNS 的攻击和防御就是网络安全的主题,这篇文章就不再详细介绍了。

总结

这篇文章我用较多的字数为你介绍了 DNS 的基本概述,DNS 的工作机制,DNS 的查询方式,DNS 的缓存机制,我们还通过 WireShark 抓包带你认识了一下 DNS 的报文,最后我为你介绍了 DNS 的攻击手段和防御方式。

这是一篇入门 DNS 较全的文章,花了我一周多的时间来写这篇文章,这篇文章了解清楚后,基本上 DNS 的大部分问题你应该都能够回答,面试我估计也稳了。

计算机网络基础知识

原文:https://zwmst.com/2688.html

如果说计算机把我们从工业时代带到了信息时代,那么计算机网络就可以说把我们带到了网络时代。随着使用计算机人数的不断增加,计算机也经历了一系列的发展,从大型通用计算机 -> 超级计算机 -> 小型机 -> 个人电脑 -> 工作站 -> 便携式电脑 -> 智能手机终端等都是这一过程的产物。计算机网络也逐渐从独立模式演变为了 网络互联模式

可以看到,在独立模式下,每个人都需要排队等待其他人在一个机器上完成工作后,其他用户才能使用。这样的数据是单独管理的。

现在切换到了网络互联模式,在这种模式下,每个人都能独立的使用计算机,甚至还会有一个服务器,来为老大哥、cxuan 和 sonsong 提供服务。这样的数据是集中管理的。

计算机网络按规模进行划分,有 WAN(Wide Area Network, 广域网)LAN(Local area Network, 局域网)。如下图所示

上面是局域网,一般用在狭小区域内的网络,一个社区、一栋楼、办公室经常使用局域网。

距离较远的地方组成的网络一般是广域网。

最初,只是固定的几台计算机相连在一起形成计算机网络。这种网络一般是私有的,这几台计算机之外的计算机无法访问。随着时代的发展,人们开始尝试在私有网络上搭建更大的私有网络,逐渐又发展演变为互联网,现在我们每个人几乎都能够享有互联网带来的便利。

计算机网络发展历程

批处理

就和早期的计算机操作系统一样,最开始都要先经历批处理(atch Processing)阶段,批处理的目的也是为了能让更多的人使用计算机。

批处理就是事先将数据装入卡带或者磁带,并且由计算机按照一定的顺序进行读入。

当时这种计算机的价格比较昂贵,并不是每个人都能够使用的,这也就客观暗示着,只有专门的操作员才能使用计算机,用户把程序提交给操作员,由操作员排队执行程序,等一段时间后,用户再来提取结果。

这种计算机的高效性并没有很好的体现,甚至不如手动运算快。

分时系统

在批处理之后出现的就是分时系统了,分时系统指的是多个终端与同一个计算机连接,允许多个用户同时使用一台计算机。分时系统的出现实现了一人一机的目的,让用户感觉像是自己在使用计算机,实际上这是一种 独占性 的特性。

分时系统出现以来,计算机的可用性得到了极大的改善。分时系统的出现意味着计算机越来越贴近我们的生活。

还有一点需要注意:分时系统的出现促进了像是 BASIC 这种人机交互语言的诞生。

分时系统的出现,同时促进者计算机网络的出现。

计算机通信

在分时系统中,每个终端与计算机相连,这种独占性的方式并不是计算机之间的通信,因为每个人还是在独立的使用计算机。

到了 20 世纪 70 年代,计算机性能有了高速发展,同时体积也变得越来越小,使用计算机的门槛变得更低,越来越多的用户可以使用计算机。

没有一个计算机是信息孤岛促使着计算机网络的出现和发展。

计算机网络的诞生

20 世纪 80 年代,一种能够互连多种计算机的网络随之诞生。它能够让各式各样的计算机相连,从大型的超级计算机或主机到小型电脑。

20 世纪 90 年代,真正实现了一人一机的环境,但是这种环境的搭建仍然价格不菲。与此同时,诸如电子邮件(E-mail)万维网(WWW,World Wide Web) 等信息传播方式如雨后春笋般迎来了前所未有的发展,使得互联网从大到整个公司小到每个家庭内部,都得以广泛普及。

计算机网络的高速发展

现如今,越来越多的终端设备接入互联网,使互联网经历了前所未有的高潮,近年来 3G、4G、5G 通信技术的发展更是互联网高速发展的产物。

许多发展道路各不相同的网络技术也都正在向互联网靠拢。例如,曾经一直作为通信基础设施、支撑通信网络的电话网。随着互联网的发展,其地位也随着时间的推移被 IP(Internet Protocol) 网所取代,IP 也是互联网发展的产物。

网络安全

正如互联网也具有两面性,互联网的出现方便了用户,同时也方便了一些不法分子。互联网的便捷也带来了一些负面影响,计算机病毒的侵害、信息泄漏、网络诈骗层出不穷。

在现实生活中,通常情况下我们挨揍了会予以反击,但是在互联网中,你被不法分子攻击通常情况下是无力还击的,只能防御,因为还击需要你精通计算机和互联网,这通常情况下很多人办不到。

通常情况下公司和企业容易被作为不法分子获利的对象,所以,作为公司或者企业,要想不受攻击或者防御攻击,需要建立安全的互联网连接。

互联网协议

协议这个名词不仅局限于互联网范畴,也体现在日常生活中,比如情侣双方约定好在哪个地点吃饭,这个约定也是一种协议,比如你应聘成功了,企业会和你签订劳动合同,这种双方的雇佣关系也是一种 协议。注意自己一个人对自己的约定不能成为协议,协议的前提条件必须是多人约定。

那么网络协议是什么呢?

网络协议就是网络中(包括互联网)传递、管理信息的一些规范。如同人与人之间相互交流是需要遵循一定的规矩一样,计算机之间的相互通信需要共同遵守一定的规则,这些规则就称为网络协议

没有网络协议的互联网是混乱的,就和人类社会一样,人不能想怎么样就怎么样,你的行为约束是受到法律的约束的;那么互联网中的端系统也不能自己想发什么发什么,也是需要受到通信协议约束的。

我们一般都了解过 HTTP 协议, **HTTP 是一个在计算机世界里专门在两点之间传输文字、图片、音频、视频等超文本数据的约定和规范

但是互联网又不只有 HTTP 协议,它还有很多其他的比如 IP、TCP、UDP、DNS 协议等。下面是一些协议的汇总和介绍

网络体系结构 协议 主要用途
TCP/IP HTTP、SMTP、TELNET、IP、ICMP、TCP、UDP 等 主要用于互联网、局域网
IPX/SPX IPX、NPC、SPX 主要用于个人电脑局域网
AppleTalk AEP、ADP、DDP 苹果公司现有产品互联

ISO 在制定标准化的 OSI 之前,对网络体系结构相关的问题进行了充分的探讨,最终提出了作为通信协议设计指标的 OSI 参考模型。这一模型将通信协议中必要的功能分为了 7 层。通过这 7 层分层,使那些比较复杂的协议简单化。

在 OSI 标准模型中,每一层协议都接收由它下一层所提供的特定服务,并且负责为上一层提供服务,上层协议和下层协议之间通常会开放 接口,同一层之间的交互所遵守的约定叫做 协议

OSI 标准模型

上图只是简单的介绍了一下层与层之间的通信规范和上层与下层的通信规范,并未介绍具体的网络协议分层,实际上,OSI 标准模型将复杂的协议整理并分为了易于理解的 7 层。如下图所示

互联网的通信协议都对应了 7 层中的某一层,通过这一点,可以了解协议在整个网络模型中的作用,一般来说,各个分层的主要作用如下

  • 应用层:应用层是 OSI 标准模型的最顶层,是直接为应用进程提供服务的。其作用是在实现多个系统应用进程相互通信的同时,完成一系列业务处理所需的服务。包括文件传输、电子邮件远程登录和远端接口调用等协议。
  • 表示层: 表示层向上对应用进程服务,向下接收会话层提供的服务,表示层位于 OSI 标准模型的第六层,表示层的主要作用就是将设备的固有数据格式转换为网络标准传输格式。
  • 会话层:会话层位于 OSI 标准模型的第五层,它是建立在传输层之上,利用传输层提供的服务建立和维持会话。
  • 传输层:传输层位于 OSI 标准模型的第四层,它在整个 OSI 标准模型中起到了至关重要的作用。传输层涉及到两个节点之间的数据传输,向上层提供可靠的数据传输服务。传输层的服务一般要经历传输连接建立阶段,数据传输阶段,传输连接释放阶段 3 个阶段才算完成一个完整的服务过程。
  • 网络层:网络层位于 OSI 标准模型的第三层,它位于传输层和数据链路层的中间,将数据设法从源端经过若干个中间节点传送到另一端,从而向运输层提供最基本的端到端的数据传送服务。
  • 数据链路层:数据链路层位于物理层和网络层中间,数据链路层定义了在单个链路上如何传输数据。
  • 物理层:物理层是 OSI 标准模型中最低的一层,物理层是整个 OSI 协议的基础,就如同房屋的地基一样,物理层为设备之间的数据通信提供传输媒体及互连设备,为数据传输提供可靠的环境。

TCP/IP 协议簇

TCP/IP 协议是我们程序员接触最多的协议,实际上,TCP/IP 又被称为 TCP/IP 协议簇,它并不特指单纯的 TCP 和 IP 协议,而是容纳了许许多多的网络协议。

OSI 模型共有七层,从下到上分别是物理层、数据链路层、网络层、运输层、会话层、表示层和应用层。但是这显然是有些复杂的,所以在TCP/IP协议中,它们被简化为了四个层次

和 OSI 七层网络协议的主要区别如下

  • 应用层、表示层、会话层三个层次提供的服务相差不是很大,所以在 TCP/IP 协议中,它们被合并为应用层一个层次。
  • 由于数据链路层和物理层的内容很相似,所以在 TCP/IP 协议中它们被归并在网络接口层一个层次里。

我们的主要研究对象就是 TCP/IP 的四层协议。

下面 cxuan 和你聊一聊 TCP/IP 协议簇中都有哪些具体的协议

IP 协议

IP 是 互联网协议(Internet Protocol) ,位于网络层。IP是整个 TCP/IP 协议族的核心,也是构成互联网的基础。IP 能够为运输层提供数据分发,同时也能够组装数据供运输层使用。它将多个单个网络连接成为一个互联网,这样能够提高网络的可扩展性,实现大规模的网络互联。二是分割顶层网络和底层网络之间的耦合关系。

ICMP 协议

ICMP 协议是 Internet Control Message Protocol, ICMP 协议主要用于在 IP 主机、路由器之间传递控制消息。ICMP 属于网络层的协议,当遇到 IP 无法访问目标、IP 路由器无法按照当前传输速率转发数据包时,会自动发送 ICMP 消息,从这个角度来说,ICMP 协议可以看作是 错误侦测与回报机制,让我们检查网络状况、也能够确保连线的准确性。

ARP 协议

ARP 协议是 地址解析协议,即 Address Resolution Protocol,它能够根据 IP 地址获取物理地址。主机发送信息时会将包含目标 IP 的 ARP 请求广播到局域网络上的所有主机,并接受返回消息,以此来确定物理地址。收到消息后的物理地址和 IP 地址会在 ARP 中缓存一段时间,下次查询的时候直接从 ARP 中查询即可。

TCP 协议

TCP 就是 传输控制协议,也就是 Transmission Control Protocol,它是一种面向连接的、可靠的、基于字节流的传输协议,TCP 协议位于传输层,TCP 协议是 TCP/IP 协议簇中的核心协议,它最大的特点就是提供可靠的数据交付。

TCP 的主要特点有 慢启动、拥塞控制、快速重传、可恢复

UDP 协议

UDP 协议就是 用户数据报协议,也就是 User Datagram Protocol,UDP 也是一种传输层的协议,与 TCP 相比,UDP 提供一种不可靠的数据交付,也就是说,UDP 协议不保证数据是否到达目标节点,也就是说,当报文发送之后,是无法得知其是否安全完整到达的。UDP 是一种无连接的协议,传输数据之前源端和终端无需建立连接,不对数据报进行检查与修改,无须等待对方的应答,会出现分组丢失、重复、乱序等现象。但是 UDP 具有较好的实时性,工作效率较 TCP 协议高。

FTP 协议

FTP 协议是 文件传输协议,英文全称是 File Transfer Protocol,应用层协议之一,是 TCP/IP 协议的重要组成之一,FTP 协议分为服务器和客户端两部分,FTP 服务器用来存储文件,FTP 客户端用来访问 FTP 服务器上的文件,FTP 的传输效率比较高,所以一般使用 FTP 来传输大文件。

DNS 协议

DNS 协议是 域名系统协议,英文全称是 Domain Name System,它也是应用层的协议之一,DNS 协议是一个将域名和 IP 相互映射的分布式数据库系统。DNS 缓存能够加快网络资源的访问。

SMTP 协议

SMTP 协议是 简单邮件传输协议,英文全称是 Simple Mail Transfer Protocol,应用层协议之一,SMTP 主要是用作邮件收发协议,SMTP 服务器是遵循 SMTP 协议的发送邮件服务器,用来发送或中转用户发出的电子邮件

SLIP 协议

SLIP 协议是指串行线路网际协议(Serial Line Internet Protocol) ,是在串行通信线路上支持 TCP/IP 协议的一种点对点(Point-to-Point)式的链路层通信协议。

PPP 协议

PPP 协议是 Point to Point Protocol,即点对点协议,是一种链路层协议,是在为同等单元之间传输数据包而设计的。设计目的主要是用来通过拨号或专线方式建立点对点连接发送数据,使其成为各种主机、网桥和路由器之间简单连接的一种共通的解决方案。

网络核心概念

传输方式

网络根据传输方式可以进行分类,一般分成两种 面向连接型和面向无连接型

  • 面向连接型中,在发送数据之前,需要在主机之间建立一条通信线路。
  • 面向无连接型则不要求建立和断开连接,发送方可用于任何时候发送数据。接收端也不知道自己何时从哪里接收到数据。

分组交换

在互联网应用中,每个终端系统都可以彼此交换信息,这种信息也被称为 报文(Message),报文是一个集大成者,它可以包括你想要的任何东西,比如文字、数据、电子邮件、音频、视频等。为了从源目的地向端系统发送报文,需要把长报文切分为一个个小的数据块,这种数据块称为分组(Packets),也就是说,报文是由一个个小块的分组组成。在端系统和目的地之间,每个分组都要经过通信链路(communication links)分组交换机(switch packets) ,分组要在端系统之间交互需要经过一定的时间,如果两个端系统之间需要交互的分组为 L 比特,链路的传输速率问 R 比特/秒,那么传输时间就是 L / R秒。

一个端系统需要经过交换机给其他端系统发送分组,当分组到达交换机时,交换机就能够直接进行转发吗?不是的,交换机可没有这么无私,你想让我帮你转发分组?好,首先你需要先把整个分组数据都给我,我再考虑给你发送的问题,这就是存储转发传输

存储转发传输

存储转发传输指的就是交换机再转发分组的第一个比特前,必须要接受到整个分组,下面是一个存储转发传输的示意图,可以从图中窥出端倪

由图可以看出,分组 1、2、3 向交换器进行分组传输,并且交换机已经收到了分组1 发送的比特,此时交换机会直接进行转发吗?答案是不会的,交换机会把你的分组先缓存在本地。这就和考试作弊一样,一个学霸要经过学渣 A 给学渣 B 传答案,学渣 A 说,学渣 A 在收到答案后,它可能直接把卷子传过去吗?学渣A 说,等我先把答案抄完(保存功能)后再把卷子给你。

排队时延和分组丢失

什么?你认为交换机只能和一条通信链路进行相连?那你就大错特错了,这可是交换机啊,怎么可能只有一条通信链路呢?

所以我相信你一定能想到这个问题,多个端系统同时给交换器发送分组,一定存在顺序到达排队的问题。事实上,对于每条相连的链路,该分组交换机会有一个输出缓存(output buffer)输出队列(output queue) 与之对应,它用于存储路由器准备发往每条链路的分组。如果到达的分组发现路由器正在接收其他分组,那么新到达的分组就会在输出队列中进行排队,这种等待分组转发所耗费的时间也被称为 排队时延,上面提到分组交换器在转发分组时会进行等待,这种等待被称为 存储转发时延,所以我们现在了解到的有两种时延,但是其实是有四种时延。这些时延不是一成不变的,其变化程序取决于网络的拥塞程度。

因为队列是有容量限制的,当多条链路同时发送分组导致输出缓存无法接受超额的分组后,这些分组会丢失,这种情况被称为 丢包(packet loss),到达的分组或者已排队的分组将会被丢弃。

下图说明了一个简单的分组交换网络

在上图中,分组由三位数据平板展示,平板的宽度表示着分组数据的大小。所有的分组都有相同的宽度,因此也就有相同的数据包大小。下面来一个情景模拟: 假定主机 A 和 主机 B 要向主机 E 发送分组,主机 A 和 B 首先通过100 Mbps以太网链路将其数据包发送到第一台路由器,然后路由器将这些数据包定向到15 Mbps 的链路。如果在较短的时间间隔内,数据包到达路由器的速率(转换为每秒比特数)超过15 Mbps,则在数据包在链路输出缓冲区中排队之前,路由器上会发生拥塞,然后再传输到链路上。例如,如果主机 A 和主机 B 背靠背同时发了5包数据,那么这些数据包中的大多数将花费一些时间在队列中等待。实际上,这种情况与许多普通情况完全相似,例如,当我们排队等候银行出纳员或在收费站前等候时。

转发表和路由器选择协议

我们刚刚讲过,路由器和多个通信线路进行相连,如果每条通信链路同时发送分组的话,可能会造成排队和丢包的情况,然后分组在队列中等待发送,现在我就有一个问题问你,队列中的分组发向哪里?这是由什么机制决定的?

换个角度想问题,路由的作用是什么?把不同端系统中的数据包进行存储和转发 。在因特网中,每个端系统都会有一个 IP 地址,当原主机发送一个分组时,在分组的首部都会加上原主机的 IP 地址。每一台路由器都会有一个 转发表(forwarding table),当一个分组到达路由器后,路由器会检查分组的目的地址的一部分,并用目的地址搜索转发表,以找出适当的传送链路,然后映射成为输出链路进行转发。

那么问题来了,路由器内部是怎样设置转发表的呢?详细的我们后面会讲到,这里只是说个大概,路由器内部也是具有路由选择协议的,用于自动设置转发表。

电路交换

在计算机网络中,另一种通过网络链路和路由进行数据传输的另外一种方式就是 电路交换(circuit switching)。电路交换在资源预留上与分组交换不同,什么意思呢?就是分组交换不会预留每次端系统之间交互分组的缓存和链路传输速率,所以每次都会进行排队传输;而电路交换会预留这些信息。一个简单的例子帮助你理解:这就好比有两家餐馆,餐馆 A 需要预定而餐馆 B 不需要预定,对于可以预定的餐馆 A,我们必须先提前与其进行联系,但是当我们到达目的地时,我们能够立刻入座并选菜。而对于不需要预定的那家餐馆来说,你可能不需要提前联系,但是你必须承受到达目的地后需要排队的风险。

下面显示了一个电路交换网络

在这个网络中,4条链路用于4台电路交换机。这些链路中的每一条都有4条电路,因此每条链路能支持4条并行的链接。每台主机都与一台交换机直接相连,当两台主机需要通信时,该网络在两台主机之间创建一条专用的 端到端的链接(end-to-end connection)

分组交换和电路交换的对比

分组交换的支持者经常说分组交换不适合实时服务,因为它的端到端时延时不可预测的。而分组交换的支持者却认为分组交换提供了比电路交换更好的带宽共享;它比电路交换更加简单、更有效,实现成本更低。但是现在的趋势更多的是朝着分组交换的方向发展。

分组交换网的时延、丢包和吞吐量

因特网可以看成是一种基础设施,该基础设施为运行在端系统上的分布式应用提供服务。我们希望在计算机网络中任意两个端系统之间传递数据都不会造成数据丢失,然而这是一个极高的目标,实践中难以达到。所以,在实践中必须要限制端系统之间的 吞吐量 用来控制数据丢失。如果在端系统之间引入时延,也不能保证不会丢失分组问题。所以我们从时延、丢包和吞吐量三个层面来看一下计算机网络

分组交换中的时延

计算机网络中的分组从一台主机(源)出发,经过一系列路由器传输,在另一个端系统中结束它的历程。在这整个传输历程中,分组会涉及到四种最主要的时延:节点处理时延(nodal processing delay)、排队时延(queuing delay)、传输时延(total nodal delay)和传播时延(propagation delay)。这四种时延加起来就是 节点总时延(total nodal delay)

如果用 dproc dqueue dtrans dpop 分别表示处理时延、排队时延、传输时延和传播时延,则节点的总时延由以下公式决定: dnodal = dproc + dqueue + dtrans + dpop。

时延的类型

下面是一副典型的时延分布图,让我们从图中进行分析一下不同的时延类型

分组由端系统经过通信链路传输到路由器 A,路由器A 检查分组头部以映射出适当的传输链路,并将分组送入该链路。仅当该链路没有其他分组正在传输并且没有其他分组排在该该分组前面时,才能在这条链路上自由的传输该分组。如果该链路当前繁忙或者已经有其他分组排在该分组前面时,新到达的分组将会加入排队。下面我们分开讨论一下这四种时延

**节点处理时延

节点处理时延分为两部分,第一部分是路由器会检查分组的首部信息;第二部分是决定将分组传输到哪条通信链路所需要的时间。一般高速网络的节点处理时延都在微秒级和更低的数量级。在这种处理时延完成后,分组会发往路由器的转发队列中

**排队时延

在队列排队转发过程中,分组需要在队列中等待发送,分组在等待发送过程中消耗的时间被称为排队时延。排队时延的长短取决于先于该分组到达正在队列中排队的分组数量。如果该队列是空的,并且当前没有正在传输的分组,那么该分组的排队时延就是 0。如果处于网络高发时段,那么链路中传输的分组比较多,那么分组的排队时延将延长。实际的排队时延也可以到达微秒级。

**传输时延

队列 是路由器所用的主要的数据结构。队列的特征就是先进先出,先到达食堂的先打饭。传输时延是理论情况下单位时间内的传输比特所消耗的时间。比如分组的长度是 L 比特,R 表示从路由器 A 到路由器 B 的传输速率。那么传输时延就是 L / R 。这是将所有分组推向该链路所需要的时间。正是情况下传输时延通常也在毫秒到微秒级

**传播时延

从链路的起点到路由器 B 传播所需要的时间就是 传播时延。该比特以该链路的传播速率传播。该传播速率取决于链路的物理介质(双绞线、同轴电缆、光纤)。如果用公式来计算一下的话,该传播时延等于两台路由器之间的距离 / 传播速率。即传播速率是 d/s ,其中 d 是路由器 A 和 路由器 B 之间的距离,s 是该链路的传播速率。

传输时延和传播时延的比较

计算机网络中的传输时延和传播时延有时候难以区分,在这里解释一下,传输时延是路由器推出分组所需要的时间,它是分组长度和链路传输速率的函数,而与两台路由器之间的距离无关。而传播时延是一个比特从一台路由器传播到另一台路由器所需要的时间,它是两台路由器之间距离的倒数,而与分组长度和链路传输速率无关。从公式也可以看出来,传输时延是 L/R,也就是分组的长度 / 路由器之间传输速率。传播时延的公式是 d/s,也就是路由器之间的距离 / 传播速率。

排队时延

在这四种时延中,人们最感兴趣的时延或许就是排队时延了 dqueue。与其他三种时延(dproc、dtrans、dpop)不同的是,排队时延对不同的分组可能是不同的。例如,如果10个分组同时到达某个队列,第一个到达队列的分组没有排队时延,而最后到达的分组却要经受最大的排队时延(需要等待其他九个时延被传输)。

那么如何描述排队时延呢?或许可以从三个方面来考虑:流量到达队列的速率、链路的传输速率和到达流量的性质。即流量是周期性到达还是突发性到达,如果用 a 表示分组到达队列的平均速率( a 的单位是分组/秒,即 pkt/s)前面说过 R 表示的是传输速率,所以能够从队列中推出比特的速率(以 bps 即 b/s 位单位)。假设所有的分组都是由 L 比特组成的,那么比特到达队列的平均速率是 La bps。那么比率 La/R 被称为流量强度(traffic intensity),如果 La/R > 1,则比特到达队列的平均速率超过从队列传输出去的速率,这种情况下队列趋向于无限增加。所以,设计系统时流量强度不能大于1

现在考虑 La / R <= 1 时的情况。流量到达的性质将影响排队时延。如果流量是周期性到达的,即每 L / R 秒到达一个分组,则每个分组将到达一个空队列中,不会有排队时延。如果流量是 突发性 到达的,则可能会有很大的平均排队时延。一般可以用下面这幅图表示平均排队时延与流量强度的关系

横轴是 La/R 流量强度,纵轴是平均排队时延。

丢包

我们在上述的讨论过程中描绘了一个公式那就是 La/R 不能大于1,如果 La/R 大于1,那么到达的排队将会无穷大,而且路由器中的排队队列所容纳的分组是有限的,所以等到路由器队列堆满后,新到达的分组就无法被容纳,导致路由器 丢弃(drop) 该分组,即分组会 丢失(lost)

计算机网络中的吞吐量

除了丢包和时延外,衡量计算机另一个至关重要的性能测度是端到端的吞吐量。假如从主机 A 向主机 B 传送一个大文件,那么在任何时刻主机 B 接收到该文件的速率就是 瞬时吞吐量(instantaneous throughput)。如果该文件由 F 比特组成,主机 B 接收到所有 F 比特用去 T 秒,则文件的传送平均吞吐量(average throughput) 是 F / T bps。

单播、广播、多播和任播

在网络通信中,可以根据目标地址的数量对通信进行分类,可以分为 **单播、广播、多播和任播

单播(Unicast)

单播最大的特点就是 1 对 1,早期的固定电话就是单播的一个例子,单播示意图如下

广播(Broadcast)

我们一般小时候经常会跳广播体操,这就是广播的一个事例,主机和与他连接的所有端系统相连,主机将信号发送给所有的端系统。

多播(Multicast)

多播与广播很类似,也是将消息发送给多个接收主机,不同之处在于多播需要限定在某一组主机作为接收端。

任播(Anycast)

任播是在特定的多台主机中选出一个接收端的通信方式。虽然和多播很相似,但是行为与多播不同,任播是从许多目标机群中选出一台最符合网络条件的主机作为目标主机发送消息。然后被选中的特定主机将返回一个单播信号,然后再与目标主机进行通信。

物理媒介

网络的传输是需要介质的。一个比特数据包从一个端系统开始传输,经过一系列的链路和路由器,从而到达另外一个端系统。这个比特会被转发了很多次,那么这个比特经过传输的过程所跨越的媒介就被称为物理媒介(phhysical medium),物理媒介有很多种,比如双绞铜线、同轴电缆、多模光纤榄、陆地无线电频谱和卫星无线电频谱。其实大致分为两种:引导性媒介和非引导性媒介。

双绞铜线

最便宜且最常用的引导性传输媒介就是双绞铜线,多年以来,它一直应用于电话网。从电话机到本地电话交换机的连线超过 99% 都是使用的双绞铜线,例如下面就是双绞铜线的实物图

双绞铜线由两根绝缘的铜线组成,每根大约 1cm 粗,以规则的螺旋形状排列,通常许多双绞线捆扎在一起形成电缆,并在双绞馅的外面套上保护层。一对电缆构成了一个通信链路。无屏蔽双绞线一般常用在局域网(LAN)中。

同轴电缆

与双绞线类似,同轴电缆也是由两个铜导体组成,下面是实物图

借助于这种结构以及特殊的绝缘体和保护层,同轴电缆能够达到较高的传输速率,同轴电缆普遍应用在在电缆电视系统中。同轴电缆常被用户引导型共享媒介。

光纤

光纤是一种细而柔软的、能够引导光脉冲的媒介,每个脉冲表示一个比特。一根光纤能够支持极高的比特率,高达数十甚至数百 Gbps。它们不受电磁干扰。光纤是一种引导型物理媒介,下面是光纤的实物图

一般长途电话网络全面使用光纤,光纤也广泛应用于因特网的主干。

陆地无线电信道

无线电信道承载电磁频谱中的信号。它不需要安装物理线路,并具有穿透墙壁、提供与移动用户的连接以及长距离承载信号的能力。

卫星无线电信道

一颗卫星电信道连接地球上的两个或多个微博发射器/接收器,它们称为地面站。通信中经常使用两类卫星:同步卫星和近地卫星。

计算机网络的数据链路层

原文:https://zwmst.com/2690.html

下面我们把关注点放在数据链路层,如果没有数据链路层,计算机网络也就不复存在;这就好比大楼没有了地基,人没有了腿;所以,数据链路层的知识也固然重要,不少小伙伴只把关注点放在 TCP 和 IP 这两个协议上,这是一种狭隘的思想,需要及时纠正,计算机网络可不只有 TCP 和 IP。下面 cxuan 就和你聊聊计算机中的数据链路层。

数据链路层

数据链路层,按照 OSI 七层模型来划分的话,就属于物理层的上层

数据链路层是一种协议层,它有很多协议。数据链路层用于跨物理层在网段节点之间传输数据,通常指以太网、无线局域网等通信手段。数据链路层提供了在网络的两个实体之间传输数据的功能,并且提供了差错检测用于纠正物理层中发生的错误。

关键概念

在数据链路层中,链路层地址有很多中不同的称谓:LAN 地址、物理地址或者 MAC 地址,因为 MAC 地址是最流行的术语,所以我们一般称呼链路层地址指的就是 MAC 地址。

下面我们就来认识一下数据链路层的几个关键概念

打包成帧

打包成帧(framing): 在每个网络层数据报在传输之前,几乎所有的链路层协议都会将数据报用链路层封装起来。数据链路层从网络层获取数据后将其封装成为 ,如果帧太大的话,数据链路层会将大帧拆分为一个个的小帧,小帧能够使传输控制和错误检测更加高效。

帧就是 0 1 序列的封装。

一个帧由 Header、Payload Field、Trailer 组成,网络层数据报就封装在 Payload Field 字段中。根据不同的物理介质,每个帧的结构也不同。帧的组成如下

帧中主要涉及的内容如下

  • 帧头(Frame header):它包含帧的源地址和目的地址。
  • 有效载荷(Payload Field):它包含要传递的数据和信息。
  • 尾部标记(Trailer):它包含错误检测和错误纠正位。
  • 标记(Flag):它标记了帧的开始和结束。

Flag 位位于帧的开头和结尾,两个连续的标志指示帧的结束和开始

帧的类型主要有两种,固定大小的帧和可变大小的帧。

  • 固定大小的帧(Fixed-sized Framing):表示帧的大小是固定的,帧的长度充当帧的边界,因此它不需要额外的边界位来标识帧的开始和结束。
  • 可变大小的帧(Sized Framing):表示每个真的大小是不固定的,因此保留了其他机制来标记一帧的结束和下一帧的开始。它通常用于局域网,在可变大小的帧中定义帧定界符的两种方法是
    • 长度字段(Length Field): 使用长度字段来确定帧的大小。它用于以太网(IEEE 802.3)
    • 结束定界符(End Delimiter): 经常用于令牌环

链路接入

链路接入主要指的是 MAC 协议,MAC(Medium Access Control) 协议规定了帧在链路上的传输规则。我们知道,数据链路层是 OSI 标准模型的第二层,数据链路层向下还能够细分,主要分为 The logical link control (LLC) 层和The medium access control (MAC) 层。

LLC 层又叫做逻辑控制链路层,它主要用于数据传输,它充当网络层和数据链路层中的媒体访问控制(MAC)子层之间的接口。LLC 层的主要功能如下

  • LLC 的主要功能是发送时在 MAC 层上多路复用协议,并在接收时同样地多路分解协议。
  • LLC 提供跳到跳的流和差错控制,像是路由器和路由器之间这种相邻节点的数据传输称为 一跳
  • 它允许通过计算机网络进行多点通信。

MAC 层负责传输介质的流控制和多路复用,它的主要功能如下

  • MAC 层为 LLC 和 OSI 网络的上层提供了物理层的抽象。
  • MAC 层负责封装帧,以便通过物理介质进行传输。
  • MAC 层负责解析源和目标地址。
  • MAC 层还负责在冲突的情况下执行冲突解决并启动重传。
  • MAC 层负责生成帧校验序列,从而有助于防止传输错误。

在 MAC 层中,有一个非常关键的概念就是 MAC 地址。MAC 地址主要用于识别数据链路中互联的节点,如下图所示

MAC 地址长 48 bit,在使用网卡(NIC) 的情况下,MAC 地址一般都会烧入 ROM 中。因此,任何一个网卡的 MAC 地址都是唯一的。MAC 地址的结构如下

MAC 地址中的 3 – 24 位表示厂商识别码,每个 NIC 厂商都有特定唯一的识别数字。25 – 48 位是厂商内部为识别每个网卡而用。因此,可以保证全世界不会有相同 MAC 地址的网卡。

MAC 地址也有例外情况,即 MAC 地址也会有重复的时候,比如你可以手动更改 MAC 地址。但是问题不大,只要两个 MAC 地址是属于不同的数据链路层就不会出现问题。

可靠交付

网络层提供的可靠交付更多指的是端系统到端系统的交付,而数据链路层提供的可靠交付更多指的是单端链路节点到节点地传送。当链路层协议提供可靠交付时,它能保证无差错地经链路层移动每个网络层数据报。链路层提供可靠交付的方法和 TCP 类似,也是使用 确认重传 取得的。

链路层的可靠交付通常用于出错率很高的链路,例如无线链路,它的目的是在本地纠正出错的帧,而不是通过运输层或应用层协议强制进行端到端的数据传输。对于出错率较低的链路,比如光纤、同轴电缆和双绞线来说,链路层的交付开销是没有必要的,由于这个原因,这些链路通常不提供可靠的交付

差错检测和纠正

链路层数据以帧的形式发送,在发送的过程中,接收方节点的链路层硬件可能会由于信号干扰或者电磁噪音等原因错误的把 1 识别为 0 ,0 识别为 1。这种情况下没有必要转发一个有差错的数据报,所以许多链路层协议提供一种机制来检测这样的比特差错。通过让方节点在帧中包括差错检测比特,让接收节点进行差错检查,以此来完成这项工作。

运输层和网络层通过因特网校验和来实现差错检测,链路层的差错检测通常更复杂,并且用硬件实现。差错纠正类似于差错检测,区别在于接收方不仅能检测帧中出现的比特差错,而且能够准确的确定帧中出现差错的位置。

差错检测和纠正的技术主要有

  • 奇偶校验:它主要用来差错检测和纠正
  • 校验和:这是一种用于运输层检验的方法
  • 循环冗余校验:它更多应用于适配器中的链路层

地址映射

因为存在网络层地址(IP 地址)和 数据链路层地址(MAC 地址),所以需要在它们之间进行转换和映射,这就是地址解析协议所做的工作,更多关于地址解析协议的理解,请查阅

深入理解 ARP 协议

数据链路层的作用

数据链路层中的协议定义了互联网络的两个设备之间传输数据的规范。数据链路层需要以通信介质 作为传输载体,通信媒介包含双绞铜线、光纤、电波等红外装置。在数据分发装置上有 交换机、网桥、中继器 等中转数据。链路层中的任何设备又被称为节点(node),而沿着通信路径相邻节点之间的通信信道被称为 链路(link)。实际上,在链路层上传输数据的过程中,链路层和物理层都在发挥作用。因为在计算机中,信息是以 0 1 这种二进制的形式进行传输,而实际的链路通信却是以电压的高低、光的闪灭以及电波的频谱来进行的,所以物理层的作用就是把二进制转换成为链路传输所需要的信息来进行传输。数据链路层传输也不只是单个的 0 1 序列,它们通常是以 为单位进行的。

现在我们知道了数据链路层大概是干啥的,那么只有理论不行,你还得有硬通货,也就是硬件,一切的理论都离不开硬件的支撑。

硬件就可以简单理解为通信介质,在通信介质上会有不同种类的信息传递方式,不过总的来说可以概括为两种:一种是共享介质型网络,一种是非共享介质型网络,下面我们就要聊一聊这两种通信类型。

通信类型分类

共享介质型网络

共享介质型网络故名思义就是多个设备共同使用同一个通信介质的网络。共享介质型网络的类型主要有以太网(Ethernet)光纤分布式数据接口(Fiber Distributed Data Interface,FDDI)

共享说的是,多个设备会使用同一个载波信道进行发送和接收,这是一种半双工的设计。

什么是半双工?

半双工指的是数据可以在一个信道上的两个方向上相互传输,但是不能同时传输,举个简单的例子,就是你能给我发消息,我也能给你发消息,但是不能你给我发消息的同时我也在给你发消息。

既然多个设备会共同使用一个信道,那么就可能存在多个数据传输到同一个介质上导致的数据争用问题,为此,共享介质型网络有两种介质访问控制方式:争用和令牌传递

争用

争用是发生在共享介质,载波监听多路访问(CSMA) 上的数据访问方式。在这种访问方式下,网络中各个介质会采用先到先得的方式占用载波信道发送数据。如果多个介质同时发送帧,就势必会产生冲突,继而导致通信性能的下降和网络拥堵。下面是争用的处理方式

如上图所示,假如 A 想要给 C 发送数据,那么介质 A 会在确认周围没有其他介质要给 C 发送数据后,也就是经过一段时间后,A 会把数据马上发送给 C。

每个介质在接受到 A 发送的数据后,会从 A 报文中解析出来 MAC 地址判断是否是发送给自己的数据包,如果不是的话就是丢弃这条数据。

上面这种方式会使用在一部分以太网中,但是另外一部分以太网却使用了 CSMA 的改良方式 – CSMA/CD 。CSMA/CD 会要求每个介质提前检查一下链路上是否有可能产生冲突的现象,一旦发生冲突,那么尽可能早地释放信道。它的具体工作原理大致如下:

  • 监听载波信道上是否会有数据流动,如果没有的话,那么任何介质都可以发送数据。
  • 介质会检查是否发生冲突,一旦发生冲突就会丢弃数据,同时立即释放载波信道。
  • 放弃数据后,会经过一段时间重新争用介质。

下面是 CSMA/CD 的改良版

上图这个过程是 CSMA(Carrier Sense Multiple Access),首先介质会监控载波信道上是否有数据存在,如果没有再发送,如果有,等一段时间再发送。

下面是 CD(Collision Detection) 的示意图

  • 在发送数据 -> 发送完成后,如果电压一直处于规定范围内,就会认为数据已经正常发送。
  • 发送途中,如果电压超过了一定范围,就会认为是数据冲突。
  • 发生冲突时会先发送一个阻塞报文,继而放弃数据,在延迟一段时间后再次发送

令牌环

第二种共享介质型网络的传输方式就是令牌环了,令牌环顾名思义就是有一个令牌一样的东西,以环为一圈进行令牌传输,那么令牌是啥呢?你想啊,我们最终的目的不就是为了传输数据吗?那么这个令牌,它可不可以作为数据呢?

其实,在这种传输方式中,令牌环是作为一种特殊报文来传输的,它是控制传输的一种方式,在数据传输的过程中同时会将令牌进行传递,只有获得令牌的介质才能够传输数据。这种方式有两个优点,即

  • 持有令牌的介质才能够传输数据,这样能够保证不会有报文冲突情况。
  • 每个介质都有平等获取令牌的机会,这样保证了即使网络拥堵也不会导致性能下降。

但是这种令牌环的传递方式也是有缺点的,因为只有持有令牌的介质才能发送数据,所以即使在网络不太拥堵的情况下,其利用率也达不到 100%。

下面是令牌的传递示意图

最一开始,令牌位于介质 A 处,此时介质 A 拥有数据传输的能力,然后介质 A 把令牌传递给介质 B。

此时 B 持有令牌,所以介质 B 具有发送数据的能力。

这个数据最终会由 D 接收,然后 D 就会设置一个已接收数据的标志位,然后数据会继续向下发送。

令牌环是一项很成功的技术,尤其是在公司环境中使用,但后来被更高版本的以太网所取代。

在了解完共享网络之后,我们来探讨一波非共享网络

非共享介质型网络

如果说共享介质型网络是共享介质的话,那么非共享介质型网络就是不共享介质,那么如何通信呢?在这种方式下,网络中的每个介质会直接连上交换机,由交换机来转发数据帧。发送端和接收端不会共享通信介质,共享通信介质的意思就是介质之间直接通信。这种网络传输方式一般采用的是全双工通信。

非共享介质型网络比较适合应用于搭建虚拟局域网(VLAN),但是这种通信方式有一个及其致命的弱点:一旦交换机发生故障,那么与交换机相连的所有计算机都无法通信。

下面是非共享介质型网络的通信示意图

如图所示,主机 A 发送了一个目标地址为 B,源地址为 A 的交换机,由交换机负责将数据转发给介质 B,如下图所示

非共享型网络是一种全双工通信的方式,每个介质在发送数据的同时也能够接受来自交换机传递过来的数据。

交换集线器

交换集线器是一种共享型网络通信介质,它是使用同轴电缆作为传输介质,通常用于以太网中,交换集线器也叫做以太网交换机

以太网交换机中的各个端口会根据介质的 MAC地址来转发数据,那么转发数据肯定得有所依靠啊,这时可以参考的表就叫做转发表(Forwarding Table),转发表中记录着每个介质的 MAC 地址。转发表当然不需要我们手动维护,交换机会自动维护转发表。交换机会自学每个数据包的经过介质的 MAC 地址,如下图所示

由于不知道主机 B 的 MAC 地址,所以主机 A 发送的数据会经过交换机广播给以太网内的其他主机,主机 B 接收到数据后,会给主机 A 回送消息。

在主机 B 给主机 A 回送消息后,交换机就知道主机 A 和主机 B 的 MAC 地址了,从此以后双方通信会在各自相连的端口之间进行。

由于 MAC 地址没有层次性,转发表中的记录个数与所有网络设备的数量有关,当设备增加时,转发表的记录也会越来越多,检索时间会逐渐增加。所以如果需要连接多个终端时,需要将网络分成多个数据链路,采用类似 IP 地址一样对地址进行分层管理。

在网络通信的过程中,由于网络链路的冗余或者路由线路冗余可能会造成闭环,也就是我们所称的环路。环路会导致数据报文在网络中不断重复复制,最终导致网络设备负载过重,无法正常运行。影响的范围可能会扩散至整个局域网,导致整个局域网里的计算机无法正常使用网络。

那么如何检测网络中出现的环路呢?

环路检测方法

目前有两种检测环路的方式,一种是生成树,一种是源路由法

生成树:生成树指的是每个网桥必须在 1 – 10 秒内相互交换生成树协议单元包,以此来判断哪些接口使用,从而消除环路,一旦发生故障后就会立刻切换线路,利用没有被使用的端口进行传输。

源路由法:源路由法通常是用来解决令牌环路。这种方式可以判断发送数据的源地址是通过哪个网桥实现传输的,并将帧写入 RIF,网桥会根据这个 RIF 信息发送给目标地址,即使网桥中出现了环路,数据帧也不存在被反复转发的可能。

虚拟局域网 VLAN

网络通信过程中经常会遇到网络负载过高,通信性能下降的情况,往往遇到这种情况,就需要分散网络负载,变换部署网络设备的位置等。在虚拟局域网出现之前,往往需要管理员手动变更网络的拓扑结构,比如变更主机网段,进行硬件线路改造等,但是使用了虚拟局域网,就可以不用再做如此复杂的操作了,只需要修改网络结构即可。

那么虚拟局域网究竟是什么呢?

如上图所示,交换机按照端口区分了多个网段,从而区分了广播数据的传播范围,提高网络安全性。然而异构的两个网段之间,需要利用具有路由功能的交换机才能实现通信。

由于交换机端口有两种 VLAN 属性,一个是 VLANID,一个是 VLANTAG,分别对应 VLAN 对数据包设置 VLAN 标签和允许通过的 VLANTAG(标签)数据包,不同 VLANID 端口,可以通过相互允许 VLANTAG,构建 VLAN。

以太网

以太网提了这么多次,那么以太网到底是什么?

数据链路层有很多分类,包括以太网、无线通信、PPP、ATM、POS、FDDI、Token Ring、HDMI 等,其中最著名的通信链路就是以太网了。

以太网最开始的时候,一般使用的是以同轴电缆为传输介质的共享介质型连接方式,这也是以太网的第一种方式,叫做经典以太网。而现在,随着互联设备的处理能力和传输速度的提高,现在都采用终端和交换机之间连接方式,这也是第二种方式,叫做交换式以太网。以太网使用的是 CSMA/CD 的总线技术,我们前面也介绍过了。

以太帧格式

在以太网链路上的数据包被称为以太帧,以太帧开头有一个叫做前导码(Preamble) 的部分,它是由 0、1 数字交替组合而成。前导码的末尾最后是一个叫做 SFD(Start Frame Delimiter) 的域,值为 11。前导码与 SFD 共同占用 8 个字节。

以太网最后 2 bit 称为 SDF,而 IEEE802.3 中将最后 8 bit 称为 SDF。

IEEE802.3 是电气和电子工程师协会 (IEEE)标准的集合制定的标准。

这是以太帧的前导码部分,下面是以太帧的本体部分

以太帧体格式也有两种,一种是以太帧格式,一种是 IEEE802.3 标准以太帧格式。

在以太帧格式中,以太帧的本体的前端是以太网的首部,总共占用 14 字节,分别是 6 字节的目标 MAC 地址、6 字节的源 MAC 地址和 2 字节的上层协议类型,后面是数据部分,占用 46 – 1500 字节,最后是 FCS(Frame Check Sequence,帧检验序列) 4 个字节。FCS 用于检查帧是否有所损坏,因为在通信过程中由于噪声干扰,可能会导致数据出现乱码位。

IEEE802.3 以太帧的格式有区别,一般以太帧中的类型字段却在 IEEE802.3 表示帧长度,此外新增加了 LLC 和 SNAP 字段。

数据链路层在细化的话可以分为两层,介质访问控制层和逻辑链路控制层

介质访问会根据以太网等不同链路特有的首部信息进行控制,逻辑链路层则根据以太网等不同链路共有的帧头信息进行控制。

LLC 和 SNAP 就是逻辑链路控制的首部信息,那么现在你应该明白怎么回事儿了吧。

计算机网络自学指南

原文:https://zwmst.com/2692.html

关于计算机网络如何学习,我就拿自己亲身实践的来举例吧,因为我也自学学起的。

我觉得最重要的就是看书(博客) + 实践。

当然视频是最快速的入门方式,你可以先看视频有所了解后再去看书系统学习

视频

今天在 b 站看视频的时候,看到了一句话众所周知,b 站是用来搞学习的,对于我们学习编程的童鞋来说,b 站有着非常多的学习资源,但是有一些质量并不是很好,看了之后不容易理解,这也是写这一篇文章的原因,为大家分

享一些质量超高的计算机基础的学习视频,往下看就完了。

1. 计算机网络微课堂

学习计算机网络,我首先推荐的 UP 主湖科大教书匠,他讲的计算机网络十分通俗易懂,重点的地方讲的十分细致,并且还有一些实验,更好的是有考研 408 的难题的讲解,也是非常适合考研党,除了课程内容外还有很多习题讲解视频,特别赞的一点是每天动态里都会更新一道考研题,播放量也非常的多。

2. 2019 王道考研 计算机网络

既然说到了考研,那我就不得不提一下王道考研了,恭喜你发现了宝藏。王道考研的计算机网络视频,播放量非常多,而且老师是一位小姐姐,声音十分动听,声音这么好听的老师给你讲课,妈妈再也不用担心我的学习了呢,总之,这个视频的质量也非常高,弹幕全是对小姐姐的高度评价。(王道考研其他的视频也不错哦,暗示一下:操作系统,数据结构等等)

3. 韩立刚计算机网络谢希仁

韩立刚老师所讲的计算机网络视频,内容比较多,但是讲解的通俗易懂,并且老师讲课的经验也十分的丰富。配套的教材是谢希仁老师的计算机网络教材,韩老师的最近的一个视频视频比较新,播放量还比较少,但是他讲的是真的不错,相比于王道考研所讲的计算机网络,韩老师更加细致一些。

4. 计算机网络(谢希仁第七版)-方老师

在计算机网络方面,我还想推荐的一位老师就是方老师,也是一位小姐姐老师。她的视频配套的教材也是谢老师的网络教材,在线看的小伙伴也超多,弹幕都是对方老师的评价。

博客

推荐几个不错的学习博客。

互联网协议入门-阮一峰:http://www.ruanyifeng.com/blog/2012/05/internet_protocol_suite_part_i….

网络协议-兰亭风雨:http://blog.csdn.net/ns_code/article/category/1805481

HTTP协议:http://www.cnblogs.com/TankXiao/category/415412.html

Unix 网络编程:http://blog.csdn.net/chenhanzhun/article/category/2767131/2

TCP/IP详解:http://blog.csdn.net/chenhanzhun/article/category/2734921/1

计算机网络面试题:http://blog.csdn.net/shadowkiss/article/details/6552144

国外优秀计算机网络站点:http://www.tcpipguide.com/free/t_TCPSlidingWindowAcknowledgmentSystemForDataTranspo-6.htm

当然最硬核的就是 RFC 文档了 RFC Index

学习 HTTP ,必须要看一下 MDN 官网 HTTP | MDN

学习计算机网络,Cloudflare 你必须要去看 https://www.cloudflare.com/zh-cn/learning/

GeeksforGeeks 学习计算机网络也非常不错 Basics of Computer Networking – GeeksforGeeks

Tutorialspoint 系统学习计算机,不仅仅局限于计算机网络 Computer – Networking

国外优秀的学习网站不能少了 javapoint Types of Computer Network – javatpoint

以上这些网站都是我精心汇总的一些内容。

书也分为不同的层次,最基础的入门书籍有

书籍

网络是怎样连接的

这本书是日本人写的,它和《程序是怎样运行的》、《计算机是怎样跑起来的》统称为图解入门系列,最大的特点就是风趣幽默,简单易懂。这本书通过多图来解释浏览器中输入网址开始,一路追踪了到显示出网页内容为止的整个过程,以图配文,讲解了网络的全貌,并重点介绍了实际的网络设备和软件是如何工作的。

本书图文并茂,通俗易懂,非常适合计算机、网络爱好者及相关从业人员阅读。

所以如果大家是新手的话,强烈推荐一下这本书。

日本人就爱图解,同样图解系列的入门书籍还有《图解 HTTP》、《图解 TCP/IP》。

图解 HTTP

《图解 HTTP》是 HTTP 协议的入门书籍,当然 HTTP 也是属于计算机网络的范畴,这本书适合于想要对 HTTP 有基本认知的程序员,同样也适合查漏补缺。

这类书看起来就毫无难度了,不得不说图解系列是给小白的圣经,它能增强你的自信,让你觉得计算机其实 "没那么难",这是非常重要的。初学者,最怕的就是劝退了。

图解 TCP/IP

上面的图解 HTTP 是针对 HTTP 协议的,那么《图解 TCP/IP》就是针对 TCP/IP 协议簇中的协议了,这本书我已经看了 80% 了,还是比较系统的,基本上涵盖了 TCP/IP 协议簇中的所有协议知识了,这本书看完了完全就可以直接深入理解 TCP/IP 协议簇了。

对于新手来说,最重要的一点就是帮助你理解,怎么简单怎么来,这样才能快速入门,对于快餐式的社会来说,快速理解当然是当仁不让的首选了。

如果上面这几本书你都搞定了的话,那你就可以读一下 《计算机网络:自顶向下方法》这本书了,这本书可以作为基础书籍也可以作为进阶书籍,这里我归为了进阶书籍,因为里面有一些章节不是那么好理解,比如介绍网络层的时候,会分为数据平面和控制平面,介绍 TCP 和 UDP 的时候,也会聊到一些原理性问题。

计算机网络 第七版

这本书是一本计算机网络的圣经书籍,圣经就在于人人都应该读一下这本书,原著非常经典,翻译也很不错,我自己也马上就看完了,这本书会从顶层,也就是网络层逐步下探到物理层,一层一层的带你入门,解释各层之间的协议,主要特征是什么,一个数据包的发送历程。这本书并不局限于某个具体的协议,而是从宏观的角度来看待计算机网络到底是什么,里面有一些专业名词,理解并掌握后会对深入学习计算机网络非常有用。

计算机网络 谢希仁

这本书是很多大学的教材,也是一本非常好的进阶书籍,这本书相对于自顶向下方法更多是对于通信网络的阐述。

这本书的特点是突出基本原理和基本概念的阐述,同时力图反映计算机网络的一些最新发展。本书可供电气信息类和计算机类专业的大学本科生和研究生使用,对从事计算机网络工作的工程技术人员也有参考价值

现在我们接着聊,如果上面这两本书随便一本看完了,那么恭喜你已经是一个"老手"了,你的网络基础能打败 90% 以上的人了,如果你还不满足的话,那你就需要继续深入,继续深入也是我推荐给你的提高书籍。

HTTP 权威指南

HTTP 权威指南是深入 HTTP 非常值得一看的书,这本书写的非常全了。

此书第一部分是HTTP的概略,如果你没有时间,通读第一部分就能让你应付普通的日常开发工作。

第二部分主要讲现实世界中HTTP的架构,也可以看作HTTP的全景图,包括Web Server/Cache/Proxy/Gateway,是全书中精华的部分。

第三部分主要是HTTP安全,其中Basic和Digest概略看下即可,现实世界中用的应该不多。看HTTPs最好有一些计算机安全基础,这样会顺畅很多。

第四部分主要是关于HTTP Message Body的部分,包括Content Negotiation,MIME Type,chunked encoding等,概略看下即可。

第五部分的内容,Web Hosting可以认真看下,了解下Virtual Host(话说我上学的时候一直搞不懂Virtual Host,一个IP怎么能同时Host两个不同域名的Web页面呢,sigh)。

剩下三章已经过时,基本可以忽略。 最后的附录,可以用作边用边学的字典,如果你自己来写Web Server,那么这一部分是极有价值的参考。

总而言之,无论你是前端还是后端,只要是Web相关的,那么此书就是必读的。

TCP/IP 详解

这是一本被翻译耽误的经典书,两个硬核作者 Kevin R. Fall 和 W. Richard Stevens 被南开大学的某计算机洗的译者给毁了。我第一开始读这本书以为是自己智商不够,原来是翻译 "瞎TM翻" 啊。语句不通且不说,您好歹走点心,改点措辞也行啊,纯碎是生搬硬套谷歌翻译啊,哎。

上面都是一些理论书籍,下面是稍微偏实战一些的书籍了。

计算机网络实战最有效的当然就属于抓包了,有很多抓包工具比如

wireshark、sniffer、httpwatch、iptool、fiddle 等,但是我用的和使用频率最高的应该就是 wireshark 了,关于 wireshark 还有两本实战方面的书你需要知道

wireshark 数据包分析实战

初学者必备,介绍了wireshark安装,嗅探网络流量,wireshark的基本使用,用wireshark分析了一圈常用的TCP,UDP协议,也简要分析了HTTP等应用层协议,概要介绍了一些TCP重传的机制,最后是无线分析

整个书定位应该是入门级别的,基本上每章都是简要介绍,并没有特别深入大张阔斧地进行描述。文章行文思路清晰,译者的翻译水平也不错。

总的来说,是初步认识和了解wireshark的好书

wireshark 网络分析就是这么简单

读的时候你会忍不住笑的,区别于《Wireshark数据包分析实战》,本书就像一本侦探小说集,以幽默风趣的语言风格,借助wireshark以理性的思考来不断探险,根据蛛丝马迹来“侦破案情”

总结,读完数据包分析实战来读这本。

Wireshark网络分析实战

其内容涵盖了Wireshark的基础知识,抓包过滤器的用法,显示过滤器的用法,基本/高级信息统计工具的用法,Expert Info工具的用法,Wiresahrk在Ethernet、LAN及无线LAN中的用法,ARP和IP故障分析,TCP/UDP故障分析,HTTP和DNS故障分析,企业网应用程序行为分析,SIP、多媒体和IP电话,排除由低带宽或高延迟所引发的故障,认识网络安全等知识。

实验

借鉴一些大佬的回答,给你推荐一个斯坦福课程的实验

推荐 Stanford 课程 cs144,配合《计算机网络:自顶向下方法》(Computer Networking: A Top-Down Approach)。具体来说就是跟着 cs144 的课程安排走一遍,完成课程的lab

计算机网络核心概念

原文:https://zwmst.com/2694.html

  1. 主机:计算机网络上任何一种能够连接网络的设备都被称为主机或者说是端系统,比如手机、平板电脑、电视、游戏机、汽车等,随着 5G 的到来,将会有越来越多的终端设备接入网络。
  2. 通信链路:通信链路是由物理链路(同轴电缆、双绞线、光纤灯)连接到一起组成的一种物理通路。
  3. 传输速率:单位是 bit/s 或者 bps ,用来度量不同链路从一个端系统到另一个端系统传输数据的速率。
  4. 分组:当一台端系统向另外一台端系统发送数据时,通常会将数据进行分片,然后为每段加上首部字节,从而形成计算机网络的专业术语:分组。这些分组通过网络发送到端系统,然后再进行数据处理。
  5. 路由器:它和链路层交换机一样,都是一种交换机,主要用于转发数据的目的。

  1. 路径:一个分组所经历一系列通信链路和分组交换机称为通过这个网络的路径。

  2. 因特网服务商:也叫 ISP,不是 lsp。这个好理解,就是网络运营商,移动、电信、联通。

  3. 网络协议:网络协议是计算机网络中进行数据交换而建立的规则、标准或者约定。

  4. IP:网际协议,它规定了路由器和端系统之间发送和接收的分组格式。

  5. TCP/IP 协议簇:不仅仅只有 TCP 协议和 IP 协议,而是以 TCP、IP 协议为主的一系列协议,比如 ICMP 协议、ARP 协议、UDP 协议、DNS 洗衣、SMTP 协议等。

  6. 分布式应用程序:多个端系统之间相互交换数据的端系统被称为分布式应用程序。

  7. 套接字接口:指的就是 socket 接口,这个接口规定了端系统之间通过因特网进行数据交换的方式。

  8. 协议:协议定义了两个以上通信实体之间交换报文格式和顺序所遵从的标准。

  9. 客户端:在客户-服务器架构中扮演请求方的角色,通常是 PC,智能手机等端系统。

  10. 服务器:在客户-服务器架构中扮演服务方的角色,通常是大型服务器集群扮演服务器的角色。

  11. 转发表:路由内部记录报文路径的映射关系的一种记录。

  12. 时延:时延指的是一个报文或者分组从网络的一端传递到另一端所需要的时间,时延分类有发送时延、传播时延、处理时延、排队时延,总时延 = 发送时延+传播时延+处理时延+排队时延。

  13. 丢包:在计算机网络中指的是分组出现丢失的现象。

  14. 吞吐量:吞吐量在计算机网络中指的是单位时间内成功传输数据的数量。

  15. 报文:通常指的是应用层的分组。

  16. 报文段:通常把运输层的分组称为报文段。

  17. 数据报:通常将网络层的分组称为数据报。

  18. :一般把链路层的分组称为帧。

  19. 客户-服务体系:它是一种面向网络应用的体系结构。把系统中的不同端系统区分为客户和服务器两类,客户向服务器发出服务请求,由服务器完成所请求的服务,并把处理结果回送给客户。在客户-服务器体系结构中,有一个总是打开的主机称为 服务器(Server),它提供来自于 客户(client) 的服务。我们最常见的服务器就是 Web 服务器,Web 服务器服务于来自 浏览器 的请求。

  1. P2P 体系:对等体系结构,相当于没有服务器了,大家都是客户机,每个客户既能发送请求,也能对请求作出响应。

  1. IP 地址:IP 地址就是网际协议地址,在互联网中唯一标识主机的一种地址。每一台入网的设备都会有一个 IP 地址,这个 IP 又分为内网 IP 和公网 IP。

  2. 端口号:在同一台主机内,端口号用于标识不同应用程序进程。

  3. URI:它的全称是(Uniform Resource Identifier),中文名称是统一资源标识符,使用它就能够唯一地标记互联网上资源。

  4. URL:它的全称是(Uniform Resource Locator),中文名称是统一资源定位符,它实际上是 URI 的一个子集。

    image-20210716172442536

  5. HTML:HTML 称为超文本标记语言,是一种标识性的语言。它包括一系列标签.通过这些标签可以将网络上的文档格式统一,使分散的 Internet 资源连接为一个逻辑整体。HTML 文本是由 HTML 命令组成的描述性文本,HTML 命令可以说明文字,图形、动画、声音、表格、链接等。

  6. Web 页面:Web 页面也叫做 Web Page,它是由对象组成,一个对象(object) 简单来说就是一个文件,这个文件可以是 HTML 文件、一个图片、一段 Java 应用程序等,它们都可以通过 URI 来找到。一个 Web 页面包含了很多对象,Web 页面可以说是对象的集合体。

  7. Web 服务器:Web 服务器的正式名称叫做 Web Server,Web 服务器可以向浏览器等 Web 客户端提供文档,也可以放置网站文件,让全世界浏览;可以放置数据文件,让全世界下载。目前最主流的三个 Web 服务器是 Apache、 Nginx 、IIS。

  8. CDN:CDN 的全称是Content Delivery Network,即内容分发网络,它应用了 HTTP 协议里的缓存和代理技术,代替源站响应客户端的请求。CDN 是构建在现有网络基础之上的网络,它依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。

  9. WAF:WAF 是一种 应用程序防护系统,它是一种通过执行一系列针对 HTTP / HTTPS的安全策略来专门为 Web 应用提供保护的一款产品,它是应用层面的防火墙,专门检测 HTTP 流量,是防护 Web 应用的安全技术。

  10. WebService :WebService 是一种 Web 应用程序,WebService 是一种跨编程语言和跨操作系统平台的远程调用技术

  11. HTTP: TCP/IP 协议簇的一种,它是一个在计算机世界里专门在两点之间传输文字、图片、音频、视频等超文本数据的约定和规范。

  12. Session:Session 其实就是客户端会话的缓存,主要是为了弥补 HTTP 无状态的特性而设计的。服务器可以利用 Session 存储客户端在同一个会话期间的一些操作记录。当客户端请求服务端时,服务端会为这次请求开辟一块内存空间,这个对象便是 Session 对象,存储结构为 ConcurrentHashMap

  13. Cookie:HTTP 协议中的 Cookie 包括 Web Cookie浏览器 Cookie,它是服务器发送到 Web 浏览器的一小块数据。服务器发送到浏览器的 Cookie,浏览器会进行存储,并与下一个请求一起发送到服务器。通常,它用于判断两个请求是否来自于同一个浏览器,例如用户保持登录状态。

  14. SMTP 协议 :提供电子邮件服务的协议叫做 SMTP 协议, SMTP 在传输层也使用了 TCP 协议。SMTP 协议主要用于系统之间的邮件信息传递,并提供有关来信的通知。

  15. DNS 协议:由于 IP 地址是计算机能够识别的地址,而我们人类不方便记忆这种地址,所以为了方便人类的记忆,使用 DNS 协议,来把我们容易记忆的网络地址映射称为主机能够识别的 IP 地址。

    image-20210716172449345

  16. TELNET 协议:远程登陆协议,它允许用户(Telnet 客户端)通过一个协商过程来与一个远程设备进行通信,它为用户提供了在本地计算机上完成远程主机工作的能力。

    image-20210716172457871

  17. SSH 协议:SSH 是一种建立在应用层上的安全加密协议。因为 TELNET 有一个非常明显的缺点,那就是在主机和远程主机的发送数据包的过程中是明文传输,未经任何安全加密,这样的后果是容易被互联网上不法分子嗅探到数据包来搞一些坏事,为了数据的安全性,我们一般使用 SSH 进行远程登录。

  18. FTP 协议:文件传输协议,是应用层协议之一。FTP 协议包括两个组成部分,分为 FTP 服务器和 FTP 客户端。其中 FTP 服务器用来存储文件,用户可以使用 FTP 客户端通过 FTP 协议访问位于 FTP 服务器上的资源。FTP 协议传输效率很高,一般用来传输大文件。

  1. MIME 类型,它表示的是互联网的资源类型,一般类型有 超文本标记语言文本 .html text/html、xml文档 .xml text/xml、普通文本 .txt text/plain、PNG图像 .png image/png、GIF图形 .gif image/gif、JPEG图形 .jpeg,.jpg image/jpeg、AVI 文件 .avi video/x-msvideo 等。

  2. 多路分解:在接收端,运输层会检查源端口号和目的端口号等字段,然后标识出接收的套接字,从而将运输层报文段的数据交付到正确套接字的过程被称为多路分解。

  3. 多路复用:在发送方,从不同的套接字中收集数据块,然后为数据块封装上首部信息从而生成报文段,然后将报文段传递给网络层的过程被称为多路复用。

  4. 周知端口号:在主机的应用程序中,从 0 – 1023 的端口号是受限制的,被称为周知端口号,这些端口号一般不能占用。

  5. 单向数据传输:数据的流向只能是单向的,也就是从发送端 -> 接收端。

  6. 双向数据传输:数据的流向是双向的,又叫做全双工通信,发送端和接收端可以相互发送数据。

  7. 面向连接的:面向连接指的是应用进程在向另一个应用进程发送数据前,需要先进行握手,即它们必须先相互发送预备报文段,用来建立确保数据传输的参数。

  8. 三次握手:TCP 连接的建立需要经过三个报文段的发送,这种连接的建立过程被称为三次握手。

    image-20210716172511682

  9. 最大报文段长度:即 MSS,它指的是从缓存中取出并放入报文段中的最大值。

  10. 最大传输单元:即 MTU,它指的是通信双方能够接收有效载荷的大小,MSS 通常会根据 MTU 来设。

  11. 冗余 ACK:就是再次确认某个报文段的 ACK,报文段的丢失会导致冗余 ACK 的出现。

  12. 快速重传:即在报文段定时器过期之前重传丢失的报文段。

  13. 选择确认:在报文段出现丢失的情况下,TCP 能够选择确认失序的报文段,这个机制通常和重传一起使用。

  14. 拥塞控制:拥塞控制说的是,当某一段时间网络中的分组过多,使得接收端来不及处理,从而引起部分甚至整个网络性能下降的现象时采取的一种抑制发送端发送数据,等过一段时间或者网络情况改善后再继续发送报文段的一种方法。

  15. 四次挥手:TCP 断开链接需要经过四个报文段的发送,这种断开过程是四次挥手。

    image-20210716172520921

  16. 路由选择算法:网络层中决定分组发送路径的一种算法。

  17. 转发:它指的是将分组从一个输入链路转移到合适的输出链路的动作。

  18. 分组调度:分组调度讨论的是分组如何经输出链路传输的问题,主要有三种调度方式:先进先出、优先级排队和"循环和加权公平排队"。

  19. IPv4:网际协议的第四个版本,也是被广泛使用的一个版本。IPv4 是一种无连接的协议,无连接不保证数据的可靠性交付。使用 32 位的地址。

  20. IPv6:网际协议的第六个版本,IPv6 的地址长度是 128 位,由于 IPv4 最大的问题在于网络地址资源不足,严重制约了互联网的应用和发展。IPv6 的使用,不仅能解决网络地址资源数量的问题,而且也解决了多种接入设备连入互联网的障碍。

  21. 接口:主机和物理链路之间的边界。

  22. ARP 协议:ARP 是一种解决地址问题的协议,通过 IP 位线索,可以定位下一个用来接收数据的网络设备的 MAC 地址。如果目标主机与主机不在同一个链路上时,可以通过 ARP 查找下一跳路由的地址。不过 ARP 只适用于 IPv4 ,不适用于 IPv6。

  23. RARP:RARP 就是将 ARP 协议反过来,通过 MAC 地址定位 IP 地址的一种协议。

    image-20210716172535871

  24. 代理 ARP:用于解决 ARP 包被路由器隔离的情况,通过代理 ARP 可以实现将 ARP 请求转发给临近的网段。

  25. ICMP 协议:Internet 报文控制协议,如果在 IP 通信过程中由于某个 IP 包由于某种原因未能到达目标主机,那么将会发送 ICMP 消息,ICMP 实际上是 IP 的一部分。

    image-20210716172545200

  26. DHCP 协议:DHCP 是一种动态主机配置协议。使用 DHCP 就能实现自动设置 IP 地址、统一管理 IP 地址分配,实现即插即用。

  27. NAT 协议:网络地址转换协议,它指的是所有本地地址的主机在接入网络时,都会要在 NAT 路由器上讲其转换成为全球 IP 地址,才能和其他主机进行通信。

  28. IP 隧道:IP 隧道技术说的是由路由器把网络层协议封装到另一个协议中从而跨过网络传输到另外一个路由器的过程。

  29. 单播:单播最大的特点就是 1 对 1,早期的固定电话就是单播的一个例子

    image-20210716172600269

  30. 广播:我们一般小时候经常会广播体操,这就是广播的一个事例,主机和与他连接的所有端系统相连,主机将信号发送给所有的端系统。

    image-20210716172606843

  31. 多播:多播与广播很类似,也是将消息发送给多个接收主机,不同之处在于多播需要限定在某一组主机作为接收端。

    image-20210716172611714

  32. 任播:任播是在特定的多台主机中选出一个接收端的通信方式。虽然和多播很相似,但是行为与多播不同,任播是从许多目标机群中选出一台最符合网络条件的主机作为目标主机发送消息。然后被选中的特定主机将返回一个单播信号,然后再与目标主机进行通信。

    image-20210716172618257

  33. IGP:内部网关协议,一般用于企业内部自己搭建的路由自治系统。

  34. EGP:外部网关协议,EGP 通常用于在网络主机之间相互交换路由信息。

  35. RIP :一种距离向量型路由协议,广泛应用于 LAN 网。

  36. OSPF:是根据 OSI 的 IS-IS 协议提出的一种链路状态型协议。这种协议还能够有效的解决网络环路问题。

  37. MPLS:它是一种标记交换技术,标记交换会对每个 IP 数据包都设定一个标记,然后根据这个标记进行转发。

  38. 节点:一般指链路层协议中的设备。

  39. 链路:一般把沿着通信路径连接相邻节点的通信信道称为链路。

  40. MAC 协议:媒体访问控制协议,它规定了帧在链路上传输的规则。

  41. 奇偶校验位:一种差错检测方式,多用于计算机硬件的错误检测中,奇偶校验通常用在数据通信中来保证数据的有效性。

  42. 向前纠错:接收方检测和纠正差错的能力被称为向前纠错。

  43. 以太网:以太网是一种当今最普遍的局域网技术,它规定了物理层的连线、电子信号和 MAC 协议的内容。

  44. VLAN:虚拟局域网(VLAN)是一组逻辑上的设备和用户,这些设备和用户并不受物理位置的限制,可以根据功能、部门及应用等因素将它们组织起来,相互之间的通信就好像它们在同一个网段中一样,所以称为虚拟局域网。

  45. 基站:无线网络的基础设施。

Web页面的请求历程

原文:https://zwmst.com/2696.html

Hey guys 各位读者姥爷们大家好,这里是程序员 cxuan 计算机网络连载系列的第 13 篇文章。

到现在为止,我们算是把应用层、运输层、网络层和数据链路层都介绍完了,那么现在是时候把这些内容都串起来,做一个全面的回顾了。那么我这就以 Web 页面的请求历程为例,来和你聊聊计算机网络中这些协议是怎样工作的、数据包是怎么收发的,从输入 URL 、敲击会车到最终完成页面呈现在你面前的这个过程。

首先,我打开了 Web Browser ,然后在 Google 浏览器 URL 地址栏中输入了 maps.google.com

然后 ……

查找 DNS 缓存

浏览器在这个阶段会检查四个地方是否存在缓存,第一个地方是浏览器缓存,这个缓存就是 DNS 记录。

浏览器会为你访问过的网站在固定期限内维护 DNS 记录。因此,它是第一个运行 DNS 查询的地方。 浏览器首先会检查这个网址在浏览器中是否有一条对应的 DNS 记录,用来找到目标网址的 IP 地址。

我是 chrome 浏览器,所以在 mac 中,无法使用 chrome://net-internals/#dns 找到对应的 IP 地址,在 windows 中是可以找到的。

那么 mac 怎么查询 DNS 记录呢?你可以使用 nslookup 命令来查找,但这不是我们讨论的重点。

DNS(Domain Name System) 是一个分布式的数据库,它用于维护网址 URL 到其 IP 地址的映射关系。在互联网中,IP 地址是计算机所能够理解的一种地址,而 DNS 的这种别名地址是我们人类能够理解和记忆的地址,DNS 就负责把人类记忆的地址映射成计算机能够理解的地址,每个 URL 都有唯一的 IP 地址进行对应。

举个例子,google 的官网是 www.google.com ,而 google 的 ip 地址是 216.58.200.228 ,这两个地址你在 URL 上输入哪个都能访问,但是 IP 地址不好记忆,而 google.com 简单明了。DNS 就相当于是我们几年前使用的家庭电话薄,比如你想给 cxuan 打电话,你有可能记不住 cxuan 的电话号码,此时你需要查询电话薄来找到 cxuan 的电话号码。

浏览器第二个需要检查的地方就是操作系统缓存。如果 DNS 记录不在浏览器缓存中,那么浏览器将对操作系统发起系统调用,Windows 下就是 getHostName

在 Linux 和大部分 UNIX 系统上,除非安装了 nscd,否则操作系统可能没有 DNS 缓存。

nscd 是 Linux 系统上的一种名称服务缓存程序

浏览低第三个需要检查的地方是路由器缓存,如果 DNS 记录不在自己电脑上的话,浏览器就会和与之相连的路由器共同维护 DNS 记录。

如果与之相连的路由器也没有 DNS 记录的话,浏览器就会检查 ISP 中是否有缓存。ISP 缓存就是你本地通信服务商的缓存,因为 ISP 维护着自己的 DNS 服务器,它缓存 DNS 记录的本质也是为了降低请求时间,达到快速响应的效果。一旦你访问过某些网站,你的 ISP 可能就会缓存这些页面,以便下次快速访问。对于经常看小电影的你是否感到震惊呢?如果家里还安装了一个可以联网的摄像头的话,那就有点嗨皮了。

你肯定比较困惑为什么第一步浏览器需要检查这么多缓存,你可能会感到不舒服,因为缓存可能会透露我们的隐私,但是这些缓存在调节网络流量和缩短数据传输时间等方面至关重要。

所以,上面涉及到 DNS 缓存的查询过程如下。

如果上面四个步骤中都不存在 DNS 记录,那么就表示不存在 DNS 缓存,这个时候就需要发起 DNS 查询,以查找目标网址(本示例中是 maps.google.com)的 IP 地址。

发起 DNS 查询

如上所述,如果想要使我的计算机和 maps.google.com 建立连接并进行通信的话,我需要知道 maps.google.com 的 IP 地址,由于 DNS 的设计原因,本地 DNS 可能无法给我提供正确的 IP 地址,那么它就需要在互联网上搜索多个 DNS 服务器,来找到网站的正确 IP 地址。

这里有个疑问,为什么我需要搜索多个 DNS 服务器的来找到网站的 IP 地址呢?一台服务器不行吗?

因为 DNS 是分布式域名服务器,每台服务器只维护一部分 IP 地址到网络地址的映射,没有任何一台服务器能够维持全部的映射关系。

在 DNS 的早期设计中只有一台 DNS 服务器。这台服务器会包含所有的 DNS 映射。这是一种集中式的设计,这种设计并不适用于当今的互联网,因为互联网有着数量巨大并且持续增长的主机,这种集中式的设计会存在以下几个问题

  • 单点故障(a single point of failure),如果 DNS 服务器崩溃,那么整个网络随之瘫痪。
  • 通信容量(traaffic volume),单个 DNS 服务器不得不处理所有的 DNS 查询,这种查询级别可能是上百万上千万级,一台服务器很难满足。
  • 远距离集中式数据库(distant centralized database),单个 DNS 服务器不可能 邻近 所有的用户,假设在美国的 DNS 服务器不可能临近让澳大利亚的查询使用,其中查询请求势必会经过低速和拥堵的链路,造成严重的时延。
  • 维护(maintenance),维护成本巨大,而且还需要频繁更新。

所以在当今网络情况下 DNS 不可能集中式设计,因为它完全没有可扩展能力,所以采用分布式设计,这种设计的特点如下

分布式、层次数据库

首先分布式设计首先解决的问题就是 DNS 服务器的扩展性问题,因此 DNS 使用了大量的 DNS 服务器,它们的组织模式一般是层次方式,并且分布在全世界范围内。没有一台 DNS 服务器能够拥有因特网上所有主机的映射。相反,这些映射分布在所有的 DNS 服务器上。

大致来说有三种 DNS 服务器:根 DNS 服务器顶级域(Top-Level Domain, TLD) DNS 服务器权威 DNS 服务器 。这些服务器的层次模型如下图所示

  • 根 DNS 服务器 ,有 400 多个根域名服务器遍及全世界,这些根域名服务器由 13 个不同的组织管理。根域名服务器的清单和组织机构可以在 https://root-servers.org/ 中找到,根域名服务器提供 TLD 服务器的 IP 地址。
  • 顶级域 DNS 服务器,对于每个顶级域名比如 com、org、net、edu 和 gov 和所有的国家级域名 uk、fr、ca 和 jp 都有 TLD 服务器或服务器集群。所有的顶级域列表参见 https://tld-list.com/ 。TDL 服务器提供了权威 DNS 服务器的 IP 地址。
  • 权威 DNS 服务器,在因特网上具有公共可访问的主机,如 Web 服务器和邮件服务器,这些主机的组织机构必须提供可供访问的 DNS 记录,这些记录将这些主机的名字映射为 IP 地址。一个组织机构的权威 DNS 服务器收藏了这些 DNS 记录。

在了解了 DNS 服务器的设计理念之后,我们回到 DNS 查找的步骤上来,DNS 的查询方式主要分为三种

DNS 查找中会出现三种类型的查询。通过组合使用这些查询,优化的 DNS 解析过程可缩短传输距离。在理想情况下,可以使用缓存的记录数据,从而使 DNS 域名服务器能够直接使用非递归查询。

  • 递归查询:在递归查询中,DNS 客户端要求 DNS 服务器(一般为 DNS 递归解析器)将使用所请求的资源记录响应客户端,或者如果解析器无法找到该记录,则返回错误消息。

  • 迭代查询:在迭代查询中,如果所查询的 DNS 服务器与查询名称不匹配,则其将返回对较低级别域名空间具有权威性的 DNS 服务器的引用。然后,DNS 客户端将对引用地址进行查询。此过程继续使用查询链中的其他 DNS 服务器,直至发生错误或超时为止。

  • 非递归查询:当 DNS 解析器客户端查询 DNS 服务器以获取其有权访问的记录时通常会进行此查询,因为其对该记录具有权威性,或者该记录存在于其缓存内。DNS 服务器通常会缓存 DNS 记录,查询到来后能够直接返回缓存结果,以防止更多带宽消耗和上游服务器上的负载。

上面负责开始 DNS 查找的介质就是 DNS 解析器,它一般是 ISP 维护的 DNS 服务器,它的主要职责就是通过向网络中其他 DNS 服务器询问正确的 IP 地址。

如果想要了解更多关于 DNS 的消息,请查阅 万字长文爆肝 DNS 协议!

所以对于 maps.google.com 这个域名来说,如果 ISP 维护的服务器没有 DNS 缓存记录,它就会向 DNS 根服务器地址发起查询,根名称服务器会将其重定向到 .com 顶级域名服务器。 .com 顶级域名服务器会将其重定向到google.com 权威服务器。google.com 名称服务器将在其 DNS 记录中找到 maps.google.com 匹配的 IP 地址,并将其返回给您的 DNS 解析器,然后将其发送回你的浏览器。

这里值得注意的是,DNS 查询报文会经过许多路由器和设备才会达到根域名等服务器,每经过一个设备或者路由器都会使用路由表 来确定哪种路径是数据包达到目的地最快的选择。这里面涉及到路由选择算法,如果小伙伴们想要了解路由选择算法,可以看看这篇文章 https://www.cisco.com/c/en/us/support/docs/ip/border-gateway-protocol-bgp/13753-25.html#anc3

ARP 请求

我看了很多篇文章都没有提到这一点,那就是 ARP 请求的这个过程。

什么时候需要发送 ARP 请求呢?

这里其实有个前提条件

  • 如果 DNS 服务器和我们的主机在同一个子网内,系统会按照下面的 ARP 过程对 DNS 服务器进行 ARP 查询
  • 如果 DNS 服务器和我们的主机在不同的子网,系统会按照下面的 ARP 过程对默认网关进行查询

ARP 协议的全称是 Address Resolution Protocol(地址解析协议),它是一个通过用于实现从 IP 地址到 MAC 地址的映射,即询问目标 IP 对应的 MAC 地址 的一种协议。

简而言之,ARP 就是一种解决地址问题的协议,它以 IP 地址为线索,定位下一个应该接收数据分包的主机 MAC 地址。如果目标主机不在同一个链路上,那么会查找下一跳路由器的 MAC 地址。

关于为什么有了 IP 地址,还要有 MAC 地址概述可以参看知乎这个回答 https://www.zhihu.com/question/21546408

ARP 的大致工作流程如下

假设 A 和 B 位于同一链路,不需要经过路由器的转换,主机 A 向主机 B 发送一个 IP 分组,主机 A 的地址是 192.168.1.2 ,主机 B 的地址是 192.168.1.3,它们都不知道对方的 MAC 地址是啥,主机 C 和 主机 D 是同一链路的其他主机。

主机 A 想要获取主机 B 的 MAC 地址,通过主机 A 会通过广播 的方式向以太网上的所有主机发送一个 ARP 请求包,这个 ARP 请求包中包含了主机 A 想要知道的主机 B 的 IP 地址的 MAC 地址。

主机 A 发送的 ARP 请求包会被同一链路上的所有主机/路由器接收并进行解析。每个主机/路由器都会检查 ARP 请求包中的信息,如果 ARP 请求包中的目标 IP 地址 和自己的相同,就会将自己主机的 MAC 地址写入响应包返回主机 A

由此,可以通过 ARP 从 IP 地址获取 MAC 地址,实现同一链路内的通信。

所以,要想发送 ARP 广播,我们需要有一个目标 IP 地址,同时还需要知道用于发送 ARP 广播的接口的 MAC 地址。

这里会涉及到 ARP 缓存的概念。

现在你知道了发送一次 IP 分组前通过发送一次 ARP 请求就能够确定 MAC 地址。那么是不是每发送一次都得经过广播 -> 封装 ARP 响应 -> 返回给主机这一系列流程呢?

想想看,浏览器是如何做的?浏览器内置了缓存能够缓存你最近经常使用的地址,那么 ARP 也是一样的。ARP 高效运行的关键就是维护每个主机和路由器上的 ARP 缓存(或表)。这个缓存维护着每个 IP 到 MAC 地址的映射关系。通过把第一次 ARP 获取到的 MAC 地址作为 IP 对 MAC 的映射关系到一个 ARP 缓存表中,下一次再向这个地址发送数据报时就不再需要重新发送 ARP 请求了,而是直接使用这个缓存表中的 MAC 地址进行数据报的发送。每发送一次 ARP 请求,缓存表中对应的映射关系都会被清除。

通过 ARP 缓存,降低了网络流量的使用,在一定程度上防止了 ARP 的大量广播。

一般来说,发送过一次 ARP 请求后,再次发送相同请求的几率比较大,因此使用 ARP 缓存能够减少 ARP 包的发送,除此之外,不仅仅 ARP 请求的发送方能够缓存 ARP 接收方的 MAC 地址,接收方也能够缓存 ARP 请求方的 IP 和 MAC 地址,如下所示

不过,MAC 地址的缓存有一定期限,超过这个期限后,缓存的内容会被清除

深入理解 ARP 协议的话,可以参考 cxuan 的这篇文章。

ARP,这个隐匿在计网背后的男人

所以,浏览器会首先查询 ARP 缓存,如果缓存命中,我们返回结果:目标 IP = MAC。

如果缓存没有命中:

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

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

直连:

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

集线器:

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

交换机:

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

ARP Reply:

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

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

(上述均来自:https://github.com/skyline75489/what-happens-when-zh_CN#dns

封装 TCP 数据包

浏览器得到目标服务器的 IP 地址后,根据 URL 中的端口可以知道端口号 (http 协议默认端口号是 80, https 默认端口号是 443),会准备 TCP 数据包。数据包的封装会经过下面的层层处理,数据到达目标主机后,目标主机会解析数据包,完整的请求和解析过程如下。

这里就不再详细介绍了,读者朋友们可以阅读 cxuan 的这篇文章 TCP/IP 基础知识详解详细了解。

浏览器与目标服务器建立 TCP 连接

在经过上述 DNS 和 ARP 查找流程后,浏览器就会收到一个目标服务器的 IP 和 MAC地址,然后浏览器将会和目标服务器建立连接来传输信息。这里可以使用很多种 Internet 协议,但是 HTTP 协议建立连接所使用的运输层协议是 TCP 协议。所以这一步骤是浏览器与目标服务器建立 TCP 连接的过程。

TCP 的连接建立需要经过 TCP/IP 的三次握手,三次握手的过程其实就是浏览器和服务器交换 SYN 同步和 ACK 确认消息的过程。

假设图中左端是客户端主机,右端是服务端主机,一开始,两端都处于CLOSED(关闭)状态。

  1. 服务端进程准备好接收来自外部的 TCP 连接。然后服务端进程处于 LISTEN 状态,等待客户端连接请求。
  2. 客户端向服务器发出连接请求,请求中首部同步位 SYN = 1,同时选择一个初始序号 sequence ,简写 seq = x。SYN 报文段不允许携带数据,只消耗一个序号。此时,客户端进入 SYN-SEND 状态。
  3. 服务器收到客户端连接后,,需要确认客户端的报文段。在确认报文段中,把 SYN 和 ACK 位都置为 1 。确认号是 ack = x + 1,同时也为自己选择一个初始序号 seq = y。请注意,这个报文段也不能携带数据,但同样要消耗掉一个序号。此时,TCP 服务器进入 SYN-RECEIVED(同步收到) 状态。
  4. 客户端在收到服务器发出的响应后,还需要给出确认连接。确认连接中的 ACK 置为 1 ,序号为 seq = x + 1,确认号为 ack = y + 1。TCP 规定,这个报文段可以携带数据也可以不携带数据,如果不携带数据,那么下一个数据报文段的序号仍是 seq = x + 1。这时,客户端进入 ESTABLISHED (已连接) 状态
  5. 服务器收到客户的确认后,也进入 ESTABLISHED 状态。

这样三次握手建立连接的阶段就完成了,双方可以直接通信了。

浏览器发送 HTTP 请求到 web 服务器

一旦 TCP 连接建立完成后,就开始直接传输数据办正事了!此时浏览器会发送 GET 请求,要求目标服务器提供 maps.google.com 的网页,如果你填写的是表单,则发起的是 POST 请求,在 HTTP 中,GET 请求和 POST 请求是最常见的两种请求,基本上占据了所有 HTTP 请求的九成以上。

除了请求类型外,HTTP 请求还包含很多很多信息,最常见的有 Host、Connection 、User-agent、Accept-language 等

首先 Host 表示的是对象所在的主机。Connection: close 表示的是浏览器需要告诉服务器使用的是非持久连接。它要求服务器在发送完响应的对象后就关闭连接。User-agent: 这是请求头用来告诉 Web 服务器,浏览器使用的类型是 Mozilla/5.0,即 Firefox 浏览器。Accept-language 告诉 Web 服务器,浏览器想要得到对象的法语版本,前提是服务器需要支持法语类型,否则将会发送服务器的默认版本。下面我们针对主要的实体字段进行介绍(具体的可以参考 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers MDN 官网学习)

HTTP 的请求标头分为四种: 通用标头请求标头响应标头实体标头

这四种标头又分别有很多内容,如果你想要深入理解一下关于 HTTP 请求头的相关内容,可以参考 cxuan 的这篇文章

深入理解 HTTP 标头

服务器处理请求并发回一个响应

这个服务器包含一个 Web 服务器,也就是 Apache 服务器,服务器会从浏览器接收请求并将其传递给请求处理程序并生成响应。

请求处理程序也是一个程序,它一般是用 .net 、php、ruby 等语言编写,用于读取请求,检查请求内容,cookie,必要时更新服务器上的信息的这么一个程序。它会以特定的格式比如 JSON、XML、HTML 组合响应。

服务器发送回一个 HTTP 响应

服务器响应包含你请求的网页以及状态代码,压缩类型(Content-Encoding),如何缓存页面(Cache-Control),要设置的 cookie,隐私信息等。

比如下面就是一个响应体

关于深入理解 HTTP 请求和响应,可以参考这篇文章

看完这篇HTTP,跟面试官扯皮就没问题了

浏览器显示 HTML 的相关内容

浏览器会分阶段显示 HTML 内容。 首先,它将渲染裸露的 HTML 骨架。 然后它将检查 HTML 标记并发送 GET 请求以获取网页上的其他元素,例如图像,CSS 样式表,JavaScript 文件等。这些静态文件由浏览器缓存,因此你再次访问该页面时,不用重新再请求一次。最后,您会看到 maps.google.com 显示的内容出现在你的浏览器中。

TCP传输控制协议

原文:https://zwmst.com/2698.html

TCP 是一种面向连接的单播协议,在 TCP 中,并不存在多播、广播的这种行为,因为 TCP 报文段中能明确发送方和接受方的 IP 地址。

在发送数据前,相互通信的双方(即发送方和接受方)需要建立一条连接,在发送数据后,通信双方需要断开连接,这就是 TCP 连接的建立和终止。

TCP 连接的建立和终止

如果你看过我之前写的关于网络层的一篇文章,你应该知道 TCP 的基本元素有四个:即发送方的 IP 地址、发送方的端口号、接收方的 IP 地址、接收方的端口号。而每一方的 IP + 端口号都可以看作是一个套接字,套接字能够被唯一标示。套接字就相当于是门,出了这个门,就要进行数据传输了。

TCP 的连接建 立 -> 终止总共分为三个阶段

下面我们所讨论的重点也是集中在这三个层面。

下图是一个非常典型的 TCP 连接的建立和关闭过程,其中不包括数据传输的部分。

TCP 建立连接 – 三次握手

  1. 服务端进程准备好接收来自外部的 TCP 连接,一般情况下是调用 bind、listen、socket 三个函数完成。这种打开方式被认为是 被动打开(passive open)。然后服务端进程处于 LISTEN 状态,等待客户端连接请求。
  2. 客户端通过 connect 发起主动打开(active open),向服务器发出连接请求,请求中首部同步位 SYN = 1,同时选择一个初始序号 sequence ,简写 seq = x。SYN 报文段不允许携带数据,只消耗一个序号。此时,客户端进入 SYN-SEND 状态。
  3. 服务器收到客户端连接后,,需要确认客户端的报文段。在确认报文段中,把 SYN 和 ACK 位都置为 1 。确认号是 ack = x + 1,同时也为自己选择一个初始序号 seq = y。这个报文段也不能携带数据,但同样要消耗掉一个序号。此时,TCP 服务器进入 SYN-RECEIVED(同步收到) 状态。
  4. 客户端在收到服务器发出的响应后,还需要给出确认连接。确认连接中的 ACK 置为 1 ,序号为 seq = x + 1,确认号为 ack = y + 1。TCP 规定,这个报文段可以携带数据也可以不携带数据,如果不携带数据,那么下一个数据报文段的序号仍是 seq = x + 1。这时,客户端进入 ESTABLISHED (已连接) 状态
  5. 服务器收到客户的确认后,也进入 ESTABLISHED 状态。

这是一个典型的三次握手过程,通过上面 3 个报文段就能够完成一个 TCP 连接的建立。三次握手的的目的不仅仅在于让通信双方知晓正在建立一个连接,也在于利用数据包中的选项字段来交换一些特殊信息,交换初始序列号

一般首个发送 SYN 报文的一方被认为是主动打开一个连接,而这一方通常也被称为客户端。而 SYN 的接收方通常被称为服务端,它用于接收这个 SYN,并发送下面的 SYN,因此这种打开方式是被动打开。

TCP 建立一个连接需要三个报文段,释放一个连接却需要四个报文段。

TCP 断开连接 – 四次挥手

数据传输结束后,通信的双方可以释放连接。数据传输结束后的客户端主机和服务端主机都处于 ESTABLISHED 状态,然后进入释放连接的过程。

TCP 断开连接需要历经的过程如下

  1. 客户端应用程序发出释放连接的报文段,并停止发送数据,主动关闭 TCP 连接。客户端主机发送释放连接的报文段,报文段中首部 FIN 位置为 1 ,不包含数据,序列号位 seq = u,此时客户端主机进入 FIN-WAIT-1(终止等待 1) 阶段。

  2. 服务器主机接受到客户端发出的报文段后,即发出确认应答报文,确认应答报文中 ACK = 1,生成自己的序号位 seq = v,ack = u + 1,然后服务器主机就进入 CLOSE-WAIT(关闭等待) 状态。

  3. 客户端主机收到服务端主机的确认应答后,即进入 FIN-WAIT-2(终止等待2) 的状态。等待客户端发出连接释放的报文段。

  4. 这时服务端主机会发出断开连接的报文段,报文段中 ACK = 1,序列号 seq = v,ack = u + 1,在发送完断开请求的报文后,服务端主机就进入了 LAST-ACK(最后确认)的阶段。

  5. 客户端收到服务端的断开连接请求后,客户端需要作出响应,客户端发出断开连接的报文段,在报文段中,ACK = 1, 序列号 seq = u + 1,因为客户端从连接开始断开后就没有再发送数据,ack = v + 1,然后进入到 TIME-WAIT(时间等待) 状态,请注意,这个时候 TCP 连接还没有释放。必须经过时间等待的设置,也就是 2MSL 后,客户端才会进入 CLOSED 状态,时间 MSL 叫做最长报文段寿命(Maximum Segment Lifetime)

  6. 服务端主要收到了客户端的断开连接确认后,就会进入 CLOSED 状态。因为服务端结束 TCP 连接时间要比客户端早,而整个连接断开过程需要发送四个报文段,因此释放连接的过程也被称为四次挥手。

TCP 连接的任意一方都可以发起关闭操作,只不过通常情况下发起关闭连接操作一般都是客户端。然而,一些服务器比如 Web 服务器在对请求作出相应后也会发起关闭连接的操作。TCP 协议规定通过发送一个 FIN 报文来发起关闭操作。

所以综上所述,建立一个 TCP 连接需要三个报文段,而关闭一个 TCP 连接需要四个报文段。TCP 协议还支持一种半开启(half-open) 状态,虽然这种情况并不多见。

TCP 半开启

TCP 连接处于半开启的这种状态是因为连接的一方关闭或者终止了这个 TCP 连接却没有通知另一方,也就是说两个人正在微信聊天,cxuan 你下线了你不告诉我,我还在跟你侃八卦呢。此时就认为这条连接处于半开启状态。这种情况发生在通信中的一方处于主机崩溃的情况下,你 xxx 的,我电脑死机了我咋告诉你?只要处于半连接状态的一方不传输数据的话,那么是无法检测出来对方主机已经下线的。

另外一种处于半开启状态的原因是通信的一方关闭了主机电源 而不是正常关机。这种情况下会导致服务器上有很多半开启的 TCP 连接。

TCP 半关闭

既然 TCP 支持半开启操作,那么我们可以设想 TCP 也支持半关闭操作。同样的,TCP 半关闭也并不常见。TCP 的半关闭操作是指仅仅关闭数据流的一个传输方向。两个半关闭操作合在一起就能够关闭整个连接。在一般情况下,通信双方会通过应用程序互相发送 FIN 报文段来结束连接,但是在 TCP 半关闭的情况下,应用程序会表明自己的想法:"我已经完成了数据的发送发送,并发送了一个 FIN 报文段给对方,但是我依然希望接收来自对方的数据直到它发送一个 FIN 报文段给我"。 下面是一个 TCP 半关闭的示意图。

解释一下这个过程:

首先客户端主机和服务器主机一直在进行数据传输,一段时间后,客户端发起了 FIN 报文,要求主动断开连接,服务器收到 FIN 后,回应 ACK ,由于此时发起半关闭的一方也就是客户端仍然希望服务器发送数据,所以服务器会继续发送数据,一段时间后服务器发送另外一条 FIN 报文,在客户端收到 FIN 报文回应 ACK 给服务器后,断开连接。

TCP 的半关闭操作中,连接的一个方向被关闭,而另一个方向仍在传输数据直到它被关闭为止。只不过很少有应用程序使用这一特性。

同时打开和同时关闭

还有一种比较非常规的操作,这就是两个应用程序同时主动打开连接。虽然这种情况看起来不太可能,但是在特定的安排下却是有可能发生的。我们主要讲述这个过程。

通信双方在接收到来自对方的 SYN 之前会首先发送一个 SYN,这个场景还要求通信双方都知道对方的 IP 地址 + 端口号

比如恋爱中的一对男女,他俩都同时说出了我爱你这个神圣的誓言,然后他俩对彼此的响应进行么么哒,这就是同时打开。

下面是同时打开的例子

如上图所示,通信双方都在收到对方报文前主动发送了 SYN 报文,都在收到彼此的报文后回复了一个 ACK 报文。

一个同时打开过程需要交换四个报文段,比普通的三次握手增加了一个,由于同时打开没有客户端和服务器一说,所以这里我用了通信双方来称呼。

像同时打开一样,同时关闭也是通信双方同时提出主动关闭请求,发送 FIN 报文,下图显示了一个同时关闭的过程。

同时关闭过程中需要交换和正常关闭相同数量的报文段,只不过同时关闭不像四次挥手那样顺序进行,而是交叉进行的。

聊一聊初始序列号

也许是我上面图示或者文字描述的不专业,初始序列号它是有专业术语表示的,初始序列号的英文名称是Initial sequence numbers (ISN),所以我们上面表示的 seq = v,其实就表示的 ISN。

在发送 SYN 之前,通信双方会选择一个初始序列号。初始序列号是随机生成的,每一个 TCP 连接都会有一个不同的初始序列号。RFC 文档指出初始序列号是一个 32 位的计数器,每 4 us(微秒) + 1。因为每个 TCP 连接都是一个不同的实例,这么安排的目的就是为了防止出现序列号重叠的情况。

当一个 TCP 连接建立的过程中,只有正确的 TCP 四元组和正确的序列号才会被对方接收。这也反应了 TCP 报文段容易被伪造 的脆弱性,因为只要我伪造了一个相同的四元组和初始序列号就能够伪造 TCP 连接,从而打断 TCP 的正常连接,所以抵御这种攻击的一种方式就是使用初始序列号,另外一种方法就是加密序列号。

TCP 状态转换

我们上面聊到了三次握手和四次挥手,提到了一些关于 TCP 连接之间的状态转换,那么下面我就从头开始和你好好梳理一下这些状态之间的转换。

首先第一步,刚开始时服务器和客户端都处于 CLOSED 状态,这时需要判断是主动打开还是被动打开,如果是主动打开,那么客户端向服务器发送 SYN 报文,此时客户端处于 SYN-SEND 状态,SYN-SEND 表示发送连接请求后等待匹配的连接请求,服务器被动打开会处于 LISTEN 状态,用于监听 SYN 报文。如果客户端调用了 close 方法或者经过一段时间没有操作,就会重新变为 CLOSED 状态,这一步转换图如下

这里有个疑问,为什么处于 LISTEN 状态下的客户端还会发送 SYN 变为 SYN_SENT 状态呢?

知乎看到了车小胖大佬的回答,这种情况可能出现在 FTP 中,LISTEN -> SYN_SENT 是因为这个连接可能是由于服务器端的应用有数据发送给客户端所触发的,客户端被动接受连接,连接建立后,开始传输文件。也就是说,处于 LISTEN 状态的服务器也是有可能发送 SYN 报文的,只不过这种情况非常少见。

处于 SYN_SEND 状态的服务器会接收 SYN 并发送 SYN 和 ACK 转换成为 SYN_RCVD 状态,同样的,处于 LISTEN 状态的客户端也会接收 SYN 并发送 SYN 和 ACK 转换为 SYN_RCVD 状态。如果处于 SYN_RCVD 状态的客户端收到 RST 就会变为 LISTEN 状态。

这两张图一起看会比较好一些。

这里需要解释下什么是 RST

这里有一种情况是当主机收到 TCP 报文段后,其 IP 和端口号不匹配的情况。假设客户端主机发送一个请求,而服务器主机经过 IP 和端口号的判断后发现不是给这个服务器的,那么服务器就会发出一个 RST 特殊报文段给客户端。

因此,当服务端发送一个 RST 特殊报文段给客户端的时候,它就会告诉客户端没有匹配的套接字连接,请不要再继续发送了

RST:(Reset the connection)用于复位因某种原因引起出现的错误连接,也用来拒绝非法数据和请求。如果接收到 RST 位时候,通常发生了某些错误。

上面没有识别正确的 IP 端口是一种导致 RST 出现的情况,除此之外,RST 还可能由于请求超时、取消一个已存在的连接等出现。

位于 SYN_RCVD 的服务器会接收 ACK 报文,SYN_SEND 的客户端会接收 SYN 和 ACK 报文,并发送 ACK 报文,由此,客户端和服务器之间的连接就建立了。

这里还要注意一点,同时打开的状态我在上面没有刻意表示出来,实际上,在同时打开的情况下,它的状态变化是这样的。

为什么会是这样呢?因为你想,在同时打开的情况下,两端主机都发起 SYN 报文,而主动发起 SYN 的主机会处于 SYN-SEND 状态,发送完成后,会等待接收 SYN 和 ACK , 在双方主机都发送了 SYN + ACK 后,双方都处于 SYN-RECEIVED(SYN-RCVD) 状态,然后等待 SYN + ACK 的报文到达后,双方就会处于 ESTABLISHED 状态,开始传输数据。

好了,到现在为止,我给你叙述了一下 TCP 连接建立过程中的状态转换,现在你可以泡一壶茶喝点水,等着数据传输了。

好了,现在水喝够了,这时候数据也传输完成了,数据传输完成后,这条 TCP 连接就可以断开了。

现在我们把时钟往前拨一下,调整到服务端处于 SYN_RCVD 状态的时刻,因为刚收到了 SYN 包并发送了 SYN + ACK 包,此时服务端很开心,但是这时,服务端应用进程关闭了,然后应用进程发了一个 FIN 包,就会让服务器从 SYN_RCVD -> FIN_WAIT_1 状态。

然后把时钟调到现在,客户端和服务器现在已经传输完数据了 ,此时客户端发送了一条 FIN 报文希望断开连接,此时客户端也会变为 FIN_WAIT_1 状态,对于服务器来说,它接收到了 FIN 报文段并回复了 ACK 报文,就会从 ESTABLISHED -> CLOSE_WAIT 状态。

位于 CLOSE_WAIT 状态的服务端会发送 FIN 报文,然后把自己置于 LAST_ACK 状态。处于 FIN_WAIT_1 的客户端接收 ACK 消息就会变为 FIN_WAIT_2 状态。

这里需要先解释一下 CLOSING 这个状态,FIN_WAIT_1 -> CLOSING 的转换比较特殊

CLOSING 这种状态比较特殊,实际情况中应该是很少见,属于一种比较罕见的例外状态。正常情况下,当你发送FIN 报文后,按理来说是应该先收到(或同时收到)对方的 ACK 报文,再收到对方的 FIN 报文。但是 CLOSING 状态表示你发送 FIN 报文后,并没有收到对方的 ACK 报文,反而却也收到了对方的 FIN 报文。

什么情况下会出现此种情况呢?其实细想一下,也不难得出结论:那就是如果双方在同时关闭一个链接的话,那么就出现了同时发送 FIN 报文的情况,也即会出现 CLOSING 状态,表示双方都正在关闭连接。

FIN_WAIT_2 状态的客户端接收服务端主机发送的 FIN + ACK 消息,并发送 ACK 响应后,会变为 TIME_WAIT 状态。处于 CLOSE_WAIT 的客户端发送 FIN 会处于 LAST_ACK 状态。

这里不少图和博客虽然在图上画的是 FIN + ACK 报文后才会处于 LAST_ACK 状态,但是描述的时候,一般通常只对于 FIN 进行描述。也就是说 CLOSE_WAIT 发送 FIN 才会处于 LAST_ACK 状态。

所以这里 FIN_WAIT_1 -> TIME_WAIT 的状态也就是接收 FIN 和 ACK 并发送 ACK 之后,客户端处于的状态。

然后位于 CLOSINIG 状态的客户端这时候还有 ACK 接收的话,会继续处于 TIME_WAIT 状态,可以看到,TIME_WAIT 状态相当于是客户端在关闭前的最后一个状态,它是一种主动关闭的状态;而 LAST_ACK 是服务端在关闭前的最后一个状态,它是一种被动打开的状态。

上面有几个状态比较特殊,这里我们向西解释下。

TIME_WAIT 状态

通信双方建立 TCP 连接后,主动关闭连接的一方就会进入 TIME_WAIT 状态。TIME_WAIT 状态也称为 2MSL 的等待状态。在这个状态下,TCP 将会等待最大段生存期(Maximum Segment Lifetime, MSL) 时间的两倍。

这里需要解释下 MSL

MSL 是 TCP 段期望的最大生存时间,也就是在网络中存在的最长时间。这个时间是有限制的,因为我们知道 TCP 是依靠 IP 数据段来进行传输的,IP 数据报中有 TTL 和跳数的字段,这两个字段决定了 IP 的生存时间,一般情况下,TCP 的最大生存时间是 2 分钟,不过这个数值是可以修改的,根据不同操作系统可以修改此值。

基于此,我们来探讨 TIME_WAIT 的状态。

当 TCP 执行一个主动关闭并发送最终的 ACK 时,TIME_WAIT 应该以 2 * 最大生存时间存在,这样就能够让 TCP 重新发送最终的 ACK 以避免出现丢失的情况。重新发送最终的 ACK 并不是因为 TCP 重传了 ACK,而是因为通信另一方重传了 FIN,客户端经常回发送 FIN,因为它需要 ACK 的响应才能够关闭连接,如果生存时间超过了 2MSL 的话,客户端就会发送 RST,使服务端出错。

Web页面的请求历程

原文:https://zwmst.com/2700.html

Hey guys 各位读者姥爷们大家好,这里是程序员 cxuan 计算机网络连载系列的第 13 篇文章。

到现在为止,我们算是把应用层、运输层、网络层和数据链路层都介绍完了,那么现在是时候把这些内容都串起来,做一个全面的回顾了。那么我这就以 Web 页面的请求历程为例,来和你聊聊计算机网络中这些协议是怎样工作的、数据包是怎么收发的,从输入 URL 、敲击会车到最终完成页面呈现在你面前的这个过程。

首先,我打开了 Web Browser ,然后在 Google 浏览器 URL 地址栏中输入了 maps.google.com

然后 ……

查找 DNS 缓存

浏览器在这个阶段会检查四个地方是否存在缓存,第一个地方是浏览器缓存,这个缓存就是 DNS 记录。

浏览器会为你访问过的网站在固定期限内维护 DNS 记录。因此,它是第一个运行 DNS 查询的地方。 浏览器首先会检查这个网址在浏览器中是否有一条对应的 DNS 记录,用来找到目标网址的 IP 地址。

我是 chrome 浏览器,所以在 mac 中,无法使用 chrome://net-internals/#dns 找到对应的 IP 地址,在 windows 中是可以找到的。

那么 mac 怎么查询 DNS 记录呢?你可以使用 nslookup 命令来查找,但这不是我们讨论的重点。

DNS(Domain Name System) 是一个分布式的数据库,它用于维护网址 URL 到其 IP 地址的映射关系。在互联网中,IP 地址是计算机所能够理解的一种地址,而 DNS 的这种别名地址是我们人类能够理解和记忆的地址,DNS 就负责把人类记忆的地址映射成计算机能够理解的地址,每个 URL 都有唯一的 IP 地址进行对应。

举个例子,google 的官网是 www.google.com ,而 google 的 ip 地址是 216.58.200.228 ,这两个地址你在 URL 上输入哪个都能访问,但是 IP 地址不好记忆,而 google.com 简单明了。DNS 就相当于是我们几年前使用的家庭电话薄,比如你想给 cxuan 打电话,你有可能记不住 cxuan 的电话号码,此时你需要查询电话薄来找到 cxuan 的电话号码。

浏览器第二个需要检查的地方就是操作系统缓存。如果 DNS 记录不在浏览器缓存中,那么浏览器将对操作系统发起系统调用,Windows 下就是 getHostName

在 Linux 和大部分 UNIX 系统上,除非安装了 nscd,否则操作系统可能没有 DNS 缓存。

nscd 是 Linux 系统上的一种名称服务缓存程序

浏览低第三个需要检查的地方是路由器缓存,如果 DNS 记录不在自己电脑上的话,浏览器就会和与之相连的路由器共同维护 DNS 记录。

如果与之相连的路由器也没有 DNS 记录的话,浏览器就会检查 ISP 中是否有缓存。ISP 缓存就是你本地通信服务商的缓存,因为 ISP 维护着自己的 DNS 服务器,它缓存 DNS 记录的本质也是为了降低请求时间,达到快速响应的效果。一旦你访问过某些网站,你的 ISP 可能就会缓存这些页面,以便下次快速访问。对于经常看小电影的你是否感到震惊呢?如果家里还安装了一个可以联网的摄像头的话,那就有点嗨皮了。

你肯定比较困惑为什么第一步浏览器需要检查这么多缓存,你可能会感到不舒服,因为缓存可能会透露我们的隐私,但是这些缓存在调节网络流量和缩短数据传输时间等方面至关重要。

所以,上面涉及到 DNS 缓存的查询过程如下。

如果上面四个步骤中都不存在 DNS 记录,那么就表示不存在 DNS 缓存,这个时候就需要发起 DNS 查询,以查找目标网址(本示例中是 maps.google.com)的 IP 地址。

发起 DNS 查询

如上所述,如果想要使我的计算机和 maps.google.com 建立连接并进行通信的话,我需要知道 maps.google.com 的 IP 地址,由于 DNS 的设计原因,本地 DNS 可能无法给我提供正确的 IP 地址,那么它就需要在互联网上搜索多个 DNS 服务器,来找到网站的正确 IP 地址。

这里有个疑问,为什么我需要搜索多个 DNS 服务器的来找到网站的 IP 地址呢?一台服务器不行吗?

因为 DNS 是分布式域名服务器,每台服务器只维护一部分 IP 地址到网络地址的映射,没有任何一台服务器能够维持全部的映射关系。

在 DNS 的早期设计中只有一台 DNS 服务器。这台服务器会包含所有的 DNS 映射。这是一种集中式的设计,这种设计并不适用于当今的互联网,因为互联网有着数量巨大并且持续增长的主机,这种集中式的设计会存在以下几个问题

  • 单点故障(a single point of failure),如果 DNS 服务器崩溃,那么整个网络随之瘫痪。
  • 通信容量(traaffic volume),单个 DNS 服务器不得不处理所有的 DNS 查询,这种查询级别可能是上百万上千万级,一台服务器很难满足。
  • 远距离集中式数据库(distant centralized database),单个 DNS 服务器不可能 邻近 所有的用户,假设在美国的 DNS 服务器不可能临近让澳大利亚的查询使用,其中查询请求势必会经过低速和拥堵的链路,造成严重的时延。
  • 维护(maintenance),维护成本巨大,而且还需要频繁更新。

所以在当今网络情况下 DNS 不可能集中式设计,因为它完全没有可扩展能力,所以采用分布式设计,这种设计的特点如下

分布式、层次数据库

首先分布式设计首先解决的问题就是 DNS 服务器的扩展性问题,因此 DNS 使用了大量的 DNS 服务器,它们的组织模式一般是层次方式,并且分布在全世界范围内。没有一台 DNS 服务器能够拥有因特网上所有主机的映射。相反,这些映射分布在所有的 DNS 服务器上。

大致来说有三种 DNS 服务器:根 DNS 服务器顶级域(Top-Level Domain, TLD) DNS 服务器权威 DNS 服务器 。这些服务器的层次模型如下图所示

  • 根 DNS 服务器 ,有 400 多个根域名服务器遍及全世界,这些根域名服务器由 13 个不同的组织管理。根域名服务器的清单和组织机构可以在 https://root-servers.org/ 中找到,根域名服务器提供 TLD 服务器的 IP 地址。
  • 顶级域 DNS 服务器,对于每个顶级域名比如 com、org、net、edu 和 gov 和所有的国家级域名 uk、fr、ca 和 jp 都有 TLD 服务器或服务器集群。所有的顶级域列表参见 https://tld-list.com/ 。TDL 服务器提供了权威 DNS 服务器的 IP 地址。
  • 权威 DNS 服务器,在因特网上具有公共可访问的主机,如 Web 服务器和邮件服务器,这些主机的组织机构必须提供可供访问的 DNS 记录,这些记录将这些主机的名字映射为 IP 地址。一个组织机构的权威 DNS 服务器收藏了这些 DNS 记录。

在了解了 DNS 服务器的设计理念之后,我们回到 DNS 查找的步骤上来,DNS 的查询方式主要分为三种

DNS 查找中会出现三种类型的查询。通过组合使用这些查询,优化的 DNS 解析过程可缩短传输距离。在理想情况下,可以使用缓存的记录数据,从而使 DNS 域名服务器能够直接使用非递归查询。

  • 递归查询:在递归查询中,DNS 客户端要求 DNS 服务器(一般为 DNS 递归解析器)将使用所请求的资源记录响应客户端,或者如果解析器无法找到该记录,则返回错误消息。

  • 迭代查询:在迭代查询中,如果所查询的 DNS 服务器与查询名称不匹配,则其将返回对较低级别域名空间具有权威性的 DNS 服务器的引用。然后,DNS 客户端将对引用地址进行查询。此过程继续使用查询链中的其他 DNS 服务器,直至发生错误或超时为止。

  • 非递归查询:当 DNS 解析器客户端查询 DNS 服务器以获取其有权访问的记录时通常会进行此查询,因为其对该记录具有权威性,或者该记录存在于其缓存内。DNS 服务器通常会缓存 DNS 记录,查询到来后能够直接返回缓存结果,以防止更多带宽消耗和上游服务器上的负载。

上面负责开始 DNS 查找的介质就是 DNS 解析器,它一般是 ISP 维护的 DNS 服务器,它的主要职责就是通过向网络中其他 DNS 服务器询问正确的 IP 地址。

如果想要了解更多关于 DNS 的消息,请查阅 万字长文爆肝 DNS 协议!

所以对于 maps.google.com 这个域名来说,如果 ISP 维护的服务器没有 DNS 缓存记录,它就会向 DNS 根服务器地址发起查询,根名称服务器会将其重定向到 .com 顶级域名服务器。 .com 顶级域名服务器会将其重定向到google.com 权威服务器。google.com 名称服务器将在其 DNS 记录中找到 maps.google.com 匹配的 IP 地址,并将其返回给您的 DNS 解析器,然后将其发送回你的浏览器。

这里值得注意的是,DNS 查询报文会经过许多路由器和设备才会达到根域名等服务器,每经过一个设备或者路由器都会使用路由表 来确定哪种路径是数据包达到目的地最快的选择。这里面涉及到路由选择算法,如果小伙伴们想要了解路由选择算法,可以看看这篇文章 https://www.cisco.com/c/en/us/support/docs/ip/border-gateway-protocol-bgp/13753-25.html#anc3

ARP 请求

我看了很多篇文章都没有提到这一点,那就是 ARP 请求的这个过程。

什么时候需要发送 ARP 请求呢?

这里其实有个前提条件

  • 如果 DNS 服务器和我们的主机在同一个子网内,系统会按照下面的 ARP 过程对 DNS 服务器进行 ARP 查询
  • 如果 DNS 服务器和我们的主机在不同的子网,系统会按照下面的 ARP 过程对默认网关进行查询

ARP 协议的全称是 Address Resolution Protocol(地址解析协议),它是一个通过用于实现从 IP 地址到 MAC 地址的映射,即询问目标 IP 对应的 MAC 地址 的一种协议。

简而言之,ARP 就是一种解决地址问题的协议,它以 IP 地址为线索,定位下一个应该接收数据分包的主机 MAC 地址。如果目标主机不在同一个链路上,那么会查找下一跳路由器的 MAC 地址。

关于为什么有了 IP 地址,还要有 MAC 地址概述可以参看知乎这个回答 https://www.zhihu.com/question/21546408

ARP 的大致工作流程如下

假设 A 和 B 位于同一链路,不需要经过路由器的转换,主机 A 向主机 B 发送一个 IP 分组,主机 A 的地址是 192.168.1.2 ,主机 B 的地址是 192.168.1.3,它们都不知道对方的 MAC 地址是啥,主机 C 和 主机 D 是同一链路的其他主机。

主机 A 想要获取主机 B 的 MAC 地址,通过主机 A 会通过广播 的方式向以太网上的所有主机发送一个 ARP 请求包,这个 ARP 请求包中包含了主机 A 想要知道的主机 B 的 IP 地址的 MAC 地址。

主机 A 发送的 ARP 请求包会被同一链路上的所有主机/路由器接收并进行解析。每个主机/路由器都会检查 ARP 请求包中的信息,如果 ARP 请求包中的目标 IP 地址 和自己的相同,就会将自己主机的 MAC 地址写入响应包返回主机 A

由此,可以通过 ARP 从 IP 地址获取 MAC 地址,实现同一链路内的通信。

所以,要想发送 ARP 广播,我们需要有一个目标 IP 地址,同时还需要知道用于发送 ARP 广播的接口的 MAC 地址。

这里会涉及到 ARP 缓存的概念。

现在你知道了发送一次 IP 分组前通过发送一次 ARP 请求就能够确定 MAC 地址。那么是不是每发送一次都得经过广播 -> 封装 ARP 响应 -> 返回给主机这一系列流程呢?

想想看,浏览器是如何做的?浏览器内置了缓存能够缓存你最近经常使用的地址,那么 ARP 也是一样的。ARP 高效运行的关键就是维护每个主机和路由器上的 ARP 缓存(或表)。这个缓存维护着每个 IP 到 MAC 地址的映射关系。通过把第一次 ARP 获取到的 MAC 地址作为 IP 对 MAC 的映射关系到一个 ARP 缓存表中,下一次再向这个地址发送数据报时就不再需要重新发送 ARP 请求了,而是直接使用这个缓存表中的 MAC 地址进行数据报的发送。每发送一次 ARP 请求,缓存表中对应的映射关系都会被清除。

通过 ARP 缓存,降低了网络流量的使用,在一定程度上防止了 ARP 的大量广播。

一般来说,发送过一次 ARP 请求后,再次发送相同请求的几率比较大,因此使用 ARP 缓存能够减少 ARP 包的发送,除此之外,不仅仅 ARP 请求的发送方能够缓存 ARP 接收方的 MAC 地址,接收方也能够缓存 ARP 请求方的 IP 和 MAC 地址,如下所示

不过,MAC 地址的缓存有一定期限,超过这个期限后,缓存的内容会被清除

深入理解 ARP 协议的话,可以参考 cxuan 的这篇文章。

ARP,这个隐匿在计网背后的男人

所以,浏览器会首先查询 ARP 缓存,如果缓存命中,我们返回结果:目标 IP = MAC。

如果缓存没有命中:

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

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

直连:

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

集线器:

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

交换机:

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

ARP Reply:

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

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

(上述均来自:https://github.com/skyline75489/what-happens-when-zh_CN#dns

封装 TCP 数据包

浏览器得到目标服务器的 IP 地址后,根据 URL 中的端口可以知道端口号 (http 协议默认端口号是 80, https 默认端口号是 443),会准备 TCP 数据包。数据包的封装会经过下面的层层处理,数据到达目标主机后,目标主机会解析数据包,完整的请求和解析过程如下。

这里就不再详细介绍了,读者朋友们可以阅读 cxuan 的这篇文章 TCP/IP 基础知识详解详细了解。

浏览器与目标服务器建立 TCP 连接

在经过上述 DNS 和 ARP 查找流程后,浏览器就会收到一个目标服务器的 IP 和 MAC地址,然后浏览器将会和目标服务器建立连接来传输信息。这里可以使用很多种 Internet 协议,但是 HTTP 协议建立连接所使用的运输层协议是 TCP 协议。所以这一步骤是浏览器与目标服务器建立 TCP 连接的过程。

TCP 的连接建立需要经过 TCP/IP 的三次握手,三次握手的过程其实就是浏览器和服务器交换 SYN 同步和 ACK 确认消息的过程。

假设图中左端是客户端主机,右端是服务端主机,一开始,两端都处于CLOSED(关闭)状态。

  1. 服务端进程准备好接收来自外部的 TCP 连接。然后服务端进程处于 LISTEN 状态,等待客户端连接请求。
  2. 客户端向服务器发出连接请求,请求中首部同步位 SYN = 1,同时选择一个初始序号 sequence ,简写 seq = x。SYN 报文段不允许携带数据,只消耗一个序号。此时,客户端进入 SYN-SEND 状态。
  3. 服务器收到客户端连接后,,需要确认客户端的报文段。在确认报文段中,把 SYN 和 ACK 位都置为 1 。确认号是 ack = x + 1,同时也为自己选择一个初始序号 seq = y。请注意,这个报文段也不能携带数据,但同样要消耗掉一个序号。此时,TCP 服务器进入 SYN-RECEIVED(同步收到) 状态。
  4. 客户端在收到服务器发出的响应后,还需要给出确认连接。确认连接中的 ACK 置为 1 ,序号为 seq = x + 1,确认号为 ack = y + 1。TCP 规定,这个报文段可以携带数据也可以不携带数据,如果不携带数据,那么下一个数据报文段的序号仍是 seq = x + 1。这时,客户端进入 ESTABLISHED (已连接) 状态
  5. 服务器收到客户的确认后,也进入 ESTABLISHED 状态。

这样三次握手建立连接的阶段就完成了,双方可以直接通信了。

浏览器发送 HTTP 请求到 web 服务器

一旦 TCP 连接建立完成后,就开始直接传输数据办正事了!此时浏览器会发送 GET 请求,要求目标服务器提供 maps.google.com 的网页,如果你填写的是表单,则发起的是 POST 请求,在 HTTP 中,GET 请求和 POST 请求是最常见的两种请求,基本上占据了所有 HTTP 请求的九成以上。

除了请求类型外,HTTP 请求还包含很多很多信息,最常见的有 Host、Connection 、User-agent、Accept-language 等

首先 Host 表示的是对象所在的主机。Connection: close 表示的是浏览器需要告诉服务器使用的是非持久连接。它要求服务器在发送完响应的对象后就关闭连接。User-agent: 这是请求头用来告诉 Web 服务器,浏览器使用的类型是 Mozilla/5.0,即 Firefox 浏览器。Accept-language 告诉 Web 服务器,浏览器想要得到对象的法语版本,前提是服务器需要支持法语类型,否则将会发送服务器的默认版本。下面我们针对主要的实体字段进行介绍(具体的可以参考 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers MDN 官网学习)

HTTP 的请求标头分为四种: 通用标头请求标头响应标头实体标头

这四种标头又分别有很多内容,如果你想要深入理解一下关于 HTTP 请求头的相关内容,可以参考 cxuan 的这篇文章

深入理解 HTTP 标头

服务器处理请求并发回一个响应

这个服务器包含一个 Web 服务器,也就是 Apache 服务器,服务器会从浏览器接收请求并将其传递给请求处理程序并生成响应。

请求处理程序也是一个程序,它一般是用 .net 、php、ruby 等语言编写,用于读取请求,检查请求内容,cookie,必要时更新服务器上的信息的这么一个程序。它会以特定的格式比如 JSON、XML、HTML 组合响应。

服务器发送回一个 HTTP 响应

服务器响应包含你请求的网页以及状态代码,压缩类型(Content-Encoding),如何缓存页面(Cache-Control),要设置的 cookie,隐私信息等。

比如下面就是一个响应体

关于深入理解 HTTP 请求和响应,可以参考这篇文章

看完这篇HTTP,跟面试官扯皮就没问题了

浏览器显示 HTML 的相关内容

浏览器会分阶段显示 HTML 内容。 首先,它将渲染裸露的 HTML 骨架。 然后它将检查 HTML 标记并发送 GET 请求以获取网页上的其他元素,例如图像,CSS 样式表,JavaScript 文件等。这些静态文件由浏览器缓存,因此你再次访问该页面时,不用重新再请求一次。最后,您会看到 maps.google.com 显示的内容出现在你的浏览器中。

TCP传输控制协议

原文:https://zwmst.com/2702.html

TCP 是一种面向连接的单播协议,在 TCP 中,并不存在多播、广播的这种行为,因为 TCP 报文段中能明确发送方和接受方的 IP 地址。

在发送数据前,相互通信的双方(即发送方和接受方)需要建立一条连接,在发送数据后,通信双方需要断开连接,这就是 TCP 连接的建立和终止。

TCP 连接的建立和终止

如果你看过我之前写的关于网络层的一篇文章,你应该知道 TCP 的基本元素有四个:即发送方的 IP 地址、发送方的端口号、接收方的 IP 地址、接收方的端口号。而每一方的 IP + 端口号都可以看作是一个套接字,套接字能够被唯一标示。套接字就相当于是门,出了这个门,就要进行数据传输了。

TCP 的连接建 立 -> 终止总共分为三个阶段

下面我们所讨论的重点也是集中在这三个层面。

下图是一个非常典型的 TCP 连接的建立和关闭过程,其中不包括数据传输的部分。

TCP 建立连接 – 三次握手

  1. 服务端进程准备好接收来自外部的 TCP 连接,一般情况下是调用 bind、listen、socket 三个函数完成。这种打开方式被认为是 被动打开(passive open)。然后服务端进程处于 LISTEN 状态,等待客户端连接请求。
  2. 客户端通过 connect 发起主动打开(active open),向服务器发出连接请求,请求中首部同步位 SYN = 1,同时选择一个初始序号 sequence ,简写 seq = x。SYN 报文段不允许携带数据,只消耗一个序号。此时,客户端进入 SYN-SEND 状态。
  3. 服务器收到客户端连接后,,需要确认客户端的报文段。在确认报文段中,把 SYN 和 ACK 位都置为 1 。确认号是 ack = x + 1,同时也为自己选择一个初始序号 seq = y。这个报文段也不能携带数据,但同样要消耗掉一个序号。此时,TCP 服务器进入 SYN-RECEIVED(同步收到) 状态。
  4. 客户端在收到服务器发出的响应后,还需要给出确认连接。确认连接中的 ACK 置为 1 ,序号为 seq = x + 1,确认号为 ack = y + 1。TCP 规定,这个报文段可以携带数据也可以不携带数据,如果不携带数据,那么下一个数据报文段的序号仍是 seq = x + 1。这时,客户端进入 ESTABLISHED (已连接) 状态
  5. 服务器收到客户的确认后,也进入 ESTABLISHED 状态。

这是一个典型的三次握手过程,通过上面 3 个报文段就能够完成一个 TCP 连接的建立。三次握手的的目的不仅仅在于让通信双方知晓正在建立一个连接,也在于利用数据包中的选项字段来交换一些特殊信息,交换初始序列号

一般首个发送 SYN 报文的一方被认为是主动打开一个连接,而这一方通常也被称为客户端。而 SYN 的接收方通常被称为服务端,它用于接收这个 SYN,并发送下面的 SYN,因此这种打开方式是被动打开。

TCP 建立一个连接需要三个报文段,释放一个连接却需要四个报文段。

TCP 断开连接 – 四次挥手

数据传输结束后,通信的双方可以释放连接。数据传输结束后的客户端主机和服务端主机都处于 ESTABLISHED 状态,然后进入释放连接的过程。

TCP 断开连接需要历经的过程如下

  1. 客户端应用程序发出释放连接的报文段,并停止发送数据,主动关闭 TCP 连接。客户端主机发送释放连接的报文段,报文段中首部 FIN 位置为 1 ,不包含数据,序列号位 seq = u,此时客户端主机进入 FIN-WAIT-1(终止等待 1) 阶段。

  2. 服务器主机接受到客户端发出的报文段后,即发出确认应答报文,确认应答报文中 ACK = 1,生成自己的序号位 seq = v,ack = u + 1,然后服务器主机就进入 CLOSE-WAIT(关闭等待) 状态。

  3. 客户端主机收到服务端主机的确认应答后,即进入 FIN-WAIT-2(终止等待2) 的状态。等待客户端发出连接释放的报文段。

  4. 这时服务端主机会发出断开连接的报文段,报文段中 ACK = 1,序列号 seq = v,ack = u + 1,在发送完断开请求的报文后,服务端主机就进入了 LAST-ACK(最后确认)的阶段。

  5. 客户端收到服务端的断开连接请求后,客户端需要作出响应,客户端发出断开连接的报文段,在报文段中,ACK = 1, 序列号 seq = u + 1,因为客户端从连接开始断开后就没有再发送数据,ack = v + 1,然后进入到 TIME-WAIT(时间等待) 状态,请注意,这个时候 TCP 连接还没有释放。必须经过时间等待的设置,也就是 2MSL 后,客户端才会进入 CLOSED 状态,时间 MSL 叫做最长报文段寿命(Maximum Segment Lifetime)

  6. 服务端主要收到了客户端的断开连接确认后,就会进入 CLOSED 状态。因为服务端结束 TCP 连接时间要比客户端早,而整个连接断开过程需要发送四个报文段,因此释放连接的过程也被称为四次挥手。

TCP 连接的任意一方都可以发起关闭操作,只不过通常情况下发起关闭连接操作一般都是客户端。然而,一些服务器比如 Web 服务器在对请求作出相应后也会发起关闭连接的操作。TCP 协议规定通过发送一个 FIN 报文来发起关闭操作。

所以综上所述,建立一个 TCP 连接需要三个报文段,而关闭一个 TCP 连接需要四个报文段。TCP 协议还支持一种半开启(half-open) 状态,虽然这种情况并不多见。

TCP 半开启

TCP 连接处于半开启的这种状态是因为连接的一方关闭或者终止了这个 TCP 连接却没有通知另一方,也就是说两个人正在微信聊天,cxuan 你下线了你不告诉我,我还在跟你侃八卦呢。此时就认为这条连接处于半开启状态。这种情况发生在通信中的一方处于主机崩溃的情况下,你 xxx 的,我电脑死机了我咋告诉你?只要处于半连接状态的一方不传输数据的话,那么是无法检测出来对方主机已经下线的。

另外一种处于半开启状态的原因是通信的一方关闭了主机电源 而不是正常关机。这种情况下会导致服务器上有很多半开启的 TCP 连接。

TCP 半关闭

既然 TCP 支持半开启操作,那么我们可以设想 TCP 也支持半关闭操作。同样的,TCP 半关闭也并不常见。TCP 的半关闭操作是指仅仅关闭数据流的一个传输方向。两个半关闭操作合在一起就能够关闭整个连接。在一般情况下,通信双方会通过应用程序互相发送 FIN 报文段来结束连接,但是在 TCP 半关闭的情况下,应用程序会表明自己的想法:"我已经完成了数据的发送发送,并发送了一个 FIN 报文段给对方,但是我依然希望接收来自对方的数据直到它发送一个 FIN 报文段给我"。 下面是一个 TCP 半关闭的示意图。

解释一下这个过程:

首先客户端主机和服务器主机一直在进行数据传输,一段时间后,客户端发起了 FIN 报文,要求主动断开连接,服务器收到 FIN 后,回应 ACK ,由于此时发起半关闭的一方也就是客户端仍然希望服务器发送数据,所以服务器会继续发送数据,一段时间后服务器发送另外一条 FIN 报文,在客户端收到 FIN 报文回应 ACK 给服务器后,断开连接。

TCP 的半关闭操作中,连接的一个方向被关闭,而另一个方向仍在传输数据直到它被关闭为止。只不过很少有应用程序使用这一特性。

同时打开和同时关闭

还有一种比较非常规的操作,这就是两个应用程序同时主动打开连接。虽然这种情况看起来不太可能,但是在特定的安排下却是有可能发生的。我们主要讲述这个过程。

通信双方在接收到来自对方的 SYN 之前会首先发送一个 SYN,这个场景还要求通信双方都知道对方的 IP 地址 + 端口号

比如恋爱中的一对男女,他俩都同时说出了我爱你这个神圣的誓言,然后他俩对彼此的响应进行么么哒,这就是同时打开。

下面是同时打开的例子

如上图所示,通信双方都在收到对方报文前主动发送了 SYN 报文,都在收到彼此的报文后回复了一个 ACK 报文。

一个同时打开过程需要交换四个报文段,比普通的三次握手增加了一个,由于同时打开没有客户端和服务器一说,所以这里我用了通信双方来称呼。

像同时打开一样,同时关闭也是通信双方同时提出主动关闭请求,发送 FIN 报文,下图显示了一个同时关闭的过程。

同时关闭过程中需要交换和正常关闭相同数量的报文段,只不过同时关闭不像四次挥手那样顺序进行,而是交叉进行的。

聊一聊初始序列号

也许是我上面图示或者文字描述的不专业,初始序列号它是有专业术语表示的,初始序列号的英文名称是Initial sequence numbers (ISN),所以我们上面表示的 seq = v,其实就表示的 ISN。

在发送 SYN 之前,通信双方会选择一个初始序列号。初始序列号是随机生成的,每一个 TCP 连接都会有一个不同的初始序列号。RFC 文档指出初始序列号是一个 32 位的计数器,每 4 us(微秒) + 1。因为每个 TCP 连接都是一个不同的实例,这么安排的目的就是为了防止出现序列号重叠的情况。

当一个 TCP 连接建立的过程中,只有正确的 TCP 四元组和正确的序列号才会被对方接收。这也反应了 TCP 报文段容易被伪造 的脆弱性,因为只要我伪造了一个相同的四元组和初始序列号就能够伪造 TCP 连接,从而打断 TCP 的正常连接,所以抵御这种攻击的一种方式就是使用初始序列号,另外一种方法就是加密序列号。

TCP 状态转换

我们上面聊到了三次握手和四次挥手,提到了一些关于 TCP 连接之间的状态转换,那么下面我就从头开始和你好好梳理一下这些状态之间的转换。

首先第一步,刚开始时服务器和客户端都处于 CLOSED 状态,这时需要判断是主动打开还是被动打开,如果是主动打开,那么客户端向服务器发送 SYN 报文,此时客户端处于 SYN-SEND 状态,SYN-SEND 表示发送连接请求后等待匹配的连接请求,服务器被动打开会处于 LISTEN 状态,用于监听 SYN 报文。如果客户端调用了 close 方法或者经过一段时间没有操作,就会重新变为 CLOSED 状态,这一步转换图如下

这里有个疑问,为什么处于 LISTEN 状态下的客户端还会发送 SYN 变为 SYN_SENT 状态呢?

知乎看到了车小胖大佬的回答,这种情况可能出现在 FTP 中,LISTEN -> SYN_SENT 是因为这个连接可能是由于服务器端的应用有数据发送给客户端所触发的,客户端被动接受连接,连接建立后,开始传输文件。也就是说,处于 LISTEN 状态的服务器也是有可能发送 SYN 报文的,只不过这种情况非常少见。

处于 SYN_SEND 状态的服务器会接收 SYN 并发送 SYN 和 ACK 转换成为 SYN_RCVD 状态,同样的,处于 LISTEN 状态的客户端也会接收 SYN 并发送 SYN 和 ACK 转换为 SYN_RCVD 状态。如果处于 SYN_RCVD 状态的客户端收到 RST 就会变为 LISTEN 状态。

这两张图一起看会比较好一些。

这里需要解释下什么是 RST

这里有一种情况是当主机收到 TCP 报文段后,其 IP 和端口号不匹配的情况。假设客户端主机发送一个请求,而服务器主机经过 IP 和端口号的判断后发现不是给这个服务器的,那么服务器就会发出一个 RST 特殊报文段给客户端。

因此,当服务端发送一个 RST 特殊报文段给客户端的时候,它就会告诉客户端没有匹配的套接字连接,请不要再继续发送了

RST:(Reset the connection)用于复位因某种原因引起出现的错误连接,也用来拒绝非法数据和请求。如果接收到 RST 位时候,通常发生了某些错误。

上面没有识别正确的 IP 端口是一种导致 RST 出现的情况,除此之外,RST 还可能由于请求超时、取消一个已存在的连接等出现。

位于 SYN_RCVD 的服务器会接收 ACK 报文,SYN_SEND 的客户端会接收 SYN 和 ACK 报文,并发送 ACK 报文,由此,客户端和服务器之间的连接就建立了。

这里还要注意一点,同时打开的状态我在上面没有刻意表示出来,实际上,在同时打开的情况下,它的状态变化是这样的。

为什么会是这样呢?因为你想,在同时打开的情况下,两端主机都发起 SYN 报文,而主动发起 SYN 的主机会处于 SYN-SEND 状态,发送完成后,会等待接收 SYN 和 ACK , 在双方主机都发送了 SYN + ACK 后,双方都处于 SYN-RECEIVED(SYN-RCVD) 状态,然后等待 SYN + ACK 的报文到达后,双方就会处于 ESTABLISHED 状态,开始传输数据。

好了,到现在为止,我给你叙述了一下 TCP 连接建立过程中的状态转换,现在你可以泡一壶茶喝点水,等着数据传输了。

好了,现在水喝够了,这时候数据也传输完成了,数据传输完成后,这条 TCP 连接就可以断开了。

现在我们把时钟往前拨一下,调整到服务端处于 SYN_RCVD 状态的时刻,因为刚收到了 SYN 包并发送了 SYN + ACK 包,此时服务端很开心,但是这时,服务端应用进程关闭了,然后应用进程发了一个 FIN 包,就会让服务器从 SYN_RCVD -> FIN_WAIT_1 状态。

然后把时钟调到现在,客户端和服务器现在已经传输完数据了 ,此时客户端发送了一条 FIN 报文希望断开连接,此时客户端也会变为 FIN_WAIT_1 状态,对于服务器来说,它接收到了 FIN 报文段并回复了 ACK 报文,就会从 ESTABLISHED -> CLOSE_WAIT 状态。

位于 CLOSE_WAIT 状态的服务端会发送 FIN 报文,然后把自己置于 LAST_ACK 状态。处于 FIN_WAIT_1 的客户端接收 ACK 消息就会变为 FIN_WAIT_2 状态。

这里需要先解释一下 CLOSING 这个状态,FIN_WAIT_1 -> CLOSING 的转换比较特殊

CLOSING 这种状态比较特殊,实际情况中应该是很少见,属于一种比较罕见的例外状态。正常情况下,当你发送FIN 报文后,按理来说是应该先收到(或同时收到)对方的 ACK 报文,再收到对方的 FIN 报文。但是 CLOSING 状态表示你发送 FIN 报文后,并没有收到对方的 ACK 报文,反而却也收到了对方的 FIN 报文。

什么情况下会出现此种情况呢?其实细想一下,也不难得出结论:那就是如果双方在同时关闭一个链接的话,那么就出现了同时发送 FIN 报文的情况,也即会出现 CLOSING 状态,表示双方都正在关闭连接。

FIN_WAIT_2 状态的客户端接收服务端主机发送的 FIN + ACK 消息,并发送 ACK 响应后,会变为 TIME_WAIT 状态。处于 CLOSE_WAIT 的客户端发送 FIN 会处于 LAST_ACK 状态。

这里不少图和博客虽然在图上画的是 FIN + ACK 报文后才会处于 LAST_ACK 状态,但是描述的时候,一般通常只对于 FIN 进行描述。也就是说 CLOSE_WAIT 发送 FIN 才会处于 LAST_ACK 状态。

所以这里 FIN_WAIT_1 -> TIME_WAIT 的状态也就是接收 FIN 和 ACK 并发送 ACK 之后,客户端处于的状态。

然后位于 CLOSINIG 状态的客户端这时候还有 ACK 接收的话,会继续处于 TIME_WAIT 状态,可以看到,TIME_WAIT 状态相当于是客户端在关闭前的最后一个状态,它是一种主动关闭的状态;而 LAST_ACK 是服务端在关闭前的最后一个状态,它是一种被动打开的状态。

上面有几个状态比较特殊,这里我们向西解释下。

TIME_WAIT 状态

通信双方建立 TCP 连接后,主动关闭连接的一方就会进入 TIME_WAIT 状态。TIME_WAIT 状态也称为 2MSL 的等待状态。在这个状态下,TCP 将会等待最大段生存期(Maximum Segment Lifetime, MSL) 时间的两倍。

这里需要解释下 MSL

MSL 是 TCP 段期望的最大生存时间,也就是在网络中存在的最长时间。这个时间是有限制的,因为我们知道 TCP 是依靠 IP 数据段来进行传输的,IP 数据报中有 TTL 和跳数的字段,这两个字段决定了 IP 的生存时间,一般情况下,TCP 的最大生存时间是 2 分钟,不过这个数值是可以修改的,根据不同操作系统可以修改此值。

基于此,我们来探讨 TIME_WAIT 的状态。

当 TCP 执行一个主动关闭并发送最终的 ACK 时,TIME_WAIT 应该以 2 * 最大生存时间存在,这样就能够让 TCP 重新发送最终的 ACK 以避免出现丢失的情况。重新发送最终的 ACK 并不是因为 TCP 重传了 ACK,而是因为通信另一方重传了 FIN,客户端经常回发送 FIN,因为它需要 ACK 的响应才能够关闭连接,如果生存时间超过了 2MSL 的话,客户端就会发送 RST,使服务端出错。

计算机网络的网络层

原文:https://zwmst.com/2704.html

前面我们学习了运输层如何为客户端和服务器输送数据的,提供进程端到端的通信。那么下面我们将学习网络层实际上是怎样实现主机到主机的通信服务的。几乎每个端系统都有网络层这一部分。所以,网络层必然是很复杂的。下面我将花费大量篇幅来介绍一下计算机网络层的知识。

网络层概述

网络层是 OSI 参考模型的第三层,它位于传输层和链路层之间,网络层的主要目的是实现两个端系统之间透明的数据传输。

网络层的作用从表面看上去非常简单,即将分组从一台主机移动到另外一台主机。为了实现这个功能,网络层需要两种功能

  • 转发:因为在互联网中有很多路由器的存在,而路由器是构成互联网的根本,路由器最重要的一个功能就是分组转发,当一个分组到达某路由器的一条输入链路时,该路由器会将分组移动到适当的输出链路。转发是在数据平面中实现的唯一功能。

在网络中存在两种平面的选择

  • 数据平面(data plane):负责转发网络流量,如路由器交换机中的转发表(我们后面会说)。
  • 控制平面(control plane):控制网络的行为,比如网络路径的选择。
  • 路由选择:当分组由发送方流向接收方时,网络层必须选择这些分组的路径。计算这些路径选择的算法被称为 路由选择算法(routing algorithm)

也就是说,转发是指将分组从一个输入链路转移到适当输出链路接口的路由器本地动作。而路由选择是指确定分组从源到目的地所定位的路径的选择。我们后面会经常提到转发和路由选择这两个名词。

那么此处就有一个问题,路由器怎么知道有哪些路径可以选择呢?

每台路由器都有一个关键的概念就是 转发表(forwarding table)。路由器通过检查数据包标头中字段的值,来定位转发表中的项来实现转发。标头中的值即对应着转发表中的值,这个值指出了分组将被转发的路由器输出链路。如下图所示

上图中有一个 1001 分组到达路由器后,首先会在转发表中进行索引,然后由路由选择算法决定分组要走的路径。每台路由器都有两种功能:转发和路由选择。下 面我们就来聊一聊路由器的工作原理。

路由器工作原理

下面是一个路由器体系结构图,路由器主要是由 4 个组件构成的

  • 输入端口:输入端口(input port)有很多功能。线路终端功能数据链路处理功能,这两个功能实现了路由器的单个输入链路相关联的物理层和数据链路层。输入端口查找/转发功能对路由器的交换功能来说至关重要,由路由器的交换结构来决定输出端口,具体来讲应该是查询转发表来确定的。
  • 交换结构:交换结构(Switching fabric)就是将路由器的输入端口连接到它的输出端口。这种交换结构相当于是路由器内部的网络。
  • 输出端口:输出端口(Output ports) 通过交换结构转发分组,并通过物理层和数据链路层的功能传输分组,因此,输出端口作为输入端口执行反向数据链接和物理层功能。
  • 路由选择处理器:路由选择处理器(Routing processor) 在路由器内执行路由协议,维护路由表并执行网络管理功能。

上面只是这几个组件的简单介绍,其实这几个组件的组成并不像描述的那样简单,下面我们就来深入聊一聊这几个组件。

输入端口

上面介绍了输入端口有很多功能,包括线路终端、数据处理、查找转发,其实这些功能在输入端口的内部有相应的模块,输入端口的内部实现如下图所示

每个输入端口中都有一个路由处理器维护的路由表的副本,根据路由处理器进行更新。这个路由表的副本能 够使每个输入端口进行切换,而无需经过路由处理器统一处理。这是一种分散式的切换,这种方式避免了路 由选择器统一处理造成转发瓶颈。

在输入端口处理能力有限的路由器中,输入端口不会进行交换功能,而是由路由处理器统一处理,然后根据 路由表查找并将数据包转发到相应的输出端口。

一般这种路由器不是单独的路由器,而是工作站或者服务器充当的路由,这种路由器内部中,路由处理器其实就是 CPU,而输入端口其实只是网卡

输入端口会根据转发表定位输出端口,然后再会进行分组转发,那么现在就有一个问题,是不是每一个分组都有自己的一条链路呢?如果分组数量非常大,到达亿级的话,也会有亿个输出端口路径吗?

我们的潜意识中显然不是的,来看下面一个例子。

下面是三个输入端口对应了转发表中的三个输出链路的示例

可以看到,对于这个例子来说,路由器转发表中不需要那么多条链路,只需要四条就够,即对应输出链路 0 1 2 3 。也就是说,能够使用 4 个转发表就可以实现亿级链路。

如何实现呢?

使用这种风格的转发表,路由器分组的地址 前缀(prefix) 会与该表中的表项进行匹配。

如果存在一个匹配项,那么就会转发到对应的链路上,可能不好理解,我举个例子来说吧。

比如这时有一个分组是 11000011 10010101 00010000 0001100 到达,因为这个分组与 11000011 10010101 00010000 相匹配,所以路由器会转发到 0 链路接口上。如果一个前缀不匹配上面三个输出链路中的一种,那么路由器将向链路接口 3 进行转发。

路由匹配遵循 最长前缀原则(longest prefix matching rule),最长匹配原则故名思义就是如果有两个匹配项一个长一个短的话,就匹配最长的。

一旦通过查找功能确定了分组的输出端口后,那么该分组就会进入交换结构。在进入交换结构时,如果交换结构正在被使用,就会阻塞新到的分组,等到交换结构调度新的分组。

交换结构

交换结构是路由器的核心功能,通过交换功能把分组从输入端口转发至输出端口,这就是交换结构的主要功能。交换结构有多种形式,主要分为 通过内存交换、通过总线交换、通过互联网络进行交换,下面我们分开来探讨一下。

  • 经过内存交换:最开始的传统计算机就是使用内存交换的,在输入端口和输出端口之间是通过 CPU 进行的。输入端口和输出端口的功能就好像传统操作系统中的 I/O 设备一样。当一个分组到达输入端口时,这个端口会首先以中断 的方式向路由选择器发出信号,将分组从输入端口拷贝到内存中。然后,路由选择处理器从分组首部中提取目标地址,在转发表中找出适当的输出端口进行转发,同时将分组复制到输出端口的缓存中。

这里需要注意一点,如果内存带宽以每秒读取或者写入 B 个数据包,那么总的交换机吞吐量(数据包从输入端口到输出端口的总速率) 必须小于 B/2。

  • 经过总线交换:在这种处理方式中,总线经由输入端口直接将分组传送到输出端口,中间不需要路由选择器的干预。总线的工作流程如下:输入端口给分组分配一个标签,然后分组经由总线发送给所有的输出端口,每个输出端口都会判断标签中的端口和自己的是否匹配,如果匹配的话,那么这个输出端口就会把标签拆掉,这个标签只用于交换机内部跨越总线。如果同时有 多个 分组到达路由器的话,那么只有一个分组能够被处理,其他分组需要再进入交换结构前等待。

  • 经过互联网络交换:克服单一、共享式总线带宽限制的一种方法是使用一个更复杂的互联网络。如下图所示

每条垂直的的总线在交叉点与每条水平的总线交叉,交叉点通过交换结构控制器能够在任何时候开启和闭合。当分组到达输入端口 A 时,如果需要转发到端口 X,交换机控制器会闭合 A 到 X 交叉部分的交叉点,然后端口 A 在总线上进行分组转发。这种网络互联式的交换结构是 非阻塞的(non-blocking)的,也就是说 A -> X 的交叉点闭合不会影响 B -> Y 的链路。如果来自两个不同输入端口的两个分组其目的地为相同的输出端口的话,这种情况下只能有一个分组被交换,另外一个分组必须进行等待。

输出端口处理

如下图所示,输出端口处理取出已经存放在输出端口内存中的分组并将其发送到输出链路上。包括选择和去除排队的分组进行传输,执行所需的链路层和物理层的功能。

在输入端口中有等待进入交换的排队队列,而在输出端口中有等待转发的排队队列,排队的位置和程度取决于流量负载、交换结构的相对频率和线路速率。

随着队列的不断增加,会导致路由器的缓存空间被耗尽,进而使没有内存可以存储溢出的队列,致使分组出现丢包(packet loss),这就是我们说的在网络中丢包或者被路由器丢弃。

何时出现排队

下面我们通过输入端口的排队队列和输出端口的排队队列来介绍一下可能出现的排队情况。

输入队列

如果交换结构的处理速度没有输入队列到达的速度快,在这种情况下,输入端口将会出现排队情况,到达交换结构前的分组会加入输入端口队列中,以等待通过交换结构传送到输出端口。

为了描述清楚输入队列,我们假设以下情况:

  • 使用网络互联的交换方式;
  • 假定所有链路的速度相同;
  • 在链路中一个分组由输入端口交换到输出端口所花的时间相同,从任意一个输入端口传送到给定的输出端口;
  • 分组按照 FCFS 的方式,只要输出端口不同,就可以进行并行传送。但是如果位于任意两个输入端口中的分组是发往同一个目的地的,那么其中的一个分组将被阻塞,而且必须在输入队列中等待,因为交换结构一次只能传输一个到指定端口。

如下图所示

在 A 队列中,输入队列中的两个分组会发送至同一个目的地 X,假设在交换结构正要发送 A 中的分组,在这个时候,C 队列中也有一个分组发送至 X,在这种情况下,C 中发送至 X 的分组将会等待,不仅如此,C 队列中发送至 Y 输出端口的分组也会等待,即使 Y 中没有出现竞争的情况。这种现象叫做 线路前部阻塞(Head-Of-The-Line, HOL)

输出队列

我们下面讨论输出队列中出现等待的情况。假设交换速率要比输入/输出的传输速率快很多,而且有 N 个输入分组的目的地是转发至相同的输出端口。在这种情况下,在向输出链路发送分组的过程中,将会有 N 个新分组到达传输端口。因为输出端口在一个单位时间内只能传输一个分组,那么这 N 个分组将会等待。然而在等待 N 个分组被处理的过程中,同时又有 N 个分组到达,所以 ,分组队列能够在输出端口形成。这种情况下最终会因为分组数量变的足够大,从而耗尽 输出端口的可用内存。

如果没有足够的内存来缓存分组的话,就必须考虑其他的方式,主要有两种:一种是丢失分组,采用 弃尾(drop-tail) 的方法;一种是删除一个或多个已经排队的分组,从而来为新的分组腾出空间。

网络层的策略对 TCP 拥塞控制影响很大的就是路由器的分组丢弃策略。在最简单的情况下,路由器的队列通常都是按照 FCFS 的规则处理到来的分组。由于队列长度总是有限的,因此当队列已经满了的时候,以后再到达的所有分组(如果能够继续排队,这些分组都将排在队列的尾部)将都被丢弃。这就叫做尾部丢弃策略。

通常情况下,在缓冲填满之前将其丢弃是更好的策略。

如上图所示,A B C 每个输入端口都到达了一个分组,而且这个分组都是发往 X 的,同一时间只能处理一个分组,然后这时,又有两个分组分别由 A B 发往 X,所以此时有 4 个分组在 X 中进行等待。

等上一个分组被转发完成后,输出端口就会选择在剩下的分组中根据 分组调度(packet scheduleer) 选择一个分组来进行传输,我们下面就会聊到分组传输。

分组调度

现在我们来讨论一下分组调度次序的问题,即排队的分组如何经输出链路传输的问题。我们生活中有无数排队的例子,但是我们生活中一般的排队算法都是 先来先服务(FCFS),也是先进先出(FIFO)

先进先出

先进先出就映射为数据结构中的队列,只不过它现在是链路调度规则的排队模型。

FIFO 调度规则按照分组到达输出链路队列的相同次序来选择分组,先到达队列的分组将先会被转发。在这种抽象模型中,如果队列已满,那么弃尾的分组将是队列末尾的后面一个。

优先级排队

优先级排队是先进先出排队的改良版本,到达输出链路的分组被分类放入输出队列中的优先权类,如下图所示

通常情况下,每个优先级不同的分组有自己的优先级类,每个优先级类有自己的队列,分组传输会首先从优先级高的队列中进行,在同一类优先级的分组之间的选择通常是以 FIFO 的方式完成。

循环加权公平排队

循环加权公平规则(round robin queuing discipline) 下,分组像使用优先级那样被分类。然而,在类之间却不存在严格的服务优先权。循环调度器在这些类之间循环轮流提供服务。如下图所示

在循环加权公平排队中,类 1 的分组被传输,接着是类 2 的分组,最后是类 3 的分组,这算是一个循环,然后接下来又重新开始,又从 1 -> 2 -> 3 这个顺序进行轮询。每个队列也是一个先入先出的队列。

这是一种所谓的保持工作排队(work-conserving queuing) 的规则,就是说如果轮询的过程中发现有空队列,输出端口不会等待分组,而是继续轮询下面的队列。

IP 协议

路由器对分组进行转发后,就会把数据包传到网络上,数据包最终是要传递到客户端或者服务器上的,那么数据包怎么知道要发往哪里呢?起到关键作用的就是 IP 协议。

IP 主要分为三个部分,分别是 IP 寻址、路由和分包组包。下面我们主要围绕这三点进行阐述。

IP 地址

既然一个数据包要在网络上传输,那么肯定需要知道这个数据包到底发往哪里,也就是说需要一个目标地址信息,IP 地址就是连接网络中的所有主机进行通信的目标地址,因此,在网络上的每个主机都需要有自己的 IP 地址。

在 IP 数据报发送的链路中,有可能链路非常长,比如说由中国发往美国的一个数据报,由于网络抖动等一些意外因素可能会导致数据报丢失,这时我们在这条链路中会放入一些 中转站,一方面能够确保数据报是否丢失,另一方面能够控制数据报的转发,这个中转站就是我们前面聊过的路由器,这个转发过程就是 路由控制

路由控制(Routing) 是指将分组数据发送到最终目标地址的功能,即使网络复杂多变,也能够通过路由控制到达目标地址。因此,一个数据报能否到达目标主机,关键就在于路由器的控制。

这里有一个名词,就是 ,因为在一条链路中可能会布满很多路由器,路由器和路由器之间的数据报传送就是跳,比如你和隔壁老王通信,中间就可能会经过路由器 A-> 路由器 B -> 路由器 C 。

那么一跳的范围有多大呢?

一跳是指从源 MAC 地址到目标 MAC 地址之间传输帧的区间,这里引出一个新的名词,MAC 地址是啥?

MAC 地址指的就是计算机的物理地址(Physical Address),它是用来确认网络设备位置的地址。在 OSI 网络模型中,网络层负责 IP 地址的定位,而数据链路层负责 MAC 地址的定位。MAC 地址用于在网络中唯一标示一个网卡,一台设备若有一或多个网卡,则每个网卡都需要并会有一个唯一的 MAC 地址,也就是说 MAC 地址和网卡是紧密联系在一起的。

路由器的每一跳都需要询问当前中转的路由器,下一跳应该跳到哪里,从而跳转到目标地址。而不是数据报刚开始发送后,网络中所有的通路都会显示出来,这种多次跳转也叫做多跳路由

IP 地址定义

现如今有两个版本的 IP 地址,IPv4 和 IPv6,我们首先探讨一下现如今还在广泛使用的 IPv4 地址,后面再考虑 IPv6 。

IPv4 由 32 位正整数来表示,在计算机内部会转化为二进制来处理,但是二进制不符合人类阅读的习惯,所以我们根据易读性的原则把 32 位的 IP 地址以 8 位为一组,分成四组,每组之间以 . 进行分割,再将每组转换为十进制数。如下图所示

那么上面这个 32 位的 IP 地址就会被转换为十进制的 156.197.1.1。

除此之外,从图中我们还可以得到如下信息

每个这样 8 位位一组的数字,自然是非负数,其取值范围是 [0,255]。

IP 地址的总个数有 2^32 次幂个,这个数值算下来是 4294967296 ,大概能允许 43 亿台设备连接到网络。实际上真的如此吗?

实际上 IP 不会以主机的个数来配置的,而是根据设备上的 网卡(NIC) 进行配置,每一块网卡都会设置一个或者多个 IP 地址,而且通常一台路由器会有至少两块网卡,所以可以设置两个以上的 IP 地址,所以主机的数量远远达不到 43 亿。

IP 地址构造和分类

IP 地址由 网络标识主机标识 两部分组成,网络标识代表着网络地址,主机标识代表着主机地址。网络标识在数据链路的每个段配置不同的值。网络标识必须保证相互连接的每个段的地址都不重复。而相同段内相连的主机必须有相同的网络地址。IP 地址的 主机标识 则不允许在同一网段内重复出现。

举个例子来说:比如说我在石家庄(好像不用比如昂),我所在的小区的某一栋楼就相当于是网络标识,某一栋楼的第几户就相当于是我的主机标识,当然如果你有整栋楼的话,那就当我没说。你可以通过xx省xx市xx区xx路xx小区xx栋来定位我的网络标识,这一栋的第几户就相当于是我的网络标识。

IP 地址分为四类,分别是 A类、B类、C类、D类、E类,它会根据 IP 地址中的第 1 位到第 4 位的比特对网络标识和主机标识进行分类。

  • A 类:(1.0.0.0 – 126.0.0.0)(默认子网掩码:255.0.0.0 或 0xFF000000)第一个字节为网络号,后三个字节为主机号。该类 IP 地址的最前面为 0 ,所以地址的网络号取值于 1~126 之间。一般用于大型网络。

  • B 类:(128.0.0.0 – 191.255.0.0)(默认子网掩码:255.255.0.0 或 0xFFFF0000)前两个字节为网络号,后两个字节为主机号。该类 IP 地址的最前面为 10 ,所以地址的网络号取值于 128~191 之间。一般用于中等规模网络。

  • C 类:(192.0.0.0 – 223.255.255.0)(子网掩码:255.255.255.0 或 0xFFFFFF00)前三个字节为网络号,最后一个字节为主机号。该类 IP 地址的最前面为 110 ,所以地址的网络号取值于 192~223 之间。一般用于小型网络。

  • D 类:是多播地址。该类 IP 地址的最前面为 1110 ,所以地址的网络号取值于 224~239 之间。一般用于多路广播用户。

  • E 类:是保留地址。该类 IP 地址的最前面为 1111 ,所以地址的网络号取值于 240~255 之间。

为了方便理解,我画了一张 IP 地址分类图,如下所示

根据不同的 IP 范围,有下面不同的地总空间分类

子网掩码

子网掩码(subnet mask) 又叫做网络掩码,它是一种用来指明一个 IP 地址的哪些位标识的是主机所在的网络。子网掩码是一个 32位 地址,用于屏蔽 IP 地址的一部分以区别网络标识和主机标识。

一个 IP 地址只要确定了其分类,也就确定了它的网络标识和主机标识,由此,各个分类所表示的网络标识范围如下

1 表示 IP 网络地址的比特范围,0 表示 IP 主机地址的范围。将他们用十进制表示,那么这三类的表示如下

保留地址

在IPv4 的几类地址中,有几个保留的地址空间不能在互联网上使用。这些地址用于特殊目的,不能在局域网外部路由。

IP 协议版本

目前,全球 Internet 中共存有两个IP版本:IP 版本 4(IPv4)IP 版本6(IPv6)。 IP 地址由二进制值组成,可驱动 Internet 上所有数据的路由。 IPv4 地址的长度为 32 位,而 IPv6 地址的长度为 128 位。

Internet IP 资源由 Internet 分配号码机构(IANA)分配给区域 Internet 注册表(RIR),例如 APNIC,该机构负责根 DNS ,IP 寻址和其他 Internet 协议资源。

下面我们就一起认识一下 IP 协议中非常重要的两个版本 IPv4 和 IPv6。

IPv4

IPv4 的全称是 Internet Protocol version 4,是 Internet 协议的第四版。IPv4 是一种无连接的协议,这个协议会尽最大努力交付数据包,也就是说它不能保证任何数据包能到达目的地,也不能保证所有的数据包都会按照正确的顺序到达目标主机,这些都是由上层比如传输控制协议控制的。也就是说,单从 IP 看来,这是一个不可靠的协议。

前面我们讲过网络层分组被称为 数据报,所以我们接下来的叙述也会围绕着数据报展开。

IPv4 的数据报格式如下

IPv4 数据报中的关键字及其解释

  • 版本字段(Version)占用 4 bit,通信双方使用的版本必须一致,对于 IPv4 版本来说,字段值是 4。
  • 首部长度(Internet Header Length) 占用 4 bit,首部长度说明首部有多少 32 位(4 字节)。由于 IPv4 首部可能包含不确定的选项,因此这个字段被用来确定数据的偏移量。大多数 IP 不包含这个选项,所以一般首部长度设置为 5, 数据报为 20 字节 。
  • 服务类型(Differential Services Codepoint,DSCP) 占用 6 bit,以便使用不同的 IP 数据报,比如一些低时延、高吞吐量和可靠性的数据报。服务类型如下表所示

  • 拥塞通告(Explicit Congestion Notification,ECN) 占用 2 bit,它允许在不丢弃报文的同时通知对方网络拥塞的发生。ECN 是一种可选的功能,仅当两端都支持并希望使用,且底层网络支持时才被使用。 最开始 DSCP 和 ECN 统称为 TOS,也就是区分服务,但是后来被细化为了 DSCP 和 ECN。

  • 数据报长度(Total Length) 占用 16 bit,这 16 位是包括在数据在内的总长度,理论上数据报的总长度为 2 的 16 次幂 – 1,最大长度是 65535 字节,但是实际上数据报很少有超过 1500 字节的。IP 规定所有主机都必须支持最小 576 字节的报文,但大多数现代主机支持更大的报文。当下层的数据链路协议的最大传输单元(MTU)字段的值小于 IP 报文长度时,报文就必须被分片。

  • 标识符(Identification) 占用 16 bit,这个字段用来标识所有的分片,因为分片不一定会按序到达,所以到达目标主机的所有分片会进行重组,每产生一个数据报,计数器加1,并赋值给此字段。

  • 标志(Flags) 占用 3 bit,标志用于控制和识别分片,这 3 位分别是

    • 0 位:保留,必须为0;
    • 1 位:禁止分片(Don’t Fragment,DF),当 DF = 0 时才允许分片;
    • 2 位:更多分片(More Fragment,MF),MF = 1 代表后面还有分片,MF = 0 代表已经是最后一个分片。

    如果 DF 标志被设置为 1 ,但是路由要求必须进行分片,那么这条数据报回丢弃

  • 分片偏移(Fragment Offset) 占用 13 位,它指明了每个分片相对于原始报文开头的偏移量,以 8 字节作单位。

  • 存活时间(Time To Live,TTL) 占用 8 位,存活时间避免报文在互联网中迷失,比如陷入路由环路。存活时间以秒为单位,但小于一秒的时间均向上取整到一秒。在现实中,这实际上成了一个跳数计数器:报文经过的每个路由器都将此字段减 1,当此字段等于 0 时,报文不再向下一跳传送并被丢弃,这个字段最大值是 255。

  • 协议(Protocol) 占用 8 位,这个字段定义了报文数据区使用的协议。协议内容可以在 https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml 官网上获取。

  • 首部校验和(Header Checksum) 占用 16 位,首部校验和会对字段进行纠错检查,在每一跳中,路由器都要重新计算出的首部检验和并与此字段进行比对,如果不一致,此报文将会被丢弃。

  • 源地址(Source address) 占用 32 位,它是 IPv4 地址的构成条件,源地址指的是数据报的发送方

  • 目的地址(Destination address)占用 32 位,它是 IPv4 地址的构成条件,目标地址指的是数据报的接收方

  • 选项(Options) 是附加字段,选项字段占用 1 – 40 个字节不等,一般会跟在目的地址之后。如果首部长度 > 5,就应该考虑选项字段。

  • 数据 不是首部的一部分,因此并不被包含在首部检验和中。

在 IP 发送的过程中,每个数据报的大小是不同的,每个链路层协议能承载的网络层分组也不一样,有的协议能够承载大数据报,有的却只能承载很小的数据报,不同的链路层能够承载的数据报大小如下。

IPv4 分片

一个链路层帧能承载的最大数据量叫做最大传输单元(Maximum Transmission Unit, MTU),每个 IP 数据报封装在链路层帧中从一台路由器传到下一台路由器。因为每个链路层所支持的最大 MTU 不一样,当数据报的大小超过 MTU 后,会在链路层进行分片,每个数据报会在链路层单独封装,每个较小的片都被称为 片(fragement)

每个片在到达目的地后会进行重组,准确的来说是在运输层之前会进行重组,TCP 和 UDP 都会希望发送完整的、未分片的报文,出于性能的原因,分片重组不会在路由器中进行,而是会在目标主机中进行重组。

当目标主机收到从发送端发送过来的数据报后,它需要确定这些数据报中的分片是否是由源数据报分片传递过来的,如果是的话,还需要确定何时收到了分片中的最后一片,并且这些片会如何拼接一起成为数据报。

针对这些潜在的问题,IPv4 设计者将 标识、标志和片偏移放在 IP 数据报首部中。当生成一个数据报时,发送主机会为该数据报设置源和目的地址的同时贴上标识号。发送主机通常将它发送的每个数据报的标识 + 1。当某路由器需要对一个数据报分片时,形成的每个数据报具有初始数据报的源地址、目标地址和标识号。当目的地从同一发送主机收到一系列数据报时,它能够检查数据报的标识号以确定哪些数据是由源数据报发送过来的。由于 IP 是一种不可靠的服务,分片可能会在网路中丢失,鉴于这种情况,通常会把分片的最后一个比特设置为 0 ,其他分片设置为 1,同时使用偏移字段指定分片应该在数据报的哪个位置。

IPv4 寻址

IPv4 支持三种不同类型的寻址模式,分别是

  • 单播寻址模式:在这种模式下,数据只发送到一个目的地的主机。

  • 广播寻址模式:在此模式下,数据包将被寻址到网段中的所有主机。这里客户端发送一个数据包,由所有服务器接收:

  • 组播寻址模式:此模式是前两种模式的混合,即发送的数据包既不指向单个主机也不指定段上的所有主机

IPv6

随着端系统接入的越来越多,IPv4 已经无法满足分配了,所以,IPv6 应运而生,IPv6 就是为了解决 IPv4 的地址耗尽问题而被标准化的网际协议。IPv4 的地址长度为 4 个 8 字节,即 32 比特, 而 IPv6 的地址长度是原来的四倍,也就是 128 比特,一般写成 8 个 16 位字节。

从 IPv4 切换到 IPv6 及其耗时,需要将网络中所有的主机和路由器的 IP 地址进行设置,在互联网不断普及的今天,替换所有的 IP 是一个工作量及其庞大的任务。我们后面会说。

我们先来看一下 IPv6 的地址是怎样的

  • 版本与 IPv4 一样,版本号由 4 bit 构成,IPv6 版本号的值为 6。
  • 流量类型(Traffic Class) 占用 8 bit,它就相当于 IPv4 中的服务类型(Type Of Service)。
  • 流标签(Flow Label) 占用 20 bit,这 20 比特用于标识一条数据报的流,能够对一条流中的某些数据报给出优先权,或者它能够用来对来自某些应用的数据报给出更高的优先权,只有流标签、源地址和目标地址一致时,才会被认为是一个流。
  • 有效载荷长度(Payload Length) 占用 16 bit,这 16 比特值作为一个无符号整数,它给出了在 IPv6 数据报中跟在鼎昌 40 字节数据报首部后面的字节数量。
  • 下一个首部(Next Header) 占用 8 bit,它用于标识数据报中的内容需要交付给哪个协议,是 TCP 协议还是 UDP 协议。
  • 跳限制(Hop Limit) 占用 8 bit,这个字段与 IPv4 的 TTL 意思相同。数据每经过一次路由就会减 1,减到 0 则会丢弃数据。
  • 源地址(Source Address) 占用 128 bit (8 个 16 位 ),表示发送端的 IP 地址。
  • 目标地址(Destination Address) 占用 128 bit (8 个 16 位 ),表示接收端 IP 地址。

可以看到,相较于 IPv4 ,IPv6 取消了下面几个字段

  • 标识符、标志和比特偏移:IPv6 不允许在中间路由器上进行分片和重新组装。这种操作只能在端系统上进行,IPv6 将这个功能放在端系统中,加快了网络中的转发速度。
  • 首部校验和:因为在运输层和数据链路执行了报文段完整性校验工作,IP 设计者大概觉得在网络层中有首部校验和比较多余,所以去掉了。IP 更多专注的是快速处理分组数据
  • 选项字段:选项字段不再是标准 IP 首部的一部分了,但是它并没有消失,而是可能出现在 IPv6 的扩展首部,也就是下一个首部中。

IPv6 扩展首部

IPv6 首部长度固定,无法将选项字段加入其中,取而代之的是 IPv6 使用了扩展首部

扩展首部通常介于 IPv6 首部与 TCP/UDP 首部之间,在 IPv4 中可选长度固定为 40 字节,在 IPv6 中没有这样的限制。IPv6 的扩展首部可以是任意长度。扩展首部中还可以包含扩展首部协议和下一个扩展字段。

IPv6 首部中没有标识和标志字段,对 IP 进行分片时,需要使用到扩展首部

具体的扩展首部表如下所示

下面我们来看一下 IPv6 都有哪些特点

IPv6 特点

IPv6 的特点在 IPv4 中得以实现,但是即便实现了 IPv4 的操作系统,也未必实现了 IPv4 的所有功能。而 IPv6 却将这些功能大众化了,也就表明这些功能在 IPv6 已经进行了实现,这些功能主要有

  • 地址空间变得更大:这是 IPv6 最主要的一个特点,即支持更大的地址空间。

  • 精简报文结构: IPv6 要比 IPv4 精简很多,IPv4 的报文长度不固定,而且有一个不断变化的选项字段;IPv6 报文段固定,并且将选项字段,分片的字段移到了 IPv6 扩展头中,这就极大的精简了 IPv6 的报文结构。

  • 实现了自动配置:IPv6 支持其主机设备的状态和无状态自动配置模式。这样,没有 DHCP 服务器不会停止跨段通信。

  • 层次化的网络结构: IPv6 不再像 IPv4 一样按照 A、B、C等分类来划分地址,而是通过 IANA -> RIR -> ISP 这样的顺序来分配的。IANA 是国际互联网号码分配机构,RIR 是区域互联网注册管理机构,ISP 是一些运营商(例如电信、移动、联通)。

  • IPSec:IPv6 的扩展报头中有一个认证报头、封装安全净载报头,这两个报头是 IPsec 定义的。通过这两个报头网络层自己就可以实现端到端的安全,而无需像 IPv4 协议一样需要其他协议的帮助。

  • 支持任播:IPv6 引入了一种新的寻址方式,称为任播寻址。

IPv6 地址

我们知道,IPv6 地址长度为 128 位,他所能表示的范围是 2 ^ 128 次幂,这个数字非常庞大,几乎涵盖了你能想到的所有主机和路由器,那么 IPv6 该如何表示呢?

一般我们将 128 比特的 IP 地址以每 16 比特为一组,并用 : 号进行分隔,如果出现连续的 0 时还可以将 0 省略,并用 :: 两个冒号隔开,记住,一个 IP 地址只允许出现一次两个连续的冒号。

下面是一些 IPv6 地址的示例

  • 二进制数表示

  • 用十六进制数表示

  • 出现两个冒号的情况

如上图所示,A120 和 4CD 中间的 0 被 :: 所取代了。

如何从 IPv4 迁移到 IPv6

我们上面聊了聊 IPv4 和 IPv6 的报文格式、报文含义是什么、以及 IPv4 和 IPv6 的特征分别是什么,看完上面的内容,你已经知道了 IPv4 现在马上就变的不够用了,而且随着 IPv6 的不断发展和引用,虽然新型的 IPv6 可以做到向后兼容,即 IPv6 可以收发 IPv4 的数据报,但是已经部署的具有 IPv4 能力的系统却不能够处理 IPv6 数据报。所以 IPv4 噬需迁移到 IPv6,迁移并不意味着将 IPv4 替换为 IPv6。这仅意味着同时启用 IPv6 和 IPv4。

那么现在就有一个问题了,IPv4 如何迁移到 IPv6 呢?这就是我们接下来讨论的重点。

标志

最简单的方式就是设置一个标志日,指定某个时间点和日期,此时全球的因特网机器都会在这时关机从 IPv4 迁移到 IPv6 。上一次重大的技术迁移是在 35 年前,但是很显然,不用我过多解释,这种情况肯定是 不行的。影响不可估量不说,如何保证全球人类都能知道如何设置自己的 IPv6 地址?一个设计数十亿台机器的标志日现在是想都不敢想的。

隧道技术

现在已经在实践中使用的从 IPv4 迁移到 IPv6 的方法是 隧道技术(tunneling)

什么是隧道技术呢?

隧道技术是一种使用互联网络的基础设施在网络之间的传输数据的方式,使用隧道传递的数据可以是不同协议的数据帧或包。使用隧道技术所遵从的协议叫做隧道协议(tunneling protocol)。隧道协议会将这些协议的数据帧或包封装在新的包头中发送。新的包头提供了路由信息,从而使封装的负载数据能够通过互联网络进行传递。

使用隧道技术一般都会建一个隧道,建隧道的依据如下:

比如两个 IPv6 节点(下方 B、E)要使用 IPv6 数据报进行交互,但是它们是经由两个 IPv4 的路由器进行互联的。那么我们就需要将 IPv6 节点和 IPv4 路由器组成一个隧道,如下图所示

借助于隧道,在隧道发送端的 IPv6 节点可将整个 IPv6 数据报放到一个 IPv4 数据报的数据(有效载荷) 字段中,于是,IPv4 数据报的地址被设置为指向隧道接收端的 IPv6 的节点,比如上面的 E 节点。然后再发送给隧道中的第一个节点 C,如下所示

隧道中间的 IPv4 提供路由,路由器不知道这个 IPv4 内部包含一个指向 IPv6 的地址。隧道接收端的 IPv6 节点收到 IPv4 数据报,会确定这个 IPv4 数据报含有一个 IPv6 数据报,通过观察数据报长度和数据得知。然后取出 IPv6 数据报,再为 IPv6 提供路由,就好像两个节点直接相连传输数据报一样。

总结

这篇文章是计算机网络系列的连载文章,这篇我们主要探讨了网络层的相关知识、路由器的内部构造、路由器如何实现转发的,IP 协议相关内容:包括 IP 地址、IPv4 和 IPv6 的相关内容,最后我们探讨了如何使 IPv4 迁移到 IPv6 。

计算机网络TCP|IP基础

原文:https://zwmst.com/2707.html

要说我们接触计算机网络最多的协议,那势必离不开 TCP/IP 协议了,TCP/IP 协议同时也是互联网中最为著名的协议,下面我们就来一起聊一下 TCP/IP 协议。

TCP/IP 的历史背景

最初还没有 TCP/IP 协议的时候,也就是在 20 世纪 60 年代,许多国家和地区认识到通信技术的重要性。美国国防部希望能够研究一种即使通信线路被破坏也能够通过其他路线进行通信的技术。为了实现这种技术,出现了分组网络。

即使在两个节点通信的过程中,几个节点遭到破坏,却依然能够通过改变线路等方式达使两个节点之间进行通信。

这种分组网络促进了 ARPANET(Advanced Research Projects Agency Network) 的诞生。ARPANET 是第一个具有分布式控制的广域包分t组交换网络,也是最早实现 TCP/IP 协议的前身。

ARPANET 其实是由 美国国防部高级研究计划局 计划建立。

所以,计算机网络的出现在最一开始是因为军事研究目的。

20 世纪 90 年代,IOS 开展了 OSI 这一国际标准化的进程,然而却没有取得实质性的进展,但是却使 TCP/IP 协议得到了广泛使用。

这种致使 TCP/IP 协议快速发展的原因可能是由于 TCP/IP 的标准化。也就是说 TCP/IP 协议中会涉及到 OSI 所没有的标准,而这种标准将是我们接下来主要探讨的内容。

这里我们先来认识一下 TCP/IP 协议,TCP/IP 协议说的不仅仅只是 TCP 和 IP 这两种协议,实际上,TCP/IP 指的是协议簇,协议簇是啥呢?简单来说就是一系列协议的综合,如果下次再问你 TCP/IP 协议有哪些的话,可以把下面这张图甩给他

以上的协议汇总起来,就是 TCP/IP 协议簇。

TCP/IP 标准

TCP/IP 相较于其他协议的标准,更注重两点:开放性实用性,即标准化能否被实际使用。

开放性说的是 TCP/IP 是由 IETF 讨论制定的,而 IETF 本身就是一个允许任何人加入进行讨论的组织。

实用性说的是就拿框架来说,如果只浮于理论,而没有落地的实践,那么永远成为不了主流。

TCP/IP 的标准协议就是我们所熟知的 RFC 文档,当然你可以在网络上看到。RFC 不仅规范了协议标准,还包含了协议的实现和使用信息。

关于更多 RFC 协议,你可以看一下官方文档 https://www.rfc-editor.org/rfc-index.html

这里我们不再详细展开介绍了,我们这篇文章的重点要放在对 TCP/IP 的研究上。

TCP/IP 协议簇

下面我们就开始聊一聊 TCP/IP 协议簇。

TCP/IP 协议是我们程序员接触最多的协议,OSI 模型共有七层,从下到上分别是物理层、数据链路层、网络层、运输层、会话层、表示层和应用层。但是这显然是有些复杂的,所以在 TCP/IP 协议中,它们被简化为了四个层次

下面我们从通信链路层开始介绍一下这些层以及与层之间的协议。

通信链路层

如果非要细分的话,通信链路层也可以分为 物理层数据链路层

物理层

物理层是 TCP/IP 的最底层是负责传输的硬件,这种硬件就相当于是以太网或电话线路等物理层的设备。

数据链路层

另外一层是数据链路层,数据链路层位于物理层和网络层中间,数据链路层定义了在单个链路上如何传输数据。

网络层

网络层主要使用 IP协议,IP 协议基于 IP 地址转发分包数据。

IP 协议的主要作用就是将分组数据包发送到目标主机

TCP/IP 分层中的互联网层与传输层的功能通常由操作系统提供。

IP 还隐含着数据链路层的功能,通过 IP 协议,相互通信的主机之间不论经过怎样的底层数据链路,都能够实现相互通信。

虽然 IP 也是一种分组交换协议,但是 IP 却不具备重发机制。即使数据没有到达另一端也不会进行重发,所以 IP 属于非可靠性协议。

网络层还有一种协议就是 ICMP,因为 IP 在数据包的发送过程中可能会出现异常,当 IP 数据包因为异常而无法到达目标地址时,需要给发送端发送一个异常通知,ICMP 的主要功能就在于此了。鉴于此情况,ICMP 也可以被用来诊断网络情况。

传输层

我们上面刚介绍完 TCP/IP 协议最重要的 IP 协议后,下面我们来介绍一下传输层协议,TCP 协议时传输层协议的一种。

传输层就好像高速公路一样,连接两个城市的道路。下面是互联网的逻辑通道,你可以把它想象成为高速公路。

传输层最主要的功能就是让应用层的应用程序之间完成通信和数据交换。在计算机内部运行着很多应用程序,每个应用程序都对应一个端口号,我们一般使用端口号来区分这些应用程序。

传输层的协议主要分为面向有连接的协议 TCP 和面向无连接的协议 UDP

**TCP

TCP 是一种可靠的协议,它能够保证数据包的可靠性交付,TCP 能够正确处理传输过程中的丢包、传输顺序错乱等异常情况。此外,TCP 还提供拥塞控制用于缓解网络拥堵。

**UDP

UDP 是一种不可靠的协议,它无法保证数据的可靠交付,相比 TCP ,UDP 不会检查数据包是否到达、网络是否阻塞等情况,但是 UDP 的效率比较高。

UDP 常用于分组数据较少或者广播、多播等视频通信和多媒体领域。

应用层

在 TCP/IP 协议簇中,将 OSI 标准模型中的会话层、表示层都归为了应用层。应用层的架构大多属于客户端/服务端模型,提供服务的程序叫做服务端、接受服务的程序叫做客户端。在这种架构中,服务端通常会提前部署到服务器上,等待客户端的连接,从而提供服务。

数据包的发送历程

下面我们来介绍一下一个数据包是如何经过应用层、运输层、网络层和通信链路层进行传输的。

数据包结构

我们首先先来认识一下数据包的结构,这里 cxuan 只是给你简单介绍一下,后面的文章会更加详细的介绍。

在上面的每个分层中,都会对所发送的数据增加一个 首部,这个首部中包含了该层必要的信息。每一层都会对数据进行处理并在数据包中附上这一层的必要信息。下面我们就来聊一聊数据包的发送过程。

数据包发送历程

假设主机 A 和主机 B 进行通信,主机 A 想要向主机 B 发送一个数据包,都会经历哪些奇特的操作?

**应用层的处理

主机 A 也就是用户点击了某个应用或者打开了一个聊天窗口输入了cxuan,然后点击了发送,那么这个 cxuan 就作为一个数据包遨游在了网络中,等下还没完呢,应用层还需要对这个数据包进行处理,包括字符编码、格式化等等,这一层其实是 OSI 中表现层做的工作,只不过在 TCP/IP 协议中都归为了应用层。

数据包在发送的那一刻建立 TCP 连接,这个连接相当于通道,在这之后其他数据包也会使用通道传输数据。

**传输层的处理

为了描述信息能准确的到达另一方,我们使用 TCP 协议来进行描述。TCP 会根据应用的指示,负责建立连接、发送数据和断开连接。

TCP 会在应用数据层的前端附加一个 TCP 首部字段,TCP 首部包含了源端口号目的端口号,这两个端口号用于表明数据包是从哪里发出的,需要发送到哪个应用程序上;TCP 首部还包含序号,用以表示该包中数据是发送端整个数据中第几个字节的序列号;TCP 首部还包含 校验和,用于判断数据是否损坏,随后将 TCP 头部附加在数据包的首部发送给 IP。

**网络层的处理

网络层主要负责处理数据包的是 IP 协议,IP 协议将 TCP 传过来的 TCP 首部和数据结合当作自己的数据,并在 TCP 首部的前端加上自己的 IP 首部。因此,IP 数据包后面会紧跟着 TCP 数据包,后面才是数据本身。IP 首部包含目的和源地址,紧随在 IP 首部的还有用来判断后面是 TCP 还是 UDP 的信息。

IP 包生成后,会由路由控制表判断应该发送至哪个主机,IP 修饰后的数据包继续向下发送给路由器或者网络接口的驱动程序,从而实现真正的数据传输。

如果不知道目标主机的 IP 地址,可以利用 ARP(Address Resolution Protocol) 地址解析协议进行查找。

**通信链路层的处理

经由 IP 传过来的数据包,以太网会给数据附上以太网首部并进行发送处理。以太网首部包含接收端的 MAC 地址、发送端的 MAC 地址以及标志以太网类型的以太网数据协议等。

下面是完整的处理过程和解析过程。

如上图所示,左侧是数据的发送处理过程,应用层的数据经过层层处理后会变为可以发送的数据包,经过物理介质发送至指定主机中。

数据包的接收流程是发送流程的逆序过程,数据包的解析同样也会经过下面这几步。

**通信链路的解析

目标主机收到数据包后,首先会从以太网的首部找到 MAC 地址判断是否是发给自己的数据包,如果不是发给自己的数据包则会丢弃该数据包。

如果收到的数据包是发送给自己的,就会查找以太网类型判断是哪种协议,如果是 IP 协议就会扔给 IP 协议进行处理,如果是 ARP 协议就会扔给 ARP 协议进行处理。如果协议类型是一种无法识别的协议,就会将该数据包直接丢弃。

**网络层的解析

经过以太网处理后的数据包扔给网络层进行处理,我们假设协议类型是 IP 协议,那么,在 IP 收到数据包后就会解析 IP 首部,判断 IP 首部中的 IP 地址是否和自己的 IP 地址匹配,如果匹配则接收数据并判断上一层协议是 TCP 还是 UDP;如果不匹配则直接丢弃。

注意:在路由转发的过程中,有的时候 IP 地址并不是自己的,这个时候需要借助路由表协助处理。

**传输层的处理

在传输层中,我们默认使用 TCP 协议,在 TCP 处理过程中,首先会计算一下 校验和,判断数据是否被损坏。然后检查是否按照序号接收数据,最后检查端口号,确定具体是哪个应用程序。

数据被完整的识别后,会传递给由端口号识别的应用程序进行处理。

**应用程序的处理

接收端指定的应用程序会处理发送方传递过来的数据,通过解码等操作识别出数据的内容,然后把对应的数据存储在磁盘上,返回一个保存成功的消息给发送方,如果保存失败,则返回错误消息。

上面是一个完整的数据包收发过程,在上面的数据收发过程中,涉及到不同层之间的地址、端口号、协议类型等,那么我们现在就来剖析一下。

数据包经过每层后,该层协议都会在数据包附上包首部,一个完整的包首部图如下所示

在数据包的发送过程中,各层以此对数据包添加了首部信息,每个首部都包含发送端和接收端地址以及上一层的协议类型。以太网会使用 MAC 地址、IP 会使用 IP 地址、TCP/UDP 则会用端口号作为识别两端主机的地址。

此外,每个分层中的包首部还包含一个识别位,它是用来标识上一层协议的种类信息。

总结

这一篇文章 cxuan 还是在和你聊一些基础知识,这些基础知识是为下面文章提前预热准备的,下一篇文章我们会聊到数据链路层的相关知识,敬请期待。

如果这篇文章还不错的话,希望各位小伙伴们点在、留言、在看、分享,cxuan 谢谢大家。

深入理解计算机系统

原文:https://zwmst.com/2709.html

什么是计算机系统

计算机系统(A computer system) 是由硬件和软件组成的,它们协同工作运行程序。不同的系统可能会有不同实现,但是核心概念是一样的,通用的。

不同的系统有 Microsoft Windows、Apple Mac OS X、Linux 等。

所有的计算机系统都有相似的软件和硬件组成,它们执行相似的功能。

你想要什么

首先,问你一个问题,你想成为哪种程序员?

这是我最近搜索到的一个很好的开源项目,它的路径是 https://github.com/keithnull/TeachYourselfCS-CN/blob/master/TeachYourselfCS-CN.md

也就是

我也把它里面涉及的中文/英文书籍都下载下来了,公众号回复 计算机基础,即可领取。(图中是冯·诺伊曼)

我一直想成为第一种工程师,即使我永远成为不了,我也要越来越靠近它。

回到正题

没错,我就想成为一种电源程序员

一段简单的程序

这次真的言归正传了,下面是一道很简单的 C 程序(不要管我的名字是 Java建设者还是什么,Java建设者就不能学习 C 了吗?虽然饭碗是 Java,但是 C 才是爸爸啊。)

#include <stdio.h>

int main(){
  pritnf("hello, world\n");
  return 0;
}

这是用 C 语言输出的一个 Hello,world 程序,尽管它是一个非常简单的程序,但系统的每个部分都必须协同工作才能运行。

这段程序的生命周期就是程序员创建程序、在系统中运行这段程序、打印出一个简单的消息然后终止。

程序员首先在文本中创建这段代码,这个文本又被称为源文件或者源程序,然后保存为 hello.c 文件,源程序实际上就是一个由 0 和 1 组成的位(又称为 比特,即 bit)。8 个 bit 成为一组,称做 字节。每个字节又表示着一个文本字符,这些文本字符通常是由 ASCII 码组成的,下面是 hello.c 程序的 ASCII 码

hello.c 程序以字节顺序存储在文件中,每个字节都对应一个整数值,也就是 8 位表示一个整数。比如第一个字符是 35,那这个 35 是从哪来的呢?这其实是有个 ASCII 码的对照表(因为 ASCII 非常多,可以去 http://ascii.911cha.com/?year=%23 官网查询,这里只选取几个作为参考哦)

每行都以不可见的 \n 来结尾,它的 ASCII 码值是 10。

注意;只由 ASCII 字符组成的诸如 hello.c 之类的文件称为文本文件。 所有其他文件称为二进制文件。

hello.c 的表示方法说明了一个基本思想:系统中所有的信息 — 包括磁盘文件、内存中的程序、内存中存放的数据以及网络上传输的数据,都是由一串比特表示的。区分不同数据对象的唯一方法是我们读取对象时的上下文,比如,在不同的上下文中,一个同样的字节序列可能表示一个整数、浮点数、字符串或者机器指令。

为什么是 C

这里插播一则新闻,为什么我们要学 C 语言?学 Java 用不用懂 C 语言?这里需要聊聊 C 语言的发家史了

C 语言起源于贝尔实验室。美国国家标准学会 ANSI 在 1981 年颁布了 ANSI C 的标准,后来 C 就被标准化了,这些标准定义了 C 语言和一系列函数库,即所谓的 C 语言标准库,那么 C 语言有什么特点呢?

  • C 语言与 Unix 操作系统密切关联。C 从一开始就被开发为 UNIX 系统的编程语言,大部分 UNIX 内核(操作系统和核心部分)和工具,动态库都是使用 C 编写的。UNIX 成为 1970 – 1980 年代最火的操作系统,而 C 成为最火的编程语言
  • C 是一种非常小巧,简单的语言。并且 C 语言的简单使他移植性比较强。
  • C 语言是为实践目的设计的。

我们上面提到了 C 语言的各种优势,但是 C 语言也并非所有程序员都能熟练掌握并运用的,C 语言的指针经常让很多程序员头疼,C 语言还缺乏对抽象的良好支持,例如类、对象,但是 C++ 和 Java 都解决了这些问题。

程序被其他程序翻译成不同的形式

C 语言程序成为高级语言的原因是它能够读取并理解人们的思想。然而,为了能够在系统中运行 hello.c 程序,则各个 C 语句必须由其他程序转换为一系列低级机器语言指令。这些指令被打包作为可执行对象程序,存储在二进制磁盘文件中。目标程序也称为可执行目标文件。

在 UNIX 系统中,从源文件到对象文件的转换是由编译器执行完成的。

gcc -o hello hello.c

gcc 编译器驱动从源文件读取 hello.c ,并把它翻译成一个可执行文件 hello。这个翻译过程可用如下图来表示

这就是一个完整的 hello world 程序执行过程,会涉及几个核心组件:预处理器、编译器、汇编器、连接器,下面我们逐个击破。

  • 预处理阶段(Preprocessing phase),预处理器会根据开始的 # 字符,修改源 C 程序。#include 命令就会告诉预处理器去读系统头文件 stdio.h 中的内容,并把它插入到程序作为文本。然后就得到了另外一个 C 程序hello.i,这个程序通常是以 .i为结尾。
  • 然后是 编译阶段(Compilation phase),编译器会把文本文件 hello.i 翻译成文本hello.s,它包括一段汇编语言程序(assembly-language program)。这个函数包含 main 函数的定义,如下
main:
    subq        $8, %rsp
    movl        $.LCO, %edi
    call        puts
    movl        &0, %eax
    addq        $8, %rsp
    ret

上面定义中的 2 – 7 描述了一种低级语言指令。汇编语言是非常有用的,因为它能够针对不同高级语言来提供自己的一套标准输出语言。

  • 编译完成之后是汇编阶段(Assembly phase),这一步,汇编器 as会把 hello.s 翻译成机器指令,把这些指令打包成可重定位的二进制程序(relocatable object program)放在 hello.c 文件中。它包含的 17 个字节是函数 main 的指令编码,如果我们在文本编辑器中打开 hello.c 将会看到一堆乱码。
  • 最后一个是链接阶段(Linking phase),我们的 hello 程序会调用 printf 函数,它是 C 编译器提供的 C 标准库中的一部分。printf 函数位于一个叫做 printf.o文件中,它是一个单独的预编译好的目标文件,而这个文件必须要和我们的 hello.o 进行链接,连接器(ld) 会处理这个合并操作。结果是,hello 文件,它是一个可执行的目标文件(或称为可执行文件),已准备好加载到内存中并由系统执行。

你需要理解编译系统做了什么

对于上面这种简单的 hello 程序来说,我们可以依赖编译系统(compilation system)来提供一个正确和有效的机器代码。然而,对于我们上面讲的程序员来说,编译器有几大特征你需要知道

  • 优化程序性能(Optimizing program performance),现代编译器是一种高效的用来生成良好代码的工具。对于程序员来说,你无需为了编写高质量的代码而去理解编译器内部做了什么工作。然而,为了编写出高效的 C 语言程序,我们需要了解一些基本的机器码以及编译器将不同的 C 语句转化为机器代码的过程。
  • 理解链接时出现的错误(Understanding link-time errors),在我们的经验中,一些非常复杂的错误大多是由链接阶段引起的,特别是当你想要构建大型软件项目时。
  • 避免安全漏洞(Avoiding security holes),近些年来,缓冲区溢出(buffer overflow vulnerabilities)是造成网络和 Internet 服务的罪魁祸首,所以我们有必要去规避这种问题

处理器读取、解释内存中的指令

现在,我们的 hello.c 源程序已经被解释成为了可执行的 hello 目标程序,它存储在磁盘上。如果想要在 UNIX 操作系统中运行这个程序,我们需要在 shell 应用程序中输入

cxuan $ ./hello
hello, world
cxuan $ 

这里解释下什么是 shell,shell 其实就是一个命令解释器,它输出一个字符,等待用户输入一条命令,然后执行这个命令。如果命令行的第一个词不是 shell 内置的命令,那么 shell 就会假设这是一个可执行文件,它会加载并运行这个可执行文件。

系统硬件组成

为了理解 hello 程序在运行时发生了什么,我们需要首先对系统的硬件有一个认识。下面这是一张 Intel 系统产品的模型,我们来对其进行解释

  • 总线(Buses):在整个系统中运行的是称为总线的电气管道的集合,这些总线在组件之间来回传输字节信息。通常总线被设计成传送定长的字节块,也就是 字(word)。字中的字节数(字长)是一个基本的系统参数,各个系统中都不尽相同。现在大部分的字都是 4 个字节(32 位)或者 8 个字节(64 位)。

  • I/O 设备(I/O Devices):Input/Output 设备是系统和外部世界的连接。上图中有四类 I/O 设备:用于用户输入的键盘和鼠标,用于用户输出的显示器,一个磁盘驱动用来长时间的保存数据和程序。刚开始的时候,可执行程序就保存在磁盘上。

    每个I/O 设备连接 I/O 总线都被称为控制器(controller) 或者是 适配器(Adapter)。控制器和适配器之间的主要区别在于封装方式。控制器是 I/O 设备本身或者系统的主印制板电路(通常称作主板)上的芯片组。而适配器则是一块插在主板插槽上的卡。无论组织形式如何,它们的最终目的都是彼此交换信息。

  • 主存(Main Memory),主存是一个临时存储设备,而不是永久性存储,磁盘是 永久性存储 的设备。主存既保存程序,又保存处理器执行流程所处理的数据。从物理组成上说,主存是由一系列 DRAM(dynamic random access memory) 动态随机存储构成的集合。逻辑上说,内存就是一个线性的字节数组,有它唯一的地址编号,从 0 开始。一般来说,组成程序的每条机器指令都由不同数量的字节构成,C 程序变量相对应的数据项的大小根据类型进行变化。比如,在 Linux 的 x86-64 机器上,short 类型的数据需要 2 个字节,int 和 float 需要 4 个字节,而 long 和 double 需要 8 个字节。

  • 处理器(Processor)CPU(central processing unit) 或者简单的处理器,是解释(并执行)存储在主存储器中的指令的引擎。处理器的核心大小为一个字的存储设备(或寄存器),称为程序计数器(PC)。在任何时刻,PC 都指向主存中的某条机器语言指令(即含有该条指令的地址)。

    从系统通电开始,直到系统断电,处理器一直在不断地执行程序计数器指向的指令,再更新程序计数器,使其指向下一条指令。处理器根据其指令集体系结构定义的指令模型进行操作。在这个模型中,指令按照严格的顺序执行,执行一条指令涉及执行一系列的步骤。处理器从程序计数器指向的内存中读取指令,解释指令中的位,执行该指令指示的一些简单操作,然后更新程序计数器以指向下一条指令。指令与指令之间可能连续,可能不连续(比如 jmp 指令就不会顺序读取)

    下面是 CPU 可能执行简单操作的几个步骤

  • 加载(Load):从主存中拷贝一个字节或者一个字到内存中,覆盖寄存器先前的内容

  • 存储(Store):将寄存器中的字节或字复制到主存储器中的某个位置,从而覆盖该位置的先前内容

  • 操作(Operate):把两个寄存器的内容复制到 ALU(Arithmetic logic unit) 。把两个字进行算术运算,并把结果存储在寄存器中,重写寄存器先前的内容。

算术逻辑单元(ALU)是对数字二进制数执行算术和按位运算的组合数字电子电路。

  • 跳转(jump):从指令中抽取一个字,把这个字复制到程序计数器(PC) 中,覆盖原来的值

剖析 hello 程序的执行过程

前面我们简单的介绍了一下计算机的硬件的组成和操作,现在我们正式介绍运行示例程序时发生了什么,我们会从宏观的角度进行描述,不会涉及到所有的技术细节

刚开始时,shell 程序执行它的指令,等待用户键入一个命令。当我们在键盘上输入了 ./hello 这几个字符时,shell 程序将字符逐一读入寄存器,再把它放到内存中,如下图所示

当我们在键盘上敲击回车键的时候,shell 程序就知道我们已经结束了命令的输入。然后 shell 执行一系列指令来加载可执行的 hello 文件,这些指令将目标文件中的代码和数据从磁盘复制到主存。

利用 DMA(Direct Memory Access) 技术可以直接将磁盘中的数据复制到内存中,如下

一旦目标文件中 hello 中的代码和数据被加载到主存,处理器就开始执行 hello 程序的 main 程序中的机器语言指令。这些指令将 hello,world\n 字符串中的字节从主存复制到寄存器文件,再从寄存器中复制到显示设备,最终显示在屏幕上。如下所示

高速缓存是关键

上面我们介绍完了一个 hello 程序的执行过程,系统花费了大量时间把信息从一个地方搬运到另外一个地方。hello 程序的机器指令最初存储在磁盘上。当程序加载后,它们会拷贝到主存中。当 CPU 开始运行时,指令又从内存复制到 CPU 中。同样的,字符串数据 hello,world \n 最初也是在磁盘上,它被复制到内存中,然后再到显示器设备输出。从程序员的角度来看,这种复制大部分是开销,这减慢了程序的工作效率。因此,对于系统设计来说,最主要的一个工作是让程序运行的越来越快。

由于物理定律,较大的存储设备要比较小的存储设备慢。而由于寄存器和内存的处理效率在越来越大,所以针对这种差异,系统设计者采用了更小更快的存储设备,称为高速缓存存储器(cache memory, 简称为 cache 高速缓存),作为暂时的集结区域,存放近期可能会需要的信息。如下图所示

图中我们标出了高速缓存的位置,位于高速缓存中的 L1高速缓存容量可以达到数万字节,访问速度几乎和访问寄存器文件一样快。容量更大的 L2 高速缓存通过一条特殊的总线链接 CPU,虽然 L2 缓存比 L1 缓存慢 5 倍,但是仍比内存要哦快 5 – 10 倍。L1 和 L2 是使用一种静态随机访问存储器(SRAM) 的硬件技术实现的。最新的、处理器更强大的系统甚至有三级缓存:L1、L2 和 L3。系统可以获得一个很大的存储器,同时访问速度也更快,原因是利用了高速缓存的 局部性原理。

局部性原理:在 cs 中,引用局部性,也称为局部性原理,是 CPU 倾向于在短时间内重复访问同一组内存的机制。

通过把经常访问的数据存放在高速缓存中,大部分对内存的操作直接在高速缓存中就能完成。

存储设备层次结构

上面我们提到了L1、L2、L3 高速缓存还有内存,它们都是用于存储的目的,下面为你绘制了它们之间的层次结构

存储器的主要思想就是上一层的存储器作为低一层存储器的高速缓存。因此,寄存器文件就是 L1 的高速缓存,L1 就是 L2 的高速缓存,L2 是 L3 的高速缓存,L3 是主存的高速缓存,而主存又是磁盘的高速缓存。这里简单介绍一下存储器设备层次结构,具体的会在后面介绍。

操作系统如何管理硬件

再回到我们这个 hello 程序中,当 shell 加载并运行 hello 程序,以及 hello 程序输出自己的消息时,shell 和 hello 程序都没有直接访问键盘、显示器、磁盘或者主存,相反,它们会依赖操作系统(operating System)做这项工作。操作系统是一种软件,我们可以将操作系统视为介于应用程序和硬件之间的软件层,所有想要直接对硬件的操作都会通过操作系统。

操作系统有两项基本的功能:

  • **操作系统能够防止硬件被失控程序滥用
  • 向应用程序提供简单一致的机制来控制低级硬件设备

那么操作系统是通过什么实现对硬件的操作的呢?无非是通过 进程、虚拟内存、文件 来实现这两个功能。

文件是对 I/O 设备的抽象表示,虚拟内存是对主存和磁盘 I/O 设备的抽象表示,进程则是对处理器、主存和 I/O 设备的抽象表示。下面我们依次来探讨一下

进程

进程 是操作系统中的核心概念,进程是对正在运行中的程序的一个抽象。操作系统的其他所有内容都是围绕着进程展开的。即使只有一个 CPU,它们也支持(伪)并发操作。它们会将一个单独的 CPU 抽象为多个虚拟机的 CPU。我们可以把进程抽象为一种进程模型。

在进程模型中,一个进程就是一个正在执行的程序的实例,进程也包括程序计数器、寄存器和变量的当前值。从概念上来说,每个进程都有各自的虚拟 CPU,但是实际情况是 CPU 会在各个进程之间进行来回切换。

如上图所示,这是一个具有 4 个程序的多道处理程序,在进程不断切换的过程中,程序计数器也在不同的变化。

在上图中,这 4 道程序被抽象为 4 个拥有各自控制流程(即每个自己的程序计数器)的进程,并且每个程序都独立的运行。当然,实际上只有一个物理程序计数器,每个程序要运行时,其逻辑程序计数器会装载到物理程序计数器中。当程序运行结束后,其物理程序计数器就会是真正的程序计数器,然后再把它放回进程的逻辑计数器中。

从下图我们可以看到,在观察足够长的一段时间后,所有的进程都运行了,但在任何一个给定的瞬间仅有一个进程真正运行

因此,当我们说一个 CPU 只能真正一次运行一个进程的时候,即使有 2 个核(或 CPU),每一个核也只能一次运行一个线程

由于 CPU 会在各个进程之间来回快速切换,所以每个进程在 CPU 中的运行时间是无法确定的。并且当同一个进程再次在 CPU 中运行时,其在 CPU 内部的运行时间往往也是不固定的。

如下图所示,从一个进程到另一个进程的转换是由操作系统内核(kernel) 管理的。内核是操作系统代码常驻的部分。当应用程序需要操作系统某些操作时,比如读写文件,它就会执行一条特殊的 系统调用 指令。

注意:内核不是一个独立的进程。相反,它是系统管理全部进程所用代码和数据结构的集合。

我们会在后面具体介绍这些过程

线程

在传统的操作系统中,每个进程都有一个地址空间和一个控制线程。事实上,这是大部分进程的定义。不过,在许多情况下,经常存在同一地址空间中运行多个控制线程的情形,这些线程就像是分离的进程。准确的说,这其实是进程模型和线程模型的讨论,回答这个问题,可能需要分三步来回答

  • 多线程之间会共享同一块地址空间和所有可用数据的能力,这是进程所不具备的
  • 线程要比进程更轻量级,由于线程更轻,所以它比进程更容易创建,也更容易撤销。在许多系统中,创建一个线程要比创建一个进程快 10 – 100 倍。
  • 第三个原因可能是性能方面的探讨,如果多个线程都是 CPU 密集型的,那么并不能获得性能上的增强,但是如果存在着大量的计算和大量的 I/O 处理,拥有多个线程能在这些活动中彼此重叠进行,从而会加快应用程序的执行速度

进程中拥有一个执行的线程,通常简写为 线程(thread)。线程会有程序计数器,用来记录接着要执行哪一条指令;线程还拥有寄存器,用来保存线程当前正在使用的变量;线程还会有堆栈,用来记录程序的执行路径。尽管线程必须在某个进程中执行,但是进程和线程完完全全是两个不同的概念,并且他们可以分开处理。进程用于把资源集中在一起,而线程则是 CPU 上调度执行的实体。

线程给进程模型增加了一项内容,即在同一个进程中,允许彼此之间有较大的独立性且互不干扰。在一个进程中并行运行多个线程类似于在一台计算机上运行多个进程。在多个线程中,各个线程共享同一地址空间和其他资源。在多个进程中,进程共享物理内存、磁盘、打印机和其他资源。因为线程会包含有一些进程的属性,所以线程被称为轻量的进程(lightweight processes)多线程(multithreading)一词还用于描述在同一进程中多个线程的情况。

下图我们可以看到三个传统的进程,每个进程有自己的地址空间和单个控制线程。每个线程都在不同的地址空间中运行

下图中,我们可以看到有一个进程三个线程的情况。每个线程都在相同的地址空间中运行。

虚拟内存

虚拟内存的基本思想是,每个程序都有自己的地址空间,这个地址空间被划分为多个称为页面(page)的块。每一页都是连续的地址范围。这些页被映射到物理内存,但并不是所有的页都必须在内存中才能运行程序。当程序引用到一部分在物理内存中的地址空间时,硬件会立刻执行必要的映射。当程序引用到一部分不在物理内存中的地址空间时,由操作系统负责将缺失的部分装入物理内存并重新执行失败的指令。

在某种意义上来说,虚拟地址是对基址寄存器和变址寄存器的一种概述。8088 有分离的基址寄存器(但不是变址寄存器)用于放入 text 和 data 。

使用虚拟内存,可以将整个地址空间以很小的单位映射到物理内存中,而不是仅仅针对 text 和 data 区进行重定位。下面我们会探讨虚拟内存是如何实现的。

虚拟内存很适合在多道程序设计系统中使用,许多程序的片段同时保存在内存中,当一个程序等待它的一部分读入内存时,可以把 CPU 交给另一个进程使用。

文件

文件(Files)是由进程创建的逻辑信息单元。一个磁盘会包含几千甚至几百万个文件,每个文件是独立于其他文件的。它是一种抽象机制,它提供了一种方式用来存储信息以及在后面进行读取。

网络通信

现代系统是不会独立存在的,因此经常通过网络和其他系统连接到一起。从一个单独的系统来看,网络可以视为 I/O 设备,如下图所示

当系统从主存复制一串字节到网络适配器时,数据流经过网络到达另一台机器,而不是说到达本地磁盘驱动器。类似的,系统可以读取其他系统发送过来的数据,把数据复制到自己的主存中。

随着 internet 的出现,数据从一台主机复制到另一台主机的情况已经成为最重要的用途之一。比如,像电子邮件、即时通讯、FTP 和 telnet 这样的应用都是基于网络复制信息的功能。

C语言教程

原文:https://zwmst.com/2711.html

前言

C 语言是一门抽象的面向过程的语言,C 语言广泛应用于底层开发,C 语言在计算机体系中占据着不可替代的作用,可以说 C 语言是编程的基础,也就是说,不管你学习任何语言,都应该把 C 语言放在首先要学的位置上。下面这张图更好的说明 C 语言的重要性

可以看到,C 语言是一种底层语言,是一种系统层级的语言,操作系统就是使用 C 语言来编写的,比如 Windows、Linux、UNIX 。如果说其他语言是光鲜亮丽的外表,那么 C 语言就是灵魂,永远那么朴实无华。

C 语言特性

那么,既然 C 语言这么重要,它有什么值得我们去学的地方呢?我们不应该只因为它重要而去学,我们更在意的是学完我们能学会什么,能让我们获得什么。

C 语言的设计

C 语言是 1972 年,由贝尔实验室的丹尼斯·里奇(Dennis Ritch)肯·汤普逊(Ken Thompson)在开发 UNIX 操作系统时设计了C语言。C 语言是一门流行的语言,它把计算机科学理论和工程实践理论完美的融合在一起,使用户能够完成模块化的编程和设计。

计算机科学理论:简称 CS、是系统性研究信息与计算的理论基础以及它们在计算机系统中如何实现与应用的实用技术的学科。

C 语言具有高效性

C 语言是一门高效性语言,它被设计用来充分发挥计算机的优势,因此 C 语言程序运行速度很快,C 语言能够合理了使用内存来获得最大的运行速度

C 语言具有可移植性

C 语言是一门具有可移植性的语言,这就意味着,对于在一台计算机上编写的 C 语言程序可以在另一台计算机上轻松地运行,从而极大的减少了程序移植的工作量。

C 语言特点

  • C 语言是一门简洁的语言,因为 C 语言设计更加靠近底层,因此不需要众多 Java 、C# 等高级语言才有的特性,程序的编写要求不是很严格。
  • C 语言具有结构化控制语句,C 语言是一门结构化的语言,它提供的控制语句具有结构化特征,如 for 循环、if⋯ else 判断语句和 switch 语句等。
  • C 语言具有丰富的数据类型,不仅包含有传统的字符型、整型、浮点型、数组类型等数据类型,还具有其他编程语言所不具备的数据类型,比如指针。
  • C 语言能够直接对内存地址进行读写,因此可以实现汇编语言的主要功能,并可直接操作硬件。
  • C 语言速度快,生成的目标代码执行效率高。

下面让我们通过一个简单的示例来说明一下 C 语言

入门级 C 语言程序

下面我们来看一个很简单的 C 语言程序,我是 mac 电脑,所以我使用的是 xcode 进行开发,我觉得工具无所谓大家用着顺手就行。

第一个 C 语言程序

#include <stdio.h>

int main(int argc, const char * argv[]) {
    printf("Hello, World!\n");

    printf("my Name is cxuan \n")

    printf("number = %d \n", number);

    return 0;
}

你可能不知道这段代码是什么意思,不过别着急,我们先运行一下看看结果。

这段程序输出了 Hello,World!My Name is cxuan,最后一行是程序的执行结果,表示这段程序是否有错误。下面我们解释一下各行代码的含义。

首先,第一行的 #include <stdio.h>, 这行代码包含另一个文件,这一行告诉编译器把 stdio.h 的内容包含在当前程序中。 stdio.h 是 C 编译器软件包的标准部分,它能够提供键盘输入和显示器输出。

什么是 C 标准软件包?C 是由 Dennis M 在1972年开发的通用,过程性,命令式计算机编程语言。C标准库是一组 C 语言内置函数,常量和头文件,例如等。此库将用作 C 程序员的参考手册。

我们后面会介绍 stdio.h ,现在你知道它是什么就好。

在 stdio.h 下面一行代码就是 main 函数。

C 程序能够包含一个或多个函数,函数是 C 语言的根本,就和方法是 Java 的基本构成一样。main() 表示一个函数名,int 表示的是 main 函数返回一个整数。void 表明 main() 不带任何参数。这些我们后面也会详细说明,只需要记住 int 和 void 是标准 ANSI C 定义 main() 的一部分(如果使用 ANSI C 之前的编译器,请忽略 void)。

然后是 /*一个简单的 C 语言程序*/ 表示的是注释,注释使用 /**/ 来表示,注释的内容在两个符号之间。这些符号能够提高程序的可读性。

注意:注释只是为了帮助程序员理解代码的含义,编译器会忽略注释

下面就是 { ,这是左花括号,它表示的是函数体的开始,而最后的右花括号 } 表示函数体的结束。 { } 中间是书写代码的地方,也叫做代码块。

int number 表示的是将会使用一个名为 number 的变量,而且 number 是 int 整数类型。

number = 11 表示的是把值 11 赋值给 number 的变量。

printf(Hello,world!\n); 表示调用一个函数,这个语句使用 printf() 函数,在屏幕上显示 Hello,world , printf() 函数是 C 标准库函数中的一种,它能够把程序运行的结果输出到显示器上。而代码 \n 表示的是 换行,也就是另起一行,把光标移到下一行。

然后接下来的一行 printf() 和上面一行是一样的,我们就不多说了。最后一行 printf() 有点意思,你会发现有一个 %d 的语法,它的意思表示的是使用整形输出字符串。

代码块的最后一行是 return 0,它可以看成是 main 函数的结束,最后一行是代码块 } ,它表示的是程序的结束。

好了,我们现在写完了第一个 C 语言程序,有没有对 C 有了更深的认识呢?肯定没有。。。这才哪到哪,继续学习吧。

现在,我们可以归纳为 C 语言程序的几个组成要素,如下图所示

C 语言执行流程

C 语言程序成为高级语言的原因是它能够读取并理解人们的思想。然而,为了能够在系统中运行 hello.c 程序,则各个 C 语句必须由其他程序转换为一系列低级机器语言指令。这些指令被打包作为可执行对象程序,存储在二进制磁盘文件中。目标程序也称为可执行目标文件。

在 UNIX 系统中,从源文件到对象文件的转换是由编译器执行完成的。

gcc -o hello hello.c

gcc 编译器驱动从源文件读取 hello.c ,并把它翻译成一个可执行文件 hello。这个翻译过程可用如下图来表示

这就是一个完整的 hello world 程序执行过程,会涉及几个核心组件:预处理器、编译器、汇编器、连接器,下面我们逐个击破。

  • 预处理阶段(Preprocessing phase),预处理器会根据开始的 # 字符,修改源 C 程序。#include 命令就会告诉预处理器去读系统头文件 stdio.h 中的内容,并把它插入到程序作为文本。然后就得到了另外一个 C 程序hello.i,这个程序通常是以 .i为结尾。

  • 然后是 编译阶段(Compilation phase),编译器会把文本文件 hello.i 翻译成文本hello.s,它包括一段汇编语言程序(assembly-language program)

  • 编译完成之后是汇编阶段(Assembly phase),这一步,汇编器 as会把 hello.s 翻译成机器指令,把这些指令打包成可重定位的二进制程序(relocatable object program)放在 hello.c 文件中。它包含的 17 个字节是函数 main 的指令编码,如果我们在文本编辑器中打开 hello.o 将会看到一堆乱码。

  • 最后一个是链接阶段(Linking phase),我们的 hello 程序会调用 printf 函数,它是 C 编译器提供的 C 标准库中的一部分。printf 函数位于一个叫做 printf.o文件中,它是一个单独的预编译好的目标文件,而这个文件必须要和我们的 hello.o 进行链接,连接器(ld) 会处理这个合并操作。结果是,hello 文件,它是一个可执行的目标文件(或称为可执行文件),已准备好加载到内存中并由系统执行。

你需要理解编译系统做了什么

对于上面这种简单的 hello 程序来说,我们可以依赖编译系统(compilation system)来提供一个正确和有效的机器代码。然而,对于我们上面讲的程序员来说,编译器有几大特征你需要知道

  • 优化程序性能(Optimizing program performance),现代编译器是一种高效的用来生成良好代码的工具。对于程序员来说,你无需为了编写高质量的代码而去理解编译器内部做了什么工作。然而,为了编写出高效的 C 语言程序,我们需要了解一些基本的机器码以及编译器将不同的 C 语句转化为机器代码的过程。
  • 理解链接时出现的错误(Understanding link-time errors),在我们的经验中,一些非常复杂的错误大多是由链接阶段引起的,特别是当你想要构建大型软件项目时。
  • 避免安全漏洞(Avoiding security holes),近些年来,缓冲区溢出(buffer overflow vulnerabilities)是造成网络和 Internet 服务的罪魁祸首,所以我们有必要去规避这种问题。

系统硬件组成

为了理解 hello 程序在运行时发生了什么,我们需要首先对系统的硬件有一个认识。下面这是一张 Intel 系统产品的模型,我们来对其进行解释

  • 总线(Buses):在整个系统中运行的是称为总线的电气管道的集合,这些总线在组件之间来回传输字节信息。通常总线被设计成传送定长的字节块,也就是 字(word)。字中的字节数(字长)是一个基本的系统参数,各个系统中都不尽相同。现在大部分的字都是 4 个字节(32 位)或者 8 个字节(64 位)。

  • I/O 设备(I/O Devices):Input/Output 设备是系统和外部世界的连接。上图中有四类 I/O 设备:用于用户输入的键盘和鼠标,用于用户输出的显示器,一个磁盘驱动用来长时间的保存数据和程序。刚开始的时候,可执行程序就保存在磁盘上。

    每个I/O 设备连接 I/O 总线都被称为控制器(controller) 或者是 适配器(Adapter)。控制器和适配器之间的主要区别在于封装方式。控制器是 I/O 设备本身或者系统的主印制板电路(通常称作主板)上的芯片组。而适配器则是一块插在主板插槽上的卡。无论组织形式如何,它们的最终目的都是彼此交换信息。

  • 主存(Main Memory),主存是一个临时存储设备,而不是永久性存储,磁盘是 永久性存储 的设备。主存既保存程序,又保存处理器执行流程所处理的数据。从物理组成上说,主存是由一系列 DRAM(dynamic random access memory) 动态随机存储构成的集合。逻辑上说,内存就是一个线性的字节数组,有它唯一的地址编号,从 0 开始。一般来说,组成程序的每条机器指令都由不同数量的字节构成,C 程序变量相对应的数据项的大小根据类型进行变化。比如,在 Linux 的 x86-64 机器上,short 类型的数据需要 2 个字节,int 和 float 需要 4 个字节,而 long 和 double 需要 8 个字节。

  • 处理器(Processor)CPU(central processing unit) 或者简单的处理器,是解释(并执行)存储在主存储器中的指令的引擎。处理器的核心大小为一个字的存储设备(或寄存器),称为程序计数器(PC)。在任何时刻,PC 都指向主存中的某条机器语言指令(即含有该条指令的地址)。

    从系统通电开始,直到系统断电,处理器一直在不断地执行程序计数器指向的指令,再更新程序计数器,使其指向下一条指令。处理器根据其指令集体系结构定义的指令模型进行操作。在这个模型中,指令按照严格的顺序执行,执行一条指令涉及执行一系列的步骤。处理器从程序计数器指向的内存中读取指令,解释指令中的位,执行该指令指示的一些简单操作,然后更新程序计数器以指向下一条指令。指令与指令之间可能连续,可能不连续(比如 jmp 指令就不会顺序读取)

    下面是 CPU 可能执行简单操作的几个步骤

  • 加载(Load):从主存中拷贝一个字节或者一个字到内存中,覆盖寄存器先前的内容

  • 存储(Store):将寄存器中的字节或字复制到主存储器中的某个位置,从而覆盖该位置的先前内容

  • 操作(Operate):把两个寄存器的内容复制到 ALU(Arithmetic logic unit) 。把两个字进行算术运算,并把结果存储在寄存器中,重写寄存器先前的内容。

算术逻辑单元(ALU)是对数字二进制数执行算术和按位运算的组合数字电子电路。

  • 跳转(jump):从指令中抽取一个字,把这个字复制到程序计数器(PC) 中,覆盖原来的值

剖析 hello 程序的执行过程

前面我们简单的介绍了一下计算机的硬件的组成和操作,现在我们正式介绍运行示例程序时发生了什么,我们会从宏观的角度进行描述,不会涉及到所有的技术细节

刚开始时,shell 程序执行它的指令,等待用户键入一个命令。当我们在键盘上输入了 ./hello 这几个字符时,shell 程序将字符逐一读入寄存器,再把它放到内存中,如下图所示

当我们在键盘上敲击回车键的时候,shell 程序就知道我们已经结束了命令的输入。然后 shell 执行一系列指令来加载可执行的 hello 文件,这些指令将目标文件中的代码和数据从磁盘复制到主存。

利用 DMA(Direct Memory Access) 技术可以直接将磁盘中的数据复制到内存中,如下

一旦目标文件中 hello 中的代码和数据被加载到主存,处理器就开始执行 hello 程序的 main 程序中的机器语言指令。这些指令将 hello,world\n 字符串中的字节从主存复制到寄存器文件,再从寄存器中复制到显示设备,最终显示在屏幕上。如下所示

高速缓存是关键

上面我们介绍完了一个 hello 程序的执行过程,系统花费了大量时间把信息从一个地方搬运到另外一个地方。hello 程序的机器指令最初存储在磁盘上。当程序加载后,它们会拷贝到主存中。当 CPU 开始运行时,指令又从内存复制到 CPU 中。同样的,字符串数据 hello,world \n 最初也是在磁盘上,它被复制到内存中,然后再到显示器设备输出。从程序员的角度来看,这种复制大部分是开销,这减慢了程序的工作效率。因此,对于系统设计来说,最主要的一个工作是让程序运行的越来越快。

由于物理定律,较大的存储设备要比较小的存储设备慢。而由于寄存器和内存的处理效率在越来越大,所以针对这种差异,系统设计者采用了更小更快的存储设备,称为高速缓存存储器(cache memory, 简称为 cache 高速缓存),作为暂时的集结区域,存放近期可能会需要的信息。如下图所示

图中我们标出了高速缓存的位置,位于高速缓存中的 L1高速缓存容量可以达到数万字节,访问速度几乎和访问寄存器文件一样快。容量更大的 L2 高速缓存通过一条特殊的总线链接 CPU,虽然 L2 缓存比 L1 缓存慢 5 倍,但是仍比内存要哦快 5 – 10 倍。L1 和 L2 是使用一种静态随机访问存储器(SRAM) 的硬件技术实现的。最新的、处理器更强大的系统甚至有三级缓存:L1、L2 和 L3。系统可以获得一个很大的存储器,同时访问速度也更快,原因是利用了高速缓存的 局部性原理。

Again:入门程序细节

现在,我们来探讨一下入门级程序的细节,由浅入深的来了解一下 C 语言的特性。

include

我们上面说到,#include<stdio.h> 是程序编译之前要处理的内容,称为编译预处理命令。

预处理命令是在编译之前进行处理。预处理程序一般以 # 号开头。

所有的 C 编译器软件包都提供 stdio.h 文件。该文件包含了给编译器使用的输入和输出函数,比如 println() 信息。该文件名的含义是标准输入/输出 头文件。通常,在 C 程序顶部的信息集合被称为 头文件(header)

C 的第一个标准是由 ANSI 发布的。虽然这份文档后来被国际标准化组织(ISO)采纳并且 ISO 发布的修订版也被 ANSI 采纳了,但名称 ANSI C(而不是 ISO C) 仍被广泛使用。一些软件开发者使用ISO C,还有一些使用 Standard C

C 标准库

除了 外,C 标准库还包括下面这些头文件

提供了一个名为 assert 的关键字,它用于验证程序作出的假设,并在假设为假输出诊断消息。

C 标准库的 ctype.h 头文件提供了一些函数,可以用于测试和映射字符。

这些字符接受 int 作为参数,它的值必须是 EOF 或者是一个无符号字符

EOF是一个计算机术语,为 End Of File 的缩写,在操作系统中表示资料源无更多的资料可读取。资料源通常称为档案或串流。通常在文本的最后存在此字符表示资料结束。

C 标准库的 errno.h 头文件定义了整数变量 errno,它是通过系统调用设置的,这些库函数表明了什么发生了错误。

**

C 标准库的 float.h 头文件包含了一组与浮点值相关的依赖于平台的常量。

limits.h 头文件决定了各种变量类型的各种属性。定义在该头文件中的宏限制了各种变量类型(比如 char、int 和 long)的值。

**

locale.h 头文件定义了特定地域的设置,比如日期格式和货币符号

**

math.h 头文件定义了各种数学函数和一个宏。在这个库中所有可用的功能都带有一个 double 类型的参数,且都返回 double 类型的结果。

setjmp.h 头文件定义了宏 setjmp()、函数 longjmp() 和变量类型 jmp_buf,该变量类型会绕过正常的函数调用和返回规则。

**

signal.h 头文件定义了一个变量类型 sig_atomic_t、两个函数调用和一些宏来处理程序执行期间报告的不同信号。

**

stdarg.h 头文件定义了一个变量类型 va_list 和三个宏,这三个宏可用于在参数个数未知(即参数个数可变)时获取函数中的参数。

**

stddef .h 头文件定义了各种变量类型和宏。这些定义中的大部分也出现在其它头文件中。

**

stdlib .h 头文件定义了四个变量类型、一些宏和各种通用工具函数。

string .h 头文件定义了一个变量类型、一个宏和各种操作字符数组的函数。

time.h 头文件定义了四个变量类型、两个宏和各种操作日期和时间的函数。

main() 函数

main 函数听起来像是调皮捣蛋的孩子故意给方法名起一个 主要的 方法,来告诉他人他才是这个世界的中心。但事实却不是这样,而 main() 方法确实是世界的中心。

C 语言程序一定从 main() 函数开始执行,除了 main() 函数外,你可以随意命名其他函数。通常,main 后面的 () 中表示一些传入信息,我们上面的那个例子中没有传递信息,因为圆括号中的输入是 void 。

除了上面那种写法外,还有两种 main 方法的表示方式,一种是 void main(){} ,一种是 int main(int argc, char* argv[]) {}

  • void main() 声明了一个带有不确定参数的构造方法
  • int main(int argc, char* argv[]) {} 其中的 argc 是一个非负值,表示从运行程序的环境传递到程序的参数数量。它是指向 argc + 1 指针数组的第一个元素的指针,其中最后一个为null,而前一个(如果有的话)指向表示从主机环境传递给程序的参数的字符串。 如果argv [0]不是空指针(或者等效地,如果argc> 0),则指向表示程序名称的字符串,如果在主机环境中无法使用程序名称,则该字符串为空。

注释

在程序中,使用 /**/ 的表示注释,注释对于程序来说没有什么实际用处,但是对程序员来说却非常有用,它能够帮助我们理解程序,也能够让他人看懂你写的程序,我们在开发工作中,都非常反感不写注释的人,由此可见注释非常重要。

C 语言注释的好处是,它可以放在任意地方,甚至代码在同一行也没关系。较长的注释可以多行表示,我们使用 /**/ 表示多行注释,而 // 只表示的是单行注释。下面是几种注释的表示形式

// 这是一个单行注释

/* 多行注释用一行表示 */

/
    多行注释用多行表示
        多行注释用多行表示
            多行注释用多行表示
                多行注释用多行表示

*/

函数体

在头文件、main 方法后面的就是函数体(注释一般不算),函数体就是函数的执行体,是你编写大量代码的地方。

变量声明

在我们入门级的代码中,我们声明了一个名为 number 的变量,它的类型是 int,这行代码叫做 声明,声明是 C 语言最重要的特性之一。这个声明完成了两件事情:定义了一个名为 number 的变量,定义 number 的具体类型。

int 是 C 语言的一个 关键字(keyword),表示一种基本的 C 语言数据类型。关键字是用于语言定义的。不能使用关键字作为变量进行定义。

示例中的 number 是一个 标识符(identifier),也就是一个变量、函数或者其他实体的名称。

变量赋值

在入门例子程序中,我们声明了一个 number 变量,并为其赋值为 11,赋值是 C 语言的基本操作之一。这行代码的意思就是把值 1 赋给变量 number。在执行 int number 时,编译器会在计算机内存中为变量 number 预留空间,然后在执行这行赋值表达式语句时,把值存储在之前预留的位置。可以给 number 赋不同的值,这就是 number 之所以被称为 变量(variable) 的原因。

printf 函数

在入门例子程序中,有三行 printf(),这是 C 语言的标准函数。圆括号中的内容是从 main 函数传递给 printf 函数的。参数分为两种:实际参数(actual argument)形式参数(formal parameters)。我们上面提到的 printf 函数括号中的内容,都是实参。

return 语句

在入门例子程序中,return 语句是最后一条语句。int main(void) 中的 int 表明 main() 函数应返回一个整数。有返回值的 C 函数要有 return 语句,没有返回值的程序也建议大家保留 return 关键字,这是一种好的习惯或者说统一的编码风格。

分号

在 C 语言中,每一行的结尾都要用 ; 进行结束,它表示一个语句的结束,如果忘记或者会略分号会被编译器提示错误。

关键字

下面是 C 语言中的关键字,C 语言的关键字一共有 32 个,根据其作用不同进行划分

数据类型关键字

数据类型的关键字主要有 12 个,分别是

  • char: 声明字符型变量或函数
  • double: 声明双精度变量或函数
  • float: 声明浮点型变量或函数
  • int : 声明整型变量或函数
  • long: 声明长整型变量或函数
  • short : 声明短整型变量或函数
  • signed : 声明有符号类型变量或函数
  • _Bool: 声明布尔类型
  • _Complex :声明复数
  • _Imaginary: 声明虚数
  • unsigned : 声明无符号类型变量或函数
  • void : 声明函数无返回值或无参数,声明无类型指针

控制语句关键字

控制语句循环的关键字也有 12 个,分别是

**循环语句

  • for : for 循环,使用的最多
  • do :循环语句的前提条件循环体
  • while:循环语句的循环条件
  • break : 跳出当前循环
  • continue:结束当前循环,开始下一轮循环

**条件语句

  • if:条件语句的判断条件
  • else : 条件语句的否定分支,与 if 连用
  • goto: 无条件跳转语句

**开关语句

  • switch: 用于开关语句
  • case:开关语句的另外一种分支
  • default : 开关语句中的其他分支

**返回语句

retur :子程序返回语句(可以带参数,也看不带参数)

存储类型关键字

  • auto : 声明自动变量 一般不使用
  • extern : 声明变量是在其他文件正声明(也可以看做是引用变量)
  • register : 声明寄存器变量
  • static: 声明静态变量

其他关键字

  • const: 声明只读变量
  • sizeof : 计算数据类型长度
  • typedef: 用以给数据类型取别名
  • volatile : 说明变量在程序执行中可被隐含地改变

后记

这篇文章我们先介绍了 C 语言的特性,C 语言为什么这么火,C 语言的重要性,之后我们以一道 C 语言的入门程序讲起,我们讲了 C 语言的基本构成要素,C 语言在硬件上是如何运行的,C 语言的编译过程和执行过程等,在这之后我们又加深讲解了一下入门例子程序的组成特征。

我向面试官讲解了单例模式,他对我竖起了大拇指

原文:https://zwmst.com/2713.html

单例模式相信大家都有所听闻,甚至也写过不少了,在面试中也是考得最多的其中一个设计模式,面试官常常会要求写出两种类型的单例模式并且解释其原理,废话不多说,我们开始学习如何很好地回答这一道面试题吧。

什么是单例模式

面试官问什么是单例模式时,千万不要答非所问,给出单例模式有两种类型之类的回答,要围绕单例模式的定义去展开。

单例模式是指在内存中只会创建且仅创建一次对象的设计模式。在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象。

image.png

单例模式的类型

单例模式有两种类型:

  • 懒汉式:在真正需要使用对象时才去创建该单例类对象
  • 饿汉式:在类加载时已经创建好该单例对象,等待被程序使用

懒汉式创建单例对象

懒汉式创建对象的方法是在程序使用对象前,先判断该对象是否已经实例化(判空),若已实例化直接返回该类对象。否则则先执行实例化操作。

image.png

根据上面的流程图,就可以写出下面的这段代码

public class Singleton {

    private static Singleton singleton;

    private Singleton(){}

    public static Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }

}

没错,这里我们已经写出了一个很不错的单例模式,不过它不是完美的,但是这并不影响我们使用这个“单例对象”。

以上就是懒汉式创建单例对象的方法,我会在后面解释这段代码在哪里可以优化,存在什么问题。

饿汉式创建单例对象

饿汉式在类加载时已经创建好该对象,在程序调用时直接返回该单例对象即可,即我们在编码时就已经指明了要马上创建这个对象,不需要等到被调用时再去创建。

关于类加载,涉及到JVM的内容,我们目前可以简单认为在程序启动时,这个单例对象就已经创建好了。

image.png

public class Singleton{

    private static final Singleton singleton = new Singleton();

    private Singleton(){}

    public static Singleton getInstance() {
        return singleton;
    }
}

注意上面的代码在第3行已经实例化好了一个Singleton对象在内存中,不会有多个Singleton对象实例存在

类在加载时会在堆内存中创建一个Singleton对象,当类被卸载时,Singleton对象也随之消亡了。

懒汉式如何保证只创建一个对象

我们再来回顾懒汉式的核心方法

public static Singleton getInstance() {
    if (singleton == null) {
        singleton = new Singleton();
    }
    return singleton;
}

这个方法其实是存在问题的,试想一下,如果两个线程同时判断 singleton 为空,那么它们都会去实例化一个Singleton 对象,这就变成多例了。所以,我们要解决的是线程安全问题。

image.png

最容易想到的解决方法就是在方法上加锁,或者是对类对象加锁,程序就会变成下面这个样子

public static synchronized Singleton getInstance() {
    if (singleton == null) {
        singleton = new Singleton();
    }
    return singleton;
}
// 或者
public static Singleton getInstance() {
    synchronized(Singleton.class) {   
        if (singleton == null) {
            singleton = new Singleton();
        }
    }
    return singleton;
}

这样就规避了两个线程同时创建Singleton对象的风险,但是引来另外一个问题:**每次去获取对象都需要先获取锁,并发性能非常地差,极端情况下,可能会出现卡顿现象。

接下来要做的就是优化性能:目标是如果没有实例化对象则加锁创建,如果已经实例化了,则不需要加锁,直接获取实例

所以直接在方法上加锁的方式就被废掉了,因为这种方式无论如何都需要先获取锁

public static Singleton getInstance() {
    if (singleton == null) {  // 线程A和线程B同时看到singleton = null,如果不为null,则直接返回singleton
        synchronized(Singleton.class) { // 线程A或线程B获得该锁进行初始化
            if (singleton == null) { // 其中一个线程进入该分支,另外一个线程则不会进入该分支
                singleton = new Singleton();
            }
        }
    }
    return singleton;
}

上面的代码已经完美地解决了并发安全 + 性能低效问题:

  • 第 2 行代码,如果 singleton 不为空,则直接返回对象,不需要获取锁;而如果多个线程发现 singleton 为空,则进入分支;
  • 第 3 行代码,多个线程尝试争抢同一个锁,只有一个线程争抢成功,第一个获取到锁的线程会再次判断singleton 是否为空,因为 singleton 有可能已经被之前的线程实例化
  • 其它之后获取到锁的线程在执行到第 4 行校验代码,发现 singleton 已经不为空了,则不会再 new 一个对象,直接返回对象即可
  • 之后所有进入该方法的线程都不会去获取锁,在第一次判断 singleton 对象时已经不为空了

因为需要两次判空,且对类对象加锁,该懒汉式写法也被称为:**Double Check(双重校验) + Lock(加锁)

完整的代码如下所示:

public class Singleton {

    private static Singleton singleton;

    private Singleton(){}

    public static Singleton getInstance() {
        if (singleton == null) {  // 线程A和线程B同时看到singleton = null,如果不为null,则直接返回singleton
            synchronized(Singleton.class) { // 线程A或线程B获得该锁进行初始化
                if (singleton == null) { // 其中一个线程进入该分支,另外一个线程则不会进入该分支
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }

}

上面这段代码已经近似完美了,但是还存在最后一个问题:指令重排

使用 volatile 防止指令重排

创建一个对象,在 JVM 中会经过三步:

(1)为 singleton 分配内存空间

(2)初始化 singleton 对象

(3)将 singleton 指向分配好的内存空间

指令重排序是指:**JVM 在保证最终结果正确的情况下,可以不按照程序编码的顺序执行语句,尽可能提高程序的性能

在这三步中,第 2、3 步有可能会发生指令重排现象,创建对象的顺序变为 1-3-2,会导致多个线程获取对象时,有可能线程 A 创建对象的过程中,执行了 1、3 步骤,线程 B 判断 singleton 已经不为空,获取到未初始化的singleton 对象,就会报 NPE 异常。文字较为晦涩,可以看流程图:

image.png

使用 volatile 关键字可以防止指令重排序,其原理较为复杂,这篇文章不打算展开,可以这样理解:使用 volatile 关键字修饰的变量,可以保证其指令执行的顺序与程序指明的顺序一致,不会发生顺序变换,这样在多线程环境下就不会发生 NPE 异常了。

volatile 还有第二个作用:使用 volatile 关键字修饰的变量,可以保证其内存可见性,即每一时刻线程读取到该变量的值都是内存中最新的那个值,线程每次操作该变量都需要先读取该变量。

最终的代码如下所示:

public class Singleton {

    private static volatile Singleton singleton;

    private Singleton(){}

    public static Singleton getInstance() {
        if (singleton == null) {  // 线程A和线程B同时看到singleton = null,如果不为null,则直接返回singleton
            synchronized(Singleton.class) { // 线程A或线程B获得该锁进行初始化
                if (singleton == null) { // 其中一个线程进入该分支,另外一个线程则不会进入该分支
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }

}

破坏懒汉式单例与饿汉式单例

无论是完美的懒汉式还是饿汉式,终究敌不过反射和序列化,它们俩都可以把单例对象破坏掉(产生多个对象)。

利用反射破坏单例模式

下面是一段使用反射破坏单例模式的例子

public static void main(String[] args) {
    // 获取类的显式构造器
    Constructor<Singleton> construct = Singleton.class.getDeclaredConstructor();
    // 可访问私有构造器
    construct.setAccessible(true); 
    // 利用反射构造新对象
    Singleton obj1 = construct.newInstance(); 
    // 通过正常方式获取单例对象
    Singleton obj2 = Singleton.getInstance(); 
    System.out.println(obj1 == obj2); // false
}

上述的代码一针见血了:利用反射,强制访问类的私有构造器,去创建另一个对象

利用序列化与反序列化破坏单例模式

下面是一种使用序列化和反序列化破坏单例模式的例子

public static void main(String[] args) {
    // 创建输出流
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Singleton.file"));
    // 将单例对象写到文件中
    oos.writeObject(Singleton.getInstance());
    // 从文件中读取单例对象
    File file = new File("Singleton.file");
    ObjectInputStream ois =  new ObjectInputStream(new FileInputStream(file));
    Singleton newInstance = (Singleton) ois.readObject();
    // 判断是否是同一个对象
    System.out.println(newInstance == Singleton.getInstance()); // false
}

两个对象地址不相等的原因是:readObject() 方法读入对象时它必定会返回一个新的对象实例,必然指向新的内存地址。

让面试官鼓掌的枚举实现

我们已经掌握了懒汉式与饿汉式的常见写法了,通常情况下到这里已经足够了。但是,追求极致的我们,怎么能够止步于此,在《Effective Java》书中,给出了终极解决方法,话不多说,学完下面,真的不虚面试官考你了。

在 JDK 1.5 后,使用 Java 语言实现单例模式的方式又多了一种:枚举

枚举实现单例模式完整代码如下:

public enum Singleton {
    INSTANCE;

    public void doSomething() {
        System.out.println("这是枚举类型的单例模式!");
    }
}

使用枚举实现单例模式较其它两种实现方式的优势有 3 点,让我们来细品。

优势 1 :一目了然的代码

代码对比饿汉式与懒汉式来说,更加地简洁。最少只需要3行代码,就可以完成一个单例模式:

public enum Test {
    INSTANCE;
}

我们从最直观的地方入手,第一眼看到这3行代码,就会感觉到,没错,就是少,虽然这优势有些牵强,但写的代码越少,越不容易出错。

优势 2:天然的线程安全与单一实例

它不需要做任何额外的操作,就可以保证对象单一性与线程安全性。

我写了一段测试代码放在下面,这一段代码可以证明程序启动时仅会创建一个 Singleton 对象,且是线程安全的。

我们可以简单地理解枚举创建实例的过程:在程序启动时,会调用 Singleton 的空参构造器,实例化好一个Singleton 对象赋给 INSTANCE,之后再也不会实例化

public enum Singleton {
    INSTANCE;
    Singleton() { System.out.println("枚举创建对象了"); }
    public static void main(String[] args) { /* test(); */ }
    public void test() {
        Singleton t1 = Singleton.INSTANCE;
        Singleton t2 = Singleton.INSTANCE;
        System.out.print("t1和t2的地址是否相同:" + t1 == t2);
    }
}
// 枚举创建对象了
// t1和t2的地址是否相同:true

除了优势1和优势2,还有最后一个优势是 保护单例模式,它使得枚举在当前的单例模式领域已经是 无懈可击

优势 3:枚举保护单例模式不被破坏

使用枚举可以防止调用者使用反射、序列化与反序列化机制强制生成多个单例对象,破坏单例模式。

**防反射

枚举类默认继承了 Enum 类,在利用反射调用 newInstance() 时,会判断该类是否是一个枚举类,如果是,则抛出异常。

**防止反序列化创建多个枚举对象

在读入 Singleton 对象时,每个枚举类型和枚举名字都是唯一的,所以在序列化时,仅仅只是对枚举的类型和变量名输出到文件中,在读入文件反序列化成对象时,使用 Enum 类的 valueOf(String name) 方法根据变量的名字查找对应的枚举对象。

所以,在序列化和反序列化的过程中,只是写出和读入了枚举类型和名字,没有任何关于对象的操作。

小结:

(1)Enum 类内部使用Enum 类型判定防止通过反射创建多个对象

(2)Enum 类通过写出(读入)对象类型和枚举名字将对象序列化(反序列化),通过 valueOf() 方法匹配枚举名找到内存中的唯一的对象实例,防止通过反序列化构造多个对象

(3)枚举类不需要关注线程安全、破坏单例和性能问题,因为其创建对象的时机与饿汉式单例有异曲同工之妙

总结

(1)单例模式常见的写法有两种:懒汉式、饿汉式

(2)懒汉式:在需要用到对象时才实例化对象,正确的实现方式是:Double Check + Lock,解决了并发安全和性能低下问题

(3)饿汉式:在类加载时已经创建好该单例对象,在获取单例对象时直接返回对象即可,不会存在并发安全和性能问题。

(4)在开发中如果对内存要求非常高,那么使用懒汉式写法,可以在特定时候才创建该对象;

(5)如果对内存要求不高使用饿汉式写法,因为简单不易出错,且没有任何并发安全和性能问题

(6)为了防止多线程环境下,因为指令重排序导致变量报NPE,需要在单例对象上添加 volatile 关键字防止指令重排序

(7)最优雅的实现方式是使用枚举,其代码精简,没有线程安全问题,且 Enum 类内部防止反射和反序列化时破坏单例。

设计模式超强总结

原文:https://zwmst.com/2715.html

设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。

设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性。

设计模式在项目开发的重要性已经不言而喻,下面就让我们一起来走进设计模式的主题中

首先先来认识一下为更好的理解设计模式打基础的 UML 建模

UML中类图以及类图之间的关系

统一建模语言(Unified Modeling Language,UML)是用来设计软件蓝图的可视化建模语言,1997 年被国际对象管理组织(OMG)采纳为面向对象的建模语言的国际标准。它的特点是简单、统一、图形化、能表达软件设计中的动态与静态信息。

UML 从目标系统的不同角度出发,定义了用例图、类图、对象图、状态图、活动图、时序图、协作图、构件图、部署图等 9 种图。

类、接口和类图

类(Class)是指具有相同属性、方法和关系的对象的抽象,它封装了数据和行为,是面向对象程序设计(OOP)的基础,具有封装性、继承性和多态性等三大特性。

类有类名、属性和方法,下面是一个表示类图的例子

上面的 no、name、school、age、sex 都表示的是属性,下面 display() 表示的是方法。

接口

接口(Interface)是一种特殊的类,它具有类的结构但不可被实例化,只可以被子类实现。接口一般用来设计类之间的关系

上面的手机接口有两个方法,可以表示call() 打电话,也可以表示receive() 接电话。

类图

类图(ClassDiagram)是用来显示系统中的类、接口、协作以及它们之间的静态结构和关系的一种静态模型。

**类与类之间的关系

在软件系统中,类不是孤立存在的,类与类之间存在各种关系。根据类与类之间的耦合度从弱到强排列,UML 中的类图有以下几种关系:依赖关系、关联关系、聚合关系、组合关系、泛化关系和实现关系。其中泛化和实现的耦合度相等,它们是最强的。

  • 依赖关系

依赖(Dependency)关系是一种使用关系,它是对象之间耦合度最弱的一种关联方式,是临时性的关联。在代码中,某个类的方法通过局部变量、方法的参数或者对静态方法的调用来访问另一个类(被依赖类)中的某些方法来完成一些事情。

在 UML 类图中,依赖关系使用带箭头的虚线来表示,箭头从使用类指向被依赖的类,例如下面这个例子

现代人需要依赖手机进行通讯和交流。在 UML 类图中,依赖关系使用带箭头的虚线来表示,箭头从使用类指向被依赖的类

  • 关联关系

关联(Association)关系是对象之间的一种引用关系,用于表示一类对象与另一类对象之间的联系,如老师和学生、师傅和徒弟、丈夫和妻子等。关联关系是类与类之间最常用的一种关系,分为一般关联关系、聚合关系和组合关系。

关联可以是双向的,也可以是单向的。在 UML 类图中,双向的关联可以用带两个箭头或者没有箭头的实线来表示,单向的关联用带一个箭头的实线来表示,箭头从使用类指向被关联的类。也可以在关联线的两端标注角色名,代表两种不同的角色。

例如一个老师和学生的关系是多对多的,一个老师可以教多个学生,一个学生有多个上课老师,那么一个学生同时也可以选择多门课程,学生和课程之间的关系也是多对多。

  • 聚合关系

聚合(Aggregation)关系是关联关系的一种,是强关联关系,是整体和部分之间的关系,是 has-a 的关系。

聚合关系也是通过成员对象来实现的,其中成员对象是整体对象的一部分,但是成员对象可以脱离整体对象而独立存在。例如,学校与老师的关系,学校包含老师,但如果学校停办了,老师依然存在。

在 UML 类图中,聚合关系可以用带空心菱形的实线来表示,菱形指向整体

一个学校会有多个老师,但是学校没有了,老师还依然会很存在。

  • 组合关系

组合(Composition)关系也是关联关系的一种,也表示类之间的整体与部分的关系,但它是一种更强烈的聚合关系,是 contains-a 关系。

在组合关系中,整体对象可以控制部分对象的生命周期,一旦整体对象不存在,部分对象也将不存在,部分对象不能脱离整体对象而存在。

在 UML 类图中,组合关系用带实心菱形的实线来表示,菱形指向整体

钱包和钱是一个组合的关系,钱包是放钱的口袋,钱包丢了钱也就没了,所以是一种组合关系。

  • 泛化关系

泛化(Generalization)关系是对象之间耦合度最大的一种关系,表示一般与特殊的关系,是父类与子类之间的关系,是一种继承关系,是 is-a 的关系。

在 UML 类图中,泛化关系用带空心三角箭头的实线来表示,箭头从子类指向父类。在代码实现时,使用面向对象的继承机制来实现泛化关系。例如,Student 类和 Teacher 类都是 Person 类的子类,

  • 实现关系

实现(Realization)关系是接口与实现类之间的关系。在这种关系中,类实现了接口,类中的操作实现了接口中所声明的所有的抽象操作。

在 UML 类图中,实现关系使用带空心三角箭头的虚线来表示,箭头从实现类指向接口。

比如表示一个交通工具的接口,其中有一个 move 方法,有两个类 Bike 和 Car 分别实现了 Vehicle,那么也就实现了 move() 方法

设计模式的原则

在了解完上面一个简单的UML建模后,下面来聊一下设计原则,在设计模式中有六种设计原则,它们分别是

开闭原则(Open Closed Principle)

核心思想是对扩展开放,对修改关闭。也就是说,对已经使用的类的改动通过增加代码进行的,而不是修改现有代码,实现一个热插拔的效果。

例如:你手机中的桌面主题,你无法修改已有的桌面主题,只能从网上下载新的桌面主题

单一职责原则(Single Responsiblity Principle)

单一职责原则的核心就是控制类的粒度大小、将对象解耦、提高其内聚性。其实就是开发人员经常说的”高内聚,低耦合”,每个类应该只有一个职责,对外只能提供一种功能,而引起类变化的原因应该只有一个。

比如一个班级会有很多课代表,语文课代表、数学课代表、英语课代表等等,那么课代表只应该负责班级特定学科的工作,语文课代表不能插手数学课代表和英语课代表的工作。

里式替换原则(Liskov Substitution Principle)

里式替换的原则认为子类可以扩展父类的功能,但不能改变父类原有的功能,也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。

例如鸟有一个fly() 方法,燕子是一种鸟,燕子继承鸟类并重写了 fly() 方法,企鹅是一种鸟,企鹅继承了鸟类但是企鹅不能飞,这就相当于是改变了鸟类的功能,不能说企鹅不能飞,所以鸟不能飞

依赖倒转原则(Dependency Inversion Principle)

依赖倒转原则的核心思想是:要依赖于抽象和接口,不要依赖于具体的实现。

其实就是说:在应用程序中,所有的类如果使用或依赖于其他的类,则应该依赖这些其他类的抽象类或者接口,而不是直接依赖这些其他类的具体类。为了实现这一原则,就要求我们在编程的时候针对抽象类或者接口编程,而不是针对具体实现编程。

比如女人去商场购物,她可能买很多东西,不局限于只买一个包,她还可能买鞋,化妆品等。那么我们可以针对商品建模

接口分离原则(Interface Segregation Principle)

要求程序员尽量将臃肿庞大的接口拆分成更小的和更具体的接口,让接口中只包含客户感兴趣的方法。

接口隔离原则和单一职责都是为了提高类的内聚性、降低它们之间的耦合性,体现了封装的思想,但两者是不同的:

  • 单一职责原则注重的是职责,而接口隔离原则注重的是对接口依赖的隔离。
  • 单一职责原则主要是约束类,它针对的是程序中的实现和细节;接口隔离原则主要约束接口,主要针对抽象和程序整体框架的构建。

迪米特原则(Principle of Least Knowledge)

迪米特法则又叫最少认知原则,它的核心思想是一个对象应当对其他对象尽可能少的了解

其实就是说:降低各个对象之间的耦合,提高系统的可维护性。在模块之间应该只通过接口编程,而不理会模块的内部工作原理,它可以使各个模块耦合度降到最低,促进软件的复用。

拿我们身边的朋友圈举例子,朋友圈的确定,如果两个人有一个共同的好友,那么他们的朋友圈点赞和留言也是可见的,如果没有共同好友,那么彼此不可见。

二十三种设计模式

设计模式概述

  • 设计模式(Design pattern代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。
  • 设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。
  • 设计模式是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。
  • 设计模式不是一种方法和技术,而是一种思想
  • 设计模式和具体的语言无关,学习设计模式就是要建立面向对象的思想,尽可能的面向接口编程,低耦合,高内聚,使设计的程序可复用
  • 学习设计模式能够促进对面向对象思想的理解,反之亦然。它们相辅相成。

设计模式的类型

总体来说,设计模式分为三类23种

  • 创建型(5种) :工厂模式、抽象工厂模式、单例模式、原型模式、构建者模式
  • 结构型(7种):适配器模式、装饰模式、代理模式、外观模式、桥接模式、组合模式、享元模式
  • 行为型(11种):模板方法模式、策略模式、观察者模式、中介者模式、状态模式、责任链模式、命令模式、迭代器模式、访问者模式、解释器模式、备忘录模式

带你一步步解析HTTPS

原文:https://zwmst.com/2717.html

下面我们来一起学习一下 HTTPS ,首先问你一个问题,为什么有了 HTTP 之后,还需要有 HTTPS ?我突然有个想法,为什么我们面试的时候需要回答标准答案呢?为什么我们不说出我们自己的想法和见解,却要记住一些所谓的标准回答呢?技术还有正确与否吗

HTTPS 为什么会出现

一个新技术的出现必定是为了解决某种问题的,那么 HTTPS 解决了 HTTP 的什么问题呢?

HTTPS 解决了什么问题

一个简单的回答可能会是 HTTP 它不安全。由于 HTTP 天生明文传输的特性,在 HTTP 的传输过程中,任何人都有可能从中截获、修改或者伪造请求发送,所以可以认为 HTTP 是不安全的;在 HTTP 的传输过程中不会验证通信方的身份,因此 HTTP 信息交换的双方可能会遭到伪装,也就是没有用户验证;在 HTTP 的传输过程中,接收方和发送方并不会验证报文的完整性,综上,为了结局上述问题,HTTPS 应用而生。

什么是 HTTPS

你还记得 HTTP 是怎么定义的吗?HTTP 是一种 超文本传输协议(Hypertext Transfer Protocol) 协议,它 是一个在计算机世界里专门在两点之间传输文字、图片、音频、视频等超文本数据的约定和规范,那么我们看一下 HTTPS 是如何定义的

HTTPS 的全称是 Hypertext Transfer Protocol Secure,它用来在计算机网络上的两个端系统之间进行安全的交换信息(secure communication),它相当于在 HTTP 的基础上加了一个 Secure 安全的词眼,那么我们可以给出一个 HTTPS 的定义:HTTPS 是一个在计算机世界里专门在两点之间安全的传输文字、图片、音频、视频等超文本数据的约定和规范。 HTTPS 是 HTTP 协议的一种扩展,它本身并不保传输的证安全性,那么谁来保证安全性呢?在 HTTPS 中,使用传输层安全性(TLS)安全套接字层(SSL)对通信协议进行加密。也就是 HTTP + SSL(TLS) = HTTPS。

HTTPS 做了什么

HTTPS 协议提供了三个关键的指标

  • 加密(Encryption), HTTPS 通过对数据加密来使其免受窃听者对数据的监听,这就意味着当用户在浏览网站时,没有人能够监听他和网站之间的信息交换,或者跟踪用户的活动,访问记录等,从而窃取用户信息。

  • 数据一致性(Data integrity),数据在传输的过程中不会被窃听者所修改,用户发送的数据会完整的传输到服务端,保证用户发的是什么,服务器接收的就是什么。

  • 身份认证(Authentication),是指确认对方的真实身份,也就是证明你是你(可以比作人脸识别),它可以防止中间人攻击并建立用户信任。

有了上面三个关键指标的保证,用户就可以和服务器进行安全的交换信息了。那么,既然你说了 HTTPS 的种种好处,那么我怎么知道网站是用 HTTPS 的还是 HTTP 的呢?给你两幅图应该就可以解释了。

HTTPS 协议其实非常简单,RFC 文档很小,只有短短的 7 页,里面规定了新的协议名,默认端口号443,至于其他的应答模式、报文结构、请求方法、URI、头字段、连接管理等等都完全沿用 HTTP,没有任何新的东西。

也就是说,除了协议名称和默认端口号外(HTTP 默认端口 80),HTTPS 协议在语法、语义上和 HTTP 一样,HTTP 有的,HTTPS 也照单全收。那么,HTTPS 如何做到 HTTP 所不能做到的安全性呢?关键在于这个 S 也就是 SSL/TLS

什么是 SSL/TLS

认识 SSL/TLS

TLS(Transport Layer Security)SSL(Secure Socket Layer) 的后续版本,它们是用于在互联网两台计算机之间用于身份验证加密的一种协议。

注意:在互联网中,很多名称都可以进行互换。

我们都知道一些在线业务(比如在线支付)最重要的一个步骤是创建一个值得信赖的交易环境,能够让客户安心的进行交易,SSL/TLS 就保证了这一点,SSL/TLS 通过将称为 X.509 证书的数字文档将网站和公司的实体信息绑定到加密密钥来进行工作。每一个密钥对(key pairs) 都有一个 私有密钥(private key)公有密钥(public key),私有密钥是独有的,一般位于服务器上,用于解密由公共密钥加密过的信息;公有密钥是公有的,与服务器进行交互的每个人都可以持有公有密钥,用公钥加密的信息只能由私有密钥来解密。

什么是 X.509:X.509 是公开密钥证书的标准格式,这个文档将加密密钥与(个人或组织)进行安全的关联。

X.509 主要应用如下

  • SSL/TLS 和 HTTPS 用于经过身份验证和加密的 Web 浏览
  • 通过 S/MIME 协议签名和加密的电子邮件
  • 代码签名:它指的是使用数字证书对软件应用程序进行签名以安全分发和安装的过程。

通过使用由知名公共证书颁发机构(例如SSL.com)颁发的证书对软件进行数字签名,开发人员可以向最终用户保证他们希望安装的软件是由已知且受信任的开发人员发布;并且签名后未被篡改或损害。

我们后面还会讨论。

HTTPS 的内核是 HTTP

HTTPS 并不是一项新的应用层协议,只是 HTTP 通信接口部分由 SSL 和 TLS 替代而已。通常情况下,HTTP 会先直接和 TCP 进行通信。在使用 SSL 的 HTTPS 后,则会先演变为和 SSL 进行通信,然后再由 SSL 和 TCP 进行通信。也就是说,HTTPS 就是身披了一层 SSL 的 HTTP。(我都喜欢把骚粉留在最后。。。)

SSL 是一个独立的协议,不只有 HTTP 可以使用,其他应用层协议也可以使用,比如 SMTP(电子邮件协议)Telnet(远程登录协议) 等都可以使用。

探究 HTTPS

我说,你起这么牛逼的名字干嘛,还想吹牛批?你 HTTPS 不就抱上了 TLS/SSL 的大腿么,咋这么牛批哄哄的,还想探究 HTTPS,瞎胡闹,赶紧改成 TLS 是我主,赞美我主。

SSL 即安全套接字层,它在 OSI 七层网络模型中处于第五层,SSL 在 1999 年被 IETF(互联网工程组)更名为 TLS ,即传输安全层,直到现在,TLS 一共出现过三个版本,1.1、1.2 和 1.3 ,目前最广泛使用的是 1.2,所以接下来的探讨都是基于 TLS 1.2 的版本上的。

TLS 用于两个通信应用程序之间提供保密性和数据完整性。TLS 由记录协议、握手协议、警告协议、变更密码规范协议、扩展协议等几个子协议组成,综合使用了对称加密、非对称加密、身份认证等许多密码学前沿技术(如果你觉得一项技术很简单,那你只是没有学到位,任何技术都是有美感的,牛逼的人只是欣赏,并不是贬低)。

说了这么半天,我们还没有看到 TLS 的命名规范呢,下面举一个 TLS 例子来看一下 TLS 的结构(可以参考 https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml

ECDHE-ECDSA-AES256-GCM-SHA384

这是啥意思呢?我刚开始看也有点懵啊,但其实是有套路的,因为 TLS 的密码套件比较规范,基本格式就是 密钥交换算法 – 签名算法 – 对称加密算法 – 摘要算法 组成的一个密码串,有时候还有分组模式,我们先来看一下刚刚是什么意思

使用 ECDHE 进行密钥交换,使用 ECDSA 进行签名和认证,然后使用 AES 作为对称加密算法,密钥的长度是 256 位,使用 GCM 作为分组模式,最后使用 SHA384 作为摘要算法。

TLS 在根本上使用对称加密非对称加密 两种形式。

对称加密

在了解对称加密前,我们先来了解一下密码学的东西,在密码学中,有几个概念:**明文、密文、加密、解密

  • 明文(Plaintext),一般认为明文是有意义的字符或者比特集,或者是通过某种公开编码就能获得的消息。明文通常用 m 或 p 表示
  • 密文(Ciphertext),对明文进行某种加密后就变成了密文
  • 加密(Encrypt),把原始的信息(明文)转换为密文的信息变换过程
  • 解密(Decrypt),把已经加密的信息恢复成明文的过程。

对称加密(Symmetrical Encryption)顾名思义就是指加密和解密时使用的密钥都是同样的密钥。只要保证了密钥的安全性,那么整个通信过程也就是具有了机密性。

TLS 里面有比较多的加密算法可供使用,比如 DES、3DES、AES、ChaCha20、TDEA、Blowfish、RC2、RC4、RC5、IDEA、SKIPJACK 等。目前最常用的是 AES-128, AES-192、AES-256 和 ChaCha20。

DES 的全称是 Data Encryption Standard(数据加密标准) ,它是用于数字数据加密的对称密钥算法。尽管其 56 位的短密钥长度使它对于现代应用程序来说太不安全了,但它在加密技术的发展中具有很大的影响力。

3DES 是从原始数据加密标准(DES)衍生过来的加密算法,它在 90 年代后变得很重要,但是后面由于更加高级的算法出现,3DES 变得不再重要。

AES-128, AES-192 和 AES-256 都是属于 AES ,AES 的全称是Advanced Encryption Standard(高级加密标准),它是 DES 算法的替代者,安全强度很高,性能也很好,是应用最广泛的对称加密算法。

ChaCha20 是 Google 设计的另一种加密算法,密钥长度固定为 256 位,纯软件运行性能要超过 AES,曾经在移动客户端上比较流行,但 ARMv8 之后也加入了 AES 硬件优化,所以现在不再具有明显的优势,但仍然算得上是一个不错算法。

(其他可自行搜索)

加密分组

对称加密算法还有一个分组模式 的概念,对于 GCM 分组模式,只有和 AES,CAMELLIA 和 ARIA 搭配使用,而 AES 显然是最受欢迎和部署最广泛的选择,它可以让算法用固定长度的密钥加密任意长度的明文。

最早有 ECB、CBC、CFB、OFB 等几种分组模式,但都陆续被发现有安全漏洞,所以现在基本都不怎么用了。最新的分组模式被称为 AEAD(Authenticated Encryption with Associated Data),在加密的同时增加了认证的功能,常用的是 GCM、CCM 和 Poly1305。

比如 ECDHE_ECDSA_AES128_GCM_SHA256 ,表示的是具有 128 位密钥, AES256 将表示 256 位密钥。GCM 表示具有 128 位块的分组密码的现代认证的关联数据加密(AEAD)操作模式。

我们上面谈到了对称加密,对称加密的加密方和解密方都使用同一个密钥,也就是说,加密方必须对原始数据进行加密,然后再把密钥交给解密方进行解密,然后才能解密数据,这就会造成什么问题?这就好比《小兵张嘎》去送信(信已经被加密过),但是嘎子还拿着解密的密码,那嘎子要是在途中被鬼子发现了,那这信可就是被完全的暴露了。所以,对称加密存在风险。

非对称加密

非对称加密(Asymmetrical Encryption) 也被称为公钥加密,相对于对称加密来说,非对称加密是一种新的改良加密方式。密钥通过网络传输交换,它能够确保及时密钥被拦截,也不会暴露数据信息。非对称加密中有两个密钥,一个是公钥,一个是私钥,公钥进行加密,私钥进行解密。公开密钥可供任何人使用,私钥只有你自己能够知道。

使用公钥加密的文本只能使用私钥解密,同时,使用私钥加密的文本也可以使用公钥解密。公钥不需要具有安全性,因为公钥需要在网络间进行传输,非对称加密可以解决密钥交换的问题。网站保管私钥,在网上任意分发公钥,你想要登录网站只要用公钥加密就行了,密文只能由私钥持有者才能解密。而黑客因为没有私钥,所以就无法破解密文。

非对称加密算法的设计要比对称算法难得多(我们不会探讨具体的加密方式),常见的比如 DH、DSA、RSA、ECC 等。

其中 RSA 加密算法是最重要的、最出名的一个了。例如 DHE_RSA_CAMELLIA128_GCM_SHA256。它的安全性基于 整数分解,使用两个超大素数的乘积作为生成密钥的材料,想要从公钥推算出私钥是非常困难的。

ECC(Elliptic Curve Cryptography)也是非对称加密算法的一种,它基于椭圆曲线离散对数的数学难题,使用特定的曲线方程和基点生成公钥和私钥, ECDHE 用于密钥交换,ECDSA 用于数字签名。

TLS 是使用对称加密非对称加密 的混合加密方式来实现机密性。

混合加密

RSA 的运算速度非常慢,而 AES 的加密速度比较快,而 TLS 正是使用了这种混合加密方式。在通信刚开始的时候使用非对称算法,比如 RSA、ECDHE ,首先解决密钥交换的问题。然后用随机数产生对称算法使用的会话密钥(session key),再用公钥加密。对方拿到密文后用私钥解密,取出会话密钥。这样,双方就实现了对称密钥的安全交换。

现在我们使用混合加密的方式实现了机密性,是不是就能够安全的传输数据了呢?还不够,在机密性的基础上还要加上完整性身份认证的特性,才能实现真正的安全。而实现完整性的主要手段是 摘要算法(Digest Algorithm)

摘要算法

如何实现完整性呢?在 TLS 中,实现完整性的手段主要是 摘要算法(Digest Algorithm)。摘要算法你不清楚的话,MD5 你应该清楚,MD5 的全称是 Message Digest Algorithm 5,它是属于密码哈希算法(cryptographic hash algorithm)的一种,MD5 可用于从任意长度的字符串创建 128 位字符串值。尽管 MD5 存在不安全因素,但是仍然沿用至今。MD5 最常用于验证文件的完整性。但是,它还用于其他安全协议和应用程序中,例如 SSH、SSL 和 IPSec。一些应用程序通过向明文加盐值或多次应用哈希函数来增强 MD5 算法。

什么是加盐?在密码学中,就是一项随机数据,用作哈希数据,密码或密码的单向函数的附加输入。盐用于保护存储中的密码。例如

什么是单向?就是在说这种算法没有密钥可以进行解密,只能进行单向加密,加密后的数据无法解密,不能逆推出原文。

我们再回到摘要算法的讨论上来,其实你可以把摘要算法理解成一种特殊的压缩算法,它能够把任意长度的数据压缩成一种固定长度的字符串,这就好像是给数据加了一把锁。

除了常用的 MD5 是加密算法外,SHA-1(Secure Hash Algorithm 1) 也是一种常用的加密算法,不过 SHA-1 也是不安全的加密算法,在 TLS 里面被禁止使用。目前 TLS 推荐使用的是 SHA-1 的后继者:SHA-2

SHA-2 的全称是Secure Hash Algorithm 2 ,它在 2001 年被推出,它在 SHA-1 的基础上做了重大的修改,SHA-2 系列包含六个哈希函数,其摘要(哈希值)分别为 224、256、384 或 512 位:SHA-224, SHA-256, SHA-384, SHA-512。分别能够生成 28 字节、32 字节、48 字节、64 字节的摘要。

有了 SHA-2 的保护,就能够实现数据的完整性,哪怕你在文件中改变一个标点符号,增加一个空格,生成的文件摘要也会完全不同,不过 SHA-2 是基于明文的加密方式,还是不够安全,那应该用什么呢?

安全性更高的加密方式是使用 HMAC,在理解什么是 HMAC 前,你需要先知道一下什么是 MAC。

MAC 的全称是message authentication code,它通过 MAC 算法从消息和密钥生成,MAC 值允许验证者(也拥有秘密密钥)检测到消息内容的任何更改,从而保护了消息的数据完整性。

HMAC 是 MAC 更进一步的拓展,它是使用 MAC 值 + Hash 值的组合方式,HMAC 的计算中可以使用任何加密哈希函数,例如 SHA-256 等。

现在我们又解决了完整性的问题,那么就只剩下一个问题了,那就是认证,认证怎么做的呢?我们再向服务器发送数据的过程中,黑客(攻击者)有可能伪装成任何一方来窃取信息。它可以伪装成你,来向服务器发送信息,也可以伪装称为服务器,接受你发送的信息。那么怎么解决这个问题呢?

认证

如何确定你自己的唯一性呢?我们在上面的叙述过程中出现过公钥加密,私钥解密的这个概念。提到的私钥只有你一个人所有,能够辨别唯一性,所以我们可以把顺序调换一下,变成私钥加密,公钥解密。使用私钥再加上摘要算法,就能够实现数字签名,从而实现认证。

到现在,综合使用对称加密、非对称加密和摘要算法,我们已经实现了加密、数据认证、认证,那么是不是就安全了呢?非也,这里还存在一个数字签名的认证问题。因为私钥是是自己的,公钥是谁都可以发布,所以必须发布经过认证的公钥,才能解决公钥的信任问题。

所以引入了 CA,CA 的全称是 Certificate Authority,证书认证机构,你必须让 CA 颁布具有认证过的公钥,才能解决公钥的信任问题。

全世界具有认证的 CA 就几家,分别颁布了 DV、OV、EV 三种,区别在于可信程度。DV 是最低的,只是域名级别的可信,EV 是最高的,经过了法律和审计的严格核查,可以证明网站拥有者的身份(在浏览器地址栏会显示出公司的名字,例如 Apple、GitHub 的网站)。不同的信任等级的机构一起形成了层级关系。

通常情况下,数字证书的申请人将生成由私钥和公钥以及证书签名请求(CSR)组成的密钥对。CSR是一个编码的文本文件,其中包含公钥和其他将包含在证书中的信息(例如域名,组织,电子邮件地址等)。密钥对和 CSR生成通常在将要安装证书的服务器上完成,并且 CSR 中包含的信息类型取决于证书的验证级别。与公钥不同,申请人的私钥是安全的,永远不要向 CA(或其他任何人)展示。

生成 CSR 后,申请人将其发送给 CA,CA 会验证其包含的信息是否正确,如果正确,则使用颁发的私钥对证书进行数字签名,然后将其发送给申请人。

总结

本篇文章我们主要讲述了 HTTPS 为什么会出现 ,HTTPS 解决了 HTTP 的什么问题,HTTPS 和 HTTP 的关系是什么,TLS 和 SSL 是什么,TLS 和 SSL 解决了什么问题?如何实现一个真正安全的数据传输?

HTTP核心概念

原文:https://zwmst.com/2719.html

上一篇文章我们大致讲解了一下 HTTP 的基本特征和使用,大家反响很不错,那么本篇文章我们就来深究一下 HTTP 的特性。我们接着上篇文章没有说完的 HTTP 标头继续来介绍(此篇文章会介绍所有标头的概念,但没有深入底层)

HTTP 标头

先来回顾一下 HTTP1.1 标头都有哪几种

HTTP 1.1 的标头主要分为四种,通用标头实体标头请求标头响应标头,现在我们来对这几种标头进行介绍

通用标头

HTTP 通用标头之所以这样命名,是因为与其他三个类别不同,它们不是限定于特定种类的消息或者消息组件(请求,响应或消息实体)的。HTTP 通用标头主要用于传达有关消息本身的信息,而不是它所携带的内容。它们提供一般信息并控制如何处理和处理消息。

尽管通用标头不会限定于是请求还是响应报文,但是某些通用标头大部分或全部用于一种特定类型的请求中。也就是说,如果某个通用标头出现在请求报文中,那么大部分通用标头都会显示在该请求报文中。响应报文也是一样的。

先列出来一个清单,讲明我们都需要介绍哪些通用标头

  • Cache-Control
  • Connection
  • Date
  • Pragma
  • Trailer
  • Transfer-Encoding
  • Upgrade
  • Via
  • Warning

Cache-Control

缓存(Cache)是计算机领域里的一个重要概念,是优化系统性能的利器。不仅计算机中的 CPU 为了提高指令执行效率从而选择使用寄存器作为辅助,计算机网络同样存在缓存,下面我们就来介绍一下计算机网络中的缓存。

Cache-Control 是通用标头的指令,它能够管理如何对 HTTP 的请求或者响应使用缓存。

因为计算机网络中是可以有第三者出现的,也就是缓存服务器,这个指令通过影响请求/响应中的缓存服务器从而达到控制缓存的目的;不仅有缓存服务器,还有浏览器内部缓存也会影响链路的缓存。

这个标头中可以出现许多单独的指令,其详细信息可以在 RFC 2616 中找到,即使这是常规标头,某些指令也只能出现在请求或响应中。下表提供了一个 Cache-Control 选项的总结并告诉你如何去使用

请注意,在 Cache-Control 标头中只能出现一个指令,但是在消息中可以出现多个这样的标头。

上面这个表格其实会有四种分类

  • 可缓存性: 它们分别是 no-cacheno-storeprivatepublic
  • 缓存有效性时间: 它们分别是 max-ages-maxagemax-stalemin-fresh
  • 重新验证并重新加载: 它们分别是 must-revalidateproxy-revalidate
  • 其他: 它们分别是 only-if-cachedno-transform

分别对表格中的内容进行一下详细介绍

**no-cache

no-cache 很容易和 no-store 混淆,一般都会把 no-cache 认为是不缓存,其实不是这样。

使用 no-cache 指令的目的是为了防止从缓存中返回过期的资源,例如下图所示

Cache-Control: no-cache

举个例子你就明白了,No-Cache 就相当于是吃着碗里的,占着锅里的,如果锅里还有新的肉片,就先吃锅里的,如果锅里没有新的,再吃自己的,这里锅里的就相当于是源服务器产生的,碗里的就相当于是缓存的。

**no-store

no-store 才是真正意义上的不缓存,每次服务器接受到客户端的请求后,都会返回最新的资源给客户端。

Cache-Control: no-store

**max-age

max-age 可以用在请求或者响应中,当客户端发送带有 max-age 的指令时,缓存服务器会判断自己缓存时间的数值和 max-age 的大小,如果比 max-age 小,那么缓存有效,可以继续给客户端返回缓存的数据,如果比 max-age 大,那么缓存服务器将不能返回给客户端缓存的数据。

Cache-Control: max-age=60

如果 max-age = 0,那么缓存服务器将会直接把请求转发到服务器

Cache-Control: max-age=0

注意:这个 max-age 的值是相对于请求时间的

**must-revalidate

表示一旦资源过期,缓存就必须在原始服务器上没有成功验证的情况下才使用其过期的数据。

Cache-Control: must-revalidate

no-storeno_cachemust-revalidatemax-age 可以一起看,下面是一个这四个标头的流程图

**public

public 属性只出现在客户端响应中,表示响应可以被任何缓存所缓存。在计算机网络中,分为两种缓存,共享缓存和私有缓存,如下所示

Cache-Control: public

**private

当指定 private 指令后,响应只以特定的用户作为对象,这与 public 的用法相反,缓存服务器只对特定的客户端进行缓存,其他客户端发送过来的请求,缓存服务器则不会返回缓存。

Cache-Control: private

**s-maxage

s-maxage 指令的功能和 max-age 指令的功能相同,不同点之处在于 s-maxage 不能用于私有缓存,只能用于多用户使用的公共服务器,对于同一用户的重复请求和响应来说,这个指令没有任何作用。

Cache-Control: s-maxage=60

**min-fresh

min-fresh只能出现在请求中,min-fresh 要求缓存服务器返回 min-fresh 时间内的缓存数据。例如 Cache-Control:min-fresh=60,这就要求缓存服务器发送60秒内的数据。

Cache-Control: min-fresh=60

**max-stable

max-stable 只能出现在请求中,表示客户端会接受缓存数据,即使过期也照常接收。

Cache-Control: max-stable=60

**only-if-cached

这个标头只能出现在请求中,使用 only-if-cached 指令表示客户端仅在缓存服务器本地缓存目标资源的情况下才会要求其返回。

Cache-Control: only-if-cached

**proxy-revalidate

proxy-revalidate 指令要求所有的缓存服务器在接收到客户端带有该指令的请求返回响应之前,必须再次验证缓存的有效性。

Cache-Control: proxy-revalidate

**no-transform

使用 no-transform 指令规定无论是在请求还是响应中,缓存都不能改变实体主体的媒体类型。

Cache-Control: no-transform

Connection

HTTP 协议使用 TCP 来管理连接方式,主要有两种连接方式,持久性连接非持久性连接

**持久性连接

持久性连接指的是一次会话完成后,TCP 连接并未关闭,第二次再次发送请求后,就不再需要建立 TCP 连接,而是可以直接进行请求和响应。它的一般表示形式如下

Connection: keep-alive

从 HTTP 1.1 开始,默认使用持久性连接

keep-alive 也是一个通用标头,一般 Connection 都会和 keep-alive 一起使用,keep-alive 有两个参数,一个是 timeout;另一个是 max,它们的主要表现形式如下

Connection: Keep-Alive
Keep-Alive: timeout=5, max=1000
  • timeout: 指的是空闲连接必须打开的最短时间,也就是说这次请求的连接时间不能少于5秒,

  • max: 指的是在连接关闭之前服务器所能够收到的最大请求数。

**非持久性连接

非持久性连接表示一次会话请求/响应后关闭连接的方式。HTTP 1.1 之前使用的连接都是非持久连接,也就是

Connection: close

Date

Date 是一个通用标头,它可以出现在请求标头和响应标头中,它的基本表示如下

Date: Wed, 21 Oct 2015 07:28:00 GMT 

表示的是格林威治标准时间,这个时间要比北京时间慢八个小时

Pragma

Pragma是 http 1.1 之前版本的历史遗留字段,仅作为与 http 的向后兼容而定义。它的一般形式如下

Pragma: no-cache

只用于客户端发送的请求中。客户端会要求所有的中间服务器不返回缓存的资源。

如果所有的中间服务器都以实现 HTTP /1.1为标准,那么直接使用 Cache-Control: no-cache 即可,如果不是的话,就要包含两个字段,如下

Cache-Control: no-cache
Pragma: no-cache

Trailer

首部字段 Trailer 会事先说明在报文主体后记录了哪些首部字段。该首部字段可应用在 HTTP/1.1 版本分块传输编码时。一般用法如下

Transfer-Encoding: chunked
Trailer: Expires

以上用例中,指定首部字段 Trailer 的值为 Expires,在报文主体之后(分块长度 0 之后)出现了首部字段 Expires。

Transfer-Encoding

Transfer-Encoding 属于内容协商的范畴,下面会具体介绍一下内容协商,现在先做个预告:Transfer-Encoding 规定了传输报文所采用的编码方式

注意:HTTP 1.1 的传输编码方式仅对分块传输有效,但是 HTTP 2.0 就不再支持分块传输,而提供了自己更有效的数据传输机制。

Transfer-Encoding: chunked

Transfer-Encoding 也属于 Hop-by-hop(逐跳) 首部 ,下面来回顾一下,HTTP 报文标头除了可以根据属性所在的位置分为 通用标头请求标头响应标头实体标头;还可以按照是否被缓存分为 端到端首部(End-to-End)逐跳首部(Top-to-Top)

除了下面八种属于逐跳首部外,其余都属于端到端首部

**Connection、Keep-Alive、Proxy-Authenticate、Proxy-Authorization、Trailer、TE、Transfer-Encoding、Upgrade

下面回到讨论中来,Transfer-Encoding 用于两个节点之间传输消息,而不是资源本身。在多个节点传输消息的过程中,每一段消息的传输都可以使用不同的 Transfer-Encoding。如图所示

Transfer-Encoding 支持文件压缩,如果你想要以文件压缩后的形式发送的话。Transfer-Encoding 所有可选类型如下

  • chunked: 数据按照一系列块发送,在这种情况下,将省略 Content-Length 标头,并在每个块的开头,需要以十六进制填充当前块的长度,后跟 '\r\n',然后是块本身,然后是另一个'\r\n'。当将大量数据发送到客户端并且在请求已被完全处理之前,可能无法知道响应的总大小时,分块编码很有用。 例如,在生成由数据库查询产生的大型 HTML 表时或在传输大型图像时。 分块的响应看起来像这样
HTTP/1.1 200 OK 
Content-Type: text/plain 
Transfer-Encoding: chunked

7\r\n
Mozilla\r\n 
9\r\n
Developer\r\n
7\r\n
Network\r\n
0\r\n 
\r\n

终止块通常是0。紧随Transfer-Encoding 后面的是 Trailer 标头, Trailer 可能为空。

  • compress: 使用 Lempel-Ziv-Welch(LZW) 算法的格式。值名称取自 UNIX 压缩程序,该程序实现了该算法。现在几乎没有浏览器使用这种内容编码了,因为这个专利在 2003 年就停掉了。
  • deflate:使用 zlib(在 RFC 1950 定义) 结构和 deflate 压缩算法
  • gzip: 使用Lempel-Ziv编码(LZ77)和32位CRC的格式。这最初是 UNIX gzip 程序的格式。HTTP / 1.1标准还建议出于兼容性目的,支持此内容编码的服务器应将 x-gzip 识别为别名。
  • identity: 使用身份功能(即无压缩或修改)。

也可以列出多个值,以逗号分隔,类似一个集合列表

Transfer-Encoding: gzip, chunked

Upgrade

首部字段 Upgrade 用于检测 HTTP 协议及其他协议是否可使用更高的版本进行通信,其参数值可以用来指定一个完全不同的通信协议。

上图用例中,首部字段 Upgrade 指定的值为 TLS/1.0。请注意此处两个字段首部字段的对应关系,Connection 的值被指定为 Upgrade。 Upgrade 首部字段产生作用的对象仅限于客户端和临近服务器之间。因此,使用首部字段 Upgrade 时,还需要额外指定 Connection: Upgrade。 对于附有首部字段 Upgrade 的请求,服务器可用 101 Switching Protocols 状态码作为响应返回。

Via

使用 Via 是为了跟踪客户端和服务器之间的请求/响应路径,避免请求循环以及能够识别请求/响应链中发送者协议的功能。Via 字段由代理服务器添加,不论是正向代理还是反向代理,并且可以出现在请求标头和响应标头中。它用于跟踪消息转发。例如下图所示

Via 后面的的 1.1, 1.0 表示接收服务器上的 HTTP 版本,Via 首部是为了跟踪路径,经常和 TRACE 方法一起使用。

Warning

注意:Warning 字段即将被弃用

查阅 Warning (https://github.com/httpwg/http-core/issues/139) and Warning: header & stale-while-revalidate (https://github.com/whatwg/fetch/issues/913) 获取更多细节

Warning 通用 HTTP 标头通常会告知用户一些与缓存相关的问题的警告

HTTP/1.1 中定义了 7 种警告。它们分别如下

请求标头

请求标头用于客户端发送 HTTP 请求到服务器中所使用的字段,下面我们一起来看一下 HTTP 请求标头都包含哪些字段,分别是什么意思。下面会介绍

  • Accept
  • Accept-Charset
  • Accept-Encoding
  • Accept-Language
  • Authorization
  • Expect
  • From
  • Host
  • If-Match
  • If-Modified-Since
  • If-None-Match
  • If-Range
  • If-Unmodified-Since
  • Max-Forwards
  • Proxy-Authorization
  • RangeReferer
  • TE
  • User-Agent

下面分别来介绍一下

Accept

HTTP 请求标头会告知客户端能够接收的 MIME 类型是什么

那么什么是 MIME 类型呢?在回答这个问题前你应该先了解一下什么是 MIME

MIME: MIME (Multipurpose Internet Mail Extensions) 是描述消息内容类型的因特网标准。MIME 消息能包含文本、图像、音频、视频以及其他应用程序专用的数据。

也就是说,MIME 类型其实就是一系列消息内容类型的集合。那么 MIME 类型都有哪些呢?

文本文件: text/html、text/plain、text/css、application/xhtml+xml、application/xml

图片文件: image/jpeg、image/gif、image/png

视频文件: video/mpeg、video/quicktime

应用程序二进制文件: application/octet-stream、application/zip

比如,如果浏览器不支持 PNG 图片的显示,那 Accept 就不指定image/png,而指定可处理的 image/gif 和 image/jpeg 等图片类型。

一般 MIME 类型也会和 q 这个属性一起使用,q 是什么?q 表示的是权重,来看一个例子

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

这是什么意思呢?若想要给显示的媒体类型增加优先级,则使用 q= 来额外表示权重值,没有显示权重的时候默认值是1.0 ,我给你列个表格你就明白了

q MIME
1.0 text/html
1.0 application/xhtml+xml
0.9 application/xml
0.8 /

也就是说,这是一个放置顺序,权重高的在前,低的在后,application/xml;q=0.9 是不可分割的整体。

Accept-Charset

Accept-Charset 表示客户端能够接受的字符编码。Accept-Charset 也是属于内容协商的一部分,它和

Accept 一样,也可以用 q 来表示字符集,用逗号进行分割,例如

Accept-Charset: iso-8859-1
Accept-Charset: utf-8, iso-8859-1;q=0.5
Accept-Charset: utf-8, iso-8859-1;q=0.5, *;q=0.1

事实上,很多以 Accept-* 开头的标头,都是属于内容协商的范畴,关于内容协商我们下面会说。

Accept-Encoding

表示 HTTP 标头会标明客户端希望服务端返回的内容编码,这通常是一种压缩算法。Accept-Encoding 也是属于内容协商 的一部分,使用并通过客户端选择 Content-Encoding 内容进行返回。

即使客户端和服务器都能够支持相同的压缩算法,服务器也可能选择不压缩并返回,这种情况可能是由于这两种情况造成的:

  • 要发送的数据已经被压缩了一次,第二次压缩并不会导致发送的数据更小
  • 服务器过载,无法承受压缩带来的性能开销,通常,如果服务器使用 CPU 超过 80% ,Microsoft 则建议不要使用压缩

下面是 Accept-Encoding 的使用方式

Accept-Encoding: gzip
Accept-Encoding: compress
Accept-Encoding: deflate
Accept-Encoding: br
Accept-Encoding: identity
Accept-Encoding: 
Accept-Encoding: deflate, gzip;q=1.0, *;q=0.5

上面的几种表述方式就已经把 Accept-Encoding 的属性列全了

Accept-Language

Accept-Language 请求表示客户端需要服务端返回的语言类型,Accept-Language 也属于内容协商的范畴。服务端通过 Content-Language 进行响应,和 Accept 首部字段一样,按权重值 q 来表示相对优先级。例如

Accept-Language: de
Accept-Language: de-CH
Accept-Language: en-US,en;q=0.5

Authorization

HTTP Authorization 请求头用于向服务器认证用户代理的凭据,通常用在服务器以401未经授权状态和WWW-Authenticate标头响应之后,啥意思呢?你不明白的话我画张图给你看

请求标头 Authorization 是用来告知服务器,用户的认证信息,服务器在只有收到认证后才会返回给客户端 200 OK 的响应,如果没有认证信息,则会返回 401 并告知客户端需要认证信息。详细关于 Authorization 的信息,后面也会详细解释

Expect

Expect HTTP 请求标头指示服务器需要满足的期望才能正确处理请求。如果服务器没有办法完成客服端所期望完成的事情并且服务端存在错误的话,会返回 417 Expectation Failed 。HTTP 1.1 只规定了100-continue

  • 如果服务器能正常完成客户端所期望的事情,会返回 100
  • 如果不能满足期望或返回任何其他4xx 的状态码,会返回 417

例如

PUT /somewhere/fun HTTP/1.1
Host: origin.example.com
Content-Type: video/h264
Content-Length: 1234567890987
Expect: 100-continue

From

From 请求头用来告知服务器使用用户代理的电子邮件地址。通常情况下,其使用目的就是为了显示搜索引擎等用户代理的负责人的电子邮件联系方式。我们在使用代理的情况下,应尽可能包含 From 首部字段。例如

From: webmaster@example.org

你不应该将 From 用在访问控制或者身份验证中

Host

Host 请求头指明了服务器的域名(对于虚拟主机来说),以及(可选的)服务器监听的TCP端口号。如果没有给定端口号,会自动使用被请求服务的默认端口(比如请求一个 HTTP 的 URL 会自动使用80作为端口)。

Host: developer.mozilla.org

Host 首部字段在 HTTP/1.1 规范内是唯一一个必须被包含在请求内的首部字段。

If-Match

If-Match 后面可以跟一大堆属性,形式像 If-Match 这种的请求头称为条件请求,服务器接收到条件请求后,需要判定条件请求是否满足,只有条件请求为真,才会执行条件请求

类似的还有 **If-Match、If-Modified-Since、If-None-Match、If-Range、If-Unmodified-Since

对于 GETPOST 方法,服务器仅在与列出的 ETag(响应标头) 之一匹配时才返回请求的资源。这里又多了一个新词 ETag,我们稍后再说 ETag 的用法。对于像是 PUT 和其他非安全的方法,在这种情况下,它仅仅将上传资源。

下面是两种常见的案例

  • 对于 GETPOST 方法,会结合使用 Range 标头,它可以确保新发送请求的范围与上一个请求的资源相同,如果不匹配的话,会返回 416 响应。
  • 对于其他方法,特别是 PUT 方法,If-Match 可以防止丢失更新,服务器会比对 If-Match 的字段值和资源的 ETag 值,仅当两者一致时,才会执行请求。反之,则返回状态码 412 Precondition Failed 的响应。例如
If-Match: "bfc13a64729c4290ef5b2c2730249c88ca92d82d"
If-Match: 

If-Modified-Since

If-Modified-Since 是 HTTP 条件请求的一部分,只有在给定日期之后,服务端修改了请求所需要的资源,才会返回 200 OK 的响应。如果在给定日期之后,服务端没有修改内容,响应会返回 304 并且不带任何响应体。If-Modified-Since 只能使用 GETHEAD 请求。

If-Modified-Since 与 If-None-Match 结合使用时,它将被忽略,除非服务器不支持 If-None-Match。一般表示如下

If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT 

注意:这是格林威治标准时间。 HTTP 日期始终以格林尼治标准时间表示,而不是本地时间。

If-None-Match

条件请求,它与 If-Match 的作用相反,仅当 If-None-Match 的字段值与 ETag 值不一致时,可处理该请求。对于GETHEAD ,仅当服务器没有与给定资源匹配的 ETag 时,服务器将返回 200 作为响应。对于其他方法,仅当最终现有资源的 ETag 与列出的任何值都不匹配时,才会处理请求。

GETPOST 发送的 If-None-MatchETag 匹配时,服务器会返回 304

If-None-Match: "bfc13a64729c4290ef5b2c2730249c88ca92d82d"
If-None-Match: W/"67ab43", "54ed21", "7892dd"
If-None-Match: 

有同学可能会好奇 W/ 是什么意思,这其实是 ETag 的弱匹配,关于 ETag 我们会在响应标头中详细讲述。

If-Range

If-Range 也是条件请求,如果满足条件(If-Range 的值和 ETag 值或者更新的日期时间一致),则会发出范围请求,否则将会返回全部资源。它的一般表示如下

If-Range: Wed, 21 Oct 2015 07:28:00 GMT 

If-Unmodified-Since

If-Unmodified-Since HTTP 请求标头也是一个条件请求,服务器只有在给定日期之后没有对其进行修改时,服务器才返回请求资源。如果在指定日期时间后发生了更新,则以状态码 412 Precondition Failed 作为响应返回。

If-Unmodified-Since: Wed, 21 Oct 2015 07:28:00 GMT 

Max-Forwards

MDN 把这个标头置灰了,所以下面内容取自《图解 HTTP》

Max-Forwards 一般用于 TRACEOPTION 方法,发送包含 Max-Forwards 的首部字段时,每经过一个服务器,Max-Forwards 的值就会 -1,直到 Max-Forwards 为0时返回。Max-Forwards 是一个十进制的整数值。

Max-Forwards: 10

可以灵活使用首部字段 Max-Forwards,针对以上问题产生的原因展开调查。由于当 Max-Forwards 字段值为 0 时,服务器就会立即返回响应,由此我们至少可以对以那台服务器为终点的传输路径的通信状况有所把握。

Proxy-Authorization

Proxy-Authorization 是属于请求与认证的范畴,我们在上面提到一个认证的 HTTP 标头是 Authorization,不同于 Authorization 发生在客户端 – 服务器之间;Proxy-Authorization 发生在代理服务器和客户端之间。它表示接收到从代理服务器发来的认证时,客户端会发送包含首部字段 Proxy-Authorization 的请求,以告知服务器认证所需要的信息。

Proxy-Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l

Range

Range HTTP 请求标头指示服务器应返回文档指定部分的资源,可以一次请求一个 Range 来返回多个部分,服务器会将这些资源返回各个文档中。如果服务器成功返回,那么将返回 206 响应;如果 Range 范围无效,服务器返回416 Range Not Satisfiable错误;服务器还可以忽略 Range 标头,并且返回 200 作为响应。

Range: bytes=200-1000, 2000-6576, 19000-

Referer

HTTP Referer 属性是请求标头的一部分,当浏览器向 web 服务器发送请求的时候,一般会带上 Referer,告诉服务器该网页是从哪个页面链接过来的,服务器因此可以获得一些信息用于处理。

Referer: https://developer.mozilla.org/testpage.html

TE

首部字段 TE 会告知服务器客户端能够处理响应的传输编码方式及相对优先级。它和首部字段 Accept-Encoding 的功能很相像,但是用于传输编码。

TE: gzip, deflate;q=0.5

首部字段 TE 除指定传输编码之外,还可以指定伴随 trailer 字段的分块传输编码的方式。应用后者时,只需把 trailers 赋值给该字段值。

TE: trailers, deflate;q=0.5

User-Agent

首部字段 User-Agent 会将创建请求的浏览器和用户代理名称等信息传达给服务器。

Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0

响应标头

刚刚我们的着重点一直放在客户端请求,现在我们把关注点转换一下放在服务器上。响应首部字段是由服务器发送给客户端响应中所包含的字段,用于补充相应信息等,这部分标头也是非常多,我们先一起来看一下

  • Accept-Ranges
  • Age
  • ETag
  • Location
  • Proxy-Authenticate
  • Retry-After
  • Server
  • Vary
  • www-Authenticate

Accept-Ranges

Accept-Ranges HTTP 响应标头,这个标头有两个值

  • 当服务器能够处理客户端发送过来的请求时,使用bytes 来指定
  • 当服务器不能处理客户端发来的请求时,使用 none 来指定
Accept-Ranges: bytes
Accept-Ranges: none

Age

Age HTTP 响应标头告诉客户端源服务器在多久之前创建了响应,它的单位为,Age 标头通常接近于0,如果是0则可能是从源服务器获取的,如果不是表示可能是由代理服务器创建,那么 Age 的值表示的是缓存后的响应再次发起认证到认证完成的时间值。代理创建响应时必须加上首部字段 Age。一般表示如下

Age: 24

ETag

ETag 对于条件请求来说真是太重要了。因为条件请求就是根据 ETag 的值进行匹配的,下面我们就来详细了解一下。

ETag 响应头是特定版本的标识,它能够使缓存变得更高效并能够节省带宽,因为如果缓存内容未发生变更,Web 服务器则不需要重新发送完整的响应。除此之外,ETag 能够防止资源同时更新互相覆盖。

如果给定 URL 上的资源发生变更,必须生成一个新的 ETag 值,通过比较它们可以确定资源的两个表示形式是否相同。

ETag 值有两种,一种是强 ETag,一种是弱 ETag;

  • 强 ETag 值,无论实体发生多么细微的变化都会改变其值,一般的表示如下
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
  • 弱 ETag 值,弱 ETag 值只用于提示资源是否相同。只有资源发生了根本改变,产生差异时才会改变 ETag 值。这时,会在字段值最开始处附加 W/。
ETag: W/"0815"

Location

Location 响应标头表示 URL 需要重定向页面,它仅仅与 3xx(重定向)201(已创建) 状态响应一起使用。下面是一个页面重定向的过程

使用首部字段 Location 可以将响应接受方引导至某个与请求 URI 位置不同的资源。

Locationcontent-Location 是不一样的:Location 表示目标的重定向(或新创建资源的 URL)。然而 Content-Location 表示发生内容协商时用于访问资源的直接 URL,而无须进一步协商。Location 是与响应相关联的标头,而 Content-Location 与返回的实体相关联。

Location: /index.html

Proxy-Authenticate

HTTP 响应标头 Proxy-Authenticate 会定义认证方法,应该使用身份验证方法来访问代理服务器后面的资源即客户端。

它与 HTTP 客户端和服务端之间的访问认证行为相似,不同之处在于 Proxy-Authenticate 的认证双方是客户端与代理之间。它的一般表示形式如下

Proxy-Authenticate: Basic
Proxy-Authenticate: Basic realm="Access to the internal site"

Retry-After

HTTP 响应标头 Retry-After 告知客户端需要在多久之后重新发送请求,使用此标头主要有如下三种情况

  • 当发送 503(服务不可用) 响应时,这表示该服务预计无法使用多长时间。
  • 当发送 429(太多请求)响应时,这表示发出新请求之前要等待多长时间。
  • 当发送重定向的响应像是 301(永久移动),这表示在发出重定向请求之前要求用户客户端等待的最短时间。

字段值可以指定为具体的日期时间,也可以是创建响应后所持续的秒数,例如

Retry-After: Wed, 21 Oct 2015 07:28:00 GMT
Retry-After: 120

Server

服务器标头包含有关原始服务器用来处理请求的软件的信息。

应该避免使用过于冗长和详细的 Server 值,因为它们可能会泄露内部实施细节,这可能会使攻击者容易地发现并利用已知的安全漏洞。例如下面这种写法

Server: Apache/2.4.1 (Unix)

Vary

Vary HTTP 响应标头确定如何匹配请求标头,以决定是否可以使用缓存的响应,而不是从原始服务器请求一个新的响应。

Vary: User-Agent

www-Authenticate

HTTP WWW-Authenticate 响应标头定义了应用于获得对资源的访问权限的身份验证方法。WWW-Authenticate标头与401未经授权的响应一起发送。它的一般表示形式如下

WWW-Authenticate: Basic
WWW-Authenticate: Basic realm="Access to the staging site", charset="UTF-8"

Access-Control-Allow-Origin

一个返回的 HTTP 标头可能会具有 Access-Control-Allow-Origin ,Access-Control-Allow-Origin 指定一个来源,它告诉浏览器允许该来源进行资源访问。 否则-对于没有凭据的请求 *通配符,告诉浏览器允许任何源访问资源。例如,要允许源 https://mozilla.org 的代码访问资源,可以指定:

Access-Control-Allow-Origin: https://mozilla.org
Vary: Origin

如果服务器指定单个来源而不是 * 通配符的话 ,则服务器还应在 Vary 响应标头中包含 Origin ,以向客户端指示 服务器响应将根据原始请求标头的值而有所不同。

实体标头

实体标头用于HTTP请求和响应中,例如 Content-Length,Content-Language,Content-Encoding 的标头是实体标头。实体标头不局限于请求标头或者响应标头,下面例子中,Content-Length 是一个实体标头,但是却出现在了请求报文中

POST /myform.html HTTP/1.1
Host: developer.mozilla.org
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:50.0) Gecko/20100101 Firefox/50.0
Content-Length: 128

下面就来说一下实体标头都包含哪些

  • Allow
  • Content-Encoding
  • Content-Language
  • Content-Length
  • Content-Location
  • Content-MD5
  • Content-Range
  • Content-Type
  • Expires
  • Last-Modified

下面来分开说一下

Allow

HTTP 实体标头 Allow 列出了资源支持的方法集合。如果服务器响应405 Method Not Allowed状态码以指示可以使用哪些请求方法,则必须发送此标头。例如

Allow: GET, POST, HEAD

这段代码表示服务器允许支持 GETPOSTHEAD 方法。当服务器接收到不支持的 HTTP 方法时,会以状态码 405 Method Not Allowed 作为响应返回。

Content-Encoding

我们上面讲过 Accept-Encoding 是客户端希望服务端返回的内容编码,但是实际上服务端返回给客户端的内容编码实际上是通过 Content-Encoding 返回的。内容编码是指在不丢失实体信息的前提下所进行的压缩。主要也是四种,和 Accept-Encoding 相同,它们是 gzip、compress、deflate、identity。下面是一组请求/响应内容压缩编码

Accept-Encoding: gzip, deflate
Content-Encoding: gzip

Content-Language

首部字段 Content-Language 会告知客户端,服务器使用的自然语言是什么,它与 Accept-Language 相对,下面是一组请求/响应使用的语言类型

Content-Language: de-DE, en-CA

Content-Length

Content-Length 的实体标头指服务器发送给客户端的实际主体大小,以字节为单位。

Content-Length: 3000

如上,服务器返回给客户端的主体大小是 3000 字节。

Content-Location

Content-Location 可不是对应 Accept-Location,因为没有这个标头哈哈哈哈。实际上 Content-Location 对应的是 Location

Location 和 Content-Location 是不一样的,Location 表示重定向的 URL,而 Content-Location 表示用于访问资源的直接 URL,以后无需进行进一步的内容协商。Location 是与响应关联的标头,而 Content-Location 是与返回的数据相关联的标头,如果你不好理解,看一下下面的表格

Request header Response header
Accept: application/json, text/json Content-Location: /documents/foo.json
Accept: application/xml, text/xml Content-Location: /documents/foo.xml
Accept: text/plain, text/* Content-Location: /documents/foo.txt

Content-MD5

客户端会对接收的报文主体执行相同的 MD5 算法,然后与首部字段 Content-MD5 的字段进行比较。

Content-MD5: e10adc3949ba59abbe56e057f20f883e

首部字段 Content-MD5 是一串由 MD5 算法生成的值,其目的在于检查报文主体在传输过程中是否保持完整,有无被修改的情况,以及确认传输到达。

Content-Range

HTTP 的 Content-Range 响应标头是针对范围请求而设定的,返回响应时使用首部字段 Content-Range,能够告知客户端响应实体的哪部分是符合客户端请求的,字段以字节为单位。它的一般表示如下

Content-Range: bytes 200-1000/67589 

上段代码表示从所有 67589 个字节中返回 200-1000 个字节的内容

Content-Type

HTTP 响应标头 Content-Type 说明了实体内对象的媒体类型,和首部字段 Accept 一样使用,表示服务器能够响应的媒体类型。

Expires

HTTP Expires 实体标头包含 日期/时间,在该日期/时间之后,响应被认为过期;在响应时间之内被认为有效。特殊的值比如0表示过去的日期,表示资源已过期。

Expires: Wed, 21 Oct 2015 07:28:00 GMT

源服务器会将资源失效的日期或时间发送给客户端,缓存服务器在接受到 Expires 的响应后,会判断是否把缓存返回给客户端。

源服务器不希望缓存服务器对资源缓存时,最好在 Expires 字段内写入与首部字段 Date 相同的时间值。但是,当首部字段 Cache-Control 有指定 max-age 指令时,比起首部字段 Expires,会优先处理 max-age 指令。

Last-Modified

实体字段 Last-Modified 指明资源的最后修改时间,它用作验证器来确定接收或存储的资源是否相同。它的作用不如 ETag 那么准确,它可以作为一种后备机制,包含 If-Modified-SinceIf-Unmodified-Since 标头的条件请求将使用此字段。它的一般表示如下

Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT

总结

本篇文章主要介绍了 HTTP 四种标头的基本概念,但是并没有涵盖全部,毕竟 HTTP 标头内容确实太多了,以上介绍的基本都是平常工作中常用的一些概念,下一篇文章预告 **HTTP 的黑科技

HTTP进阶

原文:https://zwmst.com/2722.html

HTTP 进阶

这是 HTTP 系列的第三篇文章,此篇文章为 HTTP 的进阶文章。

在前面两篇文章中我们讲述了 HTTP 的入门,HTTP 所有常用标头的概述,这篇文章我们来聊一下 HTTP 的一些 黑科技

HTTP 内容协商

什么是内容协商

在 HTTP 中,内容协商是一种用于在同一 URL 上提供资源的不同表示形式的机制。内容协商机制是指客户端和服务器端就响应的资源内容进行交涉,然后提供给客户端最为适合的资源。内容协商会以响应资源的语言、字符集、编码方式等作为判断的标准。

内容协商的种类

内容协商主要有以下3种类型:

  • 服务器驱动协商(Server-driven Negotiation)

这种协商方式是由服务器端进行内容协商。服务器端会根据请求首部字段进行自动处理

  • 客户端驱动协商(Agent-driven Negotiation)

这种协商方式是由客户端来进行内容协商。

  • 透明协商(Transparent Negotiation)

是服务器驱动和客户端驱动的结合体,是由服务器端和客户端各自进行内容协商的一种方法。

内容协商的分类有很多种,主要的几种类型是 Accept、Accept-Charset、Accept-Encoding、Accept-Language、Content-Language

一般来说,客户端用 Accept 头告诉服务器希望接收什么样的数据,而服务器用 Content 头告诉客户端实际发送了什么样的数据。

为什么需要内容协商

我们为什么需要内容协商呢?在回答这个问题前我们先来看一下 TCP 和 HTTP 的不同。

在 TCP / IP 协议栈里,传输数据基本上都是 header+body 的格式。但 TCP、UDP 因为是传输层的协议,它们不会关心 body 数据是什么,只要把数据发送到对方就算是完成了任务。

而 HTTP 协议则不同,它是应用层的协议,数据到达之后需要告诉应用程序这是什么数据。当然不告诉应用这是哪种类型的数据,应用也可以通过不断尝试来判断,但这种方式无疑十分低效,而且有很大几率会检查不出来文件类型。

所以鉴于此,浏览器和服务器需要就数据的传输达成一致,浏览器需要告诉服务器自己希望能够接收什么样的数据,需要什么样的压缩格式,什么语言,哪种字符集等;而服务器需要告诉客户端自己能够提供的服务是什么。

所以我们就引出了内容协商的几种概念,下面依次来进行探讨

内容协商标头

Accept

接受请求 HTTP 标头会通告客户端自己能够接受的 MIME 类型

那么什么是 MIME 类型呢?在回答这个问题前你应该先了解一下什么是 MIME

MIME: MIME (Multipurpose Internet Mail Extensions) 是描述消息内容类型的因特网标准。MIME 消息能包含文本、图像、音频、视频以及其他应用程序专用的数据。

也就是说,MIME 类型其实就是一系列消息内容类型的集合。那么 MIME 类型都有哪些呢?

文本文件: text/html、text/plain、text/css、application/xhtml+xml、application/xml

图片文件: image/jpeg、image/gif、image/png

视频文件: video/mpeg、video/quicktime

应用程序二进制文件: application/octet-stream、application/zip

比如,如果浏览器不支持 PNG 图片的显示,那 Accept 就不指定image/png,而指定可处理的 image/gif 和 image/jpeg 等图片类型。

一般 MIME 类型也会和 q 这个属性一起使用,q 是什么?q 表示的是权重,来看一个例子

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

这是什么意思呢?若想要给显示的媒体类型增加优先级,则使用 q= 来额外表示权重值,没有显示权重的时候默认值是1.0 ,我给你列个表格你就明白了

q MIME
1.0 text/html
1.0 application/xhtml+xml
0.9 application/xml
0.8 /

也就是说,这是一个放置顺序,权重高的在前,低的在后,application/xml;q=0.9 是不可分割的整体。

Accept-Charset

Accept-charset 属性规定服务器处理表单数据所接受的字符编码;Accept-charset 属性允许你指定一系列字符集,服务器必须支持这些字符集,从而得以正确解释表单中的数据。

Accept-Charset 没有对应的标头,服务器会把这个值放在 Content-Type中用 charset=xxx来表示,

例如,浏览器请求 GBK 或 UTF-8 的字符集,然后服务器返回的是 UTF-8 编码,就是下面这样

Accept-Charset: gbk, utf-8
Content-Type: text/html; charset=utf-8

Accept-Language

首部字段 Accept-Language 用来告知服务器用户代理能够处理的自然语言集(指中文或英文等),以及自