# 传输层协议 TCP:TCP 为什么握手是 3 次、挥手是 4 次?

因为服务端收到客户端的 SYN 连接请求报文后,可以直接发送 SYN+ACK 报文。其中 ACK 报文是用来应答的,SYN 报文是用来同步的。加上第三次客户端对服务端这个 ACK+SYN 包的确认,就是三次握手。但是关闭连接时,当服务端收到 FIN 报文时,很可能并不会立即关闭连接,所以只能先回复一个 ACK 报文,告诉客户端,"你发的 FIN 报文我收到了"。只有等到服务端所有的报文都发送完了,才能发送 FIN 报文,因此不能一起发送。故需要四步握手

# 为什么 TIME_WAIT 状态需要经过 2MSL(最大报文段生存时间)才能返回到 CLOSE 状态?

答:虽然按道理,四个报文都发送完毕,我们可以直接进入 CLOSE 状态了,但是我们必须假象网络是不可靠的,有可能最后一个 ACK 丢失。所以 TIME_WAIT 状态就是用来重发可能丢失的 ACK 报文。在客户端发送出最后的 ACK 回复,但该 ACK 可能丢失。服务端如果没有收到 ACK,将不断重复发送 FIN 片段。所以客户端不能立即关闭,它必须确认服务端接收到了该 ACK。客户端会在发送出 ACK 之后进入到 TIME_WAIT 状态。客户端会设置一个计时器,等待 2MSL 的时间。如果在该时间内再次收到 FIN,那么客户端会重发 ACK 并再次等待 2MSL。所谓的 2MSL 是两倍的 MSL(Maximum Segment Lifetime)。MSL 指一个报文段在网络中最大的存活时间,2MSL 就是一个发送和一个回复所需的最大时间。如果直到 2MSL,客户端都没有再次收到 FIN,那么客户端推断 ACK 已经被成功接收,则结束 TCP 连接。

# 为什么不能用两次握手进行连接?

答:3 次握手完成两个重要的功能,既要双方做好发送数据的准备工作(双方都知道彼此已准备好),也要允许双方就初始序列号进行协商,这个序列号在握手过程中被发送和确认。现在把三次握手改成仅需要两次握手,死锁是可能发生的。例子:假定客户端给服务端发送一个连接请求分组,服务端收到了这个分组,然后发送了确认应答分组。按照两次握手的协定,服务端认为连接已经成功地建立了,可以开始发送数据分组。可是,客户端在服务端的应答分组在传输中被丢失的情况下,将不知道服务端是否已准备好,不知道服务端建立什么样的序列号,客户端甚至怀疑服务端是否收到自己的连接请求分组。在这种情况下,客户端认为连接还未建立成功,将忽略服务端发来的任何数据分组,只等待连接确认应答分组。而服务端在发出的分组超时后,重复发送同样的分组。这样就形成了死锁。

# 如果已经建立了连接,但是客户端突然出现故障了怎么办?

TCP 有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为 2 小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔 75 秒钟发送一次。若连续发送 10 个探测报文仍然没响应,服务器就认为客户端出了故障,接着就关闭连接。

# TCP 协议是如何恢复数据的顺序的,TCP 拆包和粘包的作用是什么?

TCP 拆包的作用是将任务拆分处理,降低整体任务出错的概率,以及减小底层网络处理的压力。拆包过程需要保证数据经过网络的传输,又能恢复到原始的顺序。

这中间,需要数学提供保证顺序的理论依据。TCP 利用(发送字节数、接收字节数)的唯一性来确定封包之间的顺序关系。

粘包是为了防止数据量过小,导致大量的传输,而将多个 TCP 段合并成一个发送。

# 滑动窗口和流量控制是怎么回事?

滑动窗口是 TCP 协议控制可靠性的核心。发送方将数据拆包,变成多个分组。然后将数据放入一个拥有滑动窗口的数组,依次发出,仍然遵循先入先出(FIFO)的顺序,但是窗口中的分组会一次性发送。窗口中序号最小的分组如果收到 ACK,窗口就会发生滑动;如果最小序号的分组长时间没有收到 ACK,就会触发整个窗口的数据重新发送。

另一方面,在多次传输中,网络的平均延迟往往是相对固定的,这样 TCP 协议可以通过双方协商窗口大小控制流速。

# 如果发送过程中,部分数据没能收到 ACK 会怎样呢?这就可能发生重传。

如果发生下图这样的情况,段 4 迟迟没有收到 ACK。

image-20220219161451695

这个时候滑动窗口只能右移一个位置,如下图所示:

image-20220219161540960

在这个过程中,如果后来段 4 重传成功(接收到 ACK),那么窗口就会继续右移。如果段 4 发送失败,还是没能收到 ACK,那么接收方也会抛弃段 5、段 6、段 7。这样从段 4 开始之后的数据都需要重发

# 快速重传

在 TCP 协议中,如果接收方想丢弃某个段,可以选择不发 ACK。发送端超时后,会重发这个 TCP 段。而有时候,接收方希望催促发送方尽快补发某个 TCP 段,这个时候可以使用快速重传能力。

例如段 1、段 2、段 4 到了,但是段 3 没有到。 接收方可以发送多次段 3 的 ACK。如果发送方收到多个段 3 的 ACK,就会重发段 3。这个机制称为快速重传。这和超时重发不同,是一种催促的机制。

为了不让发送方误以为段 3 已经收到了,在快速重传的情况下,接收方即便收到发来的段 4,依然会发段 3 的 ACK(不发段 4 的 ACK),直到发送方把段 3 重传

# 流量控制和拥塞控制有区别吗

本质都是控制流速。流量控制是防止发送者发送太快,双发协商一个流速(比如用滑动窗口大小),拥塞控制不同,这个是网络的一种机制。当发生超时、丢包的时候,降低对网络质量的理解,从而降低整个网络的响应(当然如果网络质量高,也可以调高整个网络的处理速度)。

前者是基于发送端、接收端的调整;后者是对网络结构的调整。

# Moba 类游戏的网络应该用 TCP 还是 UDP?

所有在线联机游戏都有件非常重要的事情需要完成,就是确定事件发生的唯一性,这个性质和聊天工具是类似的。

你在王者荣耀中控制后羿释放技能,这是一个事件。同时,王昭君放了大招,这是第二个事件。两个事件一定要有先后顺序吗?答案是当然要有。因为游戏在同一时刻只能有一个状态。

类比一下,多个线程同时操作内存,发生了竞争条件,那么是不是意味着,内存在同一时刻有两个状态呢?当然不是,内存同时刻只能有一个状态,所以多个线程的操作必须有先有后

回到 Moba 游戏的问题,每个事件,游戏服务器必须给一个唯一的时序编号,对应后羿的技能和王昭君的技能。所以,在线竞技类游戏,事实上是玩家在不断向服务器竞争一个自增序列号的过程。无论客户端发生怎样的行为,只有竞争到自增 ID 才能进步。也就是说,服务器要尽快响应多个客户端提交的事件,并以最快的速度分配自增序号,然后返回给客户端

所以,Moba 服务端的核心是自增序号的计算和尽量缩减延迟。从这个角度出发,你再来看看,应该用 TCP 协议还是 UDP 协议呢?

虽然 TCP 协议有 3 次握手,但是连接上之后,双方就不会再有额外的传输成本,因此创建连接的成本,可以忽略不计。

同时,TCP 协议还提供稳定性支持,不需要自己实现稳定性。如果规模较小的在线竞技类游戏,TCP 完全适用。但是当游戏玩家体量上升后,TCP 协议的头部(数据封包)较大,会增加服务器额外的 I/O 压力。要发送更多的数据,自然有更大的 I/O 压力。从这个角度来看,UDP 就有了用武之地。

# 一台内存在 8G 左右的服务器,可以同时维护多少个连接?

连接是内存中的状态对象,从理论上分析,连接本身不太占用内存。不同语言连接对象大小不等,但是通常很小。下面我提供一段 Java 程序,你可以感受一下:

public class Server {
    public static void main(String[] argv) throws IOException {
        var serverSocket = new ServerSocket();
        var addr = new InetSocketAddress(3001);
        serverSocket.bind(addr);
        var list = new LinkedList<>();
        while(true) {
            var client = serverSocket.accept();
            list.add(client);
            System.out.println(list.size());
        }
    }
}

