TCP协议
本文参考了:TCP协议灵魂之问
简介
TCP 是一个面向连接的、可靠的、基于字节流的传输层协议
面向连接:指的是客户端和服务器的连接,在双方互相通信之前,TCP 需要三次握手建立连接
可靠:
- 有状态:TCP 会精准记录哪些数据发送了,哪些数据被对方接收了,哪些没有被接收到,而且保证数据包按序到达,不允许半点差错
- 可控制:当意识到丢包了或者网络环境不佳,TCP 会根据具体情况调整自己的行为,控制自己的发送速度或者重发
基于字节流:TCP 为了维护状态,将一个个 IP 包变成了字节流
建立连接
老生常谈的 TCP 建立连接的三次握手

第一次握手
客户端向服务端发问,询问是否准备好建立连接
客户端、服务端状态
CLOSED服务端状态监听某个端口,状态变为
LISTEN客户端发送
SYN和序列号seq = x,状态变为SYN-SENT
第二次握手
服务端回答客户端准备好建立连接了,并询问是否做好了准备建立连接
服务端接收到
SYN和序列号seq = x服务端发送
ACK和确认号ack = x + 1以回应客户端的SYN;同时发送SYN和序列号seq = y确认客户端状态服务端状态变为
SYN-RCVD
第三次握手
客户端回应服务端我好好的,可以开始传输数据了
客户端接收到服务端的回应
客户端发送
ACK回应服务端的SYN,同时带上了seq = x + 1,ack = y + 1客户端状态变为
ESTABLISHED服务端接收到客户端回应,状态变为
ESTABLISHED
为什么不是两次?
假设客户端发送请求建立连接 A,因为网络延迟超时了,重新发送请求建立了连接 B,数据交互完成断开了连接 B
但是,建立连接 A 的请求在这之后到达了服务端,因为只有两次握手,服务端接收到 SYN 并返回 ACK,SYN 后会判断建立了连接,但实际上这时客户端没有在维护这个连接,会造成资源浪费
如有第三次握手,那么服务端在发送 ACK,SYN 后,还需要客户端回应才会建立连接,客户端长时间没有回应,该连接就超时作废了,不会浪费资源
断开连接
需要进行四次挥手

第一次挥手
客户当告诉服务端我想要断开连接
客户端、服务端状态为
ESTABLISHED客户端发送
FIN和序列号seq = p,状态变为FIN-WAIT-1等待断开连接第一状态
第二次挥手
服务端告诉客户端我收到了,让我发完剩余的数据再通知你
服务端接收到
FIN和序列号seq = p服务端发送
ACK回应FIN,并附带确认号ack = p + 1服务端状态变为
CLOSED-WAIT等待关闭状态,继续发送之前连接时还未发送完的数据客户端收到
ACK和确认号ack = p + 1,状态变为FIN-WAIT-2等待断开连接第二状态
第三次挥手
服务端处理完剩余数据,通知客户端我要关闭连接了
服务端发送
FIN,ACK,并附带序列号seq = q和确认号ack = p + 1服务端状态变为
LAST-ACK,等待客户端的最后一次确认
第四次挥手
客户端接收到服务端的关闭通知后,等待 2MSL 时间后,回答服务端知道了
客户端接收到
FIN,ACK,序列号seq = q和确认号ack = p + 1客户端状态变为
TIME-WAIT,进入时间等待状态客户端等待
2MSL时长后,发送ACK和确认号ack = q + 1,状态变为CLOSED服务端接收到回答后,状态变为
CLOSED
为什么不是三次?
三次一般指的是第二、三次合并,服务端收到 FIN 后等待剩余数据传输完成一次性返回 ACK,FIN 给客户端
这里会造成一个问题,如果剩余数据量过大,太长时间没有回应客户端的 FIN,客户端可能会认为超时并不断重复发送 FIN,造成资源上的浪费
等待 2MSL 的意义
我们每次发送报文都需要时间,MSL (Maximum Segment Lifetime,报文最大生存时间)
等待过程中,如果没有收到服务端的重发请求,那么表示 ACK 成功到达,挥手结束,否则客户端重发 ACK
第一个 MSL 为了保证客户端的
ACK肯定能到达服务端第二个 MSL 为了保证服务端如果没有收到
ACK的情况下,服务端重新传递给客户端的FIN肯定能到达
半连接、全连接队列
服务端保存连接的队列
半连接队列:第二次握手后的连接会推入其中
全连接队列:第三次握手后的连接会推入其中
SYN Flood 攻击 (DDoS)
SYN Flood 属于典型的 DoS/DDoS 攻击
原理很简单,就是伪造大量的不存在的 IP 地址,接着在短时间内向服务器发送大量 SYN(第一次握手请求),服务器会因此出现以下问题:
收到大量的
SYN接着回应ACK,会产生大量的SYN_RCVD状态的连接,会占满半连接队列,服务器难以处理到正常请求因为是不存在的 IP,服务器收不到
ACK,会导致服务器不停的重试发送数据,占用服务器资源
TCP 报文头部字段
报文头部结构如图所示

