HTTP(HyperText Transfer Protocol):超文本传输协议,基于客户端/服务端的架构模型,用于客户端和服务端之间的通信。请求访问资源的一方为客户端,负责接收,提供响应的一方为服务端。
基于TCP/IP协议的应用层协议,不涉及数据包传输,规定了客户端和服务器之间的通信方式,默认端口为80
通过发送信息(请求)和回应信息(响应)达成交易(通信)
一个请求消息是由请求行、请求头、空行和消息主体构成。
GET /index.htm HTTP/1.1 //请求方法 URL 版本号
User-Agent: Mozilla/4.0 (compatible; MSIE5.01; Windows NT) //标志客户端的字符串
Host: example.com
Accept-Language: en-us //可接受的语言
Accept-Encoding: gzip, deflate //支持的编码类型
HTTP/1.1 200 OK //版本号 状态码 状态描述
...
Date: Tue, 10 Jul …
Content.Length: 362
Content.Type: text/html
响应头:
头部信息回应的是ASCII码,后面的数据可以是任意形式,由Content-Type指定
Content-Type的字段值
text/plain
text/html
text/css
image/jpeg
image/png
image/svg+xml
audio/mp4
video/mp4
application/javascript
application/pdf
application/zip
application/atom+xml
//表示发送的是网页,编码为utf-8
Content-Type: text/html; charset=utf-8
Accept字段声明自己可以接受哪些数据格式
Accept: /
URI:Uniform Resource Identifier,统一资源标识符
URL:Uniform Resource Locator,统一资源定位符
URN:Uniform Resource Name,统一资源名称
1.GET为获取资源数据
2.POST为提交资源数据
3.PUT为更新资源数据
4.DELETE为删除资源数据
5.HEAD为读取资源的元数据
6.OPTIONS为读取资源多支持的所有请求方法
7.TRACE为回显服务器收到额请求
8.CONNECT为保留将来使用
GET和POST的区别
100:继续,客户端应继续其请求 //例如post请求
200:请求成功
201:成功请求并创建了新的资源
202:已经接受请求,但未处理完成
203:非授权信息
204:服务器处理成功,但未返回内容,在未更新网页的情况下,可确保浏览器继续显示当前文档
301:永久重定向
302:临时重定向
304:所请求的资源未修改,服务器返回此状态码时,不会返回任何资源,客户端通常会缓存访问过的资源,通过提供一个头信息指出客户端希望只返回在指定日期后修改的资源
400:客户端请求的语法错误
401:请求要求用户的身份认证
403:服务器理解请求客户端的请求,但是拒绝执行此请求
404:请求的资源不存在
500:内部服务器错误
501:服务器不支持请求的功能,无法完成请求
502:从远程服务器收到了一个无效的请求
503:服务器暂时无法处理客户端的请求
HTTP 1.0 仅仅提供了最基本的认证,这时候用户名和密码还未经加密,因此很容易收到窥探。
HTTP 1.0 被设计用来使用短链接,即每次发送数据都会经过 TCP 的三次握手和四次挥手,效率比较低。
HTTP 1.0 只使用 header 中的 If-Modified-Since 和 Expires 作为缓存失效的标准。
HTTP 1.0 不支持断点续传,也就是说,每次都会传送全部的页面和数据。
HTTP 1.0 认为每台计算机只能绑定一个 IP,所以请求消息中的 URL 并没有传递主机名(hostname)。
HTTP/1.0的主要缺点:无状态、无连接
HTTP1.1出现于1999年
HTTP 1.1 使用了摘要算法来进行身份验证
HTTP 1.1 默认使用长连接,长连接就是只需一次建立就可以传输多次数据,传输完成后,只需要一次切断连接即可。长连接的连接时长可以通过请求头中的 keep-alive
来设置
HTTP 1.1 中新增加了 E-tag,If-Unmodified-Since, If-Match, If-None-Match 等缓存控制标头来控制缓存失效。
HTTP 1.1 支持断点续传,通过使用请求头中的 Range
来实现。
HTTP 1.1 使用了虚拟网络,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个IP地址。
HTTP1.1虽然是无状态协议,但是为了实现期望的保持状态功能,于是引入了cookie技术
向服务器发送请求时,服务端会发送一个认证信息,服务器第一次接收到请求时,会开辟一块session空间,同时生成一个sessionId,并通过响应头的Set-Cookie: JSESSIONID=XXXX的命令,向客户端放松要求设置Cookie的响应,客户端收到响应后,在本机客户端设置了一个JSESSIONID=XXXX的Cookie信息
接下来客户端每次向同一个网站发送请求时,请求头都会带上该 Cookie信息(包含 sessionId ), 然后,服务器通过读取请求头中的 Cookie 信息,获取名称为 JSESSIONID 的值,得到此次请求的 sessionId。这样,你的浏览器才具有了记忆能力。
还有一种方式是使用 JWT 机制,它也是能够让你的浏览器具有记忆能力的一种机制。与 Cookie 不同,JWT 是保存在客户端的信息,它广泛的应用于单点登录的情况。JWT 具有两个特点
JWT 的 Cookie 信息存储在客户端
,而不是服务端内存中。也就是说,JWT 直接本地进行验证就可以,验证完毕后,这个 Token 就会在 Session 中随请求一起发送到服务器,通过这种方式,可以节省服务器资源,并且 token 可以进行多次验证。
JWT 支持跨域认证,Cookies 只能用在单个节点的域
或者它的子域
中有效。如果它们尝试通过第三个节点访问,就会被禁止。使用 JWT 可以解决这个问题,使用 JWT 能够通过多个节点
进行用户认证,也就是我们常说的跨域认证
。
HTTP1.1可以持久连接,tcp连接默认不关闭,可以被多个请求复用。
//默认
Connection: keep-alive
//也可以选择关闭
Connection: close
头部压缩:由于 HTTP 1.1 经常会出现 User-Agent、Cookie、Accept、Server、Range 等字段可能会占用几百甚至几千字节,而 Body 却经常只有几十字节,所以导致头部偏重。HTTP 2.0 使用 HPACK
算法进行压缩。
二进制格式:HTTP 2.0 使用了更加靠近 TCP/IP 的二进制格式,而抛弃了 ASCII 码,提升了解析效率
强化安全:由于安全已经成为重中之重,所以 HTTP2.0 一般都跑在 HTTPS 上。
多路复用:即每一个请求都是是用作连接共享。一个请求对应一个id,这样一个连接上可以有多个请求。
随着网络技术的发展,1999 年设计的 HTTP/1.1 已经不能满足需求,所以 Google 在 2009 年设计了基于 TCP 的 SPDY,后来 SPDY 的开发组推动 SPDY 成为正式标准,不过最终没能通过。不过 SPDY 的开发组全程参与了 HTTP/2 的制定过程,参考了 SPDY 的很多设计,所以我们一般认为 SPDY 就是 HTTP/2 的前身。无论 SPDY 还是 HTTP/2,都是基于 TCP 的,TCP 与 UDP 相比效率上存在天然的劣势,所以 2013 年 Google 开发了基于 UDP 的名为 QUIC 的传输层协议,QUIC 全称 Quick UDP Internet Connections,希望它能替代 TCP,使得网页传输更加高效。后经提议,互联网工程任务组正式将基于 QUIC 协议的 HTTP (HTTP over QUIC)重命名为 HTTP/3。
网站 HTTP/2 检测工具,可一键检测指定网站是否开启了 HTTP/2
TCP 一直是传输层中举足轻重的协议,而 UDP 则默默无闻,在面试中问到 TCP 和 UDP 的区别时,有关 UDP 的回答常常寥寥几语,长期以来 UDP 给人的印象就是一个很快但不可靠的传输层协议。但有时候从另一个角度看,缺点可能也是优点。QUIC(Quick UDP Internet Connections,快速 UDP 网络连接) 基于 UDP,正是看中了 UDP 的速度与效率。同时 QUIC 也整合了 TCP、 TLS 和 HTTP/2 的优点,并加以优化。用一张图可以清晰地表示他们之间的关系。
那 QUIC 和 HTTP/3 什么关系呢?QUIC 是用来替代 TCP、SSL/TLS 的传输层协议,在传输层之上还有应用层,我们熟知的应用层协议有 HTTP、FTP、IMAP 等,这些协议理论上都可以运行在 QUIC 之上,其中运行在 QUIC 之上的 HTTP 协议被称为 HTTP/3,这就是”HTTP over QUIC 即 HTTP/3“的含义。
因此想要了解 HTTP/3,QUIC 是绕不过去的,下面主要通过几个重要的特性让大家对 QUIC 有更深的理解。
用一张图可以形象地看出 HTTP/2 和 HTTP/3 建立连接的差别。
HTTP/2 的连接需要 3 RTT,如果考虑会话复用,即把第一次握手算出来的对称密钥缓存起来,那么也需要 2 RTT,更进一步的,如果 TLS 升级到 1.3,那么 HTTP/2 连接需要 2 RTT,考虑会话复用则需要 1 RTT。有人会说 HTTP/2 不一定需要 HTTPS,握手过程还可以简化。这没毛病,HTTP/2 的标准的确不需要基于 HTTPS,但实际上所有浏览器的实现都要求 HTTP/2 必须基于 HTTPS,所以 HTTP/2 的加密连接必不可少。而 HTTP/3 首次连接只需要 1 RTT,后面的连接更是只需 0 RTT,意味着客户端发给服务端的第一个包就带有请求数据,这一点 HTTP/2 难以望其项背。那这背后是什么原理呢?
我们具体看下 QUIC 的连接过程:
这样,QUIC 从请求连接到正式接发 HTTP 数据一共花了 1 RTT,这 1 个 RTT 主要是为了获取 Server Config,后面的连接如果客户端缓存了 Server Config,那么就可以直接发送 HTTP 数据,实现 0 RTT 建立连接。
这里使用的是 DH 密钥交换算法,DH 算法的核心就是服务端生成 a、g、p 3 个随机数,a 自己持有,g 和 p 要传输给客户端,而客户端会生成 b 这 1 个随机数,通过 DH 算法客户端和服务端可以算出同样的密钥。在这过程中 a 和 b 并不参与网络传输,安全性大大提高。因为 p 和 g 是大数,所以即使在网络中传输的 p、g、A、B 都被劫持,那么靠现在的计算机算力也没法破解密钥。
TCP 连接基于四元组(源 IP、源端口、目的 IP、目的端口),切换网络时至少会有一个因素发生变化,导致连接发生变化。当连接发生变化时,如果还使用原来的 TCP 连接,则会导致连接失败,就得等原来的连接超时后重新建立连接,所以我们有时候发现切换到一个新网络时,即使新网络状况良好,但内容还是需要加载很久。如果实现得好,当检测到网络变化时立刻建立新的 TCP 连接,即使这样,建立新的连接还是需要几百毫秒的时间。
QUIC 的连接不受四元组的影响,当这四个元素发生变化时,原连接依然维持。那这是怎么做到的呢?道理很简单,QUIC 连接不以四元组作为标识,而是使用一个 64 位的随机数,这个随机数被称为 Connection ID,即使 IP 或者端口发生变化,只要 Connection ID 没有变化,那么连接依然可以维持。
HTTP/1.1 和 HTTP/2 都存在队头阻塞问题(Head of line blocking),那什么是队头阻塞呢?
TCP 是个面向连接的协议,即发送请求后需要收到 ACK 消息,以确认对方已接收到数据。如果每次请求都要在收到上次请求的 ACK 消息后再请求,那么效率无疑很低。后来 HTTP/1.1 提出了 Pipelining 技术,允许一个 TCP 连接同时发送多个请求,这样就大大提升了传输效率。
在这个背景下,下面就来谈 HTTP/1.1 的队头阻塞。下图中,一个 TCP 连接同时传输 10 个请求,其中第 1、2、3 个请求已被客户端接收,但第 4 个请求丢失,那么后面第 5 - 10 个请求都被阻塞,需要等第 4 个请求处理完毕才能被处理,这样就浪费了带宽资源。
因此,HTTP 一般又允许每个主机建立 6 个 TCP 连接,这样可以更加充分地利用带宽资源,但每个连接中队头阻塞的问题还是存在。
HTTP/2 的多路复用解决了上述的队头阻塞问题。不像 HTTP/1.1 中只有上一个请求的所有数据包被传输完毕下一个请求的数据包才可以被传输,HTTP/2 中每个请求都被拆分成多个 Frame 通过一条 TCP 连接同时被传输,这样即使一个请求被阻塞,也不会影响其他的请求。如下图所示,不同颜色代表不同的请求,相同颜色的色块代表请求被切分的 Frame。
事情还没完,HTTP/2 虽然可以解决“请求”这个粒度的阻塞,但 HTTP/2 的基础 TCP 协议本身却也存在着队头阻塞的问题。HTTP/2 的每个请求都会被拆分成多个 Frame,不同请求的 Frame 组合成 Stream,Stream 是 TCP 上的逻辑传输单元,这样 HTTP/2 就达到了一条连接同时发送多条请求的目标,这就是多路复用的原理。
我们看一个例子,在一条 TCP 连接上同时发送 4 个 Stream,其中 Stream1 已正确送达,Stream2 中的第 3 个 Frame 丢失,TCP 处理数据时有严格的前后顺序,先发送的 Frame 要先被处理,这样就会要求发送方重新发送第 3 个 Frame,Stream3 和 Stream4 虽然已到达但却不能被处理,那么这时整条连接都被阻塞。
不仅如此,由于 HTTP/2 必须使用 HTTPS,而 HTTPS 使用的 TLS 协议也存在队头阻塞问题。TLS 基于 Record 组织数据,将一堆数据放在一起(即一个 Record)加密,加密完后又拆分成多个 TCP 包传输。一般每个 Record 16K,包含 12 个 TCP 包,这样如果 12 个 TCP 包中有任何一个包丢失,那么整个 Record 都无法解密。
队头阻塞会导致 HTTP/2 在更容易丢包的弱网络环境下比 HTTP/1.1 更慢!
那 QUIC 是如何解决队头阻塞问题的呢?
主要有两点。
拥塞控制的目的是避免过多的数据一下子涌入网络,导致网络超出最大负荷。QUIC 的拥塞控制与 TCP 类似,并在此基础上做了改进。所以我们先简单介绍下 TCP 的拥塞控制。
TCP 拥塞控制由 4 个核心算法组成:慢启动、拥塞避免、快速重传和快速恢复,理解了这 4 个算法,对 TCP 的拥塞控制也就有了大概了解。
QUIC 重新实现了 TCP 协议的 Cubic 算法进行拥塞控制,并在此基础上做了不少改进。下面介绍一些 QUIC 改进的拥塞控制的特性。
TCP 中如果要修改拥塞控制策略,需要在系统层面进行操作。QUIC 修改拥塞控制策略只需要在应用层操作,并且 QUIC 会根据不同的网络环境、用户来动态选择拥塞控制算法。
QUIC 使用前向纠错(FEC,Forward Error Correction)技术增加协议的容错性。一段数据被切分为 10 个包后,依次对每个包进行异或运算,运算结果会作为 FEC 包与数据包一起被传输,如果不幸在传输过程中有一个数据包丢失,那么就可以根据剩余 9 个包以及 FEC 包推算出丢失的那个包的数据,这样就大大增加了协议的容错性。
这是符合现阶段网络技术的一种方案,现阶段带宽已经不是网络传输的瓶颈,往返时间才是,所以新的网络传输协议可以适当增加数据冗余,减少重传操作。
TCP 为了保证可靠性,使用 Sequence Number 和 ACK 来确认消息是否有序到达,但这样的设计存在缺陷。
超时发生后客户端发起重传,后来接收到了 ACK 确认消息,但因为原始请求和重传请求接收到的 ACK 消息一样,所以客户端就郁闷了,不知道这个 ACK 对应的是原始请求还是重传请求。如果客户端认为是原始请求的 ACK,但实际上是左图的情形,则计算的采样 RTT 偏大;如果客户端认为是重传请求的 ACK,但实际上是右图的情形,又会导致采样 RTT 偏小。图中有几个术语,RTO 是指超时重传时间(Retransmission TimeOut),跟我们熟悉的 RTT(Round Trip Time,往返时间)很长得很像。采样 RTT 会影响 RTO 计算,超时时间的准确把握很重要,长了短了都不合适。
QUIC 解决了上面的歧义问题。与 Sequence Number 不同的是,Packet Number 严格单调递增,如果 Packet N 丢失了,那么重传时 Packet 的标识不会是 N
,而是比 N 大的数字,比如 N + M
,这样发送方接收到确认消息时就能方便地知道 ACK 对应的是原始请求还是重传请求。
TCP 计算 RTT 时没有考虑接收方接收到数据到发送确认消息之间的延迟,如下图所示,这段延迟即 ACK Delay。QUIC 考虑了这段延迟,使得 RTT 的计算更加准确。
一般来说,接收方收到发送方的消息后都应该发送一个 ACK 回复,表示收到了数据。但每收到一个数据就返回一个 ACK 回复太麻烦,所以一般不会立即回复,而是接收到多个数据后再回复,TCP SACK 最多提供 3 个 ACK block。但有些场景下,比如下载,只需要服务器返回数据就好,但按照 TCP 的设计,每收到 3 个数据包就要“礼貌性”地返回一个 ACK。而 QUIC 最多可以捎带 256 个 ACK block。在丢包率比较严重的网络下,更多的 ACK block 可以减少重传量,提升网络效率。
TCP 会对每个 TCP 连接进行流量控制,流量控制的意思是让发送方不要发送太快,要让接收方来得及接收,不然会导致数据溢出而丢失,TCP 的流量控制主要通过滑动窗口来实现的。可以看出,拥塞控制主要是控制发送方的发送策略,但没有考虑到接收方的接收能力,流量控制是对这部分能力的补齐。
QUIC 只需要建立一条连接,在这条连接上同时传输多条 Stream,好比有一条道路,两头分别有一个仓库,道路中有很多车辆运送物资。QUIC 的流量控制有两个级别:连接级别(Connection Level)和 Stream 级别(Stream Level),好比既要控制这条路的总流量,不要一下子很多车辆涌进来,货物来不及处理,也不能一个车辆一下子运送很多货物,这样货物也来不及处理。
那 QUIC 是怎么实现流量控制的呢?我们先看单条 Stream 的流量控制。Stream 还没传输数据时,接收窗口(flow control receive window)就是最大接收窗口(flow control receive window),随着接收方接收到数据后,接收窗口不断缩小。在接收到的数据中,有的数据已被处理,而有的数据还没来得及被处理。如下图所示,蓝色块表示已处理数据,黄色块表示未处理数据,这部分数据的到来,使得 Stream 的接收窗口缩小。
随着数据不断被处理,接收方就有能力处理更多数据。当满足 (flow control receive offset - consumed bytes) < (max receive window / 2) 时,接收方会发送 WINDOW_UPDATE frame 告诉发送方你可以再多发送些数据过来。这时 flow control receive offset 就会偏移,接收窗口增大,发送方可以发送更多数据到接收方。
Stream 级别对防止接收端接收过多数据作用有限,更需要借助 Connection 级别的流量控制。理解了 Stream 流量那么也很好理解 Connection 流控。Stream 中,接收窗口(flow control receive window) = 最大接收窗口(max receive window) - 已接收数据(highest received byte offset) ,而对 Connection 来说:接收窗口 = Stream1 接收窗口 + Stream2 接收窗口 + … + StreamN 接收窗口 。
QUIC 丢掉了 TCP、TLS 的包袱,基于 UDP,并对 TCP、TLS、HTTP/2 的经验加以借鉴、改进,实现了一个安全高效可靠的 HTTP 通信协议。凭借着 0 RTT 建立连接、平滑的连接迁移、基本消除了队头阻塞、改进的拥塞控制和流量控制等优秀的特性,QUIC 在绝大多数场景下获得了比 HTTP/2 更好的效果。
微软宣布开源自己的内部 QUIC 库 – MsQuic,将全面推荐 QUIC 协议替换 TCP/IP 协议。
OSI七层网络模型
TCP/IP网络模型,一般是五层模型
但是也可以分为4层,就是把链路层和物理层都表示为网络接口层
UDP(User Datagram Protocol):用户数据报协议,不需要三次握手等操作,从而加快了通信速度, 允许网络上的其他主机在接收方统一通信之前进行数据传输。
数据报是与分组交换网络关联的传输单元。
tcpdump 和 Wireshark 的区别
tcpdump 仅支持命令行格式使用,常用在 Linux 服务器中抓取和分析网络包。
Wireshark 除了可以抓包外,还提供了可视化分析网络包的图形页面。
所以,这两者实际上是搭配使用的,先用 tcpdump 命令在 Linux 服务器上抓包,接着把抓包的文件拖出到 Windows 电脑后,用 Wireshark 可视化分析。
当然,如果你是在 Windows 上抓包,只需要用 Wireshark 工具就可以。
tcpdump
tcpdump 虽然功能强大,但是输出的格式并不直观。
所以,在工作中 tcpdump 只是用来抓取数据包,不用来分析数据包,而是把 tcpdump 抓取的数据包保存成 pcap 后缀的文件,接着用 Wireshark 工具进行数据包分析。
Wireshark
Wireshark 除了可以抓包外,还提供了可视化分析网络包的图形页面,同时,还内置了一系列的汇总分析工具。
接着把 ping.pcap 文件拖到电脑,再用 Wireshark 打开它。打开后,你就可以看到下面这个界面:
接着,在网络包列表中选择某一个网络包后,在其下面的网络包详情中,可以更清楚的看到,这个网络包在协议栈各层的详细信息。比如,以编号 1 的网络包为例子:
从 ping 的例子中,我们可以看到网络分层就像有序的分工,每一层都有自己的责任范围和信息,上层协议完成工作后就交给下一层,最终形成一个完整的网络包。
SYN(Synchronize Sequence Numbers):同步序列编号,是TCP/IP建立连接时使用的握手信号,在客户端和服务器之间建立TCP连接时,首先会发送的一个信号,客户端在接受到SYN消息时,就会在自己的段内生成一个随机值X
SYN-ACK:服务器收到SYN后,打开客户端连接,发送一个SYN-ACK作为答复,确认号设置为比接受到的序列号多一个,即X+1,服务器为数据包选择的序列号是另一个随机数Y
ACK(Acknowledge character):确认字符,表示发来的数据已确认接收无误,最后,客户端将ACK发送给服务端,序列号被设置为所接收的确认值即Y+1
因为服务器端收到客户端的
FIN
后,服务器端同时也要关闭连接,这样就可以把ACK
和FIN
合并到一起发送,节省了一个包,变成了“三次挥手”。
而通常情况下,服务器端收到客户端的
FIN
后,很可能还没发送完数据,所以就会先回复客户端一个ACK
包,稍等一会儿,完成所有数据包的发送后,才会发送FIN
包,这也就是四次挥手了。
当客户端发起的 TCP 第一次握手 SYN 包,在超时时间内没收到服务端的 ACK,就会在超时重传 SYN 数据包,每次超时重传的 RTO 是翻倍上涨的,直到 SYN 包的重传次数到达
tcp_syn_retries
(默认为5) 值后,客户端不再发送 SYN 包。
当 TCP 第二次握手 SYN、ACK 包丢了后,客户端 SYN 包会发生超时重传,服务端 SYN、ACK 也会发生超时重传。
客户端 SYN 包超时重传的最大次数,是由 tcp_syn_retries 决定的,默认值是 5 次;服务端 SYN、ACK 包时重传的最大次数,是由 tcp_synack_retries 决定的,默认值是 5 次。
TCP 第一次握手的 SYN 包超时重传最大次数是由 tcp_syn_retries 指定,TCP 第二次握手的 SYN、ACK 包超时重传最大次数是由 tcp_synack_retries 指定,TCP 建立连接后的数据包传输,最大超时重传次数是由 tcp_retries2
指定,默认值是 15 次,如下:
TCP 的 保活机制
定义一个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP 保活机制会开始作用,每隔一个时间间隔,发送一个「探测报文」,该探测报文包含的数据非常少,如果连续几个探测报文都没有得到响应,则认为当前的 TCP 连接已经死亡,系统内核将错误信息通知给上层应用程序。
在 Linux 内核可以有对应的参数可以设置保活时间、保活探测的次数、保活探测的时间间隔,以下都为默认值:
net.ipv4.tcp\_keepalive\_time=7200
net.ipv4.tcp\_keepalive\_intvl=75
net.ipv4.tcp\_keepalive\_probes=9
tcp_keepalive_time=7200:表示保活时间是 7200 秒(2小时),也就 2 小时内如果没有任何连接相关的活动,则会启动保活机制
tcp_keepalive_intvl=75:表示每次检测间隔 75 秒;
tcp_keepalive_probes=9:表示检测 9 次无响应,认为对方是不可达的,从而中断本次的连接。
也就是说在 Linux 系统中,最少需要经过 2 小时 11 分 15 秒才可以发现一个「死亡」连接。
在建立 TCP 连接时,如果第三次握手的 ACK,服务端无法收到,则服务端就会短暂处于
SYN_RECV
状态,而客户端会处于ESTABLISHED
状态。
由于服务端一直收不到 TCP 第三次握手的 ACK,则会一直重传 SYN、ACK 包,直到重传次数超过
tcp_synack_retries
值(默认值 5 次)后,服务端就会断开 TCP 连接。
客户端会处于ESTABLISHED,服务器处于SYN_RECV状态
而客户端则会有两种情况:
如果客户端没发送数据包,一直处于 ESTABLISHED
状态,然后经过 2 小时 11 分 15 秒才可以发现一个「死亡」连接,于是客户端连接就会断开连接。
如果客户端发送了数据包,一直没有收到服务端对该数据包的确认报文,则会一直重传该数据包,直到重传次数超过 tcp_retries2
值(默认值 15 次)后,客户端就会断开 TCP 连接。
客户端在向服务端发起 HTTP GET 请求时,一个完整的交互过程,需要 2.5 个 RTT 的时延。
由于第三次握手是可以携带数据的,这时如果在第三次握手发起 HTTP GET 请求,需要 2 个 RTT 的时延。
但是在下一次(不是同个 TCP 连接的下一次)发起 HTTP GET 请求时,经历的 RTT 也是一样,如下图:
在 Linux 3.7 内核版本中,提供了 TCP Fast Open 功能,这个功能可以减少 TCP 连接建立的时延。
在第一次建立连接的时候,服务端在第二次握手产生一个 Cookie
(已加密)并通过 SYN、ACK 包一起发给客户端,于是客户端就会缓存这个 Cookie
,所以第一次发起 HTTP Get 请求的时候,还是需要 2 个 RTT 的时延;
在下次请求的时候,客户端在 SYN 包带上 Cookie
发给服务端,就提前可以跳过三次握手的过程,因为 Cookie
中维护了一些信息,服务端可以从 Cookie
获取 TCP 相关的信息,这时发起的 HTTP GET 请求就只需要 1 个 RTT 的时延;
注:客户端在请求并存储了 Fast Open Cookie 之后,可以不断重复 TCP Fast Open 直至服务器认为 Cookie 无效(通常为过期)
可以通过设置 net.ipv4.tcp_fastopn
内核参数,来打开 Fast Open 功能。
net.ipv4.tcp_fastopn 各个值的意义:
0 关闭
1 作为客户端使用 Fast Open 功能
2 作为服务端使用 Fast Open 功能
3 无论作为客户端还是服务器,都可以使用 Fast Open 功能
当接收方收到乱序数据包时,会发送重复的 ACK,以使告知发送方要重发该数据包,当发送方收到 3 个重复 ACK 时,就会触发快速重传,立该重发丢失数据包。
TCP 为了防止发送方无脑的发送数据,导致接收方缓冲区被填满,所以就有了滑动窗口的机制,它可利用接收方的接收窗口来控制发送方要发送的数据量,也就是流量控制。
接收窗口是由接收方指定的值,存储在 TCP 头部中,它可以告诉发送方自己的 TCP 缓冲空间区大小,这个缓冲区是给应用程序读取数据的空间:
如果应用程序读取了缓冲区的数据,那么缓冲空间区的就会把被读取的数据移除
如果应用程序没有读取数据,则数据会一直滞留在缓冲区。
接收窗口的大小,是在 TCP 三次握手中协商好的,后续数据传输时,接收方发送确认应答 ACK 报文时,会携带当前的接收窗口的大小,以此来告知发送方。
假设接收方接收到数据后,应用层能很快的从缓冲区里读取数据,那么窗口大小会一直保持不变,过程如下:
但是现实中服务器会出现繁忙的情况,当应用程序读取速度慢,那么缓存空间会慢慢被占满,于是为了保证发送方发送的数据不会超过缓冲区大小,则服务器会调整窗口大小的值,接着通过 ACK 报文通知给对方,告知现在的接收窗口大小,从而控制发送方发送的数据大小。
假设接收方处理数据的速度跟不上接收数据的速度,缓存就会被占满,从而导致接收窗口为 0,当发送方接收到零窗口通知时,就会停止发送数据。
如下图,可以接收方的窗口大小在不断的收缩至 0:
接着,发送方会定时发送窗口大小探测报文,以便及时知道接收方窗口大小的变化。
发送方发送了数据包 1 给接收方,接收方收到后,由于缓冲区被占满,回了个零窗口通知;
发送方收到零窗口通知后,就不再发送数据了,直到过了 3.4
秒后,发送了一个 TCP Keep-Alive 报文,也就是窗口大小探测报文;
当接收方收到窗口探测报文后,就立马回一个窗口通知,但是窗口大小还是 0;
发送方发现窗口还是 0,于是继续等待了 6.8
(翻倍) 秒后,又发送了窗口探测报文,接收方依然还是回了窗口为 0 的通知;
发送方发现窗口还是 0,于是继续等待了 13.5
(翻倍) 秒后,又发送了窗口探测报文,接收方依然还是回了窗口为 0 的通知;
可以发现,这些窗口探测报文以 3.4s、6.5s、13.5s 的间隔出现,说明超时时间会翻倍递增。
这连接暂停了 25s,想象一下你在打王者的时候,25s 的延迟你还能上王者吗?
如何在包里看出发送窗口的大小?
很遗憾,没有简单的办法,发送窗口虽然是由接收窗口决定,但是它又可以被网络因素影响,也就是拥塞窗口,实际上发送窗口是值是 min(拥塞窗口,接收窗口)。
发送窗口和 MSS 有什么关系?
发送窗口决定了一口气能发多少字节,而 MSS 决定了这些字节要分多少包才能发完。
举个例子,如果发送窗口为 16000 字节的情况下,如果 MSS 是 1000 字节,那就需要发送 1600/1000 = 16 个包。
发送方在一个窗口发出 n 个包,是不是需要 n 个 ACK 确认报文?
不一定,因为 TCP 有累计确认机制,所以当收到多个数据包时,只需要应答最后一个数据包的 ACK 报文就可以了。
当我们 TCP 报文的承载的数据非常小的时候,例如几个字节,那么整个网络的效率是很低的,因为每个 TCP 报文中都有会 20 个字节的 TCP 头部,也会有 20 个字节的 IP 头部,而数据只有几个字节,所以在整个报文中有效数据占有的比重就会非常低。
这就好像快递员开着大货车送一个小包裹一样浪费。
那么就出现了常见的两种策略,来减少小报文的传输,分别是:
Nagle 算法
延迟确认
Nagle 算法是如何避免大量 TCP 小数据报文的传输?
Nagle 算法做了一些策略来避免过多的小数据报文发送,这可提高传输效率。
Nagle 算法的策略:
没有已发送未确认报文时,立刻发送数据。
存在未确认报文时,直到「没有已发送未确认报文」或「数据长度达到 MSS 大小」时,再发送数据。
只要没满足上面条件中的一条,发送方一直在囤积数据,直到满足上面的发送条件。
上图右侧启用了 Nagle 算法,它的发送数据的过程:
一开始由于没有已发送未确认的报文,所以就立刻发了 H 字符;
接着,在还没收到对 H 字符的确认报文时,发送方就一直在囤积数据,直到收到了确认报文后,此时就没有已发送未确认的报文,于是就把囤积后的 ELL 字符一起发给了接收方;
待收到对 ELL 字符的确认报文后,于是把最后一个 O 字符发送出去
可以看出,Nagle 算法一定会有一个小报文,也就是在最开始的时候。
另外,Nagle 算法默认是打开的,如果对于一些需要小数据包交互的场景的程序,比如,telnet 或 ssh 这样的交互性比较强的程序,则需要关闭 Nagle 算法。
可以在 Socket 设置 TCP_NODELAY
选项来关闭这个算法(关闭 Nagle 算法没有全局参数,需要根据每个应用自己的特点来关闭)。
那延迟确认又是什么?
事实上当没有携带数据的 ACK,他的网络效率也是很低的,因为它也有 40 个字节的 IP 头 和 TCP 头,但没有携带数据。
为了解决 ACK 传输效率低问题,所以就衍生出了 TCP 延迟确认。
TCP 延迟确认的策略:
当有响应数据要发送时,ACK 会随着响应数据一起立刻发送给对方
当没有响应数据要发送时,ACK 将会延迟一段时间,以等待是否有响应数据可以一起发送
如果在延迟等待发送 ACK 期间,对方的第二个数据报文又到达了,这时就会立刻发送 ACK
延迟等待的时间是在 Linux 内核中的定义的,如下图:
关键就需要 HZ
这个数值大小,HZ 是跟系统的时钟频率有关,每个操作系统都不一样,在我的 Linux 系统中 HZ 大小是 1000
,如下图:
知道了 HZ 的大小,那么就可以算出:
最大延迟确认时间是 200
ms (1000/5)
最短延迟确认时间是 40
ms (1000/25)
TCP 延迟确认可以在 Socket 设置 TCP_QUICKACK
选项来关闭这个算法。
延迟确认 和 Nagle 算法混合使用时,会产生新的问题
当 TCP 延迟确认 和 Nagle 算法混合使用时,会导致时耗增长,如下图:
发送方使用了 Nagle 算法,接收方使用了 TCP 延迟确认会发生如下的过程:
发送方先发出一个小报文,接收方收到后,由于延迟确认机制,自己又没有要发送的数据,只能干等着发送方的下一个报文到达;
而发送方由于 Nagle 算法机制,在未收到第一个报文的确认前,是不会发送后续的数据;
所以接收方只能等待最大时间 200 ms 后,才回 ACK 报文,发送方收到第一个报文的确认报文后,也才可以发送后续的数据。
很明显,这两个同时使用会造成额外的时延,这就会使得网络"很慢"的感觉。
要解决这个问题,只有两个办法:
要么发送方关闭 Nagle 算法
要么接收方关闭 TCP 延迟确认
tcp和udp都位于传输层,它们负责传输应用层产生的数据
HTTPS(HyperText Transfer Protocol Secure):
HTTP 是未经安全加密的协议,它的传输过程容易被攻击者监听、数据容易被窃取、发送方和接收方容易被伪造;而 HTTPS 是安全的协议,它通过 密钥交换算法 - 签名算法 - 对称加密算法 - 摘要算法 能够解决上面这些问题
TLS 旨在为 Internet 提供通信安全的加密协议。TLS 握手是启动和使用 TLS 加密的通信会话的过程。在 TLS 握手期间,Internet 中的通信双方会彼此交换信息,验证密码套件,交换会话密钥。
每当用户通过 HTTPS 导航到具体的网站并发送请求时,就会进行 TLS 握手。除此之外,每当其他任何通信使用HTTPS(包括 API 调用和在 HTTPS 上查询 DNS)时,也会发生 TLS 握手。
TLS 具体的握手过程会根据所使用的密钥交换算法的类型
和双方支持的密码套件
而不同。我们以RSA 非对称加密
来讨论这个过程。整个 TLS 通信流程图如下
在进行通信前,首先会进行 HTTP 的三次握手,握手完成后,再进行 TLS 的握手过程
ClientHello:客户端通过向服务器发送 hello
消息来发起握手过程。这个消息中会夹带着客户端支持的 TLS 版本号(TLS1.0 、TLS1.2、TLS1.3)
、客户端支持的密码套件、以及一串 客户端随机数
。
ServerHello:在客户端发送 hello 消息后,服务器会发送一条消息,这条消息包含了服务器的 SSL 证书、服务器选择的密码套件和服务器生成的随机数。
认证(Authentication):客户端的证书颁发机构会认证 SSL 证书,然后发送 Certificate
报文,报文中包含公开密钥证书。最后服务器发送 ServerHelloDone
作为 hello
请求的响应。第一部分握手阶段结束。
加密阶段
:在第一个阶段握手完成后,客户端会发送 ClientKeyExchange
作为响应,这个响应中包含了一种称为 The premaster secret
的密钥字符串,这个字符串就是使用上面公开密钥证书进行加密的字符串。随后客户端会发送 ChangeCipherSpec
,告诉服务端使用私钥解密这个 premaster secret
的字符串,然后客户端发送 Finished
告诉服务端自己发送完成了。
hosts
文件是否有配置 ip 地址,如果找到,直接返回。如果找不到,就向网络中发起一个 DNS 查询。首先来看一下 DNS 是啥,互联网中识别主机的方式有两种,通过
主机名
和IP 地址
。我们人喜欢用名字的方式进行记忆,但是通信链路中的路由却喜欢定长、有层次结构的 IP 地址。所以就需要一种能够把主机名到 IP 地址的转换服务,这种服务就是由 DNS 提供的。DNS 的全称是Domain Name System
域名系统。DNS 是一种由分层的 DNS 服务器实现的分布式数据库。DNS 运行在 UDP 上,使用 53 端口。
DNS 是一种分层数据库,它的主要层次结构如下
首先,查询请求会先找到本地 DNS 服务器来查询是否包含 IP 地址,如果本地 DNS 无法查询到目标 IP 地址,就会向根域名服务器发起一个 DNS 查询。
在由根域名服务器 -> 顶级域名服务器 -> 权威 DNS 服务器后,由权威服务器告诉本地服务器目标 IP 地址,再有本地 DNS 服务器告诉用户需要访问的 IP 地址。
注意:DNS 涉及两种查询方式:一种是
递归查询(Recursive query)
,一种是迭代查询(Iteration query)
。如果根域名服务器无法告知本地 DNS 服务器下一步需要访问哪个顶级域名服务器,就会使用递归询;如果根域名服务器能够告知 DNS 服务器下一步需要访问的顶级域名服务器,就会使用迭代查询。
第三步,浏览器需要和目标服务器建立 TCP 连接,需要经过三次握手的过程,具体的握手过程请参考上面的回答。
在建立连接后,浏览器会向目标服务器发起 HTTP-GET
请求,包括其中的 URL,HTTP 1.1 后默认使用长连接,只需要一次握手即可多次传输数据。
如果目标服务器只是一个简单的页面,就会直接返回。但是对于某些大型网站的站点,往往不会直接返回主机名所在的页面,而会直接重定向。返回的状态码就不是 200 ,而是 301,302 以 3 开头的重定向码,浏览器在获取了重定向响应后,在响应报文中 Location 项找到重定向地址,浏览器重新第一步访问即可。
然后浏览器重新发送请求,携带新的 URL,返回状态码 200 OK,表示服务器可以响应请求,返回报文。