admin 管理员组

文章数量: 887016

文章目录

  • 一、应用层
    • 1. 实现自定义协议
    • 2. DNS(域名解析系统)
  • 二、传输层
    • 1. 传输层协议介绍
    • 2. 端口号
    • 3. UDP协议
    • 4. TCP协议
      • 4.1 保证可靠性的核心
        • 4.1.1 确认应答 [可靠性]
        • 4.1.2 超时重传 [可靠性]
        • 4.1.3 连接管理 [可靠性]
        • 4.1.4 滑动窗口 [效率]
        • 4.1.5 流量控制 [可靠性]
        • 4.1.6 拥塞控制 [可靠性]
        • 4.1.7 延时应答 [效率]
        • 4.1.8 捎带应答 [效率]
        • 4.1.9 面向字节流(粘包问题) [其它]
        • 4.1.10 TCP中的一些异常情况 [其它]
      • 4.2 经典面试题
  • 三、网络层
    • 1. IP地址
      • 1.1 清楚认识 IP报文 格式的字段
        • 1.1.1 4位版本号
        • 1.1.2 4位首部长度
        • 1.1.3 8位服务类型
        • 1.1.4 16位总长度
        • 1.1.5 16位标识、3位标志、13位片偏移
        • 1.1.6 8位生存时间(TTL)
        • 1.1.7 8位协议
        • 1.1.8 16位首部校验和
        • 1.1.9 32位源IP地址和32位目的IP地址
      • 1.2 IPv6
    • 2. IP协议的一些补充
      • 2.1 地址管理的补充
      • 2.2 路由选择的补充
  • 四、数据链路层
    • 1. 以太网数据帧格式
      • 1.1 目的地址和源地址
      • 1.2 类型
      • 1.3 帧尾
      • 1.4 mac地址
    • 2. MTU
    • 3. ARP 协议
  • 五、综合运用的经典面试题

在网络协议是分层的,从上至下依次分为:应用层、传输层、网络层、数据链路层、物理层。

在不同的网络层次上,描述一个数据的术语是不同的
传输层:一个数据段(segment),如:同步报文段(SYN),确认报文段(ACK),结束报文段(FIN),复位报文段(RST)等
网络层:一个数据报
数据链路层:一个数据帧

下面就来重点介绍这五层相关的协议以及涉及到的细节内容。

一、应用层

应用层协议是程序员打交道最多的协议,它是跟应用程序是密切相关的。
1.我们可以直接使用现成的应用层协议来进行开发。
2.程序员自己自定义协议来完成需求。

协议并不是一成不变的,很多时候的协议都是由程序员来设定的,假设客户端和服务器都是由程序员去写,那么这之间使用什么样的协议进行网络传输就完全可以由我们自己决定。

现成的应用层协议用的最多的就是HTTP协议。
例如:在浏览器中输入一个网址,然后浏览器就会打开一个网页这个过程,,就是客户端(浏览器)给 bing 的服务器发送了一个请求,请求中就包含另一个链接。然后 bing 的服务器就给浏览器返回一个响应,这个响应就是一个网页。

在这个网络通信中,使用的应用层协议就是 HTTP 协议。

注:当前很多的网址,其实都是使用了 HTTPS ,HTTPS 也是基于HTTP 的。

1. 实现自定义协议

自定义协议:
协议归根到底就是一个约定,服务器和客户端之间都要遵循的约定,否则它们直接的交互结果就会跟预期有很大的差别。因此通过自定义协议可以满足不同的场景需求,让代码才能够真正地解决实际问题。

例如一个外卖软件:
此时要提供一个新的需求:要求在外卖软件的首页,就能显示”优惠活动“,用户参与活动就能领取红包。

那么:
客户端:修改界面,能够显示优惠活动的详情。
服务器:修改后台逻辑,针对什么用户就能参加优惠,具体咋样拿到红包,红包金额多少等等…

此时有一个人打开了外卖软件的客户端,客户端启动的时候,就要向服务器去申请请求查询是否可以参与活动。服务器就要返回”是“/”否“。

这个时候的协议,要在功能开发好之前决定好客户端的查询请求是啥样的(带有用户的身份信息),再约定好服务器返回的响应是啥样的(1表示能参与,0表示不能参与或者true表示能参与,false表示不能参与),这里的要求客户端和服务器必须统一。

利用自定义协议来实现一个网络计算器

自定义的协议为:客户端中发送的请求格式必须是一个字符串,并且是由“第一个操作数;第二个操作数;运算符” 来组成。而服务器计算响应后返回的结果就是计算的结果,是一个整数

利用Udp与自定义协议来实现:
服务器:

public class CalServer {
    private DatagramSocket socket = null;

    public CalServer(int port) throws SocketException {
        this.socket = new DatagramSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动");
        while(true) {
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
            socket.receive(requestPacket);
            String request = new String(requestPacket.getData(),0, requestPacket.getLength());
            String response = process(request);
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),
                     response.getBytes().length,requestPacket.getSocketAddress());
            socket.send(responsePacket);
            String log = String.format("[%s,%d] req:%s;resp:%s",requestPacket.getAddress().toString(),
                    requestPacket.getPort(),request,response);
            System.out.println(log);
        }
    }

    private String process(String request) {
        String[] tokens = request.split(";");
        if(tokens.length<3) {
            return "[请求格式出错]";
        }
        int num1 = Integer.parseInt(tokens[0]);
        int num2 = Integer.parseInt(tokens[1]);
        String operator = tokens[2];
        int result = 0;
        if(operator.equals("+")) {
            result=num1+num2;
        }else if(operator.equals("-")) {
            result=num1-num2;
        }else if(operator.equals("*")) {
            result=num1*num2;
        }else if(operator.equals("/")) {
            result=num1/num2;
        }else {
            return "[请求格式错误,操作符不支持]";
        }
        return result+"";
    }

    public static void main(String[] args) throws IOException {
        CalServer calServer = new CalServer(9090);
        calServer.start();
    }
}

客户端:

public class CalClient {
    private DatagramSocket socket = null;
    private String serverIP ;
    private int serverPort;

    public CalClient(String serverIP,int serverPort) throws SocketException {
        this.serverIP = serverIP;
        this.serverPort=serverPort;
        socket=new DatagramSocket();
    }

    public void start() throws IOException {
        Scanner scanner = new Scanner(System.in);
        while(true) {
            System.out.println("请输入num1");
            int num1 = scanner.nextInt();
            System.out.println("请输入num2");
            int num2 = scanner.nextInt();
            System.out.println("请输入操作符");
            String operator = scanner.next();
            String request = num1+";"+num2+";"+operator;
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),
                     request.getBytes().length, InetAddress.getByName(serverIP),serverPort);
            socket.send(requestPacket);
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
            socket.receive(responsePacket);
            String response = new String(responsePacket.getData(),0, responsePacket.getLength());
            System.out.println("这个结果为"+response);
        }
    }
    public static void main(String[] args) throws IOException {
       CalClient calClient = new CalClient("127.0.0.1",9090);
       calClient.start();
    }
}

在具体开发代码前,需要先设计一下,客户端和服务器之间该如何交互。
1.客户端给服务器的请求是啥样的。
2.服务器返回客户端的响应是啥样的。
回答这两个问题就是在约定自定义协议。

像上面的这样的计算,在实际开发中是非常常见的。而为什么不直接在客户端中运算,还要交给服务器去运算这么麻烦呢?
原因:运算数据会占用CPU资源,如果CPU资源已经很紧张了,就可以采用服务器来计算的方式来解决客户端中的问题

就像一个广告系统。我们用户可以在搜索框中输入查询词“不育不孕”(在客户端中),客户端就把数据交给入口服务器,就要查找一下,有哪些广告是跟该查询词是有关的。

这里的有关就称为相关性,计算相关性的过程涉及到大量的浮点数的运算。因此可以将数据传入给另一个服务器(模型服务器)来专门计算,就不会占用入口服务器的CPU资源了。实际上单看每一个运算,都是挺简单的,但是入口服务器也架不住数据量的庞大。查找完相关的还要进行排序,要考虑到广告主的出价等因素,也会涉及到大量的运算,因此使用模型服务器来专门运算是非常好的选择。只有参与运算会占用CPU,而传输数据只会占用网络带宽。

因为上面的运算太过简单,因此我们可以用暴力的方式将字符串拆分,但是一旦数据变多和字符串的构成变复杂,假设传输的请求和响应中各有几十个字段,并且有的字段是可有可无的话,上面的方法就不可取了。

在实际开发中,除了上面的构造的请求和响应必须要使用暴力拆分外,还有更好的方式来进行自定义协议。

主要分成两大类:
1.文本格式(把请求响应当成字符串来处理,处理的基本单位是字符),有 xml 和 json 等 。

2.二进制的格式(把请求响应当成二进制数据处理,处理的基本单位是字节),有 protobuffer 和 thift 等 。