源端口、目标端口
TCP 协议通过源 IP、目标 IP、源端口、目标端口来唯一标识一个连接
TCP 报文头部中只有端口信息而没有 IP,这是因为 IP 层已经处理了 IP,TCP 只需要知道端口就行
序列号
即 Sequence number, 指的是本报文段第一个字节的序列号
序列号长度为 4 字节,是 32 位无符号整数,范围 0 ~ 2^32 - 1,达到最大值循环至 0
序列号用于保证数据包按正确的顺序组装
TCP 会在 SYN 报文中互相交互双端的初始序列号 Initial Sequence Number(ISN)
ISN 并不是固定的值,它每 4ms 会加一,溢出回到 0,这样做是为了增加 TCP 攻击的难度
确认号
即 ack(Acknowledgment number),用来告知对方下一个期望接收的序列号,小于 ack 的所有字节已经全部收到,用于确保数据是按顺序传输的
前面第二次握手时,服务端发送给客户端的 ack = x + 1 就是这个作用,告诉客户端序列号小于 x + 1 的数据包已经处理好了,你可以发送序列号为 x + 1 的数据包了
标记位
用于标记该报文的作用、通信目的
常见的有 SYN,ACK,FIN,RST,PSH
SYN、ACK、FIN 在握手里体现的比较清楚了,但还是简单解释下:
SYN:用于确认对方状态,同步ISN(初始序列号)ACK:用于回应对方的请求RST:Reset,用于强制断开连接PSH:Push,告知对方这些数据包收到后应该马上交给上层的应用,不能缓存
窗口大小
发送、接收数据的缓冲区大小,用来进行流量控制
图里显示占用 2 字节 16 位,实际这样是不够的,所以 TCP 引入了窗口缩放,能动态调整窗口大小
校验和
用于校验数据是否有差错,防止传输中被损坏的数据被接收,如果遇到校验有错的报文,会直接丢弃,等待对方重新发送
可选项
可选项结构如下

常用可选项有这些:
TimeStamp: TCP 时间戳,后面介绍
MSS: 指的是 TCP 允许的从对方接收的最大报文段
SACK: 选择确认选项
Window Scale: 窗口缩放选项
时间戳
时间戳 timestamp 为可选项
长度 10 字节,除了可选结构中的 kind, length 之外,其本身信息占 8 字节
计算往返延迟 RTT
例子一:发送一个数据包,因为网络原因超时,发送了第二次,接着服务端响应了
ACK,这种情况下是用第一次的发送时间还是第二次的发送时间来计算 RTT 呢?例子二:发送一个数据包,因为网络原因超时,发送了第二次,但是就在很短的时间后,服务端立即回应了第一次请求的
ACK,这种情况下是用第一次的发送时间还是第二次的发送时间来计算 RTT 呢?
怎样的算法都会有一定的偏差,并且还会有算法的额外开销,于是就轮到时间戳 timestamp 登场了
时间戳的使用分为以下几步:
a 向 b 发送报文,头部存放了时间戳
ta1b 接收到报文后,向 a 回复发送报文,头部存放了 a 的
ta1和 b 自身当前的时间戳a 接收到 b 的回复,根据报文头部中的
ta1和自身当前的时间戳ta2来计算RTT = ta2 - ta1
这样计算出来的 RTT 是准确无误的
防止序列号冲突
前面说过,序列号溢出后会归 0 重新计数
假设有一个序列号为 0 ~ 1 的数据,因为网络波动卡在返回路上
等待了很久后序列号已经开始了第二轮计数,并且成功接收第二轮序列号为 0 ~ 1 的数据,就在这时前一轮 0 ~ 1 的数据终于回来了
这个情况下就有两个序列号相同的数据包,如何区分这两个数据包?
答案是时间戳,因为同一个序列号的请求,发送时间必然不同
TFO (TCP 快速打开)
是三次握手的一些优化,我们平时进行 http 请求,服务端需要在三次握手后才会响应
而 TFO 可以在第二次握手时就响应
具体实现:
在首次建立连接的第二次握手时,服务器还会额外在响应报文中添加一个
Fast Open数据,里面存放的是经过计算得到的SYN Cookie,客户端拿到后会缓存下来后续客户端进行 http 请求时,会在第一次握手时带上
SYN Cookie和 HTTP 请求,服务端在校验SYN Cookie合法后会在响应(第二次握手)的同时返回 HTTP 请求的响应





