TCP 作为传输层的协议,是一个软件工程师素养的体现,也是面试中经常被问到的知识点。在此,我将 TCP 核心的一些问题梳理了一下,希望能帮到各位。
001. 能不能说一说 TCP 和 UDP 的区别?
首先概括一下基本的区别:
TCP是一个面向连接的、可靠的、基于字节流的传输层协议。
而
UDP是一个面向无连接的传输层协议。
(就这么简单,其它TCP的特性也就没有了)。
具体来分析,和
UDP
相比,
TCP
有三大核心特性:
-
面向连接
。所谓的连接,指的是客户端和服务器的连接,在双方互相通信之前,TCP 需要三次握手建立连接,而 UDP 没有相应建立连接的过程。
-
可靠性
。TCP 花了非常多的功夫保证连接的可靠,这个可靠性体现在哪些方面呢?一个是有状态,另一个是可控制。
TCP 会精准记录哪些数据发送了,哪些数据被对方接收了,哪些没有被接收到,而且保证数据包按序到达,不允许半点差错。这是
有状态
。
当意识到丢包了或者网络环境不佳,TCP 会根据具体情况调整自己的行为,控制自己的发送速度或者重发。这是
可控制
。
相应的,UDP 就是
无状态
,
不可控
的。
-
面向字节流
。UDP 的数据传输是基于数据报的,这是因为仅仅只是继承了 IP 层的特性,而 TCP 为了维护状态,将一个个 IP 包变成了字节流。
002: 说说 TCP 三次握手的过程?为什么是三次而不是两次、四次?
恋爱模拟
以谈恋爱为例,两个人能够在一起最重要的事情是首先确认各自
爱
和
被爱
的能力。接下来我们以此来模拟三次握手的过程。
第一次:
男:
我爱你。
女方收到。
由此证明男方拥有
爱
的能力。
第二次:
女:
我收到了你的爱,我也爱你。
男方收到。
OK,现在的情况说明,女方拥有
爱
和
被爱
的能力。
第三次:
男:
我收到了你的爱。
女方收到。
现在能够保证男方具备
被爱
的能力。
由此完整地确认了双方
爱
和
被爱
的能力,两人开始一段甜蜜的爱情。
真实握手
当然刚刚那段属于扯淡,不代表本人价值观,目的是让大家理解整个握手过程的意义,因为两个过程非常相似。对应到 TCP 的三次握手,也是需要确认双方的两样能力:
发送的能力
和
接收的能力
。于是便会有下面的三次握手的过程:
从最开始双方都处于
CLOSED
状态。然后服务端开始监听某个端口,进入了
LISTEN
状态。
然后客户端主动发起连接,发送 SYN , 自己变成了
SYN-SENT
状态。
服务端接收到,返回
SYN
和
ACK
(对应客户端发来的SYN),自己变成了
SYN-REVD
。
之后客户端再发送
ACK
给服务端,自己变成了
ESTABLISHED
状态;服务端收到
ACK
之后,也变成了
ESTABLISHED
状态。
另外需要提醒你注意的是,从图中可以看出,SYN 是需要消耗一个序列号的,下次发送对应的 ACK 序列号要加1,为什么呢?只需要记住一个规则:
凡是需要对端确认的,一定消耗TCP报文的序列号。
SYN 需要对端的确认, 而 ACK 并不需要,因此 SYN 消耗一个序列号而 ACK 不需要。
为什么不是两次?
根本原因: 无法确认客户端的接收能力。
分析如下:
如果是两次,你现在发了 SYN 报文想握手,但是这个包
滞留
在了当前的网络中迟迟没有到达,TCP 以为这是丢了包,于是重传,两次握手建立好了连接。
看似没有问题,但是连接关闭后,如果这个
滞留
在网路中的包到达了服务端呢?这时候由于是两次握手,服务端只要接收到然后发送相应的数据包,就默认
建立连接
,但是现在客户端已经断开了。
看到问题的吧,这就带来了连接资源的浪费。
为什么不是四次?
三次握手的目的是确认双方
发送
和
接收
的能力,那四次握手可以嘛?
当然可以,100 次都可以。但为了解决问题,三次就足够了,再多用处就不大了。
三次握手过程中可以携带数据么?
第三次握手的时候,可以携带。前两次握手不能携带数据。
如果前两次握手能够携带数据,那么一旦有人想攻击服务器,那么他只需要在第一次握手中的 SYN 报文中放大量数据,那么服务器势必会消耗更多的
时间
和
内存空间
去处理这些数据,增大了服务器被攻击的风险。
第三次握手的时候,客户端已经处于
ESTABLISHED
状态,并且已经能够确认服务器的接收、发送能力正常,这个时候相对安全了,可以携带数据。
同时打开会怎样?
如果双方同时发
SYN
报文,状态变化会是怎样的呢?
这是一个可能会发生的情况。
状态变迁如下:
在发送方给接收方发
SYN
报文的同时,接收方也给发送方发
SYN
报文,两个人刚上了!
发完
SYN
,两者的状态都变为
SYN-SENT
。
在各自收到对方的
SYN
后,两者状态都变为
SYN-REVD
。
接着会回复对应的
ACK + SYN
,这个报文在对方接收之后,两者状态一起变为
ESTABLISHED
。
这就是同时打开情况下的状态变迁。
003: 说说 TCP 四次挥手的过程
过程拆解
刚开始双方处于
ESTABLISHED
状态。
客户端要断开了,向服务器发送
FIN
报文,在 TCP 报文中的位置如下图:
发送后客户端变成了
FIN-WAIT-1
状态。注意, 这时候客户端同时也变成了
half-close(半关闭)
状态,即无法向服务端发送报文,只能接收。
服务端接收后向客户端确认,变成了
CLOSED-WAIT
状态。
客户端接收到了服务端的确认,变成了
FIN-WAIT2
状态。
随后,服务端向客户端发送
FIN
,自己进入
LAST-ACK
状态,
客户端收到服务端发来的
FIN
后,自己变成了
TIME-WAIT
状态,然后发送 ACK 给服务端。
注意了,这个时候,客户端需要等待足够长的时间,具体来说,是 2 个
MSL
(
Maximum Segment Lifetime,报文最大生存时间
), 在这段时间内如果客户端没有收到服务端的重发请求,那么表示 ACK 成功到达,挥手结束,否则客户端重发 ACK。
等待2MSL的意义
如果不等待会怎样?
如果不等待,客户端直接跑路,当服务端还有很多数据包要给客户端发,且还在路上的时候,若客户端的端口此时刚好被新的应用占用,那么就接收到了无用数据包,造成数据包混乱。所以,最保险的做法是等服务器发来的数据包都死翘翘再启动新的应用。
那,照这样说一个 MSL 不就不够了吗,为什么要等待 2 MSL?
-
1 个 MSL 确保四次挥手中主动关闭方最后的 ACK 报文最终能达到对端
-
1 个 MSL 确保对端没有收到 ACK 重传的 FIN 报文可以到达
这就是等待 2MSL 的意义。
为什么是四次挥手而不是三次?
因为服务端在接收到
FIN
, 往往不会立即返回
FIN
, 必须等到服务端所有的报文都发送完毕了,才能发
FIN
。因此先发一个
ACK
表示已经收到客户端的
FIN
,延迟一段时间才发
FIN
。这就造成了四次挥手。
如果是三次挥手会有什么问题?
等于说服务端将
ACK
和
FIN
的发送合并为一次挥手,这个时候长时间的延迟可能会导致客户端误以为
FIN
没有到达客户端,从而让客户端不断的重发
FIN
。
同时关闭会怎样?
如果客户端和服务端同时发送 FIN ,状态会如何变化?如图所示:
004: 说说半连接队列和 SYN Flood 攻击的关系
三次握手前,服务端的状态从
CLOSED
变为
LISTEN
, 同时在内部创建了两个队列:
半连接队列
和
全连接队列
,即
SYN队列
和
ACCEPT队列
。
半连接队列
当客户端发送
SYN
到服务端,服务端收到以后回复
ACK
和
SYN
,状态由
LISTEN
变为
SYN_RCVD
,此时这个连接就被推入了
SYN队列
,也就是
半连接队列
。
全连接队列
当客户端返回
ACK
, 服务端接收后,三次握手完成。这个时候连接等待被具体的应用取走,在被取走之前,它会被推入另外一个 TCP 维护的队列,也就是
全连接队列(Accept Queue)
。
SYN Flood 攻击原理
SYN Flood 属于典型的 DoS/DDoS 攻击。其攻击的原理很简单,就是用客户端在短时间内伪造大量不存在的 IP 地址,并向服务端疯狂发送
SYN
。对于服务端而言,会产生两个危险的后果:
-
处理大量的
SYN
包并返回对应
ACK
, 势必有大量连接处于
SYN_RCVD
状态,从而占满整个
半连接队列
,无法处理正常的请求。
-
由于是不存在的 IP,服务端长时间收不到客户端的
ACK
,会导致服务端不断重发数据,直到耗尽服务端的资源。
如何应对 SYN Flood 攻击?
-
-
减少 SYN + ACK 重试次数,避免大量的超时重发。
-
利用 SYN Cookie 技术,在服务端接收到
SYN
后不立即分配连接资源,而是根据这个
SYN
计算出一个Cookie,连同第二次握手回复给客户端,在客户端回复
ACK
的时候带上这个
Cookie
值,服务端验证 Cookie 合法之后才分配连接资源。
005: 介绍一下 TCP 报文头部的字段
报文头部结构如下(单位为字节):
请大家牢记这张图!
源端口、目标端口
如何标识唯一标识一个连接?答案是 TCP 连接的
四元组
——源 IP、源端口、目标 IP 和目标端口。
那 TCP 报文怎么没有源 IP 和目标 IP 呢?这是因为在 IP 层就已经处理了 IP 。TCP 只需要记录两者的端口即可。
序列号
即
Sequence number
, 指的是本报文段第一个字节的序列号。
从图中可以看出,序列号是一个长为 4 个字节,也就是 32 位的无符号整数,表示范围为 0 ~ 2^32 - 1。如果到达最大值了后就循环到0。
序列号在 TCP 通信的过程中有两个作用:
-
-
ISN
即
Initial Sequence Number(初始序列号)
,在三次握手的过程当中,双方会用过
SYN
报文来交换彼此的
ISN
。
ISN 并不是一个固定的值,而是每 4 ms 加一,溢出则回到 0,这个算法使得猜测 ISN 变得很困难。那为什么要这么做?
如果 ISN 被攻击者预测到,要知道源 IP 和源端口号都是很容易伪造的,当攻击者猜测 ISN 之后,直接伪造一个 RST 后,就可以强制连接关闭的,这是非常危险的。
而动态增长的 ISN 大大提高了猜测 ISN 的难度。
确认号
即
ACK(Acknowledgment number)
。用来告知对方下一个期望接收的序列号,
小于ACK
的所有字节已经全部收到。
标记位
常见的标记位有
SYN
,
ACK
,
FIN
,
RST
,
PSH
。
SYN 和 ACK 已经在上文说过,后三个解释如下:
FIN
:即 Finish,表示发送方准备断开连接。
RST
:即 Reset,用来强制断开连接。
PSH
:即 Push, 告知对方这些数据包收到后应该马上交给上层的应用,不能缓存。
窗口大小
占用两个字节,也就是 16 位,但实际上是不够用的。因此 TCP 引入了窗口缩放的选项,作为窗口缩放的比例因子,这个比例因子的范围在 0 ~ 14,比例因子可以将窗口的值扩大为原来的 2 ^ n 次方。
校验和
占用两个字节,防止传输过程中数据包有损坏,如果遇到校验和有差错的报文,TCP 直接丢弃之,等待重传。
可选项
可选项的格式如下:
常用的可选项有以下几个:
-
TimeStamp: TCP 时间戳,后面详细介绍。
-
MSS: 指的是 TCP 允许的从对方接收的最大报文段。
-
-
006: 说说 TCP 快速打开的原理(TFO)
第一节讲了 TCP 三次握手,可能有人会说,每次都三次握手好麻烦呀!能不能优化一点?
可以啊。今天来说说这个优化后的 TCP 握手流程,也就是 TCP 快速打开(TCP Fast Open, 即TFO)的原理。
优化的过程是这样的,还记得我们说 SYN Flood 攻击时提到的 SYN Cookie 吗?这个 Cookie 可不是浏览器的
Cookie
, 用它同样可以实现 TFO。
TFO 流程
首轮三次握手
首先客户端发送
SYN
给服务端,服务端接收到。
注意哦!现在服务端不是立刻回复 SYN + ACK,而是通过计算得到一个
SYN Cookie
, 将这个
Cookie
放到 TCP 报文的
Fast Open
选项中,然后才给客户端返回。
客户端拿到这个 Cookie 的值缓存下来。后面正常完成三次握手。
首轮三次握手就是这样的流程。而后面的三次握手就不一样啦!
后面的三次握手
在后面的三次握手中,客户端会将之前缓存的
Cookie
、
SYN
和
HTTP请求
(是的,你没看错)发送给服务端,服务端验证了 Cookie 的合法性,如果不合法直接丢弃;如果是合法的,那么就正常返回
SYN + ACK
。
重点来了,现在服务端能向客户端发 HTTP 响应了!这是最显著的改变,三次握手还没建立,仅仅验证了 Cookie 的合法性,就可以返回 HTTP 响应了。
当然,客户端的
ACK
还得正常传过来,不然怎么叫三次握手嘛。
流程如下:
注意: 客户端最后握手的 ACK 不一定要等到服务端的 HTTP 响应到达才发送,两个过程没有任何关系。
TFO 的优势
TFO 的优势并不在与首轮三次握手,而在于后面的握手,在拿到客户端的 Cookie 并验证通过以后,可以直接返回 HTTP 响应,充分利用了
1 个RTT
(Round-Trip Time,往返时延)的时间
提前进行数据传输
,积累起来还是一个比较大的优势。