public class Client {
    public static void main(String[] argv) throws IOException, InterruptedException {
        var clients = new LinkedList<>();
        for(int i = 0; i < 1000000; i++) {
            var client = new Socket("127.0.0.1", 3001);
            clients.add(client);
        }
        Thread.sleep(10000000);
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

通过运行上面这段程序,你可以观察到以下这几个现象:

  1. 创建 100W 连接速度不是很快,这说明 TCP 连接创建有成本(3 次握手,都是网络 IO);
  2. jps找到对应的进程的id,在用sudo cat /proc/{进程ID}/status | grep VmHWM可以看到实际的内存占用。按照这种增长趋势,8G 内存空间可以轻轻松松存放 100W 个连接。

但是如果单机建立太多的连接,会报一个Cannot assign requested address的异常,这是因为客户端连接服务端时,操作系统要为每个客户端分配一个端口,上面的程序很快会把端口号用尽。

所以,我们可以得出一个结论:核心的问题是,通信需要缓冲区,通信需要 I/O。这是因为通信占用资源,连接本身占用资源少

# 有哪些好用的压测工具?

压力测试最常见的工具是 Apache Benchmark(简称 AB),在 Linux 下面可以通过包管理器安装 ab:

yum install httpd-tools
// 或
apt-get install apache2-utils
1
2
3

ab 安装好后,可以利用下面这条指令向某个网站发送并发 1000 的 10000 次请求:

ab -n 10000 -p 1000 https://example.com/
1

ab 是用 C 语言写的,作为一个随手就可以用的工具,它的设计非常简单,是一个单线程的工作模型,因此如果遇到阻塞情况,可能直接导致 ab 工具自己积压崩溃。

所以。这里我给你推荐一个 Java 生态好用的工具“JMeter”,拥有可视化的界面,如下图所示:

image-20220219163855802

这个工具在各个平台上都可以用,比 ab 稳定,有图形化界面,可以配置任意线程数量,还有可视化的图表支持。

# IP 协议的工作原理

IP 协议接收 IP 协议上方的 Host-To-Host 协议传来的数据,然后进行拆分,这个能力叫作分片(Fragmentation)。然后 IP 协议为每个片段(Fragment)增加一个 IP 头(Header),组成一个IP 封包(Datagram)。之后,IP 协议调用底层的局域网(数据链路层)传送数据。最后 IP 协议通过寻址路由能力最终把封包送达目的地。

# 分片(Fragmentation)

分片就是把数据切分成片。 IP 协议通过它下层的局域网(链路层)协议传输数据,因此需要适配底层传输网络的传输能力。数据太大通常就不适合底层网络传输,这就需要把大的数据切片。 当然也可能选择不切片,IP 协议提供了一个能力就是把封包标记为不切片,当底层网络看到不切片的封包,又没有能力传输的时候,就会丢弃这个封包。你要注意,在网络环境中往往存在多条路径,一条路径断了,说不定其他路径可以连通。

# 增加协议头(IP Header)

切片完成之后,IP 协议会为每个切片(数据封包 Datagram)增加一个协议头。一个 IPv4 的协议头看上去就是如下图所示的样子:

image-20220219164729435

其中分成 4 个部分。

  • 最重要的是原地址和目标地址。IPv4 的地址是 4 组 8 位的数字,总共是 32 位。
  • Type Of Service 服务的类型,是为了响应不同的用户诉求,用来选择延迟、吞吐量和丢包率之间的关系。
  • IHL(Internet Header Length)用来描述 IP 协议头的大小。所以 IP 协议头的大小是可变的。IHL 只有 4 位,最大值 1111 = 15。最大是 15 个双字(15*4 字节 = 60 字节)。
  • Total Length 定义报文(封包 Datagram)的长度。
  • Identification(报文的 ID),发送方分配,代表顺序。
  • Fragment offset 描述要不要分包(拆分),以及如何拆分。
  • Time To Live 描述封包存活的时间。因此每个 IP 封包发送出去后,就开始销毁倒计时。如果倒计时为 0,就会销毁。比如中间的路由器看到一个 TTL 为 0 的封包,就直接丢弃。
  • Protocol 是描述上层的协议,比如 TCP = 6,UDP = 17。
  • Options 代表可选项。
  • Checksum 用来检验封包的正确性,如果 Checksum 对不上,就需要选择丢弃这个封包。

# “鱼和熊掌”不能兼得——延迟、吞吐量、丢包率

上面我们看到 IPv4 协议中提供了一个叫作 Type of Service(服务类型)的字段。这个字段是为了在延迟、吞吐量和丢包率三者间选择。

延迟(latency)

延迟指的是 1 bit 的数据从网络的一个终端传送到另一个终端需要的时间。这个时间包括在发送端准备发送的时间、排队发送的时间、发送数据的时间、数据传输的时间等。

吞吐量(Throughput)

吞吐量指单位时间内可以传输的平均数据量。比如用 bit/s 作为单位,就是 bps。吞吐量和延迟没有联系,比如延迟很高的网络,有可能吞吐量很高。可以类比成水管很大流速很慢,对比水管很细流速很快,这两种情况,最终流量可以是相等的。

丢包率(Packet loss)

丢表率指发送出去的封包没有到达目的地的比例。 在最大流速确定的网络中,丢表率会直接影响吞吐量。

我们的网络有时候需要低延迟,比如玩一款 RTS 游戏或者 Moba 游戏,这种时候延迟非常重要。另外如果把延迟看作一个平均指标,丢包也会影响延迟——一个包丢了,需要重发。而有的应用需要高吞吐量,延迟不是很重要,比如说网盘下载文件。大部分应用期望丢包不能太严重,比如语音电话,少量丢包还能听清,大量丢包就麻烦了,根本听不清对方说什么。严格希望不丢包的应用比较少,只有极特殊的网络控制管理场景,才需要在互联网层要求不丢包。

当然这三个条件,通常不能同时满足。如果同时追求延迟、吞吐量、丢包率,那么对网络设备的要求就会非常高,说白了就会非常贵。因此 IP 协议头中的 Type of Service 字段里,有以下 4 种主要的类型可以选择:

  • 低延迟
  • 高吞吐量
  • 低丢包率
  • 低成本

# IPv4 协议:路由和寻址的区别是什么?

# 寻址

寻址要做的就是:给一个地址,然后找到这个东西。IPv4 协议的寻址过程是逐级寻址

# 寻址过程

寻址就是如何根据 IP 地址找到设备。因为 IPv4 的世界中,网络是一个树状模型。顶层有多个平行的网络,每个网络有自己的网络号。然后顶层网络下方又有多个子网,子网下方还有子网,最后才是设备。

image-20220219165555124

IP 协议的寻址过程需要逐级找到网络,最后定位设备。下面我们具体分析下这个过程。

步骤 1:找到顶层网络

比如103.16.3.1最顶层的网络号可以和255.0.0.0(子网掩码)做位与运算得到,如下所示

103.16.3.1 & 255.0.0.0 = 103.0.0.0
1

因此103.0.0.0就是103.16.3.1所在的顶层网络。255.0.0.0称作子网掩码。子网掩码的作用就是帮助根据 IP 地址找到对应子网。子网掩码是很多个1接着很多个0,和 IP 地址一起使用。

步骤 2:找到下一层网络

接下来要找到下一级网络,就需要用 IP 地址和下一级的子网掩码做位与运算。 比如:

103.16.3.1 & 255.255.0.0 = 103.16.0.0
1

其中103.16.0.0就是下一级的网络号。

步骤 3:找到再下一级网络

接下来使用255.255.255.0子网掩码找到下一级网络是103.16.3.0

步骤 4:定位设备

设备就在子网103.16.3.0中,最终找到的设备号是1

当然子网掩码也不一定都是255,比如这个子网掩码255.240.0.0也是可以的。但通常我们把 IPv4 的网络分成这样 4 层。

# 路由(Routing)

在寻址过程中,数据总是存于某个局域网中。如果目的地在局域网中,就可以直接定位到设备了。如果目的地不在局域网中,这个时候,就需再去往其他网络。

由于网络和网络间是网关在连接,因此如果目的地 IP 不在局域网中,就需要为 IP 封包选择通往下一个网络的路径,其实就是选择其中一个网关。你可能会问:网关有多个吗?如果一个网络和多个网络接壤,那自然需要多个网关了。下图中,路由器在选择 IP 封包下一个应该是去往哪个 Gateway?

image-20220219165933863

假如,我们要为 IP 地址 14.215.177.38 寻址,当前路由器所在的网络的编号是 16.0.0.0。那么我们就需要知道去往 14.0.0.0 网络的 Gateway IP 地址。

如果你在当前网络中用route查看路由表,可能可以看到一条下面这样的记录。

  • Destination:14.0.0.0
  • Gateway:16.12.1.100
  • Mask:255.0.0.0
  • Iface:16.12.1.1

这条记录就说明如果你要去往 14.0.0.0 网络,IP 地址 14.215.177.38 先要和 255.0.0.0 进行位运算,然后再查表,看到 14.0.0.0,得知去往 Gateway 的网卡(IFace)是 16.12.1.1。

当封包去向下一个节点后,会进入新的路由节点,然后会继续上述路由过程,直到最终定位到设备。

# 下面这几个地址 127.0.0.1, localhost, 0.0.0.0 有什么不同?

127.0.0.1是本地回环地址(loopback),发送到 loopback 的数据会被转发到本地应用。

localhost 指代的是本地计算机,用于访问绑定在 loopback 上的服务。localhost 是一个主机名,不仅仅可以指向 IPv4 的本地回环地址,也可以指向 IPv6 的本地回环地址 [::1]。

0.0.0.0是一个特殊目的 IP 地址,称作不可路由 IP 地址,它的用途会被特殊规定。通常情况下,当我们把一个服务绑定到0.0.0.0,相当于把服务绑定到任意的 IP 地址。比如一台服务器上有多个网卡,不同网卡连接不同的网络,如果服务绑定到 0.0.0.0 就可以保证服务在多个 IP 地址上都可以用。

# IPv6 的寻址

接下来我们讨论下寻址,和 IPv4 相同,寻址的目的是找到设备,以及规划到设备途经的路径。和 IPv4 相同,IPv6 寻址最核心的内容就是要对网络进行划分。IPv6 地址很充裕,因此对网络的划分和 IPv4 有很显著的差异。

IPv6 的寻址分成了几种类型:

  • 全局单播寻址(和 IPv4 地址作用差不多,在互联网中通过地址查找一个设备,简单来说,单播就是 1 对 1);
  • 本地单播(类似 IPv4 里的一个内部网络,要求地址必须以fe80开头,类似我们 IPv4 中127开头的地址);
  • 分组多播(Group Multicast),类似今天我们说的广播,将消息发送给多个接收者;
  • 任意播(Anycast),这个方式比较特殊,接下来我们会详细讲解。

# 全局单播

image-20220219172058354

全局单播,就是将消息从一个设备传到另一个设备,这和 IPv4 发送/接收消息大同小异。而全局单播地址,目标就是定位网络中的设备,这个地址和 IPv4 的地址作用相同,只不过格式略有差异。总的来说,IPv6 地址太多,因此不再需要子网掩码,而是直接将 IPv6 的地址分区即可

在实现全局单播时,IPv6 地址通常分成 3 个部分:

  • 站点前缀(Site Prefix)48bit,一般是由 ISP(Internet Service Providor,运营商)或者 RIR(Regional Internet Registry, 地区性互联网注册机构),RIR 将 IP 地址分配给运营商;
  • 子网号(Subnet ID),16bit,用于站点内部区分子网;
  • 接口号(Interface ID), 64bit,用于站点内部区分设备。

因此 IPv6 也是一个树状结构,站点前缀需要一定资质,子网号和接口号内部定义。IPv6 的寻址过程就是先通过站点前缀找到站点,然后追踪子网,再找到接口(也就是设备的网卡)。

从上面全局单播的分区,我们可以看出,IPv6 分给站点的地址非常多。一个站点,有 16bit 的子网,相当于 65535 个子网;每个子网中,还可以用 64 位整数表示设备。

# 本地单播

理论上,虽然 IPv6 可以将所有的设备都连入一个网络。但在实际场景中,很多公司还是需要一个内部网络的。这种情况在 IPv6 的设计中属于局域网络。

在局域网络中,实现设备到设备的通信,就是本地单播。IPv6 的本地单播地址组成如下图所示:

image-20220219172119988

这种协议比较简单,本地单播地址必须以fe80开头,后面 64 位的 0,然后接上 54 位的设备编号。上图中的 Interface 可以理解成网络接口,其实就是网卡。

# 分组多播

有时候,我们需要实现广播。所谓广播,就是将消息同时发送给多个接收者。

IPv6 中设计了分组多播,来实现广播的能力。当 IP 地址以 8 个 1 开头,也就是ff00开头,后面会跟上一个分组的编号时,就是在进行分组多播。

这个时候,我们需要一个广播设备,在这个设备中已经定义了这些分组编号,并且拥有分组下所有设备的清单,这个广播设备会帮助我们将消息发送给对应分组下的所有设备。

# 任意播(Anycast)

任意播,本质是将消息发送给多个接收方,并选择一条最优的路径。这样说有点抽象,接下来我具体解释一下。

比如说在一个网络中有多个授时服务,这些授时服务都共享了一个任播地址。当一个客户端想要获取时间,就可以将请求发送到这个任播地址。客户端的请求扩散出去后,可能会找到授时服务中的一个或者多个,但是距离最近的往往会先被发现。这个时候,客户端就使用它第一次收到的授时信息修正自己的时间。

# IPv4 网络和 IPv6 网络通信

例如一个 IPv6 的客户端,想要访问 IPv4 的服务器,步骤如下图所示:

image-20220219172224877

  1. 客户端通过 DNS64 服务器查询 AAAA 记录。DNS64 是国际互联网工程任务组(IETF)提供的一种解决 IPv4 和 IPv6 兼容问题的 DNS 服务。这个 DNS 查询服务会把 IPv4 地址和 IPv6 地址同时返回
  2. DNS64 服务器返回含 IPv4 地址的 AAAA 记录。
  3. 客户端将对应的 IPv4 地址请求发送给一个 NAT64 路由器
  4. 由这个 NAT64 路由器将 IPv6 地址转换为 IPv4 地址,从而访问 IPv4 网络,并收集结果。
  5. 消息返回到客户端。

# 两个 IPv6 网络被 IPv4 隔离

这种情况在普及 IPv6 的过程中比较常见,IPv6 的网络一开始是一个个孤岛,IPv6 网络需要通信,就需要一些特别的手段。

不知道你有没有联想到坐火车穿越隧道的感觉,连接两个孤岛 IPv6 网络,其实就是在 IPv4 网络中建立一条隧道。如下图所示:

image-20220219172303161

隧道的本质就是在两个 IPv6 的网络出口网关处,实现一段地址转换的程序

# IPv6 协议:Tunnel 技术是什么?

Tunnel 就是隧道,这和现实中的隧道是很相似的。隧道不是只有一辆车通过,而是每天都有大量的车辆来来往往。两个网络,用隧道连接,位于两个网络中的设备通信,都可以使用这个隧道。隧道是两个网络间用程序定义的一种通道。具体来说,如果两个 IPv6 网络被 IPv4 分隔开,那么两个 IPv6 网络的出口处(和 IPv4 网络的网关处)就可以用程序(或硬件)实现一个隧道,方便两个网络中设备的通信。

# 请你总结下 IPv6 和 IPv4 究竟有哪些区别

IPv6 和 IPv4 最核心的区别是地址空间大小不同。IPv6 用 128 位地址,解决了 IP 地址耗尽问题。因为地址空间大小不同,它们对地址的定义,对路由寻址策略都有显著的差异。

在路由寻址策略上,IPv6 消除了设备间地址冲突的问题,改变了划分子网的方式。在 IPv4 网络中,一个局域网往往会共享一个公网 IP,因此需要 NAT 协议和外网连接。

在划分子网的时候,IPv4 地址少,需要子网掩码来处理划分子网。IPv6 有充足的地址,因此不需要局域网共享外网 IP。也正因为 IPv6 地址多,可以直接将 IPv6 地址划分成站点、子网、设备,每个段都有充足的 IP 地址。

因为 IPv6 支持的 IP 地址数量大大上升,一个子网可以有 248 个 IP 地址,这个子网可能是公司网络、家庭网络等。这样 IP 地址的分配方式也发生了变化,IPv4 网络中设备分配 IP 地址的方式是中心化的,由 DHCP(动态主机协议)为局域网中的设备分配 IP 地址。而在 IPv6 网络中,因为 IP 地址很少发生冲突,可以由设备自己申请自己的 IP 地址。

另外因为 IPv6 中任何一个节点都可以是一个组播节点,这样就可以构造一个对等的网络,也就是可以支持在没有中心化的路由器,或者一个网络多个路由器的情况下工作。节点可以通过向周围节点类似打探消息的方式,发现更多的节点。这是一个配套 IPv6 的能力,叫作邻居发现(ND)。

# 同一个局域网中的设备如何交换消息。

首先,我们先明确一个概念,设备间通信的本质其实是设备拥有的网络接口(网卡)间的通信。为了区别每个网络接口,互联网工程任务组(IETF)要求每个设备拥有一个唯一的编号,这个就是 MAC 地址

你可能会问:IP 地址不也是唯一的吗?其实不然,一旦设备更换位置,比如你把你的电脑从北京邮寄的广州,那么 IP 地址就变了,而电脑网卡的 MAC 地址不会发生变化。总的来说,IP 地址更像现实生活中的地址,而 MAC 地址更像你的身份证号。

然后,我们再明确另一个基本的概念。在一个局域网中,我们不可以将消息从一个接口(网卡)发送到另一个接口(网卡),而是要通过交换机。为什么是这样呢?因为两个网卡间没有线啊!所以数据交换,必须经过交换机,毕竟线路都是由网卡连接交换机的。

总结下,数据的发送方,将自己的 MAC 地址、目的地 MAC 地址,以及数据作为一个分组(Packet),也称作 Frame 或者封包,发送给交换机。交换机再根据目的地 MAC 地址,将数据转发到目的地的网络接口(网卡)。

最后一个问题,你可能问,这个分组或者 Frame,是不是 IP 协议的分组呢?——不是,这里提到的是链路层的数据交换,它支持 IP 协议工作,是网络层的底层。所以,如果 IP 协议要传输数据,就要将数据转换成为链路层的分组,然后才可以在链路层传输

链路层分组大小受限于链路层的网络设备、线路以及使用了链路层协议的设计。你有时候可能会看到 MTU 这个缩写词,它指的是 Maximun Transmission Unit,最大传输单元,意思是链路层网络允许的最大传输数据分组的大小。因此 IP 协议要根据 MTU 拆分封包

交换机,或者称为链路层交换机,通常工作在链路层;而路由器通常也具有交换机的能力,工作在网络层和链路层

MSS(Maximun Segment Size,最大段大小)是 TCP 段,或者称为 TCP 分组(TCP Packet)的最大大小。MSS 是传输层概念,MTU 是链路层概念

你可能会意识到,这不就是下面这样一个数学关系吗?

MTU = MSS + TCP Header + IP Header
1

这个思路有一定道理,但是不对。先说说这个思路怎么来的,你可能会这么思考:TCP 传输数据大于 MSS,就拆包。每个封包加上 TCP Header ,之后经过 IP 协议,再加上 IP Header。于是这个加上 IP 头的分组(Packet)不能超过 MTU。固然这个思路很有道理,可惜是错的。因为 TCP 解决的是广域网的问题,MTU 是一个链路层的概念,要知道不同网络 MTU 是不同的,所以二者不可能产生关联。这也是为什么 IP 协议还可能会再拆包的原因

# 地址解析协议(ARP)

上面我们讨论了 MAC 地址,链路层通过 MAC 地址定位网络接口(网卡)。在一个网络接口向另一个网络接口发送数据的时候,至少要提供这样 3 个字段:

  1. 源 MAC 地址
  2. 目标 MAC 地址
  3. 数据

这里我们一起再来思考一个问题,对于一个网络接口,它如何能知道目标接口的 MAC 地址呢?我们在使用传输层协议的时候,清楚地知道目的地的 IP 地址,但是我们不知道 MAC 地址。这个时候,就需要一个中间服务帮助根据 IP 地址找到 MAC 地址——这就是地址解析协议(Address Resolution Protocol,ARP)。

整个工作过程和 DNS 非常类似,如果一个网络接口已经知道目标 IP 地址对应的 MAC 地址了,它会将数据直接发送给交换机,交换机将数据转发给目的地,这个过程如下图所示:

image-20220219182711673

那么如果网络接口不知道目的地地址呢?这个时候,地址解析协议就开始工作了。发送接口会发送一个广播查询给到交换机,交换机将查询转发给所有接口。image-20220219182749004

如果某个接口发现自己就是对方要查询的接口,则会将自己的 MAC 地址回传。接下来,会在交换机和发送接口的 ARP 表中,增加一个缓存条目。也就是说,接下来发送接口再次向 IP 地址 2.2.2.2 发送数据时,不需要再广播一次查询了。

image-20220219182900321

前面提到这个过程和 DNS 非常相似,采用的是逐级缓存的设计减少 ARP 请求。发送接口先查询本地的 ARP 表,如果本地没有数据,然后广播 ARP 查询。这个时候如果交换机中有数据,那么查询交换机的 ARP 表;如果交换机中没有数据,才去广播消息给其他接口。注意,ARP 表是一种缓存,也要考虑缓存的设计。通常缓存的设计要考虑缓存的失效时间、更新策略、数据结构等。

比如可以考虑用 TTL(Time To Live)的设计,为每个缓存条目增加一个失效时间。另外,更新策略可以考虑利用老化(Aging)算法模拟 LRU。

最后请你思考路由器和交换机的异同点。不知道你有没有在网上订购过家用无线路由器,通常这种家用设备也会提供局域网,具备交换机的能力。同时,这种设备又具有路由器的能力。所以,很多同学可能会分不清路由器和交换机。

总的来说,家用的路由器,也具备交换机的功能。但是当 ARP 表很大的时候,就需要专门的、能够承载大量网络接口的交换设备。就好比,如果用数组实现 ARP 表,数据量小的时候,遍历即可;但如果数据量大的话,就需要设计更高效的查询结构和设计缓存。

# 网络地址转换协议是如何工作的?

网络地址解析协议(NAT)解决的是内外网通信的问题。NAT 通常发生在内网和外网衔接的路由器中,由路由器中的 NAT 模块提供网络地址转换能力。从设计上看,NAT 最核心的能力,就是能够将内网中某个 IP 地址映射到外网 IP,然后再把数据发送给外网的服务器。当服务器返回数据的时候,NAT 又能够准确地判断外网服务器的数据返回给哪个内网 IP。

你可以思考下 NAT 是如何做到这点的呢?需要做两件事。

  1. NAT 需要作为一个中间层替换 IP 地址。 发送的时候,NAT 替换源 IP 地址(也就是将内网 IP 替换为出口 IP);接收的时候,NAT 替换目标 IP 地址(也就是将出口 IP 替换回内网 IP 地址)。
  2. NAT 需要缓存内网 IP 地址和出口 IP 地址 + 端口的对应关系。也就是说,发送的时候,NAT 要为每个替换的内网 IP 地址分配不同的端口,确保出口 IP 地址+ 端口的唯一性,这样当服务器返回数据的时候,就可以根据出口 IP 地址 + 端口找到内网 IP。

# IPv6 协议还需要 NAT 吗?

IPv6 解决了 IP 耗尽的问题,为机构、组织、公司、家庭等网络提供了充足的 IP 资源,从这个角度看是不是就不需要 NAT 协议了呢?

在没有 IPv6 之前,NAT 是 IP 资源耗尽的主流解决方案。在一个内网中的全部设备通过 NAT 协议共享一个外网的 IPv4 地址,是目前内外网对接的主要方式。IPv6 地址资源充足,可以给全球每个设备一个独立的地址。从这个角度看 IPv6 的确不需要 NAT 协议。

但是目前的情况,是 IPv6 网络还没有完全普及。尽管很多公司已经支持自己的互联网产品可以使用 IPv6 访问,但是公司内部员工使用的内部网络还是 IPv4。如果要连接 IPv6 和 IPv4 网络,仍然需要 NAT 协议(NAT64),这个协议可以让多个 IPv6 的设备共享一个 IPv4 的公网地址。

# Socket 是什么?

首先,Socket 是一种编程的模型。

下图中,从编程的角度来看,客户端将数据发送给在客户端侧的Socket 对象,然后客户端侧的 Socket 对象将数据发送给服务端侧的 Socket 对象。Socket 对象负责提供通信能力,并处理底层的 TCP 连接/UDP 连接。对服务端而言,每一个客户端接入,就会形成一个和客户端对应的 Socket 对象,如果服务器要读取客户端发送的信息,或者向客户端发送信息,就需要通过这个客户端 Socket 对象。

image-20220219205240771

但是如果从另一个角度去分析,Socket 还是一种文件,准确来说是一种双向管道文件。什么是管道文件呢?管道会将一个程序的输出,导向另一个程序的输入。那么什么是双向管道文件呢?双向管道文件连接的程序是对等的,都可以作为输入和输出。

比如下面这段服务端侧程序:

var serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress(80));
1
2

看起来我们创建的是一个服务端 Socket 对象,但如果单纯看这个对象,它又代表什么呢?如果我们理解成代表服务端本身合不合理呢——这可能会比较抽象,在服务端存在一个服务端 Socket。但如果我们从管道文件的层面去理解它,就会比较容易了。其一,这是一个文件;其二,它里面存的是所有客户端 Socket 文件的文件描述符。

当一个客户端连接到服务端的时候,操作系统就会创建一个客户端 Socket 的文件。然后操作系统将这个文件的文件描述符写入服务端程序创建的服务端 Socket 文件中。服务端 Socket 文件,是一个管道文件。如果读取这个文件的内容,就相当于从管道中取走了一个客户端文件描述符。

image-20220219205941806

如上图所示,服务端 Socket 文件相当于一个客户端 Socket 的目录,线程可以通过 accept() 操作每次拿走一个客户端文件描述符。拿到客户端文件描述符,就相当于拿到了和客户端进行通信的接口。

前面我们提到 Socket 是一个双向的管道文件,当线程想要读取客户端传输来的数据时,就从客户端 Socket 文件中读取数据;当线程想要发送数据到客户端时,就向客户端 Socket 文件中写入数据。客户端 Socket 是一个双向管道,操作系统将客户端传来的数据写入这个管道,也将线程写入管道的数据发送到客户端

有同学会说,那既然可以双向传送,这不就是两个单向管道被拼凑在了一起吗?这里具体的实现取决于操作系统,Linux 中的管道文件都是单向的,因此 Socket 文件是一种区别于原有操作系统管道的单独的实现。

总结下,Socket 首先是文件,存储的是数据。对服务端而言,分成服务端 Socket 文件和客户端 Socket 文件。服务端 Socket 文件存储的是客户端 Socket 文件描述符;客户端 Socket 文件存储的是传输的数据。读取客户端 Socket 文件,就是读取客户端发送来的数据;写入客户端文件,就是向客户端发送数据。对一个客户端而言, Socket 文件存储的是发送给服务端(或接收的)数据。

综上,Socket 首先是文件,在文件的基础上,又封装了一段程序,这段程序提供了 API 负责最终的数据传输

# I/O 多路复用技术

# 服务端 Socket 的绑定

为了区分应用,对于一个服务端 Socket 文件,我们要设置它监听的端口。比如 Nginx 监听 80 端口、Node 监听 3000 端口、SSH 监听 22 端口、Tomcat 监听 8080 端口。端口监听不能冲突,不然客户端连接进来创建客户端 Socket 文件,文件描述符就不知道写入哪个服务端 Socket 文件了。这样操作系统就会把连接到不同端口的客户端分类,将客户端 Socket 文件描述符存到对应不同端口的服务端 Socket 文件中。

因此,服务端监听端口的本质,是将服务端 Socket 文件和端口绑定,这个操作也称为 bind。有时候我们不仅仅绑定端口,还需要绑定 IP 地址。这是因为有时候我们只想允许指定 IP 访问我们的服务端程序。

# 扫描和监听

对于一个服务端程序,可以定期扫描服务端 Socket 文件的变更,来了解有哪些客户端想要连接进来。如果在服务端 Socket 文件中读取到一个客户端的文件描述符,就可以将这个文件描述符实例化成一个 Socket 对象。

image-20220219210435129

之后,服务端可以将这个 Socket 对象加入一个容器(集合),通过定期遍历所有的客户端 Socket 对象,查看背后 Socket 文件的状态,从而确定是否有新的数据从客户端传输过来。

image-20220219210453472

上述的过程,我们通过一个线程就可以响应多个客户端的连接,也被称作I/O 多路复用技术

总结一下,Socket 既是一种编程模型,或者说是一段程序,同时也是一个文件,一个双向管道文件。你也可以这样理解,Socket API 是在 Socket 文件基础上进行的一层封装,而 Socket 文件是操作系统提供支持网络通信的一种文件格式。

在服务端有两种 Socket 文件,每个客户端接入之后会形成一个客户端的 Socket 文件,客户端 Socket 文件的文件描述符会存入服务端 Socket 文件。通过这种方式,一个线程可以通过读取服务端 Socket 文件中的内容拿到所有的客户端 Socket。这样一个线程就可以负责响应所有客户端的 I/O,这个技术称为 I/O 多路复用。

主动式的 I/O 多路复用,对负责 I/O 的线程压力过大,因此通常会设计一个高效的中间数据结构作为 I/O 事件的观察者,线程通过订阅 I/O 事件被动响应,这就是响应式模型。在 Socket 编程中,最适合提供这种中间数据结构的就是操作系统的内核,事实上 epoll 模型也是在操作系统的内核中提供了红黑树结构。

# epoll 为什么用红黑树

在 Linux 的设计中有三种典型的 I/O 多路复用模型 select、poll、epoll。

select 是一个主动模型,需要线程自己通过一个集合存放所有的 Socket,然后发生 I/O 变化的时候遍历。在 select 模型下,操作系统不知道哪个线程应该响应哪个事件,而是由线程自己去操作系统看有没有发生网络 I/O 事件,然后再遍历自己管理的所有 Socket,看看这些 Socket 有没有发生变化。

poll 提供了更优质的编程接口,但是本质和 select 模型相同。因此千级并发以下的 I/O,你可以考虑 select 和 poll,但是如果出现更大的并发量,就需要用 epoll 模型。

epoll 模型在操作系统内核中提供了一个中间数据结构,这个中间数据结构会提供事件监听注册,以及快速判断消息关联到哪个线程的能力(红黑树实现)。因此在高并发 I/O 下,可以考虑 epoll 模型,它的速度更快,开销更小。

# BIO、NIO 和 AIO 有什么区别

总的来说,这三者是三个 I/O 的编程模型。BIO 接口设计会直接导致当前线程阻塞。NIO 的设计不会触发当前线程的阻塞。AIO 为 I/O 提供了异步能力,也就是将 I/O 的响应程序放到一个独立的时间线上去执行。但是通常 AIO 的提供者还会提供异步编程模型,就是实现一种对异步计算封装的数据结构,并且提供将异步计算同步回主线的能力。

通常情况下,这 3 种 API 都会伴随 I/O 多路复用。如果底层用红黑树管理注册的文件描述符和事件,可以在很小的开销内由内核将 I/O 消息发送给指定的线程。另外,还可以用 DMA,内存映射等方式优化 I/O。

# 缓冲区的 flip 是怎么回事?

flip 操作意味翻转,是切换缓冲区的读写状态,在 flip 操作中,通常将 position 指针置 0,limit 指针不变。

# 在缓冲区的设计当中,还通常有一个 rewind 操作,这个操作是用来做什么的呢?

切换到读取状态可以使用 flip 操作。如果一个缓冲区进行了一次写和读,接下来要用它来处理另一批数据,可以使用 clear 操作来清空缓冲区。在实战当中,有时候一个缓冲区读取过了,需要再读取一次,此时就可以用 rewind 操作来重置缓冲区的 position 指针。

# I/O 多路复用用协程和用线程的区别?

线程是执行程序的最小单位。I/O 多路复用时,会用单个线程处理大量的 I/O。还有一种执行程序的模型,叫做协程,协程是轻量级的线程。操作系统将执行资源分配给了线程,然后再调度线程运行。如果要实现协程,就要利用分配给线程的执行资源,在这之上再创建更小的执行单位。协程不归操作系统调度,协程共享线程的执行资源。

而 I/O 多路复用的意义,是减少线程间的切换成本。因此从设计上,只要是用单个线程处理大量 I/O 工作,线程和协程是一样的,并无区别。如果是单线程处理大量 I/O,使用协程也是依托协程对应线程执行能力。

# 面试中如何回答“怎样实现 RPC 框架”的问题?

总结下,设计一个 RPC 框架最基础的能力就是实现远程方法的调用。这里需要一个调用约定,比如怎么描述一个远程的方法,发送端怎么传递参数,接收方如何解析参数?如果发生异常应该如何处理?具体来说,这些事情都不难实现,只是比较烦琐。其实不仅仅在 RPC 调用时有调用约定,编译器在实现函数调用的时候,也会有调用约定。另外,还有一些在 RPC 基础上建立起来的更复杂、更体系化的约定,比如说面向服务架构(SOA)。

在实现了基本调用能力的基础上,接下来就是提供服务的注册、发现能力。有了这两个能力,就可以向客户端完全屏蔽服务的部署细节,并衍生出容灾、负载均衡的设计。

当然,程序员还需要思考底层具体网络的传输问题。如果用 TCP 要思考多路复用以及连接数量的问题;如果是 UDP,需要增加对于可靠性保证的思考。如果使用了消息队列,还需要考虑服务的幂等性设计等。

# 域名分级和数据分区

我们知道中文字典可以按照偏旁部首以及拼音索引,和字典类似,根服务器提供的目录也有一定的索引规则。在域名的世界中,通过分级域名的策略建立索引。伴随着域名的分级策略,实际上是域名数据库的拆分。通过域名的分级,可以将数据库划分成一个个区域。

平时我们看到的.com``.cn``.net等,称为顶级域名。比如对于 www.laogu.com 这个网址来说,com是顶级域名,lagou是二级域名,www是三级域名。域名分级当然是为了建立目录和索引,并对数据存储进行分区。

image-20220219220023230

从上图中可以看到,DNS 的存储设计是一个树状结构。叶子节点中才存放真实的映射关系,中间节点都是目录。存储分成 3 层:

  • 顶部第一级是根 DNS 存储,存储的是顶级域的目录,被称作根 DNS 服务器
  • 第二级是顶级域存储,存储的是二级域的目录,被称作顶级域 DNS 服务器(Top Level DNS,TLD)
  • 最后一级是叶子节点,存储的是具体的 DNS 记录,也被称作权威 DNS 服务器

# DNS 查询过程

当用户在浏览器中输入一个网址,就会触发 DNS 查询。这个时候在上述的 3 个层级中,还会增加本地 DNS 服务器层级。本地 DNS 服务器包括用户自己路由器中的 DNS 缓存、小区的 DNS 服务器、ISP 的 DNS 服务器等。

查询过程如下图所示:

image-20220219215827248

结合上图展示的 DNS 查询过程,我们再来具体介绍一下 。

  1. 用户输入网址,查询本地 DNS。本地 DNS 是一系列 DNS 的合集,比如 ISP 提供的 DNS、公司网络提供的 DNS。本地 DNS 是一个代理,将 DNS 请求转发到 DNS 网络中。如果本地 DNS 中已经存在需要的记录,也就是本地 DNS 缓存中找到了对应的 DNS 条目,就会直接返回,而跳过之后的步骤。
  2. 客户端请求根 DNS 服务器。如果本地 DNS 中没有对应的记录,那么请求会被转发到根 DNS 服务器。根 DNS 服务器只解析顶级域,以“www.lagou.com (opens new window)”为例,根 DNS 服务器只看 com 部分。
  3. 根 DNS 服务器返回顶级 DNS 服务器的 IP。
  4. 客户端请求顶级 DNS 服务器,顶级 DNS 服务器中是具体域名的目录。
  5. 顶级 DNS 服务器返回权威 DNS 服务器的 IP。
  6. 客户端请求权威 DNS 服务器。在权威 DNS 服务器上存有具体的 DNS 记录。以 lagou 为例,权威 DNS 服务器中可能有和 lagou.com 相关的上百条甚至更多的 DNS 记录,会根据不同的 DNS 查询条件返回。
  7. 权威 DNS 服务器返回 DNS 记录到本地 DNS 服务器。
  8. 本地 DNS 服务器返回具体的 DNS 记录给客户端。

在上述 8 个过程全部结束后,客户端通过 DNS 记录中的 IP 地址,可以找到请求服务的主机。在本文的例子中,客户端最终可以找到拉勾网对应的 IP 地址,从而获得 Web 服务。

# 常见的 DNS 记录类型

DNS 记录具体长什么样子:

; 定义www.example.com的ip地址
www.example.com.     IN     A     139.18.28.5;
1
2

上面的就是一条 DNS 记录,纯文本即可。IN 代表记录用于互联网,是 Intenet 的缩写。在历史上 Internet 起源于阿帕网,在同时代有很多竞争的网络,IN 这个描述也就保留了下来。

www.example.com 是要解析的域名。 (opens new window)A 是记录的类型,A 记录代表着这是一条用于解析 IPv4 地址的记录。从这条记录可知,www.example.com (opens new window)的 IP 地址是 139.18.28.5。;是语句块的结尾,也是注释。

那么除了 A 记录,还有哪些 DNS 记录的类型呢?DNS 记录的类型非常多,有 30 多种。其中比较常见的有 A、AAAA、CNAME、MX,以及 NS 等。接下来我为你一个个介绍。

# CNAME

CNAME(Canonical Name Record)用于定义域名的别名,如下面这条 DNS 记录:

; 定义www.example.com的别名
a.example.com.          IN     CNAME   b.example.com.
1
2

这条 DNS 记录定义了 a.example.com 是 b.example.com 的别名。用户在浏览器中输入 a.example.com 时候,通过 DNS 查询会知道 a.example.com 是 b.example.com 的别名,因此需要实际 IP 的时候,会去拿 b.example.com 的 A 记录。

这样用户如果在浏览器中输入 a.example.com 实际打开的就是 b.example.com。因为走的是 DNS 查询的路径,速度很快(因为有缓存),不需要 HTTP 重定向等操作。

当你想把一个网站迁移到新域名,旧域名仍然保留的时候;还有当你想将自己的静态资源放到 CDN 上的时候,CNAME 就非常有用。

# AAAA 记录

前面我们提到,A 记录是域名和 IPv4 地址的映射关系。和 A 记录类似,AAAA 记录则是域名和 IPv6 地址的映射关系。

# MX 记录(Mail Exchanger Record)

MX 记录是邮件记录,用来描述邮件服务器的域名。

在工作中,我们经常会发邮件到某个同事的邮箱。比如说,发送一封邮件到 xiaoming@lagou.com,那么拉勾网如何知道哪个 IP 地址是邮件服务器呢?

这个时候就可以用到下面这条 MX 记录:

IN MX mail.lagou.com
1

这样凡是 @lagou 的邮件都会发送到 mail.lagou.com 中,而 mail.lagou.com 的 IP 地址可以通过查询 mail.lagou.com 的 A 记录和 AAAA 记录获得。

# NS 记录

NS(Name Server)记录是描述 DNS 服务器网址。从 DNS 的存储结构上说,Name Server 中含有权威 DNS 服务的目录。也就是说,NS 记录指定哪台 Server 是回答 DNS 查询的权威域名服务器。

当一个 DNS 查询看到 NS 记录的时候,会再去 NS 记录配置的 DNS 服务器查询,得到最终的记录。如下面这个例子:

a.com.     IN      NS      ns1.a.com.
a.com.     IN      NS      ns2.a.com.
1
2

当解析 a.com 地址时,我们看到 a.com 有两个 NS 记录,所以确定最终 a.com 的记录在 ns1.a.com 和 ns2.a.com 上。从设计上看,ns1 和 ns2 是网站 a.com 提供的智能 DNS 服务器,可以提供负载均衡、分布式 Sharding 等服务。比如当一个北京的用户想要访问 a.com 的时候,ns1 看到这是一个北京的 IP 就返回一个离北京最近的机房 IP。

上面代码中 a.com 配置了两个 NS 记录。通常 NS 不会只有一个,这是为了保证高可用,一个挂了另一个还能继续服务。通常数字小的 NS 记录优先级更高,也就是 ns1 会优先于 ns2 响应。

配置了上面的 NS 记录后,如果还配置了 a.com 的 A 记录,那么这个 A 记录会被 NS 记录覆盖。

# 如果你的应用需要智能 DNS 服务,你将如何实现?

首先你可以在你的域名解析系统中增加两条(或以上)ns 记录。比如说你的域名是 example.com,那么你可以增加 ns1.exmaple.com, ns2.example.com。当然,指定这两个域名的 IP 还需要配置两个 A 记录。

然后你需要两台机器(也可以是容器或者虚拟机),对应 ns1 和 ns2。最好用不在同一个物理机上的两个容器,这样可以避免一台物理机故障导致服务瘫痪。然后在每个容器(虚拟机)上安装一个 Named 服务。Named 是一个专门用来提供 DNS 服务的工具,在虚拟机上安装完成 Named 后,这个虚拟机就变成了一个权威服务器节点。

配置好 Named 后,你需要写几个脚本文件,给要提供 DNS 的域名配置信息。Named 配套使用的有个叫作 GeoDNS 的插件,可以提供基于地理位置的智能 DNS 服务。

更具体的操作,你可以参考这篇文档:https://bind9.readthedocs.io/en/latest/configuration.html (opens new window)

# CDN 是什么?

和域名系统类似,内容分发网络(Content Dilivery Network,CDN)是一个专门用来分发内容的分布式应用。CDN 构建在现有的互联网之上,通过在各地部署数据中心,让不同地域的用户可以就近获取内容。这里的内容通常指的是文件、图片、视频、声音、应用程序安装包等,它们具有一个显著的特征——无状态,或者说是静态的。这些资源不像订单数据、库存数据等,它们一旦发布,就很少会发生变化。另一个显著的特征,是这些资源往往会被大量的用户需要,因此分发它们的流量成本是较高的。

为什么不能集中提供这些静态资源呢?这和域名系统的 DNS 记录不能集中提供是一个道理,需要考虑到流量、单点故障、延迟等因素。在离用户更近的地理位置提供资源,可以减少延迟。按照地理位置分散地提供资源,也可以降低中心化带来的服务压力。

因此,CDN 的服务商会选择在全球布点,或者在某个国家布点。具体要看 CDN 服务提供商的服务范围。目前国内的阿里云、腾讯云等也在提供 CDN 业务。

# 内容的分发

CDN 是一个分布式的内容分发网络。当用户请求一个网络资源时,用户请求的是 CDN 提供的资源。和域名系统类似,当用户请求一个资源时,首先会接触到一个类似域名系统中目录的服务,这个服务会告诉用户究竟去哪个 IP 获取这个资源。

事实上,很多大型的应用,会把 DNS 解析作为一种负载均衡的手段。当用户请求一个网址的时候,会从该网站提供的智能 DNS 中获取网站的 IP。例如当你请求拉勾的时候,具体连接到哪个拉勾的 IP,是由拉勾使用的智能 DNS 服务决定的。域名系统允许网站自己为自己的产品提供 DNS 解析

所以总体静态资源的使用路径如下图所示:

image-20220219220933871

当用户请求一个静态资源的时候,首先会触发域名系统的解析。域名系统会将解析的责任交由 CDN 提供商来处理,CDN 的智能 DNS 服务会帮助用户选择离自己距离最近的节点,返回这个节点的 A(或 AAAA)记录。然后客户端会向 CDN 的资源节点发起请求,最终获得资源。

在上面整个过程当中,CDN 的智能 DNS 还充当了负载均衡的作用。如果一个节点压力过大,则可以将流量导向其他的节点。

# CDN 回源

CDN 回源就是 CDN 节点到源站请求资源,重新设置缓存。通常服务提供方在使用 CDN 的时候,会在自己的某个域名发布静态资源,然后将这个域名交给 CDN。

比如源站在 s.example.com 中发布静态资源,然后在 CDN 管理后台配置了这个源站。在使用 CDN 时,服务提供方会使用另一个域名,比如说 b.example.com。然后配置将 b.example.com 用 CNAME 记录指向 CDN 的智能 DNS。这个时候,如果用户下载 b.example.com/a.jpg,CDN 的智能 DNS 会帮用户选择一个最优的 IP 地址(最优的 CDN 节点)响应这次资源的请求。如果这个 CDN 节点没有 a.jpg,CDN 就会到 s.example.com 源站去下载,缓存到 CDN 节点,然后再返回给用户。

CDN 回源有 3 种情况,一种是 CDN 节点没有对应资源时主动到源站获取资源;另一种是缓存失效后,CDN 节点到源站获取资源;还有一种情况是在 CDN 管理后台或者使用开放接口主动刷新触发回源。

# 浏览器缓存:强制缓存和协商缓存

缓存的资源文件到什么地方去了呢?

那么首先来看下 memory cache 和 disk cache

memory cache: 它是将资源文件缓存到内存中。等下次请求访问的时候不需要重新下载资源,而是直接从内存中读取数据。

disk cache: 它是将资源文件缓存到硬盘中。等下次请求的时候它是直接从硬盘中读取。

那么他们两则的区别是?

**memory cache(内存缓存)退出进程时数据会被清除,而disk cache(硬盘缓存)**退出进程时数据不会被清除。内存读取比硬盘中读取的速度更快。但是我们也不能把所有数据放在内存中缓存的,因为内存也是有限的。

**memory cache(内存缓存)**一般会将脚本、字体、图片会存储到内存缓存中。 disk cache(硬盘缓存) 一般非脚本会存放在硬盘中,比如 css 这些。

缓存读取的原理:先从内存中查找对应的缓存,如果内存中能找到就读取对应的缓存,否则的话就从硬盘中查找对应的缓存,如果有就读取,否则的话,就重新网络请求。

# 强制缓存

**基本原理:**浏览器在加载资源的时候,会先根据本地缓存资源的 header 中的信息(Expires 和 Cache-Control)来判断是否需要强制缓存。如果命中的话,则会直接使用缓存中的资源。否则的话,会继续向服务器发送请求。

Expires

Expires 是 http1.0 的规范,它的值是一个绝对时间的 GMT 格式的时间字符串。这个时间代表的该资源的失效时间,如果在该时间之前请求的话,则都是从缓存里面读取的。但是使用该规范时,可能会有一个缺点就是当服务器的时间和客户端的时间不一样的情况下,会导致缓存失效。

Cache-Control

Cache-Control 是 http1.1 的规范,它是利用该字段 max-age 值进行判断的。该值是一个相对时间,比如 Cache-Control: max-age=3600, 代表该资源的有效期是 3600 秒。除了该字段外,我们还有如下字段可以设置:

no-cache: 需要进行协商缓存,发送请求到服务器确认是否使用缓存。

**no-store:**禁止使用缓存,每一次都要重新请求数据。

**public:**可以被所有的用户缓存,包括终端用户和 CDN 等中间代理服务器。

**private:**只能被终端用户的浏览器缓存,不允许 CDN 等中继缓存服务器对其缓存。

Cache-Control 与 Expires 可以在服务端配置同时启用,同时启用的时候 Cache-Control 优先级高。

# 协商缓存

**协商缓存原理:**客户端向服务器端发出请求,服务端会检测是否有对应的标识,如果没有对应的标识,服务器端会返回一个对应的标识给客户端,客户端下次再次请求的时候,把该标识带过去,然后服务器端会验证该标识,如果验证通过了,则会响应 304,告诉浏览器读取缓存。如果标识没有通过,则返回请求的资源。

那么协商缓存的标识又有 2 种:ETag/if-None-Match 和 Last-Modified/if-Modify-Since

# Last-Modified/if-Modify-Since

浏览器第一次发出请求一个资源的时候,服务器会返回一个 last-Modify 到 header 中. Last-Modify 含义是最后的修改时间。

当浏览器再次请求的时候,request 的请求头会加上 if-Modify-Since,该值为缓存之前返回的 Last-Modify.

服务器收到 if-Modify-Since 后,根据资源的最后修改时间(last-Modify)和该值(if-Modify-Since)进行比较,如果相等的话,则命中缓存,返回 304,否则, 如果 Last-Modify > if-Modify-Since, 则会给出 200 响应,并且更新 Last-Modify 为新的值。

# ETag/if-None-Match

ETag 的原理和上面的 last-modified 是类似的。ETag 则是对当前请求的资源做一个唯一的标识。该标识可以是一个字符串,文件的 size,hash 等。只要能够合理标识资源的唯一性并能验证是否修改过就可以了。ETag 在服务器响应请求的时候,返回当前资源的唯一标识(它是由服务器生成的)。但是只要资源有变化,ETag 会重新生成的。浏览器再下一次加载的时候会向服务器发送请求,会将上一次返回的 ETag 值放到 request header 里的 if-None-Match 里面去,服务器端只要比较客户端传来的 if-None-Match 值是否和自己服务器上的 ETag 是否一致,如果一致说明资源未修改过,因此返回 304,如果不一致,说明修改过,因此返回 200。并且把新的 Etag 赋值给 if-None-Match 来更新该值。

last-modified 和 ETag 之间对比

