TCP 详解


TCP 详解

参考:https://blog.csdn.net/sinat_36629696/article/details/80740678

https://www.jianshu.com/p/ef892323e68f

https://mp.weixin.qq.com/s/LUtk6u_zv0w8g8GIGWEuCw

TCP协议

TCP协议全称: 传输控制协议,顾名思义,就是要对数据的传输进行一定的控制.

先来看看它的报头

img

每部分的含义和说明

  • 源端口和目的端口

    各占16位,端口是传输层与应用层的服务接口,传输层的复用和分用功能都要通过端口才能实现。

  • 序号

    占 32位,TCP 连接中传送的数据流中的每一个字节都编上一个序号(seq),序号字段的值则指的是本报文段所发送的数据的第一个字节的序号。 Seq 就是 Sequence Number 即序号,它是用来解决乱序问题的

  • 确认号

    占 32位,是期望收到对方的下一个报文段的数据的第一个字节的序号(ACK)。 ACK 就是 Acknowledgement Numer 即确认号,它是用来解决丢包情况的,告诉发送方这个包我收到啦。

  • 数据偏移/首部长度

    占 4位,它指出 TCP 报文段的数据起始处距离 TCP 报文段的起始处有多远,“数据偏移”的单位是 32 位字

  • 保留

    占 6 位,保留为今后使用,但目前应置为 0,六位分别是URG,ACK,PSH,RST,SYN,FIN。

    URG: 标识紧急指针是否有效 ,表示此报文段中有紧急数据,应尽快传送(相当于高优先级的数据)
    ACK: 标识确认序号是否有效,表示只有当 ACK有效时确认号字段才有效
    PSH: 提示接收端应用程序读取tcp缓冲区数据,表示接收应用进程应尽快地读取缓存区数据,而不再等到整个缓存都填满了后再读取
    RST: 请求重新建立连接. 我们把含有RST标识的报文称为复位报文段,表示 TCP 连接中出现严重差错,必须释放连接,然后再重新建立运输连接
    SYN: 请求建立连接. 我们把含有SYN标识的报文称为同步报文段,表示这是一个连接请求或连接接受报文
    FIN: 请求释放连接. 我们把含有FIN标识的报文称为结束报文段,表示此报文段的发送端的数据已发送完毕,并要求释放运输连接

  • 检验和

    占 16为,检验和字段检验的范围包括首部和数据这两部分,由发送端填充,,检验形式有CRC校验等。 如果接收端校验不通过,则认为数据有问题。

  • 紧急指针

    占 16 位, 用来标识哪部分数据是紧急数据,指出在本报文段中紧急数据共有多少个字节(紧急数据放在本报文段数据的最前面)

特点

TCP 是面向连接的传输层协议 每一条 TCP 连接只能有两个端点(endpoint),每一条 TCP 连接只能是点对点的(一对一) TCP 提供可靠交付的服务 TCP 提供全双工通信 面向字节流

连接管理机制

正常情况下, tcp需要经过三次握手建立连接,四次挥手断开连接

三次握手

第一次:
客户端 - - > 服务器 此时服务器知道了客户端要建立连接了
第二次:
客户端 < - - 服务器 此时客户端知道服务器收到连接请求了
第三次:
客户端 - - > 服务器 此时服务器知道客户端收到了自己的回应

到这里,就可以认为客户端与服务器已经建立了连接.

(图2)

三次握手的详细过程

开始,客户端和服务器都处于 CLOSE 状态.

此时,客户端向服务器主动发出连接请求,服务器被动接受连接请求.

1、TCP服务器进程先创建传输控制块TCB,时刻准备接受客户端进程的连接请求,此时服务器就进入了 LISTEN(监听)状态

2、TCP客户端进程也是先创建传输控制块TCB,然后向服务器发出连接请求报文,此时报文首部中的同步标志位SYN=1,同时选择一个初始序列号 seq = x,此时,TCP客户端进程进入了 SYN-SENT(同步已发送状态)状态。TCP规定,SYN报文段(SYN=1的报文段)不能携带数据,但需要消耗掉一个序号

3、TCP服务器收到请求报文后,如果同意连接,则发出确认报文。确认报文中的 ACK=1,SYN=1,确认序号是 x+1,同时也要为自己初始化一个序列号 seq = y,此时,TCP服务器进程进入了SYN-RCVD(同步收到)状态。这个报文也不能携带数据,但是同样要消耗一个序号