xml:
它是格式化(结构化)组织数据的方式.
可以写为:

整个 xml 是由“标签”构成的,并且标签是成对出现的。如< num1 >为开始标签,< /num1 >为结束标签,开始标签和结束标签中间的就是值

xml不仅可以用于网络传输,还可以用于配置文件之类的,会经常使用xml的形式来组织数据。
xml构造和解析,实际上比上面使用 文本+分隔符 的方式更复杂,但实际上有很多现成的用来构造和解析 xml 的库 。

json:它是键值对的结构,并且键和值之间使用 :来分割,键值对之间用 , 来分割,并且整体的最外面用 {} 来包裹。它也是用来结构化数据的,并且也会有第三方的库来构造和解析数据。

2. DNS(域名解析系统)

它既是应用层协议,也是一套系统。
域名,如:www.baidu,域名就是IP 地址的马甲,虽然IP 地址可以通过点分十进制 来方便用户查看,但是还是不方便记忆和传播。因此就用单词,用 . 来进行划分。
真正网络传输的时候,其实就是先根据域名,转换成对应的IP 地址,再根据IP 地址来进行传输。

最初的DNS 是一个文本文件,叫hosts文件里面保存的就是键值对(ip和域名的关系),hosts是保存在每个主机上的,一旦域名和ip的映射关系发生了改变,就比较麻烦因此hosts文件这个机制已经被淘汰,不适使用hosts文件来进行域名解析了。虽然现在已经不用它,但是它还存在

现在使用hosts 的场景:一般是测试程序的时候,某个程序依赖于另外一个服务器(通过域名来访问这个服务器),测试该程序的时候就可以修改hosts ,把这个域名映射到服务器的ip 上,保证不修改源代码的情况下就可以进行测试,也不会影响到正常的线上环境。

现在的DNS 是一组专门的服务器,通过这个服务器就可以完成域名解析。
如:

全世界的电脑都需要访问DNS 服务器,它是顶不住全世界的访问请求的。
解决方案:
1.浏览器/客户端本身会对域名解析的结果进行缓存(域名和ip的对应关系,是可能会改变,但不会很频繁),这样就避免了大量的访问DNS 的请求。
2.DNS 服务器也不是只有一台,而是有多台。这些最初的DNS 服务器(根域名系统),都是由专门的组织机构来负责维护的。根域名系统就包含了所有的DNS 信息,如果要想申请域名,就需要人家批准,然后把这个结果放到DNS 根域名服务器即可。
3.为了进一步地降低压力,各种网络运营商,也会构造自己的域名服务器镜像。因为根域名系统在国外,因此在国内就近地构造一些DNS 服务器,定期从根域名服务器中同步数据。因此国内用户使用DNS 的时候就要查国内的 DNS 服务器即可。每个地区设置每个城市都有自己的DNS 系统。
4.针对 DNS 服务器做镜像的时候还可以按照域名来进行进一步的划分,com 搞个专门的服务器,org搞个专门的服务器等,就能够保证每个服务器的数据量和请求量都不是很大。

二、传输层

1. 传输层协议介绍

传输层是 端对端 的数据传输,即只在乎起点和终点,而不在乎具体的运输过程。
传输层是系统内核来实现的,因此谈到的传输协议,一般都是指现成的一些协议,很少会提及到“自定义协议”。

UDP和TCP的特性前面的博客都已经介绍过:

它们的特性在代码中的区别:

无连接和有连接:
无连接:socket创建好后,就可以立刻尝试读取数据了。
有连接:socket创建好了之后,还需要等待建立连接,连接建立完了,再通过accept获取到连接到代码中,才可以读写数据。

可靠和不可靠:
在代码中没有体现。

面向数据报和面向字节流:
面向数据报:读写单位都是以 DatagramPacket 来进行传输。
面向字节流:读写单位都是以 byte[] 来传输的。

全双工:只创建了一个socket就完成了读写操作。

2. 端口号

端口号是属于传输层的。
端口号的用途:标识一个进程,就可以区分出当前收到的数据要交给哪个进程来处理。
例如:主机中有qq、微信等应用程序,这些应用程序都是通过一个网卡来传输数据的。让每个应用程序对应一个进程,让不同的进程绑定不同的端口号,此时收到的网络数据报中也会包含一个“目的端口”的字段。该数据报到达了网卡,就会根据 目的端口 找对对应端口的进程,从而把数据交给对应的进程

端口是一个整数,是由2个字节构成的,范围是0~65535 .
PID(PCB的标识)也是一个整数,也是用来区分进程的,也是进程的标识符,在任务管理器中也可以看到一个进程对应一个PID。

但为什么在网络编程中,不直接使用这个PID,而还要引入 端口号 这样的概念呢
原因:端口号是固定不变的,而PID是会变动的,即例如打开一个进程时,PID为748,而结束进程后再次打开,该进程的 PID 就 不一定是748了。
而如果在网络编程中设置的是PID,那么PID是会变的,客户端就无法得知要将数据传入到哪个服务器上。

那么一个进程能不能绑定多个端口号呢?
这个是可以的而且非常常见。绑定,不是把进程和端口号绑定,而是 socket 文件和端口号绑定。socket就是文件,一个进程里面可以有很多个文件,因此就可能会有很多个socket,每个socket就可以绑定不同的端口号

那么两个进程能不能绑定一个端口号呢
通常情况下是不行的,但是在Linux中可以,它先让进程绑定一个端口,接下来通过fork 这个系统调用,把进程的PCB复制一份,得到一个新的“子线程”,由于端口号关联是关联在socket文件上的,socket又在文件描述符表中,而文件描述符表又是PCB的一部分,fork把PCB复制了一份,也就把文件描述符表也继承了下来,也就顺带把这样的端口号的关联关系也继承过来了。

让多个socket绑定多个端口号的意义:
当在开发广告服务器的时候,首先会让服务器提供一个“业务端口”,通过这个端口来提供一些广告搜索的服务。(上游客户端就会通过这个端口来请求获取到广告数据),其次还会让服务器提供一个“调试端口”,服务器在运行的过程中会涉及到很多的数据,有时候为了去定位一些问题,就要去查看内存中的数据。通过“调试端口”来发送一些调试请求,于是服务器就会返回一些对应的结果。

在实际开发中,如果直接在服务器上利用调试器打断点调试是不可取的。因为即使服务器程序中有些小bug,但是不可能就让服务器停止运行,停止运行就会让服务器无法响应正常的业务请求了,这对在公司中是件损失非常大的操作。

知名端口号:实际上并不是所有的端口号程序员都可以使用,有些“知名端口号”程序员就不能随便用。如0~1023 的端口号就是知名端口号。当前已经有很多的现成的应用层协议了,因此有很多端口号都已经分配给了这些应用层协议。

针对一些知名端口号,如80就是给HTTP协议使用的,但实际开发中也不一定要完全遵守知名端口号,例如,tomcat也是一个HTTP服务器,但是它的默认端口号是8080,而不是80 . 因此我们只需要知名端口号的概念就好,只要我们绑定的端口号在0~65535之间并且尽量避免知名端口号即可

3. UDP协议

要了解UDP协议,就先理解协议的报文格式。 拼装报头的过程,就是所谓的“封装”。

UDP长度:即整个UDP数据报的长度(报头+负荷),该长度是使用两个字节的长度来表示的,单位是字节。两个字节能表示的数据范围:0~65532,即一个数据报最大就是64KB,不能超过这个大小,一旦超过可能会造成数据的丢失,后果比较严重。

那么如何避免一个数据报的大小接近于64KB呢?
两个方案:
1.在应用层代码中,对广告数据进行切分,分成多个 UDP 数据报,返回给上游的服务器,虽然可行,但是太麻烦。因为在应用层进行拆分跟合并,是不容易实现的,并且很容易引出一些bug 。
并且可能会存在“后发先至”的情况,假设发送方将数据分为1,2,3 三个数据报,但在接收端收到的数据,可能是1,2,3这个顺序,也可能是3,2,1这个顺序,这就造成了很大的麻烦。这种方法不推荐
2.直接改成TCP来实现
TCP在网络传输对数据的长度是没有限制的。

UDP校验和
在网络上传输的数据,是可能会出现一些问题的。网络上传输的数据本质上都是一些 0/1 bit流,这些 bit 流是通过光信号或者电信号来表示的。
如果传输过程中收到一些干扰,就容易出现“比推翻转”的情况(0->1 , 1->0) ,而校验和就是为了验证当前的数据是否出了问题

例如去买菜,要求的是买3样菜,要买萝卜、青菜、鸡蛋,此时我去买菜就买这三样菜,买完之后就会检查是否是三样菜,如果是,那么很有可能买的就是要求的那三样菜,但是不可避免的是,如果检查是三样菜但是品种不一样,那也是买错了的。