  1. 在精度上,ETag 要优先于 last-modified。
  2. 在性能上,Etag 要弱于 Last-Modified,Last-Modified 需要记录时间,而 Etag 需要服务器通过算法来计算出一个 hash 值。
  3. 在优先级上,服务器校验优先考虑 Etag。

# 直播网站是如何实现的

一个直播网站通常会有下面 5 个部分组成。

  1. 录制端:负责录制直播视频,用流的形式上传。
  2. 计算集群:专门负责编码上传的流数据,然后进行压缩、转码、切片等工作。
  3. 对象存储:存储原始视频和转码后的视频(相当于 CDN 的源,回源用)。
  4. CDN:将转码后的内容分发到离用户较近的节点,方便用户获取。
  5. 直播 App:给用户看直播时使用。

# 对称、非对称加密的区别是?

对称加密和解密可以用同一套密钥。非对称加密利用数学的方法生成公私钥对,公钥加密的数据私钥可以解密,私钥加密的数据公钥可以解密。但是公钥不能解密公钥加密的数据,私钥也不能解密私钥加密的数据。

# 如果公司要求你生成一个公私钥对,然后去证书机构申请证书,请问如果你丢失了这个公私钥对有什么危害?你要如何保护这个公私钥对?

先明说说为什么会有这个问题。网站拥有者向 CA 机构申请证书时,证书请求文件中只包含公钥,不包含私钥。 证书私钥由网站保存,证书请求文件提交给 CA 机构进行认证和签名后对外公开。而大部分公司都会规定:含有敏感信息的数据不能带出公司,比如只能存放在公司的笔记本、公司的网盘、公司的服务器上,但是显然对于存储证书的场景不适用。因为私钥太敏感了,有了私钥相当于可以解密用户发送给服务器的数据,泄漏的危害性非常大。

因此,此类证书文件通常不在办公电脑,或者公司网盘上备份。如果你自己的电脑中有备份,应该尽快删除。通常证书直接保存到安全级别较高的服务器上,只有需要使用证书的软件才能够访问。另外,如果外部的第三方服务需要用到私钥,比如 CDN,那么这里还会涉及一些特别的密钥分发技术,以及硬件加密技术,具体可以参考 Keyless SSL 和 Intel 的 QAT 方案。

# 为什么可以相信一个 HTTPS 网站?

当用户用浏览器打开一个 HTTPS 网站时,会到目标网站下载目标网站的证书。接下来,浏览器会去验证证书上的签名,一直验证到根证书。如果根证书被预装,那么就会信任这个网站。也就是说,网站的信用是由操作系统的提供商、根证书机构、中间证书机构一起在担保

# 哪些情况下你服务器的 /etc/passwd 文件会被黑客拿走?

比较常见的情形就是开发机器信息泄漏或者中毒成了肉鸡。很多同学的开发机器上都配置了到服务器跳板机的免密登录权限。如果开发机器上公钥泄漏,黑客就有可能登入跳板机。如果成了肉鸡,那么很多行为都可以远程操控,相当于黑客攻破了你公司的内网。

还有一种常见的情形和代码注入有些相似之处,比如说有一个获取配置文件的服务,用参数表示配置文件的名称,比如 /getfile/a.txt 代表取出代码路径某个相对目录的 a.txt。这个时候如果网站程序实现直接将参数作为文件路径的一部分,黑客可能会尝试使用 /getfile/../../../etc/passwd 去获取 /etc/passwd 文件。

# 如何抵御 SYN 拒绝攻击?

SYN 攻击是 DDoS 攻击的一种形式。这种形式攻击者伪装成终端不停地向服务器发起 SYN 请求。通常攻击者的肉鸡,发送了 SYN 之后,不等给服务端 ACK,就下线了。 这样攻击者不断发送 SYN ,然后下线,而服务端会等待一段时间(通常会在 3s 以上),等待 ACK。这样就导致了大量的连接对象在服务端被积累。

针对这个特点可以实现一个 TCP 代理(防火墙),发现有发送 SYN 但是不给 ACK 的行为就对目标 IP 地址禁用一段时间。这个策略平时可以配置成开关,等到被攻击的时候打开。另一方面,可以适当提升连接数支持。


https://mp.weixin.qq.com/s?__biz=MzUxODAzNDg4NQ==&mid=2247483971&idx=1&sn=8f2d5dae3d95efc446061b352c8e9961&chksm=f98e46e9cef9cfff1f6bee1974b8dc27dcc42f0627dcf8ff0c0df8dbaa7a1f74587e3fafc167&scene=178&cur_album_id=1337204681134751744#rd

# HTTP 是什么?描述一下

HTTP 是超文本传输协议,也就是HyperText Transfer Protocol。

# 能否详细解释「超文本传输协议」?

HTTP 的名字「超文本协议传输」,它可以拆成三个部分:

  • 超文本
  • 传输
  • 协议

图片三个部分

# 1. 「协议」

在生活中,我们也能随处可见「协议」,例如:

  • 刚毕业时会签一个「三方协议」;

  • 找房子时会签一个「租房协议」;

image-20220220005602117

生活中的协议,本质上与计算机中的协议是相同的,协议的特点:

  • 」字,代表的意思是必须有两个以上的参与者。例如三方协议里的参与者有三个:你、公司、学校三个;租房协议里的参与者有两个:你和房东。
  • 」字,代表的意思是对参与者的一种行为约定和规范。例如三方协议里规定试用期期限、毁约金等;租房协议里规定租期期限、每月租金金额、违约如何处理等。

针对 HTTP 协议,我们可以这么理解。

HTTP 是一个用在计算机世界里的协议。它使用计算机能够理解的语言确立了一种计算机之间交流通信的规范(两个以上的参与者),以及相关的各种控制和错误处理方式(行为约定和规范)。

# 2. 「传输」

所谓的「传输」,很好理解,就是把一堆东西从 A 点搬到 B 点,或者从 B 点 搬到 A 点。

别轻视了这个简单的动作,它至少包含两项重要的信息。

HTTP 协议是一个双向协议

我们在上网冲浪时,浏览器是请求方 A ,百度网站就是应答方 B。双方约定用 HTTP 协议来通信,于是浏览器把请求数据发送给网站,网站再把一些数据返回给浏览器,最后由浏览器渲染在屏幕,就可以看到图片、视频了。

image-20220220005712703

数据虽然是在 A 和 B 之间传输,但允许中间有中转或接力

就好像第一排的同学想穿递纸条给最后一排的同学,那么传递的过程中就需要经过好多个同学(中间人),这样的传输方式就从「A < --- > B」,变成了「A <-> N <-> M <-> B」。

而在 HTTP 里,需要中间人遵从 HTTP 协议,只要不打扰基本的数据传输,就可以添加任意额外的东西。

针对传输,我们可以进一步理解了 HTTP。

HTTP 是一个在计算机世界里专门用来在两点之间传输数据的约定和规范。

# 3. 「超文本」

HTTP 传输的内容是「超文本」。

我们先来理解「文本」,在互联网早期的时候只是简单的字符文字,但现在「文本」。的涵义已经可以扩展为图片、视频、压缩包等,在 HTTP 眼里这些都算做「文本」。

再来理解「超文本」,它就是超越了普通文本的文本,它是文字、图片、视频等的混合体最关键有超链接,能从一个超文本跳转到另外一个超文本。

HTML 就是最常见的超文本了,它本身只是纯文字文件,但内部用很多标签定义了图片、视频等的链接,在经过浏览器的解释,呈现给我们的就是一个文字、有画面的网页了。

OK,经过了对 HTTP 里这三个名词的详细解释,就可以给出比「超文本传输协议」这七个字更准确更有技术含量的答案:

HTTP 是一个在计算机世界里专门在「两点」之间「传输」文字、图片、音频、视频等「超文本」数据的「约定和规范」。

# 那「HTTP 是用于从互联网服务器传输超文本到本地浏览器的协议 HTTP」 ,这种说法正确吗?

这种说法是不正确的。因为也可以是「服务器< -- >服务器」,所以采用两点之间的描述会更准确。

# HTTP 常见的状态码,有哪些?

image-20220220005956277五大类 HTTP 状态码

# 1xx

1xx 类状态码属于提示信息,是协议处理中的一种中间状态,实际用到的比较少。

# 2xx

2xx 类状态码表示服务器成功处理了客户端的请求,也是我们最愿意看到的状态。

200 OK」是最常见的成功状态码,表示一切正常。如果是非 HEAD 请求,服务器返回的响应头都会有 body 数据。

204 No Content」也是常见的成功状态码,与 200 OK 基本相同,但响应头没有 body 数据。

206 Partial Content」是应用于 HTTP 分块下载或断点续传,表示响应返回的 body 数据并不是资源的全部,而是其中的一部分,也是服务器处理成功的状态。

# 3xx

3xx 类状态码表示客户端请求的资源发送了变动,需要客户端用新的 URL 重新发送请求获取资源,也就是重定向

301 Moved Permanently」表示永久重定向,说明请求的资源已经不存在了,需改用新的 URL 再次访问。

302 Moved Permanently」表示临时重定向,说明请求的资源还在,但暂时需要用另一个 URL 来访问。

301 和 302 都会在响应头里使用字段 Location,指明后续要跳转的 URL,浏览器会自动重定向新的 URL。

304 Not Modified」不具有跳转的含义,表示资源未修改,重定向已存在的缓冲文件,也称缓存重定向,用于缓存控制。

# 4xx

4xx 类状态码表示客户端发送的报文有误,服务器无法处理,也就是错误码的含义。

400 Bad Request」表示客户端请求的报文有错误,但只是个笼统的错误。

403 Forbidden」表示服务器禁止访问资源,并不是客户端的请求出错。

404 Not Found」表示请求的资源在服务器上不存在或未找到,所以无法提供给客户端。

# 5xx

5xx 类状态码表示客户端请求报文正确,但是服务器处理时内部发生了错误,属于服务器端的错误码。

500 Internal Server Error」与 400 类型,是个笼统通用的错误码,服务器发生了什么错误,我们并不知道。

501 Not Implemented」表示客户端请求的功能还不支持,类似“即将开业,敬请期待”的意思。

502 Bad Gateway」通常是服务器作为网关或代理时返回的错误码,表示服务器自身工作正常,访问后端服务器发生了错误。

503 Service Unavailable」表示服务器当前很忙,暂时无法响应服务器,类似“网络服务正忙,请稍后重试”的意思。

# http 常见字段有哪些?

# Host

客户端发送请求时,用来指定服务器的域名。

image-20220220010045140

Host: www.A.com
1

有了 Host 字段,就可以将请求发往「同一台」服务器上的不同网站。

# Content-Length 字段

服务器在返回数据时,会有 Content-Length 字段,表明本次回应的数据长度。

图片

Content-Length: 1000
1

如上面则是告诉浏览器,本次服务器回应的数据长度是 1000 个字节,后面的字节就属于下一个回应了。

# Connection 字段

Connection 字段最常用于客户端要求服务器使用 TCP 持久连接,以便其他请求复用。

image-20220220010104515

HTTP/1.1 版本的默认连接都是持久连接,但为了兼容老版本的 HTTP,需要指定 Connection 首部字段的值为 Keep-Alive

Connection: keep-alive
1

一个可以复用的 TCP 连接就建立了,直到客户端或服务器主动关闭连接。但是,这不是标准字段。

# Content-Type 字段

Content-Type 字段用于服务器回应时,告诉客户端,本次数据是什么格式。

image-20220220010239255

Content-Type: text/html; charset=utf-8
1

上面的类型表明,发送的是网页,而且编码是 UTF-8。

客户端请求的时候,可以使用 Accept 字段声明自己可以接受哪些数据格式。

Accept: */*
1

上面代码中,客户端声明自己可以接受任何格式的数据。

# Content-Encoding 字段

Content-Encoding 字段说明数据的压缩方法。表示服务器返回的数据使用了什么压缩格式

Content-Encoding: gzip
1

上面表示服务器返回的数据采用了 gzip 方式压缩,告知客户端需要用此方式解压。

客户端在请求时,用 Accept-Encoding 字段说明自己可以接受哪些压缩方法。

Accept-Encoding: gzip, deflate
1

# 说一下 GET 和 POST 的区别?

Get 方法的含义是请求从服务器获取资源,这个资源可以是静态的文本、页面、图片视频等。

比如,你打开我的文章,浏览器就会发送 GET 请求给服务器,服务器就会返回文章的所有文字及资源。

image-20220220010414827

POST 方法则是相反操作,它向 URI 指定的资源提交数据,数据就放在报文的 body 里。

比如,你在我文章底部,敲入了留言后点击「提交」(暗示你们留言),浏览器就会执行一次 POST 请求,把你的留言文字放进了报文 body 里,然后拼接好 POST 请求头,通过 TCP 协议发送给服务器。

image-20220220010432877

# GET 和 POST 方法都是安全和幂等的吗?

先说明下安全和幂等的概念:

  • 在 HTTP 协议里,所谓的「安全」是指请求方法不会「破坏」服务器上的资源。
  • 所谓的「幂等」,意思是多次执行相同的操作,结果都是「相同」的。

那么很明显 GET 方法就是安全且幂等的,因为它是「只读」操作,无论操作多少次,服务器上的数据都是安全的,且每次的结果都是相同的。

POST 因为是「新增或提交数据」的操作,会修改服务器上的资源,所以是不安全的,且多次提交数据就会创建多个资源,所以不是幂等的。


# 你知道的 HTTP(1.1) 的优点有哪些,怎么体现的?

HTTP 最凸出的优点是「简单、灵活和易于扩展、应用广泛和跨平台」。

# 1. 简单

HTTP 基本的报文格式就是 header + body,头部信息也是 key-value 简单文本的形式,易于理解,降低了学习和使用的门槛。

# 2. 灵活和易于扩展

HTTP 协议里的各类请求方法、URI/URL、状态码、头字段等每个组成要求都没有被固定死,都允许开发人员自定义和扩充

同时 HTTP 由于是工作在应用层( OSI 第七层),则它下层可以随意变化

HTTPS 也就是在 HTTP 与 TCP 层之间增加了 SSL/TLS 安全传输层,HTTP/3 甚至把 TCP 层换成了基于 UDP 的 QUIC。

# 3. 应用广泛和跨平台

互联网发展至今,HTTP 的应用范围非常的广泛,从台式机的浏览器到手机上的各种 APP,从看新闻、刷贴吧到购物、理财、吃鸡,HTTP 的应用片地开花,同时天然具有跨平台的优越性。

# 那它的缺点呢?

HTTP 协议里有优缺点一体的双刃剑,分别是「无状态、明文传输」,同时还有一大缺点「不安全」。

# 1. 无状态双刃剑

无状态的好处,因为服务器不会去记忆 HTTP 的状态,所以不需要额外的资源来记录状态信息,这能减轻服务器的负担,能够把更多的 CPU 和内存用来对外提供服务。

无状态的坏处,既然服务器没有记忆能力,它在完成有关联性的操作时会非常麻烦。

例如登录->添加购物车->下单->结算->支付,这系列操作都要知道用户的身份才行。但服务器不知道这些请求是有关联的,每次都要问一遍身份信息。

这样每操作一次,都要验证信息,这样的购物体验还能愉快吗?别问,问就是酸爽

对于无状态的问题,解法方案有很多种,其中比较简单的方式用 Cookie 技术。

Cookie 通过在请求和响应报文中写入 Cookie 信息来控制客户端的状态。

相当于,在客户端第一次请求后,服务器会下发一个装有客户信息的「小贴纸」,后续客户端请求服务器的时候,带上「小贴纸」,服务器就能认得了了

image-20220220010624842image-20220220010659526

# 2. 明文传输双刃剑

明文意味着在传输过程中的信息,是可方便阅读的,通过浏览器的 F12 控制台或 Wireshark 抓包都可以直接肉眼查看,为我们调试工作带了极大的便利性。

但是这正是这样,HTTP 的所有信息都暴露在了光天化日下,相当于信息裸奔。在传输的漫长的过程中,信息的内容都毫无隐私可言,很容易就能被窃取,如果里面有你的账号密码信息,那你号没了

图片

# 3. 不安全

HTTP 比较严重的缺点就是不安全:

  • 通信使用明文(不加密),内容可能会被窃听。比如,账号信息容易泄漏,那你号没了。
  • 不验证通信方的身份,因此有可能遭遇伪装。比如,访问假的淘宝、拼多多,那你钱没了。
  • 无法证明报文的完整性,所以有可能已遭篡改。比如,网页上植入垃圾广告,视觉污染,眼没了。

HTTP 的安全问题,可以用 HTTPS 的方式解决,也就是通过引入 SSL/TLS 层,使得在安全上达到了极致。

# 那你说下 HTTP/1.1 的性能如何?

HTTP 协议是基于 TCP/IP,并且使用了「请求 - 应答」的通信模式,所以性能的关键就在这两点里。

# 1. 长连接

早期 HTTP/1.0 性能上的一个很大的问题,那就是每发起一个请求,都要新建一次 TCP 连接(三次握手),而且是串行请求,做了无畏的 TCP 连接建立和断开,增加了通信开销。

为了解决上述 TCP 连接问题,HTTP/1.1 提出了长连接的通信方式,也叫持久连接。这种方式的好处在于减少了 TCP 连接的重复建立和断开所造成的额外开销,减轻了服务器端的负载。

持久连接的特点是,只要任意一端没有明确提出断开连接,则保持 TCP 连接状态。

image-20220220011017592

# 2. 管道网络传输

HTTP/1.1 采用了长连接的方式,这使得管道(pipeline)网络传输成为了可能。

即可在同一个 TCP 连接里面,客户端可以发起多个请求,只要第一个请求发出去了,不必等其回来,就可以发第二个请求出去,可以减少整体的响应时间。

举例来说,客户端需要请求两个资源。以前的做法是,在同一个 TCP 连接里面,先发送 A 请求,然后等待服务器做出回应,收到后再发出 B 请求。管道机制则是允许浏览器同时发出 A 请求和 B 请求。

image-20220220011032476

但是服务器还是按照顺序,先回应 A 请求,完成后再回应 B 请求。要是 前面的回应特别慢,后面就会有许多请求排队等着。这称为「队头堵塞」。

# 3. 队头阻塞

「请求 - 应答」的模式加剧了 HTTP 的性能问题。

因为当顺序发送的请求序列中的一个请求因为某种原因被阻塞时,在后面排队的所有请求也一同被阻塞了,会招致客户端一直请求不到数据,这也就是「队头阻塞」。好比上班的路上塞车

image-20220220011104709队头阻塞

总之 HTTP/1.1 的性能一般般,后续的 HTTP/2 和 HTTP/3 就是在优化 HTTP 的性能。


# HTTP 与 HTTPS 有哪些区别?

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

# HTTPS 解决了 HTTP 的哪些问题?

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

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

HTTPS 在 HTTP 与 TCP 层之间加入了 SSL/TLS 协议。

image-20220220011204763

可以很好的解决了上述的风险:

  • 信息加密:交互信息无法被窃取,但你的号会因为「自身忘记」账号而没。
  • 校验机制:无法篡改通信内容,篡改了就不能正常显示,但百度「竞价排名」依然可以搜索垃圾广告。
  • 身份证书:证明淘宝是真的淘宝网,但你的钱还是会因为「剁手」而没。

可见,只要自身不做「恶」,SSL/TLS 协议是能保证通信是安全的。

# HTTPS 是如何解决上面的三个风险的?

  • 混合加密的方式实现信息的机密性,解决了窃听的风险。
  • 摘要算法的方式来实现完整性,它能够为数据生成独一无二的「指纹」,指纹用于校验数据的完整性,解决了篡改的风险。
  • 将服务器公钥放入到数字证书中,解决了冒充的风险。

# 1. 混合加密

通过混合加密的方式可以保证信息的机密性,解决了窃听的风险。

image-20220220011314131

HTTPS 采用的是对称加密非对称加密结合的「混合加密」方式:

  • 在通信建立前采用非对称加密的方式交换「会话秘钥」,后续就不再使用非对称加密。
  • 在通信过程中全部使用对称加密的「会话秘钥」的方式加密明文数据。

采用「混合加密」的方式的原因:

  • 对称加密只使用一个密钥,运算速度快,密钥必须保密,无法做到安全的密钥交换。
  • 非对称加密使用两个密钥:公钥和私钥,公钥可以任意分发而私钥保密,解决了密钥交换问题但速度慢。

# 2. 摘要算法

摘要算法用来实现完整性,能够为数据生成独一无二的「指纹」,用于校验数据的完整性,解决了篡改的风险。

image-20220220011349146

客户端在发送明文之前会通过摘要算法算出明文的「指纹」,发送的时候把「指纹 + 明文」一同加密成密文后,发送给服务器,服务器解密后,用相同的摘要算法算出发送过来的明文,通过比较客户端携带的「指纹」和当前算出的「指纹」做比较,若「指纹」相同,说明数据是完整的。

# 3. 数字证书

客户端先向服务器端索要公钥,然后用公钥加密信息,服务器收到密文后,用自己的私钥解密。

这就存在些问题,如何保证公钥不被篡改和信任度?

所以这里就需要借助第三方权威机构 CA (数字证书认证机构),将服务器公钥放在数字证书(由数字证书认证机构颁发)中,只要证书是可信的,公钥就是可信的。

图片image-20220220011608577

通过数字证书的方式保证服务器公钥的身份,解决冒充的风险。

# HTTPS 是如何建立连接的?其间交互了什么?

SSL/TLS 协议基本流程:

  • 客户端向服务器索要并验证服务器的公钥。
  • 双方协商生产「会话秘钥」。
  • 双方采用「会话秘钥」进行加密通信。

前两步也就是 SSL/TLS 的建立过程,也就是握手阶段。

SSL/TLS 的「握手阶段」涉及四次通信,可见下图:

imgHTTPS 连接建立过程

SSL/TLS 协议建立的详细流程:

# 1. ClientHello

首先,由客户端向服务器发起加密通信请求,也就是 ClientHello 请求。

在这一步,客户端主要向服务器发送以下信息:

(1)客户端支持的 SSL/TLS 协议版本,如 TLS 1.2 版本。

(2)客户端生产的随机数(Client Random),后面用于生产「会话秘钥」。

(3)客户端支持的密码套件列表,如 RSA 加密算法。

# 2. SeverHello

服务器收到客户端请求后,向客户端发出响应,也就是 SeverHello。服务器回应的内容有如下内容:

(1)确认 SSL/ TLS 协议版本,如果浏览器不支持,则关闭加密通信。

(2)服务器生产的随机数(Server Random),后面用于生产「会话秘钥」。

(3)确认的密码套件列表,如 RSA 加密算法。

(4)服务器的数字证书。

# 3.客户端回应

客户端收到服务器的回应之后,首先通过浏览器或者操作系统中的 CA 公钥,确认服务器的数字证书的真实性。

如果证书没有问题,客户端会从数字证书中取出服务器的公钥,然后使用它加密报文,向服务器发送如下信息:

(1)一个随机数(pre-master key)。该随机数会被服务器公钥加密。

(2)加密通信算法改变通知,表示随后的信息都将用「会话秘钥」加密通信。

(3)客户端握手结束通知,表示客户端的握手阶段已经结束。这一项同时把之前所有内容的发生的数据做个摘要,用来供服务端校验。

上面第一项的随机数是整个握手阶段的第三个随机数,这样服务器和客户端就同时有三个随机数,接着就用双方协商的加密算法,各自生成本次通信的「会话秘钥」。

# 4. 服务器的最后回应

服务器收到客户端的第三个随机数(pre-master key)之后,通过协商的加密算法,计算出本次通信的「会话秘钥」。然后,向客户端发生最后的信息:

(1)加密通信算法改变通知,表示随后的信息都将用「会话秘钥」加密通信。

(2)服务器握手结束通知,表示服务器的握手阶段已经结束。这一项同时把之前所有内容的发生的数据做个摘要,用来供客户端校验。

至此,整个 SSL/TLS 的握手阶段全部结束。接下来,客户端与服务器进入加密通信,就完全是使用普通的 HTTP 协议,只不过用「会话秘钥」加密内容。


# 说说 HTTP/1.1 相比 HTTP/1.0 提高了什么性能?

HTTP/1.1 相比 HTTP/1.0 性能上的改进:

  • 使用 TCP 长连接的方式改善了 HTTP/1.0 短连接造成的性能开销。
  • 支持 管道(pipeline)网络传输,只要第一个请求发出去了,不必等其回来,就可以发第二个请求出去,可以减少整体的响应时间。

但 HTTP/1.1 还是有性能瓶颈:

  • 请求 / 响应头部(Header)未经压缩就发送,首部信息越多延迟越大。只能压缩 Body 的部分;
  • 发送冗长的首部。每次互相发送相同的首部造成的浪费较多;
  • 服务器是按请求的顺序响应的,如果服务器响应慢,会招致客户端一直请求不到数据,也就是队头阻塞;
  • 没有请求优先级控制;
  • 请求只能从客户端开始,服务器只能被动响应。

# 那上面的 HTTP/1.1 的性能瓶颈,HTTP/2 做了什么优化?

HTTP/2 协议是基于 HTTPS 的,所以 HTTP/2 的安全性也是有保障的。

那 HTTP/2 相比 HTTP/1.1 性能上的改进:

# 1. 头部压缩

HTTP/2 会压缩头(Header)如果你同时发出多个请求,他们的头是一样的或是相似的,那么,协议会帮你消除重复的分

这就是所谓的 HPACK 算法:在客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号,这样就提高速度了。

# 2. 二进制格式

HTTP/2 不再像 HTTP/1.1 里的纯文本形式的报文,而是全面采用了二进制格式。

头信息和数据体都是二进制,并且统称为帧(frame):头信息帧和数据帧

image-20220220013248533

这样虽然对人不友好,但是对计算机非常友好,因为计算机只懂二进制,那么收到报文后,无需再将明文的报文转成二进制,而是直接解析二进制报文,这增加了数据传输的效率

# 3. 数据流

HTTP/2 的数据包不是按顺序发送的,同一个连接里面连续的数据包,可能属于不同的回应。因此,必须要对数据包做标记,指出它属于哪个回应。

每个请求或回应的所有数据包,称为一个数据流(Stream)。

每个数据流都标记着一个独一无二的编号,其中规定客户端发出的数据流编号为奇数, 服务器发出的数据流编号为偶数

客户端还可以指定数据流的优先级。优先级高的请求,服务器就先响应该请求。

image-20220220013307736

# 4. 多路复用

HTTP/2 是可以在一个连接中并发多个请求或回应,而不用按照顺序一一对应

移除了 HTTP/1.1 中的串行请求,不需要排队等待,也就不会再出现「队头阻塞」问题,降低了延迟,大幅度提高了连接的利用率

举例来说,在一个 TCP 连接里,服务器收到了客户端 A 和 B 的两个请求,如果发现 A 处理过程非常耗时,于是就回应 A 请求已经处理好的部分,接着回应 B 请求,完成后,再回应 A 请求剩下的部分。

image-20220220013325186

# 5. 服务器推送

HTTP/2 还在一定程度上改善了传统的「请求 - 应答」工作模式,服务不再是被动地响应,也可以主动向客户端发送消息。

举例来说,在浏览器刚请求 HTML 的时候,就提前把可能会用到的 JS、CSS 文件等静态资源主动发给客户端,减少延时的等待,也就是服务器推送(Server Push,也叫 Cache Push)。

# HTTP/2 有哪些缺陷?HTTP/3 做了哪些优化?

HTTP/2 主要的问题在于:多个 HTTP 请求在复用一个 TCP 连接,下层的 TCP 协议是不知道有多少个 HTTP 请求的。

所以一旦发生了丢包现象,就会触发 TCP 的重传机制,这样在一个 TCP 连接中的所有的 HTTP 请求都必须等待这个丢了的包被重传回来

  • HTTP/1.1 中的管道( pipeline)传输中如果有一个请求阻塞了,那么队列后请求也统统被阻塞住了
  • HTTP/2 多请求复用一个 TCP 连接,一旦发生丢包,就会阻塞住所有的 HTTP 请求。

这都是基于 TCP 传输层的问题,所以 HTTP/3 把 HTTP 下层的 TCP 协议改成了 UDP!

image-20220220013436484

UDP 发生是不管顺序,也不管丢包的,所以不会出现 HTTP/1.1 的队头阻塞 和 HTTP/2 的一个丢包全部重传问题。

大家都知道 UDP 是不可靠传输的,但基于 UDP 的 QUIC 协议 可以实现类似 TCP 的可靠性传输。

  • QUIC 有自己的一套机制可以保证传输的可靠性的。当某个流发生丢包时,只会阻塞这个流,其他流不会受到影响
  • TL3 升级成了最新的 1.3 版本,头部压缩算法也升级成了 QPack
  • HTTPS 要建立一个连接,要花费 6 次交互,先是建立三次握手,然后是 TLS/1.3 的三次握手。QUIC 直接把以往的 TCP 和 TLS/1.3 的 6 次交互合并成了 3 次,减少了交互次数

image-20220220013609647

所以, QUIC 是一个在 UDP 之上的 TCP + TLS + HTTP/2 的多路复用的协议。

QUIC 是新协议,对于很多网络设备,根本不知道什么是 QUIC,只会当做 UDP,这样会出现新的问题。所以 HTTP/3 现在普及的进度非常的缓慢,不知道未来 UDP 是否能够逆袭 TCP。

FireBoxSafari默认禁用三方 cookie

Chrome 是通过SameSite来控制三方 cookie,SameSite 可以避免跨站请求发送 Cookie,有以下三个属性:

  • Strict

    Strict 是最严格的防护,将阻止浏览器在所有跨站点浏览上下文中将 Cookie 发送到目标站点,即使在遵循常规链接时也是如此。因此这种设置可以阻止所有 CSRF 攻击。然而,它的用户友好性太差,即使是普通的 GET 请求它也不允许通过。

    例如,对于一个普通的站点,这意味着如果一个已经登录的用户跟踪一个发布在公司讨论论坛或电子邮件上的网站链接,这个站点将不会收到 Cookie ,用户访问该站点还需要重新登陆。

    不过,具有交易业务的网站很可能不希望从外站链接到任何交易页面,因此这种场景最适合使用 strict 标志。

  • Lax

    对于允许用户从外部链接到达本站并使用已有会话的网站站,默认的 Lax 值在安全性和可用性之间提供了合理的平衡。 Lax 属性只会在使用危险 HTTP 方法发送跨域 Cookie 的时候进行阻止,例如 POST 方式。同时,使用 JavaScript 脚本发起的请求也无法携带 Cookie

    例如,一个用户在 A 站点 点击了一个 B 站点(GET 请求),而假如 B 站点 使用了Samesite-cookies=Lax,那么用户可以正常登录 B 站点。相对地,如果用户在 A 站点提交了一个表单到 B 站点(POST 请求),那么用户的请求将被阻止,因为浏览器不允许使用 POST 方式将 Cookie 从 A 域发送到B域。

  • None

    浏览器会在同站请求、跨站请求下继续发送 Cookies,不区分大小写。

# HttpOnly

前提:浏览器支持HttpOnly,不支持的话设置HttpOnly也是无效

服务端在响应报文中的Set-Cookie字段中设置HttpOnly属性,告知浏览器不允许通过脚本document.cookie去更改这个 cookie 值,事实上,设置HttpOnly之后,这个值在document.cookie中不可见,但是在 HTTP 请求的过程中,依然会发送这个 cookie 到服务端

也就是说:只能预防XSS攻击,不能预防CSRF攻击

# Secure

服务端在响应报文中的Set-Cookie字段中设置Secure属性为true时,cookie 只能通过https协议才能发送给服务端,如果是http协议则不会发送 cookie,需要重新登录

# 为什么需要 TCP 协议?TCP 工作在哪一层?

IP 层是「不可靠」的,它不保证网络包的交付、不保证网络包的按序交付、也不保证网络包中的数据的完整性。

如果需要保障网络数据包的可靠性,那么就需要由上层(传输层)的 TCP 协议来负责。

因为 TCP 是一个工作在传输层可靠数据传输的服务,它能确保接收端接收的网络包是无损坏、无间隔、非冗余和按序的。

# 什么是 TCP ?

TCP 是面向连接的、可靠的、基于字节流的传输层通信协议。

  • 面向连接:一定是「一对一」才能连接,不能像 UDP 协议 可以一个主机同时向多个主机发送消息,也就是一对多是无法做到的;
  • 可靠的:无论的网络链路中出现了怎样的链路变化,TCP 都可以保证一个报文一定能够到达接收端;
  • 字节流:消息是「没有边界」的,所以无论我们消息有多大都可以进行传输。并且消息是「有序的」,当「前一个」消息没有收到的时候,即使它先收到了后面的字节已经收到,那么也不能扔给应用层去处理,同时对「重复」的报文会自动丢弃。

# 什么是 TCP 连接?

用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括 Socket、序列号和窗口大小称为连接。

因此建立一个 TCP 连接是需要客户端与服务器端达成上述三个信息的共识。

  • Socket:由 IP 地址和端口号组成
  • 序列号:用来解决乱序问题等
  • 窗口大小:用来做流量控制

# 如何唯一确定一个 TCP 连接呢?

TCP 四元组可以唯一的确定一个连接,四元组包括如下:

  • 源地址
  • 源端口
  • 目的地址
  • 目的端口

源地址和目的地址的字段(32 位)是在 IP 头部中,作用是通过 IP 协议发送报文给对方主机。

源端口和目的端口的字段(16 位)是在 TCP 头部中,作用是告诉 TCP 协议应该把报文发给哪个进程。

# 有一个 IP 的服务器监听了一个端口,它的 TCP 的最大连接数是多少?

服务器通常固定在某个本地端口上监听,等待客户端的连接请求。

因此,客户端 IP 和 端口是可变的,其理论值计算公式如下:

image-20220225234241914

对 IPv4,客户端的 IP 数最多为 232 次方,客户端的端口数最多为 216 次方,也就是服务端单机最大 TCP 连接数,约为 248 次方。

当然,服务端最大并发 TCP 连接数远不能达到理论上限。

  • 首先主要是文件描述符限制,Socket 都是文件,所以首先要通过 ulimit 配置文件描述符的数目;
  • 另一个是内存限制,每个 TCP 连接都要占用一定内存,操作系统是有限的。

# UDP 和 TCP 有什么区别呢?分别的应用场景是?

区别:

1. 连接

  • TCP 是面向连接的传输层协议,传输数据前先要建立连接。
  • UDP 是不需要连接,即刻传输数据。

2. 服务对象

  • TCP 是一对一的两点服务,即一条连接只有两个端点。
  • UDP 支持一对一、一对多、多对多的交互通信

3. 可靠性

  • TCP 是可靠交付数据的,数据可以无差错、不丢失、不重复、按需到达。
  • UDP 是尽最大努力交付,不保证可靠交付数据。

4. 拥塞控制、流量控制

  • TCP 有拥塞控制和流量控制机制,保证数据传输的安全性。
  • UDP 则没有,即使网络非常拥堵了,也不会影响 UDP 的发送速率。

5. 首部开销

  • TCP 首部长度较长,会有一定的开销,首部在没有使用「选项」字段时是 20 个字节,如果使用了「选项」字段则会变长的。
  • UDP 首部只有 8 个字节,并且是固定不变的,开销较小。

应用场景:

由于 TCP 是面向连接,能保证数据的可靠性交付,因此经常用于:

  • FTP 文件传输
  • HTTP / HTTPS

由于 UDP 面向无连接,它可以随时发送数据,再加上 UDP 本身的处理既简单又高效,因此经常用于:

  • 包总量较少的通信,如 DNSSNMP
  • 视频、音频等多媒体通信
  • 广播通信

# 为什么 UDP 头部没有「首部长度」字段,而 TCP 头部有「首部长度」字段呢?

原因是 TCP 有可变长的「选项」字段,而 UDP 头部长度则是不会变化的,无需多一个字段去记录 UDP 的首部长度。

# 为什么 UDP 头部有「包长度」字段,而 TCP 头部则没有「包长度」字段呢?

先说说 TCP 是如何计算负载数据长度:

image-20220225234736904

其中 IP 总长度 和 IP 首部长度,在 IP 首部格式是已知的。TCP 首部长度,则是在 TCP 首部格式已知的,所以就可以求得 TCP 数据的长度。

大家这时就奇怪了问:“ UDP 也是基于 IP 层的呀,那 UDP 的数据长度也可以通过这个公式计算呀?为何还要有「包长度」呢?”

这么一问,确实感觉 UDP 「包长度」是冗余的。

因为为了网络设备硬件设计和处理方便,首部长度需要是 4字节的整数倍。

# TCP 三次握手过程和状态变迁

image-20220225234905117

  • 一开始,客户端和服务端都处于 CLOSED 状态。先是服务端主动监听某个端口,处于 LISTEN 状态
  • 客户端会随机初始化序号(client_isn),将此序号置于 TCP 首部的「序号」字段中,同时把 SYN 标志位置为 1 ,表示 SYN 报文。接着把第一个 SYN 报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于 SYN-SENT 状态。
  • 服务端收到客户端的 SYN 报文后,首先服务端也随机初始化自己的序号(server_isn),将此序号填入 TCP 首部的「序号」字段中,其次把 TCP 首部的「确认应答号」字段填入 client_isn + 1, 接着把 SYNACK 标志位置为 1。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于 SYN-RCVD 状态。
  • 客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部 ACK 标志位置为 1 ,其次「确认应答号」字段填入 server_isn + 1 ,最后把报文发送给服务端,这次报文可以携带客户到服务器的数据,之后客户端处于 ESTABLISHED 状态。
  • 服务器收到客户端的应答报文后,也进入 ESTABLISHED 状态。

从上面的过程可以发现第三次握手是可以携带数据的,前两次握手是不可以携带数据的

# 如何在 Linux 系统中查看 TCP 状态?

TCP 的连接状态查看,在 Linux 可以通过 netstat -napt 命令查看。

# TCP 连接为何是三次,不是两次,四次?

TCP 建立连接时,通过三次握手能防止历史连接的建立,能减少双方不必要的资源开销,能帮助双方同步初始化序列号。序列号能够保证数据包不重复、不丢弃和按序传输。

不使用「两次握手」和「四次握手」的原因:

  • 「两次握手」:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号;

    无法防止历史连接的建立:假设旧 SYN 数据包滞留,在新 SYN 数据包建立的情况下(此时服务端处于监听状态,可以继续接收其它 TCP 请求),旧 SYN 数据包到达,这时候服务端以为是新的连接,就正常发送 SYN-ACK 包给客户端(两次握手则连接已经建立),客户端识别出该旧数据包,想要发送 RST 包去放弃连接,但是这已经属于第三次握手了

    会造成双方资源的浪费:同样跟上面一样的情景,服务端建立了两个同端口的连接,给这两个端口分配相同的资源,造成资源浪费

    无法可靠的同步双方序列号:同步一个序列号起码需要两次握手,如果只有两次握手,服务端无法知道客户端是否已经同步序列号

  • 「四次握手」:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。

# 为什么客户端和服务端的初始序列号 ISN 是不相同的?

因为网络中的报文会延迟、会复制重发、也有可能丢失,这样会造成的不同连接之间产生互相影响,所以为了避免互相影响,客户端和服务端的初始序列号是随机且不同的。

# 初始序列号 ISN 是如何随机产生的?

起始 ISN 是基于时钟的,每 4 毫秒 + 1,转一圈要 4.55 个小时。

RFC1948 中提出了一个较好的初始化序列号 ISN 随机生成算法。

ISN = M + F (localhost, localport, remotehost, remoteport)

  • M 是一个计时器,这个计时器每隔 4 毫秒加 1。
  • F 是一个 Hash 算法,根据源 IP目的 IP、源端口、目的端口生成一个随机数值。要保证 Hash 算法不能被外部轻易推算得出,用 MD5 算法是一个比较好的选择。

# 既然 IP 层会分片,为什么 TCP 层还需要 MSS 呢?

因为 IP 层本身没有超时重传机制,它由传输层的 TCP 来负责超时和重传。如果一个 IP 分片丢失,整个 IP 报文的所有分片都得重传

因此,可以得知由 IP 层进行分片传输,是非常没有效率的。

为了达到最佳的传输效能 TCP 协议在建立连接的时候通常要协商双方的 MSS 值,当 TCP 层发现数据超过 MSS 时,则就先会进行分片,当然由它形成的 IP 包的长度也就不会大于 MTU ,自然也就不用 IP 分片了。

image-20220225235824253

经过 TCP 层分片后,如果一个 TCP 分片丢失后,进行重发时也是以 MSS 为单位,而不用重传所有的分片,大大增加了重传的效率。

image-20220225235602539

MTU:一个网络包的最大长度,以太网中一般为 1500 字节;

MSS:除去 IP 和 TCP 头部之后,一个网络包所能容纳的 TCP 数据的最大长度;

# 什么是 SYN 攻击?如何避免 SYN 攻击?

我们都知道 TCP 连接建立是需要三次握手,假设攻击者短时间伪造不同 IP 地址的 SYN 报文,服务端每接收到一个 SYN 报文,就进入SYN_RCVD 状态,但服务端发送出去的 ACK + SYN 报文,无法得到未知 IP 主机的 ACK 应答,久而久之就会占满服务端的 SYN 接收队列(未连接队列),使得服务器不能为正常用户服务。

# 避免 SYN 攻击方式一

其中一种解决方式是通过修改 Linux 内核参数,控制队列大小和当队列满时应做什么处理。

  • 当网卡接收数据包的速度大于内核处理的速度时,会有一个队列保存这些数据包。控制该队列的最大值如下参数
net.core.netdev_max_backlog
1
  • SYN_RCVD 状态连接的最大个数:
net.ipv4.tcp_max_syn_backlog
1
  • 超出处理能时,对新的 SYN 直接回 RST,丢弃连接:
net.ipv4.tcp_abort_on_overflow
1

# 避免 SYN 攻击方式二

  • 当 「 SYN 队列」满之后,后续服务器收到 SYN 包,不进入「 SYN 队列」;
  • 计算出一个 cookie 值,再以 SYN + ACK 中的「序列号」返回客户端,
  • 服务端接收到客户端的应答报文时,服务器会检查这个 ACK 包的合法性。如果合法,直接放入到「 Accept 队列」。
  • 最后应用通过调用 accpet() socket 接口,从「 Accept 队列」取出的连接。

# TCP 四次挥手过程和状态变迁

image-20220226000730966
  • 客户端打算关闭连接,此时会发送一个 TCP 首部 FIN 标志位被置为 1 的报文,也即 FIN 报文,之后客户端进入 FIN_WAIT_1 状态。
  • 服务端收到该报文后,就向客户端发送 ACK 应答报文,接着服务端进入 CLOSED_WAIT 状态。
  • 客户端收到服务端的 ACK 应答报文后,之后进入 FIN_WAIT_2 状态。
  • 等待服务端处理完数据后,也向客户端发送 FIN 报文,之后服务端进入 LAST_ACK 状态。
  • 客户端收到服务端的 FIN 报文后,回一个 ACK 应答报文,之后进入 TIME_WAIT 状态
  • 服务器收到了 ACK 应答报文后,就进入了 CLOSE 状态,至此服务端已经完成连接的关闭。
  • 客户端在经过 2MSL 一段时间后,自动进入 CLOSE 状态,至此客户端也完成连接的关闭。

你可以看到,每个方向都需要一个 FIN 和一个 ACK,因此通常被称为四次挥手

这里一点需要注意是:主动关闭连接的,才有 TIME_WAIT 状态。

# 为什么挥手需要四次?

再来回顾下四次挥手双方发 FIN 包的过程,就能理解为什么需要四次了。

  • 关闭连接时,客户端向服务端发送 FIN 时,仅仅表示客户端不再发送数据了但是还能接收数据
  • 服务器收到客户端的 FIN 报文时,先回一个 ACK 应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送 FIN 报文给客户端来表示同意现在关闭连接。

从上面过程可知,服务端通常需要等待完成数据的发送和处理,所以服务端的 ACKFIN 一般都会分开发送,从而比三次握手导致多了一次。

# 为什么 TIME_WAIT 等待的时间是 2MSL?

MSL 是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为 TCP 报文基于是 IP 协议的,而 IP 头中有一个 TTL 字段,是 IP 数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。

MSL 与 TTL 的区别:MSL 的单位是时间,而 TTL 是经过路由跳数。所以 MSL 应该要大于等于 TTL 消耗为 0 的时间,以确保报文已被自然消亡。

TIME_WAIT 等待 2 倍的 MSL,比较合理的解释是:网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待 2 倍的时间

比如,如果被动关闭方没有收到断开连接的最后的 ACK 报文,就会触发超时重发 Fin 报文,另一方接收到 FIN 后,会重发 ACK 给被动关闭方, 一来一去正好 2 个 MSL。

2MSL 的时间是从客户端接收到 FIN 后发送 ACK 开始计时的。如果在 TIME-WAIT 时间内,因为客户端的 ACK 没有传输到服务端,客户端又接收到了服务端重发的 FIN 报文,那么 2MSL 时间将重新计时

在 Linux 系统里 2MSL 默认是 60 秒,那么一个 MSL 也就是 30 秒。Linux 系统停留在 TIME_WAIT 的时间为固定的 60 秒

其定义在 Linux 内核代码里的名称为 TCP_TIMEWAIT_LEN:

#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT
                                    state, about 60 seconds  */
1
2

如果要修改 TIME_WAIT 的时间长度,只能修改 Linux 内核代码里 TCP_TIMEWAIT_LEN 的值,并重新编译 Linux 内核。

# 为什么需要 TIME_WAIT 状态?

主动发起关闭连接的一方,才会有 TIME-WAIT 状态。

需要 TIME-WAIT 状态,主要是两个原因:

  • 防止具有相同「四元组」的「旧」数据包被收到;
  • 保证「被动关闭连接」的一方能被正确的关闭,即保证最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭;

原因一:防止旧连接的数据包

假设 TIME-WAIT 没有等待时间或时间过短,被延迟的数据包抵达后会发生什么呢?

image-20220226002714603

  • 如上图黄色框框服务端在关闭连接之前发送的 SEQ = 301 报文,被网络延迟了。
  • 这时有相同端口的 TCP 连接被复用后,被延迟的 SEQ = 301 抵达了客户端,那么客户端是有可能正常接收这个过期的报文,这就会产生数据错乱等严重的问题。

所以,TCP 就设计出了这么一个机制,经过 2MSL 这个时间,足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的。

原因二:保证连接正确关闭

也就是说,TIME-WAIT 作用是等待足够的时间以确保最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭。

假设 TIME-WAIT 没有等待时间或时间过短,断开连接会造成什么问题呢?

image-20220226002911088
  • 如上图红色框框客户端四次挥手的最后一个 ACK 报文如果在网络中被丢失了,此时如果客户端 TIME-WAIT 过短或没有,则就直接进入了 CLOSE 状态了,那么服务端则会一直处在 LASE-ACK 状态。
  • 当客户端发起建立连接的 SYN 请求报文后,服务端会发送 RST 报文给客户端,连接建立的过程就会被终止。

如果 TIME-WAIT 等待足够长的情况就会遇到两种情况:

  • 服务端正常收到四次挥手的最后一个 ACK 报文,则服务端正常关闭连接。
  • 服务端没有收到四次挥手的最后一个 ACK 报文时,则会重发 FIN 关闭连接报文并等待新的 ACK 报文。

所以客户端在 TIME-WAIT 状态等待 2MSL 时间后,就可以保证双方的连接都可以正常的关闭。

# TIME_WAIT 过多有什么危害?

如果服务器有处于 TIME-WAIT 状态的 TCP,则说明是由服务器方主动发起的断开请求。

过多的 TIME-WAIT 状态主要的危害有两种:

  • 第一是内存资源占用;
  • 第二是对端口资源的占用,一个 TCP 连接至少消耗一个本地端口;

第二个危害是会造成严重的后果的,要知道,端口资源也是有限的,一般可以开启的端口为 32768~61000,也可以通过如下参数设置指定

net.ipv4.ip_local_port_range
1

如果服务端 TIME_WAIT 状态过多,占满了所有端口资源,则会导致无法创建新连接。

# 如何优化 TIME_WAIT?

这里给出优化 TIME-WAIT 的几个方式,都是有利有弊:

  • 打开 net.ipv4.tcp_tw_reuse 和 net.ipv4.tcp_timestamps 选项;
  • net.ipv4.tcp_max_tw_buckets
  • 程序中使用 SO_LINGER ,应用强制使用 RST 关闭。

# 方式一:net.ipv4.tcp_tw_reuse 和 tcp_timestamps

如下的 Linux 内核参数开启后,则可以复用处于 TIME_WAIT 的 socket 为新的连接所用

net.ipv4.tcp_tw_reuse = 1
1

使用这个选项,还有一个前提,需要打开对 TCP 时间戳的支持,即

net.ipv4.tcp_timestamps=1(默认即为 1)
1

这个时间戳的字段是在 TCP 头部的「选项」里,用于记录 TCP 发送方的当前时间戳和从对端接收到的最新时间戳。

由于引入了时间戳,我们在前面提到的 2MSL 问题就不复存在了,因为重复的数据包会因为时间戳过期被自然丢弃。

温馨提醒:net.ipv4.tcp_tw_reuse要慎用,因为使用了它就必然要打开时间戳的支持 net.ipv4.tcp_timestamps当客户端与服务端主机时间不同步时,客户端的发送的消息会被直接拒绝掉

# 方式二:net.ipv4.tcp_max_tw_buckets

这个值默认为 18000,当系统中处于 TIME_WAIT 的连接一旦超过这个值时,系统就会将所有的 TIME_WAIT 连接状态重置。

这个方法过于暴力,而且治标不治本,带来的问题远比解决的问题多,不推荐使用。

# 方式三:程序中使用 SO_LINGER

我们可以通过设置 socket 选项,来设置调用 close 关闭连接行为。

struct linger so_linger;
so_linger.l_onoff = 1;
so_linger.l_linger = 0;
setsockopt(s, SOL_SOCKET, SO_LINGER, &so_linger,sizeof(so_linger));
1
2
3
4

如果l_onoff为非 0, 且l_linger值为 0,那么调用close后,会立该发送一个RST标志给对端,该 TCP 连接将跳过四次挥手,也就跳过了TIME_WAIT状态,直接关闭。

但这为跨越TIME_WAIT状态提供了一个可能,不过是一个非常危险的行为,不值得提倡。

# 如果已经建立了连接,但是客户端突然出现故障了怎么办?

TCP 有一个机制是保活机制。这个机制的原理是这样的:

定义一个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP 保活机制会开始作用,每隔一个时间间隔,发送一个探测报文,该探测报文包含的数据非常少,如果连续几个探测报文都没有得到响应,则认为当前的 TCP 连接已经死亡,系统内核将错误信息通知给上层应用程序。

在 Linux 内核可以有对应的参数可以设置保活时间、保活探测的次数、保活探测的时间间隔,以下都为默认值:

net.ipv4.tcp_keepalive_time=7200
net.ipv4.tcp_keepalive_intvl=75
net.ipv4.tcp_keepalive_probes=9
1
2
3
  • tcp_keepalive_time=7200:表示保活时间是 7200 秒(2 小时),也就 2 小时内如果没有任何连接相关的活动,则会启动保活机制
  • tcp_keepalive_intvl=75:表示每次检测间隔 75 秒;
  • tcp_keepalive_probes=9:表示检测 9 次无响应,认为对方是不可达的,从而中断本次的连接。

也就是说在 Linux 系统中,最少需要经过 2 小时 11 分 15 秒才可以发现一个「死亡」连接。

这个时间是有点长的,我们也可以根据实际的需求,对以上的保活相关的参数进行设置。

如果开启了 TCP 保活,需要考虑以下几种情况:

第一种,对端程序是正常工作的。当 TCP 保活的探测报文发送给对端, 对端会正常响应,这样 TCP 保活时间会被重置,等待下一个 TCP 保活时间的到来。

第二种,对端程序崩溃并重启。当 TCP 保活的探测报文发送给对端后,对端是可以响应的,但由于没有该连接的有效信息,会产生一个 RST 报文,这样很快就会发现 TCP 连接已经被重置。

第三种,是对端程序崩溃,或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后,石沉大海,没有响应,连续几次,达到保活探测次数后,TCP 会报告该 TCP 连接已经死亡

# 针对 TCP 应该如何 Socket 编程?

image-20220226004143200
  • 服务端和客户端初始化 socket,得到文件描述符;
  • 服务端调用 bind,将绑定在 IP 地址和端口;
  • 服务端调用 listen,进行监听;
  • 服务端调用 accept,等待客户端连接;
  • 客户端调用 connect,向服务器端的地址和端口发起连接请求;
  • 服务端 accept 返回用于传输的 socket 的文件描述符;
  • 客户端调用 write 写入数据;服务端调用 read 读取数据;
  • 客户端断开连接时,会调用 close,那么服务端 read 读取数据的时候,就会读取到了 EOF,待处理完数据后,服务端调用 close,表示连接关闭。

这里需要注意的是,服务端调用 accept 时,连接成功了会返回一个已完成连接的 socket,后续用来传输数据。

所以,监听的 socket 和真正用来传送数据的 socket,是「两个」 socket,一个叫作监听 socket,一个叫作已完成连接 socket

成功连接建立之后,双方开始通过 read 和 write 函数来读写数据,就像往一个文件流里面写东西一样。

# listen 时候参数 backlog 的意义?

Linux 内核中会维护两个队列:

  • 未完成连接队列(SYN 队列):接收到一个 SYN 建立连接请求,处于 SYN_RCVD 状态;

  • 已完成连接队列(Accpet 队列):已完成 TCP 三次握手过程,处于 ESTABLISHED 状态;

  • int listen (int socketfd, int backlog)
    
    1
    • 参数一 socketfd 为 socketfd 文件描述符
    • 参数二 backlog,这参数在历史有一定的变化

    在早期 Linux 内核 backlog 是 SYN 队列大小,也就是未完成的队列大小。

    在 Linux 内核 2.2 之后,backlog 变成 accept 队列,也就是已完成连接建立的队列长度,所以现在通常认为 backlog 是 accept 队列。

# accept 发送在三次握手的哪一步?

image-20220226004517939
  • 客户端的协议栈向服务器端发送了 SYN 包,并告诉服务器端当前发送序列号 client_isn,客户端进入 SYNC_SENT 状态;
  • 服务器端的协议栈收到这个包之后,和客户端进行 ACK 应答,应答的值为 client_isn+1,表示对 SYN 包 client_isn 的确认,同时服务器也发送一个 SYN 包,告诉客户端当前我的发送序列号为 server_isn,服务器端进入 SYNC_RCVD 状态;
  • 客户端协议栈收到 ACK 之后,使得应用程序从 connect 调用返回,表示客户端到服务器端的单向连接建立成功,客户端的状态为 ESTABLISHED,同时客户端协议栈也会对服务器端的 SYN 包进行应答,应答数据为 server_isn+1;
  • 应答包到达服务器端后,服务器端协议栈使得 accept 阻塞调用返回,这个时候服务器端到客户端的单向连接也建立成功,服务器端也进入 ESTABLISHED 状态。

从上面的描述过程,我们可以得知客户端 connect 成功返回是在第二次握手,服务端 accept 成功返回是在三次握手成功之后。

# 客户端调用 close 了,连接是断开的流程是什么?

image-20220226004645013
  • 客户端调用 close,表明客户端没有数据需要发送了,则此时会向服务端发送 FIN 报文,进入 FIN_WAIT_1 状态;
  • 服务端接收到了 FIN 报文,TCP 协议栈会为 FIN 包插入一个文件结束符 EOF 到接收缓冲区中,应用程序可以通过 read 调用来感知这个 FIN 包。这个 EOF 会被放在已排队等候的其他已接收的数据之后,这就意味着服务端需要处理这种异常情况,因为 EOF 表示在该连接上再无额外数据到达。此时,服务端进入 CLOSE_WAIT 状态;
  • 接着,当处理完数据后,自然就会读到 EOF,于是也调用 close 关闭它的套接字,这会使得会发出一个 FIN 包,之后处于 LAST_ACK 状态;
  • 客户端接收到服务端的 FIN 包,并发送 ACK 确认包给服务端,此时客户端将进入 TIME_WAIT 状态;
  • 服务端收到 ACK 确认包后,就进入了最后的 CLOSE 状态;
  • 客户端进过 2MSL 时间之后,也进入 CLOSED 状态;

# 301 和 302 有什么应用场景,对 SEO 和缓存有什么影响?

SEO:如果一个页面有两个网址,就像http://www.yy.comhttp://yy.com,搜索引擎会认为它们是两个网站,结果造成每个网址的搜索链接都减少,从而降低排名,而搜索引擎知道 301 是永久重定向,这样就会把两个访问地址都归到同一个网站排名下

缓存:用不同的地址对缓存友好性比较差,当一个页面拥有多个地址时,它可能被多次缓存

301:永久重定向,搜索引擎在抓取新内容的同时也将旧的网址替换为重定向之后的新网址,主要有以下应用场景

  • 之前旧网址由于某种原因(域名到期等)需要移除,访问新的网址
  • 通过 301 重定向告知搜索引擎收录其它域名(比如两个域名指向同个资源,只有一个域名被搜索引擎收录的场景)

302:临时重定向,搜索引擎会抓取新内容的同时保存旧的网址,主要有以下应用场景

  • 当一个网址在短期内(比如 24~48 小时)临时移动到一个新的网址

针对 SEO,301 更优于 302

# 前端项目如何找出性能瓶颈(阿里)

打开 Chrome 开发者控制台

  • performance 选项,观察 JS 执行时间,页面渲染时间,CPU 占用情况,网络占用情况,内存堆使用情况等等

  • network 选项,观察http请求是否过久,缓存是否生效等等

  • memory 选项,对比查看内存快照,查看 GC 情况

    memory 左上角有个按钮可以手动触发 GC

  • lighthouse 插件,生成性能报告

# 跨标签页通讯

不同标签页间的通讯,本质原理就是去运用一些可以 共享的中间介质,因此比较常用的有以下方法:

  • 通过父页面window.open()和子页面postMessage
    • 异步下,通过 window.open('about: blank')tab.location.href = '*'
  • 设置同域下共享的localStorage与监听window.onstorage
    • 重复写入相同的值无法触发
    • 会受到浏览器隐身模式等的限制
  • 设置共享cookie与不断轮询脏检查(setInterval)
  • 借助服务端或者中间层实现

# Web Worker

现代浏览器为JavaScript创造的 多线程环境。可以新建并将部分任务分配到worker线程并行运行,两个线程可 独立运行,互不干扰,可通过自带的 消息机制 相互通信。

基本用法:

// 创建 worker
const worker = new Worker('work.js')

// 向 worker 线程推送消息
worker.postMessage('Hello World')

// 监听 worker 线程发送过来的消息
worker.onmessage = function(event) {
  console.log('Received message ' + event.data)
}
1
2
3
4
5
6
7
8
9
10

限制:

  • 同源限制
  • 无法使用 document / window / alert / confirm
  • 无法加载本地资源

# 为什么 HTTP1.1 不能实现多路复用

HTTP1.x 是序列和阻塞机制

HTTP2.0 是多工复用 TCP 连接,在一个连接里,客户端和浏览器都可以同时发送多个请求或回应,而且不用按照顺序一一对应,这样就避免了"队头堵塞"。

  • 举例来说,在一个 TCP 连接里面,服务器同时收到了 A 请求和 B 请求,于是先回应 A 请求,结果发现处理过程非常耗时,于是就发送 A 请求已经处理好的部分, 接着回应 B 请求,完成后,再发送 A 请求剩下的部分。
  • 旧的 http1.1 是会等 A 请求完全处理完后在 处理 B 请求,会阻塞
  • 另:http1.1 已经实现了管道机制:即 在同一个 TCP 连接里面,客户端可以同时发送多个请求。http 1.0 并做不到,所以效率很低
最近更新: 4 小时前