4、TCP客户端进程收到确认后还,要向服务器给出确认。确认报文的ACK=1,确认序号是 y+1,自己的序列号是 x+1.

5、此时,TCP连接建立,客户端进入ESTABLISHED(已建立连接)状态。当服务器收到客户端的确认后也进入ESTABLISHED状态,此后双方就可以开始通信了。

初始序列号 ISN 的取值

RFC793 中认为 ISN 要和一个假的时钟绑定在一起ISN 每四微秒加一,当超过 2 的 32 次方之后又从 0 开始,要四个半小时左右发生 ISN 回绕

SYN 超时了怎么处理

在 Linux 中就是默认重试 5 次,并且就是阶梯性的重试,间隔就是1s、2s、4s、8s、16s,再第五次发出之后还得等 32s 才能知道这次重试的结果,所以说总共等63s 才能断开连接。

SYN Flood 攻击

SYN Flood 攻击: SYN 超时需要耗费服务端 63s 的时间断开连接,也就说 63s 内服务端需要保持这个资源,所以不法分子就可以构造出大量的 client 向 server 发 SYN 但就是不回 server。 使得 server 的 SYN 队列耗尽,无法处理正常的建连请求。

可以开启 tcp_syncookies,那就用不到 SYN 队列了。

SYN 队列满了之后 TCP 根据自己的 ip、端口、然后对方的 ip、端口,对方 SYN 的序号,时间戳等一波操作生成一个特殊的序号(即 cookie)发回去,如果对方是正常的 client 会把这个序号发回来,然后 server 根据这个序号建连。

或者调整 tcp_synack_retries 减少重试的次数,设置 tcp_max_syn_backlog 增加 SYN 队列数,设置 tcp_abort_on_overflow SYN 队列满了直接拒绝连接。

四次挥手

(图3)

四次挥手的详细过程

数据传输完毕后,双方都可以释放连接.

此时客户端和服务器都是处于ESTABLISHED状态,然后客户端主动断开连接,服务器被动断开连接.

1、客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。

2、服务器收到连接释放报文,发出确认报文,ACK=1,确认序号为 u+1,并且带上自己的序列号seq=v,此时服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。

3、客户端收到服务器的确认请求后,此时客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最终数据)

4、服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,确认序号为v+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。

5、客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,确认序号为w+1,而自己的序列号是u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。

6、服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。

为什么有等待 2*MSL的时间呢

MSL 是 Maximum Segment Lifetime,即报文最长生存时间,RFC 793 定义的 MSL 时间是 2 分钟,Linux 实际实现是 30s,那么 2MSL 是一分钟。

  • 就是怕被动关闭方没有收到最后的 ACK,如果被动方由于网络原因没有到,那么它会再次发送 FIN, 此时如果主动关闭方已经 CLOSED 那就傻了,因此等一会儿。
  • 假设立马断开连接,但是又重用了这个连接,就是五元组完全一致,并且序号还在合适的范围内,虽然概率很低但理论上也有可能,那么新的连接会被已关闭连接链路上的一些残留数据干扰,因此给予一定的时间来处理一些残留数据。

等待 2MSL 会产生什么问题?

如果服务器主动关闭大量的连接,那么会出现大量的资源占用,需要等到 2MSL 才会释放资源。

如果是客户端主动关闭大量的连接,那么在 2MSL 里面那些端口都是被占用的,端口只有 65535 个,如果端口耗尽了就无法发起送的连接了,不过我觉得这个概率很低,这么多端口你这是要建立多少个连接?

确认应答机制(ACK机制)

这里写图片描述

TCP将每个字节的数据都进行了编号,即为序列号.

每一个ACK都带有对应的确认序列号,意思是告诉发送者,我已经收到了哪些数据; 下一次你要从哪里开始发.
比如,客户端向服务器发送了1005字节的数据,服务器返回给客户端的确认序号是1003,那么说明服务器只收到了1-1002的数据.1003,1004,1005都没收到.
此时客户端就会从1003开始重发.

超时重传机制

这里写图片描述

这种情况下,主机B会收到很多重复数据.