校验和是会变的,如:买了一种菜的时候检验和就是1,买了两种菜的时候校验和就为2。它是一个“证伪”,即与校验和一样,不一定是正确的,但是与检验和不一样,就一定是不正确的

校验和往往是通过原始数据的内容来生成的,不同的内容,生成的校验和就不一样。一旦数据发生改变,校验和就不一样了。就可以通过校验和来判断当前的数据是否发生了变化

实际使用校验和的算法有很多,其中比较常见的有:crc、md5 .

crc:循环冗余校验
例如选现在有一串数据,把它当成二进制的数据,依次按照字节为单位,取出数据,然后把数据进行累加。
有:

short sum;(UDP数据报的大小最大为两个字节,64KB)
for(byte b:数组) {
   sum+b;
}

可能数据加着加着就溢出来了,而且是溢出的部分就不要了。
在传输数据的时候,就把数据+crc 校验和一起传输给目标,接收方就同时收到了 数据+crc校验和 ,接收方就会再验证当前拿到的数据是否是对的,因此就会再对该数据进行累加计算,跟先前的校验和的计算方式相同,如果传输前的校验和跟传输后的校验和相同,则证明数据没有变;反之就证明数据发生改变了

即使crc计算的校验和在传输前和传输后比较仍相同,但也不排除可能数据会发生改变的情况,虽然这样的情况比较小,但还是可能会发生

md5:
md5也是一种算法,它的应用场景非常多,因此用来作为校验和,只是其中的一个场景而已。本质上是一个“非对称的哈希算法”。对字符串来作为hash值,md5就是一个针对字符串计算hash值的典型算法。

md5的特性:(md5本质上就是针对数据进行一系列的数学变换)
1.定长:无论输入的字符串有多长,最终得到的 md5 值都是固定的长度。长度的版本有:32位(4个字节),64位(8个字节),128位(16个字节) 。
2.分散:只要输入的字符串发生了一点点变化,得到的 md5 的值都会有很大的差别。
3.不可逆:一个字符串得到 md5 的值后,如果想通过 md5 的值重新获取到字符串,理论上是无法恢复出原始的字符串的

根据这三个特性,很多地方都可以用到md5
1.作为hash算法,这是原本的职责。
2.作为检验和:
很多场景,传输大文件,都会用md5作为校验和。
这主要是第二个特性的原因,只要有点数据改变,那么校验和就会差别很大,因此即使有很少的部分的数据传输过程发生了改变,那么校验和差别很大,导致数据改变后检验和仍然相同的概率就大幅降低。
3.应用于一些密码学的场景:
这主要是第三个特性的原因。现在的大部分网站对用户的密码都会加密,即使开发该网站的程序员知道了密码的 md5 值,也无法复原回原始的字符串,就无法利用用户的密码了。

4. TCP协议

TCP协议段格式:


选项上方的都是存在于报头中的(包括选项),数据就是数据报的负载部分。

保留位:它对于数据报的类型的分类有着很关键的作用。下面会讲到。

关于TCP的特性:
1.有连接
2.可靠传输
3.面向字节流
4.全双工

这四个特性是从直观上看待TCP的特性的。

而可靠性,是TCP中最核心的特性。

4.1 保证可靠性的核心

4.1.1 确认应答 [可靠性]

发送方发送数据给接收方了,接收方就回应一个应答报文,如果发送方收到了这个应答报文,就说明接收方是收到了的。

场景:

由于网络上的传输,接受到的顺序是不确定的,因此不能就单纯的按照通过收到的顺序来确定逻辑。因此此处引入了序号和确认序号,即在协议格式中出现的序号和确认序号。

解决后:

那么此时即使接受到的顺序跟不同,有着序号和确认序号就不会混乱了

因为实际上,TCP传输数据是不论条的,是论字节的(面向字节流) 。因此序号和确认序号就是以字节为单位进行编号的。

编号是针对每个字节进行编号,依次进行累加。而且实际上序号的起始不一定是从 1 开始的 。假设第一个请求,A给B发送了1000个字节的数据,序号就是1-1000(假设序号从1开始),长度为1000,因此确认应答数据报的确认序号就只是1001。
意思就是,1001之前的数据,B已经收到了,另外,也可以理解成B在向A索要 1001 开始的数据。

发送方就可以根据确认的应答报文来确定接收方是否收到。只要发送方收到了应答,就认为接收方收到了,可靠传输就完成了。

确认应答的机制是TCP的核心,但不是专属。例如生产者——消费者模型。消息队列就要负责保存一些数据,但是消息队列里面的存储空间也不是无限的。存的数据要定期的淘汰掉。那什么样的数据要淘汰掉呢?

很重要的原则,如果一个数据没有被消费过,那么就不能够轻易地淘汰。判断这个数据有没有被消费过就是用“确认应答”机制来完成

4.1.2 超时重传 [可靠性]

不可否认,确认应答机制是在数据传输比较顺利的情况来实现的。但在传输的过程中可能会出现丢包。一旦数据丢包,就要进入超时重传的机制中了。

场景:当发送数据时可能会丢包,即不成功。

这两种情况下,发送方无法区分,当前的发的数据丢了,还是应答数据丢了。发送方没有收到应答报文,那么能做的事情就只能过段时间后重新发送信息。
要过段时间再发送的原因:
数据在网络上传输是需要时间的,不是说这个数据刚发出去,就期望得到回应。可能要经过一段时间后才能收到回应。但当没有收到应答报文,就会过段时间重新发送,具体过多长时间,不同的系统实现的方式不一样。

在Linux中(BSD Unix和Windows也是如此),超时以500ms为一个单位进行控制。因此发送方在发送数据后500ms后如果没有收到应答报文就重新发送数据,就认为是丢包了。

有人会认为网络上传输的是光信号/电信号因此传输的速度跟光一样快,其实是错误的。因为每个数据都需要在每个交换机/路由器上进行转发,这个转发的过程就取决于交换机/路由器(软件设备/硬件设备)的速度了。并且传输的数据并不是一两个光子,而是在一定的时间内,传输很多的光信号。

超时重传的时间并不是一成不变的,例如第一次是经过500ms后没收到应答报文后,就再发一次。第二次是经过1000ms后没收到应答报文,就再重新发。每次的重传时间是之前的两倍。因为延长等待时间,就意味着让重试的频率尽量降低只要重传也失败,就认为大概率这个传输是通不了的

当发送方超时传输传了两次数据,都被接收方接受到了,实际上接收方也是只读取一份。因为接收方接受的数据会先放在内核的”接受缓冲区中“,它就是一段内存,每个socket都有,因此可以按照序列号来去重。此时在应用程序中读取数据,读到的数据就不会重复的。

如果网络是正常的,那么出现连续两次甚至连续三次都丢包的概率,是非常低的了。因为假设丢包的概率为10%,连续两次丢包的概率:10% * 10%=1% ,已经很低了。

经过一段的时间后,就不再继续实施超时重传机制,而是尝试重新建立连接。

4.1.3 连接管理 [可靠性]

连接管理说的就是,如何建立连接(三次握手),如何断开连接(四次挥手)。这两个都是TCP中最高频的问题。是在内核上进行的。

三次握手:
模拟三次握手:

实际上的三次握手:

就对应到:

简化:

本质上,就是A向B请求连接,B给予回应。B也向A请求连接,A也给予回应。这是四个步骤,但是 B给予回应 和 B也向A请求连接 可以合并为一个操作。

如果三次握手改为四次握手,不是不可以,只不过还要设置多一个数据报类型并且传输,因为服务器发送SYN和ACK都是在同一个时机,由操作系统内核完成。我们都知道网络传输这些数据报是要涉及到封装和分用的,每一个数据报的封装和分用的步骤也是非常多的。而三次握手改为两次握手不行

三次握手的目的:
1.投石问路:通过三次握手的过程,来确认 A 和 B 之间的传输是通畅的。尤其是要确认,A 和 B 之间的各自的发送能力以及接受能力是否正常。

如果网络出现问题,此时三次握手都会难以成功,此时就没有必要进行后续的传输了。

2.协商参数:通过三次握手,让 A 和 B 之间通通气,选择一些传输中合适的参数,如 TCP 的序号从几开始。

服务器两种关键的状态:
1.LISTEN:说明服务器已经启动,随时可以连接。当创建好了ServerSocket实例后,就进入了listen状态。
2.ESTABLISHED:说明服务器和客户端的接受和发送都没有问题,已经可以开始通信了。等同于代码中accept方法返回后,得到了一个clientSocket实例。

四次挥手:A是客户端,B是服务器

此处ACK和FIN不是同时发送的原因
ACK和FIN发送的时机是不一样的,ACK是由内核来完成发送的,服务器发送的FIN是由代码来决定的。它是当服务器一收到客户端发送的FIN后,就会立刻发送ACK了,而服务器发送FIN的时机有两种情况
1.当代码中调用了client.close()方法,服务器才会发送FIN。

