【TCP/IP】TCP协议详解

版权声明:本文为博主原创文章,允许转载,但希望标注转载来源。 https://blog.csdn.net/qq_38410730/article/details/81707212

TCP是如何确保可靠传输的?

  • 为了保证可靠传输,TCP比UDP多了很多控制协议和算法;
  • 连接管理——3次握手和4次握手;
  • 数据破坏——通过校验和;
  • 丢包——应答与超时重发机制;
  • 分片乱序——序列号;
  • 窗口滑动——提高发送效率,对发送端和接收端流量进行控制;
  • 加快通信速度——快速重发,三次收到重发消息进行重发;
  • 流控制——避免网络流量浪费;
  • 拥塞控制——慢启动算法,拥塞窗口。

TCP中的确认应答机制

在TCP中当发送端的数据达到接受主机时,接受主机端都会返回一个消息,告诉对方我已经收到了。这个消息叫确认应答。发送确认应答时,TCP首部中的ACK标志位设1。

TCP中的确认应答通过序列号和确认应答号来实现。如下图所示,主机A发送第一个包时序列号为1,数据长度为1000,那么主机B收到包后发送一个数据包给主机A,在该包中将TCP首部中的32位确认号设为1001。相当于告诉对方1001之前的我都收到了,下次从1001开始发。 

经受时延的确认应答:为了降低确认应答包的数量,TCP提出了经受时延的确认应答。接受端在收到数据后并不立即发送一个应答数据包,而是等待一段时间,如果有新的数据被接受就更新应答号,如果有其他数据要发送就坐上该数据包的顺风车。在系统的内核中维持了一个定时器,一般是200ms如果定时器溢出,即使没有其他数据到达,也发送该应答数据包。

具体的流程如下图:

Nagle算法: TCP是基于流的传输协议,在Rlogin和Telnet传输中会出现只有一个字节数据的TCP数据包。而一个TCP数据包的首部加上IP首部就有40个字节(即ip头20字节+tcp头20字节),很显然发这样的数据包划不来。为了减少这样的数据包,有人提出了Nagle算法。

Nagle算法主要是避免发送小的数据包,要求TCP连接上最多只能有一个未被确认的小分组,在该分组的确认到达之前不能发送其他的小分组。相反,TCP收集这些少量的小分组,并在确认到来时以一个分组的方式发出去。其中,小分组的定义是小于MSS(最大报文段长度)的任何分组。

也就是说,如果发送端欲多次发送多个小包的数据包,则发送端会先将第一个小包发送出去,而将后面到达的少量字符数据都缓存起来而不立即发送,直到收到接收端对前一个数据包报文段的ACK确认、或当前字符属于紧急数据,或者积攒到了一定数量的数据(比如缓存的字符数据已经达到数据包报文段的最大长度)等多种情况才将其组成一个较大的数据包发送出去。具体的流程如下图:

上左图是未开启Nagle算法的情况,此时客户端应用层下传的数据包被立即发送到网络上,而不管该数据包的大小如何,因此在网络里就有可能同时存在该连接的多个小包;而如上右图所示上,在未收到服务器对第一个包的ACK确认之前,客户端应用层下传的数据包被缓存了起来,当收到ACK确认之后才发送出去,这样不仅总包数由原来的3个变为2个,网络负载降低,与此同时,客户端和服务器都只需处理两个包,消耗的CPU等资源也减少了。

该算法的优越之处在于它是自适应的,确认到达的越快,数据也就发送的越快;而在希望减少微小分组数目的低速广域网上,则会发送更少的分组。

Nagle算法是从发送端角度考虑减少了数据包(小包)的个数,时延应答从接收端角度考虑减少了数据包(小包)的个数。

当Nagle算法遇上延迟ACK:

仍然举个例子来讲,如果发送端暂有一段数据要发送给接收端,这段数据的长度不到最大两个包,也就是说,根据Nagle算法,发送端发出去第一个数据包后,剩下的数据不足以组成一个可立即发送的数据包(即剩余数据长度没有大于等于MSS),因此发送端就会等待,直到收到接收端对第一个数据包的ACK确认或者应用层传下更多需要发送的数据等(这里暂只考虑第一个条件,即收到ACK)。

而在接收端,由于ACK延迟确认机制的作用,它不会立即发送ACK,而是等待,直到收到发送端的第二个大数据包、等待超时(比如,200毫秒)两种情况之一。当然,如果本身有反向数据包要发送,那么可以携带ACK,但是在最糟的情况下,最终的结果就是发送端的第二个数据包需要等待200毫秒才能被发送到网络上。

而在像HTTP这样的应用里,某一时刻的数据基本是单向的,所以出现最糟情况的概率非常的大,而且第二个数据包往往用于标识这一个请求或响应的成功结束,如果请求和响应都要超时等待的话,那么时延就得增大400毫秒。

所以为了避免这种情况,有时候就需要关闭Nagle算法:

Linux提供了TCP_NODELAY的选项来禁用Nagle算法。

参考文章:TCP_NODELAY详解

TCP的连接与断开(3次握手和4次握手)

三次握手

TCP建立连接时要传输三个数据包,俗称三次握手(Three-way Handshaking)。可以形象的比喻为下面的对话:

  • 套接字A:“你好,套接字B,我这里有数据要传送给你,建立连接吧。”
  • 套接字B:“好的,我这边已准备就绪。”
  • 套接字A:“谢谢你受理我的请求。

使用 connect() 建立连接时,客户端和服务器端会相互发送三个数据包,请看下图: 

客户端调用 socket() 函数创建套接字后,因为没有建立连接,所以套接字处于CLOSED状态;服务器端调用 listen() 函数后,套接字进入LISTEN状态,开始监听客户端请求。 

这个时候,客户端开始发起请求: 

当客户端调用 connect() 函数后,TCP协议会组建一个数据包,并设置 SYN 标志位,表示该数据包是用来建立同步连接的。同时生成一个随机数字 1000,填充“序号(Seq)”字段,表示该数据包的序号。完成这些工作,开始向服务器端发送数据包,客户端就进入了SYN-SEND状态;

服务器端收到数据包,检测到已经设置了 SYN 标志位,就知道这是客户端发来的建立连接的“请求包”。服务器端也会组建一个数据包,并设置 SYN 和 ACK 标志位,SYN 表示该数据包用来建立连接,ACK 用来确认收到了刚才客户端发送的数据包;服务器生成一个随机数 2000,填充“序号(Seq)”字段。2000 和客户端数据包没有关系;服务器将客户端数据包序号(1000)加1,得到1001,并用这个数字填充“确认号(Ack)”字段;服务器将数据包发出,进入SYN-RECV状态;

客户端收到数据包,检测到已经设置了 SYN 和 ACK 标志位,就知道这是服务器发来的“确认包”。客户端会检测“确认号(Ack)”字段,看它的值是否为 1000+1,如果是就说明连接建立成功。接下来,客户端会继续组建数据包,并设置 ACK 标志位,表示客户端正确接收了服务器发来的“确认包”。同时,将刚才服务器发来的数据包序号(2000)加1,得到 2001,并用这个数字来填充“确认号(Ack)”字段;客户端将数据包发出,进入ESTABLISED状态,表示连接已经成功建立;

服务器端收到数据包,检测到已经设置了 ACK 标志位,就知道这是客户端发来的“确认包”。服务器会检测“确认号(Ack)”字段,看它的值是否为 2000+1,如果是就说明连接建立成功,服务器进入ESTABLISED状态。

至此,客户端和服务器都进入了ESTABLISED状态,连接建立成功,接下来就可以收发数据了。

四次分手

建立连接非常重要,它是数据正确传输的前提;断开连接同样重要,它让计算机释放不再使用的资源。如果连接不能正常断开,不仅会造成数据传输错误,还会导致套接字不能关闭,持续占用资源,如果并发量高,服务器压力堪忧。

建立连接需要三次握手,断开连接需要四次握手,可以形象的比喻为下面的对话:

  • 套接字A:“任务处理完毕,我希望断开连接。”
  • 套接字B:“哦,是吗?请稍等,我准备一下。”
  • 等待片刻后……
  • 套接字B:“我准备好了,可以断开连接了。”
  • 套接字A:“好的,谢谢合作。”

下图演示了客户端主动断开连接的场景: 

建立连接后,客户端和服务器都处于ESTABLISED状态。这时,客户端发起断开连接的请求: 

客户端调用 close() 函数后,向服务器发送 FIN 数据包,进入FIN_WAIT_1状态。FIN 是 Finish 的缩写,表示完成任务需要断开连接;

服务器收到数据包后,检测到设置了 FIN 标志位,知道要断开连接,于是向客户端发送“确认包”,进入CLOSE_WAIT状态。注意:服务器收到请求后并不是立即断开连接,而是先向客户端发送“确认包”,告诉它我知道了,我需要准备一下才能断开连接;

客户端收到“确认包”后进入FIN_WAIT_2状态,等待服务器准备完毕后再次发送数据包;

等待片刻后,服务器准备完毕,可以断开连接,于是再主动向客户端发送 FIN 包,告诉它我准备好了,断开连接吧。然后进入LAST_ACK状态;

