网络中一次请求的执行历程

物理层

核心问题 - 如何实现物理连接

可以直接使用网线连接两台电脑,或者使用Hub连接多台电脑,Hub会将收到的每一个字节都复制到其他端口上去。

常见协议

数据链路层

数据链路层

核心问题 - 这个包是发给谁的?谁应该接收?

这里用到一个物理地址,叫作链路层地址。但是因为第二层主要解决媒体接入控制的问题,所以它常被称为MAC 地址
数据包封装时会封装上目标MAC地址,数据包在链路上广播时,MAC的网卡就可以发现这个包是发给它的。
回传时,源MAC地址就会变成目标MAC地址,再返回给请求的机器。

核心问题 - 大家都在发,会不会产生混乱?有没有谁先发、谁后发的规则?

MAC(Medium Access Control),即媒体访问控制,主要功能其实就是控制在往媒体上发数据的时候,谁先发、谁后发的问题,防止发生混乱。这个问题中的规则,学名叫多路访问
多路访问规则有好几种:

  • 方式一:分多个车道。每个车一个车道,你走你的,我走我的。这在计算机网络里叫作信道划分;
  • 方式二:今天单号出行,明天双号出行,轮着来。这在计算机网络里叫作轮流协议;
  • 方式三:不管三七二十一,有事儿先出门,发现特堵,就回去。错过高峰再出。我们叫作随机接入协议。著名的以太网,用的就是这个方式。

核心问题 - 如果发送的时候出现了错误,怎么办?

接收包时会进行CRC(循环冗余检测)校验,通过 XOR 异或的算法,来计算整个包是否在发送的过程中出现了错误。

常见协议 - MAC协议

数据链路层设备 - Hub

Hub太傻,每个包都广播,数据链路层使用交换机来转发数据包。

  1. 学习能力
    一开始不知道目标MAC地址在哪个端口,交换机会全发,但是之后再碰到发往该MAC地址的数据就可以直接发给对应的端口了。
    当然,每个机器的 IP 地址会变,所在的口也会变,因而交换机上的学习的结果,我们称为转发表,是有一个过期时间的。
  2. 环路问题
    交换机环路问题
    我们来想象一下机器 1 访问机器 2 的过程。一开始,机器 1 并不知道机器 2 的 MAC 地址,所以它需要发起一个 ARP 的广播。广播到达机器 2,机器 2 会把 MAC 地址返回来。主要问题是这里的广播包也会到达交换机,交换机A刚开始不知道机器2在哪个局域网(实际上在局域网1),因此它会将广播消息放到局域网二,交换机B此时又会将广播包发回局域网一,形成环路
    解决环路的方法是STP协议(Spanning Tree Protocol / 最小生成树)

网络层

网络层

常见协议 - ARP协议

已知IP地址,求MAC地址使用的是ARP协议。
为了知道目标主机的MAC地址,需要发送一个广播包,谁是这个 IP 谁来回答。
为了避免每次都用 ARP 请求,机器本地也会进行 ARP 缓存。当然机器会不断地上线下线,IP 也可能会变,所以 ARP 的 MAC 地址缓存过一段时间就会过期。

常用协议 - IP

IP、MAC数据包-图片来自(https://time.geekbang.org/column/article/8590)
发IP数据包时,网络程序会先判断目标IP地址和当前IP地址是否在同一个网段:

判断是否同一网段需要使用CIDR和子网掩码。

  1. 如果是同一网段,那就没网关什么事情,直接将源地址和目标地址放入 IP 头中,然后通过 ARP 获得 MAC 地址,将源 MAC 和目的 MAC 放入 MAC 头中,发出去就可以了;
  2. 如果不是同一网段,这时需要发往默认网关Gateway,Gateway的地址一定是和源IP地址是一个网段的。比如我们在宿舍里上网,路由器就是一个小网关。

如何发往默认网关呢?
网关不是和源 IP 地址是一个网段的么?这个过程就和发往同一个网段的其他机器是一样的:将源地址和目标 IP 地址放入 IP 头中,通过 ARP 获得网关的 MAC 地址,将源 MAC 和网关的 MAC 放入 MAC 头中,发送出去。网关所在的端口,例如 192.168.1.1/24 将网络包收进来,然后接下来怎么做,就完全看网关的了。

5类地址

IP的5类地址
A、B、C三类地址范围及私有地址

网络层设备 - 路由器

很多情况下,人们把网关就叫做路由器。其实不完全准确,而另一种比喻更加恰当:路由器是一台设备,它有五个网口或者网卡,相当于有五只手,分别连着五个局域网。每只手的 IP 地址都和局域网的 IP 地址相同的网段,每只手都是它握住的那个局域网的网关。

路由方式

路由分为静态路由动态路由两种:

  • 静态路由:其实就是在路由器上的路由表中,配置一条一条规则,这些规则包括:想去哪(目标IP)、先去哪(下一跳的IP)、从哪去(路由器端口)。

    除了根据目标IP配置路由外,还可以配置多参数的策略路由

  • 动态路由:使用动态路由路由器,可以根据路由协议算法生成动态路由表,随网络运行状况的变化而变化。
    具备动态路由功能的路由器能够实时计算两个节点之间的最短路径,可用的算法包括:
    • 基于Bellman-Ford算法的距离矢量路由(distance vector routing),实现为BGP(Border Gateway Protocol,外网路由协议),应用于大规模的网络中,因为大规模网络中自治系统比较少,所以不会有收敛慢、坏消息传得慢等问题。实际上BGP中使用的是路径矢量路由协议,是距离矢量路由的升级版
    • 基于Dijkstra算法的链路状态路由(link state routing),实现为OSPF(Open Shortest Path First,开放式最短路径优先),常用于组织内网中。

为了方便起见,下面讨论IP数据包的传播流程时,都是采用的静态路由。

网关类型

网关分为转发网关NAT网关

  • 转发网关:不改变 IP 地址。
    转发时,将下一跳的IP转换为MAC地址放入目标MAC地址字段,然后将当前主机的MAC地址放入源MAC地址字段,在这期间源IP目标IP都是不变的,这个过程又称为
  • NAT网关:改变IP地址。
    在局域网内部因为都在同一个网段内,大家都能互相通过ARP识别对方的IP地址(转换为MAC地址)。
    但是在公网上就不一样了,因为公网IP很少,所以大家都会用DHCP复用一个IP地址(即局域网),那么我们怎么从一台主机访问另一台主机呢?

IP数据包究竟是怎么从源主机发送到目标主机的?

如果是单纯的局域网通信会简单很多,网络拓扑中只需要转发网关就够了,一个IP数据包的转发流程如下:
IP数据包转发例子

  1. 网卡获取IP时已经拿到了网关的IP,因此可以方便地通过ARP解析出网关的MAC,所以初始数据包里的字段我们就可以确定了:
    源MAC:服务器A的MAC;
    目标MAC:192.168.1.1 这个网口的 MAC
    源 IP:192.168.1.101
    目标 IP:192.168.4.101
  2. 数据包发送到网关192.168.1.1后,发现MAC一致于是接收该数据包;
  3. 路由器A(也就是网关192.168.1.1所在的那个路由器)中配置了静态路由:要想访问子网192.168.4.0/24,就要从192.168.56.1这个网口出去,下一跳为192.168.56.2。于是这一次数据包是这样的:
    源 MAC:192.168.56.1 的 MAC 地址
    目标 MAC:192.168.56.2 的 MAC 地址
    源 IP:192.168.1.101
    目标 IP:192.168.4.101
  4. 同理,包到达192.168.56.2这个网口后,发现MAC一致于是接收数据包;
  5. 在路由器B中配置了静态路由:要访问子网192.168.4.0/24,直接从192.168.4.1这个口出去即可,因为这个网卡就是这个网段的,这就是最后一跳了。包内容是:
    源 MAC:192.168.4.1 的 MAC 地址
    目标 MAC:192.168.4.101 的 MAC 地址
    源 IP:192.168.1.101
    目标 IP:192.168.4.101
  6. 最后,MAC地址匹配,包到达服务器B。

在这个过程中,每经过一个局域网,MAC地址都会变化,但是IP地址不会变,数据包传播时只是将下一跳的IP地址转换为MAC地址放入MAC头。

现代的网络通信基本都不会是只有局域网的,数据包需要经过公网传递到世界的另一个角落,但是根据IPv4的特点,如果给世界上每个需要上网的人都分配一个IP地址,IP地址空间是绝对不够的,因此就需要引入NAT了。
引入NAT路由后,两个局域网内可以有相同的IP,只不过局域网内需要暴露到公网的服务器还需要有一个外网IP,比如,目标服务器 B 在公网上要有一个公网IP 192.168.56.2。在网关 B 上,我们记下来,公网IP 192.168.56.2 对应局域网IP 192.168.1.101。凡是要访问 192.168.56.2,都转成 192.168.1.101。
IP数据包转发例子

  1. 源服务器A要访问目标服务器B,要指定目标地址为192.168.56.2,因为192.168.56.2和服务器A不在一个网段内,因此要发送到网关,网关是之前已经静态配置好的192.168.1.1。这里同样通过ARP可以得到下一跳的MAC地址,因此数据包就是:
    源 MAC:服务器 A 的 MAC
    目标 MAC:192.168.1.1 这个网口的 MAC
    源 IP:192.168.1.101
    目标 IP:192.168.56.2
  2. 包到达192.168.1.1这个网口,路由器发现MAC一致就接收了这个包;
  3. 在路由器A中因为配置了静态路由:要想访问192.168.56.2/24这个网段,直接从192.168.56.1这个网口出去即可,当前路由器其实就是最后一跳了。这里,同样可以通过ARP获取到目标服务器的MAC地址:
    源 MAC:192.168.56.1 的 MAC 地址
    目标 MAC:192.168.56.2 的 MAC 地址
    源 IP:192.168.56.1
    目标 IP:192.168.56.2
  4. 数据包到达192.168.56.2这个网口,发现MAC地址一致,于是接收数据包。
    注意此时192.168.56.2所在的路由器B是一个NAT网关,它上面配置了公网IP 192.168.56.2对应着局域网IP的192.168.1.101,于是改为访问192.168.1.101。
  5. 在路由器 B 中配置了静态路由:要想访问 192.168.1.0/24,要从 192.168.1.1 这个口出去,那么接下来就是从192.168.1.1这个口发出去发给192.168.1.101了。同理可以通过ARP解析出MAC地址,得到数据包:
    源 MAC:192.168.1.1 的 MAC 地址
    目标 MAC:192.168.1.101 的 MAC 地址
    源 IP:192.168.56.1
    目标 IP:192.168.1.101
  6. 包到达服务器B,MAC地址匹配,所以最终接收该数据包。

当服务器B要发送响应数据包时,使用上面数据包中的源IP作为目标IP,而路由器A做NAT,将目标IP转换为局域网IP。
但是,上面的源IP是网关的公网IP,并不是我们的源服务器A的IP,那么路由器又怎么知道应该发给A而不是其局域网内的其他服务器呢?其实NAT协议只支持1对1转换,即一个内网IP和一个外网IP,如果需要支持一个外网IP对应多个内网IP,则需要NAPT协议的支持,协议会维护一张映射表:内网ip:port–>外网ip:空闲port。

常见协议 - ICMP(Internet Control Message Protocol / 互联网控制报文协议)

发一个带标识的数据包,发送方统计应答情况。

常用命令 - traceroute

  1. 故意设置特殊的 TTL,来追踪去往目的地时沿途经过的路由器。
    Traceroute 的参数指向某个目的 IP 地址,它会发送一个 UDP 的数据包。将 TTL 设置成 1,也就是说一旦遇到一个路由器或者一个网关,它就会过期了,于是,返回一个 ICMP 包,也就是网络差错包,类型是时间超时。
    之后,将TLL设置为2,只能探2个路由器或网关,以此类推,直到到达目标主机。这样,Traceroute 就拿到了所有的路由器 IP。
    怎么知道 UDP 有没有到达目的主机呢?Traceroute 程序会发送一份 UDP 数据报给目的主机,但它会选择一个不可能的值作为 UDP 端口号(大于 30000)。当该数据报到达时,将使目的主机的 UDP 模块产生一份“端口不可达”错误 ICMP 报文。如果数据报没有到达,则可能是超时。
  2. 故意设置不分片,从而确定路径的 MTU。
1
traceroute 

常用命令 - ip

添加策略路由:

1
2
3
ip rule add from 192.168.1.0/24 table 10
ip rule add from 192.168.2.0/24 table 20
ip route add default scope global nexthop via 100.100.100.1 weight 1 nexthop via 200.200.200.1 weight 2

常用命令 - 配置IP地址

使用 net-tools:

1
2
$ sudo ifconfig eth1 10.0.0.1/24
$ sudo ifconfig eth1 up

使用 iproute2:

1
2
$ sudo ip addr add 10.0.0.1/24 dev eth1
$ sudo ip link set up eth1

常用命令 - netstat

netstat 命令一般用于统计服务器上各端口的网络连接情况,可以用于分析 IP、TCP、UDP 和 ICMP 协议相关的统计数据。

1
2
3
4
5
6
7
8
9
huanggaochi@app08:~$ netstat
Active Internet connections (w/o servers)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 32 0 app08.hp.sp.tst.b:11920 123.151.71.149:https CLOSE_WAIT
...
Active UNIX domain sockets (w/o servers)
Proto RefCnt Flags Type State I-Node Path
unix 2 [ ] DGRAM 1453 /run/systemd/notify
...

netstat 的输出结果主要分为两个部分:

  1. Active Internet connections,表示有源 TCP 连接,其中,
    • Recv-QSend-Q表示接收队列和发送队列,正常情况下,这两个值应该都是 0,否则说明数据包正在队列中堆积。
    • Proto,连接使用的协议;
    • State,套接字当前状态;
  2. Active UNIX domain sockets,表示有源 Unix 域套接字(和网络套接字一样,但是只能用于本机通信,性能可以提高很多)。
    • RefCnt,连接到本套接字上的进程号;
    • Type,套接字的类型;
    • Path,连接到套接字的其他进程使用的路径名。

可以使用选项指定目标套接字类型:

  • -t:TCP
  • -u:UDP
  • -raw:RAW 类型
  • –unix:UNIX 域类型
  • –ax25:AX25 类型
  • –ipx:ipx 类型
  • –netrom:netrom 类型

连接状态:

  • LISTEN:侦听来自远方的 TCP 端口的连接请求
  • SYN-SENT:再发送连接请求后等待匹配的连接请求(如果有大量这样的状态包,检查是否中招了)
  • SYN-RECEIVED:再收到和发送一个连接请求后等待对方对连接请求的确认(如有大量此状态,估计被 flood 攻击了)
  • ESTABLISHED:代表一个打开的连接
  • FIN-WAIT-1:等待远程 TCP 连接中断请求,或先前的连接中断请求的确认
  • FIN-WAIT-2:从远程 TCP 等待连接中断请求
  • CLOSE-WAIT:等待从本地用户发来的连接中断请求
  • CLOSING:等待远程 TCP 对连接中断的确认
  • LAST-ACK:等待原来的发向远程 TCP 的连接中断请求的确认(不是什么好东西,此项出现,检查是否被攻击)
  • TIME-WAIT:等待足够的时间以确保远程 TCP 接收到连接中断请求的确认
  • CLOSED:没有任何连接状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 显示网卡列表
netstat -i
# 显示组播组的关系
netstat -g
# 显示网络统计
netstat -s
# 显示监听中的udp和tcp连接,显示数字而不是尝试用符号表示(比如localhost会显示为127.0.0.1),显示进程号
netstat -lutnp
# 显示关于以太网的统计数据,输出结果增加User、Inode两个字段,列出的项目包括传送的数据报的总字节数、错误数、删除数、数据报的数量和广播的数量。这些统计数据既有发送的数据报数量,也有接收的数据报数量。这个选项可以用来统计一些基本的网络流量)
netstat -e
# 显示路由信息,也可以用route -n命令
netstat -r
# 统计机器各个状态网络连接个数
netstat -an | awk '/^tcp/ {++S[$NF]} END {for (a in S) print a,S[a]}'
# 统计每种连接的数量
netstat -ant | awk '{print $6}' | sort | uniq -c
# 查看连接某服务端口最多的IP地址,下面假设服务器对外暴露的IP为192.168.1
netstat -ant | grep "192.168.1.*" | awk '{print $5}' | awk -F: '{print $1}' | sort -nr | uniq -c
# 显示TCP连接信息,并找出程序运行的端口,在结果中增加一列“PID/Program name”
netstat -anp | grep ssh

传输层

传输层

常见协议 - UDP

  • 面向无连接
    交互前不需要建立连接,可以直接发生数据包。
  • 提供不可靠交付
    UDP 继承了 IP 包的特性,不保证不丢失,不保证按顺序到达。
  • 基于数据报
    UDP 继承了 IP 的特性,基于数据报的,一个一个地发,一个一个地收。
  • 无拥塞控制

UDP的数据包格式:
UDP数据包格式

UDP的使用场景:

  1. 需要资源少,在网络情况比较好的内网,或者对于丢包不敏感的应用
  2. 不需要一对一沟通,建立连接,而是可以广播的应用
    UDP 的不面向连接的功能,可以使得可以承载广播或者多播的协议。DHCP 就是一种广播的形式,就是基于 UDP 协议的。
  3. 需要处理速度快,时延低,可以容忍少数丢包,即使网络拥塞也不改变发送的速率

常见协议 - TCP

  • TCP是面向连接
    所谓的建立连接,是为了在客户端和服务端维护连接,而建立一定的数据结构来维护双方交互的状态,用这样的数据结构来保证所谓的面向连接的特性
  • TCP 提供可靠交付
    通过 TCP 连接传输的数据,无差错、不丢失、不重复、并且按序到达。
  • TCP 是面向字节流
    发送的时候发的是一个流,没头没尾。
  • 拥塞控制
    TCP意识到包丢弃了或者网络的环境不好了,就会根据情况调整自己的行为,看看是不是发快了,要不要发慢点。

TCP 优点

  • 可靠,稳定
    TCP 的可靠体现在 TCP 在传递数据之前,会有三次握手来建立连接,而且在数据传递时,有确认、窗口、重传、拥塞控制机制,在数据传完后,还会断开连接用来节约系统资源。

TCP 缺点

慢,效率低,占用系统资源高,易被攻击
TCP 在传递数据之前,要先建连接,这会消耗时间,而且在数据传递时,确认机制、重传机制、拥塞控制机制等都会消耗大量的时间,而且要在每台设备上维护所有的传输连接,事实上,每个连接都会占用系统的 CPU、内存等硬件资源。 而且,因为 TCP 有确认机制、三次握手机制,这些也导致 TCP 容易被人利用,实现 DOS、DDOS、CC 等攻击。

TCP的数据包格式

TCP数据包格式

  1. 源端口号和目标端口号,用于确定数据应该发给哪个应用;
  2. 包序号
    用于解决乱序问题;
  3. 确认序号
    解决丢包问题,如果没有收到就重新发送,直到送达。
  4. 状态位
    例如SYN是发起一个连接;
    ACK是回复;
    RST是重新连接;
    FIN是结束连接。
  5. 窗口大小
    TCP要做到流量控制,通信双方各声明一个窗口,标识自己当前的处理能力。

连接的建立 - 三次握手

TCP三次握手
一开始,客户端和服务端都处于 CLOSED 状态。先是服务端主动监听某个端口,处于 LISTEN 状态。然后客户端主动发起连接 SYN,之后处于 SYN-SENT 状态。服务端收到发起的连接,返回 SYN,并且 ACK 客户端的 SYN,之后处于 SYN-RCVD 状态。客户端收到服务端发送的 SYN 和 ACK 之后,发送 ACK 的 ACK,之后处于 ESTABLISHED 状态,因为它一发一收成功了。服务端收到 ACK 的 ACK 之后,处于 ESTABLISHED 状态,因为它也一发一收了。
从连接建立的流程中可见,三次握手除了双方建立连接外,还能确定TCP包的序号问题。

总而言之:

  1. 首先 Client 端发送连接请求报文,Server 段接受连接后回复 ACK 报文,并为这次连接分配资源。
  2. Client 端接收到 ACK 报文后也向 Server 段发生 ACK 报文,并分配资源,这样 TCP 连接就建立了。
    TCP三次握手
    最初两端的 TCP 进程都处于 CLOSED 关闭状态,A(Client)主动打开连接,而 B(Server)被动打开连接。(A、B 关闭状态 CLOSED——B 创建 TCB,进入 LISTEN 状态,等待 A 请求——A 同步已发送状态 SYN-SENT——B 同步收到状态 SYN-RCVD——A、B 连接已建立状态 ESTABLISHED)
  3. 第一次握手:起初两端都处于 CLOSED 关闭状态,A(Client)将标志位 SYN 置为 1,随机产生一个值 seq=x,并将该数据包发送给 B(Server),A(Client)进入 SYN-SENT 状态,等待 B(Server)确认;
  4. 第二次握手:B(Server)收到连接请求报文段后,如同意建立连接,则向 A(Client)发送确认,在确认报文段中(SYN=1,ACK=1,确认号 ack=x+1,初始序号 seq=y),B(Server)TCP 服务器进程进入 SYN-RCVD(同步收到)状态;
  5. 第三次握手:TCP 客户进程收到 B(Server)的确认后,要向 B(Server)给出确认报文段(ACK=1,确认号 ack=y+1,序号 seq=x+1)(初始为 seq=x,第二个报文段所以要+1),ACK 报文段可以携带数据,不携带数据则不消耗序号。TCP 连接已经建立,A 进入 ESTABLISHED(已建立连接)。
    当 B 收到 A 的确认后,也进入 ESTABLISHED 状态。

    TCB 传输控制块 Transmission Control Block,存储每一个连接中的重要信息,如 TCP 连接表,到发送和接收缓存的指针,到重传队列的指针,当前的发送和接收序号。

一些问题:

  1. 为什么 A 还要发送一次确认呢?可以二次握手吗?
    主要为了防止已失效的连接请求报文段突然又传送到了 B,因而产生错误。如 A 发出连接请求,但因连接请求报文丢失而未收到确认,于是 A 再重传一次连接请求。后来收到了确认,建立了连接。数据传输完毕后,就释放了连接,A 发出了两个连接请求报文段,其中第一个丢失,第二个到达了 B,但是第一个丢失的报文段只是在某些网络结点长时间滞留了,延误到连接释放以后的某个时间才到达 B,此时 B 误认为 A 又发出一次新的连接请求,于是就向 A 发出确认报文段,同意建立连接,不采用三次握手,只要 B 发出确认,就建立新的连接了,此时 A 不理睬 B 的确认且不发送数据,则 B 一致等待 A 发送数据,浪费资源。
  2. Server 端易受到 SYN 攻击?
    服务器端的资源分配是在二次握手时分配的,而客户端的资源是在完成三次握手时分配的,所以服务器容易受到 SYN 洪泛攻击,SYN 攻击就是 Client 在短时间内伪造大量不存在的 IP 地址,并向 Server 不断地发送 SYN 包,Server 则回复确认包,并等待 Client 确认,由于源地址不存在,因此 Server 需要不断重发直至超时,这些伪造的 SYN 包将长时间占用未连接队列,导致正常的 SYN 请求因为队列满而被丢弃,从而引起网络拥塞甚至系统瘫痪。
    防范 SYN 攻击措施:降低主机的等待时间使主机尽快的释放半连接的占用,短时间受到某 IP 的重复 SYN 则丢弃后续请求。

连接的断开 - 四次挥手

TCP四次挥手
假设 Client 端发起中断连接请求,也就是发送 FIN 报文。Server 端接到 FIN 报文后,意思是说”我 Client 端没有数据要发给你了”,但是如果你还有数据没有发送完成,则不必急着关闭 Socket,可以继续发送数据。所以你先发送 ACK,”告诉 Client 端,你的请求我收到了,但是我还没准备好,请继续你等我的消息”。这个时候 Client 端就进入 FIN_WAIT 状态,继续等待 Server 端的 FIN 报文。当 Server 端确定数据已发送完成,则向 Client 端发送 FIN 报文,”告诉 Client 端,好了,我这边数据发完了,准备好关闭连接了”。Client 端收到 FIN 报文后,”就知道可以关闭连接了,但是他还是不相信网络,怕 Server 端不知道要关闭,所以发送 ACK 后进入 TIME_WAIT 状态,如果 Server 端没有收到 ACK 则可以重传。“,Server 端收到 ACK 后,”就知道可以断开连接了”。Client 端等待了 2MSL 后依然没有收到回复,则证明 Server 端已正常关闭,那好,我 Client 端也可以关闭连接了。Ok,TCP 连接就这样关闭了!

数据传输结束后,通信的双方都可释放连接,A 和 B 都处于 ESTABLISHED 状态。(A、B 连接建立状态 ESTABLISHED——A 进入等待 1 状态 FIN-WAIT-1——B 关闭等待状态 CLOSE-WAIT——A 进入等待 2 状态 FIN-WAIT-2——B 最后确认状态 LAST-ACK——A 时间等待状态 TIME-WAIT——B、A 关闭状态 CLOSED)

  1. A 的应用进程先向其 TCP 发出连接释放报文段(FIN=1,序号 seq=u),并停止再发送数据,主动关闭 TCP 连接,进入 FIN-WAIT-1(终止等待 1)状态,等待 B 的确认。
  2. B 收到连接释放报文段后即发出确认报文段,(ACK=1,确认号 ack=u+1,序号 seq=v),B 进入 CLOSE-WAIT(关闭等待)状态,此时的 TCP 处于半关闭状态,A 到 B 的连接释放。
  3. A 收到 B 的确认后,进入 FIN-WAIT-2(终止等待 2)状态,等待 B 发出的连接释放报文段。
  4. B 没有要向 A 发出的数据,B 发出连接释放报文段(FIN=1,ACK=1,序号 seq=w,确认号 ack=u+1),B 进入 LAST-ACK(最后确认)状态,等待 A 的确认。
  5. A 收到 B 的连接释放报文段后,对此发出确认报文段(ACK=1,seq=u+1,ack=w+1),A 进入 TIME-WAIT(时间等待)状态。此时 TCP 未释放掉,需要经过时间等待计时器设置的时间 2MSL 后,A 才进入 CLOSED 状态。

总而言之:

  1. 刚开始A发送断开连接请求,进入FIN_WAIT_1状态;
  2. B接收请求进入CLOSED_WAIT状态,并发回响应;
  3. A接收B响应,进入FIN_WAIT_2状态;
  4. B发送断开连接请求,并进入LAST_ACK状态;
  5. A接收请求,进入TIME_WAIT状态,并发回响应;
    A会在发出响应2MSL后自动进入CLOSED状态。
    MSL 是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为 TCP 报文基于是 IP 协议的,而 IP 头中有一个 TTL 域,是 IP 数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。协议规定 MSL 为 2 分钟,实际应用中常用的是 30 秒,1 分钟和 2 分钟等。
  6. B接收响应后进入CLOSED状态。

该流程中让人比较困惑的问题是:

  1. 为什么A断开连接后,第4步还要B在反向发起一次断开连接?
    不能直接断开,因为A单方面断开连接时,A不知道B是不是还有事情要处理。
  2. 如果第4步B没有重新发起断开连接(只有2次挥手),连接怎么断开?
    如果这期间B宕机了没有重新发起断开连接,A将永远停留在FIN_WAIT_2的状态,TCP 协议里面并没有对这个状态的处理,但是 Linux 有,可以调整 tcp_fin_timeout 这个参数,设置一个超时时间。
  3. 第5步如果A响应没有返回给B,B怎么断开?
    此时B会重新发起一次断开请求,因而TCP要求A最后等待一段时间TIME_WAIT,这个时间要足够长,长到如果B没有收到ACK,B可以重试且时间足够到达。
    不过,还有一个异常情况就是,B 超过了 2MSL 的时间,依然没有收到它发的 FIN 的 ACK,怎么办呢?按照 TCP 的原理,B 当然还会重发 FIN,这时A再收到这个包后,会直接返回RST,B就知道早已断开连接了。
  4. TIME_WAIT
    TIME_WAIT 状态容易被人误解,比如当使用三方系统的服务时,看到系统 TIME_WAIT 数量特别高,于是赖对方没有及时把连接释放掉,实际上TIME_WAIT 产生在主动断开连接的一方
  5. 为什么 A 在 TIME-WAIT 状态必须等待 2MSL 的时间?(MSL 最长报文段寿命 Maximum Segment Lifetime,MSL=2)
    原因有 2:
    保证 A 发送的最后一个 ACK 报文段能够到达 B。这个 ACK 报文段有可能丢失,使得处于 LAST-ACK 状态的 B 收不到对已发送的 FIN+ACK 报文段的确认,B 超时重传 FIN+ACK 报文段,而 A 能在 2MSL 时间内收到这个重传的 FIN+ACK 报文段,接着 A 重传一次确认,重新启动 2MSL 计时器,最后 A 和 B 都进入到 CLOSED 状态,若 A 在 TIME-WAIT 状态不等待一段时间,而是发送完 ACK 报文段后立即释放连接,则无法收到 B 重传的 FIN+ACK 报文段,所以不会再发送一次确认报文段,则 B 无法正常进入到 CLOSED 状态。
    防止“已失效的连接请求报文段”出现在本连接中。A 在发送完最后一个 ACK 报文段后,再经过 2MSL,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失,使下一个新的连接中不会出现这种旧的连接请求报文段。
  6. 为什么连接的时候是三次握手,关闭的时候却是四次握手?
    因为当 Server 端收到 Client 端的 SYN 连接请求报文后,可以直接发送 SYN+ACK 报文。其中 ACK 报文是用来应答的,SYN 报文是用来同步的。但是关闭连接时,当 Server 端收到 FIN 报文时,很可能并不会立即关闭 SOCKET,所以只能先回复一个 ACK 报文,告诉 Client 端,”你发的 FIN 报文我收到了”。只有等到我 Server 端所有的报文都发送完了,我才能发送 FIN 报文,因此不能一起发送。故需要四步握手。
  7. 为什么 TIME_WAIT 状态需要经过 2MSL(最大报文段生存时间)才能返回到 CLOSE 状态?
    虽然按道理,四个报文都发送完毕,我们可以直接进入 CLOSE 状态了,但是我们必须假象网络是不可靠的,有可能最后一个 ACK 丢失。所以 TIME_WAIT 状态就是用来重发可能丢失的 ACK 报文。
  8. 如何优化
    我们可以通过修改系统参数来优化服务器
    tcp_tw_reuse: 是否重用处于 TIME_WAIT 状态的 TCP 链接 (设为 true)
    tcp_max_tw_buckets: 处于 TIME_WAIT 状态的 SOCKET 最大数目 (调大,这个参数千万不要调小了)
    tcp_fin_timeout: 处于 FIN_WAIT_2 的时间 (调小)

TCP状态机

上面的握手和挥手流程汇总为状态机如下图所示:
TCP状态机

累计确认(cumulative acknowledgment,或称为累计应答)

TCP中并不是发1收1,而是使用一个缓冲区保存数据包,这个缓冲区称为窗口,发送端的发送窗口如下图所示:
TCP发送窗口

  1. 发送了并且已经确认的。
  2. 发送了并且尚未确认的。
  3. 没有发送,但是已经等待发送的。
  4. 没有发送,并且暂时还不会发送的。
  • LastByteAcked:第一部分和第二部分的分界线
  • LastByteSent:第二部分和第三部分的分界线
  • LastByteAcked + AdvertisedWindow:第三部分和第四部分的分界线

为什么会有第3和第4部分?难道这些不都是等待发送的吗?其实区分第3和第4部分的主要目的是流量控制,在 TCP 里,接收端会给发送端报一个窗口的大小,叫 Advertised window。这个窗口的大小应该等于上面的第2部分加上第3部分,超过了这个窗口的接收端处理不过来,就不发送了,作为第4部分。

接收端同样也有一个接收窗口:
TCP接收窗口

  1. 接受并且确认过的。
  2. 还没接收,但是马上就能接收的。
  3. 还没接收,也没法接收的。
  • MaxRcvBuffer:最大缓存的量;
  • LastByteRead 之后是已经接收了,但是还没被应用层读取的;
  • NextByteExpected 是第一部分和第二部分的分界线。

顺序问题与丢包问题

TCP中的重发有两种:

  1. 超时重试
    对每一个发送了,但是没有 ACK 的包,都有设一个定时器,超过了一定的时间,就重新尝试。
    这个时间不宜过短,时间必须大于往返时间 RTT,否则会引起不必要的重传。也不宜过长,这样超时时间变长,访问就变慢了。估计往返时间,需要 TCP 通过采样 RTT 的时间,然后进行加权平均,算出一个值,而且这个值还是要不断变化的,因为网络状况不断地变化。除了采样 RTT,还要采样 RTT 的波动范围,计算出一个估计的超时时间。由于重传时间是不断变化的,我们称为自适应重传算法(Adaptive Retransmission Algorithm)
    超时间隔加倍:每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送。
  2. 快速重传
    超时重传存在的问题是超时周期可能会比较长,为了加快重传,TCP采用一种快速重传机制:
    当接收方收到一个序号大于下一个所期望的报文段时,就会检测到数据流中的一个间隔,于是它就会发送冗余的 ACK,仍然 ACK 的是期望接收的报文段。而当客户端收到三个冗余的 ACK 后,就会在定时器过期之前,重传丢失的报文段。
    例如,接收方发现 6 收到了,8 也收到了,但是 7 还没来,那肯定是丢了,于是发送 6 的 ACK,要求下一个是 7。接下来,收到后续的包,仍然发送 6 的 ACK,要求下一个是 7。当客户端收到 3 个重复 ACK,就会发现 7 的确丢了,不等超时,马上重发。
  3. Selective Acknowledgment (SACK)
    接收方可以将缓存的地图发送给发送方,例如发送ACK6、SACK8,SACK8表示已接收但未处理的,有了地图,发送方一下子就能看出来是 7 丢了。

流量控制

当发送未确认窗口中最早的一个包接收到了确认,则窗口将前移一格:
TCP接收窗口
上图中,5接收到了确认,此时窗口前移一格,第14个包可以发送了。
TCP接收窗口移动
如果接收方处理太慢,导致缓存中没有空间了,可以通过确认信息修改窗口的大小,甚至可以设置为 0,则发送方将暂时停止发送。
假设一个极端情况,接收端的应用一直不读取缓存中的数据,当数据包 6 确认后,窗口大小就不能再是 9 了,就要缩小一个变为 8。
那么缩小后什么时候恢复呢?发送方会定时发送窗口探测数据包,看是否有机会调整窗口的大小。

拥塞控制

拥塞控制也是通过控制窗口大小来实现的,前面的滑动窗口 rwnd 是怕发送方把接收方缓存塞满,而拥塞窗口 cwnd,是怕把网络塞满。

TCP的拥塞控制主要是为了避免丢包和超时重传问题:

  • 如果窗口不经控制——像UDP那样——很有可能发送的数据量超过中间设备的承载能力,多出来的包就会被丢弃,即发生了丢包,这是我们不希望看到的。

  • 如果在这些设备上加缓存,处理不过来的先保存在缓存队列里,这样虽然不会丢失,但是会增加时延,如果时延达到一定程度,就会导致超时重传

  • 慢启动(指数增长)
    刚开始不清楚网络情况,因此发送数据包时一次只能发1个,后来按2、4、8的指数性增长速度来增长;

  • 线性增长
    当超过一个阈值ssthresh=65535时,可能速度达到了网络性能,这时会慢下来,变成线程增长,每收到一个确认后,cwnd才会增加1/cwnd

  • 指数递减
    当发生了丢包,需要超时重传时,会设置ssthresh=cwnd/2,并将cwnd设置为1,重新开始慢启动,这种减速方式的问题是太过激进,从原来的高速马上减到1,会造成明显的网络卡顿,因为这个问题,一般会采用快速重传算法

  • 快速重传算法
    当接收端发现丢了一个中间包的时候,发送三次前一个包的 ACK,于是发送端就会快速地重传,不必等待超时再重传。
    TCP 认为这种情况不严重,因为大部分没丢,只丢了一小部分,cwnd 减半为 cwnd/2,然后 sshthresh = cwnd,当三个包返回的时候,cwnd = sshthresh + 3,也就是说没有立刻减到1。

应用层

应用层

常见协议 - HTTP

一次请求的历程

HTTP请求的执行过程:

  1. 先通过DNS将域名解析为IP地址;
  2. 建立TCP连接;
  3. 将HTTP请求放到TCP数据包中发出。

请求

HTTP数据包格式:
HTTP数据包格式

1
2
3
4
5
6
7
GET /somedir/page.html?query-string#anchor HTTP/1.1
   Host:www.xxx.com
   Connection:close
   User-agent:Mozilla/4.0
   Accept-language:zh-cn
Content-length:123
  ...回车符和换行符

第一行是请求行(request line)只有固定的几个字段:
方法 Method 名称是区分大小写的。当某个请求所针对的资源不支持对应的请求方法的时候,服务器应当返回状态码 405(Method Not Allowed),当服务器不认识或者不支持对应的请求方法的时候,应当返回状态码 501(Not Implemented)。

  • GET:向指定的资源发出“显示”请求。使用 GET 方法应该只用在读取数据,而不应当被用于产生“副作用”的操作中,例如在 Web Application 中。其中一个原因是 GET 可能会被网络蜘蛛等随意访问。
  • POST:向指定资源提交数据,请求服务器进行处理(例如提交表单或者上传文件)。数据被包含在请求本文中。这个请求可能会创建新的资源或修改现有资源,或二者皆有。
  • PUT:向指定资源位置上传其最新内容。
  • DELETE:请求服务器删除 Request-URI 所标识的资源。
  • OPTIONS:这个方法可使服务器传回该资源所支持的所有 HTTP 请求方法。用“*”来代替资源名称,向 Web 服务器发送 OPTIONS 请求,可以测试服务器功能是否正常运作。
  • HEAD:与 GET 方法一样,都是向服务器发出指定资源的请求。只不过服务器将不传回资源的本文部分。它的好处在于,使用这个方法可以在不必传输全部内容的情况下,就可以获取其中“关于该资源的信息”(元信息或称元数据)。
  • TRACE:回显服务器收到的请求,主要用于测试或诊断。
  • CONNECT:HTTP/1.1 协议中预留给能够将连接改为渠道方式的代理服务器。通常用于 SSL 加密服务器的链接(经由非加密的 HTTP 代理服务器)。

对象:使用 URL 表示,有时候会带上参数(?query-string)和锚(#anchor
版本:HTTP/1.1

第二行之后是请求头(request header),包含了很多连接的属性:

  • Host:目标主机。
  • Connection:连接属性,close 表示不适用持久连接,服务器响应后直接关闭连接。
  • User-agent:指定用户代理,这里就是产生当前请求的浏览器的类型,实际上服务器可以根据不同类型的用户代理发送同一个对象的不同版本。
  • Accept-language:指定用户希望接收的语言版本,如果没有的话,服务器应该发送其默认版本。
  • Content-length:可以表示请求体(request-body)内容长度,如果请求有 body 而 header 中没有 Content-length,则返回 400 错误。
    • multipart/form-data
      它将表单的数据组织成 Key-Value 形式,用分隔符 boundary(boundary 可任意设置)处理成一条消息。由于有 boundary 隔离,所以当即上传文件,又有参数的时候,必须要用这种 content-type 类型。如下图所示。
    • x-www-form-urlencoded
      即 application/x-www-from-urlencoded,将表单内的数据转换为 Key-Value。这种和 Get 方法把参数放在 URL 后面一样的想过,这种不能文件上传。
    • raw
      可以上传任意格式的“文本”,可以上传 Text、JSON、XML、HTML 等。
    • binary
      即 Content-Type:application/octet-stream,只可以上传二进制数据流,通常用来上传文件。由于没有键值,所以一次只能上传一个文件。

响应

HTTP响应格式如下所示:
HTTP响应数据包结构

  • 状态码
    状态代码有三位数字组成,第一个数字定义了响应的类别,共分五种类别:
    1xx:指示信息–表示请求已接收,继续处理。
    2xx:成功–表示请求已被成功接收、理解、接受。
    3xx:重定向–要完成请求必须进行更进一步的操作。
    4xx:客户端错误–请求有语法错误或请求无法实现。
    5xx:服务器端错误–服务器未能实现合法的请求。
    1
    2
    3
    4
    5
    6
    7
    200 OK                        //客户端请求成功
    400 Bad Request //客户端请求有语法错误,不能被服务器所理解
    401 Unauthorized //请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用
    403 Forbidden //服务器收到请求,但是拒绝提供服务
    404 Not Found //请求资源不存在,eg:输入了错误的URL
    500 Internal Server Error //服务器发生不可预期的错误
    503 Server Unavailable //服务器当前不能处理客户端的请求,一段时间后可能恢复正常
  • Content-length
    在响应体(response-body)中表示 body 的长度,根据这个值就可以知道 body 的长度,客户端在接收 body 时,就可以根据这个长度来接收数据,而如果没有 Content-length,则客户端会一直接收数据,直到服务端主动断开连接,才表示 body 接收完了;如果是 HTTP1.1,响应头中可以用 Transfer-encoding 字段指定为 chunked 传输,则表示 body 是流式输出,body 会被分成多个块,每块的开始处会标识出当前块的长度,body 不需要通过长度来指定。
  • Keep-Alive(持久连接)
    HTTP/1.0 使用非持久连接,HTTP/1.1 默认使用持久连接,每个连接建立后可以持续用于传送多个对象。
    适合设置 keepalive 连接的场景:客户端的一次请求需要访问同一 Server 多次,比如一个网页包含多个图片,需要访问图片服务器多次才能将这些图片下全。
    持久连接的条件:HTTP1.0响应不带Content-lengthHTTP1.1非 chunked 且不带Content-length,这两种情况下 body 长度是不可知的,当服务端在输出完 body 之后,可以考虑使用长连接。另外,还需要考虑客户端的请求头中的Connection字段:如果为 close,则表示客户端需要关掉长连接;如果为 Keep-Alive,那么网关(一般是 Nginx)在输出完响应体之后,会设置当前连接的 keepalive 属性,然后等待客户端的下一次请求;如果没有 Connection 这个头,则根据协议,如果是 HTTP1.0,则默认为 close,如果为 HTTP1.1,则默认为 Keep-Alive。
    但是持久连接也不可能会一直等下去,如果客户端一直不发送数据,岂不是一直占用这个连接?所以一般网关会设置一个最大等待时间,这个值是通过keepalive_timeout属性来配置的,如果值为 0,则表示关闭 keepalive,此时不管 HTTP 版本、Connection 如何设置,都强制为 close。
    如果最后决定打开 keepalive,那么在响应头里也会包含有 Connection 域,如果值为 Close,则网关在响应完数据后,会主动关掉连接。
    总而言之:如果有办法知道服务器传来的长度,都是客户端首先断开,否则一直接收数据,直到服务端断开。
    http1.0
    带 content-length,body 长度可知,客户端在接收 body 时,就可以依据这个长度来接受数据。接受完毕后,就表示这个请求完毕了。客户端主动调用 close 进入四次挥手。
    不带 content-length,body 长度不可知,客户端一直接受数据,直到服务端主动断开。
    http1.1
    带 content-length,body 长度可知,客户端主动断开。
    带 Transfer-encoding: chunked,body 会被分成多个块,每块的开始会标识出当前块的长度,body 就不需要通过 content-length 来指定了。但依然可以知道 body 的长度 客户端主动断开。
    不带 Transfer-encoding:chunked,且不带 content-length,客户端接收数据,直到服务端主动断开连接。

缓存

为了减少网络请求次数,一般可以引入缓存机制,HTTP 服务器通过两种实体头(Entity-Header)来实现缓存的过期:Expires 和 Cache-Control 的 max-age 子项。
Expires/Cache-Control 控制浏览器是否直接从浏览器缓存取数据还是重新发请求到服务器取数据。只是 Cache-Control 比 Expires 可以控制的多一些,而且 Cache-Control 会重写 Expires 的规则。
相关 Header 如下所示:

  • Cache-Control
    常用的值有:
    (1)max-age(单位为 s)指定设置缓存最大的有效时间,定义的是时间长短。当浏览器向服务器发送请求后,在 max-age 这段时间里浏览器就不会再向服务器发送请求了。 (2)s-maxage(单位为 s)同 max-age,只用于共享缓存(比如 CDN 缓存),也就是说 max-age 用于普通缓存,而 s-maxage 用于代理缓存。如果存在 s-maxage,则会覆盖掉 max-age 和 Expires header。 (3)public 指定响应会被缓存,并且在多用户间共享。如果没有指定 public 还是 private,则默认为 public。 (4)private 响应只作为私有的缓存,不能在用户间共享。如果要求 HTTP 认证,响应会自动设置为 private。 (5)no-cache 指定不缓存响应,表明资源不进行缓存,比如,设置了 no-cache 之后并不代表浏览器不缓存,而是在缓存前要向服务器确认资源是否被更改。因此有的时候只设置 no-cache 防止缓存还是不够保险,还可以加上 private 指令,将过期时间设为过去的时间。 (6)no-store 表示绝对禁止缓存。一看就知道,如果用了这个命令,当然就是不会进行缓存啦!每次请求资源都要从服务器重新获取。 (7)must-revalidate 指定如果页面是过期的,则去服务器进行获取。这个指令并不常用,就不做过多的讨论了。
  • Expires
    缓存过期时间,用来指定资源到期的时间,是服务器端的具体时间点。也就是说,Expires=max-age + 请求时间,需要和 Last-modified 结合使用。但在上面我们提到过 cache-control 的优先级更高。Expires 是 Web 服务器响应消息头字段,在响应 HTTP 请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求。
  • Last-modified
    服务器端文件的最后修改时间,需要和 cache-control 共同使用,是检查服务器端资源是否更新的一种方式。当浏览器再次进行请求时,会向服务器传送 If-Modified-Since 报头,询问 Last-Modified 时间点之后资源是否被修改过。如果没有修改,则返回码为 304,使用缓存;如果修改过,则再次去服务器请求资源,返回码和首次请求相同为 200,资源为服务器最新资源。
  • Etag
    根据实体内容生成一段 hash 字符串,标识资源的状态,由服务端产生。浏览器会将这串字符串传回服务器,验证资源是否已经修改。
    为什么要使用 Etag 呢?Etag 主要为了解决 Last-Modified 无法解决的一些问题。
    一些文件也许会周期性的更改,但是它的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新 Get。
    某些文件修改非常频繁,比如在秒以下的时间内进行修改(比方说 1s 内修改了 N 次),If-Modified-Since 能检查到的粒度是 s 级的,这种修改无法判断(或者说 UNIX 记录 MTIME 只能精确到秒)。
    某些服务器不能精确的得到文件的最后修改时间。
    缓存过程如下图所示。
    HTTP缓存

HTTP/1.1

新增方法

新增的 HTTP 方法有 PUT、PATCH、HEAD、OPTIONS、DELETE

主机名标识

在 HTTP/1.0 中 Host 头信息不是必须项,但 HTTP/1.1 中要求必须要有 Host 头信息。

持久性连接

正如前面所说,在 HTTP/1.0 中每个连接只有一个请求,且在这个请求完成后该连接就会被关闭,从而会导致严重的性能下降及延迟问题。HTTP/1.1 引入了对持久性连接的支持,例如:默认情况下连接不会被关闭,在多个连续的请求下它会保存连接的打开状态。想要关闭这些连接,需要将 Connection: close 加入到请求的头信息中。客户端通常会在最后一次请求中发送这个头信息用来安全的关闭连接。

管道机制

HTTP/1.1 也引入了对管道机制的支持,客户端可以向服务器发送多个请求,而无需等待来自同一连接上的服务器响应,并且当收到请求时服务器必须以相同的顺序来响应。但你可能会问客户端是怎么知道第一个响应下载完成和下一个响应内容开始的?要解决这个问题,必须要有 Content-Length 头信息,客户端可以用它来确定响应结束,然后开始等待下一个响应。

HTTP/2

HTTP/2 是专为低延迟传输的内容而设计,它与 HTTP/1.1 之间的差异如下所示:

二进制协议

HTTP/2 倾向于使用二进制协议来减少 HTTP/1.x 中的延迟。二进制协议更容易解析,但可读性相对较差。HTTP/2 中的数据块是帧和流。
帧和流:HTTP 消息是由一个或多个帧组成的。有一个叫做 HEADERS 的帧存放元数据,真正的数据是放在 DATA 帧中的,帧类型定义在 the HTTP/2 specs(HTTP/2 规范),如 HEADERS、DATA、RST_STREAM、SETTINGS、PRIORITY 等。每个 HTTP/2 请求和响应都被赋予一个唯一的流 ID 且放入了帧中。帧就是一块二进制数据。一系列帧的集合就称为流。每个帧都有一个流 id,用于标识它属于哪一个流,每一个帧都有相同的头。同时,除了流标识是唯一的,值得一提的是,客户端发起的任何请求都使用奇数和服务器的响应是偶数的流 id。除了 HEADERS 和 DATA, 另外一个值得说一说帧类型是 RST_STREAM,它是一个特殊的帧类型,用于中止流,如客户端发送这儿帧来告诉服务器我不再需要这个流了。在 HTTP/1.1 中只有一种方式来实现服务器停止发送响应给客户端,那就是关闭连接引起延迟增加,因为后续的请求就需要打开一个新的连接。 在 HTTP/2 中,客户端可以使用 RST_FRAME 来停止接收指定的流而不关闭连接且还可以在此连接中接收其它流。

多路复用

由于 HTTP/2 现在是一个二进制协议,且是使用帧和流来实现请求和响应,一旦 TCP 连接打开了,所有的流都通过这一连接来进行异步的发送而不需要打开额外的连接。反过来,服务器的响应也是异步的方式,如响应是无序的、客户端使用流 id 来标识属于流的包。这就解决了存在于 HTTP/1.x 中 head-of-line 阻塞问题,如客户端将不必耗时等待请求,而其他请求将被处理。如下图所示。
HTTP2.0多路复用

HPACK 头部压缩

它是一个单独的用于明确优化发送 Header RFC 的一部分。它的本质是,当我们同一个客户端不断的访问服务器时,在 header 中发送很多冗余的数据,有时 cookie 就增大 header,且消耗带宽和增加了延迟。为了解决这个问题, HTTP/2 引入了头部压缩。与请求和响应不同,header 不是使用 gzip 或 compress 等压缩格式,它有不同的机制,它使用了霍夫曼编码和在客户端和服务器维护的头部表来消除重复的 headers(如 User Agent),在后续的请求中就只使用头部表中引用。它与 HTTP/1.1 中的一样,不过增加了伪 header,如 :method、:scheme、:host 和:path。

服务器推送

在服务器端,Server Push 是 HTTTP/2 的另外一个重要功能,我们知道,客户端是通过请求来获取资源的,它可以通过推送资源给客户端而不需客户端主动请求。例如,浏览器载入了一个页面,浏览器解析页面时发现了需要从服务器端载入的内容,接着它就发送一个请求来获取这些内容。Server Push 允许服务器推送数据来减少客户端请求。它是如何实现的呢,服务器在一个新的流中发送一个特殊的帧 PUSH_PROMISE,来通知客户端:“嘿,我要把这个资源发给你!你就不要请求了。”

请求优先级

客户端可以在一个打开的流中在流的 HEADERS 帧中放入优先级信息。在任何时间,客户端都可以发送一个 PRIORITY 的帧来改变流的优先级。如果没有优先级信息,服务器就会异步的处理请求,比如无序处理。如果流被赋予了优先级,它就会基于这个优先级来处理,由服务器决定需要多少资源来处理该请求。
安全。大家对 HTTP/2 是否强制使用安全连接(通过 TLS)进行了充分的讨论。最后的决定是不强制使用。然而,大多数厂商表示,他们将只支持基于 TLS 的 HTTP/2。所以,尽管 HTTP/2 规范不需要加密,但它已经成为默认的强制执行的。在这种情况下,基于 TLS 实现的 HTTP/2 需要的 TLS 版本最低要求是 1.2。 因此必须有最低限度的密钥长度、临时密钥等。

一次网络请求的历程

  1. 暴露服务器IP
    外网 IP 是放在虚拟网关的外网网口上的,这个 IP 是通过 BGP 路由协议让全世界知道的,也就是说网络上能找到某个IP的服务器的所在位置了。
  2. 域名解析
    通过DNS解析域名为IP.

    当这个 DNS 本地有缓存,则直接返回;如果没有缓存,本地 DNS 才需要递归地从根 DNS 服务器,查到.com 的顶级域名服务器,最终查到权威 DNS 服务器。

  3. 建立连接
    HTTP是基于TCP的,要传输HTTP数据包自然是需要先建立TCP连接。
    HTTPS的连接需要在TCP连接建立完毕后再建立一层TLS连接,连接建立完毕后即可进行对称加密传输。
  4. 发送数据包
    上层的数据交给下层时会首先进行一次封装,附加上下层协议规定的参数,比如TCP就是源、目标端口号、IP等。
  5. 转发:从私网到公网
    每个局域网一般都是通过NAT网关与外界相连的,从NAT网关出去后IP会被更换为公网可识别的IP地址,或者可以说——数据包进入了互联网。
  6. 转发:从公网到私网
    在虚拟网关节点的外网网口上,会有一个 NAT 规则,将公网 IP 地址转换为 VPC 里面的私网 IP 地址,这个私网 IP 地址就是 SLB 的 HAProxy 所在的虚拟机的私网 IP 地址。

QA

网络为什么需要分层?

将网络请求这种复杂的任务拆解,每层做该层需要做的事,可以让每层的协议实现尽量精简。

UDP 与 TCP 之间的区别?

TCP 和 UDP 的区别

为什么TCP建立连接是3次握手而不是2次或4次?

  • 如果是2次握手
    比如A和B建立连接时,A可能会重试发送,但此时可能前面的请求由于网络等情况没有及时送到B,后来重试的请求先建立了连接,然后先发出的请求才送到,这个连接仍然建立了,但是却没有任何作用(因为A已经默认之前发出的请求都失败了)。
    TCP如果是2次握手可能存在的问题
    为了解决上面的问题,必须要有一个“应答之应答”,A需要告诉B这个连接的建立是有效的,因此需要3次握手。
    那么为什么不是4次握手或更多次的握手呢?主要是因为很多次也不能保证就真的可靠了,而且太多次对效率也会有比较大的影响。

为什么TCP关闭连接是4次挥手而不是2次或6次?