2.表面上看是调用了socket.close()方法发送的FIN,本质上是 内核里面 释放了对应PCB 的文件描述符
如:虽然代码中没有调用close,但是 socket 对象被 GC 回收了,也是可能会关闭释放对应的文件描述符的。(这个操作没那么及时)
再比如:代码中虽然没有调用close,但是进程结束了,对应的PCB就会随之销毁,而PCB里面包含文件描述符表也被销毁了,进一步的文件描述符也被销毁了。就同样会触发FIN 。

四次挥手的流程图及状态转移图:

主要是两个状态
1.CLOSE_WAIT:
这个状态是服务器收到FIN 后,进入的状态,等待用户调用close方法来发送 FIN 。

2.TIME_WAIT:
表示客户端收到了FIN 后进入了 TIME_WAIT 状态,这个状态的意义就是为了处理最后一个 ACK 的丢包。

问题一:假设,如果A 收到FIN ,并返回ACK 后就断开连接,而不是进入 TIME_WAIT 状态会咋样
答:假设此时的ACK 丢包了,那么服务器就认为可能是自己发送的 FIN 丢包了,就会一直超时重传,但是连接都已经断开了,再重传也没有任何意义。

因此即使是进程已经退出了(服务器调用close方法),TIME_WAIT状态仍然会存在。
TIME_WAIT 也会等待一定的时间,如果一定的时间之内也没有重传的 FIN 过来,才会真正的销毁。这个一定的时间是2 * MSL(网络传输之间最长的一次通信时间,在Linux中默认是1min,但MSL也是可以支配的)

问题二:如果服务器上出现大量的 CLOSE_WAIT 是什么原因

说明这是代码出现了bug,没有调用到close方法。

如果服务器上出现了大量的 TIME_WAIT ,那么可能是代码的bug,但也不全是。主动发起 FIN 的一方会进入到 TIME_WAIT ,就需要排查服务器是否应该主动断开连接。

哪方先断开连接,哪方就先进入 TIME_WAIT ,进程退出后,TIME_WAIT 状态仍存在,TCP 的连接也存在。如果让服务器先退出,服务器这边就会进入到 TIME_WAIT 状态(原来的连接占据着端口),接下来如果服务器立刻启动,新的进程就又会尝试重新绑定这个端口。可能会存在端口绑定的情况。
因此在TCP 服务器的时候,都是先结束的客户端,后结束的服务器。

当然,如果使用原生的socket来尝试上面的操作,效果会非常明显,服务器会启动失败。但是Java中的socket一般来说第二次启动也是可以成功的

在原生socket API 中有个 REUSE_ADDR 选项,如果把这个选项加上,就能够在绑定端口的时候复用 TIME_WAIT 状态的端口,而Java socket中是默认设置了这个选项。

四次挥手不一定是四次,也有可能是三次。因为有延时应答和捎带应答,虽然FIN和ACK不是同一时机的,但是经过延时应答和捎带应答的情况下是有可能合并在一起的

四次挥手也不一定会执行,它是一个正常断开的流程,实际上,有的时候TCP连接也会异常断开

4.1.4 滑动窗口 [效率]

TCP不仅仅是为了保证可靠性,而且还尽可能地提高效率,其实可靠性和效率是矛盾的。TCP努力的在可靠性的前提下,还做出了很多性能优化的手段。

正常情况下建立连接:这个过程要花很长的时间来等主机B 返回 ACK,而等待的过程就严重影响到传输的效率

滑动窗口的方式建立连接:
设置一个窗口的大小来一次性发送一批SYN ,一次又等一波的 ACK,把多组数据的 ACK 的等待时间重叠了起来。

举个例子:去早餐,要买三种:包子(蒸熟3min)、烤肠(烤好4min)、炒饭(炒好5min),正常情况下是买包子的时候,等包子蒸熟才买下一个,即要等到那个制作的过程返回才执行下一个过程,以此类推。但是我们利用滑动窗口的特点,先去买包子,在包子准备蒸熟的过程中去买烤肠,接着再在烤肠去烤的过程去买炒饭。去买炒饭就等5min再去拿回烤肠和包子,一共只花5min就可以买完全部,而正常情况下要买12分钟

一次批量发的数据的长度,就称为“窗口的大小”,如果没有批量发送数据的长度的限制,就容易造成窗口无限大,ACK 就一顿乱发,此时就没有可靠性而言。

这个窗口的大小是要根据接收方的处理数据的能力来配置的。如果窗口太大,那么没有可靠性;如果窗口太小,整体的效率就降低


当窗口范围为1001-5001,就说明发送方现在同时发送了1001-2000、2001-3000、3001-4000、4001-5000,此时就在同时等待四组数据的ACK 。
假设2001这个ACK 先到,发送方就知道,1001-2000这个数据已经被接收方接收了,接下来就立刻发送5001-6000的数据。它能够仍然保证窗口的大小是4份数据,仍然保证是4份数据在等ACK 。因此滑动窗口并不是把4份ACK 都等到才去发下一个数据,而是始终保持窗口的大小为一个固定的值

那么我们之前知道了网络传输可能会出现“后发先至”的情况,如:2001、3001、4001、5001都在网络上传输,就不一定是2001先到,也可能是3001先到。确认序号也在这里体现了作用,确认序号表示,从该序号之前的前面的数据就都被接收到了。
如:假设先收了3001这个ACK ,则说明1001-2000、2001-3000 的数据都已经被对方收到了,此时2001 的这个ACK收或不收已经不是关键了

如果在滑动窗口的场景中丢包了咋办??
情况一:接收方的数据包丢了
其实这个没什么关系,还是因为确认序号的原因。确认序号能够包含前面的数据是否被接收方接受到。
如:
发送方:1001-2000、2001-3000 ; 接收方:2001、3001
此时的2001 这个ACK 丢了,只要3001这个ACK 不丢,就说明3001之前的数据都被接收方接收了,相当于1001-2000这个数据报也得到了一个确认应答

实际上,TCP为了提高效率,在滑动窗口下,并不会每一条数据都有ACK ,会隔几条数据才有一个 ACK 。

情况二:发送方的数据报发生了丢包。

前面我们知道了,发送方如果的数据丢包了,就会发生超时重传,而在滑动窗口下也会进行重传。那么接收方如何告诉发送方丢包了呢?

如图所示:假设1001-2000的数据发生了丢包,那么接收方就不可能会返回2001的ACK ,因为是1001开始丢包的,主机B 就会一直索要 1001 ,确认序号就会一直是1001直到 1001-2000的数据报在接收方收到为止。
即使后面的2001-3000、3001-4000、4001-5000的数据报都没有丢失,它们返回的ACK 的确认应答也一直会提示索要1001 ,发送方如果连续看到几次1001 这个ACK ,就知道了1001 这个数据报丢失了

上图中,当1001-2000的数据报重传成功后,返回的ACK 就转变为7001,即1001-7000的数据都已经发送成功了,再去索要7001的数据报。

实际上有一个接收缓冲区来接收先前收到的数据,收到的数据就会被缓冲区刷走,没有收到的就返回的ACK 的应答报文会一直在重复索要

4.1.5 流量控制 [可靠性]

它是针对滑动窗口的进一步补充,本质上就是在控制滑动窗口的大小,窗口的大小决定了传输的效率,窗口越大,可靠性越低,资源开销越大;窗口越小,效率越低

因此,流量控制是基于接收方的处理能力来限制窗口大小的
TCP 这个传输的过程,就类似于一个 生产者——消费者 模型

主机A发送的数据到了主机B的接收缓冲区,此时主机A 就是生产者;主机B的应用程序,通过socket api来读取数据。被socket api 读到的数据就从缓冲区中删掉了。应用程序就是消费者。接收缓冲区就是交易场所

所说的窗口大小,就是指发送方(主机A)批量发多少数据,比如:主机A 发的数据很快,窗口很大,此时接收缓冲区的数据量也会增加地很快。如果主机B的应用程序从接收缓冲区读取数据的速度没有发送方发送的快,那么接收缓冲区就容易溢出。此时仍然不加任何限制,主机A就会按照一样的速度发送,新来的数据就没有地方保存了,就被内核丢了

因此流量控制机制,就是为了解决这个问题。根据接收方的处理能力(接收缓冲区的空余空间大小),来动态决定发送方的发送速率(控制窗口的大小)。

假如此时的接收缓冲区大小为4000.

1-1000数据到达的时候,缓冲区里面就用了1000,还剩3000,返回的ACK 中就会把3000这个信息告诉发送方。发送方再次发送数据的时候,按照3000作为窗口大小来进行发送。
TCP的报文格式中就有16位窗口大小(接收缓冲区的剩余空间)的数据。

如果窗口大小为0了(接收缓冲区这边满了),那么发送方就停了吗?
此时发送方的确是不发送数据了,但是为了能够查询当前接收方的窗口大小,每隔一段时间,就重新发送一个”窗口探测包“,通过这个包(不传输具体的业务数据),接收方触发ACK ,在这个ACK 就能够知道当前窗口的大小了

那么窗口大小就真的是16个字节嘛??
**当然不是!先前我们说到,在选项中存有至少40个字节,那么这40个字节选项中包含了窗口扩大因子M ,实际窗口大小是 窗口字段的值左移M倍 **!
(左移一位,相当于乘2 ;右移一位,相当于除2) .

4.1.6 拥塞控制 [可靠性]

实际上,在主机A和主机B的传输过程中,并不是只考虑数据在主机A、主机B、接收缓冲区的大小等的运行速率,而在主机A和主机B之间,还有很多中间链路

此处跟木桶原理相似,窗口的大小只跟处理能力最小的那个服务器有关,如果窗口的大小比处理能力最小的那个服务器还大,那么该服务器就会丢失掉部分的数据,就造成了传输过程中数据的不完整。

因此,我们可以站在另外一个角度来限制发送方的大小。站在宏观的角度去看,把中间链路看成一个整体,只在乎结果而不在乎过程,并且窗口大小的调整是逐渐尝试的过程

先使用一个比较小的窗口来传输数据,观察是否丢包。
如果丢包了,就立即降低发送速率。
如果没丢包,就逐渐加大发送速率。
通过这样的方式来逐渐实验出一个比较合适的窗口的大小。因此实际的窗口的大小是在 流量控制 和 拥塞控制 中权衡窗口能取的大小的最小值


上面这张图描述了拥塞控制中,窗口大小的变化规则。一开始如果没有发生丢包,窗口就按照指数增长的速度变大,由于刚开始的时候的窗口大小很小,因此按照指数来增长能快速地扩大窗口的大小。如果到达阙值,就从指数增长变为线性增长。

假设窗口大小为24就发生丢包后,就让阙值变为24/2=12 ,窗口大小还是从1开始,在没到达阙值前指数增长,到达阙值后就线性增长,这样就能够准确并且相对快速地确定窗口的大小

可以理解为谈恋爱的热恋期和冷淡期。

4.1.7 延时应答 [效率]

延时应答也是用来调整窗口的大小的,用来提高效率的机制。
让窗口大小在保证可靠的基础上,能尽量再大一点。对于流量控制来说,窗口的大小就是又接收方的接收缓冲区的剩余空间的大小来决定的。

流量控制是发送方 向 接收方 发送“窗口探测包”后,接收方会立即返回一个ACK 来传达回发送方,来控制窗口的大小,该窗口探测包 没有进行业务的处理。

而延时应答与流量控制的流程不太相同。延时应答的场景是 发送方 向 接收方 发送“窗口探测包”后,接收方会过一段时间再返回ACK ,在这段时间中接收方是在不断从接收缓冲区消耗数据的,因此延时应答的ACK 相比流量控制的ACK 里面告诉发送方的窗口大小更大,因此就能够增大下次发送方的发送数据量,就能提升传输效率

举个现实中的例子:
我送快递,给老板卖水果。假设老板的仓库能放100箱面,而此时有40箱,从早上我就问老板,老板说要60箱面,此时就给他送到60箱面,此时仓库就有100箱。
如果是流量控制的情况,在晚上时我再问老板要不要面时,老板说只要20箱,因为这还有80箱面,因此第二天我就送20箱过去。
如果是延时应答的情况,我是第二天再问老板要多少面,此时老板说要40箱面,因为在我前一个晚上问完要多少箱面 到关门之间,又卖了20箱面,因此第二天就送40箱面,效率就提高了。

并不是所有的包都能应答:
1.数量限制:每个N个包就应答一次
2.时间限制:超过最大延迟时间就应答一次

具体的数量和超时时间,依操作系统不同也有差异;一般N取2,超时时间取200ms;

4.1.8 捎带应答 [效率]

很多服务器和客户端的通信模式,很多都是“一问一答”的方式。
如:

因为在TCP中有延时应答,因此可以让ACK 不是收到发送方的数据后就立刻ACK ,而是执行延时应答机制,让ACK 和服务器要返回业务上的 response 一起返回了。此时就能把ACK 和response 结合成一个报进行发送,本来是两个的变为一个就能够提升效率了。(牢记,网络通信过程中涉及到大量的封装和分用,针对每个包都要一顿封装,收到后再一顿解析)

因此可以改为:

这个过程类似于四次挥手,之前四次握手只是ACK 的返回时间跟FIN 的返回时间不同,因此为四次握手。有了延时应答和捎带控制,四次握手也可以变为三次握手。
将ACK 的返回时间向后移,移到与FIN 的返回时间相同,打包成一个数据报进行返回即可。

但是四次挥手啥时候能变三次是不确定的,捎带应答是一个”概率性的机制“,当前ACK 延长的时间正好要比接下来发业务数据的时间更长一些

例如:服务器收到请求到返回响应的时间,这个过程消耗 50ms ,但是延长应答假设最多等20ms,这个情况就无法触发捎带应答了。但如果假设最多等60ms,第50ms 的时候,此时触发了响应,ACK 就可以和这个响应打包成一起过去了,也就触发了延时应答。

4.1.9 面向字节流(粘包问题) [其它]

在这种面向字节流的情况下,需要注意一个重要的问题:粘包问题

例如:此时有三份数据报,而每个TCP 数据报的 payload 部分都是一个完整的应用层数据。

接收方收到这些数据之后,进行分用。就会把这些数据部分,给放到数据缓冲区里面。

当应用程序从接收缓冲区读数据的时候,就不知道从哪到哪是一个完整的应用层数据报了
应用程序此时只能看到接收缓冲区里面一个一个的字节,而无法区分当前接收缓冲区里有多少个应用层数据报,以及从哪到哪是一个完整的应用层数据报

解决 粘包问题 的方式:通过设置一个合理的应用层协议来解决:
1.给应用层数据设定”结束符“/”分隔符“
2.给应用层数据设定”长度“
两种方式的核心就是要明确应用层数据中,报和报之间的边界

方式1:给应用层数据设定”结束符“/”分隔符“

方式2:设定包的长度,约定每个应用层数据报的前 4个字节,存储数据报的长度。

粘包问题是并不是TCP 特有的问题,而是只要是面向字节流都存在的类似的问题,这两种解决粘包问题的方案,在HTTP中也有所体现。

而UDP 中就没有这样的问题:

4.1.10 TCP中的一些异常情况 [其它]

常见的异常情况:
1.进程终止
不管进程是如何终止的,本质上都会释放对应的PCB ,进一步也会释放PCB包含的 文件描述符表,同样地会触发四次挥手。“进程终止”不代表连接就断开,进程终止只不过是相当于调用了socket.close()方法而已。

2.机器重启
机器重启的时候,因为也是要按照一定的程序关闭的,因此也是先将进程关闭了,文件描述符也关闭了,也是四次握手。

3.机器掉电/网线断开
这是一个突发情况,机器来不及做任何动作!

a) 如果掉电的是接收方,此时发送方还在发送数据,显然发送方就收不到接收方发来的ACK ,于是发送方就会超时重传,超过一定是时间之后还没有收到ACK ,就重置连接
重置连接靠的是复位报文段

当然此时没有电了,复位报文段也过不去,此时靠复位报文段来重置连接了。还是不成功,再然后发送方就会放弃这个连接,把连接对应的资源就回收了

b) 掉电的是发送方
此时因为掉电的是发送方,接收方就收不到任何数据,因此就不知道是接收方还没发送数据还是接收方发送的数据丢包了

那么要如何知道呢?
此时接收方采取策略,就是**“心跳包”机制(也叫保活机制)**,每隔一段时间,接收方向对方发送一个PING 包,也期待对方回返回一个“PONG”包。
如果PING 包发过去了很久还没有PONG 包,并且重试几次也不行,此时就认为对方已经挂了

这个心跳包机制是一个应用非常广泛的机制。不仅仅是在TCP中使用到。在分布式系统中可以用于“服务发现”。

假设此时发送的数据量有10000,那么每个业务服务器分到的数据是2500,是负载均衡的。此时如果某个主机宕机了,此时入口服务器就得及时发现这个事情。就需要把请求切走。

心跳包机制就能够让入口服务器知道有一个主机宕机了,但是将入口服务器和每台主机都直接建立一个TCP 连接 的做法是可行,但是不完全可行

因为心跳包的机制的保活并不是特别及时,如果是想希望能够更及时地快速发现问题,就需要在应用层(应用程序)中自己实现心跳机制

4.2 经典面试题

1.如何基于UDP 协议实现可靠传输?
这题看似在考UDP ,实则在考TCP 。并不会让我们写代码。
答:
a)实现确认应答机制,每个数据接收到之后,都返回一个ACK (这个ack是要自己实现并发送回去,而不是内核去返回了)
b)实现 序号/确认序号 机制,以及如何去重
c)实现超时重传,说出超时重传的特点。
d)实现滑动窗口来提高效率
e)实现延时应答、捎带应答、心跳包机制等

在上面这些机制后加补充即可。

2.啥样的场景使用TCP,啥样的场景使用UDP?
a)如果需要可靠性,就选TCP
b)如果传输的单个数据报比较长(大于64k),选TCP
c)如果特别注重效率,优先考虑UDP
典型的场景:机房内部的主机之间的通信,网络环境比较简单,不易产生丢包,带宽充裕,并且机房内部主机之间的通信,往往传输数据量更大,更需要速度。
d)如果需要广播,优先考虑UDP
广播,即一份数据同时发给多个主机。UDP 自身就支持广播的。但是TCP 本身不支持,就只能在应用程序中通过多个连接,轮询的方式给每个主机发送数据(伪传播)。

三、网络层

1. IP地址

1.1 清楚认识 IP报文 格式的字段

IP 报文格式:此处主要介绍的是IPv4 ,IPv6本博客后面再大致了解即可。


同样地,IP 报头就包含报文格式中数据的上面全部部分,而数据就在IP 载荷中。

1.1.1 4位版本号

版本号就是指IPv4的4 与IPv6 的6,4位表示的数字有0->15,而现在就只有两种版本号,并且是经过很多年后,IPv6才诞生,因此4位版本号来表示版本号是绰绰有余的

1.1.2 4位首部长度

表示的是一个IP 数据报的报头部分的长度是多少,4位能表示的就是0->15,单位是4个字节,是IP 数据报的 报头 和 负载 的分界线。因此数据报最大是60个字节,去掉报头里面必须有的20个字节外,选项则可以有40个字节

1.1.3 8位服务类型

3位优先权字段(已经弃用),4位TOS字段,和1位保留字段(必须置为0),因此我们真正需要关注的就是中间的4位 TOS 字段。

在4位的 TOS字段中,必须只有一个能为1,而其它位都是0 ,不能有多个位同时是1 .

可选择的服务类型:
最小延时、最大吞吐量、最高可靠性、最小成本

虽然可以有这几种选择,但是实际开发中很少用到这里。

1.1.4 16位总长度

16位则表示的是两个字节,数据范围在0-65535 ,16位总长度记录的是整个IP 数据报的总大小,即最大为64KB 。

因此在这我们联想到UDP 中有个字段也是记录整个UDP数据报 的长度,并且大小跟IP数据报 相等,当时在分析UDP数据报 的长度这么小,如何解决当一个数据长过UDP 数据报的方案有两种:
第一种是对数据进行切分,在应用层中再进行合并,但是不推荐,因为有”后发先至“的情况会出现,并且没有标识的方法。第二种就是使用TCP 了。

而在IP数据报 中,IP协议 内置了分包组包功能。因此IP数据报中就引入了16位标识、3位标志、13位片偏移。

1.1.5 16位标识、3位标志、13位片偏移

如果一个数据太长,IP 协议就会自动的拆成多个数据报,然后进行传输,然后接收方就会重新进行组包。

其中这三个字段就是辅助实现 拆包组包的。

16位标识:相当于IP数据报的身份标识,如果是来自同一个IP数据报,那么它们的16位标识就相同

3位标志:第一位保留(保留的意思是现在不用,但是还没想好说不定以后要用到)。第二位表示”禁止分片“,第三位表示”更多分片“
如果第三位为1,则表示当前这个分片就是最后一个分片了。如果为0,则表示当前的分片不是最后一个分片,后面还会有。

13位片偏移:就是描述当前这个报拆分后的报顺序

如果被问到:如果真的要在需要在应用层,基于UDP 实现拆包组包,那么如何实现?
回答就可以是直接采用上面的三个字段即可。

1.1.6 8位生存时间(TTL)

这个字段表示一个IP 数据报最多在网络上存货多久,这个TTL不是时间概念,而是一个”次数“。大部分都是64或32 这样的整数
如:A->B发送发送一个IP 数据报,这个时候假设初始的TTL是32,那么每次经过一个路由器的转发,TTL就-1 ,直到TTL减为0就把数据丢弃

实际上32是非常够用的,我们打开cmd,输入ping 一个网站的命令,就可以看到主机到该网站需要的TTL 是多少。

可以看到初始的TTL为64,到达目标之后还剩下55,说明中间经历了9 个路由器的转发。

TTL的作用:主要是防止IP数据报 出现环路转发的情况,如主机:A->B->C->A …,之所以TTL比较充裕的原因是:六度空间。即假设主机A连接的路由器有200个,则路由器与路由器之间又有200个连接,最多六层个去每条路径去找就能找到任何一台主机。

1.1.7 8位协议

8位协议的作用是:当前的数据报被接收方接受到之后,分用的时候,要把载荷 里的内容全部交给传输层的哪一层协议。分用的时候就要保证,载荷数据内容和接下来交给的协议相匹配

如果接收方收到数据之后,要进行解析,如果解析到的载荷里面的数据报与当层协议不符,则会导致错误。
假设IP数据报里面的载荷为UDP 的数据报,如果此时把UDP 的数据报传给了传输层的TCP 协议,那么有很多数据就丢失了。因为UDP的报头大小是8个字节,而TCP的报头大小除去选项就要20个字节,这样的话UDP 里面的数据就丢失了

这个值具体是多少表示UDP 或者 TCP,我们不用去纠结,只要用到去查 RFC标准文档就可以了(记载了DP/TCP/IP协议的相关实现标准)

有个问题:网络层需要指定将数据报传给传输层的哪个协议,为什么传输层中的UDP/TCP的数据报中没有8位协议 这个参数呢?
答:其实是”目的端口“在传输层中指定了这件事情,因为应用层的协议太多,除了最典型的HTTP 外,还有我们可以自定义的协议,因此协议的类型是很多的,不可能一一概况,因此用 目的端口 就能把数据准确地传输到对应的应用程序上了

1.1.8 16位首部校验和

这个校验和是类似于UDP的校验和,主要用的是crc 这样的方法去校验,但是此处不同的是校验首部就可以了。载荷部分交给了TCP/UDP 自己去校验了。

1.1.9 32位源IP地址和32位目的IP地址

IP协议最主要的作用:
1.地址管理:能够通过一系列的规则,把网络设备的IP地址给描述出来。
2.路由选择:根据当下的源IP和目的IP ,规划出一条合适的路径

在IPv4协议中,是用32位整数来表示地址的,因为直接用32位整数来表示的话,数字太多使得我们人去看就会很难看,因此引入了 点分十进制 来解决这个问题。但是计算机内部计算的还是用32位的整数去计算。
如:127.0.0.1 ,使用三个点,把32位的整数分成了四份,每份占一个字节(8bit)

关于IP地址,它用32位的整数来表示,大概有42亿9千万个,虽然数字看上去比较多,但是让每一台主机关联不同的IP地址,那么IP地址可能就不够用了。

处理方式:
1.动态分配IP
一个设备连接网络了,就分配IP地址,反之就不分配。但是这种方法治标不治本,假设后面互联网发展更快,每人多台主机并且连网,那IP地址肯定是不够的。

2.NAT 机制 (网络地址替换)
首先,先介绍有三类IP 都是局域网IP :
a) 10.*
b) 172.16. * -172.31.*
c) 192.168.*


因此设定为:局域网内部的IP 在局域网内不能重复,但两个不同局域网可以使用重复的局域网IP 。此时直接连接到广域网(外网)的路由器设备,就会有一个外网IP

实际上,这样的局域网里面的设备可能有很多,网络结构可能也很复杂,一个外网IP 就可能代表着 几百、几千、几万个网络设备。

NAT机制就是:当局域网内部的数据往广域网上发送的时候,此时路由器就会自动的把其中的 源IP 给替换了。

NAT机制的作用就是:一个外网IP 可以对应到很多台内网设备的IP地址,而每个不同的局域网的IP地址又可以相同,数据的传输也没有问题,就能够使用重复的IP地址了

NAT机制虽然解决了IP地址不够用的问题,但也引入了一些问题:
当我们在主机自己搭建一个服务器,它是自己的机器,只有局域网IP(内网IP),是可以重复的,其它主机就没法根据一个内网IP 来找到该服务器,除非是在同一个局域网里

因此我们假设在局域网内部的主机在80端口上搞个服务器,只要有外网通过80端口来访问服务器的IP ,此时就可以通过 内网穿透 工具 ,把这个请求通过云服务器和局域网主机的TCP 连接,直接转发到局域网主机的80 端口上。

如果使用端口号+NAT机制 来设定IP地址,那么是完全够用的。因为端口号是两个字节来表示,则约6w个端口号,即一个NAT设备最多可以支持6w多个连接,这个一般还是够的。假设在极端情况下,每个外网就连接6w个设备,因此最多最多用端口号和NAT机制 来表示的电脑有 6w*42亿9千万 。

1.2 IPv6

IPv4是用4个字节来表示地址的,而IPv6是用16个字节来表示地址。本来4个字节能表示的就有42亿9千万,那么16个字节能表示的就非常多了,即使不用动态分配IP 和NAT 机制都卓卓有余。

但是IPv6和IPv4不兼容,要想升级成IPv6,得把相关的网络设备(这个网络链路上的所有的交换机和路由器都升级成IPv6),显然这是成本比较高的。

IPv6在国内普及的原因:

2. IP协议的一些补充

因为IP协议的作用主要有两个:
1.地址管理 ,2.路由选择

2.1 地址管理的补充

地址管理中有个很重要知识点:网段划分(组建局域网的时候非常关键的要点)

它要求一个局域网内的主机,网络号要相同,主机号不能相同。并且两个相邻的局域网的网络号必须不同。(一个路由器连接两个局域网,那么这两个局域网就是相邻的)

把一个IP 地址一分为二,前半部分为网络号,后半部分为主机号。

划分网络号的方法:
可以通过”子网掩码“来划分。

子网掩码非常有特点,左半部分都是1,后半部分都是0,之后把子网掩码的每一位与IP地址的每一位进行按位与,得到的结果就是网络号。
子网掩码不一定非得是255.255.255.0,但这个是最常见的

网段划分本来本质上就是对IP 地址进行分类,当下主流的分类方式就是基于子网掩码的方式来划分。

补充一些特殊的IP 地址
1.如果是主机号全为0 ,这个IP 地址就是网络号,表示当前这个网段。

2.如果是主机号全为1,这个IP 通常表示当前网段的”网关“(就是路由器,这个网络的出入口)

3.如果主机号为255,这个IP表示”广播IP“ 。
如果搞一个UDP 数据报,然后把目的IP 写成主机号为255 的IP ,此时这个数据报就能在当前的局域网中广播(会发给每个局域网中的设备)。

例如电视和遥控器,假设遥控器没了,就可以用手机中的程序来控制,该程序就把网络号改为了255,向每个设备去发送数据,但是有的设备不能处理该数据,就会丢包,而电视有对应的程序去处理该数据,也就可以用手机来进行遥控了。

4.127.* ,如 127.0.0.1就是表示本机。

2.2 路由选择的补充

”路由“在计算机中是有很多含义的,此处的”路由“指的是IP 协议中的”路径规划功能“,如:路由选择的过程其实就是在A 和 B 间选取一条合适的路径。这个”合适“并不是一个很容易权衡的事情,会综合去考虑:路径的长短,通信的速度,设备开销的大小

但IP 协议的路由选择的具体过程,跟 地图 里的规划还是有很大的区别。
地图规划规划的路线是直接从起点到终点中一次性把所有路段都衡量好,而IP协议的路由选择是:数据到达某个路由器之后,这个路由器并不知道网络整体的环境(这个环境很复杂),这个路由器只是知道它附近的情况(它了解和它相邻的设备的情况)
因此IP 协议的路由选择是一个“探索式”的过程

例如:从虎门到广州大学可以去问路。(纯属举例)
1.一出门就揪住一个人问:怎么去广州大学,这个路人甲,可能知道到广州大学咋走,也可能不知道。如果是知道的,就一路按照他给的路线前进即可。如果是不知道,路人甲就会说:虽然我不知道广州大学咋走,但你总归先去坐某公交,你去公交站牌再问问。
2.到达公交站牌后,再揪住一个路人乙,继续问广州大学怎么去,当然如果知道路线怎么走就好办,如果也是不知道,路人乙就会说先坐公交到地铁二号线,到了之后再问地铁上的人。
3.到达地铁口后,再揪住一个路人丙,继续问广州大学咋走,如果不知道路人丙就会说你坐地图二号线后往南走,坐到小寨之后再问问下一步怎么走
4.随着这个过程的推移,是逐渐向广州大学靠近的。当我在小寨后,因为离广州大学不远了,如果遇到认识的人,就会直接带我去到广州大学,并且按照他的路线走即可。

以上只是简单地举个例子,实际情况下可能还会更复杂。可能问的路人,完全不知道目标在哪,或者说告诉你的方向不一定是对的…也可能问的路人知道目标在哪,但是他知道好几条路线,我还得再权衡。

每个路人都相当于一个路由器,每个路人都有自己熟悉的范围,在熟悉的范围内,他知道具体咋走,出了范围后,就只知道一个大概的方向,这个事情就类似于路由器中的一种核心的数据结构——路由表。有了路由表之后能够在路由器中选择合适的路线到最终点。

路由器的存储空间有限,不可能通过一个路由器的路由表就保存整个互联网环境的所有节点情况。因此采用逐渐寻路的方式是一个成本更低的方式

路由器的路由表里面描述了“啥样的IP”从“啥样的网络接口”(WAN/LAN)传输。知道了从哪边出后,就知道了大概的方向

四、数据链路层

负责的工作:两个相邻结点的传输
核心的协议:以太网(涉及到数据链路层和物理层的协议)(平时用的网线,准确来说就是以太网线)

比如说回老家,老家在广东省 广州市 南沙区

站在传输层:
起始位置:广东省东莞市虎门镇某某路
目标位置:广东省广州市南沙区某某村

站在网络层:
可选路线1:虎门->威远->广州->南沙
可选路线2:虎门->太平->广州->南沙

站在数据链路层:
选了这条路径:虎门->太平->广州->南沙
虎门->太平,坐公交
太平->广州,坐汽车
广州->南沙,坐游轮

1. 以太网数据帧格式

1.1 目的地址和源地址

“目的地址”和“源地址”指的是mac 地址,而不是IP 地址。它们两个之间还是有很大的区别的。一个mac地址占6哥字节,一个IP地址占4个字节。

1.2 类型

类型中就是上面以太网数据帧的类型选择,有:0800、0806、8035 。
0800指的是数据里面存储的是IP数据报;0806指的是数据里面存储的是ARP请求/应答;8032指的是数据里面存储的是RARP请求/应答。

数据的长度不能小于46-1500的字节。

1.3 帧尾

帧尾中就是crc校验和。防止数据在网络传输中发生错误的情况。

1.4 mac地址

mac地址是 数据链路层的地址。6个字节,因此表示的范围比IPv4的地址要大很多。当前来看,mac地址是可以做到每个主机都有唯一的地址的。

它不像IP地址,IP地址是动态分配的。而mac地址是写死的(网卡出厂的时候就被写死了),因此mac地址会运用到一些场景

如:网络上,某个人是一个hacker,入侵别人的主机,找到他就要锁定他在入侵的时候暴露了自己的mac地址。
游戏设定“不能多开”,一个电脑开多个客户端是不可行的。因此就根据mac地址来判断,如果发现两个账号对应的mac 地址是同一个,就是在多开。

当下来看,IP存在的目的,是为了描述一个主机在互联网上的位置,mac地址也能描述一个主机的位置。看似没有区别,但还有很大的区别。

mac地址是由16进制来表示的

主机A 要给主机B 传输一个数据报,略过应用层和传输层的封装过程,直接考虑网络层开始:

将上面的IP数据报继续封装,到达数据链路层,封装成以太网数据帧。

注:以太网数据帧的报头装的是源mac和目的mac,即主机A的mac和路由器1的mac,而IP数据报里面记录的是最起始和最终的IP地址

接下来,路由器1转发上面的以太网数据帧给路由器2,这个转发的过程中,IP数据报的内容不变,以太网帧头变了

接下来,路由器2转发上面的以太网数据帧给主机B,相同地,IP数据报的内容不变,以太网帧头变了。

mac和IP地址看起来效果类似,但它们各司其职,IP站在全局,mac站在具体的局部,通过它们不同的功能来完成网络传输。

2. MTU

物理层其实是存在这样的硬性限制的,对应的数据链路层的数据帧,是有一定范围的大小的。这个范围的指标就是MTU 。往往MTU这个值比IP数据报的最大长度(64K)还会更小,当然,不同硬件介质对MTU 是不同的

IP数据报的分包其实不是因为触达了IP 的长度才分的包,而是因为触达了MTU 才产生的分包。因此IP数据报的分包给了MTU 的限制提供了具体的方案。因此最大值1500就是以太网的最大传输单元了。

如:

MTU对UDP 的影响:因为UDP会因为MTU 进行分包,如果传输的过程中有一个包丢失,那么接收方就会对该整个数据包的重组会失败。本来UDP就没有可靠性,分包后的丢包概率更大,因此UDP 的可靠性就会受到更大的影响。

MTU对TCP 的影响:TCP中引入了一个MSS(最大消息长度),即没到达这个MSS 的长度就不用进行分包

MTU和MSS的关系:

3. ARP 协议

ARP协议 是一个“辅助性"的协议,这个协议不仅仅是属于 数据链路层 的,而是横跨数据链路层和应用层。这个协议的功能就是根据IP 查询对应的 mac 地址。

如:

在网络传输的具体过程中,A要先把数据传给 路由器1 ,就需要先查 A 自身的路由表,路由表查到的是一个网络接口,进一步地对应着路由器1的 IP ,光知道路由器1的IP 还不够,还要知道mac 地址。把这个mac地址放到数据链路层的报头中,才能完成具体的封装过程

主机A 在最初的联网阶段,就会进行**“mac学习”**,就会把和它相邻设备的IP 和mac 的对应关系给维护起来。内部建立一个类似于哈希表 这样的结构(key:ip,value:mac),因此就是通过ARP协议来构建这个哈希表。

知道了IP后往表里查,就能够知道对应的mac 地址。

这个表不一定是在内存中,也不一定是hash 表,有可能是一个”硬表“,路由器/交换机 有一个关键的硬件设备,叫”转发芯片“,转发芯片和CPU、内存是并列的关系。在转发芯片中包含了一些寄存器,访问寄存器的速度比内存更快,因此还可能在这些寄存器中存储着

ARP协议的工作过程:
设备接入网络的时候,先广播一个 ARP 请求(在当前局域网中广播),收到这个请求的设备返回ARP响应(包含了每个设备的ip 和mac),新接入网络的设备就把这些关系保存起来了。
这样的过程可能会周期性的进行,主要是因为网络环境可能会动态变化(ip地址可能会变)。

五、综合运用的经典面试题

浏览器中输入一个 URL 之后(地址之后),都发生了哪些事情??
步骤:
1.浏览器首先会根据域名,查询对应的IP 地址。
a)先查浏览器自身的缓存
b)再查hosts文件
c)再查DNS 服务器,查DNS 服务器的过程就省略(先查主机上的DNS 的ip,ip有两个,一个是主的一个是备用的,查完后如果没有就去找上一级的DNS 服务器,直到根域名服务器为止)

2.浏览器会构造一个HTTP 请求,这个HTTP请求中就包含了刚才的这个域名的信息(用户输入的信息)

3.浏览器就要通过调用操作系统的 socket api ,把这个 HTTP 数据交给TCP 来进一步处理。
TCP协议就构造一个TCP 数据报。
a)在发送TCP 数据报之前,需要先进行三次握手来建立连接。此处三次握手涉及到的SYN/ACK 也是同样要经过 网络层,数据链路层,物理层 依次封装,到达对端服务器后再进行分用。
b)具体进行数据传输,发送方就会把这个TCP 数据进一步交给IP 协议进行进一步的封装。

4.网络层把TCP 数据报封装成一个IP 数据报,进一步地进行封装。根据MTU ,这个IP数据报可能是多个,也会进行自动分包的过程。再把IP数据报交给数据链路层。

5.数据链路层会把这个数据再封装成 以太网数据帧 ,在构造帧头的时候就需要根据IP 映射到 mac 地址,这个构造的过程依赖了 ARP协议。再把这个以太网数据帧交给物理层。

6.物理层把数据转换成电信号来继续传输。(对于以太网来说是电信号)

上面的都是发送之前的工作

7.电信号沿着网线到达下一个设备(路由器),路由器就会针对收到的数据进行分用,物理层把数据交给数据链路层,数据链路层把数据交给网络层,路由器拿到了网络层中的IP 数据报,取出其中的目的ip ,查询路由表,就找到下一个传输的目标(mac地址),再重新进行封装(把数据交给数据链路层、物理层,注:数据链路层的数据报对于之前的IP数据报里面的源mac和目的mac就发生改变了)。

7是路途中间的转发过程

8.数据到达接收方后(搜狗服务器),数据仍要继续进行分用。要层层解析。物理层把光电信号转成以太网数据帧,交给数据链路层;数据链路层解析出IP 数据报,交给网络层(都涉及到crc校验,如果发现校验和不对,就直接把数据丢弃)。IP协议再解析,解析出一个TCP 数据报(IP报头里包含着协议类型),这个解析过程可能还包含组包的过程(IP 报头中有 16标识,3位标志位,13位片偏移)。再根据TCP 数据报中的端口号,找到对应的进程,把数据就放到对应 socket 的接收缓冲区里了。

9.应用程序调用对应的 socket api ,从tcp接收缓冲区中读取数据,应用程序把这个数据按照HTTP 协议来解析。获取到其中的URL 。根据URL 中指定的路径,知道了是要获取到 / 这个根路径里了。

10.搜狗服务器会对 / 这个路径进行配置,映射到一个具体的 index.html(也可能是其它名字)这样的html文件。服务器就会读取到这个文件,把这个文件的内容构造成一个HTTP响应数据,然后再调用 socket api 进行发送。

11.重复上面的封装过程,服务器发送的响应数据也要层层封装。最终变成一个物理层上的传输的光电信号。

12.这个光电信号到达下一个路由器,路由器重复刚才的分用过程,解析到IP 地址这一层,然后取出其中的目的 IP ,查路由表,找到下一个设备在哪(mac地址),重新再封装数据。

13.重复这个过程,依次转发,最终到达用户的主机上。

14.用户主机重复上面的分用过程,达到主机后依次把数据都取出来,最终交给应用程序。

15.浏览器得到了 HTTP 的响应报文,解析这个报文,获取到其中的 html 内容,根据 html 进行渲染。
由于html 中可能会包含一些< img >,< link >,< script >这些标签,这些标签可能会触发二次请求。浏览器根据这些标签中的信息,再构造出对应的 HTTP 请求发给服务器,预期得到的响应,因此还是重复上面的过程。


网络传输本来就是一个非常复杂的事情,正因为非常复杂,所以才要”协议分层“,不可能将一个协议把这些协议所有的功能都涵盖起来,那是相当复杂的,协议分层能降低我们的学习成本。

上面的角度是站在学生的角度去回答的,是一些基本原理。核心就是这些关键的协议如何相互配合,以及封装分用,与路由转发。


如果站在后端工程师的角度,这时候重点核心就不在网络通信上了,而是看到的是应用层所做的一些事情。

1.用户此时在浏览器中输入了一个地址,此时浏览器发送一个HTTP 请求,它首先不是到搜狗的机房,而是先发给员一个叫”CDN“的服务器,它是运营商构建的缓存服务器。因为当前想要得到的是搜狗的首页,这是一个静态的页面(html文件),因此可以在CDN 服务器上缓存。而运营商会将CDN 服务器在全国各地都部署。
因为搜狗的机房可能离本地很远,因此CDN 的作用就加速查找。我们在本地访问搜狗的话可能就是CDN 服务器返回的html 文件。

2.如果CDN 服务器中没有,就会发到搜狗的机房中。当然不可能直接到达机房,而是先到达 反向代理服务器,它也是一层缓存。为了降低后续业务服务器的压力。

3.如果反向代理服务器中还是没有命中,那么就找到 反向代理服务器 后面的 应用服务器,应用服务器是一个集群,因此要依次地去寻找直到找到为止。
图示:


若更复杂一些,在浏览器中访问搜狗的搜索结果,如:输入”鲜花“。 1.同样地,先发送一个 HTTP 请求,去CDN 服务器中找,没有命中就再去找搜狗机房。

2.因为此处包含查询词,到达 反向代理服务器 后它是没有对有查询词进行缓存的,因此也没有命中。

3.因此就到了应用服务器,应用服务器会根据请求的内容去访问专门的分词服务器,对查询词进行分词。

4.除此以外,应用服务器还会去查询 搜索服务器 。根据分词结果,找到具有相关性的页面,这里往往找到的只是页面的id,不是页面的内容,将一些搜索服务器查询到的数据也放到模型服务器中。

5.应用服务器再去找物料服务器,它根据页面的id,获取到页面的具体内容。这些具体的内容来自数据库,当然访问到数据库前要有个消息队列来缓冲。该数据库的信息是用爬虫来抓取数据的,以及清洗程序整理数据。

6.应用服务器再去找用户服务器,根据用户的身份信息去对结果进行优化。

7.这些服务器产生的日志(生成的数据),还需要进行汇总,并进行一些统计性的工作。例如,汇总到 HDFS 集群上(分布式的文件系统),该集群就去访问统计程序,统计程序来跑数据。再将统计程序的统计结果更新到模型服务器。

8.统计程序的统计结果还会生成一些统计报表,这个一般是领导去看的,会根据报表的数据了解到程序的具体运行情况,如:这个产品做的怎么样,统计用户们的喜好程度等去反馈。

这只是一个”展示流“。还有很多复杂的情况。后端工作师看待的角度是:为了解决实际问题,涉及到背后的业务流程以及业务流程所带来的系统结构。
图示:


本文标签: 五层 模型 详细 刨析 TCP