如果❤️我的文章有帮助,欢迎点赞、关注。这是对我继续技术创作最大的鼓励。[更多往期文章在我的个人博客] coderdao.github.io/
什么是网络协议
网络爬虫,顾名思义就是 行走在互联网间收集信息的爬虫
;
而互联网则是覆盖各种类型网络设备(如常见的手机、电脑、服务器)的计算机互联网络,那么计算机之间是如何通信、传递信息的呢?
计算机间通信依靠的就是[网络协议]。
而网络爬虫中最常接触到、面试一定会文档的网络协议就是 传输层的 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报文的序列号。
SYN 需要对端的确认,所以 SYN 需要消耗一个序列号的。而 ACK 并不需要,下次发送对应的 ACK 序列号要加1就可以了
为什么不是两次?
根本原因: 无法确认客户端的接收能力。
引发的问题的步骤如下:
-
你发
SYN
报文想握手,结果这个包滞留
网络中未能到达。 -
TCP 超时重传机制
以为丢包于是重传,第二次握手建立好了连接。你发送完消息关闭此次链接 -
步骤 1 中
滞留
网络的包这时到达了,服务端就默认建立连接
,但是现在客户端已经断开了。
这就是 两次握手
带来的连接资源的浪费。
为什么不是四次?
根本原因: 无法确认最后一次握手肯定能到达,所以没有太大用处
三次握手是为了确认收发双方发送
和接收
的能力。
四次握手也可以,一百次都可以。但你总是没办法确认最后一次握手肯定能到达。
所以三次就足够确认收发双方发送
和接收
的能,你多发的次数没有太大意义。
前二次握手为什么不能携带数据,第三次却可以?
根本原因: 前二次握手就能携带数据的话,收发双方更容易受到攻击
引发的问题的步骤如下:
-
你点击了一个未知URL(中奖短信),向一个有毒的服务器请求建立链接
-
这时服务器在第二次握手时给你发送大量数据,你势必会消耗更多的时间和内存空间去处理这些数据
或者反过来
-
小黑想攻击你的服务器,通过代理在第一次握手中的 SYN 报文中放大量数据
-
你的服务器势必会消耗更多的时间和内存空间去处理这些数据
-
然后小黑换个代理继续上面操作,你的服务器卒
而第三次握手的时候,客户端已经处于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
到服务端,服务端返回 SYN
和 ACK
后,客户端进入 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 有两部分构成: timestamp
和timestamp 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 滑动窗口分为 接收窗口
、发送窗口
它们的构成如下:
说说我对滑动窗口的理解:
-
窗口
是指一段可以被发送方
发送的字节序列,其连续的范围称之为“窗口”; -
滑动
是指这段允许发送的范围
是可以随着发送的过程而变化的,方式就是按顺序“滑动”。
滑动窗口 表示的是 发送方
和 接收方
的数据传输能力,本质其实是一种 传输层流量控制
措施 —— 接收方
通告 发送方
自己的窗口大小,从而控制 发送方
的发送速度,防止发送速度过快而导致自己被淹没的目的
接下来我们举个例子描述下 TCP 滑动窗口的实现过程
TCP 流量控制的实现过程
首先 TCP 三次握手完成,发送方 与 接收方 建立连接。
- 接收方 通告 发送方,自己的
期待接收数据包序列号 ACK
= 0、接收窗口大小 SIZE
= 10。发送方 根据 ACK、SIZE 构建自己的 发送窗口
-
发送方 依次发送 1~10 序列号数据包,假设接收方 接收到
1~3
、5~10
序列号数据包、未接收到4 序列号数据包
-
接收方 回复一个 ACK 包说明已经接收到了
1~3
序列号数据包,并将5~10
进行缓存(保证顺序,产生一个保存4
序列号数据包 的hole) -
发送方 收到 ACK 之后,就会将
1~3
序列号数据包 从已发送、待确认
切到已发送、已确认
。假设 接收方 通告 SIZE 仍然不变,此时窗口右移,产生一些新的空位,这些是接收端允许发送的范畴
- 对于丢失的
4
序列号数据包,如果超过一定时间,TCP就会重新传送(重传机制),重传成功会4
、5~10
一块被确认;不成功,5~10
也将被丢弃
不断重复着上述 5 步,随着窗口不断滑动,将整个数据流发送到接收端,实际上 接收方 的 窗口大小(SIZE)
通告也是会变化的,发送方 根据这个值来确定何时及发送多少数据,从对数据流进行流控。原理图如下图所示:
拥塞窗口 与 拥塞控制
这里还有一个知识点,上面说到的 滑动窗口与流量控制
说的是发送、接收两天设备的流量问题。
但真实情况是,发送方与接收方见隔着 不定数的路由、交换机、网络设备
。
流量控制可以避免 发送方
的数据占满 接收方
的缓存,然而并未顾及网络的中的情况。那么一旦网络发生拥堵,如果继续发送大量数据包,延时、丢失、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 算法
意味着延迟发,延迟确认
意味着延迟接收,两者一起使用会造成更大的延迟,是会产生性能问题。