客户端收到服务器的 FIN 包后,再向服务器发送 ACK 包,告诉它你断开连接吧。然后进入TIME_WAIT状态;

服务器收到客户端的 ACK 包后,就断开连接,关闭套接字,进入CLOSED状态。 

关于 TIME_WAIT 状态(又称2MSL状态)的说明 :

客户端最后一次发送 ACK包后进入 TIME_WAIT 状态,而不是直接进入 CLOSED 状态关闭连接,这是为什么呢?

TCP 是面向连接的传输方式,必须保证数据能够正确到达目标机器,不能丢失或出错,而网络是不稳定的,随时可能会毁坏数据,所以机器A每次向机器B发送数据包后,都要求机器B”确认“,回传ACK包,告诉机器A我收到了,这样机器A才能知道数据传送成功了。如果机器B没有回传ACK包,机器A会重新发送,直到机器B回传ACK包。

客户端最后一次向服务器回传ACK包时,有可能会因为网络问题导致服务器收不到,服务器会再次发送 FIN 包,如果这时客户端完全关闭了连接,那么服务器无论如何也收不到ACK包了,所以客户端需要等待片刻、确认对方收到ACK包后才能进入CLOSED状态。那么,要等待多久呢?

数据包在网络中是有生存时间的,超过这个时间还未到达目标主机就会被丢弃,并通知源主机。这称为报文最大生存时间(MSL,Maximum Segment Lifetime)。TIME_WAIT 要等待 2MSL 才会进入 CLOSED 状态。ACK 包到达服务器需要 MSL 时间,服务器重传 FIN 包也需要 MSL 时间,2MSL 是数据包往返的最大时间,如果 2MSL 后还未收到服务器重传的 FIN 包,就说明服务器已经收到了 ACK 包。

TCP状态迁移图

其中:CLOSING状态是同时打开才发生的。

为什么是3次和4次

为什么是3次

可能你会认为第3次好像是多余的。是因为信道是不可靠的,可能存在延时或者丢包,而三次是满足可靠传输的最小次数。

举例说明:如果只有两次,假设主机A发送的第一个请求包延时,主机A在等待一段时间后重新发送一个请求包,完成数据连接并断开。但是这个时候上次的发的请求包才到达主机B,这时主机B认为是又一次连接,因此发送一个请求包给A,但是A并没有发送新的请求因此会丢失该数据包。最后,B就一直等待A发送数据,浪费了资源。

除此之外,我个人认为3次握手更加安全,加大了攻击的难度。如果只有两次,一个发送一个应答,那么攻击着可以采用IP欺骗,发动SYN洪水攻击,并且服务端还都是ESTABLISHED状态。如何防御?难度更大了。对于三次握手的可以限制半连接的数量来达到一个防御的作用。

为什么是4次

TCP通信是一种全双工的通信,可以进行半关闭(与半打开区别:半打开是连接后的客户端和服务端有一端异常关闭了),所谓半关闭是指可以只关闭从A到B的方向,而B到A的方向还可以继续传输。因此,在客户端和服务器端分别进行关闭。

超时和重传

TCP是可靠的传输协议,意味着必须按序,无差错的传送数据和目的端。通过校验和,确认应答,重传来保证。校验和应答已经介绍过,这里主要讲解重传机制。重传分为两种:超时重传和快速重传。 

超时重传(RTO) 

当一个包被发送后,就开启一个定时器,如果定时时间到了,还未收到能确认该发送包的应答包,就重传一份数据。注意收到的应答包可能是该包也可能是后面包的,但是只要能确认该包被收到就行。另外如果,是因为网络延时造成重传,则接受端收到重复数据包后丢弃该包。 

快速重传 

当如果发送端收到一个包的三次应答包后,立即重传,比超时重传更高效。

 当没有收到1001-2000的数据内容时,主机会将2001-3000之后的三段数据存放在接收缓冲区中,只不过不去确认而已。

TCP窗口

窗口是TCP中为了解决应答机制等待时间过长而引入的方法,如果没有窗口,则TCP每发送一次数据就必须等待应答,收到应答后继续发送,如果没有收到则等待一段时间后重发,如果很长时间都无法收到应答则判断为网络断开。而使用窗口后,窗口的大小指无需等待应答可以连续发送多个数据包。

下图中窗口的大小为4000,一次性发送了4个数据包,每个数据包大小为1000:

TCP窗口在每个传输方向都有两个窗口,发送端窗口和接受端窗口,又因为TCP是全双工通信,因此有四个窗口。

发送端窗口

接收端窗口

发送端窗口大小 = 已经发送但是未接到回应的部分 + 可用窗口(接收端允许发送但是没有发送的那部分) 。

接收端端口大小 = 已经接收并但是还没有回复ACK + 还没有被接收的数据(空位)= 被分配缓存区 - 已接收并确认的数据但是还没有被上层的应用程序接收(缓存的部分)。

参考文章:TCP-IP详解:滑动窗口(Sliding Window)

窗口滑动

发送端窗口,根据应答包的应答号(ACK)确定窗口的位置,根据应答包中窗口的大小(WIN)确定窗口的大小,窗口是从左向右逐渐滑动。

  • 窗口合拢:由左端边缘向右靠近,称为窗口合拢,在接受到数据后发生;
  • 窗口张开:右右端向右移动,称为窗口打开,在处理完数据后发生。

如果接收端发送的应答包中窗口大小为0,则客户端会等待一段时间后发送探测包,重新确认窗口的大小。接受端如果处理完了数据也会重新发送应答包,通知发送端。防止死锁的发生。

引入窗口后,TCP的应答包如果部分丢失,无需重传,由后面的应答包保证。TCP为了提高效率,采用延时再确认应答,和选择性确认应答,即收到数据包后不立即发送应答包,而是等待收到下一个或多个包后发一个应答。

也就是说,所谓滑动窗口协议:

  • “窗口”对应的是一段可以被发送者发送的字节序列、可以被接收者接收的字节序列,其连续的范围称之为“窗口”;
  • “滑动”则是指这段“允许发送、接收的范围”是可以随着发送的过程而变化的,方式就是按顺序“滑动”。

注意一下滑动:就是接收端可以根据自己的状况通告窗口大小(根据应答包的应答号(ACK)确定窗口的位置,根据应答包中窗口的大小(WIN)确定窗口的大小),从而控制发送端的接收,进行流量控制。

所谓流量控制,主要是接收方传递信息给发送方,使其不要发送数据太快,是一种端到端的控制。主要的方式就是返回的ACK中会包含自己的接收窗口的大小,并且利用大小来控制发送方的数据发送。

拥塞控制

流量是根据发送方和接受方的缓冲区大小来确定的,保证数据处理不出现拥堵,具体实现方法是窗口滑动。而拥塞机制是考虑到网络中的拥堵,比如路由器要处理的数据过多,导致缓冲区溢出而丢包。而拥塞处理就是来解决这种情况,避免网络出现过载的现象。

  • 判定拥塞出现的条件:网络中出现分组丢失(发生超时或收到重复确认)。
  • 拥塞避免算法中用到了慢启动,慢开始、拥塞控制。 

一切的基础还是慢开始,这种方法的思路是这样的:

  • 首先需要维持两个变量:拥塞窗口和慢启动阀值。
  • 发送方维持一个叫做“拥塞窗口”的变量,该变量和接收端口共同决定了发送者的发送窗口;
  • 当主机开始发送数据时,避免一下子将大量字节注入到网络,造成或者增加拥塞,选择发送窗口大小为1的试探报文;
  • 当收到数据的确认后,就发送窗口大小为2的报文;
  • 若再次收到确认,则发送窗口大小为4的报文,依次递增2的指数级;
  • 最后会达到一个提前预设的“慢启动阀值”,比如16,即窗口大小为16,此时遵循下面的条件判定:
  1. cwnd < ssthresh, 继续使用慢开始算法;
  2. cwnd > ssthresh,停止使用慢开始算法,改用拥塞避免算法;
  3. cwnd = ssthresh,既可以使用慢开始算法,也可以使用拥塞避免算法。

所谓拥塞避免算法就是:收到一个应答后,就把发送方的拥塞窗口+1,即让拥塞窗口缓慢地增大,按照线性规律增长;

当出现网络拥塞,比如丢包时,将慢启动阀值设为当前窗口大小的一半,然后将cwnd设为1,执行慢开始算法(较低的起点,指数级增长)。

 上述方法的目的是在拥塞发生时循序减少主机发送到网络中的分组数,使得发生拥塞的路由器有足够的时间把队列中积压的分组处理完毕。慢开始和拥塞控制算法常常作为一个整体使用,一个是低起点高增长,一个是高起点低增长。

持续定时器

TCP不对ACK应答报文进行确认,如果接受端缓冲被占满,发送一个窗口为0的应答,过了一段时间数据处理完毕,重新发送一个应答,告诉发送端窗口大小。不幸的是,如果这个包丢了,就会进入死锁状态——发送端等待更新窗口的应答包,接收端等待接收数据。

为了避免死锁发生,TCP使用了一个持续定时器来周期性地向接收方查询,以便发现窗口是否已经增大。这一过程也被称为窗口探查。

猜你喜欢

转载自blog.csdn.net/qq_38410730/article/details/81707212