基本认识
tcp 是面向连接的、可靠的、基于字节流的传输层通信协议。
-
面向连接:一定是「一对一」才能连接,不能像 udp 协议可以一个主机同时向多个主机发送消息,也就是一对多是无法做到的;
-
可靠的:无论的网络链路中出现了怎样的链路变化,tcp 都可以保证一个报文一定能够到达接收端;
-
字节流:用户消息通过 tcp 协议传输时,消息可能会被操作系统「分组」成多个的 tcp 报文,如果接收方的程序如果不知道「消息的边界」,是无法读出一个有效的用户消息的。
并且 tcp 报文是「有序的」,当「前一个」tcp 报文没有收到的时候,即使它先收到了后面的 tcp 报文,那么也不能扔给应用层去处理。
同时对「重复」的 tcp 报文会自动丢弃。
头格式
序列号(sqe):在建立连接时由计算机生成的随机数作为其初始值,通过 syn 包传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。用来解决网络包乱序问题。
初始序列号(isn),在 tcp 建立连接的时候,客户端和服务端都会各自生成一个初始序列号,它是基于时钟生成的一个随机数,来保证每个连接都拥有不同的初始序列号。初始化序列号可被视为一个 32 位的计数器,该计数器的数值每 4 微秒加 1,循环一次需要 4.55 小时。
确认应答号:指下一次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。用来解决丢包的问题。
控制位:
- ack:该位为
1
时,「确认应答」的字段变为有效,tcp 规定除了最初建立连接时的syn
包之外该位必须设置为1
。 - rst:该位为
1
时,表示 tcp 连接中出现异常必须强制断开连接。 - syn:该位为
1
时,表示希望建立连接,并在其「序列号」的字段进行序列号初始值的设定。 - fin:该位为
1
时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换fin
位为 1 的 tcp 段。
tcp 协议作用
ip
层是「不可靠」的,它不保证网络包的交付、不保证网络包的按序交付、也不保证网络包中的数据的完整性。
如果需要保障网络数据包的可靠性,那么就需要由上层(传输层)的 tcp
协议来负责。
因为 tcp 是一个工作在传输层的可靠数据传输的服务,它能确保接收端接收的网络包是无损坏、无间隔、非冗余和按序的。
tcp 连接
tcp 连接用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括socket、序列号和窗口大小称为连接。
tcp 四元组可以唯一的确定一个连接,四元组包括如下:
- 源地址
- 源端口
- 目的地址
- 目的端口
源地址和目的地址的字段(32位)是在 ip 头部中,作用是通过 ip 协议发送报文给对方主机。
源端口和目的端口的字段(16位)是在 tcp 头部中,作用是告诉 tcp 协议应该把报文发给哪个进程。
三次握手
tcp/ip 协议是传输层的一个面向连接的安全可靠的一个传输协议。三次握手的机制是为了保证能建立一个安全可靠的连接。
那么第一次握手是由客户端发起,客户端会向服务端发送一个报文。在报文里面:syn标志位置为1,表示发起新的连接。
当服务端收到这个报文之后就知道客户端要和我建立一个新的连接,于是服务端就向客户端发送一个确认消息包。在这个消息包里面:ack标志位置为1,表示确认客户端发起的第一次连接请求。
以上两次握手之后,对于客户端而言:已经明确了我既能给服务端成功发消息,也能成功收到服务端的响应。
但是对于服务端而言:两次握手是不够的,因为到目前为止,服务端只知道一件事,客户端发给我的消息我能收到,但是我响应给客户端的消息,客户端能不能收到我是不知道的。所以,还需要进行第三次握手。
第三次握手就是当客户端收到服务端发送的确认响应报文之后,还要继续去给服务端进行回应,也是一个ack标志位置1的确认消息。
通过以上三次连接,不管是客户端还是服务端,都知道我既能给对方发送消息,也能收到对方的响应。那么,这个连接就被安全的建立了。
为什么tcp三次握手中前两次不能携带数据?
第三次握手的时候,是可以携带数据的。
但是,第一次、第二次握手 不可以携带数据。
根本原因:
如果前两次握手能够携带数据,那么一旦有人想攻击服务器,那么他只需要在第一次握手中的 syn 报文中放大量数据,那么服务器势必会消耗更多的时间和内存空间去处理这些数据,增大了服务器被攻击的风险。
第三次握手的时候,客户端已经处于established状态,并且已经能够确认服务器的接收、发送能力正常,这个时候相对安全了,可以携带数据。
为什么是三次握手?不是两次、四次?
- 因为三次握手才能保证双方具有接收和发送的能力。
- 三次握手才可以阻止重复历史连接的初始化(主要原因)
- 三次握手才可以同步双方的初始序列号
- 三次握手才可以避免资源浪费
不使用「两次握手」和「四次握手」的原因:
- 「两次握手」:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号;
- 「四次握手」:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。
三次握手是如何阻止历史连接的?
我们考虑一个场景,客户端先发送了 syn(seq = 90) 报文,然后客户端宕机了,而且这个 syn 报文还被网络阻塞了,服务端并没有收到,接着客户端重启后,又重新向服务端建立连接,发送了 syn(seq = 100) 报文(注意不是重传 syn,重传的 syn 的序列号是一样的)。
在网络拥堵情况下:
- 一个「旧 syn 报文」比「最新的 syn 」 报文早到达了服务端;
- 那么此时服务端就会回一个
syn ack
报文给客户端; - 客户端收到后可以根据自身的上下文,判断这是一个历史连接(序列号过期或超时),那么客户端就会发送
rst
报文给服务端,表示中止这一次连接。
为什么两次握手连接无法阻止历史连接?
若是两次握手连接,服务器在收到 syn报文后就立刻进入到了连接状态。
这就以为着 被动接收方可以给对方发送数据。但如果 客户端发送 syn 时现了 网络阻塞。
客户端判断出 此次是 历史连接,那么就会 发送 rst 报文来断开连接,所以服务器此前发送的数据也就白白浪费了。
为什么每次建立 tcp 连接时,初始化的序列号都要求不一样呢?
主要原因有两个方面:
- 为了防止历史报文被下一个相同四元组的连接接收(主要方面);
- 为了安全性,防止黑客伪造的相同序列号的 tcp 报文被对方接收;
四次挥手
-
客户端 打算关闭连接,此时就会发送一个 tcp 首部
fin
标志位被设置为 1 的报文,之后客户端进入fin_wait_1
状态。 -
服务端收到报文后,就向客户端 发送ack 应答报文,服务端进入
closed_wait
状态 -
客户端收到
ack
应答报文后,之后进入fin_wait_2
状态 -
服务端 处理完数据后,向客户端发送
fin
报文,之后服务端进入last_ask
状态 -
客户端收到服务端的
fin
报文后,回一个ack
应答报文,之后进入time_wait
状态 -
服务器收到了
ack
应答报文后,就进入了closed
状态,至此服务端已经完成连接的关闭。 -
客户端在经过
2msl
一段时间后,自动进入closed
状态,至此客户端也完成连接的关闭。
为什么挥手需要四次?
-
关闭连接时,客户端向服务端 发送fin,仅仅表明客户端不再发送数据了,但是还能接收数据,在服务端响应发送 ack 应答报文之后,服务端可能还有数据要处理和发送,需要等服务端不再发送数据时,才发送
fin
报文给客户端表示同意断开连接。 -
正因为服务端通常需要等待完成数据的发送和出来了,所以服务端的
ack
和fin
一般都会分开发送,从而比三次握手多了一次
为什么需要 time_wait 状态?
主动发起关闭连接的一方,才会有 time-wait
状态。
需要 time-wait 状态,主要是两个原因:
- 防止历史连接中的数据,被后面相同四元组的连接错误的接收
- 防止失效的历史连接请求报文出现在下次连接当中
- 保证「被动关闭连接」的一方,能被正确的关闭;
- 等待足够的时间以确保最后的 ack 能让被动关闭方接收,
- 帮助其正常关闭。
time_wait 过多有什么危害?
第一是内存资源占用
第二是对端口资源的占用,一个 tcp 连接⾄少消耗⼀个本地端口
tcp报文中时间戳的作用?
计算往返时延
防止序列号回绕
重传机制
重传机制一般是用来解决数据包在错综复杂的网络中丢失的情况。
常见的重传机制:
- 超时重传
- 快速重传
- sack
- d-sack
超时重传
重传机制的其中一个方式,就是在发送数据时,设定一个定时器。
当超过指定的时间后,没有收到对方的 ack
确认应答报文,就会重发该数据,也就是我们常说的超时重传。
tcp 会在以下两种情况发生超时重传:
- 数据包丢失
- 确认应答丢失
超时时间应该设置为多少呢?
超时重传时间 rto 的值应该略大于报文往返 rtt 的值
rtt
指的是数据发送时刻到接收到确认的时刻的差值,也就是包的往返时间。
超时重传时间是以 rto
(retransmission timeout 超时重传时间)表示。
如果超时重发的数据,再次超时的时候,又需要重传的时候,tcp 的策略是超时间隔加倍。
也就是每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送。
快速重传
tcp 还有另外一种快速重传(fast retransmit)机制,它不以时间为驱动,而是以数据驱动重传。
快速重传机制只解决了一个问题,就是超时时间的问题,但是它依然面临着另外一个问题。就是重传的时候,是重传之前的一个,还是重传所有的问题。
为了解决不知道该重传哪些 tcp 报文,于是就有 sack
方法。
sack 方法
还有一种实现重传机制的方式叫:sack
( selective acknowledgment 选择性确认)。
这种方式需要在 tcp 头部「选项」字段里加一个 sack
的东西,它可以将缓存的地图发送给发送方,这样发送方就可以知道哪些数据收到了,哪些数据没收到,知道了这些信息,就可以只重传丢失的数据。
如下图,发送方收到了三次同样的 ack 确认报文,于是就会触发快速重发机制,通过 sack
信息发现只有 200~299
这段数据丢失,则重发时,就只选择了这个 tcp 段进行重复。
duplicate sack
duplicate sack 又称 d-sack
,其主要使用了 sack 来告诉「发送方」有哪些数据被重复接收了。
滑动窗口
滑动窗口是为了提升通信效率在操作系统开辟的一个缓存空间,发送方主机在等到确认应答返回之前,必须在缓冲区中保留已发送的数据。如果按期收到确认应答,此时数据就可以从缓存区清除。
tcp 利用滑动窗口实现流量控制。流量控制是为了控制发送方发送速率,保证接收方来得及接收。
tcp会话的双方都各自维护一个发送窗口和一个接收窗口。接收窗口大小取决于应用、系统、硬件的限制。发送窗口则取决于对端通告的接收窗口。
接收方发送的确认报文中的window字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将接收方的确认报文window字段设置为 0,则发送方不能发送数据。
tcp头包含window字段,16bit位,它代表的是窗口的字节容量,最大为65535。这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。接收窗口的大小是约等于发送窗口的大小。
窗口大小
窗口大小由哪一方决定?
tcp 头里有一个字段叫 window
,也就是窗口大小。
这个字段是接收方告诉发送端子机还有多少缓冲区可以接收数据的。
发送端会根据反馈回来的信息发送数据,这样就可以保证不会出现接收端处理不过来的情况。
这其实就是流量控制。
流量控制是tcp 提供的一种可以让「发送方」根据「接收方」的实际接收能力控制发送的数据量的一种机制。
接收窗口
- #1 是已发送并收到 ack确认的数据:1~31 字节
- #2 是已发送但未收到 ack确认的数据:32~45 字节
- #3 是未发送但总大小在接收方处理范围内(接收方还有空间):46~51字节
- #4 是未发送但总大小超过接收方处理范围(接收方没有空间):52字节以后
程序是如何表示发送方的四个部分的呢?
tcp 滑动窗口方案使用三个指针来跟踪在四个传输类别中的每一个类别中的字节。
其中两个指针是绝对指针(指特定的序列号),一个是相对指针(需要做偏移)。
snd.wnd
:表示发送窗口的大小(大小是由接收方指定的);snd.una
(send unacknoleged):是一个绝对指针,它指向的是已发送但未收到确认的第一个字节的序列号,也就是 #2 的第一个字节。snd.nxt
:也是一个绝对指针,它指向未发送但可发送范围的第一个字节的序列号,也就是 #3 的第一个字节。- 指向 #4 的第一个字节是个相对指针,它需要
snd.una
指针加上snd.wnd
大小的偏移量,就可以指向 #4 的第一个字节了。
发送窗口
- #1 #2 是已成功接收并确认的数据(等待应用进程读取);
- #3 是未收到数据但可以接收的数据;
- #4 未收到数据并不可以接收的数据;
其中三个接收部分,使用两个指针进行划分:
rcv.wnd
:表示接收窗口的大小,它会通告给发送方。rcv.nxt
:是一个指针,它指向期望从发送方发送来的下一个数据字节的序列号,也就是 #3 的第一个字节。- 指向 #4 的第一个字节是个相对指针,它需要
rcv.nxt
指针加上rcv.wnd
大小的偏移量,就可以指向 #4 的第一个字节了。
接收窗口和发送窗口的大小是相等的吗?
并不是完全相等,接收窗口的大小是约等于发送窗口的大小的。
因为接收窗口的大小是由 tcp报文中的 windows 字段来告诉发送方的,而这个传输过程是存在实验的,所以接收窗口和发送窗口是约等于的关系。
拥塞控制
为什么要有拥塞控制呀,不是有流量控制了吗?
流量控制确实避免了「发送方」的数据填满「接收方」的缓存,但是机制并不知道网络的中发生了什么。
网络中对资源需求超过了资源可用量的情况就叫做拥塞。
如果网络出现拥塞,分组将会丢失,此时发送方会继续重传,从而导致网络拥塞程度更高。因此当出现拥塞时,应当控制发送方的速率。这一点和流量控制很像,但是出发点不同。
流量控制是为了让接收方能来得及接收,而拥塞控制是为了降低整个网络的拥塞程度。
拥塞控制主要是四个算法:
- 慢启动
- 拥塞避免
- 拥塞发生
- 快速恢复
拥塞窗口:为了调节发送方所要发送数据的量而引入的概念
发送窗口 swnd
和接收窗口 rwnd
是约等于的关系
加入了拥塞窗口的概念后,此时发送窗口的值是swnd = min(cwnd, rwnd),
拥塞窗口 cwnd
变化的规则:
- 只要网络中没有出现拥塞,
cwnd
就会增大; - 但网络中出现了拥塞,
cwnd
就减少;
慢启动
机制含义:tcp 在刚建立连接完成后,缓慢提高发送数据包的数量
算法机制:当发送方每收到一个 ack,拥塞窗口 cwnd 的大小就会加 1。
有一个叫慢启动门限 ssthresh
(slow start threshold)状态变量。
- 当
cwnd
<ssthresh
时,使用慢启动算法。 - 当
cwnd
>=ssthresh
时,就会使用「拥塞避免算法」。
拥塞避免
算法机制:每当收到一个 ack 时,cwnd 增加 1/cwnd。
如图,拥塞避免算法就是将原本慢启动算法的指数增长变成了线性增长,还是增长阶段,但是增长速度缓慢了一些。
就这么一直增长着后,网络就会慢慢进入了拥塞的状况了,于是就会出现丢包现象,这时就需要对丢失的数据包进行重传。
当触发了重传机制,也就进入了「拥塞发生算法」。
拥塞发生
tcp拥塞控制默认认为网络丢包是由于网络拥塞导致的,,所以一般的tcp拥塞控制算法以丢包为网络进入拥塞状态的信号。
对于丢包有两种判定方式:
- 超时重传
- 快速重传
超时重传:是tcp协议保证数据可靠性的一个重要机制,其原理是在发送一个数据以后就开启一个计时器,在一定时间内如果没有得到发送数据报的ack报文,那么就重新发送数据,直到发送成功为止。
当 rto
超时后,tcp会重传 数据包并做出以下反映。
- 将慢启动的阈值设置为 当前 cwnd 的一半
- cwnd 重置为一
- 进入慢启动过程
快速重传:
当发送端接收到3个以上的重复ack,tcp就意识到数据发生丢失,需要快速重传。
快速重传后不会使用慢启动算法,而是直接使用拥塞避免算法。所以也叫快速恢复算法fast recovery。
- cwnd大小缩小为当前的一半
- 将慢启动的阈值设置为为缩小后的cwnd大小
- 然后进入快速恢复算法fast recovery。
为何快速重传是选择3次ack?
两次duplicated ack时很可能是乱序造成的!三次duplicated ack时很可能是丢包造成的!四次duplicated ack更更更可能是丢包造成的,但是这样的响应策略太慢。丢包肯定会造成三次duplicated ack!综上是选择收到三个重复确认时窗口减半效果最好,这是实践经验。
快速恢复
快速重传和快速恢复算法一般同时使用,快速恢复算法是认为,你还能收到 3 个重复 ack 说明网络也不那么糟糕,所以没有必要像 rto
超时那么强烈。
然后,进入快速恢复算法如下:
- 拥塞窗口
cwnd = ssthresh 3
( 3 的意思是确认有 3 个数据包被收到了); - 重传丢失的数据包;
- 如果再收到重复的 ack,那么 cwnd 增加 1;
- 如果收到新数据的 ack 后,把 cwnd 设置为第一步中的 ssthresh 的值,原因是该 ack 确认了新的数据,说明从 duplicated ack 时的数据都已收到,该恢复过程已经结束,可以回到恢复之前的状态了,也即再次进入拥塞避免状态;