目录

计算机网络面试题总结

目录

基础篇

TCP/IP网络模型有哪几层?

为什么要有TCP/IP网络模型?

对于同一台设备的进程之间的通信,通信方式有很多,而对于不同设备上进程之间的通信,就需要Socket网络通信,而设备是多样性的,所以要兼容多种多样的设备,就协商出了一套通用的网络协议

应用层

当两个不同设备的应用需要通信的时候,应用就把应用数据传给下一层,也就是传输层。应用层只需要专注于为用户提供应用功能,比如 HTTP、FTP、Telnet、DNS、SMTP等。应用层是工作在操作系统中的用户态,传输层及以下则工作在内核态。

传输层

传输层为应用层提供网络支持。传输层会有两个协议:分别是TCP和UDP。

TCP 的全称叫传输控制协议Transmission Control Protocol),大部分应用使用的正是 TCP 传输层协议,比如 HTTP 应用层协议。TCP 相比 UDP 多了很多特性,比如流量控制、超时重传、拥塞控制等,这些都是为了保证数据包能可靠地传输给对方。

UDP 相对来说就很简单,简单到只负责发送数据包,不保证数据包是否能抵达对方,但它实时性相对更好,传输效率也高。当然,UDP 也可以实现可靠传输,把 TCP 的特性在应用层上实现就可以。

https://narcissusblog-img.oss-cn-beijing.aliyuncs.com/uPic/file-2023-02/image-20230208140000134.png

应用需要传输的数据可能会非常大,如果直接传输就不好控制,因此当传输层的数据包大小超过 MSS(TCP 最大报文段长度) ,就要将数据包分块,这样即使中途有一个分块丢失或损坏了,只需要重新发送这一个分块,而不用重新发送整个数据包。在 TCP 协议中,我们把每个分块称为一个 TCP 段TCP Segment)。

当设备作为接收方时,传输层则要负责把数据包传给应用,但是一台设备上可能会有很多应用在接收或者传输数据,因此需要用一个编号将应用区分开来,这个编号就是端口。对于浏览器中的每一个标签栏都是一个独立的进程,操作系统会为这些进程分配临时的端口号。

网络层

我们不希望传输层协议处理太多的事情,只需要服务好应用即可,让其作为应用间数据传输的媒介,帮助实现应用到应用的通信,而实际的传输功能就交给下一层,也就是网络层。网络层最常使用的是 IP 协议(Internet Protocol),IP 协议会将传输层的报文作为数据部分,再加上 IP 包头组装成 IP 报文,如果 IP 报文大小超过 MTU(以太网中一般为 1500 字节)就会再次进行分片,得到一个即将发送到网络的 IP 报文。

为了区分不同的设备,使用IP地址给设备编号。并将IP地址分成两种意义:

  • 一个是网络号,负责标识该 IP 地址是属于哪个「子网」的

  • 一个是主机号,负责标识同一「子网」下的不同主机

那么在寻址的过程中,先匹配到相同的网络号(表示要找到同一个子网),才会去找对应的主机。需要配合子网掩码才能算出IP地址的网络号和主机号。

例如,10.100.122.0/24,后面的/24表示就是 255.255.255.0 子网掩码,24含义是24个1。

将 10.100.122.2 和 255.255.255.0 进行按位与运算,就可以得到网络号。

将 255.255.255.0 取反后与IP地址进行进行按位与运算,就可以得到主机号。

除了寻址能力, IP 协议还有另一个重要的能力就是路由。 所以, IP 协议的寻址作用是告诉我们去往下一个目的地该朝哪个方向走,路由则是根据「下一个目的地」选择路径。寻址更像在导航,路由更像在操作方向盘

网络接口层

生成了 IP 头部之后,接下来要交给网络接口层Link Layer)在 IP 头部的前面加上 MAC 头部,并封装成数据帧(Data frame)发送到网络上。

以太网在判断网络包目的地时和 IP 的方式不同,因此必须采用相匹配的方式才能在以太网中将包发往目的地,而 MAC 头部就是干这个用的,所以,在以太网进行通讯要用到 MAC 地址。MAC 头部是以太网使用的头部,它包含了接收方和发送方的 MAC 地址等信息,我们可以通过 ARP 协议获取对方的 MAC 地址。

网络接口层主要为网络层提供「链路级别」传输的服务,负责在以太网、WiFi 这样的底层网络上发送原始数据包,工作在网卡这个层次,使用 MAC 地址来标识网络上的设备。

输入地址到网页显示,期间发生了什么?

1. HTTP-解析URL

浏览器做的第一步工作就是要对 URL 进行解析,从而生成发送给 Web 服务器的请求信息。

URL元素组成如下:

https://narcissusblog-img.oss-cn-beijing.aliyuncs.com/uPic/file-2023-02/image-20230208142836884.png

当没有数据源的路径名时,就代表访问根目录下事先设置的默认文件,也就是 /index.html 或者 /default.html 这些文件。

URL 进行解析之后,浏览器确定了 Web 服务器和文件名,接下来就是根据这些信息来生成 HTTP 请求消息了。

https://narcissusblog-img.oss-cn-beijing.aliyuncs.com/uPic/file-2023-02/image-20230208143242733.png

2. DNS-真实地址查询

通过浏览器解析 URL 并生成 HTTP 消息后,需要委托操作系统将消息发送给 Web 服务器。但在发送之前,要查询服务器域名对应的 IP 地址

域名的层级关系

DNS 中的域名都是用句点来分隔的,比如 www.server.com,这里的句点代表了不同层次之间的界限。在域名中,越靠右的位置表示其层级越高。实际上域名最后还有一个点,比如 www.server.com.,这个最后的一个点代表根域名。所以域名的层级关系类似一个树状结构:

  • 根 DNS 服务器(.)
  • 顶级域 DNS 服务器(.com)
  • 权威 DNS 服务器(server.com)

根域的 DNS 服务器信息保存在互联网中所有的 DNS 服务器中。 因此,客户端只要能够找到任意一台 DNS 服务器,就可以通过它找到根域 DNS 服务器,然后再一路顺藤摸瓜找到位于下层的某台目标 DNS 服务器。


域名的解析工作流程

  1. 客户端首先会发出一个 DNS 请求,问 www.server.com 的 IP 是啥,并发给本地 DNS 服务器。
  2. 本地域名服务器收到客户端的请求后,如果缓存里的表格能找到 www.server.com,则它直接返回 IP 地址。如果没有,本地 DNS 会去问它的根域名服务器。
  3. 根 DNS 收到来自本地 DNS 的请求后,会返回顶级域名服务器地址。
  4. 本地 DNS 收到顶级域名服务器的地址后,询问顶级域名服务器地址。
  5. 顶级域名服务器会返回权威域名服务器地址。
  6. 本地 DNS 于是转向问权威 DNS 服务器,权威域名服务器会返回域名对应的IP地址。

但是整个过程会有缓存的参与:浏览器会先看自身有没有对这个域名的缓存,如果有,就直接返回,如果没有,就去问操作系统,操作系统也会去看自己的缓存,如果有,就直接返回,如果没有,再去 hosts 文件看,也没有,才会去问「本地 DNS 服务器」。

3. 协议栈-操作系统

通过 DNS 获取到 IP 后,就可以把 HTTP 的传输工作交给操作系统中的协议栈

https://narcissusblog-img.oss-cn-beijing.aliyuncs.com/uPic/file-2023-02/image-20230208145350485.png

应用程序(浏览器)通过调用 Socket 库,来委托协议栈工作。协议栈的上半部分有两块,分别是负责收发数据的 TCP 和 UDP 协议,这两个传输协议会接受应用层的委托执行收发数据的操作。

协议栈的下面一半是用 IP 协议控制网络包收发操作,在互联网上传数据时,数据会被切分成一块块的网络包,而将网络包发送给对方的操作就是由 IP 负责的。

IP 下面的网卡驱动程序负责控制网卡硬件,而最下面的网卡则负责完成实际的收发操作,也就是对网线中的信号执行发送和接收操作。


HTTP篇

HTTP基本概念

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

HTTP常见的状态码有哪些?

https://narcissusblog-img.oss-cn-beijing.aliyuncs.com/uPic/file-2023-02/image-20230208151805354.png

常见状态码:

  • 200 OK」是最常见的成功状态码,表示一切正常。
  • 301 Moved Permanently」表示永久重定向,说明请求的资源已经不存在了,需改用新的 URL 再次访问。
  • 302 Found」表示临时重定向,说明请求的资源还在,但暂时需要用另一个 URL 来访问。

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

  • 403 Forbidden」表示服务器禁止访问资源,并不是客户端的请求出错。
  • 404 Not Found」表示请求的资源在服务器上不存在或未找到,所以无法提供给客户端。
  • 501 Not Implemented」表示客户端请求的功能还不支持,类似“即将开业,敬请期待”的意思。
  • 502 Bad Gateway」通常是服务器作为网关或代理时返回的错误码,表示服务器自身工作正常,访问后端服务器发生了错误。
  • 503 Service Unavailable」表示服务器当前很忙,暂时无法响应客户端,类似“网络服务正忙,请稍后重试”的意思。

Get/Post介绍

GET 请求的参数位置一般是写在 URL 中,URL 规定只能支持 ASCII,所以 GET 请求的参数只允许 ASCII 字符 ,而且浏览器会对 URL 的长度有限制。

POST 请求携带数据的位置一般是写在报文 body 中,body 中的数据可以是任意格式的数据,只要客户端与服务端协商好即可,而且浏览器不会对 body 大小做限制。

Http/1.1特性

HTTP的优点

Http最突出的优点就是:简单、灵活和易于扩展、应用广泛和跨平台。

  • 简单

HTTP基本的报文格式就是header+body,头部信息的格式化也是key-value的形式,易于理解。

  • 灵活和易于扩展

HTTP协议里的各个请求方法、状态码等的组合并没有固定死,允许开发人员自定义和扩充。并且由于HTTP是工作在应用层,下层协议栈可以随意变化。

  • 应用广泛和跨平台

台式机、手机都可以使用。

HTTP的缺陷

HTTP的缺陷也体现在了无状态、明文传输和不安全。

  • 无状态

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

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

无状态的解决方案有很多,其中最简单的方式就是Cookie。通过在请求和响应报文中写入Cookie信息来控制客户端状态。

  • 明文传输

明文传输意味着传输的所有信息都暴露在外,容易被窃取。

  • 不安全

HTTP的不安全体现在:通信使用明文传输,内容可能被窃取;不验证通信双方身份,可能遭遇伪装;无法证明报文的完整新,可能内容被篡改。HTTP的安全问题可以使用HTTPS的方式进行解决。

HTTP与HTTPS

HTTP和HTTPS的区别?

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

HTTPS解决了HTTP哪些问题?

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

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

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

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

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

为了保证传输的内容不被篡改,我们需要对内容计算出一个「指纹」,然后与内容一起传输给对方。接收方收到后也会根据内容计算出一个指纹,与接收到的进行比较。

指纹是指通过摘要算法(哈希函数)计算出来的哈希值。

但由于无法保证「内容+哈希值」不会被中间人一起替换掉,因此这里缺少了对客户端收到的信息是否来源于服务端的证明?

非对称加密过程中,流程不同意味着目的也不同:

  • 公钥加密,私钥解密。这个目的是为了保证内容传输的安全,因为被公钥加密的内容,其他人是无法解密的,只有持有私钥的人,才能解密出实际的内容;
  • 私钥加密,公钥解密。这个目的是为了保证消息不会被冒充,因为私钥是不可泄露的,如果公钥能正常解密出私钥加密的内容,就能证明这个消息是来源于持有私钥身份的人发送的。

非对称加密的用途主要在于通过「私钥加密,公钥解密」的方式,来确认消息的身份,我们常说的数字签名算法,就是用的是这种方式,不过私钥加密内容不是内容本身,而是对内容的哈希值加密

  1. 数字证书

数字证书可以保证服务器公钥的身份,防止公私钥被伪造的情况。服务器会将自己的公钥注册到CA(数字证书认证机构)机构中,CA机构会将公钥通过自己的私钥进行数字签名,并颁发数字证书给服务器,同时CA的公钥事先已经置入到了客户端的浏览器或者操作系统中,客户端拿到服务端传送的数字证书后,就可以使用CA的公钥确认数字证书的真实性并获取到服务器的公钥。

https://narcissusblog-img.oss-cn-beijing.aliyuncs.com/uPic/file-2023-02/image-20230213100043755.png

HTTPS建立连接过程

HTTPS建立连接的过程主要在TCP三次握手之后,会增加SSL/TLS 的建立过程,也就是 TLS 握手阶段。TLS 的「握手阶段」涉及四次通信,使用不同的密钥交换算法,TLS 握手流程也会不一样的,现在常用的密钥交换算法有两种:RSA 算法 和 ECDHE 算法。

RSA算法的缺陷:不支持前向保密

因为客户端传递随机数(用于生成对称加密密钥的条件之一)给服务端时使用的是公钥加密的,服务端收到后,会用私钥解密得到随机数。所以一旦服务端的私钥泄漏了,过去被第三方截获的所有 TLS 通讯密文都会被破解。

下面介绍基于RSA算法的TLS握手过程:

  1. ClientHello

首先,由客户端向服务器发起加密通信请求,也就是 ClientHello 请求。这一步,客户端主要向服务端发送以下请求:

  • 客户端支持的 TLS 协议版本,如 TLS 1.2 版本。
  • 客户端生产的随机数(Client Random,后面用于生成「会话秘钥」条件之一。
  • 客户端支持的密码套件列表,如 RSA 加密算法。
  1. ServerHello

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

  • 确认 TLS 协议版本,如果浏览器不支持,则关闭加密通信。
  • 服务器生产的随机数(Server Random,也是后面用于生产「会话秘钥」条件之一。
  • 确认的密码套件列表,如 RSA 加密算法。
  • 服务器的数字证书
  1. 客户端回应

客户端收到服务器的回应之后,首先通过浏览器或者操作系统中的 CA 公钥,确认服务器的数字证书的真实性。如果证书没有问题,客户端会从数字证书中取出服务器的公钥,然后使用它加密报文,向服务器发送如下信息:

  • 一个随机数(pre-master key。该随机数会被服务器公钥加密。
  • 加密通信算法改变通知,表示随后的信息都将用「会话秘钥」加密通信。
  • 客户端握手结束通知,表示客户端的握手阶段已经结束。这一项同时把之前所有内容的发生的数据做个摘要,用来供服务端校验。

服务器和客户端有了这三个随机数(Client Random、Server Random、pre-master key),接着就用双方协商的加密算法,各自生成本次通信的「会话秘钥」

  1. 服务器最后回应

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

  • 加密通信算法改变通知,表示随后的信息都将用「会话秘钥」加密通信。
  • 服务器握手结束通知,表示服务器的握手阶段已经结束。这一项同时把之前所有内容的发生的数据做个摘要,用来供客户端校验。

客户端校验服务端的数字证书的流程:

  • CA机构签发数字证书的流程

首先 CA 会把持有者的公钥、用途、颁发者、有效时间等信息打成一个包,然后对这些信息进行 Hash 计算,得到一个 Hash 值;然后 CA 会使用自己的私钥将该 Hash 值加密,生成 Certificate Signature(数字签名),也就是 CA 对证书做了签名;最后将 Certificate Signature 添加在文件证书上,形成数字证书。

  • 客户端校验服务端的数字证书的过程

首先客户端会使用同样的 Hash 算法获取该证书的 Hash 值 H1;通常浏览器和操作系统中集成了 CA 的公钥信息,浏览器收到证书后可以使用 CA 的公钥解密 Certificate Signature 内容,得到一个 Hash 值 H2 ;最后比较 H1 和 H2,如果值相同,则为可信赖的证书,否则则认为证书不可信。

https://narcissusblog-img.oss-cn-beijing.aliyuncs.com/uPic/file-2023-02/image-20230213103009169.png

HTTPS一定安全可靠吗?

问题的场景是这样的:客户端通过浏览器向服务端发起 HTTPS 请求时,被「假基站」转发到了一个「中间人服务器」,于是客户端是和「中间人服务器」完成了 TLS 握手,然后这个「中间人服务器」再与真正的服务端完成 TLS 握手。这样中间人就可以解开浏览器发起的 HTTPS 请求里的数据,也可以解开服务端响应给浏览器的 HTTPS 响应数据。

注意:上述场景发生的前提是用户点击接受了中间人服务器的证书。 中间人服务器与客户端在 TLS 握手过程中,实际上发送了自己伪造的证书给浏览器,而这个伪造的证书是能被浏览器(客户端)识别出是非法的,于是就会提醒用户该证书存在问题。

所以,HTTPS 协议本身到目前为止还是没有任何漏洞的,即使你成功进行中间人攻击,本质上是利用了客户端的漏洞(用户点击继续访问或者被恶意导入伪造的根证书),并不是 HTTPS 不够安全

HTTP/1.1如何优化?

HTTP/1.1协议的优化主要从如下三个方面进行考虑:

  • 通过缓存基础来避免发送HTTP请求

客户端收到第一个请求的响应后,可以将其缓存在本地磁盘,下次请求的时候,如果缓存没过期,就直接读取本地缓存的响应数据。如果缓存过期,客户端发送请求的时候带上响应数据的摘要,服务器比对后发现资源没有变化,就发出不带包体的 304 响应,告诉客户端缓存的响应仍然有效。

  • 减少HTTP的请求次数
  1. 将原本由客户端处理的重定向请求,交给代理服务器处理,这样可以减少重定向请求的次数;
  2. 将多个小资源合并成一个大资源再传输,能够减少 HTTP 请求次数以及头部的重复传输,再来减少 TCP 连接数量,进而省去 TCP 握手和慢启动的网络消耗;
  3. 按需访问资源,只访问当前用户看得到/用得到的资源,当客户往下滑动,再访问接下来的资源,以此达到延迟请求,也就减少了同一时间的 HTTP 请求次数。
  • 压缩响应资源

通过压缩响应资源,降低传输资源的大小,从而提高传输效率,所以应当选择更优秀的压缩算法。

HTTPS如何优化?

HTTPS的优化主要从以下几个方面:

  • 协议优化
  1. 密钥交换算法应该选择 ECDHE 算法,而不用 RSA 算法,因为 ECDHE 算法具备前向安全性,而且客户端可以在第三次握手之后,就发送加密应用数据,节省了 1 RTT(往返时延)。
  2. 将 TLS1.2 升级 TLS1.3,因为 TLS1.3 的握手过程只需要 1 RTT,而且安全性更强。
  • 证书优化
  1. 服务器应该选用 ECDSA 证书,而非 RSA 证书,因为在相同安全级别下,ECC 的密钥长度比 RSA 短很多,这样可以提高证书传输的效率;
  2. 服务器应该开启 OCSP Stapling 功能,由服务器预先获得 OCSP 的响应,并把响应结果缓存起来,这样 TLS 握手的时候就不用再访问 CA 服务器,减少了网络通信的开销,提高了证书验证的效率;
  • 会话复用

对于重连 HTTPS 时,我们可以使用一些技术让客户端和服务端使用上一次 HTTPS 连接使用的会话密钥,直接恢复会话,而不用再重新走完整的 TLS 握手过程。

常见的会话重用技术有 Session ID 和 Session Ticket,用了会话重用技术,当再次重连 HTTPS 时,只需要 1 RTT 就可以恢复会话。对于 TLS1.3 使用 Pre-shared Key 会话重用技术,只需要 0 RTT 就可以恢复会话。这些会话重用技术虽然好用,但是存在一定的安全风险,它们不仅不具备前向安全,而且有重放攻击的风险,所以应当对会话密钥设定一个合理的过期时间。

HTTP/2协议详解

HTTP/1.1最大的性能问题就是高延迟,主要原因有如下几个:

  • 延迟难以下降,虽然现在网络的「带宽」相比以前变多了,但是延迟降到一定幅度后,就很难再下降了,说白了就是到达了延迟的下限;
  • 并发连接有限,谷歌浏览器最大并发连接数是 6 个,而且每一个连接都要经过 TCP 和 TLS 握手耗时,以及 TCP 慢启动过程给流量带来的影响;
  • 队头阻塞问题,同一连接只能在完成一个 HTTP 事务(请求和响应)后,才能处理下一个事务;
  • HTTP 头部巨大且重复,由于 HTTP 协议是无状态的,每一个请求都得携带 HTTP 头部,特别是对于有携带 Cookie 的头部,而 Cookie 的大小通常很大;
  • 不支持服务器推送消息,因此当客户端需要获取通知时,只能通过定时器不断地拉取消息,这无疑浪费大量了带宽和服务器资源。

头部压缩

HTTP协议的报文由「Header+Body」组成,Body部分可以通过Content-Encoding指定压缩方式,而HTTP/1.1的报文头部存在如下问题。

  • 含有很多固定字段
  • 大量的请求和响应的报文里有很多字段值都是重复的
  • 字段是 ASCII 编码的,虽然易于人类观察,但效率低,所以有必要改成二进制编码

HTTP/2使用HPACK算法,主要包含三部分:静态表、动态表和Huffman编码(压缩算法)。客户端和服务器两端都会建立和维护「字典」,用长度较小的索引号表示重复的字符串,再用 Huffman 编码压缩数据,可达到 50%~90% 的高压缩率

  • 静态表编码

对于在HTTP头部高频出现的字符串和字段,建立一张静态表。通过静态表的索引号和字符串长度来表示一个头部数据。

  • 动态表编码

对于静态表中没有出现的头部字符串,采用动态表编码的方式。第一次发送请求时,客户端和服务器双方都会更新自己的动态表,那么在下一次发送的时候,就不用重复发这个字段的数据了,只用发 1 个字节的 Index 号就好了,因为双方都可以根据自己的动态表获取到字段的数据

注意:动态表生效有一个前提:必须同一个连接上,重复传输完全相同的 HTTP 头部

二进制帧

HTTP/2 将 HTTP/1 的文本格式改成二进制格式传输数据,极大提高了 HTTP 传输效率,而且二进制数据使用位运算能高效解析。

并发传输

HTTP/2 实现了 Stream 并发,多个 Stream 只需复用 1 个 TCP 连接,节约了 TCP 和 TLS 握手时间,以及减少了 TCP 慢启动阶段对流量的影响。不同的 Stream ID 可以并发,即使乱序发送帧也没问题,比如发送 A 请求帧 1 -> B 请求帧 1 -> A 请求帧 2 -> B 请求帧2,但是同一个 Stream 里的帧必须严格有序。

服务器主动推送资源

服务器支持主动推送资源,大大提升了消息的传输性能,服务器推送资源时,会先发送 PUSH_PROMISE 帧,告诉客户端接下来在哪个 Stream 发送资源,然后用偶数号 Stream 发送资源给客户端。

HTTP/3协议

虽然HTTP/2通过大量新特性提升了HTTP/1.1的性能,但美中不足就是HTTP/2协议是基于TCP实现的,有如下的缺陷:

  • 队头阻塞:HTTP/2 多个请求跑在一个 TCP 连接中,如果序列号较低的 TCP 段在网络传输中丢失了,即使序列号较高的 TCP 段已经被接收了,应用层也无法从内核中读取到这部分数据,从 HTTP 视角看,就是多个请求被阻塞了;

  • TCP 与 TLS 的握手时延迟:TCP 三次握手和 TLS 四次握手,共有 3-RTT 的时延

  • 网络迁移需要重新连接:移动设备从 4G 网络环境切换到 WiFi 时,由于 TCP 是基于四元组(源 IP 地址,源端口,目标 IP 地址,目标端口)来确认一条 TCP 连接的,那么网络环境变化后,就会导致 IP 地址或端口变化,于是 TCP 只能断开连接,然后再重新建立连接,切换网络环境的成本高

HTTP/3 就将传输层从 TCP 替换成了 UDP,并在 UDP 协议上开发了 QUIC 协议,来保证数据的可靠传输。

QUIC 协议的特点:

  • 无队头阻塞:QUIC 连接上的多个 Stream 之间并没有依赖,都是独立的,也不会有底层协议限制,某个流发生丢包了,只会影响该流,其他流不受影响;

  • 建立连接速度快:因为 QUIC 内部包含 TLS 1.3,因此仅需 1 个 RTT 就可以「同时」完成建立连接与 TLS 密钥协商,甚至在第二次连接的时候,应用数据包可以和 QUIC 握手信息(连接信息 + TLS 信息)一起发送,达到 0-RTT 的效果。

  • 连接迁移:QUIC 协议没有用四元组的方式来“绑定”连接,而是通过「连接 ID 」来标记通信的两个端点,客户端和服务器可以各自选择一组 ID 来标记自己,因此即使移动设备的网络变化后,导致 IP 地址变化了,只要仍保有上下文信息(比如连接 ID、TLS 密钥等),就可以“无缝”地复用原连接,消除重连的成本;

有了HTTP,为什么还需要RPC?

纯裸 TCP 是能收发数据(通过Socket通信),但它是个无边界的数据流,上层需要定义消息格式用于定义消息边界。于是就有了各种协议,HTTP 和各类 RPC 协议就是在 TCP 之上定义的应用层协议。

RPCRemote Procedure Call),又叫做远程过程调用。它本身并不是一个具体的协议,而是一种调用方式。目的是能够像调用本地方法一样去调用远程服务的方法,屏蔽掉网络的细节。基于此,有了非常多款式的 RPC 协议,比如比较有名的gRPCthrift

HTTP和RPC协议的区别

  • 服务发现

首先要向某个服务器发起请求,你得先建立连接,而建立连接的前提是,你得知道 IP 地址和端口。这个找到服务对应的 IP 端口的过程,其实就是服务发现

HTTP 中,你知道服务的域名,就可以通过 DNS 服务去解析得到它背后的 IP 地址

RPC 的话,就有些区别,一般会有专门的中间服务去保存服务名和IP信息,想要访问某个服务,就去这些中间服务去获得 IP 和端口信息。

  • 底层连接形式

以主流的 HTTP/1.1 协议为例,其默认在建立底层 TCP 连接之后会一直保持这个连接(Keep Alive),之后的请求和响应都会复用这条连接。

RPC 协议一般会建立连接池 ,在请求量大的时候,建立多条连接放在池内,要发数据的时候就从池里取一条连接出来,用完放回去,下次再复用,节约资源。由于连接池有利于提升网络请求性能,所以不少编程语言的网络库里都会给 HTTP 加个连接池,比如 Go

  • 传输内容

对于主流的 HTTP/1.1,虽然它现在叫超文本协议,支持音频视频,但 HTTP 设计初是用于做网页文本展示的,所以它传的内容以字符串为主。

而RPC,因为它定制化程度更高,可以采用体积更小的 Protobuf 或其他序列化协议去保存结构体数据,同时也不需要像 HTTP 那样考虑各种浏览器行为,比如 302 重定向跳转啥的。因此性能也会更好一些,这也是在公司内部微服务中抛弃 HTTP,选择使用 RPC 的最主要原因。

注意:上述都是基于HTTP/1.1进行的对比,HTTP/2 在前者的基础上做了很多改进,所以性能可能比很多 RPC 协议还要好,甚至连 gRPC 底层都直接用的 HTTP/2

TCP篇

TCP基本认识

TCP头格式

https://narcissusblog-img.oss-cn-beijing.aliyuncs.com/uPic/file-2023-02/image-20230213162404495.png

  • 序列号:在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。用来解决网络包乱序问题。序列号是一个 32 位的无符号数,因此在到达 4G 之后再循环回到 0

  • 确认应答号:指下一次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。用来解决丢包的问题。

  • 控制位

    ACK:该位为 1 时,「确认应答」的字段变为有效,TCP 规定除了最初建立连接时的 SYN 包之外该位必须设置为 1

    RST:该位为 1 时,表示 TCP 连接中出现异常必须强制断开连接。

    SYN:该位为 1 时,表示希望建立连接,并在其「序列号」的字段进行序列号初始值的设定。

    FIN:该位为 1 时,表示今后不会再有数据发送,希望断开连接。

为什么需要TCP,工作在哪一层?

IP 层是「不可靠」的,它不保证网络包的交付、不保证网络包的按序交付、也不保证网络包中的数据的完整性。如果需要保障网络数据包的可靠性,那么就需要由上层(传输层)的 TCP 协议来负责。

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

什么是TCP?

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

  • 面向连接的:一定是「一对一」才能连接,不能像 UDP 协议可以一个主机同时向多个主机发送消息,也就是一对多是无法做到的;
  • 可靠的:无论的网络链路中出现了怎样的链路变化,TCP 都可以保证一个报文一定能够到达接收端
  • 字节流:用户消息通过 TCP 协议传输时,消息可能会被操作系统「分组」成多个的 TCP 报文,如果接收方的程序如果不知道「消息的边界」,是无法读出一个有效的用户消息的。

如何确定一个TCP连接?

TCP四元组(源地址、源端口、目标地址、目标端口)可以唯一确定一个连接。

TCP/UDP区别?应用场景?

UDP不提供复杂的控制机制,利用 IP 提供面向「无连接」的通信服务。头部只有8个字节,格式如下:

https://narcissusblog-img.oss-cn-beijing.aliyuncs.com/uPic/file-2023-02/image-20230213164534244.png

  • 目标端口和源端口:主要是告诉 UDP 协议应该把报文发给哪个进程。
  • 包长度:该字段保存了 UDP 首部的长度跟数据的长度之和。
  • 校验和:校验和是为了提供可靠的 UDP 首部和数据而设计,防止收到在网络传输中受损的 UDP 包。
  • TCP和UDP的区别
  1. 连接
  • TCP 是面向连接的传输层协议,传输数据前先要建立连接。

  • UDP 是不需要连接,即刻传输数据。

  1. 服务对象
  • TCP 是一对一的两点服务,即一条连接只有两个端点。
  • UDP 支持一对一、一对多、多对多的交互通信
  1. 可靠性
  • TCP 是可靠交付数据的,数据可以无差错、不丢失、不重复、按序到达。
  • UDP 是尽最大努力交付,不保证可靠交付数据。但是我们可以基于 UDP 传输协议实现一个可靠的传输协议,比如 QUIC 协议。
  1. 拥塞控制、流量控制
  • TCP 有拥塞控制和流量控制机制,保证数据传输的安全性。
  • UDP 则没有,即使网络非常拥堵了,也不会影响 UDP 的发送速率。
  1. 首部开销
  • TCP 首部长度较长,会有一定的开销,首部在没有使用「选项」字段时是 20 个字节,如果使用了「选项」字段则会变长的。
  • UDP 首部只有 8 个字节,并且是固定不变的,开销较小。
  1. 传输方式
  • TCP 是流式传输,没有边界,但保证顺序和可靠。
  • UDP 是一个包一个包的发送,是有边界的,但可能会丢包和乱序。
  1. 分片不同
  • TCP 的数据大小如果大于 MSS 大小,则会在传输层进行分片,目标主机收到后,也同样在传输层组装 TCP 数据包,如果中途丢失了一个分片,只需要传输丢失的这个分片。
  • UDP 的数据大小如果大于 MTU 大小,则会在 IP 层进行分片,目标主机收到后,在 IP 层组装完数据,接着再传给传输层。
  • TCP/UDP的应用场景

由于 TCP 是面向连接,能保证数据的可靠性交付,因此经常用于:FTP 文件传输;HTTP / HTTPS。

由于 UDP 面向无连接,它可以随时发送数据,再加上 UDP 本身的处理既简单又高效,因此经常用于:包总量较少的通信,如 DNSSNMP 等;视频、音频等多媒体通信;广播通信。


TCP连接建立

TCP三次握手的过程

TCP 是面向连接的协议,所以使用 TCP 前必须先建立连接,而建立连接是通过三次握手来进行的。过程如下图:

https://narcissusblog-img.oss-cn-beijing.aliyuncs.com/uPic/file-2023-02/image-20230213170727505.png

  • 一开始,客户端和服务端都处于 CLOSE 状态。先是服务端主动监听某个端口,处于 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 状态。

为什么是三次握手?不是两次、四次?

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

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

  • 「两次握手」:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号;
  • 「四次握手」:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。
  1. 防止历史连接的建立

三次握手的首要原因是为了防止旧的重复连接初始化造成混乱。

我们考虑一个场景,客户端先发送了 SYN(seq = 90)报文,然后客户端宕机了,而且这个 SYN 报文还被网络阻塞了,服务端并没有收到,接着客户端重启后,又重新向服务端建立连接,发送了 SYN(seq = 100)报文。

下面看看三次握手是如何阻止历史连接的:

  • 一个「旧 SYN 报文」比「最新的 SYN」 报文早到达了服务端,那么此时服务端就会回一个 SYN + ACK 报文给客户端,此报文中的确认号是 91(90+1)。
  • 客户端收到后,发现自己期望收到的确认号应该是 100 + 1,而不是 90 + 1,于是就会回 RST 报文。
  • 服务端收到 RST 报文后,就会释放连接。
  • 后续最新的 SYN 抵达了服务端后,客户端与服务端就可以正常的完成三次握手了。

注意:如果服务端在收到RST报文之前,新的SYN报文抵达了服务端,此时会发生什么?

服务端第一次收到SYN报文时,会回复「SYN+ACK」报文给客户端,确认号是90+1,当再次收到新的SYN报文时,会回复Challenge ACK报文给客户端,该报文的确认号仍然是90+1,客户端发现不满足预期,会回复RST报文。

如果是两次握手,就无法阻止历史连接的建立,主要是因为在两次握手的情况下,服务端没有中间状态给客户端来阻止历史连接,导致服务端可能建立一个历史连接,造成资源浪费


  1. 同步双方初始序列号

TCP通信双方都维护了一个「序列号」,它的作用是:

  • 接收方可以去除重复数据
  • 接收方可以根据序列号按序接收
  • 可以标识发送出去的数据包中, 哪些是已经被对方收到的(通过 ACK 报文中的序列号知道)

当客户端发送携带「初始序列号」的 SYN 报文的时候,需要服务端回一个 ACK 应答报文,表示客户端的 SYN 报文已被服务端成功接收,那当服务端发送「初始序列号」给客户端的时候,依然也要得到客户端的应答回应,这样一来一回,才能确保双方的初始序列号能被可靠的同步。但由于第二步和第三步可以优化成一步,所以就成了「三次握手」。


  1. 避免资源浪费

如果客户端发送的 SYN 报文在网络中阻塞了,重复发送多次 SYN 报文,那么服务端在收到请求后就会建立多个冗余的无效链接,造成不必要的资源浪费。


为什么每次建立TCP连接时,初始化序列号要求都不一样?

主要有两个方面的原因:

  • 为了防止历史报文被下一个相同四元组的连接接收(主要方面)
  • 为了安全性,防止黑客伪造的相同序列号的 TCP 报文被对方接收

如果能正常四次挥手,由于 TIME_WAIT 状态会持续 2 MSL 时长,历史报文会在下一个连接之前就会自然消失。但如果不是正确关闭连接的情况,。

我们假设每次初始化序列号都是0,客户端与服务端建立连接后,发生了网络阻塞,此时服务端断电重启,再收到客户端的数据就会恢复RST报文,如果此时客户端与服务端建立了相同四元组的连接,上一个连接中被网络阻塞的数据包正好抵达了服务端,刚好该数据包的序列号正好是在服务端的接收窗口内,所以该数据包会被服务端正常接收,就会造成数据错乱。

所以,每次初始化序列号不一样能够很大程度上避免历史报文被下一个相同四元组的连接接收。

初始化序列号 ISN 随机生成算法:ISN = M + F(localhost, localport, remotehost, remoteport)。随机数是会基于时钟计时器递增的,基本不可能会随机成一样的初始化序列号。

  • M是一个计时器,这个计时器每隔 4 微秒加1。
  • F 是一个 Hash 算法,根据源IP、目的IP、源端口、目的端口生成一个随机数值。

由于序列号最大是4G,之后会循环回到0。可能会发生序列号回绕的问题。如何解决?

为了解决这个问题,就需要有 TCP 时间戳。tcp_timestamps 参数是默认开启的,开启了 tcp_timestamps 参数,TCP 头部就会使用时间戳选项,它有两个好处,一个是便于精确计算 RTT ,另一个是能防止序列号回绕(PAWS)

防回绕序列号算法要求连接双方维护最近一次收到的数据包的时间戳(Recent TSval),每收到一个新数据包都会读取数据包中的时间戳值跟 Recent TSval 值做比较,如果发现收到的数据包中时间戳不是递增的,则表示该数据包是过期的,就会直接丢弃这个数据包


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

  • MTU:一个网络包的最大长度,以太网中一般为 1500 字节
  • MSS:除去 IP 和 TCP 头部之后,一个网络包所能容纳的 TCP 数据的最大长度

在TCP层进行分片,目的是为了防止IP层对数据进行分片,如果一个 TCP 分片丢失后,进行重发时也是以 MSS 为单位,而不用重传所有的分片,大大增加了重传的效率。

当 IP 层有一个超过 MTU 大小的数据(TCP 头部 + TCP 数据)要发送,那么 IP 层就要进行分片,把数据分片成若干片,保证每一个分片都小于 MTU。把一份 IP 数据报进行分片以后,由目标主机的 IP 层来进行重新组装后,再交给上一层 TCP 传输层。但这存在隐患的,那么当如果一个 IP 分片丢失,整个 IP 报文的所有分片都得重传


第一次握手丢失,会发生什么?

当客户端想和服务端建立 TCP 连接的时候,首先第一个发的就是 SYN 报文,然后进入到 SYN_SENT 状态。

在这之后,如果客户端迟迟收不到服务端的 SYN-ACK 报文(第二次握手),就会触发「超时重传」机制,重传 SYN 报文,而且重传的 SYN 报文的序列号都是一样的。 当达到超时重传最大次数后,会再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到服务端的第二次握手(SYN-ACK 报文),那么客户端就会断开连接。在 Linux 里,客户端的 SYN 报文最大重传次数由 tcp_syn_retries内核参数控制,默认是5。


第二次握手丢失,会发生什么?

当服务端收到客户端的第一次握手后,就会回 SYN-ACK 报文给客户端,这个就是第二次握手,此时服务端会进入 SYN_RCVD 状态。

第二次握手的报文有两个目的:第二次握手里的ACK,是对第一次握手的确认报文;第二次握手里的SYN,是服务端发起建立TCP连接的报文。

  • 因为第二次握手报文里是包含对客户端的第一次握手的 ACK 确认报文,所以,如果客户端迟迟没有收到第二次握手,那么客户端就觉得可能自己的 SYN 报文(第一次握手)丢失了,于是客户端就会触发超时重传机制,重传 SYN 报文。达到最大重传次数后,客户端会断开连接。
  • 因为第二次握手中包含服务端的 SYN 报文,所以当客户端收到后,需要给服务端发送 ACK 确认报文(第三次握手),服务端才会认为该 SYN 报文被客户端收到了。如果第二次握手丢失了,服务端就收不到第三次握手,于是服务端这边会触发超时重传机制,重传 SYN-ACK 报文。达到最大重传次数后,服务端也会断开连接。

第三次握手丢失,会发生什么?

客户端收到服务端的 SYN-ACK 报文后,就会给服务端回一个 ACK 报文,也就是第三次握手,此时客户端状态进入到 ESTABLISH 状态。

因为这个第三次握手的 ACK 是对第二次握手的 SYN 的确认报文,所以当第三次握手丢失了,如果服务端那一方迟迟收不到这个确认报文,就会触发超时重传机制,重传 SYN-ACK 报文,直到收到第三次握手,或者达到最大重传次数。


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

攻击者短时间伪造不同 IP 地址的 SYN 报文,服务端每接收到一个 SYN 报文,就进入SYN_RCVD 状态,但服务端发送出去的 ACK + SYN 报文,无法得到未知 IP 主机的 ACK 应答,久而久之就会占满服务端的半连接队列,当 TCP 半连接队列满了,后续再在收到 SYN 报文就会丢弃,使得服务端不能为正常用户服务。这就是SYN洪泛攻击。

在TCP三次握手的时候,Linux内核会维护两个队列:

  • 半连接队列,也称SYN队列
  • 全连接队列,也称accept队列

正常流程:

  • 当服务端接收到客户端的 SYN 报文时,会创建一个半连接的对象,然后将其加入到内核的「 SYN 队列」;
  • 接着发送 SYN + ACK 给客户端,等待客户端回应 ACK 报文;
  • 服务端接收到 ACK 报文后,从「 SYN 队列」取出一个半连接对象,然后创建一个新的连接对象放入到「 Accept 队列」;
  • 应用通过调用 accpet() socket 接口,从「 Accept 队列」取出连接对象。

https://narcissusblog-img.oss-cn-beijing.aliyuncs.com/uPic/file-2023-02/image-20230214094434178.png


避免SYN攻击有如下四种方式:

  • 调大 netdev_max_backlog;
  • 增大 TCP 半连接队列;
  • 开启 tcp_syncookies;
  • 减少 SYN+ACK 重传次数
  1. 调大netdev_max_backlog

当网卡接收数据包的速度大于内核处理的速度时,会有一个队列保存这些数据包。我们可以适当调大该值。

  1. 增大TCP半连接队列

增大 TCP 半连接队列,要同时增大三个参数:增大 net.ipv4.tcp_max_syn_backlog;增大 listen() 函数中的 backlog;增大 net.core.somaxconn。

  1. 开启 tcp_syncookies

开启 syncookies 功能就可以在不使用 SYN 半连接队列的情况下成功建立连接,相当于绕过了 SYN 半连接来建立连接。

具体过程:

  • 当 「 SYN 队列」满之后,后续服务端收到 SYN 包,不会丢弃,而是根据算法,计算出一个 cookie 值;
  • 将 cookie 值放到第二次握手报文的「序列号」里,然后服务端回第二次握手给客户端
  • 服务端接收到客户端的应答报文时,服务端会检查这个 ACK 包的合法性。如果合法,将该连接对象放入到「 Accept 队列」
  1. 减少 SYN+ACK 重传次数

当服务端受到 SYN 攻击时,就会有大量处于 SYN_REVC 状态的 TCP 连接,处于这个状态的 TCP 会重传 SYN+ACK ,当重传超过次数达到上限后,就会断开连接。减少 SYN-ACK 的重传次数,以加快处于 SYN_REVC 状态的 TCP 连接断开。



TCP断开连接

TCP四次挥手的过程

TCP四次挥手过程如下:

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

注意:主动关闭连接的一端才会有TIME_WAIT状态。

https://narcissusblog-img.oss-cn-beijing.aliyuncs.com/uPic/file-2023-02/image-20230214100134725.png


为什么挥手需要四次?

关闭连接时,客户端向服务端发送 FIN 时,仅仅表示客户端不再发送数据了但是还能接收数据。服务端收到客户端的 FIN 报文时,先回一个 ACK 应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送 FIN 报文给客户端来表示同意现在关闭连接。所以服务端通常需要等待数据发送完毕,服务端的ACKFIN一般会分开发送,因此需要四次握手。

注意:在特定的情况下,四次挥手也可以变成三次挥手。


第一次挥手丢失,会发生什么?

当客户端(主动关闭方)调用 close 函数后,就会向服务端发送 FIN 报文,试图与服务端断开连接,此时客户端的连接进入到 FIN_WAIT_1 状态。如果能正常接收到服务端的ACK就会很快进入到FIN_WAIT_2状态。

如果第一次挥手丢失了,那么客户端迟迟收不到被动方的 ACK 的话,也就会触发超时重传机制,重传 FIN 报文,重发次数由 tcp_orphan_retries 参数控制。当客户端重传 FIN 报文的次数超过 tcp_orphan_retries 后,就不再发送 FIN 报文,则会在等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到第二次挥手,那么直接进入到 close 状态。


第二次挥手丢失,会发生什么?

当服务端收到客户端的第一次挥手后,就会先回一个 ACK 确认报文,此时服务端的连接进入到 CLOSE_WAIT 状态。

ACK 报文是不会重传的,所以如果服务端的第二次挥手丢失了,客户端就会触发超时重传机制,重传 FIN 报文,直到收到服务端的第二次挥手,或者达到最大的重传次数。

客户端收到第二次挥手后,会处于FIN_WAIT_2的状态,等待第三次挥手的报文。

  • 对于 close 函数关闭的连接,由于无法再发送和接收数据,所以FIN_WAIT2 状态不可以持续太久,而 tcp_fin_timeout 控制了这个状态下连接的持续时长,默认值是 60 秒。60秒后会直接关闭。
  • 如果主动关闭方使用 shutdown 函数关闭连接,指定了只关闭发送方向,而接收方向并没有关闭,那么意味着主动关闭方还是可以接收数据的。如果主动关闭方一直没收到第三次挥手,那么主动关闭方的连接将会一直处于 FIN_WAIT2 状态(tcp_fin_timeout 无法控制 shutdown 关闭的连接)。

第三次挥手丢失,会发生什么?

服务端处于 CLOSE_WAIT 状态时,调用了 close 函数,内核就会发出 FIN 报文,同时连接进入 LAST_ACK 状态,等待客户端返回 ACK 来确认连接关闭。如果迟迟收不到这个 ACK,服务端就会重发 FIN 报文,重发次数仍然由 tcp_orphan_retries 参数控制,当达到了重传最大次数后,会再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到客户端的第四次挥手(ACK报文),那么服务端就会断开连接。

客户端因为是通过 close 函数关闭连接的,处于 FIN_WAIT_2 状态是有时长限制的,如果 tcp_fin_timeout 时间内还是没能收到服务端的第三次挥手(FIN 报文),那么客户端就会断开连接。


第四次挥手丢失,会发生什么?

如果第四次挥手的 ACK 报文没有到达服务端,服务端就会重发 FIN 报文,当达到了最大重传次数后,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到客户端的第四次挥手(ACK 报文),那么服务端就会断开连接。

客户端在收到第三次挥手后,就会进入 TIME_WAIT 状态,开启时长为 2MSL 的定时器,如果途中再次收到第三次挥手(FIN 报文)后,就会重置定时器,当等待 2MSL 时长后,客户端就会断开连接。


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

MSL 是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。2MSL 的时间是从客户端接收到 FIN 后发送 ACK 开始计时的。如果在 TIME-WAIT 时间内,因为客户端的 ACK 没有传输到服务端,客户端又接收到了服务端重发的 FIN 报文,那么 2MSL 时间将重新计时。其实就相当于至少允许第四次挥手的ACK报文丢失一次。

IP 头中有一个 TTL 字段,是 IP 数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。MSL 应该要大于等于 TTL 消耗为 0 的时间,以确保报文已被自然消亡。TTL 的值一般是 64,Linux 将 MSL 设置为 30 秒,意味着 Linux 认为数据报文经过 64 个路由器的时间不会超过 30 秒,如果超过了,就认为报文已经消失在网络中了

为什么不是 4 或者 8 MSL 的时长呢?你可以想象一个丢包率达到百分之一的糟糕网络,连续两次丢包的概率只有万分之一,这个概率实在是太小了,忽略它比解决它更具性价比。


为什么需要TIME_WAIT状态?

主动发起关闭连接请求的一方,才会有TIME_WAIT状态,需要该状态主要有以下两个原因:

  • 防止历史连接中的数据,被后面相同四元组的连接错误的接收;
  • 保证「被动关闭连接」的一方,能被正确的关闭

原因一:防止历史连接中的数据,被后面相同四元组的连接错误的接收

为了防止历史连接中的数据,被后面相同四元组的连接错误的接收,因此 TCP 设计了 TIME_WAIT 状态,状态会持续 2MSL 时长,这个时间足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的。

原因二:保证「被动关闭连接」的一方,能被正确的关闭

等待足够的时间以确保最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭。

如果客户端(主动关闭方)最后一次 ACK 报文(第四次挥手)在网络中丢失了,那么按照 TCP 可靠性原则,服务端(被动关闭方)会重发 FIN 报文。如果不设置TIME_WAIT状态而是直接进入CLOSE,此时收到服务端重传的 FIN 报文后,就会回 RST 报文。导致服务端异常终止。


TIME_WAIT过多有什么危害?

  • 如果是客户端(主动发起关闭的一方)的TIME_WAIT状态过多,会占用端口资源。

    不过,即使是在这种场景下,只要连接的是不同的服务端,端口是可以重复使用的,所以客户端还是可以向其他服务端发起连接的,这是因为内核在定位一个连接的时候,是通过四元组(源IP、源端口、目的IP、目的端口)信息来定位的,并不会因为客户端的端口一样,而导致连接冲突。

  • 如果是客户端(主动发起关闭的一方)的TIME_WAIT状态过多,并不会导致端口资源受限,因为服务端只监听一个端口,但是 TCP 连接过多,会占用系统资源,比如文件描述符、内存资源、CPU 资源、线程资源等。


如何优化TIME_WAIT?

可以从如下四个方面优化TIME_WAIT,都是有利有弊的:

  • 打开 net.ipv4.tcp_tw_reuse 和 net.ipv4.tcp_timestamps 选项

Linux开启net.ipv4.tcp_tw_reuse选项后,则可以复用处于 TIME_WAIT 的 socket 为新的连接所用。但需要打开时间戳的支持。这个时间戳的字段是在 TCP 头部的「选项」里,它由一共 8 个字节表示时间戳,其中第一个 4 字节字段用来保存发送该数据包的时间,第二个 4 字节字段用来保存最近一次接收对方发送到达数据的时间。由于引入了时间戳,我们在前面提到的 2MSL 问题就不复存在了,因为重复的数据包会因为时间戳过期被自然丢弃。

  • net.ipv4.tcp_max_tw_buckets

这个值默认为 18000,当系统中处于 TIME_WAIT 的连接一旦超过这个值时,系统就会将后面的 TIME_WAIT 连接状态重置,这个方法比较暴力。

  • 程序中使用 SO_LINGER ,应用强制使用 RST 关闭。

开启后,那么调用close后,会立该发送一个RST标志给对端,该 TCP 连接将跳过四次挥手,也就跳过了TIME_WAIT状态,直接关闭。


服务端出现大量TIME_WAIT状态的原因?

服务端出现大量TIME_WAIT状态,是由于服务端主动断开连接,什么场景下服务端才会主动断开连接呢?

  • 第一个场景:HTTP 没有使用长连接
  • 第二个场景:HTTP 长连接超时
  • 第三个场景:HTTP 长连接的请求数量达到上限

第一个场景:HTTP没有使用长连接

HTTP/1.0默认使用短连接,HTTP/1.1默认使用长连接,但只要客户端和服务端任意一方的 HTTP header 中有 Connection:close 信息,那么就无法使用 HTTP 长连接的机制

当服务端出现大量的 TIME_WAIT 状态连接的时候,可以排查下是否客户端和服务端都开启了 HTTP Keep-Alive,因为任意一方没有开启 HTTP Keep-Alive,都会导致服务端在处理完一个 HTTP 请求后,就主动关闭连接。


第二个场景:HTTP长连接超时

HTTP如果使用长连接,为了避免服务端的资源浪费,往往会设置长连接的超时时间,超时后,服务端将会主动断开连接,此过程会出现TIME_WAIT。

当服务端出现大量 TIME_WAIT 状态的连接时,有可能是因为 HTTP 长连接超时,导致服务端主动关闭连接,产生大量处于 TIME_WAIT 状态的连接。


第三个场景:HTTP 长连接的请求数量达到上限

Web 服务端通常会有个参数,来定义一条 HTTP 长连接上最大能处理的请求数量,当超过最大限制时,就会主动关闭连接。


服务端出现大量CLOSE_WAIT状态的原因?

CLOSE_WAIT 状态是「被动关闭方」才会有的状态,而且如果「被动关闭方」没有调用 close 函数关闭连接,那么就无法发出 FIN 报文,从而无法使得 CLOSE_WAIT 状态的连接转变为 LAST_ACK 状态。所以,当服务端出现大量 CLOSE_WAIT 状态的连接的时候,说明服务端的程序没有调用 close 函数关闭连接。这通常是代码方面的问题。


建立了连接,客户端突然出现故障了怎么办?

为了避免由于客户端故障,导致服务端一直处于ESTABLISH状态,占用系统资源,TCP有一个保活机制。原理是:

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

  • 如果客户端正常工作,探测报文会被正常响应,TCP保活时间则会被重置
  • 如果客户端宕机重启,探测报文会被回复为RST报文。
  • 如果客户端宕机或者由于某些原因导致报文不可达,那么探测次数上限后,会被判定TCP连接死亡。

有如下场景,如果客户端宕机重启后,向服务端发送SYN报文重新建立连接,此时服务端会怎么处理?

由于TCP连接通过四元组唯一确认,此场景这要考虑源端口是否相同的情况。

  • 客户端的SYN报文里源端口和历史连接不相同

如果SYN报文里的源端口和历史连接的源端口不相同,则服务端会认为这是新的连接,通过三次握手建立新连接。服务端的旧连接处于ESTABLISH状态,会如何处理呢?

如果服务端发送了数据包给客户端,由于客户端的连接已经被关闭了,此时客户的内核就会回 RST 报文,服务端收到后就会释放连接。

如果服务端一直没有发送数据包给客户端,在超过一段时间后,TCP 保活机制就会启动,检测到客户端没有存活后,接着服务端就会释放掉该连接。

  • 客户端的SYN报文里的源端口和历史连接相同

处于 Established 状态的服务端,如果收到了客户端的 SYN 报文(注意此时的 SYN 报文其实是乱序的,因为 SYN 报文的初始化序列号其实是一个随机数),会回复一个携带了正确序列号和确认号的 ACK 报文,这个 ACK 被称之为 Challenge ACK。

接着,客户端收到这个 Challenge ACK,发现确认号(ack num)并不是自己期望收到的,于是就会回 RST 报文,服务端收到后,就会释放掉该连接。


建立了连接,服务端突然出现故障了怎么办?

TCP 的连接信息是由内核维护的,所以当服务端的进程崩溃后,内核需要回收该进程的所有 TCP 连接资源,于是内核会发送第一次挥手 FIN 报文,后续的挥手过程也都是在内核完成,并不需要进程的参与,所以即使服务端的进程退出了,还是能与客户端完成 TCP 四次挥手的过程。



重传机制

在复杂的网络中,针对TCP丢包的问题,会用重传机制解决。常见的重传机制有:超时重传、快速重传、SACK和D-SACK

超时重传

超时重传是指在发送数据时,设定一个定时器,当超过指定的时间后,没有收到对方的 ACK 确认应答报文,就会重发该数据。TCP会在以下两种情况下发生超时重传:数据包丢失、确认应答丢失。

超时时间如何设置?

RTT(Round-Trip Time 往返时延)指的是数据发送时刻到接收到确认的时刻的差值,也就是包的往返时间。

超时重传时间是以 RTO (Retransmission Timeout 超时重传时间)表示。超时重传时间 RTO 的值应该略大于报文往返 RTT 的值。 并且RTO的值应该随RTT根据网络情况动态变化。

如果超时重发的数据,再次超时的时候,又需要重传的时候,TCP 的策略是超时间隔加倍。

也就是每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送。


快速重传

超时重传的超时周期可能相对较长,「快速重传」机制可以解决超时重发的时间等待。快速重传机制不以时间为驱动,而是以数据驱动重传。快速重传的工作方式是当收到三个相同的 ACK 报文时,会在定时器过期之前,重传丢失的报文段。

如下场景,发送发发送1、2、3、4、5份数据,Seq2在网络中丢失了,1、3、4都能到达,那么发送方将会收到三个ACK=2的确认,发送方则会在定时器过期之前,重传丢失的Seq2数据。

但是快速重传仍然面临一个问题就是:**重传的时候,是重传一个,还是重传所有的问题。**于是就有了下面的SACK方法。


SACK方法和D-SACK方法

SACK(Selective Acknowledgment)选择性确认。这种方式需要在 TCP 头部「选项」字段里加一个 SACK 的东西,它可以将已收到的数据的信息发送给「发送方」,这样发送方就可以知道哪些数据收到了,哪些数据没收到,知道了这些信息,就可以只重传丢失的数据

Duplicate SACK 又称 D-SACK,其主要使用了 SACK 来告诉「发送方」有哪些数据被重复接收了。

如下案例:

https://narcissusblog-img.oss-cn-beijing.aliyuncs.com/uPic/file-2023-02/image-20230214154946844.png

「接收方」发现数据是重复收到的,于是回了一个 SACK = 3000~3500,告诉「发送方」 3000~3500 的数据早已被接收了,因为 ACK 都到了 4000 了,已经意味着 4000 之前的所有数据都已收到。


滑动窗口

为什么需要滑动窗口?

TCP为每一个数据包确认应答,这样的传输方式的缺陷就是:数据包的往返时间越长,通信的效率就越低

为了解决这个问题,TCP引入了窗口的概念,那么有了窗口,就可以指定窗口大小,窗口大小就是指无需等待确认应答,而可以继续发送数据的最大值

窗口的实现实际上是操作系统开辟的一个缓存空间,发送方主机在等到确认应答返回之前,必须在缓冲区中保留已发送的数据。如果按期收到确认应答,此时数据就可以从缓存区清除。

窗口中的数据并不需要每一个数据都需要收到应答,当收到某一个应答为ACK=X,则表明序列号X之前的数据均收到了,这种方式就叫累计确认或者累计应答


窗口大小由哪一方确认?

TCP 头里有一个字段叫 Window,也就是窗口大小。**这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。**所以,通常窗口的大小是由接收方的窗口大小来决定的。

由于网络延迟,接收窗口的大小是约等于发送窗口的大小的。


流量控制

发送方不能无脑的发送数据给接收方,如果接收方处理不过来,就会触发重发机制,无端的浪费流量。为了解决这种现象发生,TCP 提供一种机制可以让「发送方」根据「接收方」的实际接收能力控制发送的数据量,这就是所谓的流量控制。 接收方通过指明希望从发送方接收的数据大小(窗口大小)来进行流量控制。


  • 窗口关闭

如果窗口大小为 0 时,就会阻止发送方给接收方传递数据,直到窗口变为非 0 为止,这就是窗口关闭。

窗口关闭的风险

当发生窗口关闭时,接收方处理完数据后,会向发送方通告一个窗口非 0 的 ACK 报文,如果这个通告窗口的 ACK 报文在网络中丢失了,就会造成死锁现象

如何解决?

TCP 为每个连接设有一个持续定时器,只要 TCP 连接一方收到对方的零窗口通知,就启动持续计时器。 如果持续计时器超时,就会发送窗口探测 ( Window probe ) 报文,而对方在确认这个探测报文时,给出自己现在的接收窗口大小。

  • 糊涂窗口综合症

如果接收方太忙了,来不及取走接收窗口里的数据,那么就会导致发送方的发送窗口越来越小。到最后,如果接收方腾出几个字节并告诉发送方现在有几个字节的窗口,而发送方会义无反顾地发送这几个字节,这就是糊涂窗口综合症

如何解决

  1. 让接收方不通告小窗口

当「窗口大小」小于 min( MSS,缓存空间/2 ) ,也就是小于 MSS 与 1/2 缓存大小中的最小值时,就会向发送方通告窗口为 0,也就阻止了发送方再发数据过来。

  1. 让发送方避免发送小数据

使用 Nagle 算法,该算法的思路是延时处理,只有满足下面两个条件中的任意一个条件,才可以发送数据:

  • 条件一:要等到窗口大小 >= MSS 并且 数据大小 >= MSS
  • 条件二:收到之前发送数据的 ack 回包;


拥塞控制

  • 为什么需要拥塞控制,不是有流量控制吗?

流量控制是避免「发送方」的数据填满「接收方」的缓存,但是并不知道网络的中发生了什么。

拥塞控制的目的就是避免「发送方」的数据填满整个网络。 为了在发送方调节所要发送的数据的量,就有了拥塞窗口。

  • 什么是拥塞窗口?和发送窗口有什么关系?

拥塞窗口 cwnd是发送方维护的一个的状态变量,它会根据网络的拥塞程度动态变化的

前面提到过发送窗口 swnd 和接收窗口 rwnd 是约等于的关系,那么由于加入了拥塞窗口的概念后,此时发送窗口的值是swnd = min(cwnd, rwnd),也就是拥塞窗口和接收窗口中的最小值。

  • 怎么判定网络是否发生了拥塞?

其实只要「发送方」没有在规定时间内接收到 ACK 应答报文,也就是发生了超时重传,就会认为网络出现了拥塞。

  • 拥塞控制有哪些算法?

拥塞控制主要是四个算法:慢启动、拥塞避免、拥塞发生和快速恢复


慢启动

TCP 在刚建立连接完成后,首先是有个慢启动的过程,这个慢启动的意思就是一点一点的提高发送数据包的数量。慢启动的算法记住一个规则就行:当发送方每收到一个 ACK,拥塞窗口 cwnd 的大小就会加 1。 因此每一轮拥塞窗口都会增加一倍,呈指数性增长

有一个慢启动门限 ssthresh (slow start threshold)状态变量。

  • cwnd < ssthresh 时,使用慢启动算法。
  • cwnd >= ssthresh 时,就会使用「拥塞避免算法」。

拥塞避免算法

进入拥塞避免算法后,它的规则是:每当收到一个 ACK 时,cwnd 增加 1/cwnd。,相当于每一轮拥塞窗口增加1,呈现线性增长。

在这个无限增长过程中,会出现网络拥塞,当触发了重传机制,也就进入了「拥塞发生算法」。

拥塞发生

当网络出现拥塞,也就是会发生数据包重传,重传机制主要有两种:超时重传和快速重传

  • 发生超时重传的拥塞发生算法

当发生了「超时重传」,则就会使用拥塞发生算法。这个时候,ssthresh 和 cwnd 的值会发生变化:ssthresh 设为 cwnd/2cwnd 重置为 1 (是恢复为 cwnd 初始化值,我这里假定 cwnd 初始化值 1)。 接着就重新开始慢启动。

  • 发生快速重传的拥塞发生算法

当接收方发现丢了一个中间包的时候,发送三次前一个包的 ACK,于是发送端就会快速地重传,不必等待超时再重传。TCP 认为这种情况不严重,因为大部分没丢,只丢了一小部分,则 ssthreshcwnd 变化如下:cwnd=cwnd/2,也就是设置为原来的一半;ssthresh=cwnd;进入快速恢复算法

快速恢复

快速重传和快速恢复算法一般同时使用,快速恢复算法是认为,你还能收到 3 个重复 ACK 说明网络也不那么糟糕,所以没有必要像 RTO 超时那么强烈。

进入快速恢复之前,cwndssthresh 已被更新了:

  • cwnd = cwnd/2 ,也就是设置为原来的一半;
  • ssthresh = cwnd;

然后,进入快速恢复算法如下:

  • 拥塞窗口 cwnd = ssthresh + 3 ( 3 的意思是确认有 3 个数据包被收到了);
  • 重传丢失的数据包;
  • 如果再收到重复的 ACK,那么 cwnd 增加 1;
  • 如果收到新数据的 ACK 后,把 cwnd 设置为第一步中的 ssthresh 的值,原因是该 ACK 确认了新的数据,说明从 duplicated ACK 时的数据都已收到,该恢复过程已经结束,可以回到恢复之前的状态了,也即再次进入拥塞避免状态;

变化过程如下:

https://narcissusblog-img.oss-cn-beijing.aliyuncs.com/uPic/file-2023-02/image-20230214185446012.png



TCP粘包

如何理解TCP是面向字节流?

TCP是面向字节流的协议,UDP是面向报文的协议。

为什么说UDP是面向报文的协议?

当用户通过UDP协议传输消息时,操作系统不会对消息进行拆分,在组装好 UDP 头部后就交给网络层来处理,所以发出去的 UDP 报文中的数据部分就是完整的用户消息,也就是每个 UDP 报文就是一个用户消息的边界,这样接收方在接收到 UDP 报文后,读一个 UDP 报文就能读取到完整的用户消息。操作系统并通过队列来维护一个一个的UDP消息。

为什么说TCP是面向字节流的协议?

当用户消息通过 TCP 协议传输时,消息可能会被操作系统分组成多个的 TCP 报文,也就是一个完整的用户消息被拆分成多个 TCP 报文进行传输。接收方的程序如果不知道发送方发送的消息的长度,也就是不知道消息的边界时,是无法读出一个有效的用户消息。正因为如此,所以我们说TCP是面向字节流的协议。

当两个消息的某个部分内容被分到同一个 TCP 报文时,就是我们常说的 TCP 粘包问题,这时接收方不知道消息的边界的话,是无法读出有效的消息。


如何解决粘包?

一般有三种分包方式:固定长度的消息、特殊字符作为边界、自定义消息结构。

  • 固定长度的消息

这种是最简单方法,即每个用户消息都是固定长度的,但是这种方式灵活性不高,实际中很少用。比如规定一个消息的长度是 64 个字节,当接收方接满 64 个字节,就认为这个内容是一个完整且有效的消息。

  • 特殊字符作为边界

我们可以在两个用户消息之间插入一个特殊的字符串,这样接收方在接收数据时,读到了这个特殊字符,就把认为已经读完一个完整的消息。

HTTP通过设置回车符、换行符作为 HTTP 报文协议的边界。

需要注意的是,如果消息内容中刚好有特殊字符,则需要对字符进行转义。

  • 自定义消息结构

我们可以自定义一个消息结构,由包头和数据组成,其中包头包是固定大小的,而且包头里有一个字段来说明紧随其后的数据有多大


SYN报文什么情况下会被丢弃?

SYN报文被丢弃主要有以下两种情况:

  • 开启 tcp_tw_recycle 参数,并且在 NAT 环境下,造成 SYN 报文被丢弃
  • TCP 两个队列满了(半连接队列和全连接队列),造成 SYN 报文被丢弃

开启 tcp_tw_recycle 参数

Linux提供了两个参数来快速回收处于TIME_WAIT状态的连接:

  • net.ipv4.tcp_tw_reuse,如果开启该选项的话,客户端(连接发起方) 在调用 connect() 函数时,**如果内核选择到的端口,已经被相同四元组的连接占用的时候,就会判断该连接是否处于 TIME_WAIT 状态,如果该连接处于 TIME_WAIT 状态并且 TIME_WAIT 状态持续的时间超过了 1 秒,那么就会重用这个连接,然后就可以正常使用该端口了。**所以该选项只适用于连接发起方。
  • net.ipv4.tcp_tw_recycle,如果开启该选项的话,允许处于 TIME_WAIT 状态的连接被快速回收。

要使得这两个选项生效,有一个前提条件,就是要打开 TCP 时间戳

对于服务器来说,如果同时开启了recycle 和 timestamps 选项,则会开启一种称之为「 per-host 的 PAWS 机制」。

PAWS机制要求连接双方维护最近一次收到的数据包的时间戳(Recent TSval ),每收到一个新数据包都会读取数据包中的时间戳值跟 Recent TSval 值做比较,如果发现收到的数据包中时间戳不是递增的,则表示该数据包是过期的,就会直接丢弃这个数据包

per-host 是对「对端 IP 做 PAWS 检查」,而非对「IP + 端口」四元组做 PAWS 检查。

如果客户端网络环境是用了 NAT 网关,那么客户端环境的每一台机器通过 NAT 网关后,都会是相同的 IP 地址,在服务端看来,就好像只是在跟一个客户端打交道一样,无法区分出来。

如下场景:

当客户端 A 通过 NAT 网关和服务器建立 TCP 连接,然后服务器主动关闭并且快速回收 TIME-WAIT 状态的连接后,客户端 B 也通过 NAT 网关和服务器建立 TCP 连接,注意客户端 A 和 客户端 B 因为经过相同的 NAT 网关,所以是用相同的 IP 地址与服务端建立 TCP 连接,如果客户端 B 的 timestamp 比 客户端 A 的 timestamp 小,那么由于服务端的 per-host 的 PAWS 机制的作用,服务端就会丢弃客户端主机 B 发来的 SYN 包。

连接队列满了

当TCP半连接队列满了以后,如果没有开启syncookies功能,这是后续来的syn包就会被丢弃。

在服务端并发处理大量请求时,如果 TCP accpet 队列(全连接队列)过小,或者应用程序调用 accept() 不及时,就会造成 accpet 队列满了 ,这时后续的连接就会被丢弃,这样就会出现服务端请求数量上不去的现象。


TCP连接中,断电和进程崩溃会发生什么?

问题场景,TCP连接中,没有开启TCP keepAlive的情况下,没有数据交互,一端断电或者发生进程崩溃会发生什么?

注意:TCP keepAlive是TCP的保活机制,而HTTP keepAlive则是HTTP长连接,两者没有任何关系。

客户端进程崩溃

TCP 的连接信息是由内核维护的,所以当服务端的进程崩溃后,内核需要回收该进程的所有 TCP 连接资源,于是内核会发送第一次挥手 FIN 报文,后续的挥手过程也都是在内核完成,并不需要进程的参与,所以即使服务端的进程退出了,还是能与客户端完成 TCP四次挥手的过程。

客户端主机崩溃

  • 无数据交互

客户端主机崩溃了,服务端是无法感知到的,在加上服务端没有开启 TCP keepalive,又没有数据交互的情况下,服务端的 TCP 连接将会一直处于 ESTABLISHED 连接状态,直到服务端重启进程。

所以,我们可以得知一个点,在没有使用 TCP 保活机制且双方不传输数据的情况下,一方的 TCP 连接处在 ESTABLISHED 状态,并不代表另一方的连接还一定正常。

  • 有数据交互

如果客户端一直没有重启,服务端的数据报文会超时重传,当重传总间隔时长达到一定阈值(内核会根据 tcp_retries2 设置的值计算出一个阈值)后,会断开 TCP 连接。

如果客户端及时重启,服务端触发数据超时重传过程中,客户端及时重启,但当客户端收到TCP数据报文后,会回复RST报文,断开连接。


TCP四次挥手可以变成三次吗?

为什么需要TCP四次挥手?

服务器收到客户端的 FIN 报文时,内核会马上回一个 ACK 应答报文,但是服务端应用程序可能还有数据要发送,所以并不能马上发送 FIN 报文,而是将发送 FIN 报文的控制权交给服务端应用程序

  • 如果服务端应用程序有数据要发送的话,就发完数据后,才调用关闭连接的函数;
  • 如果服务端应用程序没有数据要发送的话,可以直接调用关闭连接的函数,

注意:FIN报文并不一定得调用关闭函数才能发送,如果进程退出了,不管是不是正常退出,还是异常退出(如进程崩溃),内核都会发送 FIN 报文,与对方完成四次挥手。

什么情况下会出现三次挥手?

当被动关闭方(上图的服务端)在 TCP 挥手过程中,「没有数据要发送」并且「开启了 TCP 延迟确认机制」,那么第二和第三次挥手就会合并传输,这样就出现了三次挥手。 并且TCP延迟确认机制是默认开启的。

TCP延迟确认机制

当发送没有携带数据的 ACK,它的网络效率也是很低的,因为它也有 40 个字节的 IP 头 和 TCP 头,但却没有携带数据报文。 为了解决 ACK 传输效率低问题,所以就衍生出了 TCP 延迟确认。 TCP 延迟确认的策略:

  • 当有响应数据要发送时,ACK 会随着响应数据一起立刻发送给对方
  • 当没有响应数据要发送时,ACK 将会延迟一段时间,以等待是否有响应数据可以一起发送
  • 如果在延迟等待发送 ACK 期间,对方的第二个数据报文又到达了,这时就会立刻发送 ACK

TCP协议有什么缺陷?

TCP协议的缺陷主要有如下四个方面:

  • 升级TCP的工作很困难
  • TCP建立连接的延迟
  • TCP存在队头阻塞问题
  • 网络迁移需要重新建立TCP连接

升级TCP的工作很困难

由于TCP协议是在内核中实现的,应用程序只能使用不能修改,如果要想升级 TCP 协议,那么只能升级内核。但是升级内核这项工作非常麻烦,由于内核升级涉及到底层软件和运行库的更新,那么就需要考虑到升级内核后,应用服务的兼容问题。

TCP建立连接的延迟

基于TCP实现的应用层协议,都需要通过三次握手来建立连接,并且常用的HTTPS,还需要经过TLS四次握手。这在一定程度上增加了数据传输的延迟。

虽然现在TCP有Fast Open这个特性,但是该特性还没有全面普及。

第一次建立连接依然要消耗2RTT的延迟,但第二次握手会产生一个 Cookie (已加密)并通过 SYN、ACK 包一起发给客户端,于是客户端就会缓存这个 Cookie,下一次建立连接时,客户端在 SYN 包带上 Cookie 发给服务端,就可以跳过三次握手的过程,消耗时延减少到了1RTT。

TCP存在队头阻塞问题

TCP 是字节流协议,TCP 层必须保证收到的字节数据是完整且有序的,如果序列号较低的 TCP 段在网络传输中丢失了,即使序列号较高的 TCP 段已经被接收了,应用层也无法从内核中读取到这部分数据,这就是队头阻塞问题。

网络迁移需要重新建立TCP连接

基于TCP传输协议的HTTP协议,由于是通过四元组确定一条TCP连接,那么当移动设备的网络从 4G 切换到 WIFI 时,意味着 IP 地址变化了,那么就必须要断开连接,然后重新建立 TCP 连接。而建立连接的过程包含 TCP 三次握手和 TLS 四次握手的时延,以及 TCP 慢启动的减速过程,给用户的感觉就是网络突然卡顿了一下,因此连接的迁移成本是很高的。


如何基于UDP协议实现可靠传输?

如果将TCP的可靠传输应用到应用层,那么属于重复造轮子。我们应该了解TCP协议的缺陷,基于UDP实现的可靠传输是否可以解决这些缺陷?现在的成熟方案就是QUIC协议

QUIC协议如何实现可靠传输的?

HTTP/3的头如下所示:

https://narcissusblog-img.oss-cn-beijing.aliyuncs.com/uPic/file-2023-02/image-20230217091023030.png

  • Packet header

Packet header分为了:Long Packet header用于首次建立连接;short packet header用于日常传输数据。 QUIC首次建立连接时需要连接双方协商出连接ID,因此也需要三次握手,后续建立连接则不需要。

日常传输数据的short packet header中包含有Packet Number 是每个报文独一无二的编号,它是严格递增的,也就是说就算 Packet N 丢失了,重传的 Packet N 的 Packet Number 已经不是 N,而是一个比 N 大的值。

为什么要这样设计?

TCP中对于重传的数据,需要使用相同的序列号。这样可能造成计算RTT时产生歧义,因为如法确认是哪一次重传的ACK确认报文,而RTO(超时时间)又是通过RTT计算得来的, 那么如果 RTT 计算不精准,那么 RTO (超时时间)也会不精确,这样可能导致重传的概率事件增大。

所以Packet Number采用严格递增的设计有两点好处:可以更加精确计算 RTT,没有 TCP 重传的歧义性问题;可以支持乱序确认,因为丢包重传将当前窗口阻塞在原地,而 TCP 必须是顺序确认的,丢包时会导致窗口不滑动。

  • QUIC Frame header

一个Packet报文中可以有多个Frame header,每个Frame可以理解为HTTP/2里面的Stream,相当于一条连接。每个Stream中包含有:StreamID和offset等字段信息。

通过引入 Frame Header 这一层,通过 Stream ID + Offset 字段信息实现数据的有序性,通过比较两个数据包的 Stream ID 与 Stream Offset ,如果都是一致,就说明这两个数据包的内容一致。

总的来说,QUIC 通过单向递增的 Packet Number,配合 Stream ID 与 Offset 字段信息,可以支持乱序确认而不影响数据包的正确组装,摆脱了TCP 必须按顺序确认应答 ACK 的限制,解决了 TCP 因某个数据包重传而阻塞后续所有待发送数据包的问题。

QUIC是如何迁移连接的?

QUIC 协议没有用四元组的方式来“绑定”连接,而是通过连接 ID来标记通信的两个端点,客户端和服务器可以各自选择一组 ID 来标记自己,因此即使移动设备的网络变化后,导致 IP 地址变化了,只要仍保有上下文信息(比如连接 ID、TLS 密钥等),就可以“无缝”地复用原连接,消除重连的成本,没有丝毫卡顿感,达到了连接迁移的功能。

QUIC更快地连接建立

对于 HTTP/1 和 HTTP/2 协议,TCP 和 TLS 是分层的,分别属于内核实现的传输层、openssl 库实现的表示层,因此它们难以合并在一起,需要分批次来握手。

但是 HTTP/3 的 QUIC 协议并不是与 TLS 分层,而是QUIC 内部包含了 TLS,它在自己的帧会携带 TLS 里的“记录”,再加上 QUIC 使用的是 TLS1.3,因此仅需 1 个 RTT 就可以「同时」完成建立连接与密钥协商,甚至在第二次连接的时候,应用数据包可以和 QUIC 握手信息(连接信息 + TLS 信息)一起发送,达到 0-RTT 的效果



IP篇

IP基础介绍

IP属于网络层,主要作用是:实现主机与主机之间的通信,也叫点对点(end to end)通信。

MAC (数据链路层)的作用则是实现「直连」的两个设备之间通信,而 IP 则负责在「没有直连」的两个网络之间进行通信传输。

ARP协议

在传输一个 IP 数据报的时候,确定了源 IP 地址和目标 IP 地址后,就会通过主机「路由表」确定 IP 数据包下一跳。然而,网络层的下一层是数据链路层,所以我们还要知道「下一跳」的 MAC 地址。由于主机的路由表中可以找到下一跳的 IP 地址,所以可以通过 ARP 协议,求得下一跳的 MAC 地址。

ARP 是借助 ARP 请求与 ARP 响应两种类型的包确定 MAC 地址的。

  • 主机会通过广播发送 ARP 请求,这个包中包含了想要知道的 MAC 地址的主机 IP 地址。
  • 当同个链路中的所有设备收到 ARP 请求时,会去拆开 ARP 请求包里的内容,如果 ARP 请求包中的目标 IP 地址与自己的 IP 地址一致,那么这个设备就将自己的 MAC 地址塞入 ARP 响应包返回给主机。

NAT协议

由于IPV4地址非常紧缺,于是,提出了一种网络地址转换 NAT 的方法,缓解了 IPv4 地址耗尽的问题。

简单的来说 NAT 就是同个公司、家庭、教室内的主机对外部通信时,把私有 IP 地址转换成公有 IP 地址。

平时我们办公室、家里、学校用的 IP 地址,一般都是私有 IP 地址。因为这些地址允许组织内部的 IT 人员自己管理、自己分配,而且可以重复。

公有 IP 地址是有个组织统一分配的,假设你要开一个博客网站,那么你就需要去申请购买一个公有 IP,这样全世界的人才能访问。并且公有 IP 地址基本上要在整个互联网范围内保持唯一。

由于绝大多数的网络应用都是使用传输层协议 TCP 或 UDP 来传输数据的。因此,可以把 IP 地址 + 端口号一起进行转换。这样,就用一个全球 IP 地址就可以了,这种转换技术就叫网络地址与端口转换 NAPT。 并通过NAPT 路由器的转换表来管理之间的映射,这种转换表在 NAT 路由器上自动生成。例如,在 TCP 的情况下,建立 TCP 连接首次握手时的 SYN 包一经发出,就会生成这个表。而后又随着收到关闭连接时发出 FIN 包的确认应答从表中被删除。

由于 NAT/NAPT 都依赖于自己的转换表,因此会有以下的问题:

  • 外部无法主动与 NAT 内部服务器建立连接,因为 NAPT 转换表没有转换记录。
  • 转换表的生成与转换操作都会产生性能开销。
  • 通信过程中,如果 NAT 路由器重启了,所有的 TCP 连接都将被重置。

解决方法:

  • 改用IPV6,IPv6 可用范围非常大,以至于每台设备都可以配置一个公有 IP 地址。
  • **NAT穿透技术:**NAT 穿越技术拥有这样的功能,就是客户端主动从 NAT 设备获取公有 IP 地址,然后自己建立端口映射条目,然后用这个条目对外通信,就不需要 NAT 设备来进行转换了。