jieye の 数字花园

Search

Search IconIcon to open search

HTTP

Last updated Nov 1, 2021

# 特性

  1. keepalive 在第一个 HTTP 请求完后,先不断开 TCP 连接,让后续的 HTTP 请求继续使用此连接?当然可以,HTTP 的 Keep-Alive 就是实现了这个功能,可以使用同一个 TCP 连接来发送和接收多个 HTTP 请求/应答,避免了连接建立和释放的开销,这个方法称为 HTTP 长连接HTTP 1.1 开始,就默认是开启 (关于 tcp 的 keepalive ^d5b1b1

# HTTP 基本概念

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

提纲

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

三个部分

# HTTP 常见的状态码

五大类 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」表示服务器当前很忙,暂时无法响应服务器,类似“网络服务正忙,请稍后重试”的意思。

# 常见字段

Host

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

img

1
Host: www.A.com

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

Content-Length 字段

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

img

1
Content-Length: 1000

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

Connection 字段

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

image

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

1
Connection: keep-alive

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

Content-Type 字段

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

img

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

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

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

1
Accept: */*

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

Content-Encoding 字段

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

img

1
Content-Encoding: gzip

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

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

1
Accept-Encoding: gzip, deflate

# HTTPS

# 实现原理

大家可能都听说过 HTTPS 协议之所以是安全的是因为 HTTPS 协议会对传输的数据进行加密,而加密过程是使用了非对称加密实现。但其实,HTTPS 在内容传输的加密上使用的是对称加密,非对称加密只作用在证书验证阶段。

相关文章:

你连 HTTPS 原理都不懂,还讲“中间人攻击”?

HTTPS 原理分析:带着疑问层层深入

HTTPS 加密原理

HTTPS 的整体过程分为证书验证和数据传输阶段,具体的交互过程如下:

WX20191127-133805@2x.png

① 证书验证阶段

  1. 浏览器发起 HTTPS 请求
  2. 服务端返回 HTTPS 证书
  3. 客户端验证证书是否合法,如果不合法则提示告警

② 数据传输阶段

  1. 当证书验证合法后,在本地生成随机数
  2. 通过公钥加密随机数,并把加密后的随机数传输到服务端
  3. 服务端通过私钥对随机数进行解密
  4. 服务端通过客户端传入的随机数构造对称加密算法,对返回结果内容进行加密后传输

# 为什么数据传输是用对称加密?

首先,非对称加密的加解密效率是非常低的,而 http 的应用场景中通常端与端之间存在大量的交互,非对称加密的效率是无法接受的;

另外,在 HTTPS 的场景中只有服务端保存了私钥,一对公私钥只能实现单向的加解密,所以 HTTPS 中内容传输加密采取的是对称加密,而不是非对称加密。

# 为什么需要 CA 认证机构颁发证书?

HTTP 协议被认为不安全是因为传输过程容易被监听者勾线监听、伪造服务器,而 HTTPS 协议主要解决的便是网络传输的安全性问题。

首先我们假设不存在认证机构,任何人都可以制作证书,这带来的安全风险便是经典的 “中间人攻击” 问题。
“中间人攻击”的具体过程如下:

WX20191126-212406@2x.png

过程原理:

  1. 本地请求被劫持(如 DNS 劫持等),所有请求均发送到中间人的服务器
  2. 中间人服务器返回中间人自己的证书
  3. 客户端创建随机数,通过中间人证书的公钥对随机数加密后传送给中间人,然后凭随机数构造对称加密对传输内容进行加密传输
  4. 中间人因为拥有客户端的随机数,可以通过对称加密算法进行内容解密
  5. 中间人以客户端的请求内容再向正规网站发起请求
  6. 因为中间人与服务器的通信过程是合法的,正规网站通过建立的安全通道返回加密后的数据
  7. 中间人凭借与正规网站建立的对称加密算法对内容进行解密
  8. 中间人通过与客户端建立的对称加密算法对正规内容返回的数据进行加密传输
  9. 客户端通过与中间人建立的对称加密算法对返回结果数据进行解密

由于缺少对证书的验证,所以客户端虽然发起的是 HTTPS 请求,但客户端完全不知道自己的网络已被拦截,传输内容被中间人全部窃取。

# 浏览器是如何确保 CA 证书的合法性?

# 1. 证书包含什么信息?
# 2. 证书的合法性依据是什么?

首先,权威机构是要有认证的,不是随便一个机构都有资格颁发证书,不然也不叫做权威机构。另外,证书的可信性基于信任制,权威机构需要对其颁发的证书进行信用背书,只要是权威机构生成的证书,我们就认为是合法的。所以权威机构会对申请者的信息进行审核,不同等级的权威机构对审核的要求也不一样,于是证书也分为免费的、便宜的和贵的。

# 3. 浏览器如何验证证书的合法性?

浏览器发起 HTTPS 请求时,服务器会返回网站的 SSL 证书,浏览器需要对证书做以下验证:

  1. 验证域名、有效期等信息是否正确。证书上都有包含这些信息,比较容易完成验证;

  2. 判断证书来源是否合法。每份签发证书都可以根据验证链查找到对应的根证书,操作系统、浏览器会在本地存储权威机构的根证书,利用本地根证书可以对对应机构签发证书完成来源验证;

    WX20191127-084216@2x.png

  3. 判断证书是否被篡改。需要与 CA 服务器进行校验;

  4. 判断证书是否已吊销。通过 CRL(Certificate Revocation List 证书注销列表)和 OCSP(Online Certificate Status Protocol 在线证书状态协议)实现,其中 OCSP 可用于第 3 步中以减少与 CA 服务器的交互,提高验证效率。

以上任意一步都满足的情况下浏览器才认为证书是合法的。

这里插一个我想了很久的但其实答案很简单的问题:
既然证书是公开的,如果要发起中间人攻击,我在官网上下载一份证书作为我的服务器证书,那客户端肯定会认同这个证书是合法的,如何避免这种证书冒用的情况?

其实这就是非加密对称中公私钥的用处,虽然中间人可以得到证书,但私钥是无法获取的,一份公钥是不可能推算出其对应的私钥,中间人即使拿到证书也无法伪装成合法服务端,因为无法对客户端传入的加密数据进行解密。

# 4. 只有认证机构可以生成证书吗?

如果需要浏览器不提示安全风险,那只能使用认证机构签发的证书。但浏览器通常只是提示安全风险,并不限制网站不能访问,所以从技术上谁都可以生成证书,只要有证书就可以完成网站的
HTTPS 传输。例如早期的 12306 采用的便是手动安装私有证书的形式实现 HTTPS 访问。

# 本地随机数被窃取怎么办?

证书验证是采用非对称加密实现,但是传输过程是采用对称加密,而其中对称加密算法中重要的随机数是由本地生成并且存储于本地的,HTTPS 如何保证随机数不会被窃取?

其实 HTTPS 并不包含对随机数的安全保证,HTTPS 保证的只是传输过程安全,而随机数存储于本地,本地的安全属于另一安全范畴,应对的措施有安装杀毒软件、反木马、浏览器升级修复漏洞等。

# 用了 HTTPS 会被抓包吗?

HTTPS 的数据是加密的,常规下抓包工具代理请求后抓到的包内容是加密状态,无法直接查看。

但是,正如前文所说,浏览器只会提示安全风险,如果用户授权仍然可以继续访问网站,完成请求。因此,只要客户端是我们自己的终端,我们授权的情况下,便可以组建中间人网络,而抓包工具便是作为中间人的代理。通常 HTTPS 抓包工具的使用方法是会生成一个证书,用户需要手动把证书安装到客户端中,然后终端发起的所有请求通过该证书完成与抓包工具的交互,然后抓包工具再转发请求到服务器,最后把服务器返回的结果在控制台输出后再返回给终端,从而完成整个请求的闭环。

既然 HTTPS 不能防抓包,那 HTTPS 有什么意义?
HTTPS 可以防止用户在不知情的情况下通信链路被监听,对于主动授信的抓包操作是不提供防护的,因为这个场景用户是已经对风险知情。要防止被抓包,需要采用应用级的安全防护,例如采用私有的对称加密,同时做好移动端的防反编译加固,防止本地算法被破解。

# 总结 Q&A

以下用简短的 Q&A 形式进行全文总结:

Q: HTTPS 为什么安全?
A: 因为 HTTPS 保证了传输安全,防止传输过程被监听、防止数据被窃取,可以确认网站的真实性。

Q: HTTPS 的传输过程是怎样的?
A: 客户端发起 HTTPS
请求,服务端返回证书,客户端对证书进行验证,验证通过后本地生成用于改造对称加密算法的随机数,通过证书中的公钥对随机数进行加密传输到服务端,服务端接收后通过私钥解密得到随机数,之后的数据交互通过对称加密算法进行加解密。

Q: 为什么需要证书?
A: 防止”中间人“攻击,同时可以为网站提供身份证明。

Q: 使用 HTTPS 会被抓包吗?
A: 会被抓包,HTTPS 只防止用户在不知情的情况下通信被监听,如果用户主动授信,是可以构建“中间人”网络,代理软件可以对传输内容进行解密。

顺手 po 一张学习的过程图,高清大图点这里 ☞ HTTPS 学习草稿图.jpg

img

# 抓包

提纲

# 显形“不可见”的网络包

网络世界中的数据包交互我们肉眼是看不见的,它们就好像隐形了一样,我们对着课本学习计算机网络的时候就会觉得非常的抽象,加大了学习的难度。

还别说,我自己在大学的时候,也是如此。

直到工作后,认识了两大分析网络的利器:tcpdump 和 Wireshark,这两大利器把我们“看不见”的数据包,呈现在我们眼前,一目了然。

唉,当初大学学习计网的时候,要是能知道这两个工具,就不会学的一脸懵逼。

tcpdump 和 Wireshark 有什么区别?

tcpdump 和 Wireshark 就是最常用的网络抓包和分析工具,更是分析网络性能必不可少的利器。

所以,这两者实际上是搭配使用的,先用 tcpdump 命令在 Linux 服务器上抓包,接着把抓包的文件拖出到 Windows 电脑后,用 Wireshark 可视化分析。

当然,如果你是在 Windows 上抓包,只需要用 Wireshark 工具就可以。

tcpdump 在 Linux 下如何抓包?

tcpdump 提供了大量的选项以及各式各样的过滤表达式,来帮助你抓取指定的数据包,不过不要担心,只需要掌握一些常用选项和过滤表达式,就可以满足大部分场景的需要了。

假设我们要抓取下面的 ping 的数据包:

img

要抓取上面的 ping 命令数据包,首先我们要知道 ping 的数据包是 icmp 协议,接着在使用 tcpdump 抓包的时候,就可以指定只抓 icmp 协议的数据包:

img

那么当 tcpdump 抓取到 icmp 数据包后, 输出格式如下:

img

img

从 tcpdump 抓取的 icmp 数据包,我们很清楚的看到 icmp echo 的交互过程了,首先发送方发起了 ICMP echo request 请求报文,接收方收到后回了一个 ICMP echo reply 响应报文,之后 seq 是递增的。

我在这里也帮你整理了一些最常见的用法,并且绘制成了表格,你可以参考使用。

首先,先来看看常用的选项类,在上面的 ping 例子中,我们用过 -i 选项指定网口,用过 -nn 选项不对 IP 地址和端口名称解析。其他常用的选项,如下表格:

tcpdump 常用选项类tcpdump 常用选项类

接下来,我们再来看看常用的过滤表用法,在上面的 ping 例子中,我们用过的是 icmp and host 183.232.231.174,表示抓取 icmp 协议的数据包,以及源地址或目标地址为 183.232.231.174 的包。其他常用的过滤选项,我也整理成了下面这个表格。

tcpdump 常用过滤表达式类tcpdump 常用过滤表达式类

说了这么多,你应该也发现了,tcpdump 虽然功能强大,但是输出的格式并不直观。

所以,在工作中 tcpdump 只是用来抓取数据包,不用来分析数据包,而是把 tcpdump 抓取的数据包保存成 pcap 后缀的文件,接着用 Wireshark 工具进行数据包分析。

Wireshark 工具如何分析数据包?

Wireshark 除了可以抓包外,还提供了可视化分析网络包的图形页面,同时,还内置了一系列的汇总分析工具。

比如,拿上面的 ping 例子来说,我们可以使用下面的命令,把抓取的数据包保存到 ping.pcap 文件

img

接着把 ping.pcap 文件拖到电脑,再用 Wireshark 打开它。打开后,你就可以看到下面这个界面:

img

是吧?在 Wireshark 的页面里,可以更加直观的分析数据包,不仅展示各个网络包的头部信息,还会用不同的颜色来区分不同的协议,由于这次抓包只有 ICMP 协议,所以只有紫色的条目。

接着,在网络包列表中选择某一个网络包后,在其下面的网络包详情中,可以更清楚的看到,这个网络包在协议栈各层的详细信息。比如,以编号 1 的网络包为例子:

ping 网络包ping 网络包

Wireshark 用了分层的方式,展示了各个层的包头信息,把“不可见”的数据包,清清楚楚的展示了给我们,还有理由学不好计算机网络吗?是不是相见恨晚

从 ping 的例子中,我们可以看到网络分层就像有序的分工,每一层都有自己的责任范围和信息,上层协议完成工作后就交给下一层,最终形成一个完整的网络包。

img


# 解密 TCP 三次握手和四次挥手

既然学会了 tcpdump 和 Wireshark 两大网络分析利器,那我们快马加鞭,接下用它俩抓取和分析 HTTP 协议网络包,并理解 TCP 三次握手和四次挥手的工作原理。

本次例子,我们将要访问的 http://192.168.3.200 服务端。在终端一用 tcpdump 命令抓取数据包:

img

接着,在终端二执行下面的 curl 命令:

img

最后,回到终端一,按下 Ctrl+C 停止 tcpdump,并把得到的 http.pcap 取出到电脑。

使用 Wireshark 打开 http.pcap 后,你就可以在 Wireshark 中,看到如下的界面:

HTTP 网络包HTTP 网络包

我们都知道 HTTP 是基于 TCP 协议进行传输的,那么:

Wireshark 可以用时序图的方式显示数据包交互的过程,从菜单栏中,点击 统计 (Statistics) -> 流量图 (Flow Graph),然后,在弹出的界面中的「流量类型」选择 「TCP Flows」,你可以更清晰的看到,整个过程中 TCP 流的执行过程:

TCP 流量图TCP 流量图

你可能会好奇,为什么三次握手连接过程的 Seq 是 0 ?

实际上是因为 Wireshark 工具帮我们做了优化,它默认显示的是序列号 seq 是相对值,而不是真实值。

如果你想看到实际的序列号的值,可以右键菜单, 然后找到「协议首选项」,接着找到「Relative Seq」后,把它给取消,操作如下:

取消序列号相对值显示取消序列号相对值显示

取消后,Seq 显示的就是真实值了:

TCP 流量图TCP 流量图

可见,客户端和服务端的序列号实际上是不同的,序列号是一个随机值。

这其实跟我们书上看到的 TCP 三次握手和四次挥手很类似,作为对比,你通常看到的 TCP 三次握手和四次挥手的流程,基本是这样的:

TCP 三次握手和四次挥手的流程TCP 三次握手和四次挥手的流程

为什么抓到的 TCP 挥手是三次,而不是书上说的四次?

因为服务器端收到客户端的 FIN 后,服务器端同时也要关闭连接,这样就可以把 ACKFIN 合并到一起发送,节省了一个包,变成了“三次挥手”。

而通常情况下,服务器端收到客户端的 FIN 后,很可能还没发送完数据,所以就会先回复客户端一个 ACK 包,稍等一会儿,完成所有数据包的发送后,才会发送 FIN 包,这也就是四次挥手了。

如下图,就是四次挥手的过程:

四次挥手四次挥手


# TCP 三次握手异常情况实战分析

TCP 三次握手的过程相信大家都背的滚瓜烂熟,那么你有没有想过这三个异常情况:

有的小伙伴可能说:“很简单呀,包丢了就会重传嘛。”

那我在继续问你:

是不是哑口无言,无法回答?

不知道没关系,接下里我用三个实验案例,带大家一起探究探究这三种异常。

# 实验场景

本次实验用了两台虚拟机,一台作为服务端,一台作为客户端,它们的关系如下:

实验环境实验环境

# 实验一:TCP 第一次握手 SYN 丢包

为了模拟 TCP 第一次握手 SYN 丢包的情况,我是在拔掉服务器的网线后,立刻在客户端执行 curl 命令:

img

其间 tcpdump 抓包的命令如下:

img

过了一会, curl 返回了超时连接的错误:

img

date 返回的时间,可以发现在超时接近 1 分钟的时间后,curl 返回了错误。

接着,把 tcp_sys_timeout.pcap 文件用 Wireshark 打开分析,显示如下图:

SYN 超时重传五次SYN 超时重传五次

从上图可以发现, 客户端发起了 SYN 包后,一直没有收到服务端的 ACK ,所以一直超时重传了 5 次,并且每次 RTO 超时时间是不同的:

可以发现,每次超时时间 RTO 是指数(翻倍)上涨的,当超过最大重传次数后,客户端不再发送 SYN 包。

在 Linux 中,第一次握手的 SYN 超时重传次数,是如下内核参数指定的:

1
2
$ cat /proc/sys/net/ipv4/tcp_syn_retries
5

tcp_syn_retries 默认值为 5,也就是 SYN 最大重传次数是 5 次。

接下来,我们继续做实验,把 tcp_syn_retries 设置为 2 次:

1
$ echo 2 > /proc/sys/net/ipv4/tcp_syn_retries

重传抓包后,用 Wireshark 打开分析,显示如下图:

SYN 超时重传两次SYN 超时重传两次

实验一的实验小结

通过实验一的实验结果,我们可以得知,当客户端发起的 TCP 第一次握手 SYN 包,在超时时间内没收到服务端的 ACK,就会在超时重传 SYN 数据包,每次超时重传的 RTO 是翻倍上涨的,直到 SYN 包的重传次数到达 tcp_syn_retries 值后,客户端不再发送 SYN 包。

SYN 超时重传SYN 超时重传

# 实验二:TCP 第二次握手 SYN、ACK 丢包

为了模拟客户端收不到服务端第二次握手 SYN、ACK 包,我的做法是在客户端加上防火墙限制,直接粗暴的把来自服务端的数据都丢弃,防火墙的配置如下:

img

接着,在客户端执行 curl 命令:

img

date 返回的时间前后,可以算出大概 1 分钟后,curl 报错退出了。

客户端在这其间抓取的数据包,用 Wireshark 打开分析,显示的时序图如下:

img

从图中可以发现:

所以,我们可以发现,当第二次握手的 SYN、ACK 丢包时,客户端会超时重发 SYN 包,服务端也会超时重传 SYN、ACK 包。

咦?客户端设置了防火墙,屏蔽了服务端的网络包,为什么 tcpdump 还能抓到服务端的网络包?

添加 iptables 限制后, tcpdump 是否能抓到包 ,这要看添加的 iptables 限制条件:

网络包进入主机后的顺序如下:

tcp_syn_retries 是限制 SYN 重传次数,那第二次握手 SYN、ACK 限制最大重传次数是多少?

TCP 第二次握手 SYN、ACK 包的最大重传次数是通过 tcp_synack_retries 内核参数限制的,其默认值如下:

1
2
$ cat /proc/sys/net/ipv4/tcp_synack_retries
5

是的,TCP 第二次握手 SYN、ACK 包的最大重传次数默认值是 5 次。

为了验证 SYN、ACK 包最大重传次数是 5 次,我们继续做下实验,我们先把客户端的 tcp_syn_retries 设置为 1,表示客户端 SYN 最大超时次数是 1 次,目的是为了防止多次重传 SYN,把服务端 SYN、ACK 超时定时器重置。

接着,还是如上面的步骤:

  1. 客户端配置防火墙屏蔽服务端的数据包
  2. 客户端 tcpdump 抓取 curl 执行时的数据包

把抓取的数据包,用 Wireshark 打开分析,显示的时序图如下:

img

从上图,我们可以分析出:

接着,我把 tcp_synack_retries 设置为 2tcp_syn_retries 依然设置为 1:

1
2
$ echo 2 > /proc/sys/net/ipv4/tcp_synack_retries
$ echo 1 > /proc/sys/net/ipv4/tcp_syn_retries

依然保持一样的实验步骤进行操作,接着把抓取的数据包,用 Wireshark 打开分析,显示的时序图如下:

img

可见:

实验二的实验小结

通过实验二的实验结果,我们可以得知,当 TCP 第二次握手 SYN、ACK 包丢了后,客户端 SYN 包会发生超时重传,服务端 SYN、ACK 也会发生超时重传。

客户端 SYN 包超时重传的最大次数,是由 tcp_syn_retries 决定的,默认值是 5 次;服务端 SYN、ACK 包时重传的最大次数,是由 tcp_synack_retries 决定的,默认值是 5 次。

# 实验三:TCP 第三次握手 ACK 丢包

为了模拟 TCP 第三次握手 ACK 包丢,我的实验方法是在服务端配置防火墙,屏蔽客户端 TCP 报文中标志位是 ACK 的包,也就是当服务端收到客户端的 TCP ACK 的报文时就会丢弃,iptables 配置命令如下:

img

接着,在客户端执行如下 tcpdump 命令:

img

然后,客户端向服务端发起 telnet,因为 telnet 命令是会发起 TCP 连接,所以用此命令做测试:

img

此时,由于服务端收不到第三次握手的 ACK 包,所以一直处于 SYN_RECV 状态:

img

而客户端是已完成 TCP 连接建立,处于 ESTABLISHED 状态:

img

过了 1 分钟后,观察发现服务端的 TCP 连接不见了:

img

过了 30 分别,客户端依然还是处于 ESTABLISHED 状态:

img

接着,在刚才客户端建立的 telnet 会话,输入 123456 字符,进行发送:

img

持续「好长」一段时间,客户端的 telnet 才断开连接:

img

以上就是本次的实现三的现象,这里存在两个疑点:

不着急,我们把刚抓的数据包,用 Wireshark 打开分析,显示的时序图如下:

img

上图的流程:

通过这一波分析,刚才的两个疑点已经解除了:

TCP 第一次握手的 SYN 包超时重传最大次数是由 tcp_syn_retries 指定,TCP 第二次握手的 SYN、ACK 包超时重传最大次数是由 tcp_synack_retries 指定,那 TCP 建立连接后的数据包最大超时重传次数是由什么参数指定呢?

TCP 建立连接后的数据包传输,最大超时重传次数是由 tcp_retries2 指定,默认值是 15 次,如下:

1
2
$ cat /proc/sys/net/ipv4/tcp_retries2
15

如果 15 次重传都做完了,TCP 就会告诉应用层说:“搞不定了,包怎么都传不过去!”

那如果客户端不发送数据,什么时候才会断开处于 ESTABLISHED 状态的连接?

这里就需要提到 TCP 的 保活机制。这个机制的原理是这样的:

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

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

1
2
3
net.ipv4.tcp_keepalive_time=7200
net.ipv4.tcp_keepalive_intvl=75
net.ipv4.tcp_keepalive_probes=9

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

img

这个时间是有点长的,所以如果我抓包足够久,或许能抓到探测报文。

实验三的实验小结

在建立 TCP 连接时,如果第三次握手的 ACK,服务端无法收到,则服务端就会短暂处于 SYN_RECV 状态,而客户端会处于 ESTABLISHED 状态。

由于服务端一直收不到 TCP 第三次握手的 ACK,则会一直重传 SYN、ACK 包,直到重传次数超过 tcp_synack_retries 值(默认值 5 次)后,服务端就会断开 TCP 连接。

而客户端则会有两种情况:


# TCP 快速建立连接

客户端在向服务端发起 HTTP GET 请求时,一个完整的交互过程,需要 2.5 个 RTT 的时延。

由于第三次握手是可以携带数据的,这时如果在第三次握手发起 HTTP GET 请求,需要 2 个 RTT 的时延。

但是在下一次(不是同个 TCP 连接的下一次)发起 HTTP GET 请求时,经历的 RTT 也是一样,如下图:

常规 HTTP 请求常规 HTTP 请求

在 Linux 3.7 内核版本中,提供了 TCP Fast Open 功能,这个功能可以减少 TCP 连接建立的时延。

常规 HTTP 请求 与 Fast Open HTTP 请求常规 HTTP 请求 与 Fast Open HTTP 请求

注:客户端在请求并存储了 Fast Open Cookie 之后,可以不断重复 TCP Fast Open 直至服务器认为 Cookie 无效(通常为过期)

在 Linux 上如何打开 Fast Open 功能?

可以通过设置 net.ipv4.tcp_fastopn 内核参数,来打开 Fast Open 功能。

net.ipv4.tcp_fastopn 各个值的意义:

TCP Fast Open 抓包分析

在下图,数据包 7 号,客户端发起了第二次 TCP 连接时,SYN 包会携带 Cooike,并且有长度为 5 的数据。

服务端收到后,校验 Cooike 合法,于是就回了 SYN、ACK 包,并且确认应答收到了客户端的数据包,ACK = 5 + 1 = 6

TCP Fast Open 抓包分析TCP Fast Open 抓包分析


# TCP 重复确认和快速重传

当接收方收到乱序数据包时,会发送重复的 ACK,以使告知发送方要重发该数据包,当发送方收到 3 个重复 ACK 时,就会触发快速重传,立该重发丢失数据包。

快速重传机制快速重传机制

TCP 重复确认和快速重传的一个案例,用 Wireshark 分析,显示如下:

img

注意:快速重传和重复 ACK 标记信息是 Wireshark 的功能,非数据包本身的信息。

以上案例在 TCP 三次握手时协商开启了选择性确认 SACK,因此一旦数据包丢失并收到重复 ACK ,即使在丢失数据包之后还成功接收了其他数据包,也只需要重传丢失的数据包。如果不启用 SACK,就必须重传丢失包之后的每个数据包。

如果要支持 SACK,必须双方都要支持。在 Linux 下,可以通过 net.ipv4.tcp_sack 参数打开这个功能(Linux 2.4 后默认打开)。


# TCP 流量控制

TCP 为了防止发送方无脑的发送数据,导致接收方缓冲区被填满,所以就有了滑动窗口的机制,它可利用接收方的接收窗口来控制发送方要发送的数据量,也就是流量控制。

接收窗口是由接收方指定的值,存储在 TCP 头部中,它可以告诉发送方自己的 TCP 缓冲空间区大小,这个缓冲区是给应用程序读取数据的空间:

接收窗口的大小,是在 TCP 三次握手中协商好的,后续数据传输时,接收方发送确认应答 ACK 报文时,会携带当前的接收窗口的大小,以此来告知发送方。

假设接收方接收到数据后,应用层能很快的从缓冲区里读取数据,那么窗口大小会一直保持不变,过程如下:

理想状态下的窗口变化理想状态下的窗口变化

但是现实中服务器会出现繁忙的情况,当应用程序读取速度慢,那么缓存空间会慢慢被占满,于是为了保证发送方发送的数据不会超过缓冲区大小,则服务器会调整窗口大小的值,接着通过 ACK 报文通知给对方,告知现在的接收窗口大小,从而控制发送方发送的数据大小。

服务端繁忙状态下的窗口变化服务端繁忙状态下的窗口变化

# 零窗口通知与窗口探测

假设接收方处理数据的速度跟不上接收数据的速度,缓存就会被占满,从而导致接收窗口为 0,当发送方接收到零窗口通知时,就会停止发送数据。

如下图,可以接收方的窗口大小在不断的收缩至 0:

窗口大小在收缩窗口大小在收缩

接着,发送方会定时发送窗口大小探测报文,以便及时知道接收方窗口大小的变化。

以下图 Wireshark 分析图作为例子说明:

零窗口 与 窗口探测零窗口 与 窗口探测

可以发现,这些窗口探测报文以 3.4s、6.5s、13.5s 的间隔出现,说明超时时间会翻倍递增。

这连接暂停了 25s,想象一下你在打王者的时候,25s 的延迟你还能上王者吗?

# 发送窗口的分析

在 Wireshark 看到的 Windows size 也就是 " win = “,这个值表示发送窗口吗?

这不是发送窗口,而是在向对方声明自己的接收窗口。

你可能会好奇,抓包文件里有「Window size scaling factor」,它其实是算出实际窗口大小的乘法因子,「Windos size value」实际上并不是真实的窗口大小,真实窗口大小的计算公式如下:

「Windos size value」 * 「Window size scaling factor」 = 「Caculated window size 」

对应的下图案例,也就是 32 * 2048 = 65536。

img

实际上是 Caculated window size 的值是 Wireshark 工具帮我们算好的,Window size scaling factor 和 Windos size value 的值是在 TCP 头部中,其中 Window size scaling factor 是在三次握手过程中确定的,如果你抓包的数据没有 TCP 三次握手,那可能就无法算出真实的窗口大小的值,如下图:

img

如何在包里看出发送窗口的大小?

很遗憾,没有简单的办法,发送窗口虽然是由接收窗口决定,但是它又可以被网络因素影响,也就是拥塞窗口,实际上发送窗口是值是 min(拥塞窗口,接收窗口)。

发送窗口和 MSS 有什么关系?

发送窗口决定了一口气能发多少字节,而 MSS 决定了这些字节要分多少包才能发完。

举个例子,如果发送窗口为 16000 字节的情况下,如果 MSS 是 1000 字节,那就需要发送 1600/1000 = 16 个包。

发送方在一个窗口发出 n 个包,是不是需要 n 个 ACK 确认报文?

不一定,因为 TCP 有累计确认机制,所以当收到多个数据包时,只需要应答最后一个数据包的 ACK 报文就可以了。


# TCP 延迟确认与 Nagle 算法

当我们 TCP 报文的承载的数据非常小的时候,例如几个字节,那么整个网络的效率是很低的,因为每个 TCP 报文中都有会 20 个字节的 TCP 头部,也会有 20 个字节的 IP 头部,而数据只有几个字节,所以在整个报文中有效数据占有的比重就会非常低。

这就好像快递员开着大货车送一个小包裹一样浪费。

那么就出现了常见的两种策略,来减少小报文的传输,分别是:

Nagle 算法是如何避免大量 TCP 小数据报文的传输?

Nagle 算法做了一些策略来避免过多的小数据报文发送,这可提高传输效率。

Nagle 算法的策略:

只要没满足上面条件中的一条,发送方一直在囤积数据,直到满足上面的发送条件。

禁用 Nagle 算法 与 启用 Nagle 算法禁用 Nagle 算法 与 启用 Nagle 算法

上图右侧启用了 Nagle 算法,它的发送数据的过程:

可以看出,Nagle 算法一定会有一个小报文,也就是在最开始的时候。

另外,Nagle 算法默认是打开的,如果对于一些需要小数据包交互的场景的程序,比如,telnet 或 ssh 这样的交互性比较强的程序,则需要关闭 Nagle 算法。

可以在 Socket 设置 TCP_NODELAY 选项来关闭这个算法(关闭 Nagle 算法没有全局参数,需要根据每个应用自己的特点来关闭)。

关闭 Nagle 算法关闭 Nagle 算法

那延迟确认又是什么?

事实上当没有携带数据的 ACK,他的网络效率也是很低的,因为它也有 40 个字节的 IP 头 和 TCP 头,但没有携带数据。

为了解决 ACK 传输效率低问题,所以就衍生出了 TCP 延迟确认

TCP 延迟确认的策略:

TCP 延迟确认TCP 延迟确认

延迟等待的时间是在 Linux 内核中的定义的,如下图:

img

关键就需要 HZ 这个数值大小,HZ 是跟系统的时钟频率有关,每个操作系统都不一样,在我的 Linux 系统中 HZ 大小是 1000,如下图:

img

知道了 HZ 的大小,那么就可以算出:

TCP 延迟确认可以在 Socket 设置 TCP_QUICKACK 选项来关闭这个算法。

关闭 TCP 延迟确认关闭 TCP 延迟确认

延迟确认 和 Nagle 算法混合使用时,会产生新的问题

当 TCP 延迟确认 和 Nagle 算法混合使用时,会导致时耗增长,如下图:

TCP 延迟确认 和 Nagle 算法混合使用TCP 延迟确认 和 Nagle 算法混合使用

发送方使用了 Nagle 算法,接收方使用了 TCP 延迟确认会发生如下的过程:

很明显,这两个同时使用会造成额外的时延,这就会使得网络"很慢"的感觉。

要解决这个问题,只有两个办法: