总结向:TCP/IP 网络协议

683 阅读19分钟

这是我参与更文挑战的第19天,活动详情查看:更文挑战

如果❤️我的文章有帮助,欢迎点赞、关注。这是对我继续技术创作最大的鼓励。更多往期文章在我的个人博客

什么是网络协议

网络爬虫,顾名思义就是 行走在互联网间收集信息的爬虫

而互联网则是覆盖各种类型网络设备(如常见的手机、电脑、服务器)的计算机互联网络,那么计算机之间是如何通信、传递信息的呢?

计算机间通信依靠的就是网络协议,而网络爬虫中最常接触到、面试一定会文档的网络协议就是 传输层的 TPC/IP 协议应用层 HTTP 协议

搞清楚他们俩,无论是之后的排查问题还是 吊打面试官 都有莫大的好处,话不多说赶紧开车

HTTP 协议 与 TCP/IP 协议 关系

根据 TCP/IP五层模型 首先要明确两点:

  • TPC/IP 协议是传输层协议,主要讲述 数据如何在网络中传输

  • HTTP 协议是应用层协议,主要讲述 如何打包数据

关于 TCP/IP 和 HTTP 协议的关系,比较通俗的解释:

我们仅使用(传输层)TCP/IP 协议,来源计算机也能传输数据到目标计算机。但被传送的数据是带有特定编码、格式...,如果来源 与 目标计算机不一致将导致数据无法被解析使用。

所以就需要 (应用层)HTTP 等协议规定,传输数据的编译、解析方法保证数据的正常传输使用

“我们在传输数据时,可以只使用(传输层)TCP/IP 协议,但是那样的话,如果没有应用层,便无法识别数据内容,如果想要使传输的数据有意义,则必须使用到应用层协议。



应用层协议有很多,比如 HTTP、FTP、TELNET 等,也可以自己定义应用层协议。WEB 使用 HTTP 协议作应用层协议,以封装 HTTP 文本信息,然后使用 TCP/IP 做传输层协议将它发到网络上。”

所以你能够将 IP 想像成一种高速公路,它允许其它协议在上面行驶并找到到其它电脑的出口。TCP 和 UDP 是高速公路上的“卡车”,它们携带的货物就是像 HTTP,文件传输协议 FTP 这样的协议等 —— 这是来自于一位老司机合格的表述

聊完了他们的关系,接下来就改细说他们中重要(经常问)的知识点

TCP/IP 协议

TCP 三次握手

TCP 三次握手

TCP 的三次握手过程如上。这么做为了确认收发双方的 发送接收 的能力正常。

凡是需要对端确认的,一定消耗TCP报文的序列号。

SYN 需要对端的确认,所以 SYN 需要消耗一个序列号的。而 ACK 并不需要,下次发送对应的 ACK 序列号要加1就可以了

为什么不是两次?

根本原因: 无法确认客户端的接收能力。

引发的问题的步骤如下:

  1. 你发 SYN 报文想握手,结果这个包 滞留 网络中未能到达。

  2. TCP 超时重传机制 以为丢包于是重传,第二次握手建立好了连接。你发送完消息关闭此次链接

  3. 步骤 1 中 滞留 网络的包这时到达了,服务端就默认 建立连接,但是现在客户端已经断开了。

这就是 两次握手 带来的连接资源的浪费。

为什么不是四次?

根本原因: 无法确认最后一次握手肯定能到达,所以没有太大用处

三次握手是为了确认收发双方发送接收的能力。

四次握手也可以,一百次都可以。但你总是没办法确认最后一次握手肯定能到达。

所以三次就足够确认收发双方发送接收的能,你多发的次数没有太大意义。

前二次握手为什么不能携带数据,第三次却可以?

根本原因: 前二次握手就能携带数据的话,收发双方更容易受到攻击

引发的问题的步骤如下:

  1. 你点击了一个未知URL(中奖短信),向一个有毒的服务器请求建立链接

  2. 这时服务器在第二次握手时给你发送大量数据,你势必会消耗更多的时间和内存空间去处理这些数据

或者反过来

  1. 小黑想攻击你的服务器,通过代理在第一次握手中的 SYN 报文中放大量数据

  2. 你的服务器势必会消耗更多的时间和内存空间去处理这些数据

  3. 然后小黑换个代理继续上面操作,你的服务器卒

而第三次握手的时候,客户端已经处于ESTABLISHED状态,并且已经能够确认服务器的接收、发送能力正常,这个时候相对安全了,可以携带数据。

TCP 四次挥手的过程

图片描述

开始时刚开始双方处于ESTABLISHED状态:

第一次挥手:客户端要断开连接,客户端向其 TCP 发出连接 FIN 释放报文,进入 FIN-WAIT-1 状态,无法再发送报文、只能接收。等待服务端的确认。

第二次挥手:服务端收到客户端 FIN 释放报文段后即发出 ACK 确认报文。

  • 服务端进入 CLOSE-WAIT 关闭等待状态,此时的TCP处于半关闭状态。把 未发送数据 传输完毕。

  • 客户端接收到了服务端的 ACK 确认报文,变成了FIN-WAIT2状态。

第三次挥手:当服务端数据传输完毕后,向客户端发送 FIN 释放报文,自己进入 LAST-ACK 状态,

第四次挥手:客户端收到服务端 FIN 释放报文后,发送 ACK 确认报文给服务端。自己进入TIME-WAIT状态,客户端等 2 个 MSL(Maximum Segment Lifetime,报文最大生存时间), 在这段时间内如果客户端没有收到服务端的重发请求,那么表示 ACK 确认报文成功到达服务端,挥手结束,否则客户端重发 ACK 确认报文

为什么是四次挥手?

根本原因:服务端收到 客户端 FIN后需要先发 服务端 ACK 表示已经收到客户端 FIN,然后把剩下报文发送完毕。服务端 FIN 再发送给客户端,这才需要四次挥手

为什么不能是三次挥手?

根本原因:服务端待发送报文数据不定,容易导致客户端不断重发客户端FIN

如果三次握手相当于,把 第二、第三次(上述第3、4步)合并,等待服务端把剩下报文( 数据量 或多或少 )发送完毕 —— 这段时间容易让客户端误认为 客户端FIN 没有到达服务端,而不断重发 客户端FIN

等待2MSL的意义

根本原因:为了保证客户端发送的最后一个ACK报文段能够到达服务器。

MSL 是什么?

每个具体TCP实现必须选择一个报文段最大生存时间MSL(Maximum Segment Lifetime),它是任何报文段被丢弃前在网络内的最长时间。这个时间是有限的,因为TCP报文段以IP数据报在网络内传输,而IP数据报则有限制其生存时间的TTL字段。

为了保证客户端发送的最后一个ACK报文段能够到达服务器。因为这个ACK有可能丢失,从而导致处在LAST-ACK状态的服务器收不到对FIN-ACK的确认报文。服务器会超时重传这个FIN-ACK,接着客户端再重传一次确认,重新启动时间等待计时器( 2MSL )

所以等待 2MSL 分别用来:

  • 1 个 MSL 确保四次挥手中主动关闭方最后的 ACK 报文最终能达到对端

  • 1 个 MSL 确保对端没有收到 ACK 重传的 FIN 报文可以到达

半连接队列 与 SYN Flood 攻击

半连接队列

当客户端发送 SYN 到服务端,服务端返回 SYNACK后,客户端进入 SYN_RCVD 状态,此时这个连接就被推入了SYN队列,也就是半连接队列。

全连接队列

当三次握手完成后,连接会被推入另外一个 TCP 维护的队列,也就是全连接队列(Accept Queue),等待被具体的应用取走。

SYN Flood 攻击原理

客户端在短时间内伪造大量不存在的 IP 地址,并疯狂发送 SYN 给目标服务器。导致:

  • 大量连接处于 SYN_RCVD 状态,占满整个半连接队列,无法处理正常的请求。

  • 大量连接使用不存在的 IP,服务端长时间收不到 客户端ACK,会导致服务端不断重发报文,直到服务端资源耗尽。

怎样发现自己处于被攻击状态

  • 访问服务端被拒绝或超时,无法提供正常的TCP服务

  • 通过 netstat -an 命令检查系统,发现有大量的SYN_RECV连接状态

如何应对 SYN Flood 攻击?

  • 短时间内连续受到某个IP的重复SYN报文,就认定是受到了攻击,并记录地址信息封 IP,可能会影响客户的正常访问。

  • 缩短SYN Timeout时间,缩短占用半连接队列时间,可以成倍的降低服务器的负荷。但过低的SYN Timeout设置会影响客户的正常访问。

  • 利用 SYN Cookie 技术,第二次握手带上Cookie回复给客户端,在客户端回复ACK的时候带上这个Cookie值,服务端验证 Cookie 合法之后才分配连接资源。

  • 使用SYN Proxy 防火墙对试图穿越的SYN请求进行验证后才放行。

TCP报文中时间戳的作用

timestamp是 TCP 报文首部的一个可选项,一共占 10 个字节,格式如下:

kind(1 字节) + length(1 字节) + info(8 个字节)

其中 kind = 8, length = 10, info 有两部分构成: timestamptimestamp echo,各占 4 个字节。

那么这些字段都是干嘛的呢?它们用来解决那些问题?

接下来我们就来一一梳理,TCP 的时间戳主要解决两大问题:

  • 计算往返时延 RTT(Round-Trip Time)

  • 防止序列号的回绕问题

计算往返时延 RTT

在没有时间戳的时候,计算 RTT 会遇到的问题如下图所示:

图片描述

如果以第一次发包为开始时间的话,就会出现左图的问题,RTT 明显偏大,开始时间应该采用第二次的;

如果以第二次发包为开始时间的话,就会导致右图的问题,RTT 明显偏小,开始时间应该采用第一次发包的。

实际上无论开始时间以第一次发包还是第二次发包为准,都是不准确的。

那这个时候引入时间戳就很好的解决了这个问题。

比如现在 a 向 b 发送一个报文 s1,b 向 a 回复一个含 ACK 的报文 s2 那么:

  • step 1: a 向 b 发送的时候,timestamp 中存放的内容就是 a 主机发送时的内核时刻 ta1。

  • step 2: b 向 a 回复 s2 报文的时候,timestamp 中存放的是 b 主机的时刻 tb, timestamp echo字段为从 s1 报文中解析出来的 ta1。

  • step 3: a 收到 b 的 s2 报文之后,此时 a 主机的内核时刻是 ta2, 而在 s2 报文中的 timestamp echo 选项中可以得到 ta1, 也就是 s2 对应的报文最初的发送时刻。然后直接采用 ta2 - ta1 就得到了 RTT 的值

防止序列号回绕问题

说的是 seq 序列号是 有范围的 序列号的范围其实是在0 ~ 2 ^ 32 - 1,

如果上一个相同序列号发包滞留在网络,等下一个 相当序列表发包发出时,到达接收端会造成混乱

用 timestamp 就能很好地解决这个问题,因为每次发包的时候都是将发包机器当时的内核时间记录在报文中,那么两次发包序列号即使相同,时间戳也不可能相同,这样就能够区分开两个数据包了。

糊涂窗口综合症

如果由于大量负载的原因,接收端处理不了这么多字节,,来不及取走接收窗口里的数据,那么就会导致发送方的发送窗口越来越小。

到最后,如果接收方腾出几个字节并告诉发送方现在有几个字节的窗口,而发送方会义无反顾地发送这几个字节,这就是糊涂窗口综合症。

要知道,我们的 TCP + IP 头有 40 个字节,为了传输那几个字节的数据,要达上这么大的开销,这太不经济了。

所以,糊涂窗口综合症的现象是可以发生在发送方和接收方:

  • 接收方可以通告一个小的窗口

  • 而发送方可以发送小数据

于是,要解决糊涂窗口综合症,就解决上面两个问题就可以了

  • 让接收方不通告小窗口给发送方

  • 使用 Nagle 算法让发送方避免发送小数据

TCP 滑动窗口 与 流量控制

TCP 滑动窗口

TCP 滑动窗口分为 接收窗口发送窗口 它们的构成如下:

图片描述

说说我对滑动窗口的理解:

  1. 窗口 是指一段可以被 发送方 发送的字节序列,其连续的范围称之为“窗口”;

  2. 滑动 是指这段 允许发送的范围 是可以随着发送的过程而变化的,方式就是按顺序“滑动”。

滑动窗口 表示的是 发送方接收方 的数据传输能力,本质其实是一种 传输层流量控制 措施 —— 接收方 通告 发送方 自己的窗口大小,从而控制 发送方 的发送速度,防止发送速度过快而导致自己被淹没的目的

接下来我们举个例子描述下 TCP 滑动窗口的实现过程

TCP 流量控制的实现过程

首先 TCP 三次握手完成,发送方 与 接收方 建立连接。

  1. 接收方 通告 发送方,自己的 期待接收数据包序列号 ACK = 0、接收窗口大小 SIZE = 10。发送方 根据 ACK、SIZE 构建自己的 发送窗口

图片描述

  1. 发送方 依次发送 1~10 序列号数据包,假设接收方 接收到 1~35~10 序列号数据包、未接收到 4 序列号数据包

  2. 接收方 回复一个 ACK 包说明已经接收到了 1~3 序列号数据包,并将 5~10 进行缓存(保证顺序,产生一个保存 4 序列号数据包 的hole)

  3. 发送方 收到 ACK 之后,就会将 1~3 序列号数据包 从 已发送、待确认 切到 已发送、已确认。假设 接收方 通告 SIZE 仍然不变,此时窗口右移,产生一些新的空位,这些是接收端允许发送的范畴

图片描述

  1. 对于丢失的 4 序列号数据包,如果超过一定时间,TCP就会重新传送(重传机制),重传成功会 45~10一块被确认;不成功,5~10也将被丢弃

不断重复着上述 5 步,随着窗口不断滑动,将整个数据流发送到接收端,实际上 接收方 的 窗口大小(SIZE) 通告也是会变化的,发送方 根据这个值来确定何时及发送多少数据,从对数据流进行流控。原理图如下图所示:

图片 来源于 The TCP/IP Guide

拥塞窗口 与 拥塞控制

这里还有一个知识点,上面说到的 滑动窗口与流量控制 说的是发送、接收两天设备的流量问题。

但真实情况是,发送方与接收方见隔着 不定数的路由、交换机、网络设备

流量控制可以避免 发送方 的数据占满 接收方 的缓存,然而并未顾及网络的中的情况。那么一旦网络发生拥堵,如果继续发送大量数据包,延时、丢失、TCP 超时重传将进一步加剧网络拥堵陷入恶性循环。

为了避免该情况,TCP 协议就有了 拥塞窗口拥塞控制 -- 顾及中间设备的传输能力、网络情况 的调整方法,

拥塞窗口 与 滑动窗口关系

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

拥塞窗口 cwnd 变化规则如下:

  • 网络没有出现拥堵 cwnd 增大

  • 网络出现拥堵 cwnd 减小

因此上述提及的 发送(滑动)窗口 swnd接收(滑动)窗口 rwnd 的实际关系是:

swnd = min(cwnd, rwnd) # 拥塞窗口 与 接收(滑动)窗口 的最小值

而当 发送方 发生超时重传,就会默认为网络出现拥塞。

既然 拥堵出现 了,总得维护嘛。不然一直堵下去也不是办法。下面举例一下拥塞控制的控制算法

拥塞控制算法

拥塞控制算法主要有四种:

  • 慢启动

  • 拥塞避免

  • 拥塞发生

  • 快速恢复

慢启动

TCP 三次握手建立连接后,发送方会 一点点 提高发送数据包的数量的 慢启动 过程,去试探目前网络是否拥塞。

慢启动有一个很简单的规则:

收发双方初始化自己的 拥塞窗口 cwnd 值后。发送方每收到一个 ACK,拥塞窗口 cwnd 的值就会 加 1。

但它也不会无限 加下去(不然还是会拥塞),拥塞窗口 cwnd 到达 慢启动门限 ssthresh (slow start threshold) 后就交由下面的 拥塞避免 算法控制 cwnd 的大小

  • 当 cwnd < ssthresh 时,使用 慢启动算法

  • 当 cwnd >= ssthresh 时,就会使用 拥塞避免算法

一般来说 ssthresh 的大小为 65535 字节。

拥塞避免

当拥塞窗口 cwnd 到达 慢启动门限 ssthresh,则自动进入 拥塞避免算法 的规则是:

每当收到一个 ACK 时,cwnd 增加 1/cwnd。

也就是一轮 往返时间 RTT 下来,收到 cwnd 个 ACK,最后拥塞窗口的大小总共才 加 1。

快速重传

TCP 传输过程中,如果发生了丢包,即 接收端发现数据段不是按序到达 的时候,接收端的处理是重复发送 之前一个包的 ACK 确认报文

当接收方发现丢了一个中间包的时候,发送 之前一个包的 ACK,于是发送端 意识到丢包 就会快速地重传,不必等待一个 超时重传时间 RTO (Retransmission TimeOut) 的时间到了才重传。

TCP 认为这种情况不严重,因为大部分没丢,只丢了一小部分,则 ssthresh 和 cwnd 变化如下:

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

  • ssthresh = cwnd

  • 进入快速恢复算法

选择性重传

那你可能会问了,既然要重传,那么只重传 “丢失的数据包” 还是 “从丢失数据包开始都重传” 呢?

丢包的时候,TCP 的设计是 接收端 在下一个回复的 ACK 报文中可以加上 SACK 这个属性,记录一下哪些包到了,哪些没到,针对性地重传。

发送端在收到服务端的 ACK 报文后,会解析里面 SACK 这个属性中 Left Edge(已收到的不连续块的第一个序号) 和 Right Edge(已收到的不连续块的最后一个序号+1)就知道 选择性重传 丢失的数据包

这就是选择性重传 SACK(,Selective Acknowledgment)

SACK 结构如下:

图片描述

快速恢复

上面 拥塞窗口 与 滑动窗口关系 我们说到 发送方 发生超时重传,就会认为网络出现拥塞。

这是就会进入 快速回复 状态,发送端会产生如下改变:

  • cwnd = cwnd/2

  • cwnd 开始线性增加

拥塞控制主要四种算法,在这里就全部说完啦

Nagle 算法 与 延迟确认

Nagle 算法

试想一个场景,发送端不停地给接收端发很小的数据包,每个 1 字节,发 1 千个字节需要发 1000 次。这种频繁的发送是存在问题的,不光是传输时延 RTT 消耗,发送和确认本身也是需要耗时的,频繁的发送接收带来了巨大的时延。

Nagle 算法就是为了避免小包的频繁发送,其规则如下:

  • 当第一次发送数据时不用等待,就算是 1byte 的小包也立即发送

  • 后面发送满足下面条件之一就可以发了:

  • 数据包大小达到最大段大小(Max Segment Size, 即 MSS)

  • 之前所有包的 ACK 都已接收到

延迟确认

试想这样一个场景,当我收到了发送端的一个包,然后在极短的时间内又接收到了第二个包,是一个个地回复,还是稍微等一下,把两个包的 ACK 合并后一起回复呢?

延迟确认(delayed ack)这是就会介入,稍稍延迟后合并 ACK,最后才回复给发送端。

TCP 要求这个延迟的时延必须小于500ms,一般操作系统实现都不会超过200ms。

不过有些场景是不能延迟确认,收到了就要马上回复:

  • 接收到了大于一个 frame 的报文,且需要调整窗口大小

  • TCP 处于 quickack 模式(通过tcp_in_quickack_mode设置)

  • 发现了乱序包

需要注意的是:

Nagle 算法 意味着延迟发,延迟确认 意味着延迟接收,两者一起使用会造成更大的延迟,是会产生性能问题。