那么TCP协议需要识别出哪些包是重复的,并且把重复的丢弃.

这时候利用前面提到的序列号,就可以很容易做到去重.

超时时间如何确定?

最理想的情况下,找到一个最小的时间,保证 “确认应答一定能在这个时间内返回”.

但是这个时间的长短,随着网络环境的不同,是有差异的.

如果超时时间设的太长,会影响整体的重传效率; 如果超时时间设的太短,有可能会频繁发送重复的包.

TCP为了保证任何环境下都能保持较高性能的通信,因此会动态计算这个最大超时时间.

这个最大超时时间就叫 RTT,即 Round Trip Time,然后根据这个时间制定超时重传的时间 RTO,即 Retransmission Timeout。

Linux中(BSD Unix和Windows也是如此),超时以500ms为一个单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍.

如果重发一次之后,仍然得不到应答,等待 2500ms 后再进行重传. 如果仍然得不到应答,等待 4500ms 进行重传.

依次类推,以指数形式递增. 累计到一定的重传次数,TCP认为网络异常或者对端主机出现异常,强制关闭连接.

为什么还需要快速重传机制?

超时重传是按时间来驱动的,如果是网络状况真的不好的情况,超时重传没问题,但是如果网络状况好的时候,只是恰巧丢包了,那等这么长时间就没必要。

于是又引入了数据驱动的重传叫快速重传,什么意思呢?就是发送方如果连续三次收到对方相同的确认号,那么马上重传数据。

因为连续收到三次相同 ACK 证明当前网络状况是 ok 的,那么确认是丢包了,于是立马重发,没必要等这么久。

image

看起来好像挺完美的,但是你有没有想过我发送1、2、3、4这4个包,就 2 对方没收到,1、3、4都收到了,然后不管是超时重传还是快速重传反正对方就回 ACK 2。

这时候要重传 2、3、4 呢还是就 2 呢?

SACK 的引入是为了解决什么问题?

SACK 即 Selective Acknowledgment,它的引入就是为了解决发送方不知道该重传哪些数据的问题。

我们来看一下下面的图就知道了。

图片

SACK 就是接收方会回传它已经接受到的数据,这样发送方就知道哪一些数据对方已经收到了,所以就可以选择性的发送丢失的数据。

滑动窗口

刚才我们讨论了确认应答机制,对每一个发送的数据段,都要给一个ACK确认应答. 收到ACK后再发送下一个数据段.

这样做有一个比较大的缺点,就是性能较差. 尤其是数据往返时间较长的时候.

那么我们可不可以一次发送多个数据段呢?

这里写图片描述

一个概念: 窗口

窗口大小指的是无需等待确认应答就可以继续发送数据的最大值.

上图的窗口大小就是4000个字节 (四个段).

发送前四个段的时候,不需要等待任何ACK,直接发送

收到第一个ACK确认应答后,窗口向后移动,继续发送第五六七八段的数据…

因为这个窗口不断向后滑动,所以叫做滑动窗口.

操作系统内核为了维护这个滑动窗口,需要开辟发送缓冲区来记录当前还有哪些数据没有应答

只有ACK确认应答过的数据,才能从缓冲区删掉.

这里写图片描述

如果出现了丢包,那么该如何进行重传呢?

1、数据包已经收到,但确认应答ACK丢了.

这种情况下,部分ACK丢失并无大碍,因为还可以通过后续的ACK来确认对方已经收到了哪些数据包.

2、数据包丢失

可以通过高速重发控制控制

流量控制

接收端处理数据的速度是有限的. 如果发送端发的太快,导致接收端的缓冲区被填满,这个时候如果发送端继续发送,就会造成丢包,进而引起丢包重传等一系列连锁反应.

因此TCP支持根据接收端的处理能力,来决定发送端的发送速度.

这个机制就叫做 流量控制(Flow Control)

接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 “窗口大小” 字段,

通过ACK通知发送端;

窗口大小越大,说明网络的吞吐量越高;

接收端一旦发现自己的缓冲区快满了,就会将窗口大小设置成一个更小的值通知给发送端;

发送端接受到这个窗口大小的通知之后,就会减慢自己的发送速度;

如果接收端缓冲区满了,就会将窗口置为0;

这时发送方不再发送数据,但是需要定期发送一个窗口探测数据段,让接收端把窗口大小再告诉发送端.

这里写图片描述

拥塞控制

虽然TCP有了滑动窗口这个大杀器,能够高效可靠地发送大量数据.

但是如果在刚开始就发送大量的数据,仍然可能引发一些问题.

因为网络上有很多计算机,可能当前的网络状态已经比较拥堵.

在不清楚当前网络状态的情况下,贸然发送大量数据,很有可能雪上加霜.

因此,TCP引入 慢启动 机制,先发少量的数据,探探路,摸清当前的网络拥堵状态以后,再决定按照多大的速度传输数据.

这里写图片描述

在此引入一个概念 拥塞窗口

发送开始的时候,定义拥塞窗口大小为1;

每次收到一个ACK应答,拥塞窗口加1;

每次发送数据包的时候,将拥塞窗口和接收端主机反馈的窗口大小做比较,取较小的值作为实际发送的窗口
像上面这样的拥塞窗口增长速度,是指数级别的.

“慢启动” 只是指初使时慢,但是增长速度非常快.

为了不增长得那么快,此处引入一个名词叫做慢启动的阈值,当拥塞窗口的大小超过这个阈值的时候,不再按照指数方式增长,而是按照线性方式增长.

image

  • 当TCP开始启动的时候,慢启动阈值等于窗口最大值
  • 在每次超时重发的时候,慢启动阈值会变成原来的一半,同时拥塞窗口置回1

少量的丢包,我们仅仅是触发超时重传;

大量的丢包,我们就认为是网络拥塞;

当TCP通信开始后,网络吞吐量会逐渐上升;

随着网络发生拥堵,吞吐量会立刻下降.

拥塞控制,归根结底是TCP协议想尽可能快的把数据传输给对方,但是又要避免给网络造成太大压力的折中方案.

延迟应答

如果接收数据的主机立刻返回ACK应答,这时候返回的窗口可能比较小.

假设接收端缓冲区为1M. 一次收到了500K的数据;

如果立刻应答,返回的窗口大小就是500K;

但实际上可能处理端处理的速度很快,10ms之内就把500K数据从缓冲区消费掉了; 在这种情况下,接收端处理还远没有达到自己的极限,即使窗口再放大一些,也能处理过来;

如果接收端稍微等一会儿再应答,比如等待200ms再应答,那么这个时候返回的窗口大小就是1M

窗口越大,网络吞吐量就越大,传输效率就越高.
TCP的目标是在保证网络不拥堵的情况下尽量提高传输效率;

那么所有的数据包都可以延迟应答么?

肯定也不是

有两个限制

数量限制: 每隔N个包就应答一次

时间限制: 超过最大延迟时间就应答一次

具体的数量N和最大延迟时间,依操作系统不同也有差异

一般 N 取2,最大延迟时间取200ms

粘包问题

首先要明确,粘包问题中的 “包”,是指应用层的数据包.

在TCP的协议头中,没有如同UDP一样的 “报文长度” 字段

但是有一个序号字段.

站在传输层的角度,TCP是一个一个报文传过来的. 按照序号排好序放在缓冲区中.

站在应用层的角度,看到的只是一串连续的字节数据.

那么应用程序看到了这一连串的字节数据,就不知道从哪个部分开始到哪个部分是一个完整的应用层数据包.

此时数据之间就没有了边界,就产生了粘包问题

那么如何避免粘包问题呢?

归根结底就是一句话,明确两个包之间的边界

对于定长的包

  • 保证每次都按固定大小读取即可
    例如上面的Request结构,是固定大小的,那么就从缓冲区从头开始按sizeof(Request)依次读取即可

对于变长的包

  • 可以在数据包的头部,约定一个数据包总长度的字段,从而就知道了包的结束位置
    还可以在包和包之间使用明确的分隔符来作为边界(应用层协议,是程序员自己来定的,只要保证分隔符不和正文冲突即可)

对于UDP协议来说,是否也存在 “粘包问题” 呢?

对于UDP,如果还没有向上层交付数据,UDP的报文长度仍然存在.

同时,UDP是一个一个把数据交付给应用层的,就有很明确的数据边界.

站在应用层的角度,使用UDP的时候,要么收到完整的UDP报文,要么不收.不会出现收到 “半个” 的情况.


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM